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:
---
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.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 }));| Function | Adds |
|---|---|
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.outputparsePolicy 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.