claude-hook-utils
This is a note from my public notes. View the canonical version: 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.
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, 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.mddocumentUserPromptSubmitandSessionStarthandlers. 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_resultandtool_error. The official docs name the fieldtool_response. Sotool_resultstays empty, and thesucceeded/failedhelpers can't tell you much. Failed tool calls fire a separatePostToolUseFailureevent, which the library doesn't handle - SubagentStart. Claude Code lets this hook inject
additionalContextinto 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: 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).
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). Laravel-inspired fluent API for building hook responses, installed with Composer
- claude-hooks (John Lindquist, TypeScript, MIT).
npx claude-hooksscaffolds typed hooks that run on Bun. Last push in August 2025 - claude-code-hooks-mastery (IndyDevDan, Python). 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 (README,
docs/CONTRACTS.md,pyproject.toml,src/claude_hook_utils/source,tests/, git history and tags; GitHub API for issues and releases) - claude-hook-utils on PyPI (versions, Python requirement)
- Hooks reference (Claude Code docs: event list, PostToolUse
tool_response, SubagentStart/SubagentStop input and output, exit code behavior) - Intercept and control agent behavior with hooks (Claude Agent SDK docs)
- Python utility package for building Claude Code hooks (Hacker News, 2026-05-29, read through the HN Algolia API)
- RasmusGodske/claude-liv-conventions (README,
plugins/liv-hooks/.claude-plugin/plugin.json, per-hookpyproject.toml) - RasmusGodske/reldo (README,
docs/PRD.md) - Rasmus Godske's website and GitHub profile
- GowayLee/cchooks (README)
- beyondcode/claude-hooks-sdk (README)
- johnlindquist/claude-hooks (README)
- disler/claude-code-hooks-mastery (GitHub API metadata)
Related
About Sébastien
Ready to get to the next level?
Found this valuable? Share it with someone who needs it.