Claude Code · July 11, 2026 · 3 min read
Make Claude Code follow your rules with hooks
CLAUDE.md and skills only suggest behavior. Hooks enforce it at fixed lifecycle points. Copy a hook that blocks .env edits even when permissions are bypassed.
The problem
You put “never touch .env files” in your CLAUDE.md. Claude Code respects it for a week, then one long session it edits the file anyway. That is not a bug. CLAUDE.md, and every skill you write, is text the model reads and usually follows. Usually is not always. Rules that must hold every single time need enforcement, not a polite request, and that is exactly what Claude Code hooks are for.
When does a rule belong in a hook?
A hook is a shell command Claude Code runs automatically at a fixed point in its lifecycle: before a tool call, after an edit, when a session starts. The trigger is guaranteed, which gives you the whole split in four words: skills suggest, hooks enforce.
The test I use: if the rule needs no judgment and must fire every time, it is a hook. If Claude should reason about how to apply it, it belongs in CLAUDE.md or a skill. Run the formatter after every edit: hook. Never touch protected files: hook. “Prefer small components”: CLAUDE.md. “Here is our deploy checklist”: skill.
An instruction is a request. A hook is a guarantee.
The hook to copy
This blocks every edit to any .env file. Paste it into .claude/settings.json in your project (commit it and the whole team gets the guardrail), or into ~/.claude/settings.json to cover all your projects:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path // empty' | grep -q '\\.env' && { echo 'Blocked: .env files are protected' >&2; exit 2; }; exit 0"
}
]
}
]
}
}
Every hook receives the event as JSON on stdin. This one pulls the file path of the pending edit and, if it contains .env, exits with exit code 2: the action is blocked and your stderr message is fed back to Claude so it adjusts instead of retrying. Run /hooks inside Claude Code to confirm it registered.
The formatter version is the same pattern after the edit instead of before it: a PostToolUse hook with the same matcher and the command jq -r '.tool_input.file_path' | xargs npx prettier --write.
When a rule needs a judgment call at a guaranteed moment, there is a middle ground: a hook with "type": "prompt" sends the event to a small Claude model that returns ok or a reason to block. Deterministic trigger, model decision.
Do this now
- Pick the one rule Claude keeps breaking in your project.
- Paste the hook above into
.claude/settings.jsonand point the grep at your protected path. - Run
/hooksto confirm it registered, then ask Claude to edit the file and watch the block land.