Hooks and deterministic automation
Run your own commands at lifecycle events, block a dangerous tool call with an exit code, and format every edited file without asking the model.
Lifecycle events and the settings shape
Hooks are shell commands, HTTP calls or prompts that run at fixed points in a session. They are deterministic: unlike an instruction in CLAUDE.md, a hook always fires. That makes them the right tool for anything that must not depend on the model's willingness.
| Event | Fires | Typical use |
|---|---|---|
PreToolUse | Before a tool runs; can block it | Refuse writes to protected paths |
PostToolUse | After a tool succeeds | Run a formatter on edited files |
UserPromptSubmit | When you submit a prompt | Inject the current branch or ticket id |
SessionStart | When a session begins | Load environment context |
Stop | When the agent finishes responding | Run the test suite and report |
SubagentStop | When a subagent finishes | Log what a delegated task concluded |
PreCompact | Before history is summarised | Archive the transcript |
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_FILE_PATHS\"",
"timeout": 30
}
]
}
]
}
}- Configuration lives in
.claude/settings.json(shared) or.claude/settings.local.json(yours). matcheris a regular expression over the tool name, soEdit|Writecovers both.- The hook receives a JSON payload on stdin describing the event, including the tool name and its input.
Blocking a call
#!/usr/bin/env bash
# .claude/hooks/guard-secrets.sh
# Refuse any write that touches a secret file or hardcodes a key pattern.
set -euo pipefail
payload=$(cat)
path=$(printf '%s' "$payload" | jq -r '.tool_input.file_path // ""')
content=$(printf '%s' "$payload" | jq -r '.tool_input.content // .tool_input.new_string // ""')
if [[ "$path" == *".env"* || "$path" == secrets/* ]]; then
echo "Blocked: $path is a protected path." >&2
exit 2
fi
if printf '%s' "$content" | grep -Eq 'sk-[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}'; then
echo "Blocked: looks like a hardcoded credential." >&2
exit 2
fi
exit 0{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{ "type": "command", "command": ".claude/hooks/guard-secrets.sh" }
]
}
]
}
}⚠️
Exit code 2 is the blocking signal: stderr is fed back to the model as an explanation, so write the reason there. Any other non-zero exit is reported as a hook failure and the tool call still proceeds - which means a hook that crashes silently approves everything it was meant to stop. Test it by trying to write a file it should reject.
Hook safety and debuggability
- A hook runs with your user's privileges. Treat a shared settings file as executable code: review changes to it like you would review a shell script from a stranger.
- Read the payload from stdin; do not interpolate tool input into a shell string without quoting.
- Keep hooks fast. A hook on
PostToolUseruns on every edit; a five-second formatter on a busy session is five seconds times every file. - Log to a file if you need to know whether a hook fired -
/hooksshows what is configured, not what ran. - Prefer
PostToolUsefor formatting andPreToolUseonly for genuinely blocking checks, to avoid interrupting normal edits.
# verify that stdin really is what you think it is
echo '{"tool_name":"Write","tool_input":{"file_path":".env","content":"A=1"}}' \
| .claude/hooks/guard-secrets.sh ; echo "exit=$?"
# then force the whole run to fail when the guard is wrong
claude -p "append DEBUG=1 to .env" --allowedTools "Edit" ; echo "exit=$?"FAQ
Hooks or CLAUDE.md instructions?
If the requirement is absolute - never write to this directory, always format after editing - use a hook. Instructions are probabilistic; hooks are not. Reserve instructions for judgement calls the model should make, and hooks for rules that must hold every time.
Why does my hook not fire at all?
Check the matcher's exact tool name, that the settings file is in the directory you launched from, and restart the session after editing it. Hooks are read at startup, so a running session will not see a newly added hook.
Related
Permissions and safety with agents Headless mode, scripts and CI automation
Last refreshed 2026-09-18.