Utilities for building Claude Code hooks with minimal boilerplate
Project description
claude-hook-utils
A Python utility package for building Claude Code hooks with minimal boilerplate.
What Are Claude Code Hooks?
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)
Why This Package?
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.
Design Philosophy
- 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
Installation
pip install claude-hook-utils
Quick Start
#!/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"
}
]
}
]
}
}
Supported Hook Types
| 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 |
API Reference
HookHandler Base Class
Extend this class and override the hooks you need:
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()
PreToolUseInput
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)."""
PreToolUseResponse
Response builder for PreToolUse hooks:
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)."""
HookLogger
Optional logging utility:
from claude_hook_utils import HookHandler, HookLogger
class MyHandler(HookHandler):
def __init__(self):
super().__init__(
logger=HookLogger(
log_file="my-validator.log",
include_timestamp=True,
)
)
def pre_tool_use(self, input: PreToolUseInput) -> PreToolUseResponse | None:
self.logger.info(f"Checking: {input.file_path}")
# ... validation logic ...
self.logger.decision("allow", input.file_path)
return PreToolUseResponse.allow()
Examples
Validate Vue Component Structure
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: <script> before <template> before <style>
script_pos = content.find('<script')
template_pos = content.find('<template')
style_pos = content.find('<style')
if script_pos > template_pos or template_pos > style_pos:
return PreToolUseResponse.deny(
"Vue components must have tags in order: <script>, <template>, <style>"
)
# Check for setup lang="ts"
if '<script setup lang="ts">' not in content:
return PreToolUseResponse.deny(
"Vue components must use <script setup lang=\"ts\">"
)
return PreToolUseResponse.allow()
Validate Controller Location
class ControllerValidator(HookHandler):
def pre_tool_use(self, input: PreToolUseInput) -> PreToolUseResponse | None:
if not input.file_path_matches('**/*Controller.php'):
return None
# Controllers must be in app/Http/Controllers/
if not input.file_path_matches('**/app/Http/Controllers/**/*.php'):
return PreToolUseResponse.deny(
f"Controllers must be in app/Http/Controllers/. "
f"Found: {input.file_path}"
)
return PreToolUseResponse.allow()
Block FormRequest Usage (Suggest Data Class)
class NoFormRequestValidator(HookHandler):
def pre_tool_use(self, input: PreToolUseInput) -> PreToolUseResponse | None:
if not input.file_path_matches('**/*Controller.php'):
return None
content = input.content or ''
if 'FormRequest' in content:
return PreToolUseResponse.deny(
"Do not use FormRequest classes. Use Data classes instead. "
"See: app/Data/ for examples."
)
return PreToolUseResponse.allow()
Multi-Hook Handler (Pre + Post)
class FileTracker(HookHandler):
def __init__(self):
super().__init__()
self._pending_writes: set[str] = set()
def pre_tool_use(self, input: PreToolUseInput) -> PreToolUseResponse | None:
if input.tool_name == 'Write' and input.file_path:
self._pending_writes.add(input.file_path)
self.logger.info(f"Tracking write: {input.file_path}")
return PreToolUseResponse.allow()
def post_tool_use(self, input: PostToolUseInput) -> PostToolUseResponse | None:
if input.tool_name == 'Write' and input.file_path:
self._pending_writes.discard(input.file_path)
self.logger.info(f"Write completed: {input.file_path}")
return None
Claude Code Hook Response Format
This package generates responses in the official hookSpecificOutput format:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Your reason here"
}
}
Decision Options
| Decision | Effect |
|---|---|
allow |
Tool executes immediately, reason shown to user |
deny |
Tool blocked, reason shown to Claude (so it can adapt) |
ask |
User confirmation dialog shown |
Modifying Tool Input
Use with_updated_input() to modify parameters before execution:
def pre_tool_use(self, input: PreToolUseInput) -> PreToolUseResponse | None:
# Auto-correct a common mistake
if input.file_path and '/data/' in input.file_path:
corrected = input.file_path.replace('/data/', '/Data/')
return PreToolUseResponse.allow("Auto-corrected path").with_updated_input(
file_path=corrected
)
return PreToolUseResponse.allow()
Error Handling
The package handles errors gracefully:
- Invalid JSON input: Returns exit 0 (no output = allow)
- Unknown hook type: Returns None (skip)
- Exception in handler: Logged to stderr, returns exit 0 (fail open)
This "fail open" approach ensures your hooks don't block Claude Code if something goes wrong.
Environment Variables
Claude Code provides these environment variables to hooks:
| Variable | Description |
|---|---|
CLAUDE_PROJECT_DIR |
Absolute path to project root |
CLAUDE_CODE_REMOTE |
"true" if running in web environment |
Access via input.cwd or os.environ.
Extending for New Hook Types
To add support for a new hook type:
- Create input dataclass in
inputs/ - Create response class in
responses/ - Add handler method to
HookHandler - Add dispatch case in
HookHandler._dispatch()
See existing implementations for patterns to follow.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file claude_hook_utils-0.1.0.tar.gz.
File metadata
- Download URL: claude_hook_utils-0.1.0.tar.gz
- Upload date:
- Size: 14.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
da125c03c40c3418bbb5a85b9162da0fd0e4cafeca27c8acbfac00dbed897131
|
|
| MD5 |
156c291fa2ddb58e3994fc207b864d25
|
|
| BLAKE2b-256 |
e7f5eca1fb68c52923332ad23e8f7fe8cef64744062dca82c91692804e9e2e1f
|
File details
Details for the file claude_hook_utils-0.1.0-py3-none-any.whl.
File metadata
- Download URL: claude_hook_utils-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6f8a754ffc2dc1d1b3e4a5114b4097deedaffbe5db355f79f72e8f229fbeb0ec
|
|
| MD5 |
3b455921d022a005ca1b4211c9e0dfb7
|
|
| BLAKE2b-256 |
1a24c3e165830f0023e99cd8b88da324303d0ac45726e5ec9fc17dd3bfd82729
|