Documentation

A guide to running coding agents you don't fully trust — safely, and at scale. It explains not just how each piece works, but why it works that way.

The idea

Coding agents are at their best with the guardrails off — free to edit any file and run any command without stopping to ask. That's also exactly when you don't want one running loose on your laptop.

Coop's answer is simple: run the agent inside a disposable container that can only see the one repo you're working on. Your home directory, SSH keys, cloud credentials, and every other project simply aren't in there. The repo's own secrets — .env files, private keys, a secrets/ folder — are blanked out before the agent ever starts.

Why it's built this way

The container is the boundary — not a list of permissions you have to get exactly right. You don't audit what the agent is allowed to do; you put it somewhere it can't do harm. The worst an off-the-rails agent can manage is a mess in one repo you can restore from git.

Everything else is built on that one box. Run an agent interactively, hand work off like a pull request, point a whole fleet at a backlog overnight, or convene a council of models on a hard problem — it's the same cage every time.

Install

One binary, no Go toolchain, no clone. A container runtime is the only requirement.

curl -fsSL https://raw.githubusercontent.com/AndrewDryga/coop/main/install.sh | sh

This drops coop into ~/.local/bin, and — if a container runtime is already installed — builds the sandbox image and runs coop doctor for you. If no runtime was found, do that one step yourself once a runtime is in place:

coop build && coop doctor

For the runtime, take your pick: Apple container (macOS 26+), Docker, or Podman — coop auto-detects whichever you have. The coop binary itself is fully static with no other dependencies.

The installer verifies what it downloads. With cosign on your PATH it checks a keyless Sigstore signature; otherwise it falls back to a SHA-256 check and tells you the signature wasn't verified. Prefer to build from source? git clone https://github.com/AndrewDryga/coop && cd coop && make install works too.

Quickstart

The shortest path from nothing to a sandboxed agent that lands reviewed work.

cd ~/code/your-repo          # 1. any git repo
coop doctor                  # 2. prove the isolation holds
coop login claude            # 3. sign in once — paste a code, no browser
coop claude                  # 4. a sandboxed agent, brakes off, secrets hidden

That's the whole everyday flow. When you'd rather not work in your live checkout — for a bigger or riskier change — hand it to a fork instead and review the result like a PR:

coop tasks add "wire up --json" # 1. queue the work as a task
coop fork feature claude --loop # 2. a fork works the queue in its own clone…
coop fork review feature        # 3. …you read the diff like a PR…
coop fork merge feature         # 4. …and land it on your branch

Anything you type after the agent's name is passed straight through to it, so coop claude --continue resumes the last session, still sandboxed. From here, the rest of this guide goes feature by feature — but you already know enough to be useful.

The sandbox

What's inside the box, what's deliberately left out, and why that line is the entire security model.

Your repo is bind-mounted into the container at the same path it has on your machine. That matters for two reasons: the agent edits your real files (you watch the changes land live in your editor), and absolute paths line up between coop, the loop, and your IDE. Everything outside that one directory — the rest of your disk — is just not present in the container.

Your git identity, not the box's

The container has no ~/.gitconfig of its own, so coop mounts a curated one: your name and email (commits are authored as you), your global gitignore, and commit.gpgsign=false. Your signing key never enters the box — so commits made inside it are authored as you but left unsigned.

If you sign your commits (commit.gpgsign=true in your global git config), coop closes that gap on the host, where your key lives: it re-signs the box's commits with your key on every path that produces them — when an interactive or editor box exits, after each coop loop cycle, and as a fork lands through coop fork merge. You can also re-sign the current branch's unpushed range yourself with coop sign. Your history ends up signed by you, exactly as if you'd typed the commits — so a protected main that requires signatures accepts work an agent did in a box. More on forks below.

Secrets stay out

Two layers keep credentials away from the agent: secrets are hidden by name, and a content scan catches the ones hiding inside ordinary files.

Files and folders that look like secrets — .env, *.pem, *.tfvars, secrets/, .ssh, and a long list of well-known names — are shadowed: an empty in-memory filesystem covers secret directories, and a blank read-only file covers secret files. The agent sees that the file exists, but it's empty and can't be written. Templates (*.example, *.sample, *.template) stay visible on purpose, so the agent can still learn the shape of your config.

