On this page

Documentation

co:op runs coding agents in a sandbox on your computer, so you can give them full freedom without risking your files and keys.

The idea

Coding agents do their best work with the guardrails off, free to edit any file and run any command without stopping to ask. But the same freedom lets them read your SSH keys, upload your code or delete the wrong folder, and nobody can review every command an agent runs.

co:op runs the agent in a box: a disposable container that holds only the repo you're working on. Your home folder, SSH keys, cloud credentials and other projects aren't in it, and the repo's known secret files, like .env files and private keys, are replaced with empty files. Inside the box, the agent can do anything.

Interactive sessions, forks, loops and preset roles all run in the same box.

One exception: an editor connected over ACP can act on your computer for the agent, with the editor's own permissions. See Editors.

Install

co:op is one static binary, with no Go toolchain or clone needed. It runs on macOS and Linux (amd64 or arm64), with Git and a container runtime, and installs with one command:

  • curl -fsSL https://coop.dryga.com/install.sh | sh

The script puts coop in ~/.local/bin. If it finds a container runtime, it also builds the sandbox image and runs coop doctor to check it. If you install the runtime later, run those two steps yourself:

  • coop build --egress open && coop doctor

Docker is required for online provider runs, including open networking, because it supports a private credential broker for each run. Apple container (macOS 26+) remains available for raw uncredentialed open-network workloads. co:op detects both and prefers Docker.

The one-line install trusts coop.dryga.com, which GitHub Pages serves from the repository's main branch, since it runs the script it downloads. The script then checks the release checksum with a local SHA-256 tool and stops if it can't; with cosign on your PATH, it also checks that the checksum came from the exact release workflow and tag. To verify the download before running anything, follow the manual steps.

You can also build co:op from source:

  • git clone https://github.com/AndrewDryga/coop && cd coop && make install

Quickstart

With co:op installed, three commands take you from a repo to an agent at work in the box. Sign in once, then set up your project from its folder and start the agent:

  • coop login claude# sign in, once per account
  • coop init# set up the project
  • coop claude# start the agent in the box

coop init gives the project its settings and a task queue, and turns on filtered networking. coop claude starts Claude Code in the box. Arguments after the agent's name go to the agent, so coop claude --continue resumes your last session. co:op reads a few options itself, such as --peer and --egress. To send one of these to the agent, put it after --.

For a bigger or riskier change, you don't have to work in your checkout at all. Write the work down as a task, and let a fork do it in its own copy while you keep working:

  • coop tasks add "Add JSON output"# queue the work
  • coop fork feature claude --loop# a fork works on it
  • coop fork review feature# review it like a PR
  • coop fork merge feature# then merge it

Before you start the fork, fill in the generated task.md with the problem, what done means, the approach and the subtasks. More in Forks and The loop.

Run an agent

coop claude is one way into the box. The other agents start the same way, and so do a plain shell and a single command when you want to work in the box yourself:

  • coop claude# Claude Code
  • coop codex# Codex, in the same box
  • coop gemini# Gemini
  • coop grok# Grok
  • coop shell# a shell in the box
  • coop run -- npm test# one command in the box

Every agent starts with its permission prompts turned off, so it works through a task without stopping to ask at each step.

Why skipping the prompts is safe here

Permission prompts protect your computer from the agent. The box already does that, so in the box the prompts only slow the agent down.

Before the agent starts, co:op shows you what it set up: which secrets it hid, which account it signed in with and what the box can reach on the network.

~/code/shop $ coop claudeProtecting secrets  ✓ 2 secret paths hidden from the boxConnecting account  ✓ Claude (default) · Signed inConfiguring network access  ✓ Anthropic endpoints allowed  ✓ Everything else blockedStarting Claude Code

The sandbox

The box is a container with one folder from your computer in it: your repo. What co:op mounts into the box decides what the agent can reach.

co:op mounts your repo at the same path it has on your machine, so the agent edits your real files: you watch changes land in your editor as they happen, and paths match between co:op, its loops and your IDE. Apart from that folder, the selected agents' repository-local native homes and a shared cache for dependencies, nothing from your disk is in the box.

Native history and settings stay with this repository. Other projects' conversations are not mounted, and reusable provider credentials stay outside the box: a run-local broker authenticates provider requests. Local settings and history remain writable, so review changes you do not trust before reusing that repository's home.

Your Git identity, not the box's

The agent commits as you. co:op gives the box a trimmed Git config with your name and email, your global gitignore and commit.gpgsign=false, but never your signing key, so the box's commits are authored by you and left unsigned.

If you sign your commits (commit.gpgsign=true in your global Git config), co:op signs those commits afterwards on your computer, where your key lives: when an interactive run exits, after each coop loop cycle, and when coop fork merge merges a fork. Commits from an editor session aren't signed for you, and coop sign signs the current branch's unpushed commits by hand. Either way your history ends up signed by you, so a protected main that requires signatures accepts an agent's work.

