ccppccpp home
Browse the documentation

Plugins · CPO

Work

The work ledger: which changes in the repository are Claude's, what a shell command wrote, and how to undo it.

Many rules need to know which changes are Claude's: refuse to write into a tree holding someone else's uncommitted work, or refuse to end a turn while Claude's own work is uncommitted. The engine keeps one work ledger per session, shared by every policy, exposed as work in CEL and templates.

The work object

Field
trackedthe ledger is active for this project
touchedevery path Claude claimed
minedirty paths whose current content is exactly what Claude left
foreigndirty paths that are not mine: yours, another session's, or Claude's work edited since
uncommitteddirty paths Claude changed, even if edited since: what Claude should commit
callnull outside a tracked tool use, else { tool, foreign, changed, written, restored }

call.foreign holds the foreign paths at the tool's call; changed and written (changed and still dirty) are filled at its tool.result or tool.error; restored lists what restore did.

What the engine records

WhenWhat
tool.call of Bash or PowerShellthe content of every dirty path (stored as git blobs, up to 16 MiB each), and which were foreign
tool.call of Write, Edit, MultiEdit, NotebookEditthe same ids, and the tool's paths
tool.result / tool.errorwhich paths the call changed; those left dirty become Claude's claims
tool.denied, or a blocked tool.callthe pending call is dropped
session.endthe ledger is deleted
  • A shell call can change any path; a file tool only its own, so your concurrent edits are not claimed.
  • A claim records the exact content Claude left. If you edit the file afterwards, it is no longer mine.
  • A claim lives while its path is dirty: once committed, stashed or checked out, it is dropped.
  • Paths are what git status reports, relative to the root; ignored files are invisible.

Tracking costs a few git calls per writing tool, so it only runs when an active policy references work., uses restore, or has a module check. Outside a repository, work.tracked is false and every list is empty.

What a shell wrote

Before a command runs, only >, >> and tee are visible, with unknown content. A heredoc into cat, sed -i, cp or python -c goes unseen. So at the tool.result of a Bash or PowerShell call, the engine derives one file.write per regular file the command changed, with applied: true, via: shell, the content before the call and after it. Every file.write policy thus sees shell writes once they happened.

on: [file.write]
when: { builtin: max-lines, with: { max: 400 } }
then: [restore, block]   # a file tool is refused up front; a file a shell wrote is restored

restore

- restore, or - restore: { paths: <CEL list> }, at tool.result or tool.error only, puts each path back as it was at the call: the stored content of a path that was dirty, the HEAD version of one that was clean (deleted if HEAD lacks it). Claude and the user get one warning listing each path. Defaults: work.call.written, or the event's file on file.write.

A file.write that has not happened yet (applied: false) has nothing to restore. Anywhere else restore throws, so the action is blocked. ccpp cpo trace only reports what it would restore.

Example: respect other people's work, commit before stopping

dirty-tree.policy.md
on: [file.write]
when: { cel: "event.data.via != 'shell' && size(work.foreign) > 0" }
dirty-tree-shell.policy.md
on: [tool.result, tool.error]
when: { cel: "work.call != null && size(work.call.foreign) > 0 && size(work.call.written) > 0" }
then: [restore, block]
commit.policy.md
on: [turn.end]
when: { cel: "size(work.uncommitted) > 0" }

A command that only cleans the tree (git commit, git stash, git checkout --) writes nothing dirty, so it passes.

Edit this page on GitHub