# claude-hook-utils claude-hook-utils is a small [[Python]] library for writing [[Claude Code Hooks]] as classes. You subclass `HookHandler`, override the method for the event you care about and return a response object. The library reads the JSON that [[Claude Code]] sends on stdin and prints JSON in the exact shape Claude Code expects. Rasmus Godske published it in January 2026 under the [[MIT License]]. ## The problem it solves Every command hook does the same plumbing before it does anything useful. Parse stdin. Work out which event fired. Dig the file path or the shell command out of `tool_input`. Then build an output object with exactly the right field names (`hookSpecificOutput`, `permissionDecision`, `permissionDecisionReason`...). Get one name wrong and your decision is ignored or reported as a hook error. The library moves that plumbing into typed dataclasses and response builders, so a hook file is mostly the rule you want to enforce. The author built it for a concrete need: keeping Claude in line with [[Laravel]]/Inertia/Vue conventions. His `claude-liv-conventions` plugin (see [[Claude Code Plugins]]) uses it for hooks such as `FormRequestBlocker`, which denies `FormRequest` classes and points Claude to `spatie/laravel-data` Data classes, and `VueScriptValidator`, which requires `<script setup lang="ts">`. His code review orchestrator, Reldo, names it as the infrastructure for its SubagentStart/SubagentStop handling. ## How it works A hook is a script that ends with `MyHandler().run()`. `run()` reads stdin, dispatches on `hook_event_name`, calls your method and prints the response. Returning `None` means "no opinion", so the tool call goes through the normal flow. ```python from claude_hook_utils import HookHandler, PreToolUseInput, PreToolUseResponse class DataClassValidator(HookHandler): def pre_tool_use(self, input: PreToolUseInput) -> PreToolUseResponse | None: if not input.file_path_matches('**/app/Data/**/*.php'): return None if input.content and '#[TypeScript()]' not in input.content: return PreToolUseResponse.deny( "Data classes must have #[TypeScript()] annotation for type generation" ) return PreToolUseResponse.allow() if __name__ == "__main__": DataClassValidator().run() ``` You register the script like any command hook: in `.claude/settings.json` under `hooks.PreToolUse`, with a matcher such as `Write|Edit` and `"command": "python3 /path/to/data_class_validator.py"`. Events the code handles (version 0.4.0): | Event | Method | What you can return | |---|---|---| | `PreToolUse` | `pre_tool_use` | `allow(reason)`, `deny(reason)`, `ask(reason)`. `.with_updated_input(**fields)` rewrites the tool arguments (allow only) | | `PostToolUse` | `post_tool_use` | `acknowledge()`, or `with_context(text)`, sent to Claude as `additionalContext` | | `SubagentStart` | `subagent_start` | `allow()`, which prints nothing | | `SubagentStop` | `subagent_stop` | `allow()` or `block(reason)`. The reason goes back to the subagent so it can fix the issue and retry; `stop_hook_active` tells you it's a retry | The input classes carry helpers. `file_path_matches()` and `file_path_excludes()` take globs with `**`, and properties such as `file_path`, `content`, `command`, `old_string`, `new_string`, `url` and `query` pull values out of `tool_input`. One class can implement several methods, so a single script can serve several events and keep state between calls in the same process. There's also `HookLogger`. It writes JSON Lines to `{cwd}/.claude/logs/{namespace}/hooks.jsonl`; `CLAUDE_HOOK_LOG_DIR` and `CLAUDE_HOOK_LOG_NAMESPACE` override the location. Once a logger is attached, the handler logs every invocation and its outcome (allow, deny, skip, block...) with the session id. That alone is worth something, because debugging hooks is usually guesswork. The deny reason is the interesting part. It goes back to Claude, so a blocked write becomes a correction ("use a Data class instead, see `app/Data/`") and Claude can adapt instead of retrying blindly. One commenter on the [[Hacker News]] thread singled that out. ## Install and run `pip install claude-hook-utils`. It needs Python 3.10 or later and has zero runtime dependencies. Another HN commenter objected to the README's approach: install with pip, then run the script in whatever environment happens to be active. Their advice was [[UV (Python)|uv]], and the author's own plugin already works that way. Each hook gets its own `pyproject.toml` declaring `claude-hook-utils>=0.3.0` and is launched with `cd ${CLAUDE_PLUGIN_ROOT}/hooks/<HookName> && uv run python main.py`. Copy that pattern. ## Where the README and the code disagree The source tells a slightly different story than the README: - **Events.** The README and `docs/CONTRACTS.md` document `UserPromptSubmit` and `SessionStart` handlers. The code has never implemented them: no input class and no dispatch case, anywhere in the git history. Any event other than the four above is logged as "Unknown hook event" and ignored. Claude Code itself documents more than 30 events today - **PostToolUse input.** The library reads `tool_result` and `tool_error`. The official docs name the field `tool_response`. So `tool_result` stays empty, and the `succeeded`/`failed` helpers can't tell you much. Failed tool calls fire a separate `PostToolUseFailure` event, which the library doesn't handle - **SubagentStart.** Claude Code lets this hook inject `additionalContext` into the subagent. The library's response always prints nothing, so you can't - **Fail open.** The README says exceptions in your handler are logged and turned into exit 0. Only invalid JSON on stdin is caught. An exception in your method crashes the script with exit code 1, which Claude Code treats as a non-blocking error: the action proceeds and the transcript shows a hook error notice. Same outcome, different path. Keep it in mind for policy hooks, because a crash lets the action through ## Maturity Small and young. As of 2026-10-03: 31 stars, 2 forks, 15 commits (all between January 3 and 12, 2026), nothing since, and no issues or pull requests. The latest release is 0.4.0 (2026-01-12), which added SubagentStart and SubagentStop. PyPI has 0.1.0, 0.2.0, 0.2.3, 0.3.0 and 0.4.0; the GitHub tags don't line up exactly (v0.0.1, v0.0.3, v0.2.1...). Version 0.3.0 changed the log format, flagged as a breaking change in the commit. The package classifier says Beta, and the test suite covers the logger only. The Hacker News submission (2026-05-29, posted by someone other than the author) reached 18 points and 2 comments. ## The author Rasmus Godske. His site, rasmusgodske.com, describes him as "Software Engineer | Selfhost Enthusiast | HomeLab" and has posts on Laravel, TypeScript and Coolify. His recent GitHub repositories are mostly coding-agent tooling: Claude Code convention plugins, a Claude Agent SDK HTTP wrapper and Reldo. ## Alternatives - **cchooks** (GowayLee, Python, MIT, 132 stars). `create_context()` detects the hook type for you, and the README claims 9 hook events including SessionStart and SessionEnd, with an exit-code mode and a JSON mode. Wider coverage than claude-hook-utils - **claude-hooks-sdk** (Beyond Code, [[PHP]], MIT, 68 stars). Laravel-inspired fluent API for building hook responses, installed with Composer - **claude-hooks** (John Lindquist, [[TypeScript]], MIT, 395 stars). `npx claude-hooks` scaffolds typed hooks that run on [[Bun]]. Last push in August 2025 - **claude-code-hooks-mastery** (IndyDevDan, Python, 3,930 stars). A collection of example hooks to learn from rather than a library - **Claude Agent SDK hooks.** If you drive Claude through the Agent SDK, hooks are in-process callback functions; no stdin or stdout involved - **[[Claude Code Mods]]**. TypeScript function hooks that run inside Claude Code itself ## My take The design is good. One base class, typed inputs, response builders that can't produce a wrong field name, and deny reasons that reach Claude. For a `PreToolUse` guardrail on writes and edits, its main use case, it does the job. The whole package is about 1,700 lines, so you can read all of it in an hour. Beyond that I'd hesitate. It handles four events, the README promises two more that don't exist, the PostToolUse input reads the wrong field, and nothing has moved since January. If you need wider event coverage in Python, look at cchooks first. Or vendor the code and fix it; it's small enough. ## References - [RasmusGodske/claude-hook-utils on GitHub](https://github.com/RasmusGodske/claude-hook-utils) (README, `docs/CONTRACTS.md`, `pyproject.toml`, `src/claude_hook_utils/` source, `tests/`, git history and tags; GitHub API for stars, forks, issues and releases) - [claude-hook-utils on PyPI](https://pypi.org/project/claude-hook-utils/) (versions, Python requirement) - [Hooks reference](https://code.claude.com/docs/en/hooks) (Claude Code docs: event list, PostToolUse `tool_response`, SubagentStart/SubagentStop input and output, exit code behavior) - [Intercept and control agent behavior with hooks](https://code.claude.com/docs/en/agent-sdk/hooks) (Claude Agent SDK docs) - [Python utility package for building Claude Code hooks](https://news.ycombinator.com/item?id=48318978) (Hacker News, 2026-05-29, read through the HN Algolia API) - [RasmusGodske/claude-liv-conventions](https://github.com/RasmusGodske/claude-liv-conventions) (README, `plugins/liv-hooks/.claude-plugin/plugin.json`, per-hook `pyproject.toml`) - [RasmusGodske/reldo](https://github.com/RasmusGodske/reldo) (README, `docs/PRD.md`) - [Rasmus Godske's website](https://rasmusgodske.com/) and [GitHub profile](https://github.com/RasmusGodske) - [GowayLee/cchooks](https://github.com/GowayLee/cchooks) (README) - [beyondcode/claude-hooks-sdk](https://github.com/beyondcode/claude-hooks-sdk) (README) - [johnlindquist/claude-hooks](https://github.com/johnlindquist/claude-hooks) (README) - [disler/claude-code-hooks-mastery](https://github.com/disler/claude-code-hooks-mastery) (GitHub API metadata) ## Related - [[Claude Code Hooks]] - [[Claude Code]] - [[Claude Code Plugins]] - [[Claude Code Mods]] - [[AI Subagents]] - [[Python]]