Claude Code Hooks Daemon: Robust Hook Development for AI-Assisted Workflows

ByEdmonds Commerce
18 mins read

How we built an open-source Python daemon that makes Claude Code hooks properly testable and iterable. Restart independently of Claude Code, develop handlers with TDD, and manage 48 built-in rules across 11 languages.

Claude Code hooks give you deterministic control over AI-assisted development workflows. They fire at specific lifecycle points, letting you block dangerous operations, enforce coding standards, and inject context without relying on the LLM to remember your rules. The concept is powerful. But building and maintaining hooks with the native system is painful.

The real problem is not just performance. It is that hooks are difficult to develop, difficult to test, and difficult to iterate on. You write a shell script, register it in settings, and then discover that testing it means triggering the exact scenario in a live session. If something goes wrong, you edit the script externally and restart your session to pick up the change. You can resume with --continue, but the iteration cycle is painfully slow.

We built the Claude Code Hooks Daemon[src] to fix this. It is an open-source Python daemon that runs as a separate process, entirely independent of Claude Code. You can restart the daemon without interrupting your Claude Code session. You can use Claude Code itself to write and test new handlers. And because handlers are proper Python classes rather than shell scripts, you get real testing, type safety, and software engineering practices around your hook logic.

This article explains why hook development is hard with the native system, how the daemon solves it, and what you get out of the box.

Understanding Claude Code Hooks

Before diving into the daemon, it helps to understand what Claude Code hooks actually do and why they matter for AI-assisted development.

Claude Code hooks are user-defined commands that execute at specific points in Claude Code's lifecycle[src]. Unlike the LLM itself, hooks are deterministic. They always run, they always produce the same result for the same input, and they cannot be talked out of their decisions by a clever prompt.

The hook system supports multiple event types. PreToolUse fires before any tool execution and can block the action entirely. PostToolUse fires after a tool succeeds. SessionStart runs when a session begins or resumes. Other events cover compaction, subagent completion, user prompt submission, notifications, and session termination.

Each hook receives structured JSON on stdin describing the event context: which tool is being called, what arguments it received, what file is being written. The hook processes this input and responds through exit codes and stdout. Exit code 0 means "allow the action." Exit code 2 means "block it." The hook can also return JSON with additional context that gets injected into the conversation.

The Hook Development Problem

The native hook system works well for simple, static rules. But as soon as you want to build anything substantial, you run into friction on multiple fronts.

Iteration requires session restarts. Hooks are registered in Claude Code's settings file. When you change a hook script, Claude Code does not automatically pick up the change. You can resume with --continue, but for hooks that need tuning through trial and error, this restart-and-iterate cycle is painfully slow.

Shell scripts do not scale. A hook that blocks git push --force is a few lines of bash. A hook that validates QA suppression patterns across 11 programming languages, each with its own comment syntax and suppression mechanisms, is not. Complex enforcement logic in shell scripts quickly becomes unmaintainable. There is no type system, no proper testing framework, no dependency management, and no way to share utility code between hooks.

Testing is ad hoc. The only way to test a native hook is to trigger the real scenario in a live Claude Code session. There is no unit test framework, no way to mock inputs, and no way to run a hook in isolation. You cannot write a failing test first and then implement the handler. This makes test-driven development impossible.

Process spawning adds up. Every native hook spawns a new shell process on every matching event. On a powerful desktop or laptop the per-invocation cost is small, but it still compounds. With ten hooks registered on PreToolUse events, each tool call spawns ten processes. Over a session with hundreds of tool calls, it adds up to thousands of process spawns that could be avoided entirely.

The real problem is not latency. It is that you cannot use proper software engineering practices to build your hooks. No tests, no types, no hot reloading.

Daemon Architecture

The daemon takes a fundamentally different approach. It runs as a separate Python process, completely independent of Claude Code[src]. When installed, your project has just five Claude Code hooks registered in settings, one per event type. Each is a lightweight shell forwarder that sends events to the daemon over a Unix domain socket and returns the response. The forwarder opens the socket, sends JSON, reads the reply, and exits. All the real logic lives in the daemon.

Because the daemon is a separate process, you can restart it without touching Claude Code. Your session continues uninterrupted with all its context intact. Change a handler, restart the daemon with a single CLI command, and test your change immediately. The restart takes under a second.