Hiding secrets

Your repo is in the box, and repos hold secrets of their own: .env files, private keys, live API tokens. co:op hides those by name before the agent starts, and coop check-secrets finds the ones hiding inside ordinary files.

Files and folders that look like secrets, such as .env, *.pem, *.tfvars, secrets/, .ssh and a long list of well-known names, are shadowed: each secret file is covered by an empty read-only file, and each secret folder by an empty read-only folder. The agent can see that they exist, but they're empty and can't be written. Templates (*.example, *.sample, *.template) stay visible on purpose, so the agent still learns the shape of your config.

The built-in list can't know where your repo keeps its own secrets, like live keys in config/stripe.live.json. List those in a .coopignore file at the repo root, one pattern per line, and they're hidden the same way. Comments go on their own lines:

.coopignore

# a file name, hidden at any depth
prod.yml
# a path from the repo root
config/stripe.live.json
# a folder, hidden whole
vault/

The boundary you control is .coopignore, not .gitignore. The box mounts your whole working tree, so a secret that Git ignores but that still sits on disk (a real serviceAccount.json, say) is visible to the agent. Anything that must stay in the tree but out of the agent's reach belongs in .coopignore.

Hiding works by file name, so a token pasted inside an ordinary file is still readable in the box. coop check-secrets finds those: it scans the files you'd commit for known provider token formats and long random-looking values assigned to secret-named keys, and reports each one as file:line so you can move it out.

~/code/shop $ coop check-secrets✗ 1 possible secret found  config/client.go:12    OpenAI API keyRemove real secrets from files you intend to commit.If this is a false positive, add this entry to .coopsecretsignoreand replace <reason> with an explanation:  # config/client.go — OpenAI API key  fp-v1:b2758b3a796f81888b6f896f9c420ee855608fdcb828dfc2b83f736768171a6c # <reason>

Add --include-ignored to scan the files Git ignores too, since the box can still read them. The command exits non-zero when it finds something, so it also works as a pre-flight or CI check, and a false positive you've reviewed goes in .coopsecretsignore.

Networking

With filtered networking, the box reaches only its AI provider and the hosts you approve. Everything else is blocked, so the agent can't send your code or files anywhere else.

coop init turns filtering on for a new project, with egress: filtered in .agent/project.yaml. The box can then reach its provider's own endpoints, like api.anthropic.com for Claude, and nothing more until you approve it. The block works on the network itself, not through a proxy setting, so curl, an SDK and any subprocess are blocked the same way.

When the agent needs more, such as a documentation site, add a rule to the same file:

.agent/project.yaml

box:
  egress: filtered
  egress_rules:
    - to: {domain: "docs.example.com"}
      protocol: tls
      ports: [443]

A rule takes effect only after you approve it with coop approve in your terminal, so an agent can't widen its own access by editing the file. The coop net commands show what the box can reach, and what it tried:

  • coop approve# approve the project's rules
  • coop net# what a new run here can reach
  • coop net inspect# a run's connections and blocks
  • coop net blocked <host># how to allow a blocked host

Besides filtered, a project can be open (everything) or offline (no network), and open needs your approval too. Filtering also protects API keys: if you sign in with one, the real key stays outside the box and the agent gets a stand-in that works only for that run.

The first filtered run on a computer checks once that the host can enforce the rules, and coop net setup does that ahead of time. The networking guide lists every kind of rule and what each provider can reach.

Prove it

coop doctor tests these protections. It starts a box, tries to break out of it, and shows the result of each check.

It uses a throwaway project with test secrets, never your own files or credentials. It checks that the secrets read as empty and can't be written, that the box has no way into your computer and no root privileges, that offline mode is really offline, that tasks and credentials stay scoped to the run, and that a fork carries neither the secrets nor anywhere to push to:

~/code/shop $ coop doctorChecking the Coop box on the Docker runtimeProtecting secrets  ✓ .env is hidden  ✓ .envrc is hidden  ✓ Terraform variable files are hidden  ✓ Private keys are hidden  ✓ Custom .coopignore paths are hidden  ✓ Secret directories are empty  ✓ Links cannot reveal hidden secrets  ✓ Hidden secret files cannot be written  ✓ Environment templates stay readable  ✓ Source files stay readable  ✓ The test secret cannot be readHost access and privileges  ✓ The host Coop CLI is absent  ✓ The host Docker socket is absent  ✓ The box runs as a non-root user  ✓ Linux capabilities are removed  ✓ The process limit is enforced (4096)Offline access  ✓ Offline mode leaves only the loopback interfaceTask access  ✓ The task channel exposes only its 8 task tools  ✓ Calls outside the task tools are refused  ✓ Changes to a task held by another process are refused  ✓ The assigned task can be updatedCredentials and settings  ✓ Claude's native home is available  ✓ Host account grants stay outside Claude's home  ✓ Codex's native home is hidden from Claude  ✓ Gemini's native home is hidden from Claude  ✓ Grok's native home is hidden from Claude  ✓ Claude's saved login takes priority over its environment key  ✓ Codex's environment key is hidden from Claude  ✓ Gemini's environment key is hidden from Claude  ✓ The box can write its settings directoryFork handoff  ✓ .env is absent from the clone  ✓ .envrc is absent from the clone  ✓ Secret directories are absent from the clone  ✓ Private keys are absent from the clone  ✓ Tracked source files are present in the clone  ✓ The test secret is absent from the clone  ✓ The clone's origin is a local path✓ All 37 checks passed

