Overview
Hooks let you attach shell commands to events in a Claude Code session. The harness runs them, not Claude, so they fire reliably regardless of model output. Use hooks for anything that must happen unconditionally: format on write, lint on stop, log on every user message. Configuration lives in settings.json. See claude-code for the broader workflow context.
Configure hooks in settings.json
All hook configuration sits under the hooks key in .claude/settings.json (project-level) or ~/.claude/settings.json (user-level).
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\"" }
]
}
],
"Stop": [
{
"hooks": [
{ "type": "command", "command": "npm run lint 2>&1 | tee /tmp/cc-lint.log" }
]
}
]
}
}Project-level settings override user-level for the fields they set. Hooks merge additively: both fire if both define a Stop hook.
PostToolUse hooks run after every matched tool call
PostToolUse hooks receive the tool name and input via environment variables. Use them for side-effects that should follow a specific tool.
CLAUDE_TOOL_NAME: the tool that just ran (Write,Edit,Bash, etc.).CLAUDE_TOOL_INPUT_FILE_PATH: the file path from the tool input, for file tools.
Common uses: auto-format on Write or Edit, update an index file after Write, log every Bash command to an audit file.
The matcher is a regex applied to the tool name. "matcher": "Write|Edit" fires on both. Omit the matcher to fire on every tool call.
PreToolUse hooks run before the tool fires
PreToolUse hooks can block the tool by exiting non-zero. Use them as safety valves.
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "/usr/local/bin/audit-bash \"$CLAUDE_TOOL_INPUT_COMMAND\"" }
]
}If the hook exits non-zero, Claude Code cancels the tool call and surfaces the hook’s stderr as an error message. This lets you enforce allowlists at the harness level rather than trusting model judgment.
Stop hooks run when Claude finishes a turn
A Stop event fires each time the model stops generating, before control returns to the user. Use stop hooks for session hygiene.
- Run the test suite and append the result to
SESSION_LOG.md. - Lint the entire modified file set.
- Push a metrics beacon to a logging endpoint.
Stop hooks run synchronously; a slow hook delays the prompt returning. Keep them under two seconds or run heavy work in the background with &.
UserPromptSubmit hooks run before each user message reaches the model
UserPromptSubmit fires after the user presses enter, before the message is sent. Use this hook to inject context automatically.
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{ "type": "command", "command": "echo \"Current branch: $(git rev-parse --abbrev-ref HEAD)\"" }
]
}
]
}
}The hook’s stdout is prepended to the user message as a system note. Inject git status, environment name, or session metadata here without cluttering the prompt.
Use the auto-format pattern for consistent output
Pairing a PostToolUse hook on Write|Edit with your formatter eliminates the “Claude forgot to format” class of bugs.
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{ "type": "command", "command": "npx prettier --write \"$CLAUDE_TOOL_INPUT_FILE_PATH\" --log-level silent" }
]
}
]This fires on every file write, including writes Claude makes in multi-step plans. The formatter runs on the actual output, not a copy.
Use the lint-on-stop pattern to catch regressions early
A Stop hook that runs lint turns every turn boundary into a regression check.
"Stop": [
{
"hooks": [
{ "type": "command", "command": "npm run lint --silent && echo 'lint: ok' >> .claude/session.log || echo 'lint: FAIL' >> .claude/session.log" }
]
}
]The log entry is cheap. If lint fails, the model’s next turn can read the log and self-correct. Combine this with a UserPromptSubmit hook that prepends the last lint status to every user message.