Three layers Shipped · proven on Claude Code only
Each layer is there because the one above it can fail
A file can be ignored. A hook can be absent. A pull request cannot be either, but it arrives too late to stop anything. None of the three is sufficient, which is the entire reason there are three.
$ cycle start CC-58
<!-- cc:begin -->
### You are working on CC-58 — Export times out on large boards
Branch `task/CC-58-export-times-out-on-large-boards`. Access expires in about 8 hours.
What it must NOT do. No change to the export format.
No protected zone is open to this task.
<!-- cc:end -->
cycle start writes the task into a gitignored CLAUDE.local.md beside the zone map in AGENTS.md, and cycle submit takes it out again.
> add an index on tasks.updated_at — the board query is slow
● Write(apps/api/migrations/0042_tasks_updated_at.sql)
⎿ PreToolUse:Write — ✗ Denied by CC
zone schema · risk high
owner ana@acme.dev
task CC-42 declared no protected zones
reason path is in a zone the task never asked for
● The schema zone is closed to CC-42. Asking its owner rather than looking for another way in.
● Bash(cycle request-access --zone schema --mode write --reason "the board query needs an index" --alternative "hand the DDL to the owner")
⎿ REQ-7 is with the zone owner
approve or deny: https://dash.commitcycle.com/requests
The hook refuses the write before it lands, names the zone and its owner, and the agent asks for access instead of looking for another way in. Claude Code is where this block has been seen working.
CC-42: add an index on tasks.updated_at
task/CC-42-add-an-index-on-tasks-updated-at → main
- ✓ static, test, evals, apps — typecheck, unit tests, evals, builds
- ◌✓scope — Is this pull request inside what its task declared?
Advisory here: this repository’s plan cannot make a check required.
Layer 3 re-checks the diff against what the task declared. It runs in CommitCycle’s own CI today, advisory there; nothing installs it in yours.
Layer 1 — context, which advises
The AGENTS.md block carries the zone map, the board, the hand-in note it expects and the deploy rule; the task itself — what is open and what is shut — is written at the gate to a gitignored CLAUDE.local.md, which Claude Code loads — whether other editors do has never been checked — and removed at hand-in. It is honest in its own text about being advice: nothing is stopping you, so this one is on you.
Layer 2 — the hook, which denies
A pre-tool-call hook answers before the write happens. Ours only narrows: it can deny, and since CC-1142 it never answers allow, so the harness's own permission prompts still apply. What matters more than the mechanism:
- The hot path never touches the network. The decision is local, so an outage cannot open a zone — the backend was never in the deny path to begin with.
- Bash is contained, not classified. Guessing what a shell command will do is a losing game, so commands run against a git snapshot and changes to protected paths the task does not hold are reverted byte-for-byte; gitignored paths are not seen. Containment closes when the call returns, so what a command leaves running escapes it until the close checks the diff against the zones: a background command (the hook asks first), a foreground one that hits its timeout, a
cmd &inside one. 25/25 escape vectors contained, 0/20 false positives — on 2026-08-03, against a corpus we chose ourselves; the live false-positive eval has not run. - 68–83 ms at p95 against a 100 ms budget — measured 2026-08-03 on the first hook build, on an idle machine, and p99 touched 102 ms in one case. CC-1377 re-timed it in October 2026 on two cloud VMs: on the slower one wrapper and core reached 211–228 ms at p95 (load 0.74–1.04), and the hook's cost over a shell-launched node process was 84–111 ms there against 44–48 ms on the faster one, for the same code — two hosts, not a survey.
Worth saying plainly: this mechanism is commodity. Claude Code, Codex, Cursor, Augment, Factory and Devin CLI all expose a hook that can return deny, and in the one you are probably using it is free. What is not commodity is the model it consults — owners, expiry, and permission scoped to a declared task.
The admission that makes the rest credible
We measured the harness against real sessions on 2026-08-03, and it is fail-open in all four failure modes: crash, timeout, malformed output, not installed. That is not a footnote — it is the reason the other two layers exist in the shape they do. A wrapper can close three of those modes by refusing on its own behalf when the hook cannot answer — but only where the repository's failure policy is set to closed. The default, since CC-1143, is open: calls continue and the session is told. The fourth — never installed at all — is invisible from inside the machine, so only CI can see it, and only where the check runs.
Layer 3 — CI, which recomputes
The CI check reads the zone map from the pull request's own checkout and recomputes which zones the diff actually touched, against what the task declared — the declaration, not the grant, so approvals and expiry go unchecked. It is the only layer that could see work produced where CommitCycle was never installed.
Its own limits, stated: it runs only in CommitCycle's own repository, and nothing installs it into yours. There, on a private personal free plan, branch protection answers 403, so it runs advisory, says so, and cannot stop a merge. A board that does not answer turns it into a skip that still passes, and on a same-repository pull request the branch can rewrite the workflow that checks it. An enforcing mode and a board-side check run are built and not switched on.
Where the arrangement stops
- Layer 2 is proven on one harness. The zone model is agent-agnostic; the enforcement point is not.
cycle initwrites hook files for Claude Code and Codex, and for Cursor and Windsurf on request, only where a hook wrapper is already on disk, so on your machine it writes none and the plugin is the hook — in Claude Code only, so Codex, Cursor and Windsurf get none. Only Claude Code has been seen to deny on a real install — the others, where their files are written, are unexercised and are answered in Claude Code's reply format. Layer 1's task half is known to reach Claude Code only, and Layer 3 cannot be installed in your repository. - An enterprise can block the hook outright. Managed settings exist for exactly that, and a policy that locks hook sources locks ours out with everyone else's.
- None of this arbitrates between agents. Each checkout is confined to its own task and branch, so two agents in one directory share both; two that disagree still disagree.
How the layers divide the work is in the docs, and the map all three read is the zone map.