Run it after you install, after you update co:op and whenever you change your config. If a check fails, it tells you what to fix.

Toolchain and services

Declare your project's language toolchain and databases once, and every run starts with them ready, so the agent doesn't reinstall them.

If your repo pins versions in a .tool-versions file (asdf), coop init writes a .agent/Dockerfile that builds those versions into the box's image. coop init --stack asdf asks for the same file explicitly, and also needs it.

Databases and caches run next to the box as project services, each in its own disposable container that the box reaches by name. They're opt-in: coop init asks, or you pass --services postgres,redis. From then on:

  • coop up# start and wait until healthy
  • coop claude# reach them by Compose name
  • coop down# stop them, keep their data
  • coop down --delete-volumes# also delete their data

Services are isolated like the box. Secrets in the files you mount into them are hidden the same way, and their ports open only on your computer. With filtered networking, the box reaches only services you approved with a to: {service: "db"} rule. Those services run on a private network with no direct internet; their HTTPS goes through the same filter. If a service's definition changes, it needs your approval again.

When coop up finishes, it names the services Compose resolved, in Compose order; if Compose can't resolve them, co:op stops before starting anything and tells you what to fix. Agents can change data in any service they can reach, so give them disposable development databases. Deleting service volumes can't be undone, which is why it asks first.

When the base image needs refreshing, coop update updates co:op itself and then rebuilds the box on fresh packages, and coop update --box-only rebuilds just the box. coop build is the stable, pinned rebuild (coop build --egress filtered for a project with filtered networking). Each one installs the agent versions your co:op release pins.

Credentials and failover

Each agent runs on an account, and a long run can hit its usage limit. If you sign in to more than one account, co:op switches to the next one instead of stopping.

Each account you sign in to becomes a named credential:

  • coop login claude@work# sign in to a work account
  • coop login claude@personal# and a personal one
  • coop credentials# list accounts and sign-ins

When a loop hits a limit on one account, it moves to another and keeps going, and waits only when every account is at its limit. Limits are tracked per model and account, so claude:opus@personal stays usable while claude:opus@work waits for its reset. Switching loses no work, because the loop keeps its progress in the task queue and the repo.

A loop uses all your signed-in accounts without any setup: a bare provider:model step in its target ladder covers every account, the default first. A pinned provider:model@account runs on just that one, and coop credentials <agent> <name> default chooses the account a plain coop <agent> uses.

API keys

To use an API key instead of a subscription, put ANTHROPIC_API_KEY (Claude), OPENAI_API_KEY (Codex) or GEMINI_API_KEY (Gemini) in ~/.config/coop/agents/env. Keys work with open or filtered networking on Docker. A run-local provider broker keeps the real key outside the box; subscription credentials use the same boundary. Other Claude and Codex token variables, Vertex's GOOGLE_API_KEY and Grok's XAI_API_KEY are refused; the credential reference lists every supported way to sign in.

A key in that file counts as the provider's default credential. A bare KEY line copies the value from your shell, and is skipped when it's unset; a later line wins over an earlier one. The key stands for one default account, so when a named account you signed in to runs, co:op leaves the key out to keep it from overriding that account, even if that account is the default.

Check limits and usage

coop usage shows each account's current limits and reset times, and what its last 30 days of tokens would cost at API prices. coop usage codex@work shows one account without changing your default. The estimate includes subscription use, but it isn't a bill: it marks partial history, missing prices and editor history it can't tie to an account, and it leaves out use on the web, on phones, on other machines and charges that aren't tokens. Quota reads use the selected host-only account and send no model prompt.

Pick the model

You choose the model separately from the account: for one run in the target, like claude:opus, or for good in a preset.

  • coop claude:opus# one run on the big model
  • coop claude:opus/xhigh# at extra-high reasoning effort
  • coop claude:opus@work# on a specific account
  • coop frontier# a preset's lead and roles
  • coop models# the models each agent offers

The model in the target wins over every default; the README lists the full order. co:op never validates a model name. Whatever the agent's CLI accepts works, a wrong name fails with the agent's own error, and new models work the day they ship.

