Caelan's Domain

Hooks — The Event System

aicagentsclaudehookseventsinternals

Created: March 29, 2026 | Modified: June 21, 2026

A session directory is the static record of what happened. A hook is the live machinery that runs while the pipeline is still moving — because the session can only describe what already finished.

Hooks are how cAgents reacts to events in real time: tracking which subagents are running, catching dangerous commands before they execute, saving state before the context window compacts, and coordinating teams of parallel agents. Sessions record. Hooks intervene.

Hooks themselves are a Claude Code primitive — Claude Code fires events at specific moments and runs whatever script you registered against each event. cAgents adds a launcher convention and a tree of scripts on top, but the underlying mechanism is Claude Code's. The full schema, payload shape, and event list live at the canonical reference (see Hooks in the official docs); Part 5 of the Claude Code guide walks through authoring one from scratch. This article is about what cAgents does with the primitive, not how the primitive itself works.

The .claude/hooks/ directory ships 32 .cjs files, and the count splits in a way worth keeping straight. 26 are unique registered hooks — each one wired against an event in settings.json. 3 more are dispatched Write|Edit sub-validators that never register directly; write-edit-dispatch.cjs runs them in-process when a file write or edit fires, so one registration fans out to several checks. The remaining three are plumbing: hook-utils.cjs (shared helpers), run-hook.cjs (the launcher every registered hook routes through), and eval-runner.cjs (a CLI for testing hooks outside a live session). Those 26 hooks fire across 18 of Claude Code's 24 event types — cAgents listens at most of the lifecycle, not all of it.


How a hook fires

Hooks are configured in .claude/settings.json. Each entry maps an event to a shell command, with an optional matcher that filters which tool triggers it. Claude Code pipes the event payload to stdin as JSON; the script reads it, does its one focused thing, and exits inside its timeout (5 seconds is the cAgents convention).

cAgents does not register each script directly. It registers a single launcher and passes the hook name as an argument:

"PreToolUse": [
  {
    "matcher": "Bash",
    "hooks": [
      {
        "type": "command",
        "command": "bash -c '... node \"$R/.claude/hooks/run-hook.cjs\" bash-validator'",
        "timeout": 5
      }
    ]
  }
]

The matcher narrows the trigger — this entry only fires on Bash. The timeout caps how long the hook can take before Claude Code gives up on it. The launcher (run-hook.cjs) dispatches to the right script under .claude/hooks/<name>.cjs. The prompts panel on the right walks you through wiring one of these into your own project end to end — the article frames the primitive, the panel is the reproducible build.

Hooks running as Node .cjs files via a launcher is a cAgents convention, not a Claude Code requirement. Claude Code will run any executable command. The launcher pattern keeps settings.json short when two dozen hooks share the same setup; for a one-off project hook, registering a script directly is simpler.

Where cAgents listens

cAgents wires hooks against 18 of the 24 events Claude Code exposes — most of the lifecycle, not all of it. The full event list and payload schema is canonical Anthropic territory (see Hooks in the official docs); the tables below are the cAgents-specific column — which script runs at each event, and what it does. The tables name the load-bearing hooks rather than enumerating all 26; the source tree has the rest.

Session and agent lifecycle

EventcAgents hookWhat it does
SessionStartsession-catchup.cjsRestores state from an incomplete prior session
Stopverify-completion.cjsChecks the pipeline wrote its completion summary
SubagentStartsubagent-tracker.cjsRegisters the agent in agent_tree.yaml

Safety and tool guards (PreToolUse / PostToolUse)

MatchercAgents hookWhat it does
Bashbash-validator.cjsBlocks dangerous shell commands
Write | Editsecret-detection.cjsScans writes for committed credentials, with a bounded head+tail size cap
Write | Editwrite-edit-dispatch.cjsOne registration that runs three sub-validators in-process
Write | Editcontroller-delegation-validator.cjsWarns when a controller writes an implementation file instead of delegating
Write | Edit | Bashapproval-gate.cjsEnforces approval for sensitive ops

The Write|Edit row for write-edit-dispatch.cjs is the one that explains the file-count arithmetic: it registers once but runs three sub-validators that never appear in settings.json themselves. That is why 32 files on disk amount to 26 registered hooks.

Context survival and team coordination

EventcAgents hookWhat it does
PreCompactpre-compact-save.cjsSaves mission and plan state before compression
PostCompactpost-compact-restore.cjsRe-injects the plan after compression
SessionStart (team)team-start.cjsBoots team coordination state
SessionEnd (team)team-stop.cjsCleans up team processes
TaskCompletedteam-task-complete.cjsTriggers the next wave
TeammateIdleteammate-idle-handler.cjsReassigns or stops idle teammates

Twenty-six small scripts, each focused on one job. The coordination layer that makes multi-agent pipelines reliable is the sum of them, not any single one.


Which hooks do the most work

Six worth reading top to bottom:

subagent-tracker.cjs

Every time a subagent spawns, this hook writes an entry to agent_tree.yaml in the session directory — id, type, parent, role, spawn timestamp. When the agent finishes, subagent-stop-tracker.cjs appends its completion summary alongside.

This is the hook that builds the audit trail Part 5 describes. Without it, the session would know what was requested and what state the pipeline reached, but not which agents did the work or in what order. Every entry in agent_tree.yaml exists because this hook wrote it.

