ccppccpp home
Browse the documentation

Plugins · CPO

SDK and extensions

Extend CPO in TypeScript: policy modules, builtins, effects, CEL functions, events and derivers.

@ccpp/cpo is both the SDK (file format, registry, discovery, engine) and the ccpp cpo command.

Policy modules

When a condition is easier to write as code, point when to a module next to the policy:

.claude/policies/electron-http-only.policy.md
---
description: Renderer code calls the main process over HTTP only
on: [file.write]
when: { module: ./electron-http-only.policy.ts }
---
Use the HTTP bridge, not ipcRenderer.
.claude/policies/electron-http-only.policy.ts
import { allow, definePolicy, deny } from "@ccpp/cpo";
 
export default definePolicy({
  evaluate({ event }) {
    const { after } = event.data as { after: string | null };
    return after?.includes("ipcRenderer.") ? deny({ found: "ipcRenderer" }) : allow();
  },
});

In the ccpp binary, a module imports @ccpp/cpo, @ccpp/cpo-code and @ccpp/cpo-lsp from the binary itself: no node_modules is needed next to it. Return allow(), deny(facts) (the check holds) or skip(reason).

Builtins, effects and CEL functions

import { defaultRegistry, defineBuiltin, defineExtension, discoverPolicies, activePolicies, z } from "@ccpp/cpo";
 
const noTodo = defineBuiltin({
  name: "no-todo",
  description: "Denies TODO comments.",
  events: ["file.write"],
  params: z.strictObject({ marker: z.string().default("TODO") }),
});
 
const registry = defaultRegistry.use(defineExtension({ name: "acme", builtins: [noTodo] }));
const policies = activePolicies(discoverPolicies({ projectDir: process.cwd(), registry }));
FunctionAdds
defineCheckKind({ key, description, cost, schema, validate?, evaluate })a new when leaf; cost orders evaluation
defineBuiltin({ name, description, events, params, evaluate })a builtin; params validates with
defineEffect({ name, params, blocks?, writesState?, run })a then effect returning actions
defineCelFunction({ signature, description, handler })a synchronous CEL function
defineExtension({ name, checkKinds?, builtins?, effects?, celFunctions?, events?, derivers? })a bundle; registry.use() returns a new registry

Use the re-exported z so parameters share the SDK's zod instance.

Events and derivers

const registry = defaultRegistry.use(defineExtension({
  name: "acme",
  events: [defineEvent<"skill.use", { skill: string }>({ name: "skill.use", description: "…", group: "action", stage: 30 })],
  derivers: [defineDeriver<ToolRef, { skill: string }>({ name: "skill", from: "tool.call", to: "skill.use",
    derive: ({ data }) => (data.tool === "Skill" ? [{ skill: String(data.input["skill"]) }] : []) })],
}));
  • defineLifecycleEvent({ hook, gate, input, map, … }) binds a new Claude Code hook.
  • Derivers run in registration order, depth-first, up to depth 8.
  • Type new events with declare module "@ccpp/cpo" { interface CpoEventMap { "skill.use": … } }.

Running the engine

const result = await createEngine({ registry, policies, state: memoryStateStore() }).dispatch(hookInput);
// result.events, result.plan, result.evaluations, result.decision, result.output

parsePolicy and readPolicy return { id, file, policy, issues } with field and line; policyJsonSchema(registry) serves editors. Code and language-server rules expose their own SDKs, @ccpp/cpo-code and @ccpp/cpo-lsp.

Edit this page on GitHub