You can write “never edit files in prisma/migrations” in your CLAUDE.md, and most of the time Claude Code will respect it. Most of the time is the problem. An instruction is a request that competes with everything else in the context window. On a long session, deep in a tricky fix, it can lose.
Hooks are the other half. They are shell commands that Claude Code runs at fixed points in a session, every time, whatever the model is thinking. If something must always or never happen, it should be a hook.
Where hooks live and when they run
Hooks are configured in settings files: .claude/settings.json in the repo (checked in, shared by the team), .claude/settings.local.json for your own overrides, and ~/.claude/settings.json for every project on your machine. The events you will use most:
- SessionStart: when a session begins. Whatever the command prints is added to the context.
- UserPromptSubmit: when you send a prompt, before Claude sees it. Its output is added to the context too.
- PreToolUse: before a tool runs, such as an edit or a shell command. It can block the call.
- PostToolUse: after a tool succeeds. Good for formatting and checks.
- Stop: when Claude is about to finish its turn. It can send Claude back to work.
Each hook receives a JSON description of the event on standard input, such as the tool name and the file path. Exit code 0 means carry on. Exit code 2 means stop: for a PreToolUse hook the tool call is blocked, and whatever you wrote to standard error is shown to Claude so it can adjust. Type /hooks in a session to see what is active.
Here are four hooks worth adding to a team repo.
1. Protect the paths that must not change
Applied migrations, generated files, lockfiles, secrets. Block edits to them, and say why, so Claude can take the right path instead. In .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|MultiEdit|Write",
"hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-paths.sh" }]
}
]
}
}#!/usr/bin/env bash
# .claude/hooks/protect-paths.sh
path=$(jq -r '.tool_input.file_path // empty')
case "$path" in
*/prisma/migrations/*)
echo "Applied migrations are append-only. Create a new migration instead of editing $path." >&2
exit 2 ;;
*.env|*.env.*|*/pnpm-lock.yaml)
echo "$path is managed outside the agent. Ask the user to change it." >&2
exit 2 ;;
esac
exit 0The message matters as much as the block. “Denied” leaves the agent guessing. “Create a new migration instead” gets the job done the right way.
2. Format after every edit
Asking an agent to follow your formatting is a waste of its attention. Let the formatter do it, every time a file is written:
"PostToolUse": [
{
"matcher": "Edit|MultiEdit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs -I{} npx prettier --write --ignore-unknown \"{}\""
}
]
}
]Swap in ruff format, gofmt or whatever your stack uses. Once this hook exists, delete the formatting rules from your CLAUDE.md. They are now enforced, so they no longer need to be read.
3. Brief the session at the start
A SessionStart hook can hand Claude the state of the world before you type anything: the branch, what changed recently, anything a teammate flagged.
#!/usr/bin/env bash
# .claude/hooks/briefing.sh
echo "Branch: $(git branch --show-current)"
echo "Recent commits:"
git log --oneline -5
if [ -f NOTES.md ]; then
echo "Team notes:"
head -n 40 NOTES.md
fiKeep it short. Everything this prints sits in the context for the whole session, so a briefing should be a page, not a dump.
4. Don't let it stop on red tests
“Done!” with a failing test suite is a classic. A Stop hook can run the tests when Claude tries to finish, and send it back with the failures if they don't pass:
#!/usr/bin/env bash
# .claude/hooks/tests-must-pass.sh
input=$(cat)
# Claude is already continuing because of this hook: don't loop forever.
[ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ] && exit 0
# Nothing changed, nothing to test.
git diff --quiet && git diff --cached --quiet && exit 0
if ! out=$(pnpm -s test 2>&1); then
echo "Tests are failing. Fix them before finishing:" >&2
echo "$out" | tail -n 30 >&2
exit 2
fiTwo details make this usable. The stop_hook_active check stops an endless loop when the tests can't be fixed. And only showing the last 30 lines of output keeps the context clean.
Rules for writing hooks
- Keep them fast. A hook on every edit that takes five seconds makes every session slower. Run the full test suite at
Stop, not after each edit. - Decide how each hook fails. A formatter that crashes should let the session continue. A path guard that crashes should probably block.
- Test them with sample input. Pipe a small JSON file into the script before you rely on it.
- Review them like code. Hooks run with your user's permissions. A change to
.claude/settings.jsondeserves the same review as a change to CI.
Instructions and hooks work together
Instructions explain intent and handle judgment calls. Hooks enforce the few things that must never depend on judgment. A good split: if breaking the rule would cause an incident, make it a hook; if it is a preference or a default, write it down.
Memmo itself is installed as four Claude Code hooks: a briefing at session start, relevant memory on each prompt, warnings before an edit to a risky file, and learning when the session ends. Hooks are the reliable way to put the right information in front of the agent at the right moment.