The boundary you control is .coopignore, not .gitignore. A normal run mounts your whole working tree, so a secret that's gitignored-but-still-on-disk (a real serviceAccount.json, say) is fully visible to the agent — gitignoring it does not hide it. Anything that must live in the tree but stay secret belongs in .coopignore.

The built-in list can't know your repo holds a config/credentials.yaml. Drop a .coopignore at the repo root — one pattern per line — and those paths are hidden everywhere the box can see:

prod.yml                 # basename — matched at any depth
config/credentials.yaml  # a slash makes it a repo-relative path
vault/                   # a directory — hidden whole

That handles secrets identified by their name. But a token can also hide inside an ordinary file. coop check-secrets reads your files looking for secret-shaped content — known provider token formats and high-entropy strings — and reports each as file:line, exiting non-zero on a hit so it doubles as a pre-flight or CI gate. The same scan runs automatically every time you merge a fork.

coop check-secrets

Prove it

Security you can't verify is just a promise. coop doctor attacks its own box and shows you, check by check, that it holds.

Run it any time — and especially after you change config. doctor plants a decoy secret in your repo, launches the box, and then tries to break in: from inside the container it confirms the secret is unreadable and unwritable while ordinary source stays readable, and on the host it confirms a fork carries neither the secret nor anywhere to push to. If anything fails, it tells you exactly what.

coop doctor

Run an agent

The everyday command: a sandboxed agent in your repo, brakes off.

coop claude           # Claude — no permission prompts, secrets hidden
coop codex            # the same box, Codex instead
coop gemini           # …or Gemini
coop grok             # …or Grok (xAI)
coop shell            # just a shell in the box, to look around
coop run -- npm test  # run one command in the box and exit
Why brakes-off is fine here

Each agent launches with its most autonomous flags on purpose. Outside a sandbox that's reckless; inside one it's the whole point — you get an agent that doesn't nag you for permission on every step, and the box makes sure it can't reach anything that matters.

coop claude

Forks — hand off work like a PR

Give a task to an agent the way you'd give it to a contractor: it works in its own copy, you review the diff, and you decide what lands.

A fork is a throwaway local clone of your repo, handed to the agent instead of your live working tree. Because its origin is a local path, the agent has nowhere to push — and since a clone only carries committed content, gitignored secrets never even come along.

When to reach for a fork

Use it for anything bigger or riskier than a quick edit, and any time you want to run several attempts at once. The agent never touches your checkout, so you can keep working while it does — and you stay the only person who lands anything.

The lifecycle mirrors a pull request: open → work → review → land. You review the diff in your terminal (a brief, then the changes) or in your IDE. Landing rebases the fork onto your current branch for linear history — and because that rebase runs on the host where your key lives, the commits can be signed even though the box committed them unsigned.

Set a gate and every landing earns its place: COOP_GATE="make check" re-runs your tests in the box on the rebased tree and rolls back if they go red. A content scan also flags any secret-shaped or oversized files on the way in.

Landing brings over code, not queue state: a fork works a copy of the .agent/tasks/ queue, so after a merge the parent still lists the tasks the fork finished — mark them done or remove them, and run coop tasks watch for the deduped view across the parent and its forks while they run.

coop fork — review & land

Fusion — a council of models

Don't bet the run on one model's blind spots. Let one model lead while a configured council weighs in before every response or task.

In coop fusion, one model is the governor: it does the real work — edits, runs the gate, commits. Explicit peers and preset consult roles form a one-or-many-member council, consulted read-only and in parallel where practical. The governor synthesizes their strongest conclusions. The combined answer reliably beats any single model working alone.

Why it stays simple

There's no extra service or protocol — the leader simply asks each council member from its own shell before it answers or starts a task. Each consultation adds one read-only run per member, so cost scales with the council you configure.

coop fusion claude --peer codex --peer gemini  # a council: claude governs, peers weigh in
coop claude --peer codex                       # lighter, opt-in: one peer on hard calls only
coop fusion claude --peer codex --peer gemini

Credentials & failover

Hold more than one account for an agent so a long run never has to park on a rate limit.

Sign in to several subscriptions as named credentials. When a loop hits a limit on one, it rotates to another that's still fresh and keeps going — only ever waiting once every account is capped.

coop login claude@work       # a second account…
coop login claude@personal   # …and a third
coop credentials             # list them + which are signed in

