Skip to main content

Delightful Claude Code hooks - FastAPI-like DX for building hooks

Project description

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

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

fasthooks-0.1.4.tar.gz (23.0 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fasthooks-0.1.4-py3-none-any.whl (233.6 kB view details)

Uploaded Python 3

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

Hashes for fasthooks-0.1.4.tar.gz
Algorithm Hash digest
SHA256 ee8f5dc108d2aa66181a39f3b906c2ca847161bc7fe529646933dcc6cc101ed1
MD5 1d04b600e8c821091dbf3e52122a9575
BLAKE2b-256 622cd8afbe0a8ed1bfbffcf504f2cf12afeede509aba08c04f7f9276eecc708a

See more details on using hashes here.

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

Hashes for fasthooks-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 1b5e3338c42087c82babd9300d9ea3e59779aa6935c9e389d3285bf52e75c38f
MD5 e627810bee1bbfaab563b1f6e0e725f0
BLAKE2b-256 6a6355886e32bbbaefdbae8dd93fa1881c530148557457cbaf49a625727e7b54

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page