ccppccpp home
Browse the documentation

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.

PrecedenceDirectory
1.claude/policies/ in the projectYour 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

.claude/policies/file-size.policy.md
---
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

FieldTypeDefaultMeaning
descriptionstring, 200 maxrequiredOne-line summary shown by ccpp cpo view and ccpp cpo list.
onevent listrequiredEvents the policy applies to.
letname → CELnoneLazy variables (Rules).
whenconditionalwaysA check or all / any / not (Rules).
theneffect list[block]Actions run in order.
priorityinteger0Higher runs first; ties run cheaper conditions first, then by id.
scopeglob listwhole projectProject areas it applies to (Scope).
paths / excludeglob listall / noneFile filters for file.* and search events.
agentsstring listallAgent types it applies to; the main thread is main.
stopOnFirstbooleanfalseStop evaluating after this policy blocks.
enabledbooleantruefalse in a project file disables a user policy of the same id.
claudeCanEditbooleantruefalse: Claude may not change, override or disable it.
userMessagestringnoneExtra message shown to the user when the policy blocks.
idkebab-casefile nameOptional; must match the file name.
format22Format version. Format 1's check and action became when and then.
x-*anynoneFree extension fields. Any other unknown field is an error.

Placeholders

The body and the text of effects are templates. {{ path }} reads a value:

PlaceholderContent
{{ 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 editors

lint 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.

Edit this page on GitHub