For months, Claude Code has been running in production, handling parallel agents and autonomous sub-tasks. Yet, a profoundly powerful feature within its settings file – "hooks" – often goes unnoticed. These undocumented shell commands execute automatically before or after tool calls, at session start, and at session end. Many developers building AI automation pipelines remain unaware of their existence and potential.
The Hook Types
Claude Code's ~/.claude/settings.json file defines four specific hook points:
- PreToolUse: Executes before any tool call. It can inspect the tool's name and input, and crucially, it has the ability to block the tool call from proceeding.
- PostToolUse: Runs after any tool call completes. In addition to the tool's name and input, it also gains access to the output of the tool call.
- SessionStart: Triggered once when a Claude Code session is initiated.
- SessionEnd: Executed when the Claude Code session concludes.
The configuration structure for these hooks within the settings.json file is as follows:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 /path/to/your/hook.py"
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit",
"hooks": [
{
"type": "command",
"command": "bash /path/to/post-edit.sh"
}
]
}
],
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash /path/to/session-start.sh"
}
]
}
]
}
}The matcher field is a string used to specify which tool a particular hook applies to. For instance, "Bash" targets the Bash tool, while "Edit" applies to the Edit tool. A wildcard ".*" can be used to match all tools.
How Hooks Receive Context
A critical detail often overlooked in documentation is how Claude Code passes context to your hooks. It delivers relevant tool context to your hook script via stdin as a JSON object. For a PreToolUse hook, the incoming payload structure will be:
{
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/old-builds",
"description": "Clean old build artifacts"
}
}When a PostToolUse hook is triggered, the JSON payload expands to include the tool's response:
{
"tool_name": "Edit",
"tool_input": {
"file_path": "/src/app/api/route.ts",
"old_string": "...",
"new_string": "..."
},
"tool_response": "The file has been updated successfully."
}Your hook script is expected to read this JSON from stdin, perform its designated operations, and then exit. The exit code determines the outcome: an exit code of 0 signifies approval (allowing the tool call to proceed for PreToolUse hooks), while any non-zero exit code will block the tool call for PreToolUse hooks. For PostToolUse hooks, the exit code does not block further execution but merely logs the outcome of the hook script.
Real Example 1: Blocking Destructive Commands
One practical application involves preventing autonomous agents from executing potentially dangerous commands like git reset --hard or rm -rf some-dir. By implementing a PreToolUse hook, a human checkpoint can be enforced before such destructive actions are carried out, ensuring greater control and safety in automated workflows.