Create Hook
Hooks are small programs the harness runs at lifecycle events. Keep them simple, lightweight, and working on macOS, Linux, and Windows alike.
.claude/hooks/block-env-edit.mjs
const chunks = [];
for await (const c of process.stdin) chunks.push(c);
const input = JSON.parse(Buffer.concat(chunks).toString());
const file = input.tool_input?.file_path ?? "";
const name = file.split(/[\\/]/).pop() ?? ""; // handles / and \
if (name === ".env" || name.startsWith(".env.")) {
console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: `${name} is blocked by hook; edit it manually.`,
},
}));
}
// no output, exit 0: no opinion — normal permission flow applies
.claude/settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node",
"args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/block-env-edit.mjs"]
}
]
}
]
}
}
Exec form + Node stdlib: identical behavior on all three platforms, no shell quoting, no external dependencies.
1---2name: create-hook3description: Use when writing, editing, or debugging a Claude Code hook — hook scripts, settings.json hook configuration, matchers, or hook JSON output. Do not use for authoring skills or slash commands, git hooks, or MCP servers.4license: MIT5---67# Create Hook89Hooks are small programs the harness runs at lifecycle events. Keep them simple, lightweight, and working on macOS, Linux, and Windows alike.1011<workflow>121. Pick the narrowest event and matcher that observes what you need — full event table in `references/hooks-reference.md`.132. Write the handler following <principles>, modeled on <example>.143. Wire it into settings at the right scope (user `~/.claude/settings.json` vs project `.claude/settings.json`), then verify BOTH cases: the event fires and acts, and unrelated activity passes through untouched.15</workflow>1617<principles>18- **Simple**: one script, one decision. A hook that wants config files and helpers wants to be a skill or CLI instead.19- **Lightweight**: hooks run often and block the loop. Do the check, exit. No network calls unless `async: true`; stay well inside the event's timeout.20- **Cross-platform by default**: exec form (`"command": "node", "args": [...]`) so no shell ever parses the command; script logic in Node or Python stdlib — no `jq`, `grep`, or bash-isms. Split paths on both `/` and `\`. Only write shell-form one-liners when a single platform is the explicit target.21- **Paths**: always `${CLAUDE_PROJECT_DIR}` (or `${CLAUDE_PLUGIN_ROOT}` inside plugins), never relative — cwd moves during sessions.22- **Current output schema**: block via `hookSpecificOutput` decisions or exit code 2. `{"continue": false}`-style fields are outdated and silently ignored — a hook that "works" but never blocks usually has this bug. Exit 0 with no output means no opinion.23- **Choose your failure mode**: a crashing hook (exit 1) does NOT block. If the hook is a guardrail, route errors to exit 2 (fail closed); if it's a convenience, let it fail open.24</principles>2526<example>27Block edits to `.env` files, everywhere:2829`.claude/hooks/block-env-edit.mjs`30```js31const chunks = [];32for await (const c of process.stdin) chunks.push(c);33const input = JSON.parse(Buffer.concat(chunks).toString());3435const file = input.tool_input?.file_path ?? "";36const name = file.split(/[\\/]/).pop() ?? ""; // handles / and \3738if (name === ".env" || name.startsWith(".env.")) {39 console.log(JSON.stringify({40 hookSpecificOutput: {41 hookEventName: "PreToolUse",42 permissionDecision: "deny",43 permissionDecisionReason: `${name} is blocked by hook; edit it manually.`,44 },45 }));46}47// no output, exit 0: no opinion — normal permission flow applies48```4950`.claude/settings.json`51```json52{53 "hooks": {54 "PreToolUse": [55 {56 "matcher": "Edit|Write",57 "hooks": [58 {59 "type": "command",60 "command": "node",61 "args": ["${CLAUDE_PROJECT_DIR}/.claude/hooks/block-env-edit.mjs"]62 }63 ]64 }65 ]66 }67}68```6970Exec form + Node stdlib: identical behavior on all three platforms, no shell quoting, no external dependencies.71</example>7273<references>74- `references/hooks-reference.md` — events, matchers, config shape, stdin fields, exit codes, output schema, placeholders, platform shell behavior. Distilled September 2026 from the canonical docs: https://code.claude.com/docs/en/hooks — recheck there when behavior surprises.75</references>