Hook Event Catalog
Complete reference for CompozyOS hook events, payload schemas, patch schemas, sync eligibility, and example payloads.
CompozyOS hooks fire at named runtime points such as session creation, prompt assembly, tool execution, permission decisions, automation runs, and the agent's own context compaction. Each event has a fixed payload type and a fixed patch type. A hook reads the payload from stdin, decides whether it applies, and writes a JSON patch to stdout.
Runtime Placement
Rendering diagram…
Reading This Catalog
| Column | Meaning |
|---|---|
| Event | The exact value used in a hook declaration. |
| Sync | yes means a declaration may set mode = "sync". async only means sync declarations are rejected. |
| Payload | The JSON object type delivered to the executor. |
| Patch | The JSON object type the executor may write to stdout. Empty stdout means an empty patch. |
| Fires when | The runtime point that dispatches the event. |
Most session-scoped payloads include common session fields:
| Field | Meaning |
|---|---|
event | Current event name. |
timestamp | Dispatch timestamp. |
session_id, session_name, session_type | Session identity and kind when available. |
agent_name | Agent attached to the session or runtime operation. |
workspace_id, workspace | Workspace identity and root path when available. |
turn_id | Current turn when the event is turn-scoped. |
Control patches use deny and deny_reason. A deny patch stops the sync hook chain. For
pre-operation control points, CompozyOS returns the event-specific denial error to the caller.
Session Events
| Event | Sync | Payload | Patch | Fires when |
|---|---|---|---|---|
session.pre_create | yes | SessionPreCreatePayload | SessionCreatePatch | Before CompozyOS creates a session record. |
session.post_create | yes | SessionPostCreatePayload | SessionPostCreatePatch | After CompozyOS creates a session record. |
session.pre_resume | yes | SessionPreResumePayload | SessionPreResumePatch | Before CompozyOS resumes an existing session. |
session.post_resume | yes | SessionPostResumePayload | SessionPostResumePatch | After CompozyOS resumes an existing session. |
session.pre_stop | yes | SessionPreStopPayload | SessionPreStopPatch | Before CompozyOS stops a session. |
session.post_stop | yes | SessionPostStopPayload | SessionPostStopPatch | After CompozyOS stops a session. |
session.runtime_recovery.started | async only | SessionRuntimeRecoveryStartedPayload | AuthoredContextObservationPatch | Before one automatic recovery attempt. |
session.runtime_recovery.succeeded | async only | SessionRuntimeRecoverySucceededPayload | AuthoredContextObservationPatch | After the replacement runtime accepts the turn. |
session.runtime_recovery.exhausted | async only | SessionRuntimeRecoveryExhaustedPayload | AuthoredContextObservationPatch | After the final automatic recovery attempt fails. |
session.attention.changed | async only | SessionAttentionChangedPayload | SessionAttentionObservationPatch | After a canonical session-attention edge is committed. |
Session lifecycle patches can set session_name, session_type, agent_name, workspace_id,
or workspace. session.pre_create cannot change a session into or out of the daemon-owned
coordinator and spawned types. After creation, hooks cannot change the session type or workspace
identity. Pre-create, pre-resume, and pre-stop hooks can also deny the operation.
session.attention.changed is observation-only and carries from, to, class, and at; hook
failure does not change the committed session state.
Runtime recovery hooks are also observation-only. Their payload carries the turn, attempt limit,
runtime generation, failure kind, and a bounded redacted failure detail. Hook failure never blocks
or rolls back recovery.
Input And Prompt Events
| Event | Sync | Payload | Patch | Fires when |
|---|---|---|---|---|
input.pre_submit | yes | InputPreSubmitPayload | InputPreSubmitPatch | Before a user message is submitted to the agent turn. |
prompt.post_assemble | yes | PromptPayload | PromptPatch | After CompozyOS assembles the final prompt and before it reaches the agent. |
input.pre_submit patches can replace message, append or replace context_blocks, or deny the
submission. Its attachments field exposes only read-only id, name, mime, bytes, and kind
metadata; file contents never enter the hook payload or patch. Its read-only mentions field
(InputMentionMetadata: kind, token, target, label, start, end) lists the
mentions CompozyOS verified; after a message rewrite only mentions
whose exact token is still present survive, and a patch cannot add one. prompt.post_assemble
patches can replace prompt, adjust context_blocks, or deny the assembled prompt.
Event Persistence Events
| Event | Sync | Payload | Patch | Fires when |
|---|---|---|---|---|
event.pre_record | async only | EventPreRecordPayload | EventPreRecordPatch | Before an ACP/runtime event is persisted. |
event.post_record | async only | EventPostRecordPayload | EventPostRecordPatch | After an ACP/runtime event is persisted. |
Event-record hooks are observation hooks. Their payload includes record_type, sequence, and raw
content. Their patch type accepts labels, but the current dispatch path does not mutate the
stored event record from that patch.
Automation Events
| Event | Sync | Payload | Patch | Fires when |
|---|---|---|---|---|
automation.job.pre_fire | yes | AutomationJobPreFirePayload | AutomationFirePatch | Before a scheduled or manually triggered job dispatches. |
automation.job.post_fire | async only | AutomationJobPostFirePayload | AutomationObservationPatch | After a job hands work to a session or run. |
automation.trigger.pre_fire | yes | AutomationTriggerPreFirePayload | AutomationFirePatch | Before a trigger dispatches an automation run. |
automation.trigger.post_fire | async only | AutomationTriggerPostFirePayload | AutomationObservationPatch | After a trigger hands work to a session or run. |
automation.run.completed | async only | AutomationRunCompletedPayload | AutomationObservationPatch | After an automation run completes successfully. |
automation.run.failed | async only | AutomationRunFailedPayload | AutomationObservationPatch | After an automation run fails. |
Automation pre-fire patches can replace prompt or set cancel = true. Observation patches are
empty structs and are used only to record hook execution.
Autonomy Events
Autonomy hooks cover coordinator bootstrap, task-run leases, and safe child-session spawn. They do not replace the authority of task-run leases or spawn policy. A hook can deny or narrow the exposed pre-operation payload where a control patch exists, but it cannot mint raw claim tokens, complete a task run through prompt text, bypass TTL, widen permissions, or bypass child depth and child count limits.
Coordinator
| Event | Sync | Payload | Patch | Fires when |
|---|---|---|---|---|
coordinator.pre_spawn | yes | CoordinatorPreSpawnPayload | CoordinatorSpawnPatch | Before CompozyOS creates a managed coordinator session. |
coordinator.spawned | yes | CoordinatorSpawnedPayload | CoordinatorObservationPatch | After a managed coordinator session is created. |
coordinator.decision | yes | CoordinatorDecisionPayload | CoordinatorObservationPatch | When a coordinator records a semantic coordination decision. |
coordinator.stopped | yes | CoordinatorStoppedPayload | CoordinatorObservationPatch | After a managed coordinator session stops. |
coordinator.failed | yes | CoordinatorFailedPayload | CoordinatorObservationPatch | After coordinator bootstrap or lifecycle handling fails. |
CoordinatorSpawnPatch may deny coordinator spawn or replace agent_name, provider, and
model. Observation patches are empty and record hook execution only.
Task
These are task-level state transitions, deliberately outside the run-level task.run.* namespace —
they describe the task's blocking and escalation state, not a run's lifecycle.
| Event | Sync | Payload | Patch | Fires when |
|---|---|---|---|---|
task.blocked | yes | TaskBlockedPayload | TaskObservationPatch | After a typed block is created on a task. |
task.unblocked | yes | TaskUnblockedPayload | TaskObservationPatch | After a block is cleared explicitly or by transient expiry. |
task.needs_attention | yes | TaskNeedsAttentionPayload | TaskObservationPatch | After the unblock-loop breaker escalates a task. |
task.recovered | yes | TaskRecoveredPayload | TaskObservationPatch | After an operator or agent recovers an escalated task. |
TaskBlockedPayload and TaskUnblockedPayload carry block_id, kind, reason, opaque
details, and (on unblock) cleared_at / clear_note. TaskNeedsAttentionPayload and
TaskRecoveredPayload carry the escalation reason / recovery note plus at. All four share the
TaskContext fields (task_id, workspace_id, task_status, actor/origin identity, and — when a
run was parked — run_id and claim_token_hash). TaskObservationPatch is empty; these hooks may
annotate or deny but cannot mint claim tokens or bypass the breaker. The raw bearer lease token never
appears in these payloads. See the block lifecycle
for the operator and agent surfaces that drive these transitions.
Task Run
| Event | Sync | Payload | Patch | Fires when |
|---|---|---|---|---|
task.run.enqueued | yes | TaskRunEnqueuedPayload | TaskRunObservationPatch | After a task run is enqueued and its audit event is committed. |
task.run.pre_claim | yes | TaskRunPreClaimPayload | TaskRunPreClaimPatch | Before a task-run claim transaction commits. |
task.run.post_claim | yes | TaskRunPostClaimPayload | TaskRunObservationPatch | After a task-run claim and audit event commit. |
task.run.lease_extended | yes | TaskRunLeaseExtendedPayload | TaskRunObservationPatch | After a token-fenced heartbeat extends a task-run lease. |
task.run.lease_expired | yes | TaskRunLeaseExpiredPayload | TaskRunObservationPatch | After a task-run lease reaches expiry. |
task.run.lease_recovered | yes | TaskRunLeaseRecoveredPayload | TaskRunObservationPatch | After lease recovery requeues or finalizes expired work. |
task.run.released | yes | TaskRunReleasedPayload | TaskRunObservationPatch | After a token-fenced task-run lease release. |
task.run.completed | yes | TaskRunCompletedPayload | TaskRunObservationPatch | After a token-fenced task-run completion. |
task.run.failed | yes | TaskRunFailedPayload | TaskRunObservationPatch | After a token-fenced task-run failure. |
TaskRunPreClaimPatch may deny the claim, add required capabilities, or raise priority_min. It
cannot remove task-run requirements or change the selected run after the claim transaction commits.
The raw bearer lease token never appears in these hook payloads.
Spawn
| Event | Sync | Payload | Patch | Fires when |
|---|---|---|---|---|
spawn.pre_create | yes | SpawnPreCreatePayload | SpawnCreatePatch | Before CompozyOS creates a child session from an agent-facing request. |
spawn.created | yes | SpawnCreatedPayload | SpawnObservationPatch | After a child session is created. |
spawn.parent_stopped | yes | SpawnParentStoppedPayload | SpawnObservationPatch | When parent-stop cleanup reaps a child session. |
spawn.ttl_expired | yes | SpawnTTLExpiredPayload | SpawnObservationPatch | When TTL expiry reaps a child session. |
spawn.reaped | yes | SpawnReapedPayload | SpawnObservationPatch | After child-session reap cleanup completes. |
SpawnCreatePatch may deny the spawn or narrow the child request by replacing agent_name,
spawn_role, ttl_seconds, or child_permissions. The runtime still rejects permission widening,
coordinator-role spawn, unauthorized cross-workspace spawn, and invalid TTL/depth/child caps.
spawn.pre_create also runs before every subagent delegation. Those
payloads carry spawn_role: "subagent" and a subagent object with title, role, and
task_chars (the task length, never the task text), plus read-only isolation and worktree_id.
A patch that rebinds isolation or the worktree is rejected with capability_denied. The same deny and narrowing rules apply; a patch
that sets a TTL for the subagent role is rejected with capability_denied, because subagents have no
TTL.
Subagent
| Event | Sync | Payload | Patch | Fires when |
|---|---|---|---|---|
subagent.settled | no | SubagentSettledPayload | SubagentObservationPatch | When a subagent reaches a terminal status, after its record is committed. |
subagent.settled is observe-only: a hook cannot change the status, the result, or the parent's
wake. It fires once per subagent for both delegated and provider-native subagents. Its payload
additively includes isolation and optional worktree facts (identity, branch, base, Git and PR
snapshot); unavailable settlement facts remain absent.
| Field | Meaning |
|---|---|
event, workspace_id | The event name and the subagent's workspace. |
subagent_id | The subagent record id (sub-…). |
parent_session_id, child_session_id | The delegating session and the subagent's own session; child_session_id is null for provider-native subagents. |
origin | delegated or provider_native. |
status | completed, failed, canceled, or interrupted. |
runtime | provider and model the subagent ran on. |
duration_ms | Time from delegation to settle. |
isolation | shared or worktree. |
worktree | Optional checkout identity, branch, base and available Git/PR settlement facts; unknown fields are omitted. |
Agent Process Events
| Event | Sync | Payload | Patch | Fires when |
|---|---|---|---|---|
agent.pre_start | yes | AgentPreStartPayload | AgentStartPatch | Before CompozyOS starts the ACP agent process. |
agent.spawned | yes | AgentSpawnedPayload | AgentSpawnedPatch | After the agent process is spawned. |
agent.crashed | yes | AgentCrashedPayload | AgentCrashedPatch | When the agent process exits unexpectedly. |
agent.stopped | yes | AgentStoppedPayload | AgentStoppedPatch | After the agent process stops. |
agent.pre_start patches can replace command, args, or cwd, or deny process startup. The
post-start lifecycle patches accept labels for observation.
Turn And Message Events
| Event | Sync | Payload | Patch | Fires when |
|---|---|---|---|---|
turn.start | yes | TurnStartPayload | TurnStartPatch | When an agent turn starts. |
turn.end | yes | TurnEndPayload | TurnEndPatch | When an agent turn ends. |
message.start | yes | MessageStartPayload | MessageStartPatch | When a streamed message begins. |
message.delta | async only | MessageDeltaPayload | MessageDeltaPatch | For streaming message deltas. |
message.end | yes | MessageEndPayload | MessageEndPatch | When a streamed message finishes. |
Turn patches can set labels and include the common deny fields. A turn deny stops the hook chain;
the current turn events do not return a user-visible denial error. Message patches can replace
role, delta_type, or text; only message.start and message.end can run sync hooks.
Tool Events
| Event | Sync | Payload | Patch | Fires when |
|---|---|---|---|---|
tool.pre_call | yes | ToolPreCallPayload | ToolCallPatch | Before a tool call executes. |
tool.post_call | yes | ToolPostCallPayload | ToolResultPatch | After a tool call succeeds. |
tool.post_error | yes | ToolPostErrorPayload | ToolPostErrorPatch | After a tool call fails. |
Tool payloads include tool_call_id, the canonical tool_id, read_only, and tool_input.
Pre-call patches expose tool_id, read_only, and tool_input, plus the common deny fields.
Post-call and post-error patches can replace title, tool_result, or error; deny
fields on post events stop the hook chain but do not undo an already completed tool result.
Permission Events
| Event | Sync | Payload | Patch | Fires when |
|---|---|---|---|---|
permission.request | yes | PermissionRequestPayload | PermissionRequestPatch | Before a permission decision resolves. |
permission.resolved | async only | PermissionResolvedPayload | PermissionResolvedPatch | After a permission decision resolves. |
permission.denied | async only | PermissionDeniedPayload | PermissionDeniedPatch | After a permission denial resolves. |
permission.request patches can set decision, decision_class, and reason, or deny the
request. CompozyOS rejects escalation patches that try to turn a blocked, denied, or rejected decision
into an allow decision; those hook runs are recorded as rejected. Request and resolved payloads
include tool_input; denied payloads omit it so rejected input is not exposed to async hooks.
Context Events (experimental)
| Event | Sync | Payload | Patch | Fires when |
|---|---|---|---|---|
context.pre_compact | yes | ContextPreCompactPayload | ContextPreCompactPatch | On the first update CompozyOS observes for an agent compaction id. |
context.post_compact | yes | ContextPostCompactPayload | ContextPostCompactPatch | At the first terminal status (completed, failed, or cancelled) for that id. |
Context events are observation-only and experimental: they follow the unstable ACP compaction
contract and may change. CompozyOS never compacts a session itself; the agent owns its context
window. These hooks fire only when the agent honors the ACP compaction capability, once per
compaction id (post_compact at most once), whether a person requested the compaction with
Compact now or the agent started it on its own.
They never fire for an agent that does not honor the capability. When the first update CompozyOS
observes for an id is already terminal, both events fire for it, context.pre_compact first.
Both events run after CompozyOS has recorded the observed compaction snapshot, so the hook sees a
compaction that already happened. A failing hook, including a required one, is logged as a
session: hook dispatch failed warning and never changes the compaction or the turn.
The two events share one payload shape and one patch shape. In the daemon they are the single types
ContextCompactionPayload and ContextCompactionPatch; ContextPreCompactPayload,
ContextPostCompactPayload, and the matching ContextPreCompactPatch and ContextPostCompactPatch
are aliases of them. Hook introspection (compozy hooks events, GET /api/hooks/events) names the
event-specific types, and the Go and TypeScript SDKs generate both the event-specific types and the
shared pair with identical fields.
| Field | Events | Meaning |
|---|---|---|
event, timestamp | both | The event name and the time of the agent update that triggered the dispatch. |
| Session fields | both | The common session fields above, plus profile_id, worktree_id, acp_session_id, state, soul_snapshot_id, soul_digest, created_at, and updated_at when available. |
turn_id | both | The turn that carried the compaction. |
compaction_id | both | The compaction's identifier. Both events for one compaction carry the same value. |
trigger | both | requested when the compaction happened on a turn CompozyOS started to ask for it (Compact now, compozy session compact, the compozy__session_compact tool, or a Goal); agent for every other. |
status | post | The first terminal status: completed, failed, or cancelled. Absent on context.pre_compact. |
summary | post | The agent's summary text, when it sent one. |
error | post | The agent's error text, when it sent one. |
The patch has one optional field, labels, a map of string keys to string values. It is accepted
and applied as a no-op annotation: it does not change the payload, a later hook's input, or the
compaction, and the hook run keeps the patch only while the daemon log level is debug. The patch
is decoded strictly, so a patch with any other field (deny, deny_reason, summary, or
context_blocks, for example) is invalid. The hook run is recorded as failed with the error
hooks: decode patch for hook "<name>": json: unknown field "<field>". A sync hook that is not
required is skipped and the chain continues, a required sync hook ends the chain, and an async
hook logs hook.dispatch.async_failed. In every case the compaction continues, because a hook
cannot veto, delay, or rewrite an agent's compaction.
Example Payloads And Patches
Deny a risky permission request
permission.request payload:
{
"event": "permission.request",
"timestamp": "2026-04-16T15:04:05Z",
"session_id": "sess_9f4a",
"agent_name": "coder",
"workspace_id": "ws_checkout",
"turn_id": "turn_42",
"request_id": "perm_123",
"action": "write",
"resource": "/Users/you/src/checkout/.env",
"decision": "ask",
"decision_class": "write",
"tool_input": {
"path": "/Users/you/src/checkout/.env"
},
"tool_call": {
"id": "toolu_123",
"kind": "edit_file",
"title": "Edit .env"
}
}PermissionRequestPatch denying the operation:
{
"deny": true,
"deny_reason": "Secrets files require manual review.",
"decision": "deny",
"decision_class": "write",
"reason": "Protected file pattern matched."
}Add workspace context to a prompt
prompt.post_assemble payload:
{
"event": "prompt.post_assemble",
"timestamp": "2026-04-16T15:04:05Z",
"session_id": "sess_9f4a",
"agent_name": "coder",
"workspace_id": "ws_checkout",
"workspace": "/Users/you/src/checkout",
"turn_id": "turn_42",
"input_class": "user",
"prompt": "Review the current diff.",
"context_blocks": [
{
"kind": "workspace",
"text": "Repository: checkout"
}
]
}PromptPatch replacing the assembled prompt:
{
"prompt": "[Workspace: checkout]\n\nReview the current diff."
}Cancel an automation fire
automation.job.pre_fire payload:
{
"job_id": "job_daily_review",
"job_name": "weekday-review",
"agent_name": "reviewer",
"workspace_id": "ws_checkout",
"prompt": "Review the repository.",
"schedule": {
"mode": "cron",
"expr": "0 9 * * 1-5"
},
"attempt": 1
}AutomationFirePatch canceling dispatch:
{
"cancel": true
}Observe a finished compaction
A config declaration that runs for compactions the agent started on its own:
[[hooks.declarations]]
name = "audit-agent-compaction"
event = "context.post_compact"
mode = "sync"
timeout = "3s"
command = "./hooks/audit-compaction"
[hooks.declarations.matcher]
compaction_trigger = "agent"context.post_compact payload (ContextPostCompactPayload):
{
"event": "context.post_compact",
"timestamp": "2026-10-08T14:02:19Z",
"session_id": "sess_9f4a",
"agent_name": "coder",
"workspace_id": "ws_checkout",
"workspace": "/Users/you/src/checkout",
"state": "active",
"created_at": "2026-10-08T13:10:02Z",
"updated_at": "2026-10-08T14:02:19Z",
"turn_id": "turn_42",
"compaction_id": "c1f0b8f4-6a52-4d7e-9b0c-2f3a8d1e5c77",
"trigger": "agent",
"status": "completed",
"summary": "Kept the checkout refactor plan and the open review comments."
}ContextPostCompactPatch attaching a label. It is the only field the patch accepts, and it has no
effect on the compaction:
{
"labels": {
"audited": "true"
}
}Introspection Surfaces
Use the CLI for local inspection:
compozy hooks events --family permission
compozy hooks events --sync-only -o jsonUse the API when building tooling:
GET /api/hooks/events?family=permission&sync_only=true
GET /api/hooks/catalog?workspace=checkout&event=permission.request
GET /api/workspaces/ws_checkout/hooks/runs?session=sess_9f4a&event=permission.request&last=20Related references:
Subagent Events
| Event | Sync | Payload | Patch | When |
|---|---|---|---|---|
subagent.settled | no | SubagentSettledPayload | SubagentObservationPatch | A subagent reaches its terminal state. |
This family is observe-only. The payload includes workspace_id, subagent_id,
parent_session_id, nullable child_session_id, origin, status, runtime.provider,
runtime.model, and duration_ms, in addition to the common event metadata.
Delegation also uses spawn.pre_create with spawn_role: "subagent" and a typed
subagent object containing title, role, task_chars, and read-only isolation and
worktree_id. Denying that hook, rebinding isolation/worktree, or rewriting a TTL for this role
rejects delegation with capability_denied.