Plugins · CPO
Policy files
One policy per .policy.md file: where they live, which fields the frontmatter takes, what the body is for.
Location and precedence
One policy per <id>.policy.md file. The id is the file name, in kebab-case.
| Precedence | Directory | |
|---|---|---|
| 1 | .claude/policies/ in the project | Your project's own policies. |
| 2 | .claude/policies/profile/ | Policies installed by project profiles. |
| 3 | ~/.ccpp/policies/ ($CCPP_HOME/policies/) | Your user policies, for every project. |
| 4 | ~/.ccpp/policies/profile/ | Policies installed by user profiles. |
A policy with the same id higher up overrides the lower one; enabled: false there disables it. Claude changing
any of these files needs your confirmation, and is refused for claudeCanEdit: false policies
(Protection).
Format
---
description: Keep source files under 400 lines
priority: 100
on: [file.write]
paths: ["src/**/*.{ts,tsx}"]
when: { builtin: max-lines, with: { max: 400 } }
x-owner: platform
---
`{{ file.relative }}` would have {{ facts.lines }} lines (max {{ params.max }}). Split it.The body is the message of block and deny, and of a bare warn, info or steer. It may be empty
when no effect renders it, for instance then: [snapshot].
Frontmatter
| Field | Type | Default | Meaning |
|---|---|---|---|
description | string, 200 max | required | One-line summary shown by ccpp cpo view and ccpp cpo list. |
on | event list | required | Events the policy applies to. |
let | name → CEL | none | Lazy variables (Rules). |
when | condition | always | A check or all / any / not (Rules). |
then | effect list | [block] | Actions run in order. |
priority | integer | 0 | Higher runs first; ties run cheaper conditions first, then by id. |
scope | glob list | whole project | Project areas it applies to (Scope). |
paths / exclude | glob list | all / none | File filters for file.* and search events. |
agents | string list | all | Agent types it applies to; the main thread is main. |
stopOnFirst | boolean | false | Stop evaluating after this policy blocks. |
enabled | boolean | true | false in a project file disables a user policy of the same id. |
claudeCanEdit | boolean | true | false: Claude may not change, override or disable it. |
userMessage | string | none | Extra message shown to the user when the policy blocks. |
id | kebab-case | file name | Optional; must match the file name. |
format | 2 | 2 | Format version. Format 1's check and action became when and then. |
x-* | any | none | Free extension fields. Any other unknown field is an error. |
Placeholders
The body and the text of effects are templates. {{ path }} reads a value:
| Placeholder | Content |
|---|---|
{{ policy.* }} | The policy: id, description… |
{{ event.* }} | The event: name, data… |
{{ file.* }} | path and relative for events with a path. |
{{ facts.* }} | What the checks found, such as lines for max-lines. |
{{ params.* }} | The with parameters of the builtin. |
{{ vars.* }} | The let bindings. |
{{ state.* }}, {{ git.* }} | State and git. |
Validate
ccpp cpo lint # every discovered policy file
ccpp cpo lint .claude/policies/x.policy.md
ccpp cpo schema # JSON Schema of the frontmatter, for editorslint exits 1 on errors and reports the field and line at fault. An invalid policy is ignored by hooks, and
ccpp doctor fails on it.