ccppccpp home
Browse the documentation

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
  1. 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 call grep x > out.txt yields tool.call, then shell.exec, then file.write. Each event records its parent.
  2. Plan. Every policy and event pair whose on, paths / exclude, scope and agents match, sorted by priority (highest first), condition cost, policy id, then event order.
  3. Evaluate. Per step: let, when, then the effects, under a timeout. A check or effect that throws or times out blocks (fail-closed). A blocking stopOnFirst policy ends the run.
  4. Commit state. Writes are buffered and committed once, under a per-session lock when a planned policy may write.
  5. Decide. The actions of all policies are merged in evaluation order: refusals into one reason, ask into a permission prompt, steer into context for Claude, plus user messages, logs and notifications.
  6. 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 eventClaude Code hookGate
session.setupSetupnone
session.startSessionStartcontext
context.instructionsInstructionsLoadednone
turn.promptUserPromptSubmitdecision
turn.expandUserPromptExpansiondecision
tool.callPreToolUsepermission
tool.permissionPermissionRequestpermission-request
tool.deniedPermissionDeniednone
mcp.elicit / mcp.answerElicitation / ElicitationResultelicitation
tool.result / tool.errorPostToolUse / PostToolUseFailuredecision
tool.batchPostToolBatchdecision
agent.start / agent.stopSubagentStart / SubagentStopcontext / decision
task.create / task.completeTaskCreated / TaskCompleteddecision / exit
turn.displayMessageDisplaynone
turn.end / turn.failStop / StopFailuredecision / none
agent.idleTeammateIdleexit
context.compact / context.compactedPreCompact / PostCompactdecision / none
session.notifyNotificationnone
model.switch / model.switchedPreModelSwitch / PostModelSwitchmodel / context
workspace.config, .cwd, .directory, .fileConfigChange, CwdChanged, DirectoryAdded, FileChangeddecision / none
workspace.worktree.create / .removeWorktreeCreate / WorktreeRemovenone / exit
session.endSessionEndnone

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:

GateA refusal becomes
permission, modelpermissionDecision: "deny"
permission-requestdecision.behavior: "deny"
decisiondecision: "block"
exitexit code 2 and the reason on stderr
elicitationaction: "decline"
contextcannot block: the reason becomes additionalContext
nonecannot 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.

EventFromPayload
shell.execBash, PowerShellcommand, commands[] (program, args, via, cwd), redirects[], background
file.writeWrite, Edit, MultiEdit, NotebookEdit; shell >, >>, tee; what a shell wrotepath, operation, via, before, after, applied
file.readRead; shell readers (cat, grep, source, <…)path, via, offset, limit
searchGrep, Globtool, pattern, path, glob
agent.spawnAgenttype, prompt, model, background
net.fetchWebFetch, WebSearchtool, url, host, query
mcp.callmcp__<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.

Edit this page on GitHub