Plugins · CPO
Messages, logs and notifications
Effects that never block: text for the user or Claude, JSON logs, desktop notifications and webhooks.
Messages
message: { to: user | claude | both, level: info | warn, text? } never blocks. The text defaults to the
policy's body. Shortcuts:
| Effect | Same as | The user sees |
|---|---|---|
info / info: "…" | message: { to: user, level: info } | CPO ℹ … |
warn / warn: "…" | message: { to: user, level: warn } | CPO ⚠ … |
steer / steer: "…" | message: { to: claude } | nothing: Claude reads it as context |
steer is the way to guide Claude without refusing anything:
---
description: Remind Claude how migrations are made
on: [file.write]
paths: ["db/migrations/**"]
then: [steer]
---
Migrations are generated: run `pnpm db:generate` instead of writing SQL by hand.Claude receives context at session.start, turn.prompt, turn.expand, tool.call (and action events),
tool.result, tool.error, tool.batch, turn.end, agent.start, agent.stop and model.switched. At
turn.end, a steer keeps Claude working. Elsewhere the text is shown to the user and cpo lint warns.
Log
log appends one JSON line per action to $CPO_LOG_DIR/<session>.jsonl (default <tmp>/ccpp-cpo/log):
{ "time": "…", "session": "…", "hook": "PreToolUse", "agent": "main", "policy": "file-size",
"event": "file.write", "seq": 2, "outcome": "block", "text": "…", "data": { "lines": 450 } }Forms: log (the body), log: "template", log: { text?, data: { key: <CEL> } }. A failed write never
blocks: the user is warned. ccpp cpo trace never writes logs.
Notify
notify raises a desktop notification through the terminal: notify, notify: "template" or
notify: { title?, text? } (title Claude Code). It uses OSC 777 for Ghostty, Warp and urxvt, OSC 99 for
kitty, OSC 9 otherwise.
Webhook
webhook POSTs to a URL: webhook: <url> or { url, text?, title?, format: json | text, headers? }.
then: [block, { webhook: { url: "https://ntfy.sh/$CPO_NTFY_TOPIC", format: text, title: "CPO {{ policy.id }}" } }]json(default) sends{ time, session, hook, agent, policy, event, seq, outcome, title, text }; a Slack incoming webhook reads itstext.textsends the text alone with aTitleheader, as ntfy expects.$VARand${VAR}inurlandheadersread the environment, so secrets stay out of policy files.- A request has 3 seconds. A failure never blocks: the user is warned.
ccpp cpo tracesends nothing.