ccppccpp home
Browse the documentation

Plugins · CPO

Rules

A policy is a deterministic rule in four parts: on, let, when, then. Conditions are checks and CEL expressions.

on: [file.write]                                   # events
let:                                               # variables
  lines: "lines(event.data.after)"
  writes: "state.session.?writes.orValue(0)"       # persistent state
when:                                              # composable conditions
  all:
    - cel: "lines > 300"
    - not: { builtin: path-match, with: { glob: "**/*.gen.ts" } }
then:                                              # effects, in order
  - block
  - set: { state.session.writes: "writes + 1" }

on: events

The events the policy applies to, such as file.write, shell.exec or turn.end. The full list is on the Events page and in ccpp cpo catalog.

let: variables

name: <CEL>. Each binding is evaluated lazily, once per policy and event, so a guard such as event.data.after != null && lines > 3 never evaluates lines on a null. Bindings may use each other; a cycle is an error. event, file, state, facts and CEL keywords are reserved names.

CEL sees these objects:

VariableContent
event{ name, seq, parent, hook, session, data }; whole JSON numbers are CEL int
file{ path, relative } for events with a path, else null
statestate.session, state.turn, state.toolUse: maps of JSON values
factsin then and where: the event data merged with the facts of the checks that held
gitthe project's repository, read once per hook call (State and git)
workClaude's changes this session (Work)

Functions: lines(s), glob(path, pattern), readFile(path) (null if missing), exists(path), diff(snapshot), diff(before, after), snapshotFile(snapshot, path), s.replace(old, new), s.replaceRegex(pattern, replacement) ($1 for groups), plus standard CEL: size, matches, split, exists, all, map, filter, optionals. Extensions add their own.

A missing map key is an error in CEL. Read state with a default: state.session.?n.orValue(0). In YAML double quotes \n is a real newline, which CEL rejects inside a string: use lines(…) or single quotes.

when: conditions

A condition is one check or a combinator. Leaves are any check kind.

when: { builtin: max-lines, with: { max: 400 } }
when: { cel: "event.data.command.contains('--force')" }
when: { all: [<condition>, …] }      # every one holds
when: { any: [<condition>, …] }      # at least one holds
when: { not: <condition> }
Check kindExampleFrom
builtin{ builtin: command-ban, with: { commands: [rm] } }Builtins
cel{ cel: "lines(event.data.after) > 400" }core
module{ module: ./electron-http-only.policy.ts }SDK
tree-sitter, ast-grep, config{ tree-sitter: { query: '…', onlyNew: true } }Code rules
  • A check holds when it finds what it looks for. For a builtin, where: <CEL> keeps it only when the expression holds over its facts.
  • Logic is three-valued. A check that cannot run (content unknown before a shell write, a missing language server) is unknown. all is false as soon as one child is false, any true as soon as one is true; otherwise unknown propagates and the policy is skipped.
  • Children run in the written order and short-circuit: put expensive checks last.
  • Without when, the policy applies to every planned event.

then: effects

Effects run in order once when holds. The default is [block].

Effect
block, deny, ask, stop, escalateRefusals and confirmations (Actions).
rewrite, allowCorrect the tool input, skip the permission prompt.
message, info, warn, steerText for the user or Claude (Messages).
log, notify, webhookA JSON line, a desktop notification, an HTTP POST.
set, unset, snapshotWrite state, record the work tree (State and git).
restoreUndo a tool use's changes (Work).

Outcomes

OutcomeWhen
blocka refusal ran
askthe user is asked to confirm
applyeffects ran, none refused
allowwhen was false
skipwhen was unknown
errora check or effect threw or timed out: the action is blocked and the policy's state changes are rolled back

ccpp cpo trace prints the outcome of every evaluation.

Edit this page on GitHub