API-token users can put provider keys in ~/.config/coop/agents/env. Accepted keys are ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, and CLAUDE_CODE_OAUTH_TOKEN for Claude; OPENAI_API_KEY for Codex; GEMINI_API_KEY or GOOGLE_API_KEY for Gemini; and XAI_API_KEY for Grok. Coop recognizes an env-only login as the provider's default credential and scopes every accepted key like a file-backed login. A bare key imports an existing ambient value; if it is unset, the entry is omitted. A present import or later assignment wins over earlier entries. The env token is one effective default credential, not a substitute for additional named-account logins; Coop removes those env keys when a file-backed named account runs so the global token cannot override it, including when that account is marked as the default.

There's no pool to configure: a loop rotates whatever its lead's target ladder expands to. A bare provider:model rung in that ladder fans out across every signed-in account (the marked default first), so with no setup a loop already uses all your accounts; a pinned provider:model@account runs just one. Limits are tracked per (model, account), so claude:opus@personal stays usable while claude:opus@work cools down. Mark which account a plain coop <agent> uses with coop credentials <agent> <name> default.

Only the active credential is ever mounted into the box, so a running agent sees just the account it's using — never your whole vault. Switching mid-run loses nothing: each iteration is a fresh run, and the queue plus git carry the progress.

Pick the model

The model is its own axis — pick it per run in the target (claude:opus), or as a standing recipe in a preset. A credential is just an account; models don't live on it.

coop claude:opus            # one run on the big model
coop claude:opus/xhigh      # …at max reasoning effort (low·medium·high·xhigh·max)
coop claude:opus@work       # …pinned to a specific account
coop frontier               # a standing lead model + roles, from a preset
coop models                 # the model menu per agent

Precedence is most-specific-first: the model in the target (claude:opus), then the active ladder entry's model (a loop stepping through a preset), then COOP_LOOP_MODEL (loop runs only), then COOP_<AGENT>_MODEL. Coop never validates a model id — whatever the agent's own CLI accepts works, and a bad one fails loudly in the agent's own error, so new models work the day they ship.

Reasoning effort is a sibling axis — add /effort to the same target (claude:opus/xhigh, codex/high): low · medium · high · xhigh · max. Coop passes the level straight to the agent's CLI (Claude's --effort, Codex's model_reasoning_effort, Grok's --reasoning-effort) and never validates it; Gemini has no effort control, so a /effort on it is rejected up front. It mirrors the model's precedence, and one env var carries both axes: COOP_<AGENT>_MODEL, COOP_LOOP_MODEL, and COOP_REVIEW_MODEL take model/effort (e.g. opus/high) — no separate effort var.

This is what makes the orchestrator pattern cheap to run. coop init scaffolds two starter subagents: deep-reasoner (pinned to Opus — architecture, complex debugging) and fast-worker (pinned to Sonnet — mechanical work). A lead on a bigger model spends its tokens on planning and synthesis while each delegated turn bills at the subagent's cheaper model. Add --peer codex --peer gemini — naming each peer — and the lead can also ask them read-only — peer engineers with different blind spots, no plugins required. Wrap the whole arrangement in a preset.

Presets — the whole arrangement in one file

A lead model, its fallback ladder, and the roles it routes work to — declared once as a file, instead of assembled from flags every time.

The fusion and orchestrator patterns above are wired up by hand — a model here, a --peer codex there. A preset captures the whole recipe in .agent/presets/<name>/preset.yaml: who leads, on which models, and the named roles it can hand work to. Run it by naming it in the who-runs slot — coop <name>, coop loop <name>, fusion, acp, or a fork.

coop presets init  # scaffold the documented "frontier" recipe
coop presets       # list every preset (a broken one shows its error)
coop loop frontier # run the loop under it

The lead's agent: holds one target or a fallback ladder of them — the same ladder a loop rotates (a bare provider:model fans across every account; provider:model@account pins one). Each role sets a mode that decides how the lead reaches it:

