cchooks vs fasthooks¶
cchooks is a Python SDK for building Claude Code hooks. This is the most direct comparison - both libraries solve the same problem with different approaches.
Same Goal, Different Approaches¶
| Aspect | cchooks | fasthooks |
|---|---|---|
| Pattern | Context factory | Decorator handlers |
| Philosophy | Minimal, explicit | Batteries-included |
| API Style | create_context() → methods |
@app.pre_tool() → return |
Both eliminate JSON boilerplate and provide type-safe hook development.
Architecture¶
cchooks: Context Factory Pattern¶
from cchooks import create_context, PreToolUseContext
c = create_context() # Reads stdin, detects hook type
assert isinstance(c, PreToolUseContext)
if c.tool_name == "Bash" and "rm -rf" in c.tool_input.get("command", ""):
c.output.deny(reason="Dangerous command blocked")
else:
c.output.allow()
Flow: stdin → create_context() → type-specific context → output.method()
fasthooks: Decorator Handlers¶
from fasthooks import HookApp, deny
app = HookApp()
@app.pre_tool("Bash")
def check_bash(event):
if "rm -rf" in event.command:
return deny("Dangerous command blocked")
if __name__ == "__main__":
app.run()
Flow: stdin → app.run() → route to handler → return response
Event Coverage¶
Both support all Claude Code hook events:
| Event | cchooks | fasthooks |
|---|---|---|
| PreToolUse | ✅ | ✅ |
| PostToolUse | ✅ | ✅ |
| Stop | ✅ | ✅ |
| SubagentStop | ✅ | ✅ |
| SessionStart | ✅ | ✅ |
| SessionEnd | ✅ | ✅ |
| UserPromptSubmit | ✅ | ✅ |
| Notification | ✅ | ✅ |
| PreCompact | ✅ | ✅ |
| PermissionRequest | ❌ | ✅ |
Response Format¶
cchooks: Method Calls¶
# PreToolUse
c.output.allow()
c.output.deny(reason="Blocked", system_message="Warning")
c.output.ask(reason="Please confirm")
# Stop
c.output.prevent(reason="Keep working")
c.output.halt(reason="Fatal error")
# SessionStart
c.output.add_context("Additional context for Claude")
# Exit codes
c.output.exit_success() # exit 0
c.output.exit_non_block() # exit 1
c.output.exit_block() # exit 2
fasthooks: Return Values¶
from fasthooks import allow, deny, block
@app.pre_tool("Bash")
def check(event):
return deny("Blocked") # Prevent execution
return allow(message="OK") # Continue
return None # Allow (implicit)
@app.on_stop()
def check_stop(event):
return block("Keep working") # Prevent stop
| Action | cchooks | fasthooks |
|---|---|---|
| Allow tool | c.output.allow() |
return allow() or None |
| Deny tool | c.output.deny(reason=...) |
return deny("reason") |
| Prevent stop | c.output.prevent(reason=...) |
return block("reason") |
| Add context | c.output.add_context(...) |
return allow(message=...) |
Type Safety¶
cchooks: Context Classes¶
from cchooks import PreToolUseContext
c = create_context()
assert isinstance(c, PreToolUseContext)
c.tool_name # str
c.tool_input # dict[str, Any]
c.session_id # str
c.transcript_path # str
c.cwd # str
Manual dict access for tool_input: c.tool_input.get("command", "")
fasthooks: Pydantic Models with Properties¶
@app.pre_tool("Bash")
def check(event):
event.command # str - direct property
event.description # str | None
event.timeout # int | None
event.tool_input # dict - raw access if needed
IDE autocomplete works on properties like event.command.
Handler Registration¶
cchooks: Single Entry Point¶
# hook.py - handles ONE hook type
from cchooks import create_context, PreToolUseContext
c = create_context()
# Must check type manually
if isinstance(c, PreToolUseContext):
# handle PreToolUse
Each hook type = separate file (or manual dispatch).
fasthooks: Multiple Handlers¶
# hooks.py - handles ALL hook types
app = HookApp()
@app.pre_tool("Bash")
def check_bash(event): ...
@app.pre_tool("Write")
def check_write(event): ...
@app.on_stop()
def on_stop(event): ...
app.run() # Routes automatically
One file can handle multiple events with automatic routing.
Tool Matching¶
cchooks: Manual Check¶
c = create_context()
if c.tool_name == "Bash":
# handle Bash
elif c.tool_name in ["Write", "Edit"]:
# handle Write/Edit
fasthooks: Decorator Routing + Guards¶
@app.pre_tool("Bash") # Single tool
def check_bash(event): ...
@app.pre_tool("Write", "Edit") # Multiple tools
def check_write(event): ...
@app.pre_tool() # Catch-all
def check_any(event): ...
@app.pre_tool("Bash", when=lambda e: "sudo" in e.command)
def check_sudo(event): ... # Conditional
State Management¶
cchooks: None Built-in¶
# Must manually implement persistence
import json
from pathlib import Path
STATE_FILE = Path.home() / ".my-hook-state.json"
c = create_context()
state = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else {}
state["count"] = state.get("count", 0) + 1
STATE_FILE.write_text(json.dumps(state))
fasthooks: Dependency Injection¶
from fasthooks.depends import State, Transcript
@app.pre_tool("Bash")
def check(event, state: State, transcript: Transcript):
# state: auto-loaded JSON, session-scoped
state["count"] = state.get("count", 0) + 1
state.save()
# transcript: parsed conversation history
if transcript.stats.tool_calls.get("Bash", 0) > 100:
return deny("Rate limit exceeded")
Feature Comparison¶
| Feature | cchooks | fasthooks |
|---|---|---|
| Typed Events | ✅ (context classes) | ✅ (Pydantic models) |
| Property Accessors | ❌ (dict access) | ✅ (event.command) |
| Response Helpers | ✅ (methods) | ✅ (functions) |
| Multi-Handler Routing | ❌ | ✅ |
| Guards/Filters | ❌ | ✅ (when=) |
| Catch-All Handlers | ❌ | ✅ (@app.pre_tool()) |
| State Persistence | ❌ | ✅ (State) |
| Transcript Parsing | ❌ | ✅ (Transcript) |
| Background Tasks | ❌ | ✅ (Tasks) |
| Blueprints | ❌ | ✅ |
| Middleware | ❌ | ✅ |
| Testing Utils | ❌ | ✅ (MockEvent, TestClient) |
| Tool Input Modification | ✅ (updated_input) |
❌ |
| Safe Error Handling | ✅ (safe_create_context) |
✅ |
Unique to cchooks¶
Tool Input Modification¶
cchooks can modify tool inputs before execution:
c = create_context()
if c.tool_name == "Write":
# Redirect writes to safe location
c.output.allow(
updated_input={"file_path": "/safe/location/" + c.tool_input["file_path"]}
)
fasthooks currently doesn't support updated_input.
Exit Code Control¶
cchooks provides explicit exit code methods:
c.output.exit_success() # exit 0 - success, stdout in transcript
c.output.exit_non_block() # exit 1 - error shown to user
c.output.exit_block() # exit 2 - blocking error
Unique to fasthooks¶
Dependency Injection¶
@app.pre_tool("Bash")
def handler(event, state: State, transcript: Transcript, tasks: Tasks):
# All injected automatically based on type hints
Blueprints¶
security = Blueprint()
@security.pre_tool("Bash")
def no_sudo(event):
if "sudo" in event.command:
return deny("No sudo")
app.include(security)
Middleware¶
@app.middleware
def timing(event, call_next):
start = time.time()
response = call_next(event)
print(f"Hook took {time.time() - start:.3f}s")
return response
Testing Utilities¶
from fasthooks.testing import MockEvent, TestClient
def test_blocks_rm():
client = TestClient(app)
response = client.send(MockEvent.bash(command="rm -rf /"))
assert response.decision == "deny"
Background Tasks¶
from fasthooks.tasks import task, Tasks
@task
def analyze(code: str) -> str:
return expensive_analysis(code)
@app.pre_tool("Write")
def on_write(event, tasks: Tasks):
tasks.add(analyze, event.content)
Code Comparison¶
Blocking Dangerous Commands¶
cchooks:
#!/usr/bin/env python3
from cchooks import create_context, PreToolUseContext
c = create_context()
assert isinstance(c, PreToolUseContext)
if c.tool_name == "Bash":
cmd = c.tool_input.get("command", "")
if "rm -rf" in cmd:
c.output.deny(reason="Dangerous command blocked")
else:
c.output.allow()
else:
c.output.allow()
fasthooks:
#!/usr/bin/env python3
from fasthooks import HookApp, deny
app = HookApp()
@app.pre_tool("Bash")
def check_bash(event):
if "rm -rf" in event.command:
return deny("Dangerous command blocked")
if __name__ == "__main__":
app.run()
Rate Limiting with State¶
cchooks:
#!/usr/bin/env python3
import json
from pathlib import Path
from cchooks import create_context, PreToolUseContext
STATE_FILE = Path.home() / ".hook-state.json"
c = create_context()
assert isinstance(c, PreToolUseContext)
# Manual state management
state = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else {}
count = state.get("bash_count", 0) + 1
state["bash_count"] = count
STATE_FILE.write_text(json.dumps(state))
if count > 100:
c.output.deny(reason=f"Rate limit: {count}/100 commands")
else:
c.output.allow()
fasthooks:
#!/usr/bin/env python3
from fasthooks import HookApp, deny
from fasthooks.depends import State
app = HookApp(state_dir="/tmp/hook-state")
@app.pre_tool("Bash")
def rate_limit(event, state: State):
count = state.get("bash_count", 0) + 1
state["bash_count"] = count
state.save()
if count > 100:
return deny(f"Rate limit: {count}/100 commands")
if __name__ == "__main__":
app.run()
When to Use Each¶
Use cchooks When:¶
- You want a minimal, lightweight SDK
- You prefer explicit control over magic
- You need tool input modification (
updated_input) - You're building simple, single-purpose hooks
- You don't need state management or testing utilities
Use fasthooks When:¶
- You want batteries-included with DI, state, transcripts
- You prefer decorator-based handler registration
- You need multiple handlers in one file
- You want guards and filters for conditional logic
- You need testing utilities for TDD
- You're building complex, multi-event hook systems
- You want blueprints for modular organization
Summary¶
| Aspect | cchooks | fasthooks |
|---|---|---|
| Philosophy | Minimal, explicit | Batteries-included |
| API Style | Context factory | Decorators |
| Learning Curve | Lower | Slightly higher |
| Boilerplate | More | Less |
| Features | Core only | Rich ecosystem |
| Best For | Simple hooks | Complex systems |