← All posts

Claude Code · Agent Infrastructure · Developer Tools

CLAUDE.md is advice. Hooks enforce it.

Claude Code's PreToolUse hooks run before every tool call and can block it entirely — here's why that matters more than any CLAUDE.md instruction.

Ashrey · July 27, 2026 · 6 min read

There is a pattern every Claude Code user eventually hits. You write a clear instruction in CLAUDE.md: "never edit .env directly." The model reads it. The session starts cleanly. Then the conversation gets long, the context fills up, and forty turns in it edits .env anyway. You fix the instruction. The same thing happens next week.

This is not a prompting problem. No wording fixes it. The instruction is advice — the model reads it, weighs it against the task at hand, and sometimes the task wins. That is how language models work. The instruction lives in the context window, and context windows have limits, priorities, and attention gradients that no amount of UPPERCASE or repetition fully overcomes.

Claude Code's hook system solves a different class of problem. A hook does not persuade the model to avoid a file. It runs before the tool call happens and refuses it at the operating system level. The model never writes. It gets told why, and moves on. There is no prompting workaround because the hook is not in the prompt.

The contract

PreToolUse hooks are simple by design. Claude Code pipes a JSON object to your hook script on stdin before executing any tool call. The object contains tool_name, tool_input, and cwd. Your script reads it, decides what to do, and exits with one of three codes:

  • Exit 0: allow the tool call to proceed.
  • Exit 1: report an error, but still allow the call.
  • Exit 2: block the tool call entirely. Anything written to stderr is shown to the model as the reason for the refusal.

That's the full interface. No SDK, no network call, no configuration format to learn. Any script that reads stdin and writes to stderr is a valid hook. Python, Bash, Rust, whatever you have in your PATH. The hook runs synchronously before every matching tool invocation for as long as the session runs, regardless of how long the conversation has been going.

guard.py: a concrete example

This week on Hacker News, a developer posted a 60-line Python hook called guard.py that does exactly one thing: block writes to files listed in a .claude/protected.txt config file. It appeared this morning (July 27) and captures the core pattern cleanly.

The hook intercepts four tool types — Edit, Write, MultiEdit, and NotebookEdit — and anything else passes through untouched. For each blocked write, it exits 2 with a specific error message that includes the file path, the matched pattern, and an explicit instruction to the model: "Do not try another tool or another path to reach this file. Tell the user it is protected and ask them to edit it themselves."

That last part matters. The refusal doesn't just block the write — it tells the model what to do instead. Without that guidance, the model might try a workaround (a shell command, a different tool). With it, the model understands the constraint and reports it cleanly to the user. The hook is doing two things at once: enforcement and communication.

The default protected patterns are what you'd expect: .env, .env.*, *.pem, *.key, credentials.json, secrets.*. The last three defaults are the guard script itself — .claude/guard.py, .claude/protected.txt, and .claude/settings.json. Without those, a sufficiently motivated agent could modify or delete its own constraints mid-session. The hook protects itself.

Install in two minutes: copy guard.py to .claude/guard.py, add the hook entry to .claude/settings.json, list your protected files in .claude/protected.txt, restart Claude Code. No dependencies beyond Python 3.8.

The architectural difference

The distinction between CLAUDE.md instructions and hooks is not one of degree — it is one of kind. An instruction in CLAUDE.md is processed by the language model along with everything else in the context. It influences behavior through the same mechanism that all text influences behavior: weight, attention, position, and the model's training. A hook operates outside that mechanism entirely.

This has a specific implication for security-sensitive operations. The model is not an adversary trying to subvert your instructions — it is a capable, well-intentioned system that sometimes makes mistakes under the pressure of a complex task. When a long-running agent session is trying to complete a multi-file refactor and your .env file is in the path, the model may genuinely believe touching it is necessary. An instruction says "I'd prefer you didn't." A hook says "you cannot."

Hooks are also immune to prompt injection in a way that instructions aren't. A malicious string in a file the agent is reading cannot override a hook — the hook runs at a layer the model has no access to. This matters as agents start reading more untrusted content: third-party APIs, scraped web pages, user-uploaded documents. Any of those could contain text that attempts to redirect the agent's behavior. Hooks don't read that text.

What else hooks let you do

File protection is the most obvious use case, but the hook contract enables a broader class of enforcement patterns.

Auditing. A hook that always exits 0 but logs every tool call to a file gives you a complete record of what the agent did and when. PostToolUse hooks (which run after a tool completes and receive the tool's output) let you log results alongside the calls. Run a session, diff the logs, understand exactly what happened. This is valuable for debugging and for compliance in environments where agent actions need to be auditable.

Cost controls. A hook can count tool calls and exit 2 once you've hit a threshold. If you're running an expensive automated session and want a hard ceiling on how many shell commands or file writes it can make, a stateful hook (writing a counter to a temp file between invocations) can enforce that ceiling without relying on the model to self-limit.

Pattern enforcement. The same approach that protects .env files can enforce structural rules: block writes to files in a migrations/ directory, prevent the agent from touching anything in dist/, refuse Edit calls targeting files the agent didn't create this session. Any rule that can be expressed as a check over a file path or tool input becomes enforceable.

Approval gates. A hook can prompt for human confirmation before allowing high-impact operations. Write a hook that exits 2 unless a human types "yes" in a separate terminal window. This turns every dangerous tool call into a checkpoint without changing anything in your prompts or system configuration.

What stays in CLAUDE.md

None of this means CLAUDE.md is useless. It serves a different function. CLAUDE.md communicates intent, context, and preference. "We keep migrations in /migrations and they're managed by the ORM — don't touch them" is guidance the model needs to do good work. The hook that blocks writes to migrations/** is the backstop for when the guidance doesn't hold.

The right mental model is defense in depth. CLAUDE.md tells the model what you want. Hooks enforce what you require. For most operations, the model's judgment is fine and CLAUDE.md is sufficient. For the handful of operations where "fine" isn't good enough — credentials, secrets, infrastructure configs, the agent's own settings — hooks are the right tool.

Anthropic's recent agent harness design guidance draws a related line: some constraints belong in the model's context, others belong in the scaffolding around it. The more consequential the constraint, the more it should live in scaffolding that the model cannot modify. A hook is the minimal viable scaffolding — a script, a config file, and an exit code.

The implication for agent design

As Claude Code agents run longer sessions, tackle bigger codebases, and get handed more autonomy, the gap between "what the model was told" and "what the model will reliably do" becomes more important to manage. Instructions handle the gap well for the 95% of cases where the model is doing exactly what you'd want a capable developer to do. Hooks handle the 5% where the consequences of a mistake are severe enough that you don't want to rely on probability.

Knowing which operations fall into which category — and wiring up hooks for the latter before you need them — is worth more than any amount of careful CLAUDE.md wording. The session that damages something important is always the long one you didn't expect to go sideways.

If you're building with Claude

Tools and services for agent builders

Audit your MCP server configurations

Hooks stop the call. The CLI tells you what was reachable in the first place. Worth running once while you have the config open.

mcp-doctor on GitHub, MIT
claude-mcp-starter-kit, free
What the lab builds now

The Agent Lab builds live AI quoting engines for home service contractors. Supplier APIs, a labor rate matrix built from the shop's own bids, automated job intake. The same infrastructure work, pointed at an industry that still prices jobs by hand.

Read the engine spec
If you sell to contractors already

Agencies and dev shops white label the engine and put their own name on it. Installs start from 8,000 USD and the partner keeps 30 percent of the install and 30 percent of the monthly. No developer on the payroll.

Review the math