Appendix A - 17. Lifecycle hooks (interpose on the agent runtime's events)
The Structure of Lifecycle hooks (interpose on the agent runtime's events) — its shape at a glance:
The runtime fires the hook on a named event; the hook's payload is either a hard block that denies the action or soft guidance re-injected into the agent's context — the firing is hard, the payload's force varies.
flowchart LR
Event([Runtime lifecycle event]) --> Hook{{Bound hook}}
Hook -->|block payload| Deny([Action denied])
Hook -->|guidance payload| Inject([Guidance re-injected])
Inject --> Decide[Agent decides]
Accessible description: a runtime lifecycle event fires a bound hook whose payload is either a hard block that denies the action or soft guidance re-injected into the agent's context for it to weigh; the firing is guaranteed by the runtime, the payload's force is the design choice.
Projected from the catalogue entry lifecycle-and-observability / Lifecycle hooks (interpose on the agent runtime's events).
On this page: Intent · Motivation · Applicability · Structure · Sample Code · Consequences · Example use within DocAble · Related Patterns
Intent
Intent — Bind a script to the agent runtime's lifecycle events (turn-stop, pre-compaction, session-start, before-a-tool-call) so a step the operator keeps omitting at runtime fires deterministically, whether or not anyone remembered it.
Motivation
Some recurring failures live not in the code an agent writes but in the loop that drives it: the operator (a human, or an orchestrator agent) skipping a step at a predictable moment. Ending a turn with ratified work still queued. Compacting context without first writing a hand-off. Opening a session without reading the alert backlog. Editing outside the sanctioned worktree. A lint can't reach these: there is no source artifact to analyze, and the omission happens at runtime, in the loop itself. A house-rule ("remember to…") only aims a probabilistic operator, and it rots, because the step gets skipped exactly when attention is thin. The failure recurs every session.
Applicability
- A runtime that exposes lifecycle events, plus a registration surface. Without the seam there is nothing to hook.
- A cheap check. The hook fires on every occurrence of its event; an expensive one stalls the loop and creates pressure to disable it.
- Fail-open for guidance hooks. A bug in the hook must not brick the session; only the deliberate block is fail-closed.
- A named, recurring omission. Reach for a hook once a soft reflex has demonstrably failed to hold, not on first sight; a hook manufactured for a one-off is pure overhead.
Structure
The Structure diagram appears at the top of this page.
Sample Code
The hook splits the two halves a lint fuses: the runtime guarantees the firing, and the payload is either a hard veto or soft guidance. The reflex case — hard delivery of soft guidance — makes the aiming deterministic without swapping judgment for machinery. Validate the hook's output against the runtime's actual schema, or a wired-but-dead hook is silently dropped on every fire.
from dataclasses import dataclass
@dataclass
class HookResult:
decision: str # "block" | "guide" | "allow" — the runtime's real contract shape
message: str = ""
def on_turn_stop(work_queued: bool, pool_has_room: bool) -> HookResult:
if work_queued and pool_has_room: # a "guide" payload: aim, don't compel
return HookResult("guide", "ratified work remains and the pool is under cap — keep going")
return HookResult("allow") # default silent: a false nudge is worse than a miss
def on_pre_edit(path: str, worktree_root: str) -> HookResult:
if not path.startswith(worktree_root): # a "block" payload: deny the unsafe action
return HookResult("block", f"edit outside the sanctioned worktree: {path}")
return HookResult("allow")
VALID_DECISIONS = {"block", "guide", "allow"} # build-time check: reject any other shape
Consequences
- It fires on every event, not only when needed. A turn-stop hook runs at every stop, including the ones where nothing was owed; the check must stay cheap or the tax is constant and resented.
- A guidance hook can be ignored. Its payload is soft: the agent may read the re-injected text and proceed anyway. It aims deterministically; it does not compel. Only the blocking variant compels.
- A buggy hook is a loop-level outage. A blocking hook that misfires, or a guidance hook that crashes without fail-open, stalls the whole session; the blast radius is the loop, not one commit.
- It can nag. A hook that fires on a state it misreads ("work still queued" when the operator is legitimately blocked on a decision) produces repeated false prompts, and a signal that cries wolf gets tuned out.
Example use within DocAble
- A turn-stop hook that refuses to let the loop rest while ratified work remains and the worker pool is under its cap (hard delivery of soft guidance): it re-prompts, the agent decides.
- A turn-stop self-check hook that, at most once per long window, re-arms the operator's reflex to convert a recurring failure into a durable mechanism, the operate→harden handoff fired as a runtime event. It ships a firing-telemetry log and a yield query correlating its nudges against real conversions, and biases its payload hard toward silence. Its first organic firing closed the whole loop on itself: the telemetry had shown the convert-a-recurrence-into-a-mechanism discipline was reached for ~never (its knowledge applied ambiently instead); the hook turned that dormant reflex into a deterministic nudge; the nudge produced the discipline's first real invocation; and that invocation routed a failure recurring all session into a mechanism: the yield query's first positive correlation, and the keep-signal that says don't pull the hook.
- A pre-compaction hook that writes a hand-off before context is compacted, so in-flight state survives the summarization.
- A session-start hook that runs the session-start ritual (reading the unconsumed alert backlog) before the first action of a resumed or compacted session.
- A before-a-tool-call guard: the hard-block variant denies an edit outside the sanctioned worktree; a guidance variant surfaces unresolved alerts before a dispatch.
- A build-time output-conformance check validating every wired hook's output against the runtime's actual schema, so a hook emitting a shape the runtime silently drops (wired-but-dead) is a build error, not an invisible no-op.
Related Patterns
- Counterpart — pre-commit-hook: both are hooks, but that one fires on a commit and guards what gets written; this one fires on a runtime lifecycle event and guards what the loop does. The named axis is commit-content vs runtime-loop.
- See also — cron-alerts-gate: a before-a-tool-call hook is one delivery surface for its "an unresolved high-severity alert blocks new work" rule. The gate supplies the state; the hook fires the check at the moment of action.
- Specialized by — reflection-facet-substrate: what you build once a second reflection hook appears. A single hook re-arms one omitted reflex; the substrate consolidates many policy-reflection facets over one shared tempo budget (≤1 emission/window), so N soft nudges don't compound into the alarm fatigue the measured leash above warns about.
- See also (temporal complement) — dynamic-context-injection: the feed-forward twin. Injection pushes the governing constraints into an agent at dispatch, before it acts; a tempo-gated reflection hook pulls the operator back to the same kind of policy at turn-tempo, while or after acting. Forward-priming versus backward-reflection: the same "right policy at the right lifecycle moment" move, mirrored in time.
- See also (placement) — semantic-level-enforcement: a runtime lifecycle event is the rung that judgment picks for an operator-loop omission; this entry is a mechanism placed by it.
- Layer — it sits at the agent runtime, upstream of the commit → cron → merge-train → deploy staircase; it governs the operator driving that staircase, not the work flowing through it.