To set reasoning effort, add it to the target after a slash: claude:opus/xhigh or codex/high, with one of low, medium, high, xhigh or max. co:op passes it straight to the agent's CLI (Claude's --effort, Codex's model_reasoning_effort, Grok's --reasoning-effort) without validating it. Gemini has no effort flag, so co:op turns low or high into Gemini's thinking setting, on the models it can map; a thinking setting you pin in your own Gemini settings still wins. Targets, the ladders in presets and .agent/loop.yaml, and COOP_<AGENT>_MODEL all accept model/effort, like opus/high.

Second opinions

Different models have different blind spots. For a hard call, the lead agent can ask another model for a second opinion, while it stays the only one changing your code.

  • coop claude --peer codex --peer gemini# two models to ask
  • coop acp claude --peer codex# from an editor

Add --peer <agent> to an interactive run, a loop, a fork's loop or an ACP entry. A peer is a target, so you can pin its model and effort, though not its account. Only those peers' credentials are mounted, and coop-consult runs them read-only: the lead still decides, edits, runs the checks and commits.

Why it stays simple

There's no extra service or protocol. Asking a peer is optional, kept for hard or risky calls, and only the lead can do it, so a peer never asks another peer or writes to the repo.

Presets

A preset keeps a whole team in one file: the lead, its fallback models and the roles it hands work to. Use one when a lead works with the same helpers every time.

This is the orchestrator pattern: a lead on a bigger model spends its tokens on planning and on putting the results together, while each role works at its own model's price. coop init installs the workflow skills but commits no starter subagents; the roles come from a preset, kept in .agent/presets/<name>/preset.yaml, and you run it by name wherever you'd name an agent: coop <name>, coop loop <name>, coop acp <name> or a fork.

  • coop presets init# write the starter "frontier" preset
  • coop presets# list presets, with any errors
  • coop loop frontier# run the loop with it

