CLI
The command is cycle. It is not cc, because on every Unix machine cc is already the system C compiler, and a governance tool that shadows a compiler is one people uninstall.
It arrives two ways — inside the plugin, or from npm with npm i -g commitcycle. Every command below resolves its board the same way: the environment wins, and when the environment says nothing, the committed .zones/board.json answers.
Every command explains itself before it does anything. cycle <command> --help (or -h) prints what that command changes, and it is answered before anything else happens — no repository, board or network is needed to explain a command, so asking cannot be what runs it. The commands whose accidental run is expensive — submit, sync and verify among them — also refuse a flag they do not take, by name, and do nothing: a silently dropped --dry-run is a command that ran when somebody asked it to explain itself.
Getting connected
Section titled “Getting connected”| Command | Does |
|---|---|
cycle login |
Sign in to the board: an address, a six-digit code, done. More than one address is fine — each repository uses the one its organization knows |
cycle accounts |
Who this machine is signed in as, and which identity this repository uses |
cycle logout [email] |
End one identity for this board — at the board, then here — or all of them |
cycle pair |
Join this machine to a board: a code here, confirmed in your browser. Writes .zones/board.json — no name to type |
cycle unpair |
Revoke this machine’s key for this board and forget it. A machine key never expires, so this is the only way it stops |
cycle connect <ticket> |
The same, started from the board: paste the line it gave you |
More than one identity on one laptop is an ordinary case, not an edge one — two accounts on one machine covers which one answers where.
Both credentials are ended by the command that mints them, in one direction only: cycle logout ends a session and cycle unpair revokes a machine key. Pairing again undoes nothing — it mints a second key beside the first. Either can also be taken back from the console when the machine is gone or offline: sessions under Settings → Your sessions, machines under Settings → Connect a machine.
Setting a repository up
Section titled “Setting a repository up”| Command | Does |
|---|---|
cycle init [--yes] [--harness <names>] |
Set this repository up: propose zones, write the zone map and the agent context |
cycle doctor [--enforcement] |
Why is CC behaving like this? A diagnosis: it moves no task |
cycle init does not install the hook, and it does not write hook configuration unless a wrapper is already on this disk. Claude Code lets a write through when the script a hook names is missing, so config with nothing behind it is a repository that looks guarded and blocks nothing — worse than one that admits it, because you stop checking. The wall is the plugin.
What cycle init asks, and what it writes
Section titled “What cycle init asks, and what it writes”cycle init proposes zones from what is actually in the repository and asks one question about each — is this dangerous to change? — plus one about who owns them, offering the address you are signed in as, or else your git identity. A zone nobody said yes to is not written, and an owner nobody accepted stays TODO@example.com, which cycle doctor keeps flagging. The one proposal that is not a question is the schema system zone: spike mode is built on it, so it is accepted without asking.
--yes (or -y) accepts every proposal and the proposed owner without asking. It is also the only way to run cycle init without a terminal: piped or scripted without it, the command refuses and writes nothing rather than answering its own questions.
--harness picks which editors get a hook file — claude-code, codex, cursor, windsurf, or all — comma-separated, repeated, or both. Left out, it writes Claude Code’s and Codex’s, which is what cycle init has always written. A name that is not one of the four stops the command before anything is written. The selection does not bend the rule above: asking for all four on a machine with no wrapper still writes none.
| Harness | File it writes | What it does not promise |
|---|---|---|
claude-code |
.claude/settings.json |
The only harness whose wall has ever been seen to fire |
codex |
.codex/hooks.json |
Codex ignores project hooks until the project is trusted, and no Codex install has ever run this entry |
cursor |
.cursor/hooks.json |
Ignored in an untrusted workspace. failClosed: true is set, but only preToolUse is wired, so treat shell commands and file reads in Cursor as unguarded. Never exercised against a Cursor install |
windsurf |
.windsurf/hooks.json |
No channel for a denial’s reason, so a blocked action can stop with no explanation. Windsurf is becoming Devin Desktop, and whether this path keeps working is documented nowhere. Never exercised against a Windsurf install |
cycle init prints those caveats beside each file as it writes it. The three entries that are not Claude Code’s carry no argument that picks the hook’s reply format, so the hook answers them in Claude Code’s, and what those editors do with that reply has never been observed — cycle init says to treat those files as filed, not as enforcing. What blocks in each editor has the detail per editor.
Beside the zone map, cycle init writes the durable block in AGENTS.md — the zone map, the board and the deploy rule, never the active task — a CLAUDE.md that imports it (appended to one you already have, never replacing it), the .gitattributes line and the git config that register the AGENTS.md merge driver, and two .gitignore entries: CC’s runtime state and CLAUDE.local.md. It writes .mcp.json only when this project holds a CommitCycle CLI to start. A hook file or .mcp.json that already exists is left alone and listed as such. It names no board — when .zones/board.json is missing it tells you to run cycle pair — and it never moves a task.
What cycle doctor checks
Section titled “What cycle doctor checks”cycle doctor is the first thing to run when something is not behaving. It is a diagnosis, not a repair: it moves no task, and every question it asks the board is a read. Each check prints as one line marked ok, warn or FAIL, with the fix beneath it where there is one, and the last line sums them up. It exits 1 when any check failed; warnings alone exit 0.
Several lines print only when there is something to say. In the order they appear:
| Line | What it answers |
|---|---|
git repository |
The repository root being diagnosed |
zone map, zone owners |
Whether .zones/zones.yml exists and parses, what it declares, and which owners still say TODO |
hook installed, hook executable |
Which layer is wired — this repository’s hook configuration, a plugin installed on this machine, or neither — and whether the wrapper it invokes is on disk and can run on this platform |
enforcing bundle, built bundle vs source, shipped bundle vs source, session vs bundle, one hook, not two |
How old the artifact judging this session is, and whether two hooks are registered for one repository and are the same artifact |
active task, branch matches the grant, grant holder is live |
Whether the branch names a task, and that task’s grant: missing, expired (the fix is cycle renew), issued for another branch, or held by a checkout git can no longer find |
guard prerequisites, hook timeout |
Whether the working tree can be read, which containing Bash commands depends on, and whether a hung hook can be cut off |
deploy guard |
Whether a deploy script — in the root package.json, or one under apps/, packages/ or services/ — can publish without going through cycle guard-deploy |
AGENTS.md merge driver |
Whether .gitattributes maps AGENTS.md to the merge driver, and whether this clone has the driver registered |
board scope |
Which organization and repository this reports to, and whether the environment or .zones/board.json said so |
tool surfaces |
Which tool surfaces this checkout’s event log has seen, and whether each one is accounted for |
one identity |
Whether the signed-in address and the git identity name the same person |
board address |
Whether the board is a local one, whose task ids will collide with the shared board’s |
With a board address configured, two more lines ask the board: version handshake — do the CLI and the server speak the same protocol — and up to date, which says whether a newer CLI has shipped and is never a failure. With an organization as well, doctor compares the board’s task list with the record files (records match the board, record titles drift, record state is stale, ahead of the board, work with no record, In Progress is running, stranded grants, unreadable record) and asks whether each zone’s owner is a member of the board: zone owners are members, or zone owners unchecked when the board cannot say. Among those, only a record that describes different work from the board’s task under the same id is a failure; the rest can only warn.
cycle doctor --enforcement answers one question only, and it is a question the other lines cannot reach: how old is the bundle actually enforcing this session, measured against the source it claims to be built from. It prints the four bundle lines — enforcing bundle, built bundle vs source, shipped bundle vs source and session vs bundle — and nothing else: no board, no task, no hook wiring. It exits non-zero when any of them fails: a build older than its source, a shipped bundle behind it, a session running an older or different artifact than this checkout produces, or an enforcing bundle that is not where the wrapper looks.
That matters because the hook runs from a built bundle loaded once per session, and a bundle can be behind the source that fixed something — in which case the repository is being judged by a rule its own code no longer contains, and every other doctor line still says ok. What it does not check: whether the source itself is correct, whether the board agrees, or whether the bundle on any other machine matches. It compares the artefacts on this machine against the source in this checkout, right now.
The task lifecycle
Section titled “The task lifecycle”| Command | Does |
|---|---|
cycle start "title" | <task> |
One command: intake → scope → branch → gate → grant → record |
cycle scope <task> |
Correct what a task says. It cannot move it — that is the gate |
cycle show [task] |
Read a task as the board holds it, or list the board. Read-only |
cycle status |
What task this branch is bound to, what its grant opens and for how long, and which board this repository reports to. Needs no board |
cycle sync [--no-reconcile] |
Push the zone map and events, pull the grant for this branch, and repair the drift a checkout collects. Never moves a task |
cycle pull |
Reconcile the record files against the board’s task states |
cycle renew |
The grant ran out and the work has not: re-issue exactly the zones it already holds, for another TTL |
cycle submit [--close] [--evidence <url|path>]… |
Hand the work in: In Progress → In Review, and close the zones |
cycle pause [task] --reason "…" |
Stop without giving up: In Progress → Todo, and hand the grant back |
cycle start and cycle scope share the same scoping flags — the four fields, plus what the task will touch:
cycle start "the thing" \ --goal "…" --non-goals "…" --criteria "…" \ --zone schema:write --topics backend,db --priority high --gate nonecycle start <task> on a task whose branch already exists reuses that branch — it is the task’s binding — and first says where it stands against the trunk: how many commits behind origin/main it is, which commits on it name another task, and which tracked files checking it out would take back to an older state. If any of the three is not zero, it refuses before anything is scoped, checked out or started, and names them. --accept-position adopts the branch where it stands anyway; bring the trunk in afterwards with git merge origin/main, which adds a commit and rewrites none. Nothing moves or re-cuts the branch.
When there is nobody to hand to
Section titled “When there is nobody to hand to”Two steps in the loop are waits for another person, and on a board with one member there is no other person to wait for. Both collapse — the wait, never the check (D-33).
cycle renewre-issues the grant this task already holds when its eight hours run out: the same zones, the same modes, the same branch, a new expiry. It cannot widen anything — the board re-issues what the grant carried, not what the task now declares — and a high-risk zone whose approval was withdrawn or timed out is refused exactly as it would be at the gate. Every renewal is recorded with an actor and appears in the audit record.cycle submit --closecarries the hand-in straight on into the close instead of leaving the work in In Review for a reader who is you. Nothing about the close changes: the same checks run, the same audit record is written, and the task still passes through In Review.
Both read the board’s own membership list — there is no mode to set and nothing to remember. Adding a second person to the board turns both off in the same act, and the commands then say so and name the path that is theirs.
If the board reports more than one member — or does not say how many it has, which is what an older board looks like — cycle submit --close stops after the hand-in, says so, and exits 0: the task is in In Review and the work is safe. If the close itself is refused, the hand-in stands: the task stays in In Review, the refusals print, and the command exits 1. Fix what they name and run cycle verify --close — not a second cycle submit.
cycle scope deliberately cannot move a task between states. Correcting a description and deciding that work may begin are different acts, and only the gate does the second.
Two of those flags carry meaning beyond being stored, and both are easy to pass without knowing what they buy:
--topics a,bnames the disciplines the work needs, and each one loads its playbook for the agent — the per-topic, per-project context in.zones/playbooks/that a close can feed back into. Topics are advisory: none of them blocks, and a topic nobody declares never loads, which is why the loop’s own playbook is named in the agent guide rather than left to a topic.--priorityis the triage property, on the same scale a reader already knows from Linear:urgent,high,medium,low,none. It orders the board and nothing else — the gate does not read it, and a high priority does not shorten a grant or soften a check.
cycle pull takes --adopt and --write-missing: the first catches stale record files up to the board’s states, the second writes a record for a task the board holds and disk does not. A record ahead of the board is left alone.
What cycle status reads
Section titled “What cycle status reads”cycle status answers from this machine and needs no board: it reads the branch, the local grant file, the zone map, and the board binding — the environment, or .zones/board.json. It prints the branch and the task it names, the organization and repository this reports to, which hook bundle is deciding the rules here and whether a newer release is known, and every zone as closed, readable, closed for writing, open, or open for the mode the grant gave it. While a grant is active it ends with how long the grant has left.
Three situations get their own paragraph: a grant that has expired — which is why writes stopped, and cycle renew is the way back — a grant issued for a different branch than the one checked out, which would attribute a manifest computed here to work the grant never covered, and a grant held by another checkout, since write access lives where the grant is held. With no zone map, or one that does not parse, it says so and stops there. On a spike it names the mode and the time box.
What cycle sync does in one run
Section titled “What cycle sync does in one run”cycle sync sends some things up, brings some down and settles a few files here, each kind of data in one direction only. It never moves a task between states.
- Up: the zone map, and this task’s event log.
- Down: the grant for this branch’s task — including a revocation. When the board says there is no grant and one is sitting on disk, sync deletes it, because the hook reads that file and never the board. It also brings down the two repository policies a person sets on the board — the failure policy below, and whether this repository’s agents may open pull requests — and any mentions addressed to this task, which are advisory.
- Here: the durable block in
AGENTS.md, rewritten only when the zone map, the board or the enforcement it describes has changed;CLAUDE.local.md, which names the active task while a grant exists and is cleared when none does; the playbook catalog in.zones/playbooks/README.md; and theAGENTS.mdmerge driver, registered in this clone if it is missing. A merge conflict inAGENTS.mdthat touches only CC’s block is resolved to this side; one outside it is left for you.
A board that cannot be reached, or cannot see this repository, is a warning, not a failure: nothing local is deleted on a call that got no real answer, and the offline half of the run still completes.
When the board did answer, sync also repairs the drift a checkout collects when nobody runs maintenance, one printed line per repair:
- a record file whose state is behind the board catches up to it;
- a task the board holds past Triage with no record file gets one written from the board — unless another checkout is standing on that task, or a file in
.zones/tasks/carries no id; - a grant file for a task the board holds in any state other than In Progress is deleted — never the current branch’s own.
What it will not do on its own: pause an In Progress task with nothing running under it (a pause needs your reason, so it prints the command instead), rewrite a record that is ahead of the board (that is evidence — cycle pull reads its events), or touch anything at all when a record file and the board describe different work under one id. That last case aborts the whole pass and exits 1. Commit what the repairs write with the task that ran them. --no-reconcile skips the stage entirely.
What cycle sync also brings down
Section titled “What cycle sync also brings down”Of the things cycle sync brings down, one is worth knowing in detail: the repository’s failure policy — what the hook’s wrapper honors when the core cannot answer at all.
It is set per repository, by a signed-in person on the board, and never by a machine: a token can read it, and an agent choosing how enforced its own outage is would defeat the point. cycle sync carries it down to .zones/state/failure-policy as a single line the wrapper can read with shell builtins, because the premise of that code path is that nothing more capable is working.
That file is runtime state, not configuration you edit: it is gitignored, it lives inside the trust root the hook reads its own grants from, and cycle sync overwrites it with whatever the board says. Change the policy on the board and sync; a hand-edit is either reverted on the next sync or refused outright.
| Value | The wrapper’s answer while the core is down |
|---|---|
closed |
Deny everything. The default, and the only default a guard can ship with |
reads |
A fixed list of tool names passes, each announced as degraded |
journal |
Everything passes onto the unwitnessed record — and if the journal cannot be written, it falls back to closed, because allow-without-record is the one combination nobody chose |
No file means closed. A board you cannot reach changes nothing at all: an outage must not loosen or tighten anything by itself. What is not configurable is honesty — the wrapper always answers, and every degraded call says so in its own reason string.
This is here rather than under Operations because it is a property of a command in this table: cycle sync is what writes it, and editing that file by hand is not how it is set.
What cycle submit changes
Section titled “What cycle submit changes”cycle submit takes no task id — the branch names the task — and asks the gate to move it In Progress → In Review. If the gate refuses, its own words print and nothing here changes. If it says yes, in the same run:
- the task’s grant file is deleted, so protected zones close here in the same breath the board revokes them;
CLAUDE.local.mdstops naming the task;- the durable block in
AGENTS.mdis re-rendered, and committed on its own only when that changed it. The active task no longer lives inAGENTS.md— it moved toCLAUDE.local.md— so on a repository already in that form there is ordinarily nothing to commit.
It does not close the task. Its last lines name cycle verify --close, which is In Review → Done and writes the audit record; until that runs, the task sits in In Review and leaves no trail.
--evidence <url|path> names what the task’s quality gate asks for, and it can be repeated. It is handed to the close that --close runs, but it is checked first: a path that is not in the repository refuses the whole command before anything is handed in. A value that starts with http:// or https:// is a URL; anything else is a path.
Access
Section titled “Access”| Command | Does |
|---|---|
cycle request-access [task] --zone <id> --mode read|write --reason "…" --alternative "…" |
Ask the zone’s owner to open it for this task |
cycle requests |
What is waiting on you — the zone, the task, why, and what they would do instead |
cycle approve <id> [--hours N] · cycle deny <id> |
Answer one from here |
--zone, --reason and --alternative are all mandatory, and a request missing any of them is refused before anything is sent. A request without an alternative gives the approver no way to weigh saying no, and approval-as-reflex is how gates rot. --mode defaults to write; pass --mode read (or --read) when reading is enough — read never grants write.
cycle request-access asks for this branch’s task and no other. It refuses, before the board is called, when the checkout is not on a task branch, when the [task] it is given is not the branch’s own, and when the zone map on disk declares no such zone. It grants nothing and changes no file here: the ask is filed at the board for the zone’s owner, the reply prints the request’s id and the console address where it is answered, and once it is answered cycle sync brings the decision down.
If the board reports that the zone’s owner is not a member, or that the zone declares no owner, the request is still filed — and the reply says that nobody who can answer it will see it until that is fixed. cycle doctor lists every zone in that state. Either way, carry on with the rest of the task: working around the block is worse than the block.
Answering from the terminal is supported, and the console is the stronger door — the trail records which one answered.
Closing
Section titled “Closing”| Command | Does |
|---|---|
cycle verify |
Run the closing checks against this branch. Changes nothing, and needs no board |
cycle verify --from-history [task] |
The same, for work that merged before it closed — against the task’s own commits in the trunk |
cycle verify --base <ref> |
The same, against a fork point you name instead of the one CC works out |
cycle verify --close [--evidence <url|path>]… [--override "reason"] [--guard-change "why"] |
Close it: submit the manifest and write the audit record |
Without --close, cycle verify changes nothing, locally or at the board. It builds the manifest — every file changed since the fork point, committed or not — and runs the same local checks the close runs, in the same words, so a refusal costs no round trip. It exits 1 when they refuse. Rehearse with it before every close. The closing gate describes what the close checks; the one part the rehearsal cannot run is the evidence a quality gate asks for, which lives on the board — so the rehearsal warns about it from the task record, and the board checks it at the close.
Beyond the gate’s own checks, three refusals are worth knowing before they happen:
- The guard moved. When the diff touches a file that defines the guard itself —
.zones/zones.yml,.claude/settings.json,.mcp.json, anything under.github/workflows/orplugin/— the check refuses until--guard-change "why"says why. Moving the boundary is allowed, andcycle protectandcycle dismissdo it on purpose; what is refused is the move passing unsaid. An empty reason is refused too. The reason is written under its own heading in the audit record, and passing the flag when no such file changed says that nothing was acknowledged. - The diff is empty because the work already merged. An empty manifest on a branch that is already in the trunk is refused rather than passed — closing on it would check nothing — and the refusal says whether commits in the trunk name the task, so
--from-historycan verify those instead. - The task id is not the branch’s. A
[task]is honoured only with--from-history, the one mode that reads a task out of history. Without it the branch names the task, and an id that is not the branch’s own is refused rather than silently ignored.
With no grant on disk — the ordinary state after a hand-in — the declared-zones check does not run locally, and cycle verify says so: the transition runs it against the board’s copy of the task.
--from-history exists because work that merges before it closes cannot be verified from its branch: the merge base is the tip, so the diff is empty. It rebuilds the manifest from the task’s own commits and labels it as such, so no audit record mistakes it for a branch diff.
--base overrides the fork point the manifest is measured from, and the output says so — it reports “the --base you passed” rather than the base it found, so a record never hides that a human chose the comparison. Most of the time you will not need it: CC finds the fork point against the trunk on its own, and when it is right the flag adds nothing. Reach for it when it is wrong — a branch cut from another branch, a rebase that moved the base out from under the task, or a trunk this checkout resolves differently from the one the work actually left. The wrong base is not a cosmetic problem: it decides which files land in the manifest, and a manifest is what the closing gate checks the declared zones against.
With --close, cycle verify needs a board. It runs the same local checks first and stops on any refusal before the board is asked; then it mirrors this task’s event log to the board, asks for In Review → Done with the manifest and the evidence, and writes the audit record to .zones/audit/<task>.md — commit it with the work. A record already there that differs is kept beside the new one as .superseded, never overwritten. A successful close ends by offering a playbook feed for each declared topic whose playbook the branch did not touch, and skipping it is legal.
--evidence is a URL or a path, and it can be repeated. A path that is not in the repository is refused before anything else runs: the repository is the source of truth, not object storage.
--override "reason" records a bypass rather than hiding one, and only the board applies it. It travels with the transition, signed by the actor this shell reports — CC_ACTOR, or else USER — and the local checks run before the board is asked, so an override cannot answer a refusal the local checks print: the close stops there, and the override goes nowhere. When the board does apply one, the audit record carries it as an exception, never as a pass, and the close says so — or warns when the record it wrote does not carry it.
The zone map, over time
Section titled “The zone map, over time”| Command | Does |
|---|---|
cycle protect <glob> --zone <id> |
Answer “should this be protected?” with yes |
cycle dismiss <glob> |
Answer it with no — reviewed, left open, stop asking |
These two exist because a close asks about paths that fell in no zone. Deciding once, at close, with the evidence in front of you, is cheaper than deciding at declaration time when it is hypothetical.
Both are local edits to .zones/zones.yml, in your diff: neither talks to the board, and neither grants anything. The edit keeps the file’s comments, and it is recorded in the task’s event log as CC’s own write, so the close does not read it as a change that went around the hook. A zone map that is missing or does not parse is refused before anything is written, and repeating either command with the same glob does nothing the second time.
cycle protect <glob> --zone <id> adds the glob to that zone’s paths. Put the glob first: the command takes the first argument that is not a flag as the glob. It cannot create a zone — a zone needs a name, an owner and a risk level, which are judgement calls for the editor with the rest of the map in view — so an id the map does not declare is refused, with the ids it does. Run cycle sync afterwards so the board learns the new boundary.
cycle dismiss <glob> adds the glob to the map’s unprotected: list, creating the list — with a comment saying what it means — the first time. The list only silences the closing gate’s question: anything no zone names is unprotected whether it is listed or not.
Because zones.yml defines the guard, a close whose diff carries either edit refuses until --guard-change "why" says why the boundary moved — see Closing.
What the work taught
Section titled “What the work taught”| Command | Does |
|---|---|
cycle feed <topic> "one bullet (CC-xxx)" |
Append what a close taught to the topic’s playbook — always optional |
cycle seed [--topics a,b] [--yes] |
Draft playbooks from the codebase, as choices. Claims cite real files or are dropped; topics with nothing to cite are not born |
cycle challenge |
The seed’s counterweight: proposals for what this repository lacks, each with a market source |
cycle challenge has no --yes. Accepting and rejecting both write a decision file, and deciding is yours.
Shipping
Section titled “Shipping”| Command | Does |
|---|---|
cycle guard-deploy |
Refuse a deploy that is not from the trunk |
Put it in front of the deploy command — cycle guard-deploy && <your deploy>. It exits 0 on the trunk, 2 on a branch (nothing deployed, and the build must not go red) and 1 when no branch name can be found. Full reasoning in the deploy guard.
For MCP clients
Section titled “For MCP clients”| Command | Does |
|---|---|
cycle mcp |
Serve the MCP tools over stdio, for a client’s .mcp.json |
Five describe-only tools. The MCP reference covers what they are and how they differ from the connector’s nine.
Names borrowed rather than invented
Section titled “Names borrowed rather than invented”init from git init and npm init. status from git status. doctor from brew doctor. submit from Gerrit. request-access from Teleport’s access requests. A command set you can guess is a command set people keep.