Claude Code Hooks
Claude Code hooks connect Claude Code session events to automation, such as checking proposed tool calls or formatting edits. Command hooks provide programmatic checks; prompt and agent hooks use model judgment.[1]
This reference follows Anthropic's documentation checked on September 27, 2026. Consult the installed version and release history when maintaining older configurations.[6]
Choose the event
The following is a selection of common events, not an exhaustive catalog.[1]
| Need | Event | Timing |
|---|---|---|
| Inspect a proposed tool call | PreToolUse | Before execution |
| Process a successful tool result | PostToolUse | After execution |
| Handle an approval request | PermissionRequest | When permission is requested |
| Check a response ending | Stop | When Claude finishes responding |
Stop marks a response ending, not necessarily task completion. Check stop_hook_active to avoid loops. Plain claude -p lacks the usual interactive approval prompt; consult PermissionRequest restrictions before relying on that event.[1]
Configuration locations
Settings locations: user ~/.claude/settings.json, shared project .claude/settings.json, private project .claude/settings.local.json. Managed policy takes precedence.[3]
The example below is an entry to merge into a settings file, not a replacement for that file. Keep unrelated configuration intact. Start in a disposable project before adopting a hook across a team.
Decisions and safety boundaries
Command hooks receive event JSON on stdin. PreToolUse exit 2 blocks the call; exit 0 without a decision leaves normal permission checks in place. PermissionRequest requires its JSON decision object and ignores exit 2. A post-tool hook cannot undo execution. Ordinary exit 1 is generally a non-blocking error.[2]
A synchronous PreToolUse command, HTTP, or MCP-tool hook that times out supplies no decision, and normal permission processing continues. An Agent SDK callback timeout instead blocks. Command hooks run with full user privileges.[2]
For the configuration below, Edit|Write matches either tool. Exec form uses command plus args without a shell. ${CLAUDE_PROJECT_DIR} identifies the starting project root. The five-second timeout is a waiting limit, not a fail-closed guarantee.[2]
A Bash sandbox does not automatically isolate hook commands.[2][5]
Example: check sensitive filenames
This original example rejects direct Edit and Write requests with a .git path segment, basename .env, or basename beginning .env.. It deliberately rejects .env.example too. The policy is a demonstration, not a recommended list of every sensitive file.
Save the script as .claude/hooks/check-edit.cjs in a test project with a current Node.js installation:
const fs = require("node:fs");
function block(message) {
process.stderr.write(message + "\n");
process.exit(2);
}
let event;
try {
event = JSON.parse(fs.readFileSync(0, "utf8"));
} catch {
block("Cannot validate this edit: invalid event JSON.");
}
if (!event || event.hook_event_name !== "PreToolUse") {
block("Cannot validate this edit: unexpected event.");
}
if (!["Edit", "Write"].includes(event.tool_name)) {
process.exit(0);
}
const filePath = event.tool_input?.file_path;
if (typeof filePath !== "string" || filePath.length === 0) {
block("Cannot validate this edit: missing file_path.");
}
const segments = filePath.replaceAll("\\", "/").split("/");
const basename = segments.at(-1);
if (
segments.includes(".git") ||
basename === ".env" ||
basename.startsWith(".env.")
) {
block("This filename is excluded from direct Edit and Write calls.");
}
process.exit(0);
Use this settings entry:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/check-edit.cjs"],
"timeout": 5
}
]
}
]
}
}
The script only reads stdin and evaluates strings. It never opens the requested file, writes to that path, or launches another program. That makes fictional paths sufficient for its standalone tests. Its own behavior can be checked without installing a hook.
For a POSIX shell, run:
printf '%s\n' '{"hook_event_name":"PreToolUse","tool_name":"Write","tool_input":{"file_path":"/demo/.env"}}' | node .claude/hooks/check-edit.cjs
echo $?
The expected result is a diagnostic and exit 2. Replacing /demo/.env with /demo/src/main.js produces no output and exit 0.
| Fixture | Expected script result |
|---|---|
/demo/.env.production | Reject |
/demo/.git/config | Reject |
C:\demo\.env | Reject after separator normalization |
/demo/.environment | Pass this check |
Missing file_path for Write | Reject |
| Malformed JSON | Reject |
A Read tool call targeting .env | Pass this check because the script only checks edits |
These expectations describe the script, not complete Claude Code authorization. A standalone pass does not prove that the application loaded the configuration, that no other hook interfered, or that the eventual tool call succeeded. Verify those separately in a disposable session.
The code also has intentional limits. It does not resolve symbolic links or normalize filesystem case. An alias such as /demo/secret-link is not recognized as a sensitive target, and .ENV differs from .env in its string comparison. It does not inspect shell commands or external processes. Extending this small filename filter into a general access-control system would require a different design.
Operational review
Review repository configuration before trusting its executable behavior. Anthropic's security guidance warns about prompt injection, recommends reviewing proposed commands and critical changes, and notes that non-interactive -p operation disables the interactive trust verification. A repository's checked-in automation deserves the same scrutiny as its build scripts.[4]
For a deployment review, record the installed version, configuration source, intended event, expected rejection behavior, and a small set of allowed and rejected fixtures. Add a deliberately broken-script test: observe the permission outcome when the interpreter is unavailable or the program fails. This distinguishes "the validator rejected the request" from "the validator never completed."
Retain test output without collecting real credentials or full private prompts. A diagnostic that states which rule failed is often enough for this example; recording the target file contents would serve no purpose because the script never evaluates them.
For related configuration topics, see Claude Code Permissions and Sandboxing and Claude Code CLI Flags.
References
- ^1 ^2 ^3Anthropic. "Automate actions with hooks." Claude Code documentation. Accessed September 27, 2026.
- ^1 ^2 ^3 ^4Anthropic. "Hooks reference." Claude Code documentation. Accessed September 27, 2026.
- ^Anthropic. "Settings files and precedence." Claude Code documentation. Accessed September 27, 2026.
- ^Anthropic. "Security." Claude Code documentation. Accessed September 27, 2026.
- ^Anthropic. "Configure the sandboxed Bash tool." Claude Code documentation. Accessed September 27, 2026.
- ^Anthropic. "Claude Code changelog." Official release history. Accessed September 27, 2026.
Improve this article
Add missing citations, update stale details, or suggest a clearer explanation. Every suggestion is reviewed for sourcing before it goes live.
v1 · 1,004 words · full history
Fact-checks are independent of edits: a reviewer re-verifies the article against its sources and stamps the date. How we verify
Research and drafting on this wiki are AI-assisted, under named human editorial standards. How AI is used here
Reviewer note: Independent AI-assisted editorial review checked the published text against cited primary documentation and research. Version-specific behavior and study limitations are stated in the article; this is not a guarantee of runtime behavior or factual infallibility.
Cite this page: AI Wiki. "Claude Code Hooks." aiwiki.ai, updated 27 Sept 2026, fact-checked 27 Sept 2026. CC BY 4.0. https://aiwiki.ai/wiki/claude_code_hooks