Skip to main content

fasthooks

Claude Code Hook SDK for Python

PyPI version Downloads GitHub stars License

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 - MockEvent and TestClient for 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)

Source distribution for fasthooks 0.1.4
File Size Uploaded
fasthooks-0.1.4.tar.gz 23.0 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for fasthooks 0.1.4
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.4 This release

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page