ccppccpp home
Browse the documentation

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:

.claude/policies/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.
  • scope takes the globs of a policy's scope. A policy applies where its own scope and its profile's both hold. Editing scope in profile.yml applies at once, params at the next update.

To give each area of a monorepo its own stack, add one profile per area.

Layering

PrecedenceDirectory
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

~/.ccpp/config.yml
cpo:
  profiles:
    source: https://acme-ccpp.s3.eu-west-3.amazonaws.com/profiles/
    publicKey: ed25519:3q2+7w…
  1. Create a key pair once, and keep the private key out of the bucket (a CI secret):

    ccpp cpo profile keygen acme-profiles.pem
  2. Validate the profiles, write every index.json and sign it, then upload the tree as is:

    ccpp cpo profile index ./profiles --sign acme-profiles.pem

    --sign defaults to $CPO_PROFILES_SIGNING_KEY; --check only verifies that the indexes are current.

  3. 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).

Edit this page on GitHub