Mode What it is
native a Claude subagent that runs inside the lead's own session — no separate box. coop generates it (coop-<role>, from the role's model + prompt) in the box, or set subagent: to reuse an existing one. It's Claude's own subagent, so it needs a Claude lead; under a Codex, Gemini, or Grok lead this role degrades to a read-only coop-consult <role> (same model + persona)
consult a read-only peer, asked as coop-consult <role> — runs the role's target or fallback ladder, with the role's prompt as the peer's persona; it can't edit files
delegate a write-capable worker via coop-delegate: it may edit the worktree but never commits — fallback requires an unchanged clean worktree; the lead reviews the diff, runs the gate, and commits
# .agent/presets/frontier/preset.yaml
lead:
  agent: [claude:claude-fable-5, claude:claude-opus-4-8@work]
roles:
  thinker:                        # deep thinking, in the lead's session
    mode: native
    agent: claude:claude-opus-4-8 # coop generates a coop-thinker subagent from this
    prompt: roles/thinker.md
  critic:                         # read-only cross-vendor review
    mode: consult
    agent: [codex:gpt-5.6-sol, grok:grok-4.5]
  fast:                           # cheap, write-capable mechanical work
    mode: delegate
    agent: [gemini:gemini-3.5-flash, codex:gpt-5.4-mini]
What coop generates from it

coop turns the YAML into the lead's routing contract — each role, when to use it, and the exact invocation — and mounts the coop-consult / coop-delegate wrappers. A native role generates its subagent (coop-<role>) in the box from the role's model + prompt, never written to your repo. coop presets init scaffolds the starter prompt files under roles/; an explicit target on the command line (claude:opus@work) still overrides the recipe.

Consult and delegate ladders skip providers without mounted credentials. Every available rung's credential home is mounted in the lead box. Consult fallback replays its saved transcript; delegate fallback requires every file and Git history to remain unchanged.

The loop — work the queue while you sleep

Give the agent a checklist and let it work the list on its own — through the night, through rate limits, without your hand on the wheel.

The problem it solves

Most real agent work isn't one big task — it's a queue of small, well-defined ones. Driving the agent through them by hand, one at a time, is the bottleneck. And a single marathon session quietly degrades: the context window fills with old work, the agent loses the plot, and quality slides. The loop sidesteps both.

You write each task as a folder under .agent/tasks/ (its state is its directory). coop loop then starts a fresh agent for each one — so every task begins with a clean head, no leftover context to rot. The agent claims a task, does it, runs your gate, commits the result, and moves to the next. It won't stop while any task remains in 00_todo/ or 10_in_progress/. A monorepo? List the members under subprojects: in a committed .agent/project.yaml and one loop drains every component's queue — no configuration beyond that.

coop init                                    # scaffold AGENTS.md, the .agent/ folder, and the hooks
coop tasks add "…"                           # a folder per task under .agent/tasks/
coop loop claude                             # disposable agents drain the queue, then review
coop loop claude:claude-fable-5 --peer codex # pin its model; let it ask a peer

It reviews its own work. When the list is empty, coop runs a review pass — a senior reviewer's bar re-reads every task marked done, confirms the git history backs it up, and reopens anything that falls short. If a pass reopened work, the loop drains and reviews again, repeating until a pass reopens nothing — so "done" ends up meaning verified, not just claimed. Tighten the bar with your own checks under signoff.prompt in .agent/loop.yaml — a task that fails one (changelog updated, docs regenerated, no stray TODOs) gets reopened.

It survives the daily cap. Long runs hit subscription rate limits. Rather than failing, the loop reads the reset time from the agent's own output and waits it out — or, if you've added a second account, switches to it and keeps going. An overnight run rides straight through the limit instead of dying on it.

It parks a decision instead of guessing. On a one-way-door call it shouldn't make alone, the agent moves the task to 50_blocked/ and writes a decision.md (the question, options, its recommendation) rather than picking blind. When nothing else is actionable the loop stops and exits 3, so cron/CI can tell "stalled on a decision" from "done." Answer them with coop tasks decisions -i — it walks each open decision, records your answer, and unblocks the task — then re-run coop loop (or start it with --preflight to unblock already-answered ones first). coop tasks watch shows the live board — the queue plus any running forks, deduped — while it works. For an at-a-glance status in your shell prompt or tmux, coop prompt prints this repo's state on one line (task counts and fork/loop activity, non-zero segments only) — read-only and cheap, safe on every redraw.

Good fits