Inside the daemon, a front controller receives each event, identifies its type, and dispatches it to a pipeline of handlers ordered by priority. Handlers execute sequentially, and any handler can block the action by returning the appropriate response. The architecture follows a strategy pattern: adding a new handler means dropping a Python class into the right directory. The front controller and dispatch logic stay unchanged.

The performance benefit is a nice bonus. After the initial warmup, the daemon achieves sub-millisecond response times per hook invocation, compared to 10-50ms per native process spawn. But the real value is the development workflow: you get a proper programming environment for hook logic instead of shell scripts.

Why Python

The choice of Python was deliberate. We evaluated several languages before settling on it, and the decision came down to a specific combination of requirements that no other language satisfied as well.

The daemon needs to combine library code (the built-in handlers shipped with the project) with project-level code (custom handlers written for a specific codebase). That means the language needs to support clean extensibility without a compilation step. TypeScript was a strong candidate, but it requires a build pipeline to compile to JavaScript, or you code in plain JavaScript and lose type safety entirely. Neither option is appealing for something that enforces code quality rules.

Python hits the sweet spot. It has type annotations with tools like mypy for static analysis, but no mandatory build step. You edit a handler file and the daemon picks it up on restart. It is universally available on Linux environments, which is where most Claude Code users run their sessions. And the ecosystem around testing, linting, and quality assurance is mature: pytest, ruff, mypy, and the rest of the Python toolchain are battle-tested and well understood.

For project-level handlers specifically, this matters. A team writing custom enforcement rules for their codebase needs a language that is already on their machine, does not require additional toolchain setup, and supports proper software engineering practices out of the box. Python checks all three boxes.

Developing Hooks with Claude Code

This is the feature that changes everything. The daemon's handlers are Python files in a directory. Claude Code can read, modify, and test them directly[src]. You can ask Claude to write a new blocking pattern, add a test case, or debug unexpected behaviour. Then restart the daemon and verify. No external tooling, no context switching, no session restarts.

The workflow looks like this: you notice a pattern you want to catch (say, Claude keeps trying to use sed instead of the Edit tool). You tell Claude to write a handler that blocks it. Claude creates the handler Python file, writes tests for it, and runs them. You restart the daemon. Now try the scenario again, and the handler blocks the action. If the blocking message needs tweaking, you ask Claude to update it. Restart, verify, done.

The tool you use to edit code becomes the tool you use to improve the hooks that govern how you edit code.

Because handlers are Python classes with proper type annotations, you get real test-driven development. The daemon ships with 6,255 tests and enforces 95% code coverage as a minimum requirement[src]. The same standard applies to handlers you write for your own project. Write a failing test that reproduces the scenario, implement the handler to make it pass, then restart and confirm it works in practice. Catch regressions before restarting, not after.

This is a sharp contrast to native hooks where "testing" means triggering the exact scenario in a live session and hoping you got it right. With the daemon, you can validate handler logic against dozens of edge cases in seconds, without Claude Code running at all.

Built-in Handlers

The daemon ships with 48 production handlers across 10 event types[src]. These cover the most common guardrails for AI-assisted development. You can enable or disable each one independently through the YAML configuration.

Safety handlers (priority 10-20) block genuinely dangerous operations. The destructive git handler prevents force pushes, hard resets, and branch deletions. The sed blocker encourages the Edit tool instead of sed, which provides better audit trails. The absolute path enforcer catches hardcoded paths that break across environments.

Code quality handlers (priority 25-35) enforce development standards. The QA suppression handler prevents comments like // eslint-disable, # type: ignore, or // @ts-ignore across 11 programming languages. The TDD enforcement handler requires test files to exist before source files can be created. The lock file protection handler prevents editing generated lock files.

Workflow handlers (priority 36-55) provide contextual guidance. The plan workflow handler assists with project planning conventions. The pipe blocker prevents piping expensive commands to tail or head, which loses information through truncation. The web search year handler corrects year references in search queries to use the current year. The git context injector adds repository status to every prompt.

Beyond PreToolUse, the daemon handles other lifecycle events. SessionStart handlers detect container environments with confidence scoring and check for daemon version updates. PreCompact handlers archive conversation state before context compaction. UserPromptSubmit handlers inject git status into every prompt for situational awareness.

Language-Aware Validation

One of the daemon's more sophisticated features is language-aware validation through a strategy pattern implementation. The QA suppression handler covers 11 programming languages: Python, JavaScript, TypeScript, PHP, Go, Rust, Java, Ruby, Kotlin, Swift, and C#[src].

