Build your own Claude Code
The permission engine·18 min

Protected paths

0 / 1
# grade this stage (from your course directory)
npx theinference test --stage protected-paths --watch

There is a category of file the agent should never quietly overwrite: .git (your history), .claude (its own config and the deny rules), and dotfiles that change how your shell or tools behave (.npmrc, .bashrc, .mcp.json, .ssh). acceptEdits exists to stop nagging you about ordinary source edits — but it must not silently approve a write to .git/config. This step adds a hard floor: edits to protected targets are always gated, no matter how permissive the mode.

What to build

Your task

Gate protected paths in decide

Extend decide in src/permissions.ts with a protected-path floor. Keep a PROTECTED list (.git, .claude, .bashrc, .zshrc, .npmrc, .mcp.json, .gitconfig, .ssh). After the deny/ask checks but before the allow check and the mode default: if the call is a write tool (Write/Edit/Create) whose input.path is or sits inside a protected target, force the decision to 'ask' — unless the mode is bypassPermissions. So a write to .git/config under acceptEdits returns 'ask', while a write to src/app.ts is unaffected. The grading subcommand reuses decide.

tests/adapters.ts
import { decide, type PermissionSettings, type ToolCall, type Decision } from '../src/permissions'

// Same entrypoint as step 03 — the protected-path floor lives inside decide().
export function runProtected(settings: PermissionSettings, call: ToolCall): Decision {
  return decide(settings, call)
}
Order matters: floor sits below deny/ask, above allow

Place the protected check after deny and ask (an explicit deny on a protected path should still read as deny, not be softened to ask) but before the allow rules and the mode default — otherwise a broad Edit(./**) allow rule or acceptEdits would approve .git writes before the floor ever runs. And it only forces ask, never deny: the user can still approve a deliberate .npmrc edit, they just can't have it happen silently.

How it's graded

Process mode runs program.ts protected over a {settings, calls[]} fixture and compares the allow|ask|deny vector to expected.json. Cases assert protected writes resolve to ask even under acceptEdits, that bypassPermissions and an explicit deny still take precedence, and that ordinary in-tree paths are untouched.