Claude Code pre-commit hook
Block Claude from committing if lint or typecheck fails — a zero-config quality gate via settings.json.
Hooks are shell commands the harness runs in response to Claude Code events. They fire deterministically — Claude can't forget to run them. Below is the gate I put on every project.
settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash -lc 'case \"$CLAUDE_TOOL_INPUT\" in *git\\ commit*) npm run lint && npm run type-check ;; esac'"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "say -v Samantha 'Done.'"
}
]
}
]
}
}
What's happening
- PreToolUse on Bash + git commit — before any
git commitruns, lint and typecheck must pass. A non-zero exit blocks the commit; Claude sees the failure and fixes it. - Stop — when Claude finishes a turn, a TTS voice says "Done." so I can switch tabs without watching the spinner. macOS only; swap for
notify-sendon Linux.
Useful events
| Event | Fires when |
|---|---|
| PreToolUse | Before any tool call (matcher narrows by tool name) |
| PostToolUse | After a tool call returns — perfect for autoformatting after Write |
| UserPromptSubmit | When you hit enter — useful for redacting secrets from outgoing prompts |
| Stop | When the model returns end_turn |
| SessionStart | When a new Claude Code session begins — load env, warm caches |
A real PostToolUse autoformat
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash -lc 'echo \"$CLAUDE_TOOL_INPUT\" | jq -r .file_path | xargs -r prettier --write'"
}
]
}
]
}
}
Every Write or Edit call now ends with the file getting prettier'd. Claude never has to remember.
Two rules I follow
- Hooks are not Claude. Anything you express as "Claude should always do X" is actually a hook on the harness — Claude can drift, the runtime can't.
- Hooks must be idempotent and fast. A flaky 10s hook turns every tool call into a 10s wait.