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
Release files for fasthooks 0.1.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| fasthooks-0.1.4.tar.gz | 23.0 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| fasthooks-0.1.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 23.2 MB
Release files / fasthooks-0.1.4.tar.gz
| Download URL | fasthooks-0.1.4.tar.gz |
|---|---|
| Size | 23.0 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ee8f5dc108d2aa66181a39f3b906c2ca847161bc7fe529646933dcc6cc101ed1
|
|
BLAKE2b-256 checksum How to use checksums |
622cd8afbe0a8ed1bfbffcf504f2cf12afeede509aba08c04f7f9276eecc708a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.7.5
|
Release files / fasthooks-0.1.4-py3-none-any.whl
| Download URL | fasthooks-0.1.4-py3-none-any.whl |
|---|---|
| Size | 233.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1b5e3338c42087c82babd9300d9ea3e59779aa6935c9e389d3285bf52e75c38f
|
|
BLAKE2b-256 checksum How to use checksums |
6a6355886e32bbbaefdbae8dd93fa1881c530148557457cbaf49a625727e7b54
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.7.5
|