Plugins · CPO
Profile sources
Several profiles in one project, where profiles come from, and how an organisation publishes and signs its own.
Several profiles
Each scope (project or user) records its profiles in one profile.yml:
profiles:
api: # ccpp cpo profile add fastify-5 --name api --scope apps/api
id: fastify-5
version: 5.12.5
scope: [apps/api]
params: { packageManager: pnpm }
files: [{ path: api-f5-p15-import-cycle.policy.md, sha256: … }, …]
web: # ccpp cpo profile create web no-console --scope apps/web
scope: [apps/web]
policies: [no-console]- Every downloaded profile writes its files to the scope's one
profile/directory. - Its name defaults to its id and keeps the published file names. Any other name prefixes them (
api-<file>), so one profile can be added twice under two names. Two profiles writing the same file are refused: give one a--name. - A local profile downloads nothing: it names and scopes policies of the scope's own directory.
scopetakes the globs of a policy'sscope. A policy applies where its own scope and its profile's both hold. Editingscopeinprofile.ymlapplies at once,paramsat the nextupdate.
To give each area of a monorepo its own stack, add one profile per area.
Layering
| Precedence | Directory |
|---|---|
| 1 | .claude/policies/ |
| 2 | .claude/policies/profile/ (project profiles) |
| 3 | ~/.ccpp/policies/ |
| 4 | ~/.ccpp/policies/profile/ (user profiles) |
A policy with the same id higher up overrides a profile's, and enabled: false there disables it, unless the
profile's policy says claudeCanEdit: false and Claude is the one writing. Once set, edits are reported:
profile view shows each profile's integrity, and hooks and ccpp doctor warn about edited files and files no
profile installed.
Where profiles come from
The source is, in order: --source <dir|url>, $CPO_PROFILES, cpo.profiles.source in ~/.ccpp/config.yml,
else the profiles bundled with ccpp. An http(s):// source is read with plain GETs, so any static host works,
such as an S3 bucket.
profiles/
├── categories.yml # categories, in display order (authored)
├── index.json (+ .sig) # categories + one summary per profile (generated, signed)
└── <category>/<profile>/
├── profile.yml # metadata, extends, requires, params (authored)
├── *.policy.md # the policies, plus any module they use (authored)
└── index.json (+ .sig) # files with size and sha256, policies (generated, signed)Enterprise sources
cpo:
profiles:
source: https://acme-ccpp.s3.eu-west-3.amazonaws.com/profiles/
publicKey: ed25519:3q2+7w…-
Create a key pair once, and keep the private key out of the bucket (a CI secret):
ccpp cpo profile keygen acme-profiles.pem -
Validate the profiles, write every
index.jsonand sign it, then upload the tree as is:ccpp cpo profile index ./profiles --sign acme-profiles.pem--signdefaults to$CPO_PROFILES_SIGNING_KEY;--checkonly verifies that the indexes are current. -
Pin the printed public key in every client's
config.yml, distributed by MDM or dotfiles.
With a pinned key, every index read from any source must carry a valid Ed25519 signature: an unsigned,
tampered or foreign index is refused. Indexes carry each file's size and sha256, so the signature covers every
policy. Mark the rules no one may weaken claudeCanEdit: false (Protection).