Plugins · CPO
Builtins
Ready-made checks for when: size limits, banned commands, counters, path globs and protected paths.
A builtin is used as when: { builtin: <name>, with: { … }, where: <CEL> }. It holds when it finds what
it looks for, and reports facts that where, the body ({{ facts.* }}) and the effects can read.
| Builtin | Events | Parameters | Facts |
|---|---|---|---|
max-lines | file.write | max, onlyGrowth | lines, linesBefore |
max-bytes | file.write | max, onlyGrowth | bytes, bytesBefore |
command-ban | shell.exec | commands, allowIn | program, args, via |
counter | any | key, max, scope | count |
path-match | any | glob | path |
protected-paths | file.write, shell.exec | paths | path, protected, program |
ts-no-any, comments | file.write | onlyNew… | see Code rules |
lsp-diagnostics, lsp-query | file.write… | severity, query… | see Language servers |
Size limits: max-lines, max-bytes
when: { builtin: max-bytes, with: { max: 4096, onlyGrowth: true } }Both hold when the write leaves the file over max: lines (a trailing newline ends the last line) or UTF-8
bytes. linesBefore / bytesBefore measure the file before the write, null for a new file.
onlyGrowth: true judges the change, not the file: the write holds only if it ends over the limit and
makes the file larger. A file already over the limit may shrink or keep its size.
Before → after, max: 4096 | default | onlyGrowth |
|---|---|---|
| 3193 → 4097 | holds | holds |
| new file → 5000 | holds | holds |
| 5000 → 5100 | holds | holds |
| 5000 → 4900 | holds | does not hold |
| 3193 → 3500 | does not hold | does not hold |
Content unknown before the write (shell redirections, tee) skips the check; what a shell wrote is checked
once it ran. In CEL, the same measures are lines(s) and size(bytes(s)).
Banned programs: command-ban
---
description: Install with pnpm
on: [shell.exec]
when: { builtin: command-ban, with: { commands: [npm, yarn], allowIn: [npx] } }
---
This project uses pnpm.Holds when any parsed command runs one of commands, compared by program name, wherever it sits: after &&,
in a pipeline, inside sudo, xargs or sh -c. allowIn lists enclosing programs under which the command is
allowed. Combine with where to look at arguments:
when: { builtin: command-ban, with: { commands: [rm] }, where: "facts.args.exists(a, a == '-rf')" }Counting: counter
on: [net.fetch]
when: { builtin: counter, with: { key: fetches, max: 20, scope: turn } }
then: [ask]Adds one to state.<scope>.<key> (scope session by default, or turn, toolUse) each time it is
evaluated, and holds once the count exceeds max. Put it last in an all so it only counts matching events.
Globs: path-match
when: { not: { builtin: path-match, with: { glob: ["**/*.gen.ts", "**/generated/**"] } } }Holds when the event's project-relative path matches glob, one glob or a list.
Protected paths: protected-paths
when: { builtin: protected-paths, with: { paths: [infra/prod, ~/.config/acme] } }
then: [ask]Holds when a write reaches one of paths (files or directories; absolute, ~/… or project-relative), and when a
shell command may change one: a redirection, rm / mv / chmod -R on it or a directory above it, sed -i,
git checkout -- <path>, a ccpp cpo profile command, or any argument of a program that is not a known reader
(cat, grep…) or inspector (ls, stat…).
It is deliberately broad, so pair it with ask rather than deny where false alarms matter. CPO's own
protection is built on it.