Python utility package for building Claude Code hooks Anthropic released claude-hook-utils, a Python utility package that reduces boilerplate for building Claude Code hooks. The package provides typed dataclasses, a builder pattern for responses, and multi-hook support, allowing developers to validate tool calls, react to results, intercept prompts, and initialize session state with minimal code. A Python utility package for building Claude Code hooks https://docs.anthropic.com/en/docs/claude-code/hooks with minimal boilerplate. Claude Code hooks are custom scripts that run at specific points during Claude Code's execution. They allow you to: Validate tool calls before they execute PreToolUse React to tool results after execution PostToolUse Intercept user prompts before Claude sees them UserPromptSubmit Initialize state when a session starts SessionStart Building Claude Code hooks involves repetitive boilerplate: - Parsing JSON from stdin - Validating input structure - Formatting responses in the correct schema - Handling errors gracefully claude-hook-utils handles all of this, letting you focus on your validation logic. One Pattern - Extend HookHandler , override the hooks you need Type Safety - Typed dataclasses for inputs, builder pattern for responses Explicit Control - Helper methods on inputs, but you decide when to skip/allow/deny Multi-Hook Support - One Python program can handle multiple hook types No Heavy Dependencies - Core package has minimal dependencies; bring your own AI SDK if needed pip install claude-hook-utils bash /usr/bin/env python3 """Validate that Data classes have TypeScript annotation.""" from claude hook utils import HookHandler, PreToolUseInput, PreToolUseResponse class DataClassValidator HookHandler : def pre tool use self, input: PreToolUseInput - PreToolUseResponse | None: Skip if not a Data class file if not input.file path matches ' /app/Data/ / .php' : return None Check for required annotation 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 Configure in .claude/settings.json : { "hooks": { "PreToolUse": { "matcher": "Write|Edit", "hooks": { "type": "command", "command": "python3 /path/to/data class validator.py" } } } } | Hook Type | When It Runs | Use Cases | |---|---|---| PreToolUse | Before a tool executes | Validate file paths, check content, block dangerous operations | PostToolUse | After a tool completes | Log results, trigger follow-up actions, collect metrics | UserPromptSubmit | When user submits a prompt | Validate prompts, add context, enforce policies | SessionStart | When a Claude Code session begins | Initialize state, set environment variables | Extend this class and override the hooks you need: python from claude hook utils import HookHandler class MyHandler HookHandler : def init self : super . init Add any shared state here self. cache: dict = {} def pre tool use self, input: PreToolUseInput - PreToolUseResponse | None: """Called before tool execution. Return None to skip.""" return None def post tool use self, input: PostToolUseInput - PostToolUseResponse | None: """Called after tool execution. Return None to skip.""" return None def user prompt submit self, input: UserPromptSubmitInput - UserPromptSubmitResponse | None: """Called when user submits a prompt. Return None to skip.""" return None def session start self, input: SessionStartInput - SessionStartResponse | None: """Called when session starts. Return None to skip.""" return None if name == " main ": MyHandler .run Input for PreToolUse hooks: @dataclass class PreToolUseInput: Common fields session id: str cwd: str hook event name: str Always "PreToolUse" PreToolUse-specific tool name: str "Write", "Edit", "Bash", etc. tool input: dict Tool-specific parameters tool use id: str Helper methods def file path matches self, globs: str - bool: """Check if tool input.file path matches any glob pattern.""" def file path excludes self, globs: str - bool: """Check if tool input.file path does NOT match any glob pattern.""" Convenience properties @property def file path self - str | None: """Get file path from tool input for Write/Edit/Read tools .""" @property def content self - str | None: """Get content from tool input for Write tool .""" @property def command self - str | None: """Get command from tool input for Bash tool .""" Response builder for PreToolUse hooks: python class PreToolUseResponse: @staticmethod def allow reason: str | None = None - PreToolUseResponse: """Allow the tool to execute.""" @staticmethod def deny reason: str - PreToolUseResponse: """Block the tool. Reason is shown to Claude as feedback.""" @staticmethod def ask reason: str - PreToolUseResponse: """Request user confirmation before proceeding.""" def with updated input self, updates - PreToolUseResponse: """Modify tool input before execution only valid with allow .""" JSONL-based logging for easy debugging. Logs are organized by namespace plugin name . python from claude hook utils import HookHandler, HookLogger class MyHandler HookHandler : def init self : Logs to .claude/logs/my-plugin/hooks.jsonl super . init logger=HookLogger.create default "MyHandler", namespace="my-plugin" def pre tool use self, input: PreToolUseInput - PreToolUseResponse | None: Session ID is automatically added from input self.logger.info "Checking file", file path=input.file path ... validation logic ... self.logger.decision "allow", reason="Validation passed" return PreToolUseResponse.allow Log format JSONL - one JSON object per line : {"ts": "2025-01-04T10:15:23.456+00:00", "level": "INFO", "hook": "MyHandler", "namespace": "my-plugin", "session": "abc123", "msg": "Checking file", "file path": "/path/to/file.php"} {"ts": "2025-01-04T10:15:23.458+00:00", "level": "DECISION", "hook": "MyHandler", "namespace": "my-plugin", "session": "abc123", "msg": "decision=allow", "decision": "allow", "reason": "Validation passed"} Configuration: - Default location: {cwd}/.claude/logs/{namespace}/hooks.jsonl - Without namespace: {cwd}/.claude/logs/hooks.jsonl - Override directory with CLAUDE HOOK LOG DIR env var - Override namespace with CLAUDE HOOK LOG NAMESPACE env var - Session ID is automatically extracted from hook input python class VueValidator HookHandler : def pre tool use self, input: PreToolUseInput - PreToolUseResponse | None: if not input.file path matches ' / .vue' : return None content = input.content or '' Check tag order: