feat: per-skill content hash for change detection + differential autosync#205
Conversation
Add a `hash` field ("sha256:<hex>") to each skill entry so consumers can
detect which skills changed from a single index fetch, without downloading
and hashing every file. The hash is computed over the skill's served files
(path + per-file sha256, sorted) — order-independent and rename-sensitive.
`files` stays a string array; the field is purely additive, so already
deployed sync scripts are unaffected. Includes the design doc documenting
the hash contract.
This is the publishing half; the differential-sync consumer upgrade in
autosync-ic-skills is a follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Skill Validation ReportValidating skill: /home/runner/work/icskills/icskills/skills/autosync-ic-skillsStructure
Frontmatter
Markdown
Tokens
Content Analysis
Contamination Analysis
Result: passed Project Checks |
Replace the blind full-mirror with a differential sync. The script fetches
index.json once, compares each skill's published `hash` against a
{name: hash} manifest (.ic-managed.json), and re-downloads only changed or
new skills — pruning removed ones. Unchanged skills are skipped with no
per-file downloads, and a no-op sync is silent.
Falls back to re-downloading any skill the server publishes no `hash` for,
keeps cached skills on network/jq failure, retains the old hash on a failed
download so the next run retries, and transparently migrates the legacy
bare-array manifest format.
The script is now shipped as an attached file
(scripts/sync-ic-skills.sh) and the installer fetches it via curl for
byte-exact delivery instead of transcribing an inline block. On change it
emits a SessionStart JSON object (systemMessage + additionalContext) so the
summary surfaces in the Claude Code UI and Claude's context.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
marc0olo
left a comment
There was a problem hiding this comment.
The hash mechanism and differential sync logic are solid — good addition. Two things worth addressing before or after merge:
reloadSkills: true missing from JSON output
The script emits a SessionStart JSON object but doesn't include reloadSkills: true. Without it, skills downloaded during the first-ever sync (or when a new skill is added) won't be triggerable until the next session start — even though the script just fetched them. Claude Code re-scans skill directories only before SessionStart hooks run, so anything installed by the hook is invisible to the current session unless you request a re-scan.
reloadSkills: true triggers that re-scan immediately after the hook completes. It doesn't load skill content into context — it just registers newly installed skills so they can be triggered in the same session. The docs even show this exact pattern (sync a skill repo, return reloadSkills: true) as the canonical example.
Fix is a one-liner in sync-ic-skills.sh:
jq -n --arg msg "$summary" '{
systemMessage: $msg,
reloadSkills: true,
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: $msg
}
}'curl-to-fetch vs inline script
Moving the script out of the SKILL.md into a remote fetch avoids transcription errors, which is a real concern for 130-line bash scripts. The tradeoff worth being aware of: an agent following this skill can no longer audit what it's installing before writing it to disk, and setup fails hard if the URL is unavailable at install time (rather than falling back to cached content like the sync itself does).
Not a blocker — the approach works and the URL will be stable — just flagging the tradeoff so it's an informed design decision.
Summary
Adds a per-skill content
hashto the skills discovery index and rewrites theautosync-ic-skillssync script to use it for differential sync — so consumer projects re-download only the skills that actually changed instead of blindly re-mirroring everything every session.Two commits:
Publish
hashin.well-known/skills/index.json— each skill entry gains"hash": "sha256:<hex>", an aggregate over the skill's served files (per-file sha256 + path, sorted). Content-based (not git-based), so it coversreferences//scripts/files and is rename-sensitive. Purely additive:filesstays a string array, so already-deployed consumers are unaffected.Differential sync in
autosync-ic-skills— the script fetchesindex.jsononce, diffs each skill'shashagainst a{name: hash}manifest, and downloads only changed/new skills (pruning removed ones). No-op syncs are silent; on change it emits aSessionStartJSON object (systemMessage+additionalContext) so the summary surfaces in the UI and Claude's context. The script is now shipped as an attached file and fetched viacurlfor byte-exact delivery rather than transcribed from an inline block.Behavior / robustness
hashfor a skill, the script re-downloads it every run (records an empty hash that never matches), so it stays correct against an un-upgraded server.jqfailure; on a partial download keeps the old hash so the next run retries.Testing
getSkillHashverified deterministic across rebuilds and sensitive to subdir-file changes. Sync script exercised against a local server serving the hash-bearing build:Sync script behavior matrix
23 added, hashessha256:f3ee…1 updated0 added, 1 updated, 0 removed (22 unchanged)23 updated, manifest now object23 updated(fallback)npm run buildclean;node scripts/check-project.jspasses (pre-existing missing-eval warnings only).Note
Step 1 of the skill fetches the script from the live site, which won't serve the new
scripts/sync-ic-skills.shpath or thehashfield until this is merged and deployed.🤖 Generated with Claude Code