A backlog of small refactors. Migrating one pattern across dozens of files. Working down a test-coverage or lint list. A "Friday cleanup" queue you'd never get to by hand. Anything that's many small, checkable steps.

coop loop
Each iteration shows the model it's on; a rate limit triggers failover, then the run continues.

The .agent/ folder

coop init creates a small, tool-neutral folder the agent reads back on every start (and after each compaction). The work queue — tasks/ and the backlog — is local, git-ignored working state; the knowledge and config beside it are committed with the repo, so they travel with the project.

Path What it's for
tasks/ the work queue — a folder per task under 00_todo/, 10_in_progress/, 50_blocked/, 99_done/; its state is its directory (the numeric prefix sorts ls in lifecycle order; coop tasks shows the clean names). Local working state
the backlog unscheduled ideas — work, chores, ideas — as task folders in the tasks/xx_backlog/ drawer (coop backlog); outside the lifecycle, so never auto-worked, and coop backlog promote <id> moves one into 00_todo/ when it's ready
rules/ the taste knowledge base (normative) — a correction becomes a "do X, not Y" rule here
kb/ the descriptive knowledge base — subsystem maps, cross-cutting traps, and gotchas the code doesn't carry; a self-improving wiki the agents keep current as they work
skills/ the workflow skills (spec, work, sweep, investigate, review-board, verify-api, release); each agent's dir symlinks to them
presets/ orchestration recipes — a lead plus roles (thinker, critic, fast); see Presets
project.yaml per-project config: a monorepo's members, the dev-server ports coop publishes, the box posture, and the fork-merge gate
loop.yaml configures coop loop's stages — preflight, work, between, signoff — each with its own model ladder and prompt
fleet.yaml declares a fleet of fork loops for coop fleet — one entry per fork

A fleet — many agents at once

One loop is one agent working one queue. A fleet is several — often different models — each on its own slice of the work, all running at the same time.

Split the backlog into separate task trees (coop tasks split 3 slices the queue into copy-trees), declare a fork per slice in .agent/fleet.yaml (coop fleet init writes the template), then coop fleet up starts them all in the background; coop fleet watch is the live board, showing each fork's progress, the task it's on, who's finished, and what each has cost.

coop tasks split 3    # slice the todo queue 3 ways into copy-trees
coop fleet init       # write .agent/fleet.yaml, then declare a fork per slice
coop fleet up         # start every fork in .agent/fleet.yaml, detached
coop fleet watch      # the live dashboard
coop fork merge --all # land the whole fleet through a revalidating rebase