pre-compact-save.cjs and post-compact-restore.cjs

Long pipelines push the conversation toward compaction — the model summarises older turns to free room for new ones. Compaction is useful, but lossy: which task is in progress, what's been completed, what files changed, what the plan says to do next can all blur together in the recap. That blur is drift — the pipeline honours its own plan less reliably the further it gets from the turn where the plan was set.

This pair is the goal-drift defence. pre-compact-save.cjs fires before compression and writes the current workflow state to disk — plan progress, active tasks, file change list, in-flight coordination data. After compression, post-compact-restore.cjs reads that state back and re-injects the mission and the plan. The agent might not remember the full conversation, but it knows exactly where it is in the plan and what to do next — the recap can smooth over detail without the pipeline losing the thread.

bash-validator.cjs

This hook intercepts every Bash command before execution. It checks the command against a set of safety rules and blocks destructive operations — rm -rf /, sudo invocations that mutate system state, the patterns that would damage the host or the project.

Agents run shell commands constantly: installing dependencies, running builds, checking file state, executing tests. Most are fine. The validator's job is to catch the rare ones that aren't. It's a PreToolUse hook, so it can return an error to the agent explaining why the call was blocked rather than letting the command go through.

secret-detection.cjs

This hook fires on Write|Edit and scans the content about to land on disk for credential patterns — API keys, tokens, private-key headers, connection strings with passwords in them. Catch it before the write, and the secret never reaches the file; catch it after, and it's already in your history.

The detail that keeps it inside the 5-second budget is the size cap: rather than regex the entire content of a large file, the hook reads a bounded head plus a bounded tail and scans those. Secrets in real code overwhelmingly live near the top (imports, config blocks) or get appended at the bottom; a head+tail window catches the common cases without letting a multi-megabyte write blow the hook's deadline. It's a deliberate trade — coverage for speed — and the right one for a guard that has to run on every file write without becoming the bottleneck.

controller-delegation-validator.cjs

This one enforces the architectural pattern that makes cAgents work. The pipeline is supposed to delegate — planners plan, executors execute, validators validate, and controllers coordinate without ever implementing. A controller is a capable Claude instance, though, so without guardrails it can quietly skip the handoff and write the code directly — collapsing the pipeline back into one-shot prompting.

controller-delegation-validator.cjs runs at Write/Edit time and warns the moment a controller tries to write an implementation file itself instead of handing the work to an executor. It's the architectural enforcement that keeps the multi-agent pattern honest under pressure.

verify-completion.cjs

This hook fires when an agent stops and checks whether execution_summary.yaml was written to the session directory. A properly completed pipeline always writes a summary — it's the last step before VALIDATED. If the file is missing, the pipeline ended abnormally: it was interrupted, errored out, or the agent stopped without finishing.

The hook flags the incomplete session so you know to investigate or resume. Just ask Claude what happened with that last run? and it can read the session files, diagnose the failure, and pick up where things stopped. Without this hook, a silently failed pipeline looks the same as a finished one.

The team-coordination set

/team runs several teammates inside one wave, and the lifecycle that keeps them in step is four hooks pulling together. team-start.cjs boots the shared coordination state when a team session begins. team-task-complete.cjs fires when a teammate finishes its slice and is the trigger that releases the next wave once the gate clears. teammate-idle-handler.cjs watches for a teammate that has gone quiet and either reassigns it or stops it so the wave doesn't stall on a worker with nothing left to do. team-stop.cjs cleans up the team's processes when the session ends so nothing leaks past it.

None of these is interesting on its own. Together they are why a parallel wave advances cleanly instead of deadlocking on the slowest or the stuck teammate — the synchronisation that Part 4 treats as given is these four hooks doing the bookkeeping.


Why one launcher beats many registrations

You may have noticed the registration above points at run-hook.cjs with the actual hook name as an argument, rather than at each script directly. That's deliberate.

Twenty-six separate command entries in settings.json would mean twenty-six paths, twenty-six timeouts, twenty-six places to keep convention in sync. The launcher is one command shape. It reads stdin, loads the session context, picks the right .cjs file, and runs it. Adding a hook becomes: write a new .cjs, register one more launcher entry. Nothing more.

For a project that doesn't already have the launcher wired, registering a script directly is the right pick — it's one less moving part. The launcher pays for itself once you have more than a handful of hooks, not before. The prompts panel covers both shapes and which to choose.

The hooks registered for your project are visible in .claude/settings.json. Plugin-supplied hooks appear alongside your own; you can add custom hooks against the same event names without disturbing the cAgents ones.

When you're done

The hook system is what turns cAgents from a collection of agents into an actual orchestration framework. Sessions record what already happened; hooks track, guard, save, restore, and coordinate while the pipeline is still moving. The session is the static record; the hooks are the live machinery that writes it.

This is the last part. The series started at installation and the four commands, ran through /designer, /run, and /team, turned over the session directories those commands leave behind, and ends here at the event layer underneath all of it. You now have the whole shape: the commands you type, the artifacts they write, and the hooks that hold the pipeline together between events. If you want to go deeper than any article can, the cAgents source is open — every hook named above is a file you can read.

The full series:

Part 1: Getting Started | Part 2: /designer | Part 3: /run | Part 4: /team | Part 5: Sessions | Part 6: Hooks