[Claude Code in Action]
Claude Code Hands-on #3 — Controlling Tool Execution with Hooks
This post is a translation and summary of Anthropic's official course Claude Code in Action. This is the third installment in the Hands-on series.
What Are Hooks?#
Hooks let you run custom commands immediately before or after Claude executes a tool.
Let's start with the normal flow. When a user sends a request, the Claude model generates a response and decides whether it needs to call a tool. Claude Code then executes that tool and returns the result.
Hooks intercept this process, allowing user-defined code to run right before or right after a tool executes.
Two Hook Types#
There are two kinds of hooks.
PreToolUse Hook — runs before a tool executes. It can allow or block the tool call.
PostToolUse Hook — runs after a tool executes. It can't undo what already happened, but it can trigger follow-up actions or send additional feedback back to Claude.
Where to Configure Hooks#
Hooks are defined in Claude's settings files. You can add them in three places.
| File | Scope |
|---|---|
~/.claude/settings.json | Global — applies to all projects |
.claude/settings.json | Project — shared with the team |
.claude/settings.local.json | Project — personal only, not committed |
You can edit the settings file directly, or use the /hooks command inside Claude Code to configure them interactively.
PreToolUse Hook Structure#
A PreToolUse Hook consists of a matcher and a command to run.
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": "node /home/hooks/read_hook.js"
}
]
}
]
Before the Read tool runs, the specified command is invoked. The command receives details about the tool call and decides one of the following:
- Allow the tool call to proceed normally
- Block the tool call and send an error message to Claude
PostToolUse Hook Structure#
A PostToolUse Hook runs after a tool has executed.
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{
"type": "command",
"command": "node /home/hooks/edit_hook.js"
}
]
}
]
The | character is an OR operator — the hook fires whenever any of Write, Edit, or MultiEdit runs.
Since the action has already been taken, blocking is not possible. Instead, you handle follow-up work like formatting the file that was just edited or automatically running tests.
Writing Hook Scripts#
When a hook fires, Claude passes the tool call details as JSON to the script via stdin.
{
"session_id": "2d6a1e4d-6...",
"transcript_path": "/Users/sg/...",
"hook_event_name": "PreToolUse",
"tool_name": "Read",
"tool_input": {
"file_path": "/code/queries/.env"
}
}
The script parses this data, inspects the tool name and input parameters, then makes its decision.
Controlling Flow with Exit Codes#
The script's exit code communicates the outcome back to Claude.
| Exit Code | Meaning |
|---|---|
0 | Success — allow the tool call |
2 | Block — only valid in PreToolUse |
When exiting with code 2, any message written to stderr is sent to Claude as feedback. Claude uses this message to understand why the action was blocked.
Real-World Example — Protecting .env Files#
One of the most practical uses for hooks is blocking access to sensitive files like .env.
Keep in mind that not only the Read tool but also the Grep tool can access file contents, so both need to be monitored.
Step 1 — Configure the Hook#
Add a PreToolUse Hook to .claude/settings.local.json.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Grep",
"hooks": [
{
"type": "command",
"command": "node ./hooks/read_hook.js"
}
]
}
]
}
}
Step 2 — Write the Hook Script#
Create ./hooks/read_hook.js with the following logic.
async function main() {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
const toolArgs = JSON.parse(Buffer.concat(chunks).toString());
const readPath =
toolArgs.tool_input?.file_path || toolArgs.tool_input?.path || "";
if (readPath.includes('.env')) {
console.error("You cannot read the .env file");
process.exit(2);
}
}
main();
The script parses the JSON received from stdin, checks the file path, and if it contains .env, exits with code 2 to block the call.
Step 3 — Test It#
Save the configuration and restart Claude Code. Ask Claude to read a .env file and the hook will intercept the request, returning an error message.
Claude will recognize that the action was blocked by a hook and inform the user accordingly.
Ideas for What You Can Build#
There are many workflows you can implement with hooks.
- Code formatting — automatically run Prettier or Black after editing a file
- Test automation — run related tests automatically whenever a file changes
- Access control — block read/write access to specific files or directories
- Code quality — run a linter or type checker and feed the results back to Claude
- Logging — keep a record of files Claude accessed or modified
- Convention enforcement — verify adherence to naming conventions or coding standards
PreToolUse Hooks control what Claude can do; PostToolUse Hooks augment what Claude has done. Combining the two lets you build sophisticated automation workflows tailored to your development environment.
Summary#
| Concept | Description |
|---|---|
PreToolUse Hook | Intercepts before tool execution — allow or block |
PostToolUse Hook | Intercepts after tool execution — run follow-up actions |
matcher | Specifies which tools to watch (Read, Write, Read|Grep, etc.) |
Exit code 0 | Allow the tool call to proceed |
Exit code 2 | Block the tool call (PreToolUse only) |
stderr output | The block reason message sent back to Claude |
/hooks command | Interactively configure hooks from within Claude Code |
// Related Posts
Claude Code Hands-on #4 — Hooks in Practice and the Claude Code SDK
A practical guide covering Hook security settings, automated TypeScript type-checking, and a duplicate-query prevention Hook — plus a full reference of all Hook types and how to use the Claude Code SDK for programmatic automation.
Claude Code Hands-on #2 — MCP Servers and GitHub Integration
A walkthrough of adding MCP servers to Claude Code for direct browser control, and integrating with GitHub Actions to automate PR reviews and issue handling.
Claude Code Hands-on #1 — Context Management to Custom Commands
Effective Claude Code usage comes down to context management. This post covers the essential features you'll use in real projects: /init, CLAUDE.md, @ file mentions, Planning Mode, Thinking Mode, conversation flow control, and custom commands.