The lead's agent: holds one target or a fallback ladder of them, the same ladder a loop rotates through (a bare provider:model fans out 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 subagent inside the lead's own session, with no separate box. co:op generates it in the box (coop-<role>, from the role's model and prompt) in that agent's own subagent format, or set subagent: to reuse one you have. Because it runs in the lead's session, every lead in the preset must use the role's provider; if one doesn't, co:op stops before starting and names the role to change.
consult A read-only peer, asked with coop-consult <role>. It runs the role's target or ladder, with the role's prompt as its persona, and can't edit files.
delegate A worker that can write, run with coop-delegate. It may edit the working tree but never commits; the lead reviews the diff, runs the checks and commits. It falls back to the next model only while the tree is unchanged.

Here is a preset with one role in each mode:

preset.yaml

lead:
  agent: [claude:claude-opus-5-5, claude:claude-fable-5-1@work]
roles:
  thinker:                         # thinks in the lead's session
    mode: native
    agent: claude:claude-fable-5-1 # a coop-thinker subagent
    prompt: roles/thinker.md
  critic:                          # read-only cross-vendor review
    mode: consult
    agent: [codex:gpt-6-astra, grok:grok-4.5]
  fast:                            # cheap work that edits files
    mode: delegate
    agent: [gemini:gemini-3.8-flash, codex:gpt-6-luna]
What co:op generates from it

co:op turns the YAML into instructions for the lead (each role, when to use it, and the exact command) and mounts the coop-consult and coop-delegate wrappers. A native role's subagent (coop-<role>) is generated in the box from the role's model and prompt, never written to your repo. coop presets init writes starter prompt files under roles/, and a target you give on the command line (claude:opus@work) still overrides the recipe.

Consult and delegate ladders skip providers you haven't signed in to; every usable step's credentials are mounted in the lead's box. A consult that falls back replays its saved conversation, and a delegate falls back only if every file and the Git history are unchanged.

Forks

A fork gives the agent its own copy of your repo. You review its work like a pull request and decide what to merge.

A fork is a disposable local clone of your repo. Its origin is a local path, so the agent has nowhere to push, and because a clone carries only committed files, the secrets Git ignores never come along.

When to reach for a fork

Use one for anything bigger or riskier than a quick edit, or to try several approaches at once. The agent never touches your checkout, so you keep working while it does, and only you merge anything.

The flow is a pull request's: open, work, review, merge. coop fork review shows the fork's commits and changed files, and you can read the diff in your terminal or your IDE. Merging rebases the fork onto your current branch for a linear history, and since that rebase runs on your computer, where your key lives, the commits can be signed even though the box made them unsigned:

~/code/shop $ coop fork api codex --loop -dCreating fork: api  /Users/you/code/shop-forks/api✓ Started fork api in the background  Agent: codex  Logs:  coop fork logs api --follow  Stop:  coop fork stop api~/code/shop $ coop fork lsForks  api · ready to merge · updated 4 minutes ago    Agent: codex    Branch: api    Tasks: 1 ready to merge    Changes: +88 −6Review a fork: coop fork review <name>~/code/shop $ coop fork review api --statChanges in fork api  Into: main  2 commits · 3 files · +88 −6Commits  9c41e2a webhook: verify Stripe signatures  3a7f0db webhook: dedupe replayed events by idFiles  code:    M  src/webhook/handler.ts  +40 -6    A  src/webhook/verify.ts  +24 -0  tests:    A  tests/webhook.test.ts  +24 -0Project checks will run when you merge.~/code/shop $ coop fork merge apiMerge fork api into main  2 commits · +88 −6Merge these commits? [Y/n] yRebasing onto mainRunning project checks  Running: npm test✓ Merged fork api into main

Before your branch moves, the merge runs your project's checks, set with gate: in .agent/project.yaml (like gate: make check; coop init leaves the line there, commented out). They run in the box on the rebased code, and if they fail, nothing is merged and your branch is untouched. coop fork review --gate runs them early.

The merge also stops on anything risky coming in: secret-shaped content, files over 5 MB, and files that run on your computer, like Git hooks, .envrc, editor and agent settings, Compose files, Dockerfiles, CI config and new npm install scripts. Review them, then merge with --force; the checks still run.

With --loop, a fork works through tasks on its own: it takes them from your project's .agent/tasks/ queue one at a time, they stay in progress while you review the fork, and merging it completes them.

The loop

Most agent work is a queue of small, well-defined tasks. coop loop works through the queue on its own, overnight and through usage limits.

One long session gets worse as old work fills its context, so the loop starts a fresh agent for each task: it picks the next task, and the agent does the work, runs your checks, commits the result and marks the task done. Then the loop picks the next one, until no task it can take is left or --max-tasks is reached.

--max-tasks pauses before final review and exits 0 on a successful pause. Run the same command without the limit to finish the review; a pause is not verified completion.

Each task is a folder under .agent/tasks/, and the folder it sits in is its state. Fill in each generated task.md before you start, so the agent knows what done means:

  • coop tasks add "Fix the flaky test"# a folder per task
  • coop loop claude# work the queue
  • coop loop claude --peer codex# with a peer to ask

In a monorepo, list the members under subprojects: in a committed .agent/project.yaml, and one loop works every member's queue.

~/code/shop $ coop loop frontier━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Task 1 - Attempt 1 Make POST /checkout idempotent Agent  claude:claude-opus-5-5/xhigh@work Queue  0 completed · 1 active · 2 pending · 0 blocked━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Starting claude:claude-opus-5-5/xhigh@work✦ A retried checkout charges twice. I’ll key each order on its Idempotency-Key, so a replay returns the first charge.✎ Edit src/payments/checkout.ts⚙ Bash npm test✓ Task completed: Make POST /checkout idempotent━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Task 2 - Attempt 1 Cache the /health DB probe Agent  claude:claude-opus-5-5/xhigh@work Queue  1 completed · 1 active · 1 pending · 0 blocked━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Starting claude:claude-opus-5-5/xhigh@work✦ The probe counts orders on every call. Caching it for five seconds takes that load off the database.✎ Edit src/health.ts✓ Task completed: Cache the /health DB probe━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Task 3 - Attempt 1 Rate-limit the public API Agent  claude:claude-opus-5-5/xhigh@work Queue  2 completed · 1 active · 0 pending · 0 blocked━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Starting claude:claude-opus-5-5/xhigh@work✎ Edit src/middleware/rate-limit.ts⚙ Bash npm test✓ Task completed: Rate-limit the public API──────────────────────────────────────────────────────────────── Final review · Round 1 of 3 Agent  claude:claude-opus-5-5/xhigh@work Tasks  3 completed──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── Final review · 1 task needs more work  Rate-limit the public API Continuing the task queue────────────────────────────────────────────────────────────────━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Task 3 - Attempt 2 Rate-limit the public API Agent  claude:claude-opus-5-5/xhigh@work Queue  2 completed · 1 active · 0 pending · 0 blocked━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Starting claude:claude-opus-5-5/xhigh@work✦ Internal callers were throttled too. The limit now applies only to public routes.✎ Edit src/middleware/rate-limit.ts✓ Task completed: Rate-limit the public API──────────────────────────────────────────────────────────────── Final review · Round 2 of 3 Agent  claude:claude-opus-5-5/xhigh@work Tasks  1 completed────────────────────────────────────────────────────────────────✓ All tasks passed final review · 3/3 done

While the loop works, coop tasks watch shows the live board, including any running forks. For a status line in your shell prompt or tmux, coop prompt prints the repo's state on one line (task counts, and fork or loop activity); it only reads, so it's safe on every redraw.

Final review

When the queue is empty, a final review re-reads the tasks this run completed, checks that the Git history backs them up, and reopens anything that falls short. The loop then works the reopened tasks and reviews again. After three to five rounds, depending on how much the run did, a task that still falls short is blocked for your decision. Add your own checks under signoff.prompt in .agent/loop.yaml, and a task that fails one (changelog updated, docs regenerated, no stray TODOs) is reopened.

Usage limits

When the agent hits a usage limit, the loop reads the reset time from the agent's output and waits. If you've signed in to another account, it switches to that account and keeps going.

Decisions

For a call it shouldn't make alone, the agent blocks the task, which moves it to 50_blocked/, and writes a decision.md with the question, the options and its recommendation. The loop moves on to the other tasks, and when nothing else can move, it stops and exits with status 3, so cron or CI can tell "waiting on a decision" from "done". coop tasks decisions -i walks you through each open decision, records your answer and unblocks the task; then run coop loop again, or start it with --preflight to unblock answered tasks first. To answer "not now", run coop tasks park <id>, or press :b in the walkthrough: the task waits in the backlog with its question, and coop backlog promote brings it back.

Good fits

Small refactors, moving one pattern across dozens of files, a test-coverage or lint list, or the Friday cleanup queue you'd never get to by hand: any work made of many small, checkable steps.

The .agent/ folder

Everything the loop works from lives in the .agent/ folder that coop init creates. It's the same for every agent, and the agent reads it on every start and after each compaction. The work queue (tasks/ and the backlog) is local working state that Git ignores; the knowledge and settings beside it are committed, 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/ and 99_done/. The folder a task sits in is its state; the numbers sort ls in order, and coop tasks shows the plain names. Local working state.
the backlog Work for later, as task folders in tasks/xx_backlog/ (coop backlog). Loops never pick it up; coop backlog promote <id> moves an item into 00_todo/ when it's ready.
kb/ The knowledge base: maps of each part of the code, and the traps and gotchas the code doesn't show. Agents keep it current as they work.
kb/rules/ Your taste, as rules: each correction becomes a "do X, not Y" card.
skills/ The workflow skills (spec, work, sweep, investigate, review-board, verify-api); each agent's own folder links to them.
presets/ Team recipes: a lead and its roles, like thinker, critic and fast. See Presets.
project.yaml Project settings: a monorepo's members, the dev server ports co:op publishes, the box's network setting, and the checks a fork must pass to merge.
loop.yaml The stages of coop loop (preflight, work, between, verify and signoff), and the models and prompts they use.

Parallel forks

One loop is one agent working one queue. To get through a queue faster, run several forks at once, each taking the next task from the same queue.

Start one fork per agent, with --loop -d so it works in the background. co:op hands each fork one task at a time from the shared .agent/tasks/ queue, so no two forks do the same work:

  • coop fork api frontier --loop -d# the frontier team
  • coop fork web codex@work --loop -d# one on Codex
  • coop fork deps gemini --loop -d# and one on Gemini
  • coop tasks watch# every fork's tasks
  • coop fork merge --all# merge them all

Each fork names a target or a preset, whose lead ladder its loop rotates through; give forks that run at once different accounts, so they don't share one limit. coop fork ls shows each fork's state, and coop fork logs <name> -f its output.

Why run parallel forks

When agents write code quickly, reviewing it takes most of your time. Run the agents in parallel and review their forks in batches.

Evaluations

Evaluations show which model, preset or loop recipe works best on repeatable tasks. A separate verifier grades each result; the model never grades its own work.

Start with a free preview

The shipped core suite has three small coding tasks. This command launches no container or provider and spends nothing:

  • coop eval run core codex --timeout 35m --dry-run

A real run needs a sign-in (coop login codex), a working container runtime and a current co:op box image. If the image is missing, run coop build --egress open from a folder without a project Dockerfile. The build may download dependencies. Then run the suite and read the result:

  • coop eval run core codex --timeout 35m# run it for real
  • coop eval inspect# read the result

A real run uses your provider account and may spend credits, and a preview checks only the plan, not your credentials or whether the runtime is ready. --timeout is a total time limit, not a money cap; each case also has its own timeout. More configurations or repetitions mean more paid work.

Compare before and after

To check whether a change helped:

  1. Run a suite and inspect the result.
  2. Change your model, preset, loop recipe or co:op build, then run the same suite with the same number of repetitions.
  3. Use coop eval runs to find the two run IDs, then coop eval compare <before-id> <after-id>. The argument order decides which run is the baseline. Inspect and compare show each configuration's short fingerprint and co:op build.

Choose a suite

  • core: three single-agent tasks. Use provider targets, such as codex:gpt-6.1-sol/xhigh, not presets.
  • queue: ten tasks through the real loop, including review and signoff. Use targets or presets, and allow hours, not minutes. For your frontier preset, try coop eval run queue frontier --timeout 2h. Add --loop-config .agent/loop.yaml to test your recipe.
  • Your own: coop eval init ./evals/my-suite creates a working example. Edit suite.yaml, files/greeting/ and the separate verifiers/greeting/verify.sh. The verifier's README explains grading.
  • An example in the source tree: examples/evals/maintenance/ has three independent shell, Python and Node maintenance loops and unpaid grader controls. It isn't an official benchmark or a shipped starter.

Each target after the suite is a separate configuration, not a fallback. --repeat 3 runs every case three times, and --jobs 2 allows two agent trials at once, though each loop trial works its tasks in order. coop help eval run lists every option.

Understand failures before spending again

When a run doesn't go as hoped, coop eval inspect shows the trials that didn't pass, their recorded causes, and where to find saved diagnostics and kept workspaces; add a run ID to read an older run. Each trial ends in one of these states:

  • failed: the independent verifier rejected the work.
  • error: running or grading couldn't produce a verdict. A provider's quota refusal is not a model-quality failure.
  • timed_out or pending: the trial ran out of time or didn't start. Grading is incomplete.

An incomplete comparison has no definitive winner. For sign-in or quota refusals, check coop credentials <agent>; for timeouts, check the case's suite.yaml budget as well as --timeout. A run without a final summary may be running or interrupted: you can inspect it, but you can't compare or resume it, and running it again is a new paid run. Check the recorded provider output for private data before you share it.

The trials themselves are isolated. Each gets a private workspace without the source's Git history, and the hidden verifier is kept out of the model's mounts and runs afterwards in a separate container with no credentials or network. Your own MCP servers aren't exposed to trials, and the agents' built-in web search and fetch are off, including for co:op's peers and delegates; terminal and file tools stay available. This is a control the trusted client applies, not a firewall that checks each request, and regular co:op runs are unchanged. Results describe this workload, not how providers compare in general, and code size is a review signal, not a quality score.

Compare matching evidence

compare pairs only runs with the same suite, cases and number of repeats, and it reports missing or damaged records instead of guessing. Its report describes the difference between the runs and never names a winner. The README explains how the results are compared.

Editors

To steer the agent from your editor instead of a terminal, co:op serves it over ACP, the Agent Client Protocol. Any editor that supports ACP can then drive the agent in the box.

coop acp [target|preset] speaks ACP over stdio, so you add co:op as a custom agent in an editor that supports the protocol. Zed supports it today, and others are adding it. The setup is the same everywhere, with one entry for each target or preset you use; in Zed's settings.json, for example:

Zed settings.json

{
  "agent_servers": {
    "coop": {
      "type": "custom",
      "command": "coop",
      // one target; pin it like "claude:opus/xhigh@work"
      "args": ["acp", "claude"]
    },
    "coop · second opinion": {
      "type": "custom",
      "command": "coop",
      // a named peer; the target pins the lead's model
      "args": ["acp", "claude:opus/xhigh", "--peer", "codex"]
    }
  }
}

Pin the model, reasoning effort and account in co:op's target inside args (provider[:model][/effort][@account]), not in the editor's own options. The toolbar menus start on that target, and you can still switch them from there.

co:op sits between the editor and the agent, so in the editor:

  • A plain session's toolbar has Preset, Provider and Account menus; a session running a preset keeps the Preset menu.
  • Switching accounts, or between models of one provider, keeps the conversation. Switching providers starts a new session and carries over the messages and a one-line note for each tool call, as closely as it can.
  • At a usage limit, co:op moves to your next signed-in account and sends your prompt again, or waits for the nearest reset if every account is at its limit.
  • A box restart (coop build, coop update or running out of memory) replays the handshake, so your editor stays connected.

The agent's own permission prompts stay off, as in the terminal. But over ACP the agent can also ask the editor to read or write files on your computer and run commands in its terminal; co:op passes those requests on, and the editor's permissions, not the box, limit them. For a boundary that is only the box, use a terminal or loop run. See the ACP security note.

To open a dev server from the box in your browser, list its port under serve: in a committed .agent/project.yaml and bind the server to 0.0.0.0 inside the box. The thread then shows the address the box publishes it on, http://localhost:<port>.

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

MCP and remote sessions

Agents also use MCP servers, and co:op itself can run as a worker for another system.

Define MCP servers once, outside your repo, and co:op makes them available to supported agents, within the project's network setting, while keeping eligible HTTP tokens outside the box. The MCP guide covers local commands, remote HTTP servers, tokens and network settings.

As a remote worker, co:op connects out over TLS to a compatible controller, which supplies each job's repository, model targets, network access and limits; co:op fetches the code and runs the model. The worker keeps its private state and model credentials on its own machine, and needs Git, Git LFS and a supported container runtime:

  • coop sessions connect --controller <url> --token-file <path>

The worker connector guide has the details.

Configuration

co:op works with its defaults. These are the settings you're most likely to change.

Set them as environment variables or in ~/.config/coop/coop.conf; the last three work only as environment variables.

Variable What it does
COOP_GATE Overrides the project's gate: checks on this computer, like make check.
COOP_EGRESS filtered lets the box reach only its AI provider and the destinations you approve; coop init sets it for the project. open, the built-in default, leaves the network open. none cuts the box off entirely: the agent can't reach its model, so use it to quarantine a box you suspect, then look inside.
COOP_RUNTIME Force docker or container instead of detecting one (detection prefers Docker).
COOP_MEMORY, COOP_CPUS Cap the box's memory and CPUs, like 4g and 2.
COOP_<AGENT>_MODEL An agent's default model everywhere it runs, like COOP_CLAUDE_MODEL=opus.
COOP_CAFFEINATE On by default: while a loop runs, macOS caffeinate keeps the machine from sleeping. Set 0 to turn it off.
COOP_HOMES On by default: each agent's home folder is mounted into the box. COOP_HOMES=0 keeps them out, so an agent can't read your agents' settings; presets and peers need them, so those runs stop with an error.
COOP_SPINNER On by default: the live spinners animate. Set 0 or false to freeze the spinners and stop the loop's fast redraws while debugging or recording.
COOP_CONF Moves the coop.conf file co:op reads its COOP_* defaults from.
NO_COLOR Set to any value to turn off color everywhere (no-color.org).

On Docker, the box also runs with every Linux capability dropped, no-new-privileges and a process limit. The configuration reference lists every variable.

Troubleshooting

The problems people hit most, and the quickest fix for each.

Symptom Fix
"no container runtime found" Install Docker for online provider runs, then run coop build --egress open && coop doctor. To pick it explicitly, set COOP_RUNTIME=docker. Apple container supports raw uncredentialed open-network workloads only.
"image … not built" Run the coop build command the message names: --egress open, or --egress filtered for a project with filtered networking.
Sign-in hangs, or "usage limit reached" Run coop login <agent> again. A subscription limit resets on a schedule, and a loop waits for it on its own. An editor session moves to your next signed-in account and sends your prompt again.
Gemini says its Google sign-in client is no longer supported Run host coop login gemini and paste an AI Studio API key from the link it shows. Existing importable plain OAuth accounts also work through the broker; encrypted-only caches cannot be imported. Vertex GOOGLE_API_KEY is refused in every network mode.
A background loop won't stop Watch it with coop fork logs <name> -f, and stop it with coop fork stop <name>.
A secret is still visible Add its path to .coopignore. coop check-secrets finds secrets hiding inside files.
"permission denied" writing ~/.cache or build caches Check who owns the cache volume: coop-cache for the default user, or coop-cache-<uid>-<gid> on Linux (see id -u and id -g). If it has the wrong owner, save anything useful, remove only that idle volume, then run coop build. Never loosen your credential folder's permissions.
go or gofmt: "No version is set for command go" The box installs toolchains from .tool-versions with asdf. Add the golang version there. A filtered box installs it only at build time, so run coop build after. COOP_NO_ASDF=1 turns this off.
A pinned tool is installed but "not found" in a login shell A login shell (sh -lc, bash -l) reads /etc/profile, which resets PATH and drops asdf's shims. The box adds them back with an /etc/profile.d file; rebuild an older box with coop build to get it.
"box image is stale … run 'coop build'" You changed .agent/Dockerfile or .tool-versions since the image was built. Run coop build, and the warning goes away.
A merge refuses Commit or stash a dirty tree first. If the policy flagged a secret or a large file, review it, then pass --force. Without a terminal, pass --yes. If the gate failed on the rebased tree, the merge rolled back: fix it and run the merge again.
Zed can't find the agent Zed has to launch coop from a shell where it's on PATH (the installer puts it in ~/.local/bin). Give Zed the full path if needed, and check that coop acp runs in a terminal first.
An editor session misbehaves Turn on tracing: set COOP_ACP_TRACE=1 in the agent server's env, or touch ~/.config/coop/acp-debug, which also works on a server that's already running. co:op writes the editor and box traffic to ~/.config/coop/acp-trace-<pid>.log, which is size-bounded and pruned. It holds prompts and file contents, so treat it as sensitive.
A loop's live view shows provider activity wrong Run the loop with COOP_STREAM_TRACE=1. Each attempt writes the raw JSON lines and co:op's rendered lines under .agent/runs/<run>.streams/. They may hold prompts, tool inputs and model output, so treat them as sensitive.
A scaffolded Postgres 18 db exits 1 on coop up Older scaffolds mounted pgdata at /var/lib/postgresql/data, which Postgres 18 and later refuses. In .agent/compose.yml, move the mount up to /var/lib/postgresql. New scaffolds already do this.

The command reference lists every command's flags, and coop help (or coop <command> --help) is the live reference. Stuck on something that isn't covered? Open an issue.