{"slug": "claude-code-hooks-a-practical-guide-with-examples", "title": "Claude Code hooks: a practical guide with examples", "summary": "Anthropic's Claude Code hooks, documented in the hooks guide and reference as of Claude Code 2.1.280 on September 23, 2026, run commands deterministically at more than thirty session events, and a PreToolUse hook can block a tool call outright by exiting with code 2. Hooks are configured under a `hooks` key in settings files such as `.claude/settings.json`, where matchers like `Edit|Write` pair with commands, and their location determines scope from a single project to an entire organization via managed policy settings. Unlike advisory `CLAUDE.md` instructions, hooks always run, which is why teams convert ignored rules into hooks.", "body_md": "Blog\n\n# Claude Code hooks: a practical guide with examples\n\nPublished: September 23, 2026\n\nClaude Code hooks are commands that run at fixed points in a session: before a tool runs, after a file is edited, when a prompt is submitted, when Claude stops. Unlike a line in `CLAUDE.md`, a hook is not advice. It always runs, and a `PreToolUse` hook can block the action outright by exiting with code 2.\n\nChecked against [the hooks guide](https://code.claude.com/docs/en/hooks-guide) and [reference](https://code.claude.com/docs/en/hooks) on 2026-09-23 (Claude Code 2.1.280). The event list has grown to more than thirty; the handful below are the ones most teams use.\n\n## What are Claude Code hooks?\n\nAnthropic's [best practices](https://code.claude.com/docs/en/best-practices) put it in one line: unlike `CLAUDE.md` instructions, which are advisory, hooks are deterministic and guarantee the action happens. A rule asks the model. A hook does not ask anyone. That is why the answer to “Claude keeps ignoring my rule” is so often “make it a hook”, and why the causes behind the ignoring, covered in [why Claude ignores CLAUDE.md](https://gethrbr.com/blog/why-claude-ignores-claude-md), do not apply to one.\n\n## Which hook events should you know?\n\n| Event | Fires | Typical use | \n|---|---|---|\n| PreToolUse | Before a tool call runs. Can block it | Refuse dangerous commands, protect files | \n| PostToolUse | After a tool call succeeds | Format or lint the file that was just edited | \n| UserPromptSubmit | When you submit a prompt, before Claude sees it | Add context to the prompt, such as branch state | \n| SessionStart | When a session begins, resumes, clears or compacts | Load context the session should start with | \n| Stop | When Claude finishes responding | Run the tests and refuse to stop until they pass | \n| PreCompact / PostCompact | Around context compaction | Re-inject what must survive a summary | \n| SessionEnd | When the session terminates | Write logs, clean up | \n| Notification | When Claude Code sends a notification | Desktop alert when Claude is waiting for you | \n\n## How do you configure a hook?\n\nHooks live in a settings file under a `hooks` key. Each event takes matchers (a tool name or a regex such as `Edit|Write`) and the commands to run. This formats every file Claude edits:\n\n```\n// .claude/settings.json\n{\n  \"hooks\": {\n    \"PostToolUse\": [\n      {\n        \"matcher\": \"Edit|Write\",\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"jq -r '.tool_input.file_path' | xargs npx prettier --write\"\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\nWhere the block goes decides who it applies to:\n\n| Location | Scope | Shared | \n|---|---|---|\n| `~/.claude/settings.json` | All your projects | No | \n| `.claude/settings.json` | This project | Yes, commit it | \n| `.claude/settings.local.json` | This project | No, gitignored | \n| Managed policy settings | The whole organization | Admin controlled | \n| Plugin, skill or subagent | While that is enabled or running | Yes, ships with it | \n\nRun `/hooks` inside Claude Code to see everything configured, grouped by event.\n\n## How do you block a command with a PreToolUse hook?\n\nThe hook receives the tool call as JSON on stdin. Exit 0 lets it through. Exit 2 blocks it, and what you write to stderr goes back to Claude as the reason, so it can change course. This is the pattern from Anthropic's guide, protecting files:\n\n``` bash\n#!/bin/bash\n# .claude/hooks/protect-files.sh\nINPUT=$(cat)\nFILE_PATH=$(echo \"$INPUT\" | jq -r '.tool_input.file_path // empty')\n\nfor pattern in \".env\" \"package-lock.json\" \".git/\"; do\n  if [[ \"$FILE_PATH\" == *\"$pattern\"* ]]; then\n    echo \"Blocked: $FILE_PATH matches protected pattern '$pattern'\" >&2\n    exit 2\n  fi\ndone\nexit 0\n```\n\nRegister it on `PreToolUse` with the matcher `Edit|Write`, make it executable, and ask Claude to edit `.env` to test it. For structured control, exit 0 and print JSON with `permissionDecision` set to `deny`, `ask` or `allow`. When several hooks answer, the most restrictive wins.\n\n## Which commands are worth blocking?\n\nThe ones your agents actually run, not the ones that sound scariest. A guard written from imagination can sit for months without matching anything. The costly commands are often ordinary: `rm -r` on a source directory, `git checkout -- <paths>` or `git reset --hard` throwing away uncommitted work. Read your agents' command history before you decide which guard comes first.\n\nStart a new guard in log-only mode for a week, count what it would have blocked, then switch it to blocking. A guard that fires on legitimate work gets disabled by the second person it annoys.\n\n## Can a hook add context for Claude?\n\nYes. On `UserPromptSubmit`, return JSON with `hookSpecificOutput.additionalContext` and the text is added to Claude's context for that prompt. The guide's example adds the branch and a deploy freeze. Put the field inside `hookSpecificOutput`; at the top level it is silently ignored. `SessionStart` with the `compact` matcher is the documented way to re-inject what must survive a compaction.\n\nThis is the part of hooks that is easiest to overdo. Every line a hook injects is in the prompt, with the same cost and the same [context rot](https://gethrbr.com/blog/context-rot-stale-rules) as a line in `CLAUDE.md`. Inject what applies to this prompt, not everything that might.\n\n## How do you debug a hook that is not running?\n\nPress `Ctrl+O` for the transcript view. A successful hook shows nothing; a block shows its reason; a failing hook shows a *hook error* notice. For the full picture, start with `claude --debug-file /tmp/claude.log` and tail the log, or run `/debug` mid-session. The usual culprits: the script is not executable, `jq` is missing, the matcher does not match the tool name, or a project setting set `disableAllHooks`.\n\n## Where Harbor fits\n\nHarbor is itself delivered through hooks. `harbor init` installs hooks in Claude Code, Codex and Cursor, and they serve each session the team's approved facts that apply to the repo and the task, so the agent starts with the decision from last month's review instead of rediscovering it. What a session learns is written back through review, not straight into the next prompt.\n\nHarbor's [guardrails](https://gethrbr.com/docs/guardrails) follow the log-first advice above. Each one watches for a destructive command and records every match; nothing is refused until your team promotes a guard to block, after seeing what it would have caught. `harbor off` pauses the whole thing for one repo, and `harbor doctor` checks that the hooks are installed and reaching the agents you think they reach.\n\n## Questions\n\n### What are hooks in Claude Code?\n\nHooks are commands Claude Code runs at fixed points in a session, such as before a tool call, after a file edit, or when Claude stops. Unlike CLAUDE.md instructions, they always run.\n\n### How do I block a command in Claude Code?\n\nAdd a PreToolUse hook with a matcher such as Bash or Edit|Write. The script reads the tool call as JSON on stdin and exits with code 2 to block it; what it writes to stderr is passed back to Claude as the reason.\n\n### Where are Claude Code hooks configured?\n\nIn a settings file under a hooks key: ~/.claude/settings.json for all your projects, .claude/settings.json to share with the team, .claude/settings.local.json for yourself, or managed settings for the organization. Run /hooks to see them all.\n\n### Can a hook add context to Claude?\n\nYes. A UserPromptSubmit hook can return JSON with hookSpecificOutput.additionalContext, and that text is added to Claude’s context for the prompt. A SessionStart hook with the compact matcher can re-inject context after compaction.\n\n### Why is my Claude Code hook not running?\n\nCommon causes are a script that is not executable, a missing jq, a matcher that does not match the tool name, or disableAllHooks set in a settings file. Run Claude Code with --debug-file and read the log to see which hooks matched.", "url": "https://wpnews.pro/news/claude-code-hooks-a-practical-guide-with-examples", "canonical_source": "https://gethrbr.com/blog/claude-code-hooks", "published_at": "2026-09-23 00:00:00+00:00", "updated_at": "2026-09-29 00:19:44.982120+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-products"], "entities": ["Anthropic", "Claude Code", "PreToolUse", "PostToolUse", "UserPromptSubmit", "SessionStart", "Stop"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/claude-code-hooks-a-practical-guide-with-examples", "markdown": "https://wpnews.pro/news/claude-code-hooks-a-practical-guide-with-examples.md", "text": "https://wpnews.pro/news/claude-code-hooks-a-practical-guide-with-examples.txt", "jsonld": "https://wpnews.pro/news/claude-code-hooks-a-practical-guide-with-examples.jsonld"}}