$ cat .claude/settings.local.json▊

Hooks over prose

A rule written in a prompt is a request. A hook is a fact. Four small scripts on Claude Code tool events replaced paragraphs of instructions that four files repeated, and a deny list with an incident behind every line closed the doors the hooks could not.

Andrii Siuta · July 2026 · ~5 min read Claude Code hooks guardrails

01. The incident

Stopping a dev server is easy: pkill -f "ng serve". Except the command an agent runs is itself a shell whose command line contains ng serve, so the pattern matches the agent's own shell and kills the tool call with exit 143. The rules file said not to do it. It happened 181 times in one week.

Now pkill -f and pgrep -f are denied in settings, and the rule says stop a server by port with fuser -k 4200/tcp. It has not happened since. That is the whole argument of this post in one example: an instruction an agent may skip under load is a script's job.

02. Four hooks

EventHookBlocks when
Before any shell commandgit guarda commit or push on main; a commit naming a local-only harness file; a push whose range carries one
After every edit or writePrettiernever blocks; formatting just stops being a review finding
When an implementer agent stopsreport checkits last message lacks Files:, Checks:, Deferred: and Plan edits:
When the spec-writer stopsword captoday's spec exceeds 1,500 words, tables included
the shape of a blocking hook// the tool call arrives as JSON on stdin; exit 2 blocks it
const { tool_input } = JSON.parse(await readStdin());
const cmd = tool_input.command ?? '';
if (/\bgit\b[^|;&]*\b(commit|push)\b/.test(cmd) && currentBranch(dirOf(cmd)) === 'main') {
  process.stderr.write('make the branch first: git switch -c <name> origin/main');
  process.exit(2);
}

Each hook replaced prose. The report check exists because manual checks used to be deferred silently; now the word Deferred: is there every time. The word cap exists because a prose cap nobody checked produced a 16,800-word spec. The git guard replaced four paragraphs that four files repeated, slightly differently.

03. The deny list

Twenty-one denied commands, and each line has a story:

  • git reset --hard: in a checkout shared with other sessions, one moved the wrong branch. Every commit, push or reset now prints the current branch in the same command.
  • Stash pop, drop and clear: the stashes hold uncommitted local tooling, and stash indices shift, so a stash is cited by hash and only applied.
  • rm -rf, git clean, force push: obvious, and still worth writing down.
  • Queueing a pipeline: outward-facing, a human's call.
  • The issue tracker MCP server's six write tools: a shell deny list does not cover MCP tool names, so a denied CLI command had a second door until those were denied too.

04. How to start

  • Grep your rules file for "never" and "always". Each hit is a hook candidate.
  • Start with the git guard. It is the one with the worst failure mode.
  • Make the hook's message an instruction ("make the branch first"), because the agent reads it and tries again.
  • Recheck after a platform upgrade: one matcher we rely on works but is not documented.

If a rule can be a script, it is a script.