feat: skills pinning — supply chain & update drift defense (AST 02 + AST 07)#88
Closed
andreykh89 wants to merge 1 commit intonode9-ai:mainfrom
Closed
feat: skills pinning — supply chain & update drift defense (AST 02 + AST 07)#88andreykh89 wants to merge 1 commit intonode9-ai:mainfrom
andreykh89 wants to merge 1 commit intonode9-ai:mainfrom
Conversation
…AST 07) Extends the MCP tool pinning primitive (v1.5.0 / PR #81) to agent skill repositories. On the first tool call of a session, SHA-256 hashes are recorded for every skill file in known roots (~/.claude/skills/, ~/.claude/CLAUDE.md, ~/.claude/rules/, project .claude/CLAUDE.md, .claude/CLAUDE.local.md, .claude/rules/, .cursor/rules/, AGENTS.md, CLAUDE.md). On subsequent sessions the hook re-verifies; any drift quarantines the session and blocks every tool call until a human reviews via `node9 skill pin update <rootKey>`. One feature, two threats covered in a single primitive — AST 02 Supply Chain Compromise and AST 07 Update Drift — at the skills layer that no competitor currently defends at runtime. Security properties: - Fail-closed on corrupt pin file - Symlink-safe (never follows symlinks out of the tree) - Size-capped at 5000 files / 50 MB per root - Path-traversal-safe session IDs (/^[A-Za-z0-9_-]{1,128}$/) - Atomic writes, mode 0o600 for ~/.node9/skill-pins.json and flags CLI: - `node9 skill pin list` — show pinned roots, hashes, file counts - `node9 skill pin update <rootKey> [--yes]` — diff + re-pin - `node9 skill pin reset` — clear pins AND wipe session flags Config: - `policy.skillRoots: string[]` extends the default root set (absolute, `~/`-prefixed, or cwd-relative; relative paths require absolute cwd). Tests (TDD, all green): - src/__tests__/skill-pin.unit.test.ts (36 tests) - src/__tests__/skill-pin-cli.integration.test.ts (7 tests, spawnSync) - src/__tests__/check-skill-pin.integration.test.ts (8 tests, spawnSync) - src/__tests__/skill-roots-config.spec.ts (4 tests) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Closing and reopening from my personal fork (Xaik89) instead of the work account. Same commit, same diff — see the replacement PR link below. |
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
~/.claude/skills/,~/.claude/CLAUDE.md,~/.claude/rules/, project.claude/CLAUDE.md,.claude/CLAUDE.local.md,.claude/rules/,.cursor/rules/,AGENTS.md,CLAUDE.md. On the first tool call of a session the hook records SHA-256 hashes of every skill file; subsequent sessions re-hash and compare. Any drift quarantines the session and blocks every tool call until a human reviews withnode9 skill pin update <rootKey>.What's new
src/skill-pin.ts— core module mirroringsrc/mcp-pin.ts: SHA-256 content hashing (files or recursive directories, symlink-safe, 5000 files / 50 MB caps), atomic writes to~/.node9/skill-pins.json(mode 0o600), per-rootexistsflag (so "skill appeared" and "skill vanished" both count as drift), batchedverifyAndPinRoots(),computePinDiff().src/cli/commands/skill-pin.ts—node9 skill pin list | update <rootKey> [--yes] | reset.updateshows per-file diff (added/removed/modified) before re-pinning;resetwipes both pins and session flags so no quarantine state can leak.src/cli/commands/check.ts— first call of a session verifies; result memoised in~/.node9/skill-sessions/<session_id>.jsonso the hashing cost is paid once per session, not once per tool call. Session IDs restricted to/^[A-Za-z0-9_-]{1,128}$/to defeat path traversal. Best-effort GC of flags older than 7 days. Corrupt pin file → fail-closed. Unknown errors → fail-open (debug-logged) so a skill-pin bug can never brick Claude Code.policy.skillRoots: string[]config field — user-extensible list of extra skill paths beyond the defaults (absolute,~/-prefixed, or cwd-relative; relative paths require an absolute cwd per CLAUDE.md path-validation rules).Security properties
skill-pins.json— only recovery isnode9 skill pin resetlstat+ explicitisSymbolicLink()check; symlinked roots and child symlinks are treated as missing / skipped, never followed out of the intended treerandomBytes().tmp → renamepattern and owner-only permissions already shipping formcp-pins.jsonTest plan
src/__tests__/skill-pin.unit.test.ts— 36 tests covering hash contract, order-invariance, file/dir add/remove/modify sensitivity, symlink safety, mode 0o600, fail-closed semantics, pin diffsrc/__tests__/skill-pin-cli.integration.test.ts— 7 tests (spawnSyncagainstdist/cli.js) covering list empty/populated/corrupt, update unknown/with-diff, reset full wipesrc/__tests__/check-skill-pin.integration.test.ts— 8 tests (spawnSync) covering first-call-pins, short-circuit, fresh-session verify, drift blocks, quarantine persists, corrupt fails closed, missingsession_idskips, relative cwd scoped to global rootssrc/__tests__/skill-roots-config.spec.ts— 4 tests covering default empty, merge, dedup, defensive filternpm run typecheckcleannpm run lintcleannpm run format:checkcleannpm run buildcleannpm test— 1195 / 1196 pass; the one failure is a pre-existing environmental flake inhud.spec.tsunrelated to this change (fails whenever~/.claude/CLAUDE.mdexists on the test machine; passes cleanly under an isolated HOME)skill pin listshows roots, tamper + new session blocks with JSONdecision: "block"(exit 2) and recovery message names the exactnode9 skill pin update <rootKey>command🤖 Generated with Claude Code