Guarded run pipeline
The gates every cco run passes: clean-tree preflight, baseline, sandbox, scope check, shell-free verify, keep or rollback.
Overview
cco run is a guarded runner. It launches a third-party agent CLI (claude, codex, opencode, or any command) inside a fixed envelope: baseline snapshot, execute, post-hoc scope check, verify, then keep or rollback. The agent writes directly; the runner never trusts what the agent says about itself. Acceptance depends only on the scope check and the verify step.
Exit codes: 0 kept (state=ready), 1 rolled back or failed, 2 blocked before execution (bad contract, dirty worktree, executor missing).
The gates, in order
| # | Gate | What it enforces | Diagnostic on failure |
|---|---|---|---|
| 1 | Contract load | The contract is validated against the v1 schema: task, allowed_files, verify_commands. | contract_load_failed, contract_schema_unsupported |
| 2 | Clean-tree preflight | The repo must be a git work tree with an empty git status --porcelain --untracked-files=all, and the executor binary must exist. This is what makes rollback safe. | dirty_worktree, not_a_git_repo, executor_unavailable |
| 3 | Baseline snapshot | Records HEAD, branch, ignored entries and sha256 of .git/config, .git/hooks/* and .git/info/*. | - |
| 4 | Sandbox | With --executor-sandbox the executor's own sandbox is requested (claude sandbox setting, codex --sandbox workspace-write). Without it a kept run is degraded. | executor_sandbox_disabled, executor_sandbox_required |
| 5 | Execute | The agent runs with a sanitized environment plus a per-adapter auth allowlist. A timeout kills the whole process group. | executor_timeout, executor_failed |
| 6 | Post-hoc scope check | Changed paths (tracked, untracked, new ignored) are compared to allowed_files and forbidden_paths. Moved HEAD or branch and tampered .git metadata are detected by hash and repaired before any further git command. | unexpected_mutation, executor_moved_head, git_metadata_tampered |
| 7 | No-op rule | No changes at all fails unless the contract sets allow_noop. | noop_not_allowed |
| 8 | Shell-free verify | Each verify command is split into argv and must be in the command allowlist. Shell syntax (| && ; > $() is refused. Stops at the first failure. | verify_failed, verify_command_rejected, verify_command_requires_shell |
| 9 | Keep or rollback | Keep only when gates 6 to 8 passed; otherwise rollback. | rollback_failed if recovery itself fails |
Scope and protected paths
Besides the contract's allowed_files (exact path, directory prefix ending in /, or glob), a fixed set of protected paths can never change in a run, even if listed as allowed:
.git/**- history, config and hooks.github/workflows/**- CI definitionsAGENTS.md- repository instructions.context/**- durable project knowledge
A touched protected path is recorded in the outcome's scope_violations with kind protected_path; other kinds are forbidden_path, outside_allowed and new_ignored_file. Any violation sends the run to rollback.
The prompt given to the agent states the same boundary up front: modify only the listed files, do not commit, do not switch branches, do not edit git config or hooks.
Shell-free verify allowlist
Verify commands are plain argv strings and are never run through a shell. A command outside the allowlist is a configuration error, reported as its own diagnostic verify_command_rejected so routing never burns an escalation on it. Add a legitimate command to command_allowlist in the cco config, or split a compound command into one plain entry per line of the contract.
verify_commands: - python3 -m pytest -q # ok: plain argv # - pytest -q && ruff check . # refused: shell syntax
Verify must also be side-effect-free: it runs after the scope check, and a second scope check follows it. Anything verify writes outside the allowed set is an unexpected mutation.
Keep or rollback
Rollback is performed by the trusted runner, not the model. It first saves rejected.patch (diff plus untracked contents) in the run directory, restores .git/config and hooks from the baseline copies, runs git reset --hard to the baseline HEAD and git clean -fd, deletes newly created ignored files and re-checks-out the baseline branch. A rolled-back run leaves the tree identical to the baseline.
If recovery fails, the run is blocked with rollback_failed and a next_command containing the manual recovery commands.
Routing and escalation
cco run --task-class NAME picks the starting executor and model from runner.routing and escalates to a stronger tier when an attempt fails in a way a stronger model might fix. Every attempt is a normal guarded run, so the whole envelope above applies per attempt.
- Retried:
verify_failed,unexpected_mutation,executor_failed. - Never retried: config and preflight blocks,
rollback_failed,executor_timeout,verify_command_rejected. - Bounded by
max_escalations(default 1) and an optional cumulativemax_budget_usd_per_task. - The routing record, including
stop_reason, is written toexecution_envelope.json.
What it does not cover
Scope enforcement is detection plus reversal inside the repo, not prevention. Writes outside the repo, network exfiltration, detached long-lived processes, changes inside pre-existing ignored directories (.venv, node_modules) and secrets readable from disk are not covered. Use --executor-sandbox for any non-trivial repo, and a container wrapper for untrusted tasks.