Each fork in .agent/fleet.yaml takes two keys: tasks: points at its slice, and agent: is the who-runs — either a target (provider:model@account; give each fork a different account so they don't contend) or a preset name, whose lead ladder the fork's loop rotates.

Why run a fleet

Once generating code is cheap, reviewing it becomes the bottleneck — so the move is to parallelize generation and batch the review. Add agents until your review queue, not the agents, is what's keeping you busy.

coop fleet watch
Three models working their own slices — watch them finish in real time.

Editors

Prefer your editor to a terminal? coop serves the sandboxed agent over ACP, so any editor that speaks the protocol drives it while it runs in the box — your editor is the cockpit, the box stays the cage.

ACP (the Agent Client Protocol) is an open, editor-agnostic standard. coop acp [agent] speaks it over stdio, so you register coop as a custom agent in any editor that supports ACP and steer it from there. Zed ships ACP today and others are adding it; the setup is the same anywhere — point one entry at each agent you use. In Zed's settings.json, for example:

{
  "agent_servers": {
    "coop": {
      "type": "custom",
      "command": "coop",
      "args": ["acp", "claude"]                      // one agent; pin the target: "claude:opus/xhigh@work"
    },
    "coop · fusion": {
      "type": "custom",
      "command": "coop",
      "args": ["acp", "fusion", "claude:opus/xhigh", "--peer", "codex"] // a council; governor's model/effort ride the target
    }
  }
}

Pin the model, reasoning effort, and account in coop's target inside argsprovider[:model][/effort][@account] — not the editor's per-option defaults; the toolbar dropdowns come up reflecting it and stay switchable, and coop always runs yolo whatever the editor's permission-mode setting. One caveat: a codex governor takes its model from codex's own config.toml (coop can't set it over ACP), so pick a claude, gemini, or grok governor when you want coop to choose the model.

Because the repo mounts at its real path, sessions line up across tools — a thread you started with coop loop is right there to resume in your editor.

coop's proxy owns the editor session. The toolbar grows a coop dropdown to switch the credential or preset mid-thread — the conversation survives the switch. Every session runs yolo (the box is the boundary, so there are no permission prompts to click). A rate limit is handled for you: coop rotates to your next signed-in account and re-sends your prompt — or, with every account cooling, waits out the nearest reset and sends then. And a box restart (coop build / coop update, an OOM) replays the handshake, so your editor never disconnects.

Building a website? List the dev server's port under serve: in a committed .agent/project.yaml and the thread announces the stable http://localhost:<port> URL the box publishes it at (bind the server to 0.0.0.0 inside the box).

GUI apps don't always inherit your shell's PATH. If your editor can't find coop, use the absolute path from command -v coop as the command.

Toolchain & services

Real projects need a language toolchain and a database. Declare them once, instead of having the agent reinstall them on every run.

If your repo pins versions in a .tool-versions (asdf), the box provisions that toolchain the first time and caches it — so a repo with nothing but a .tool-versions just works. For a fully reproducible image, coop init --stack asdf bakes the same versions into a Dockerfile.agent at build time.

Need a Postgres or Redis alongside? They're opt-in — coop init asks, or pass --services postgres,redis — and run as their own throwaway containers the box can reach by name:

coop up        # start the services, wait until healthy
coop claude    # the box reaches each service by its Compose name
coop down -v   # stop them and wipe the throwaway data

The final coop up status names the exact non-empty services resolved by Compose, in Compose order. If resolution fails, Coop stops before startup and tells you to fix the reported cause and retry coop up.

To pull the latest agent CLIs and base image, coop update rebuilds the box fresh; coop build is the stable, pinned rebuild.

Configuration

Sensible defaults out of the box; a handful of knobs for when you need them.

Set these as environment variables or in ~/.config/coop/coop.conf. The ones you're most likely to touch:

Variable What it does
COOP_GATE a command re-run in the box before a fork merge lands (e.g. make check)
COOP_EGRESS none cuts the box off the network entirely (loopback only). An agent can't reach its model API this way, so it isn't a working mode — it's for forensics: quarantine a box you think is compromised, then exec in to see what it did. The default is open.
COOP_RUNTIME force container / docker / podman instead of auto-detect
COOP_MEMORY · COOP_CPUS cap the box's resources (e.g. 4g, 2)
COOP_<AGENT>_MODEL an agent's default model everywhere it runs (e.g. COOP_CLAUDE_MODEL=opus)
COOP_LOOP_MODEL the model for loop iterations only — overnight runs on a cheaper model than interactive ones
COOP_CAFFEINATE on by default: while a loop runs, hold a sleep inhibitor (macOS caffeinate) so the machine doesn't idle-sleep mid-drain; set 0 to disable
COOP_SPINNER 1 Animate Coop's live-view spinners: five-column Box Run beside progress bars and one-column Corner Run (◰ ◳ ◲ ◱) in dense task rows. Set to 0 or false to freeze them and suppress the loop's fast repaint ticker while debugging or recording.
COOP_HOMES on by default: mount your per-agent home dirs into the box; COOP_HOMES=0 keeps them out so an agent can't read your host agent configs
COOP_CONF relocate the coop.conf file coop reads its COOP_* defaults from
NO_COLOR present at any value disables ANSI color everywhere (no-color.org)

On Docker and Podman the box additionally runs with every Linux capability dropped, no-new-privileges, and a process cap. The complete list of variables lives in the README reference.

Troubleshooting

The handful of things that trip people up, and the quickest fix for each.

Symptom Fix
"no container runtime found" Install Apple container, Docker, or Podman, then coop build && coop doctor.
"image isn't built" Run coop build once.
Login hangs / "usage limit reached" Re-run coop login <agent>. A subscription cap resets on a schedule — a loop waits it out on its own.
A detached loop won't quit coop fork logs <name> -f to watch it; coop fork stop <name> to stop it.
A secret is still visible Run coop doctor to see what's shadowed, then add the path to .coopignore.

The full troubleshooting guide and every command's flags are in the README — or run coop help (and coop <command> --help) for the live reference.

Stuck on something not here? Open an issue.