Back to Blog
|15 min read|Last updated: Mar 13, 2026|AI

How I Made My Claude Code Hooks 73x Faster (Dispatcher Pattern)

How I Made My Claude Code Hooks 73x Faster

If you're building with Claude Code and using hooks, you probably started the same way I did: one hook per problem. It works great — until it doesn't.

This is the story of how my 115-hook system was eating 4.7 seconds of overhead on every agent interaction, and how I fixed it with one architectural change.

The Problem: Death by a Thousand Hooks

Claude Code hooks are scripts that run automatically on events — before a tool is used (PreToolUse), after an agent completes (SubagentStop), when a user sends a message (UserPromptSubmit), and more.

Each hook solves a real problem:

  • master-enforcer blocks file edits without proper authorization
  • verify-build-success checks that code compiles after changes
  • mandatory-routing ensures morning greetings trigger the right workflow

The trap is that each hook is a separate process. When SubagentStop fires, Claude Code spawns a new Python interpreter for every registered hook. At 80ms per cold start, 34 hooks means 2.7 seconds of pure overhead — before any check logic even runs.

The worst part? I discovered that 30+ of those 34 hooks were immediately exiting. They'd check the agent name, see it wasn't their target, and quit. Thirty Python processes spawned just to do nothing.

The Numbers (Before)

Event Hooks Overhead
SubagentStop 34 ~2.7 seconds
PreToolUse 14 ~1.1 seconds
UserPromptSubmit 11 ~880ms
Total per agent cycle 59 ~4.7 seconds

That's 4.7 seconds of tax on every single agent interaction. For a system that runs hundreds of agent calls per day, this adds up to hours of wasted compute.

The Solution: The Dispatcher Pattern

Instead of registering 34 separate scripts, register one dispatcher that:

  1. Reads which agent just completed
  2. Looks up which checks apply to that agent
  3. Imports and runs only the relevant checks in-process (no subprocess spawning)
  4. Returns a single merged response

Architecture Diagram

BEFORE (34 separate processes):
┌──────────────┐
│ SubagentStop  │
│   Event       │──→ hook_1.py (spawn process) → exit
│               │──→ hook_2.py (spawn process) → exit
│               │──→ hook_3.py (spawn process) → check → exit
│               │──→ ...
│               │──→ hook_34.py (spawn process) → exit
└──────────────┘
Total: 34 cold starts × ~80ms = 2,720ms

AFTER (1 dispatcher process):
┌──────────────┐     ┌─────────────────────┐
│ SubagentStop  │     │  dispatcher.py       │
│   Event       │──→  │  read agent name     │
│               │     │  lookup bundles       │
│               │     │  import check_3()     │
│               │     │  run in-process       │
│               │     │  return result        │
└──────────────┘     └─────────────────────┘
Total: 1 cold start + in-process calls = ~25ms

Step 1: Agent Tagging

At SubagentStart, a small hook writes the agent's name to a tag file:

# agent-tagger.py (SubagentStart hook)
def main(input_data=None):
    agent_name = input_data.get('agent_name', '')
    with open('/tmp/.last-agent-name', 'w') as f:
        json.dump({'agent': agent_name}, f)

Step 2: Bundle Configuration

A JSON config maps each agent to its check bundles:

{
  "agent_bundles": {
    "web-developer": ["universal", "code"],
    "chief-of-staff": ["universal", "health"],
    "impl-planner": ["universal", "planning"],
    "DEFAULT": ["universal"]
  },
  "bundles": {
    "universal": ["verify-agent-protocol.py", "knowledge-evaluation.py"],
    "code": ["verify-build-success.py", "verify-no-dev-servers.py"],
    "health": ["verify-morning-output.py", "verify-weight-gate.py"],
    "planning": ["verify-confidence-scoring.py"]
  }
}

A web-developer completion runs 8 checks (universal + code). A planning agent runs 8 checks (universal + planning). An unknown agent runs only 5 (universal). Instead of all 34 every time.

Step 3: The Dispatcher

The dispatcher reads the tag, loads the config, and imports each check as a Python module:

# dispatch_subagent_stop.py
from dispatcher_utils import (
    load_config, read_agent_name, import_hook,
    run_check_safe, merge_responses
)

def main():
    input_data = json.load(sys.stdin)
    agent_name = read_agent_name(input_data)
    config = load_config()

    bundle_names = get_bundles_for_agent(agent_name, config)
    check_files = get_checks_for_bundles(bundle_names, config)

    results = []
    for hook_file in check_files:
        module = import_hook(hook_file)
        exit_code, stdout, stderr = run_check_safe(module, input_data)
        results.append((hook_file, (exit_code, stdout, stderr)))

    # Merge and return single response
    final_exit, outputs, errors, blocks = merge_responses(results)
    sys.exit(final_exit)

