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

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

Example use within DocAble

Contents
© James C. Davis, 2026–present