Plugins · CPO
Events
How a Claude Code hook becomes ordered CPO events, and which events a policy can watch.
The pipeline
hook input → normalize → ordered CPO events → plan → evaluate (let → when → then) → commit state → decide → answer- Normalize. The hook's common fields become a typed
session; its own fields are validated and mapped to one camelCase lifecycle event. Derivers then expand it into action events: a Bash callgrep x > out.txtyieldstool.call, thenshell.exec, thenfile.write. Each event records itsparent. - Plan. Every policy and event pair whose
on,paths/exclude,scopeandagentsmatch, sorted by priority (highest first), condition cost, policy id, then event order. - Evaluate. Per step:
let,when, then the effects, under a timeout. A check or effect that throws or times out blocks (fail-closed). A blockingstopOnFirstpolicy ends the run. - Commit state. Writes are buffered and committed once, under a per-session lock when a planned policy may write.
- Decide. The actions of all policies are merged in evaluation order: refusals into one reason,
askinto a permission prompt,steerinto context for Claude, plus user messages, logs and notifications. - Answer. The decision is rendered in the protocol of that hook. Input that cannot be parsed blocks the action; unknown future hooks are allowed silently.
Lifecycle events
| CPO event | Claude Code hook | Gate |
|---|---|---|
session.setup | Setup | none |
session.start | SessionStart | context |
context.instructions | InstructionsLoaded | none |
turn.prompt | UserPromptSubmit | decision |
turn.expand | UserPromptExpansion | decision |
tool.call | PreToolUse | permission |
tool.permission | PermissionRequest | permission-request |
tool.denied | PermissionDenied | none |
mcp.elicit / mcp.answer | Elicitation / ElicitationResult | elicitation |
tool.result / tool.error | PostToolUse / PostToolUseFailure | decision |
tool.batch | PostToolBatch | decision |
agent.start / agent.stop | SubagentStart / SubagentStop | context / decision |
task.create / task.complete | TaskCreated / TaskCompleted | decision / exit |
turn.display | MessageDisplay | none |
turn.end / turn.fail | Stop / StopFailure | decision / none |
agent.idle | TeammateIdle | exit |
context.compact / context.compacted | PreCompact / PostCompact | decision / none |
session.notify | Notification | none |
model.switch / model.switched | PreModelSwitch / PostModelSwitch | model / context |
workspace.config, .cwd, .directory, .file | ConfigChange, CwdChanged, DirectoryAdded, FileChanged | decision / none |
workspace.worktree.create / .remove | WorktreeCreate / WorktreeRemove | none / exit |
session.end | SessionEnd | none |
The table is in lifecycle order, which is also the order of evaluation when one hook yields several events.
Gates
How a refusal reaches Claude Code depends on the hook:
| Gate | A refusal becomes |
|---|---|
permission, model | permissionDecision: "deny" |
permission-request | decision.behavior: "deny" |
decision | decision: "block" |
exit | exit code 2 and the reason on stderr |
elicitation | action: "decline" |
context | cannot block: the reason becomes additionalContext |
| none | cannot block: the reason becomes a systemMessage |
WorktreeCreate is never routed: its hook replaces git and must print a path.
Action events
Derived from tool.call (and, for shell writes, from tool.result), at the same stage.
| Event | From | Payload |
|---|---|---|
shell.exec | Bash, PowerShell | command, commands[] (program, args, via, cwd), redirects[], background |
file.write | Write, Edit, MultiEdit, NotebookEdit; shell >, >>, tee; what a shell wrote | path, operation, via, before, after, applied |
file.read | Read; shell readers (cat, grep, source, <…) | path, via, offset, limit |
search | Grep, Glob | tool, pattern, path, glob |
agent.spawn | Agent | type, prompt, model, background |
net.fetch | WebFetch, WebSearch | tool, url, host, query |
mcp.call | mcp__<server>__<tool> | server, tool, input |
For edits, after is the file rebuilt with the edit applied; it is null when unknown (a shell redirection,
before it runs). What a shell wrote is checked once it ran.
Shell parsing
commands comes from a side-effect-free shell parser: lists, pipelines, quotes, $( ), backticks, heredocs,
and programs run by sudo, env, xargs, timeout, sh -c, eval, find -exec… via lists the
enclosing programs: grep in sudo xargs grep has via: [sudo, xargs]. cwd is described on the
Scope page.
Every event is { name, seq, parent, hook, session, data }. ccpp cpo catalog lists events, payloads and
everything else a policy can use.