Step 4: Safe Execution Wrapper

The critical piece — each check runs inside a wrapper that catches SystemExit (so hooks calling sys.exit() don't kill the dispatcher):

def run_check_safe(hook_module, input_data, hook_name="unknown"):
    stdout_capture = io.StringIO()
    stderr_capture = io.StringIO()
    exit_code = 0
    try:
        with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
            hook_module.main(input_data)
    except SystemExit as e:
        exit_code = e.code if e.code is not None else 0
    except Exception:
        exit_code = 0  # Never block on hook crash
    return exit_code, stdout_capture.getvalue(), stderr_capture.getvalue()

Step 5: Adapt Existing Hooks

Each hook needs a one-line change to accept pre-parsed input data:

# Before
def main():
    input_data = json.load(sys.stdin)

# After
def main(input_data=None):
    if input_data is None:
        input_data = json.load(sys.stdin)

This preserves backward compatibility — hooks still work standalone when piped JSON via stdin.

The Numbers (After)

Event Before After Speedup
SubagentStop ~2.7s (34 processes) 23ms (1 process) 117x
PreToolUse ~1.1s (13 processes) 18ms (1 process) 61x
UserPromptSubmit ~880ms (10 processes) 23ms (1 process) 38x
Total per cycle ~4.7s ~64ms 73x

Same enforcement. Same checks. Zero behavior change. Just smarter architecture.

The Second Win: Rule Consolidation

While optimizing hooks, I applied the same principle to my instruction rules — the markdown files that Claude Code loads every session.

I had accumulated 1,824 lines of rules across 13 files. Many were duplicated, some contradicted each other. Research on AI instruction compliance showed that:

  • Optimal always-loaded instructions: 50-200 lines — beyond that, compliance degrades
  • "Lost in the Middle" effect — instructions buried in long files get ignored by the model
  • Progressive disclosure is better — lean core loaded always, details loaded on-demand

After auditing: 1,824 → 248 lines (-86%). Thirteen files archived, five essential ones kept. Agent behavior actually improved because the remaining instructions were clear, non-contradictory, and short enough to be fully processed.

Key Principles

1. Measure Before Optimizing

I assumed my hooks were efficient because each one was small. I never measured the cumulative cost of spawning 34+ processes per event. Always measure.

2. Incremental Additions Compound Into Complexity Debt

Each hook was rational individually. The 115th hook was no different from the first. But the system-level cost grew linearly while the per-hook value didn't.

3. The Dispatcher Pattern Applies Everywhere

One entry point that routes conditionally is almost always better than many independent entry points. This applies to hooks, middleware, event handlers, and API routes.

4. Less Context = Better AI Compliance

For AI agent systems specifically: the model processes your rules better when there are fewer of them. Duplication doesn't add safety — it adds noise.

5. Architecture Over Addition

When your system feels slow or unreliable, the answer is rarely "add another check." It's usually "restructure how checks are organized."

How to Implement This Yourself

  1. Count your hooks: python3 -c "import json; ..." on your settings.json
  2. Measure overhead: Add timing to one hook to see cold start cost
  3. Identify waste: How many hooks exit immediately for non-matching agents?
  4. Create dispatcher_config.json: Map agents to check bundles
  5. Adapt hooks: Add input_data=None parameter to each main()
  6. Build dispatchers: One per high-frequency event (SubagentStop, PreToolUse)
  7. Switch settings.json: Replace N entries with 1 dispatcher entry
  8. Keep backups: Your old settings.json is your 2-minute rollback plan

The dispatcher pattern turned my slowest system component into my fastest. The same approach will work for any Claude Code setup with more than 10 hooks.

FAQ

How many hooks is too many?

There's no hard limit, but if you have more than 10 hooks on SubagentStop, you should consider a dispatcher. The overhead grows linearly with hook count.

Does the dispatcher pattern change what gets enforced?

No. The same checks run for the same agents. The only change is architectural — checks are imported as Python modules instead of spawned as separate processes.

What if the dispatcher crashes?

The dispatcher wraps everything in a top-level try/except that exits 0. A dispatcher crash never blocks your agent. Individual hooks can be re-registered as a fallback in under 2 minutes.

Can I use this with non-Python hooks?

Shell scripts stay as separate registrations (they can't be imported as modules). The dispatcher pattern works best when most of your hooks are in the same language.

How do I add a new check after implementing the dispatcher?

Add the check file, add it to the appropriate bundle in dispatcher_config.json, and add the input_data=None parameter to its main(). No settings.json change needed.

About the Author

DG

Dawid Gac

E-commerce Educator & Entrepreneur

Dawid Gac is a Polish entrepreneur, e-commerce educator, and co-founder of EcomBrain. He helps entrepreneurs build and scale online businesses through his YouTube channel, community, and 1:1 coaching.

Related Articles