Delightful Claude Code hooks - FastAPI-like DX for building hooks
Project description
fasthooks
Claude Code Hook SDK for Python
Documentation · GitHub · PyPI
Delightful Claude Code hooks with a FastAPI-like developer experience.
from fasthooks import HookApp, deny
app = HookApp()
@app.pre_tool("Bash")
def no_rm_rf(event):
if "rm -rf" in event.command:
return deny("Dangerous command")
if __name__ == "__main__":
app.run()
Features
- Typed events - Autocomplete for
event.command,event.file_path, etc. - Decorators -
@app.pre_tool("Bash"),@app.on_stop(),@app.on_session_start() - Dependency injection -
def handler(event, transcript: Transcript, state: State) - Background tasks - Spawn async work that feeds back in subsequent hooks
- Claude sub-agents - Use Claude Agent SDK for AI-powered background tasks
- Blueprints - Compose handlers from multiple modules
- Middleware - Cross-cutting concerns like timing and logging
- Guards -
@app.pre_tool("Bash", when=lambda e: "sudo" in e.command) - Testing utilities -
MockEventandTestClientfor easy testing
Installation
pip install fasthooks
Or with uv:
uv add fasthooks
Quick Start
1. Create a hooks project
fasthooks init my-hooks
cd my-hooks
2. Edit hooks.py
from fasthooks import HookApp, allow, deny
app = HookApp()
@app.pre_tool("Bash")
def check_bash(event):
# event.command has autocomplete!
if "rm -rf" in event.command:
return deny("Dangerous command blocked")
return allow()
@app.pre_tool("Write")
def check_write(event):
# event.file_path, event.content available
if event.file_path.endswith(".env"):
return deny("Cannot modify .env files")
return allow()
@app.on_stop()
def on_stop(event):
return allow()
if __name__ == "__main__":
app.run()
3. Configure Claude Code
Add to your settings.json:
{
"hooks": {
"PreToolUse": [{"command": "python /path/to/hooks.py"}],
"Stop": [{"command": "python /path/to/hooks.py"}]
}
}
API Reference
Responses
from fasthooks import allow, deny, block, approve_permission, deny_permission
return allow() # Proceed
return allow(message="Approved by hook") # With message
return deny("Reason shown to Claude") # Block tool
return block("Continue working on X") # For Stop hooks
# For PermissionRequest hooks
return approve_permission() # Auto-approve permission
return approve_permission(modify={"command": "safe"}) # Approve with modified input
return deny_permission("Not allowed") # Deny permission
Tool Decorators
@app.pre_tool("Bash") # Single tool
@app.pre_tool("Write", "Edit") # Multiple tools
@app.post_tool("Bash") # After execution
Lifecycle Decorators
@app.on_stop() # Main agent stops
@app.on_subagent_stop() # Subagent stops
@app.on_session_start() # Session begins
@app.on_session_end() # Session ends
@app.on_pre_compact() # Before compaction
@app.on_prompt() # User submits prompt
@app.on_notification() # Notification sent
@app.on_permission("Bash") # Permission dialog shown (tool-specific)
@app.on_permission() # Permission dialog (catch-all)
Typed Events
@app.pre_tool("Bash")
def handle_bash(event):
event.command # str
event.description # str | None
event.timeout # int | None
@app.pre_tool("Write")
def handle_write(event):
event.file_path # str
event.content # str
@app.pre_tool("Edit")
def handle_edit(event):
event.file_path # str
event.old_string # str
event.new_string # str
Dependency Injection
from fasthooks.depends import Transcript, State
@app.on_stop()
def with_deps(event, transcript: Transcript, state: State):
# transcript - lazy-parsed transcript with stats
print(transcript.stats.tool_calls) # {"Bash": 5, "Read": 3}
print(transcript.stats.duration_seconds)
# state - persistent dict (session-scoped)
state["count"] = state.get("count", 0) + 1
state.save()
Guards
@app.pre_tool("Write", when=lambda e: e.file_path.endswith(".py"))
def python_only(event):
# Only called for .py files
pass
@app.on_session_start(when=lambda e: e.source == "startup")
def startup_only(event):
# Only on fresh startup, not resume
pass
Blueprints
from fasthooks import Blueprint
security = Blueprint("security")
@security.pre_tool("Bash")
def no_sudo(event):
if "sudo" in event.command:
return deny("sudo not allowed")
# In main app
app.include(security)
Middleware
import time
@app.middleware
def timing(event, call_next):
start = time.time()
response = call_next(event)
print(f"Took {time.time() - start:.3f}s")
return response
Background Tasks
Spawn async work that completes independently and feeds back results in subsequent hooks:
from fasthooks import HookApp, allow
from fasthooks.tasks import task, Tasks
@task
def analyze_code(code: str) -> str:
# Long-running analysis...
return "Analysis result"
app = HookApp()
@app.pre_tool("Write")
def on_write(event, tasks: Tasks):
# Spawn task (key defaults to function name)
tasks.add(analyze_code, event.content)
return allow()
@app.on_prompt()
def check_results(event, tasks: Tasks):
# Pop by function reference (no string typos)
if result := tasks.pop(analyze_code):
return allow(message=f"Previous analysis: {result}")
return allow()
Claude Sub-Agents
Use Claude Agent SDK for AI-powered background tasks (requires pip install fasthooks[claude]):
from fasthooks.contrib.claude import ClaudeAgent, agent_task
from fasthooks.tasks import Tasks
@agent_task(model="haiku", system_prompt="You review code for bugs.")
async def review_code(agent: ClaudeAgent, code: str) -> str:
return await agent.query(f"Review this code:\n{code}")
@app.pre_tool("Write")
def on_write(event, tasks: Tasks):
tasks.add(review_code, event.content)
return allow()
Testing
from fasthooks.testing import MockEvent, TestClient
def test_no_rm_rf():
app = HookApp()
@app.pre_tool("Bash")
def handler(event):
if "rm" in event.command:
return deny("No rm")
return allow()
client = TestClient(app)
# Safe command - allowed
response = client.send(MockEvent.bash(command="ls"))
assert response is None
# Dangerous command - denied
response = client.send(MockEvent.bash(command="rm -rf /"))
assert response.decision == "deny"
CLI
# Initialize a new project (creates hooks.py, pyproject.toml, .claude/settings.json)
fasthooks init my-hooks
# Run hooks (called by Claude Code)
fasthooks run hooks.py
# Generate sample event JSON for testing
fasthooks example bash
fasthooks example bash_dangerous > event.json
# Test hooks locally
fasthooks run hooks.py --input event.json
# Show help
fasthooks --help
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 fasthooks-0.1.4.tar.gz.
File metadata
- Download URL: fasthooks-0.1.4.tar.gz
- Upload date:
- Size: 23.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ee8f5dc108d2aa66181a39f3b906c2ca847161bc7fe529646933dcc6cc101ed1
|
|
| MD5 |
1d04b600e8c821091dbf3e52122a9575
|
|
| BLAKE2b-256 |
622cd8afbe0a8ed1bfbffcf504f2cf12afeede509aba08c04f7f9276eecc708a
|
File details
Details for the file fasthooks-0.1.4-py3-none-any.whl.
File metadata
- Download URL: fasthooks-0.1.4-py3-none-any.whl
- Upload date:
- Size: 233.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b5e3338c42087c82babd9300d9ea3e59779aa6935c9e389d3285bf52e75c38f
|
|
| MD5 |
e627810bee1bbfaab563b1f6e0e725f0
|
|
| BLAKE2b-256 |
6a6355886e32bbbaefdbae8dd93fa1881c530148557457cbaf49a625727e7b54
|