ccppccpp home
Browse the documentation

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:

EffectSame asThe 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:

.claude/policies/migrations-hint.policy.md
---
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 its text.
  • text sends the text alone with a Title header, as ntfy expects.
  • $VAR and ${VAR} in url and headers read the environment, so secrets stay out of policy files.
  • A request has 3 seconds. A failure never blocks: the user is warned. ccpp cpo trace sends nothing.

Edit this page on GitHub