Each language has its own strategy that understands language-specific conventions. Consider QA suppression detection as an example. In Python, suppression comments look like # type: ignore or # noqa. In TypeScript, they look like // @ts-ignore or // eslint-disable. In PHP, they look like // @phpstan-ignore or // phpcs:ignore. In Go, they look like //nolint.

A naive approach would maintain a single list of patterns and hope they match across languages. The strategy pattern instead delegates to language-specific handlers that know the correct patterns, comment syntax, and suppression mechanisms for their language. When the daemon detects a file write, it identifies the language from the file extension and applies the corresponding strategy.

This design makes the system extensible. Adding support for a new language means implementing a new strategy class that follows the established protocol. The front controller and dispatch logic remain unchanged. Existing strategies are unaffected. The same pattern applies to TDD enforcement, where each language strategy knows the correct test file conventions and testing framework expectations.

Project-Level Handlers

The built-in handlers cover common patterns, but every project has its own conventions. The daemon supports project-level handlers in a .claude/project-handlers/ directory, auto-discovered on daemon restart and co-located with their tests[src].

A CLI scaffolding command creates the directory structure with an example handler and tests. Validation and test commands let you verify handlers load correctly and pass their test suites before deploying them. Because project handlers follow the same Python class pattern as built-in handlers, everything you learn writing built-in handlers transfers directly.

The configuration also supports tag-based filtering, so you can enable only the handlers relevant to your tech stack. Available tags include safety, tdd, python, typescript, php, go, and others. A Python project can disable PHP-specific handlers, and a TypeScript project can skip Go-related rules. This keeps the handler pipeline lean and relevant.

Getting Started

The daemon is released under the MIT licence and available on GitHub[src]. Installation takes about 30 seconds with the AI-assisted installer. Copy a curl command into Claude Code and it handles the rest: cloning the repository, creating a virtual environment, running the installer, and verifying everything works.

The core configuration lives in a single YAML file at .claude/hooks-daemon.yaml, which you track in version control. This means your entire team shares the same hook configuration automatically. A typical configuration looks like this:

hooks-daemon-config.yaml
version: "2.0"

daemon:
  idle_timeout_seconds: 600
  log_level: INFO

handlers:
  pre_tool_use:
    destructive_git:
      enabled: true
      priority: 10
    sed_blocker:
      enabled: true
      priority: 10
    tdd_enforcement:
      enabled: true
      priority: 25
    british_english:
      enabled: true
      priority: 60
      mode: warn
    enable_tags: [python, typescript, safety, tdd]
    disable_tags: [ec-specific]

project_handlers:
  enabled: true
  path: .claude/project-handlers

Each handler can be enabled or disabled independently. Priorities control execution order. Tag-based filtering lets you pick the handlers relevant to your stack. The daemon auto-discovers both built-in and project-level handlers on restart.

The project itself requires Python 3.11 or newer and runs on Linux and macOS. Dependencies are installed automatically. The codebase uses strict MyPy type checking throughout.

Conclusion

Claude Code hooks are essential for maintaining control over AI-assisted development workflows. They provide the deterministic guardrails that probabilistic models cannot guarantee on their own. But the native implementation makes hooks hard to develop, hard to test, and hard to iterate on.

The hooks daemon fixes this by running as an independent process that you control separately from Claude Code. Restart it without losing your session. Write handlers in Python with proper types, tests, and abstractions. Use Claude Code itself to develop and refine the hooks that govern your development workflow. The 48 built-in handlers give you a solid starting point, and the project-level handler system lets you extend it with your own rules.

We built this tool because we needed it for our own development work. Every Edmonds Commerce Claude Code session runs the hooks daemon, and the handlers you see in the repository reflect real problems we encountered and solved across production projects. We open-sourced it because we believe the entire Claude Code community benefits from better tooling around hook management.

If you want a structured, testable, extensible way to manage Claude Code hooks, explore the project on GitHub[src]. The README covers installation, configuration, and the full handler catalogue.

About Edmonds Commerce

We design and build developer tooling and infrastructure for modern development teams. With over 18 years of software engineering expertise, we help organisations adopt AI-enhanced development practices that improve productivity without sacrificing code quality. Contact us to discuss how we can help your team work more effectively with AI development tools.

Ready to eliminate your technical debt?

Transform unmaintainable legacy code into a clean, modern codebase that your team can confidently build upon.