An approval gate for project instructions.
Every time a Claude Code session starts in a project with a CLAUDE.md or .claude/rules/*, those files are loaded as system instructions — they shape Claude's behavior for the entire session. A malicious commit, a compromised dependency that drops one in, or even an accidental edit can re-program Claude silently.
affirm makes that trust explicit. At session start it lists the project's instruction files and marks each as affirmed, new, or changed. If anything is new or changed, you review the file and run /affirm to record its hash. Until you do, the warning persists on every session start.
- SessionStart hook computes SHA-256 of
<cwd>/CLAUDE.md, every file under<cwd>/.claude/rules/, and any files they@import(followed two levels deep; out-of-tree imports are hashed and flagged), compares to hashes stored in~/.claude/affirm-hashes.json, and emits asystemMessagebanner. New/changed files carry their last-modified age and git info inline so you can judge a change at a glance. The banner is the only output — instruction content is never injected into Claude's context. The hook also dedupes re-fires persession_id(markers at~/.claude/state/affirm-firstfire/, pruned after 7 days) so Claude Code's multi-fire lifecycle (startup, resume, /clear, /compact) doesn't surface the banner ten times in one session. /affirmskill wraps a small CLI that records, shows, or revokes hashes for the current cwd.
Affirm: instruction files in this project:
✓ CLAUDE.md
✓ ~/.claude/shared.md ← @from CLAUDE.md (out-of-tree)
✦ .claude/rules/style.md [NEW — unaffirmed]
modified 3d ago
✧ .claude/rules/security.md ← @from CLAUDE.md [CHANGED — unaffirmed]
modified 12m ago · Alice, 2026-06-20T09:12:00-05:00 (uncommitted)
⚠ Review unaffirmed files, then run /affirm.
ℹ 1 @import beyond depth 2 not tracked: docs/a.md → docs/b.md
| Marker | Meaning |
|---|---|
✓ |
Hash matches the affirmed value — trusted. |
✦ |
No record of this file — never affirmed. |
✧ |
Hash differs from the affirmed value — content changed. |
← @from <file> marks a file pulled in by another's @import; (out-of-tree) marks one that lives outside the project. New/changed files get a second line with their modified age and git info — that's where it helps you judge whether a change is yours or a surprise. When everything is affirmed the banner shows only ✓ lines and no warning. A trailing ℹ line summarizes any @imports deeper than two levels, which are reported but not hashed.
Read-only. Shows each instruction file in the current cwd with its affirmation status, modification time, and git info (last commit author + date, and whether there are uncommitted local changes). @imported files are listed too, annotated with the file that pulled them in, their depth, and an out-of-tree marker when they live outside the project.
Records SHA-256 hashes for every CLAUDE.md / .claude/rules/* file in the current cwd to ~/.claude/affirm-hashes.json. Invoking -a is itself the attestation — there's no separate "are you sure?" prompt.
Removes affirmation records for the current cwd. The next session will surface those files as NEW. Useful for forcing yourself to re-review.
If you don't want to go through the skill, run the CLI from a shell in the project root:
bun run <plugin-root>/lib/cli.ts # show details
bun run <plugin-root>/lib/cli.ts -a # record hashes
bun run <plugin-root>/lib/cli.ts -r # revokeIn scope:
<cwd>/CLAUDE.md- Every file under
<cwd>/.claude/rules/(recursive, symlinks skipped to avoid following malicious links out of the tree) - Files those reach via Claude Code's
@importsyntax, followed two levels deep. Imports are resolved relative to the importing file (with~/and absolute paths supported) and skipped inside code spans/blocks, matching Claude Code. An import that points outside the project is still hashed, just flaggedout-of-tree. Imports deeper than two levels are reported in the banner but not hashed — depth is capped to keep an unbounded graph from quietly pulling in the world.
Out of scope:
~/.claude/CLAUDE.md— user-global instructions. You control your own dotfiles; tracking them here would mostly produce noise. (An@importto a home-dir file from a project file is still caught — that's the project choosing to load it.)- Nested
CLAUDE.mdfiles in subdirectories of the project. Add this if you have a multi-package repo where each package ships its own CLAUDE.md — file an issue.
This is a speed-bump against prompt injection, not a guarantee. It catches:
- A malicious branch merging changes to CLAUDE.md.
- A dependency or scaffolding tool dropping a CLAUDE.md or
.claude/rules/*into your project. - An accidental edit you forgot you made.
- An
@imported file changing content even though CLAUDE.md itself didn't — each imported file is hashed independently.
It does NOT catch:
- Prompt injection arriving via files Claude reads during the session.
- Tools or MCP servers acting maliciously after being trusted.
- Anyone with write access to
~/.claude/affirm-hashes.jsonitself.
Hashes live at ~/.claude/affirm-hashes.json:
{
"/path/to/projectA/CLAUDE.md": "abc4fd38…",
"/path/to/projectB/.claude/rules/style.md": "9f1a2b…"
}Absolute paths so the same project on different machines re-affirms independently. The file is written atomically (temp + rename).
/plugin marketplace add nullphase-net/enfurbish
/plugin install affirm@enfurbish
Once installed:
/affirmappears as a slash command.- The SessionStart hook fires automatically. The first session in any project will surface every instruction file as
NEW— review, then/affirm.
Settings-watcher caveat: if you install the hook into ~/.claude/settings.json while a Claude Code session is already running, the hook won't fire in that session. Start a fresh claude process to pick it up.
- Bun on
PATH— the lib scripts and hook are TypeScript run directly viabun run. - A Claude Code installation that supports plugins.
MIT.