{"id":51649902,"url":"https://github.com/andrewdryga/coop","last_synced_at":"2026-07-14T03:00:31.452Z","repository":{"id":364877536,"uuid":"1268620423","full_name":"AndrewDryga/coop","owner":"AndrewDryga","description":"Run a coding agent on your real repos every day — in a box it can't escape, with your secrets out of its reach, and with a whip that will make them work all night.","archived":false,"fork":false,"pushed_at":"2026-07-13T18:34:50.000Z","size":8805,"stargazers_count":29,"open_issues_count":0,"forks_count":3,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-13T20:32:11.200Z","etag":null,"topics":["agent","ai","llm","loops","sanbox","zed"],"latest_commit_sha":null,"homepage":"http://coop.dryga.com/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AndrewDryga.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-06-13T18:41:55.000Z","updated_at":"2026-07-13T18:34:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/AndrewDryga/coop","commit_stats":null,"previous_names":["andrewdryga/coop"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/AndrewDryga/coop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrewDryga%2Fcoop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrewDryga%2Fcoop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrewDryga%2Fcoop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrewDryga%2Fcoop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AndrewDryga","download_url":"https://codeload.github.com/AndrewDryga/coop/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AndrewDryga%2Fcoop/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35443979,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-14T02:00:06.603Z","response_time":114,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent","ai","llm","loops","sanbox","zed"],"created_at":"2026-07-14T03:00:21.498Z","updated_at":"2026-07-14T03:00:31.433Z","avatar_url":"https://github.com/AndrewDryga.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\n# co:op\n\n\u003cimg src=\".github/assets/coop.png\" alt=\"Coop\" width=\"180\"\u003e\n\n**Run a coding agent on your real repos every day — in a box it can't escape, with your secrets out of its reach, and with a whip that will make them work all night.**\n\n[![CI](https://github.com/AndrewDryga/coop/actions/workflows/ci.yml/badge.svg)](https://github.com/AndrewDryga/coop/actions/workflows/ci.yml)\n[![Release](https://img.shields.io/github/v/release/AndrewDryga/coop?sort=semver)](https://github.com/AndrewDryga/coop/releases/latest)\n[![Go Report Card](https://goreportcard.com/badge/github.com/AndrewDryga/coop)](https://goreportcard.com/report/github.com/AndrewDryga/coop)\n[![License](https://img.shields.io/github/license/AndrewDryga/coop)](LICENSE)\n\n\u003c/div\u003e\n\nCoding agents are most useful with the brakes off (`--dangerously-skip-permissions`,\n`--yolo`) — and that's exactly when you don't want them loose on your laptop.\nCoop runs them in a disposable container that mounts only the repo you're working\non, shadows its secrets, and can't reach your home dir, SSH keys, or other projects.\nOne command, installed once; the same box drives Claude, Codex, Gemini, and Grok.\n\n```bash\ncd ~/code/some-repo \u0026\u0026 coop claude     # a sandboxed Claude, brakes off, secrets hidden\n```\n\n\u003e **📖 New here? Start at [coop.dryga.com](https://coop.dryga.com)** — the friendly guide, with\n\u003e the *why* behind each feature, walkthroughs, and live terminal demos. This README is the\n\u003e quick reference; the site is the readable docs.\n\nIt's the working tooling behind two write-ups:\n[Running an AI coding agent you can't trust](https://dryga.com/blog/untrusted-ai-coding-agent/)\n(the sandbox) and [One brain, two agents](https://dryga.com/blog/os-for-coding-agents/)\n(the queue, hooks, and the foreman that runs it unattended).\n\n---\n\n## Contents\n\n- [Install](#install) · [Quickstart](#quickstart) · [Command reference](#command-reference)\n- [The sandbox](#the-sandbox) — what's mounted · secrets shadowed · git identity · `coop doctor`\n- [Forks](#forks-hand-off-work-like-a-pr) — open · review · land work like a contractor's PR\n- [Agents \u0026 config](#agents--config) — authentication · credentials · models · presets · instructions · MCP servers\n- [Fusion](#fusion-a-governed-council) — a council of models that argues before it commits\n- [Drive it from Zed (ACP)](#drive-it-from-zed-acp)\n- [Run it unattended](#run-it-unattended) — the loop · the `.agent/` folder · monorepos · a fleet\n- [Project toolchain \u0026 services](#project-toolchain--services) — `.tool-versions` · `Dockerfile.agent` · services · dev-server ports\n- [Configuration](#configuration) · [Troubleshooting](#troubleshooting) · [Layout \u0026 development](#layout--development)\n\n---\n\n## Install\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/AndrewDryga/coop/main/install.sh | sh\n```\n\nDownloads the prebuilt `coop` binary for your OS/arch into `~/.local/bin` — no Go, no\nclone. If a container runtime is present, the installer also builds the sandbox image\nand runs `coop doctor`; otherwise do that once yourself:\n\n```bash\ncoop build \u0026\u0026 coop doctor\n```\n\n**Requirements:** a container runtime — Apple\n[`container`](https://github.com/apple/container) (macOS 26+), Docker, or Podman,\nauto-detected. `coop` itself is a single static binary with no other dependencies.\n\n**Staying current:** [`coop update`](#keeping-the-box-current) self-updates the binary\n*and* rebuilds the box image fresh, pulling the latest agent CLIs and ACP adapters\n(they ship features often) plus a newer base. (Re-running the install one-liner still works.)\n\n\u003cdetails\u003e\u003csummary\u003e\u003cb\u003eOther ways to install\u003c/b\u003e\u003c/summary\u003e\n\n```bash\ngit clone https://github.com/AndrewDryga/coop \u0026\u0026 cd coop \u0026\u0026 make install   # from source\n```\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003e\u003cb\u003eVerifying a download\u003c/b\u003e\u003c/summary\u003e\n\n`install.sh` verifies automatically: when [cosign](https://github.com/sigstore/cosign)\nis on your `PATH` it checks `checksums.txt`'s keyless Sigstore signature (so the\nchecksum file itself is trusted, not just internally consistent) and aborts on failure;\notherwise it compares the archive's SHA-256 and prints that the signature was not\nverified. To verify by hand, set `VER` and `ASSET` for your platform — e.g.\n`VER=v0.1.0 ASSET=coop_0.1.0_darwin_arm64.tar.gz`:\n\n```bash\nbase=\"https://github.com/AndrewDryga/coop/releases/download/$VER\"\ncurl -fsSLO \"$base/$ASSET\"\ncurl -fsSLO \"$base/checksums.txt\"\ncurl -fsSLO \"$base/checksums.txt.bundle\"\n\n# 1. checksums.txt is signed by the release workflow (keyless Sigstore):\ncosign verify-blob checksums.txt \\\n  --bundle checksums.txt.bundle \\\n  --certificate-identity-regexp '^https://github.com/AndrewDryga/coop/' \\\n  --certificate-oidc-issuer https://token.actions.githubusercontent.com\n\n# 2. the archive matches the now-trusted checksum (Linux: sha256sum -c -):\nawk -v f=\"$ASSET\" '$2==f{print $1\"  \"f}' checksums.txt | shasum -a 256 -c -\n```\n\u003c/details\u003e\n\n## Quickstart\n\nFrom nothing to disposable agents draining a task queue, sandboxed:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/AndrewDryga/coop/main/install.sh | sh\n# ^ installs the binary, and (if a runtime is present) builds the box + runs coop doctor\n\ncd ~/code/your-repo        # 1. any git repo\ncoop init                  # 2. scaffold AGENTS.md, the .agent/ queue, and the hooks\ncoop login claude          # 3. authenticate once (paste-code, no browser; token persists)\ncoop doctor                # 4. prove isolation holds (run 'coop build' first if needed)\n\n# 5. queue a few tasks (a folder each under .agent/tasks/00_todo/)…\ncoop tasks add \"Add a /health endpoint\"\ncoop tasks add \"Backfill tests for the parser\"\ncoop tasks add \"Document the config file\"\n\ncoop loop claude           # 6. disposable agents work the queue until done, then sign off\n```\n\nPrefer to steer an agent yourself? Skip the queue and go interactive:\n\n```bash\ncoop claude                                   # sandboxed Claude — no permission prompts, secrets shadowed\ncoop codex                                    # same box, Codex instead\ncoop gemini                                   # ...or Gemini\ncoop fusion claude --peer codex --peer gemini # a council: claude leads, named peers advise, then it synthesizes\ncoop shell                                    # a shell in the box, to look around\ncoop run -- npm test                          # run any command in the box\n```\n\nPoint it at a repo and go. Each agent launches with its own \"don't stop to ask\" flags\n(`--dangerously-skip-permissions`, `--dangerously-bypass-approvals-and-sandbox`,\n`--yolo`), all inside the same sandbox. The worst an off-the-rails agent can do is\ntrash one repo you can restore from git.\n\nAnything after the agent name is passed through to it, on top of those flags — so\n`coop claude --continue` resumes Claude's last session, still sandboxed. (Codex is the\nexception: its `-p` is `--profile`, not a prompt, so run a one-shot prompt with\n`coop codex exec \"…\"` and use `-p` only to pick a profile.)\n\nIf `coop doctor` says the image isn't built, run `coop build` once. Stuck on any step?\nSee [Troubleshooting](#troubleshooting). New to forks and reviewing agent work like a\nPR? Jump to [Forks](#forks-hand-off-work-like-a-pr).\n\n## Command reference\n\nEvery command runs against the repo in your current directory. `-h`/`--help` works on\nany of them.\n\nThe groups below match `coop help` — same commands in each, with more of the flags\nspelled out here (there's room to render them).\n\n**Agents**\n\n| Command | What it does |\n|---|---|\n| `coop claude` · `codex` · `gemini` · `grok` `[args]` | a sandboxed agent — its autonomous flags, plus any args you add |\n| `coop fusion \u003cgov\u003e --peer \u003cagent\u003e…` | a [governed council](#fusion-a-governed-council): the governor leads, the named peers advise |\n| `coop acp \u003cagent\u003e[:model][@account]` · `coop acp \u003cpreset\u003e` | run as an [ACP](#drive-it-from-zed-acp) agent over stdio (for Zed) — coop owns the toolbar (credential/preset switch, yolo) and rides out box restarts and rate limits for you; pin a per-entry model/account in the target (or name a preset in the same slot); name each peer with `--peer \u003cagent\u003e` (repeatable) to let it ask them read-only, or drive a council with `coop acp fusion \u003cgov\u003e --peer \u003cagent\u003e…` |\n| `coop \u003cagent\u003e --peer \u003cpeer\u003e…` | [opt-in second opinion](#second-opinions---peer) — name each peer with `--peer \u003cagent\u003e` (repeatable); may ask those peers on hard calls |\n| `coop \u003cpreset\u003e` | run an [orchestration preset](#presets-the-whole-arrangement-in-one-yaml-file) interactively — its lead leads, its roles ride along (a preset name shares the who-runs slot with an agent target) |\n| `coop \u003cagent\u003e:\u003cmodel\u003e` | [pick the model](#picking-models) for that run — works on agent runs, fusion, forks, the loop, and acp |\n\n**Credentials, models \u0026 presets** ([details](#agents--config))\n\n| Command | What it does |\n|---|---|\n| `coop login \u003cagent\u003e[@\u003cname\u003e]` | [authenticate](#authentication) an agent (token persists in the config dir); `@\u003cname\u003e` adds a second account |\n| `coop credentials [agent [credential]]` | list stored credentials + which are signed in; a path grammar edits one (e.g. `coop credentials claude work default` · `… rm`) |\n| `coop models [agent]` | the model menu per agent ([picking models](#picking-models)) — set one in the target (`\u003cagent\u003e:\u003cmodel\u003e`) or a [preset](#presets-the-whole-arrangement-in-one-yaml-file) |\n| `coop presets [name]` | list [orchestration presets](#presets-the-whole-arrangement-in-one-yaml-file) (lead + roles) or show one; `coop presets init` scaffolds the frontier recipe |\n\n**The box**\n\n| Command | What it does |\n|---|---|\n| `coop run -- \u003ccmd\u003e` | run any command in the box (raw — none of coop's agent flags) |\n| `coop shell` | a shell in the box, to look around |\n\n**Forks** — hand off work like a PR ([details](#forks-hand-off-work-like-a-pr))\n\n| Command | What it does |\n|---|---|\n| `coop fork \u003cname\u003e [agent] [--new]` | open or re-enter a [secrets-free fork](#forks-hand-off-work-like-a-pr) + run an agent (re-entry resumes the session; `--new` resets) |\n| `coop fork ls` | list this repo's forks: agent, branch, state, tasks done/total, change size, last activity |\n| `coop fork review \u003cname\u003e [--stat\\|--tool]` | dossier + diff; `--stat` = dossier only, `--tool` = your `git difftool` |\n| `coop fork merge \u003cname\u003e [--all] [--yes]` | rebase the fork onto your branch and land it (`--all` = the whole fleet; `--yes` confirms non-interactively) |\n| `coop fork logs [name] [-f]` · `stop \u003cname\u003e` | tail a loop log (no name = all) · stop a detached loop |\n| `coop fork rm \u003cname\u003e [--force] [--yes]` | discard a fork — confirms first (`--yes` skips it; refuses unmerged/dirty work without `--force`) |\n| `coop fork open \u003cname\u003e` · `path \u003cname\u003e` | open the fork in your editor · print its filesystem path |\n| `coop fork \u003cname\u003e \u003cagent\u003e --loop [--tasks \u003cpath\u003e] [-d]` | loop a tasks queue unattended in the fork (`-d` detaches) |\n| `coop fork \u003cname\u003e acp [agent]` | drive the fork's [sandboxed agent from Zed](#drive-it-from-zed-acp) over ACP |\n\n**Unattended** ([details](#run-it-unattended))\n\n| Command | What it does |\n|---|---|\n| `coop loop [\u003cagent\u003e[:model][@account] \\| \u003cpreset\u003e] [--tasks \u003cpath\u003e] [--peer \u003cpeer\u003e…] [--preflight] [--debug-on-fail]` | work the [`.agent/tasks/`](#the-loop) queue until done, then sign off (name the agent — `claude`/`codex`/`gemini` — or a preset name in the same slot, whose lead supplies it); `--tasks` picks the queue (default `.agent/tasks`, repeatable); the target's model / the preset's ladder set the [rotation](#picking-models); name each peer with `--peer \u003cagent\u003e` (repeatable) so iterations may ask them read-only; `--preflight` tidies `.agent/` state first; `--debug-on-fail` opens a box shell on a failure |\n| `coop fleet init` · `up` · `down` · `watch` · `prune` | scaffold then drive a [declared fleet](#a-fleet) from `.agent/fleet.yaml` (`init` writes a documented template; `watch` is the live board; `prune` clears merged forks; `coop tasks split \u003cn\u003e` bootstraps the file) |\n\n**Tasks** — a folder-per-task queue in `.agent/tasks/` ([details](#the-loop))\n\n| Command | What it does |\n|---|---|\n| `coop tasks ls` | show the queue, grouped by state (a folder per task; its directory *is* its state) |\n| `coop tasks watch` | live board of the queue + any active forks, merged and deduped by id (auto-exits when every task's done; Ctrl-C anytime) |\n| `coop tasks add \"\u003ctitle\u003e\"` · `claim` · `block` · `unblock` · `done` · `rm` | move one task through its states (moving its folder is the state change) |\n| `coop tasks decisions [-i]` · `lint` · `split \u003cn\u003e` | what's blocked on a decision (`-i` to answer) · check the tree · carve todo tasks into per-fork slices |\n| `coop backlog` · `add \"\u003ctitle\u003e\"` · `promote \u003cid\u003e` · `rm \u003cid\u003e` | park unscheduled ideas in the `xx_backlog/` drawer — same folder format, but outside the lifecycle (never auto-worked, never nagged); `promote` moves one into `00_todo/` when it's ready |\n\n**Services** — the box's `.agent/compose.yml` sidecars ([details](#services))\n\n| Command | What it does |\n|---|---|\n| `coop up` · `down [-v]` | start/stop [sibling services](#services) (Postgres, Redis) for this repo |\n\n**Safety** — prove the box holds, catch committed secrets\n\n| Command | What it does |\n|---|---|\n| `coop doctor` | [prove isolation](#prove-it-coop-doctor) — attack the box and check it holds |\n| `coop check-secrets [--include-ignored]` | scan committed files for secrets by content — `--include-ignored` widens to the [whole visible tree](#secrets-never-enter-the-box) (exit 1 on a hit) |\n\n**Set up \u0026 maintain**\n\n| Command | What it does |\n|---|---|\n| `coop init [--stack asdf]` | [scaffold](#project-toolchain--services) the queue, hooks, skills, and [starter subagents](#the-orchestrator-pattern) (and optionally a toolchain) |\n| `coop build` · `update` | build the box image · [self-update coop + rebuild it fresh](#keeping-the-box-current) (latest agents/adapters) |\n| `coop completion \u003cshell\u003e` | shell tab-completion (bash, zsh) |\n| `coop help` · `version` | print help · print the version |\n\n## The sandbox\n\nThe repo is bind-mounted into the box at the same path it has on your machine, so\nthe agent edits real files and you see them live in your editor. Everything else —\nyour home dir, SSH keys, the rest of the disk — simply isn't in the container.\n\n### Secrets never enter the box\n\n`.env`, `*.tfvars`, `*.pem`, `secrets/`, `.ssh`, and friends are shadowed: an empty\n`tmpfs` over secret directories, a read-only empty file over secret files. Templates\n(`*.example`, `*.sample`, `*.template`) stay visible. The defaults are compiled in\n(`internal/box/secrets.go`); add a `.coopignore` at the repo root for your own:\n\n```gitignore\nprod.yml                 # basename — matched at any depth\nconfig/credentials.yaml  # a slash makes it a repo-relative path\nvault/                   # a directory — its contents are hidden whole\n```\n\n**The boundary is `.coopignore`, not `.gitignore`.** A normal `coop run`/`loop`/`shell`\nbinds your *whole* working tree, so a gitignored-but-present file (e.g. a\n`serviceAccount.json`) is **fully visible** to the agent — shadow it with `.coopignore`.\nFor a token hiding *inside* a file, `coop check-secrets` scans by content (`file:line`,\nexit 1 on a hit); `--include-ignored` widens it to the whole visible tree. Prove your\nsetup holds with [`coop doctor`](#prove-it-coop-doctor).\n\n\u003e Full walkthrough — subdirectory scoping, template re-hiding, the fork exception:\n\u003e [**coop.dryga.com/docs.html#secrets**](https://coop.dryga.com/docs.html#secrets).\n\n### Your git identity, not the box's\n\nThe box has no `~/.gitconfig` of its own, so coop mounts a curated one into every run:\nyour global `user.name`/`user.email` (commits are authored as you), your global\ngitignore (`core.excludesfile`), and `commit.gpgsign=false` — the box holds no\nsigning key, so without that a global `gpgsign=true` would fail every commit. Your key\nnever enters the box; commits made inside it are unsigned. (Forks can still land\nsigned — see [Landing](#land-it-rebase-gate-sign).)\n\nSince box commits are unsigned, a remote that requires signed commits (a protected `main`)\nwould reject them. If you sign by default (`commit.gpgsign=true`), coop re-signs on the HOST,\nwhere your key lives: `coop loop` signs each cycle's commits, an interactive/run/fusion box\nsigns its session's commits on exit (a dirty tree is skipped — commit or stash, then\n`coop sign`), and `coop sign` re-signs the unpushed range (`@{upstream}..HEAD`, or `--from\n\u003cref\u003e`) anytime. For editor (ACP) sessions, `coop prompt` shows an `unsigned` nudge when HEAD\nis unsigned — run `coop sign` before pushing. It never pushes or rewrites pushed history.\n\n### Prove it: `coop doctor`\n\n```bash\ncoop doctor\n```\n\n`doctor` plants a fake secret, launches the box, and checks from inside that the\nsecret is unreachable and unwritable — then checks on the host that a fork carries\nneither the secret nor a pushable remote. Run it anytime, especially after changing\nconfig.\n\n## Forks: hand off work like a PR\n\nA fork is a throwaway local clone of your repo, handed to an agent instead of your\nworking tree. It's an extra layer of isolation (the agent never touches your checkout)\nand the unit of parallelism (run several at once). Because a fork's `origin` is a local\npath, the agent has nowhere to push — and gitignored secrets were never committed,\nso they don't come along. You stay the reviewer and the only one who lands anything.\n\nThe lifecycle mirrors a contractor's PR: open → work → review → land.\n\n```bash\ncoop fork perf codex     # open: clone into ../\u003crepo\u003e-forks/perf, run codex there\ncoop fork perf           # re-enter: continues codex's last session by default\ncoop fork ls             # list your forks (branch, changes, state, last activity)\ncoop fork review perf    # review: the dossier, then the diff\ncoop fork merge perf     # land: rebase onto your branch, then close the fork\ncoop fork rm perf        # or discard it (confirms first; refuses unmerged/dirty work without --force)\n```\n\n`coop fork \u003cname\u003e` opens a new fork or re-enters an existing one. Name the agent as a\ntarget — `coop fork perf codex` (or `codex:gpt-5.5@work` to pick its model + account) —\nor name a preset in that same slot (`coop fork perf frontier`), whose lead runs it. A fork inherits your git identity, signing key, and global gitignore from\nthe parent — so the agent can commit *as you* and ignores the same noise you do.\n\n**Loop one unattended.** Point a fork at a task queue and it works it on its own:\n\n```bash\ncoop fork api codex --loop -d   # loops the repo's task queue(s); -d detaches — tail with coop fork logs api -f\n```\n\nSee [the loop](#the-loop) for how iterations work, and [a fleet](#a-fleet) to run several at once.\n\n### Re-entry resumes the session\n\nA fork remembers the agent it was created with, so `coop fork perf` after\n`coop fork perf codex` re-enters with *codex*, not a silent fallback to claude (pass an\nagent to switch; `coop fork ls` shows each fork's agent). The agent's session history\npersists too, so re-entry continues its last conversation instead of starting fresh.\nFor claude and gemini, coop assigns each fork its own session id (kept in the fork's\ngit-excluded `.coop/`) and resumes exactly that one — so a loop or a peer consult that\nshares the fork's directory can never hijack the \"continue\". codex can't be handed an\nid, so it resumes the most-recent *interactive* session recorded for the fork (skipping\n`codex exec` loop/consult runs). It falls back to a fresh session when none exists.\nForce one with `--new`; `--fresh` recreates the whole fork (refusing to discard unmerged/dirty\nwork without `--force`).\n\n### Review — in your terminal or your IDE\n\n`coop fork review \u003cname\u003e` opens with a dossier that maps the risk before the patch:\nthe commits; the agent's claim (the latest task `log.md`, labeled — it's the fork's own\nvoice); policy findings from the same scan `coop fork merge` enforces, so nothing first\nsurfaces as a failed merge; the changed files risk-ordered (config \u0026 instructions, then\ncode by churn, then tests, then docs, each with `+N -N`); and whether a merge gate is\nconfigured. Then the diff shows in your pager. No setup needed. To review in an IDE\ninstead:\n\n| | |\n|---|---|\n| `--stat` | dossier only, skip the diff |\n| `--tool` | open each changed file in your GUI difftool |\n\nTo review in your editor's SCM panel instead, open the fork as a folder with `coop fork\nopen \u003cname\u003e` (it uses the editor resolution below).\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e\u003ccode\u003ecoop fork open\u003c/code\u003e\u003c/b\u003e — which editor it opens\u003c/summary\u003e\n\nIt opens the fork directory with the first of these that's set:\n\n1. `COOP_EDITOR` — a coop-only override\n2. `git config core.editor` — your normal git editor (local config beats global)\n3. an auto-detected GUI editor on `PATH`: `cursor`, `code`, `zed`, `idea`, `subl`\n4. `$VISUAL` / `$EDITOR`\n\nDetection (step 3) is just a best-effort fallback — with both `code` and `zed`\ninstalled it picks `code`. To choose, set one of the first two:\n\n```bash\ngit config --global core.editor \"zed --wait\" # your standard git editor (git commit uses it too)\nexport COOP_EDITOR=\"zed\"                     # coop-only; overrides core.editor\n```\n\ncoop runs the editor command verbatim, so `core.editor = \"zed --wait\"` blocks the\nterminal until you close the window — that's what `--wait` is for. If you'd rather the\ncommand return immediately, set `COOP_EDITOR` without `--wait` (e.g. `COOP_EDITOR=zed`).\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e\u003ccode\u003e--tool\u003c/code\u003e\u003c/b\u003e — register your difftool\u003c/summary\u003e\n\n`--tool` runs `git difftool`, which opens each changed file in whatever\n`git config diff.tool` points at. Register one once:\n\n```bash\n# VS Code\ngit config --global diff.tool vscode\ngit config --global difftool.vscode.cmd 'code --wait --diff \"$LOCAL\" \"$REMOTE\"'\n\n# Zed\ngit config --global diff.tool zed\ngit config --global difftool.zed.cmd 'zed --wait --diff \"$LOCAL\" \"$REMOTE\"'\n```\n\nJetBrains, Meld, Beyond Compare, vimdiff, etc. work the same way — see\n`git difftool --tool-help` for the tools git already knows.\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003e\u003ccode\u003eCOOP_REVIEW_CMD\u003c/code\u003e\u003c/b\u003e — wire any tool\u003c/summary\u003e\n\nFor full control, set `COOP_REVIEW_CMD`. coop runs it via `sh -c` from the parent repo\nwith `$COOP_FORK_PATH`, `$COOP_FORK_NAME`, and `$COOP_REVIEW_REF` in the environment, so\nyou can launch a TUI, a script, anything:\n\n```bash\nexport COOP_REVIEW_CMD='cd \"$COOP_FORK_PATH\" \u0026\u0026 lazygit'\n```\n\u003c/details\u003e\n\n### Land it: rebase, gate, sign\n\n`coop fork merge \u003cname\u003e` rebases the fork onto your current branch and\nfast-forwards — linear history, no merge commits. The rebase happens on the host, where\nyour key lives, so if you sign commits (`commit.gpgsign=true`) the landed commits are\nsigned with your key even though the box committed them unsigned. Then `git push` —\nthe only step the agents can't do.\n\nSet a merge gate — `gate: make check` in [`.agent/project.yaml`](#agents--config) (committed,\nshared with the team) or `COOP_GATE=\"make check\"` (per-machine, and it wins) — and every merge\nre-runs that gate in the box on the *rebased* tree, rolling back if it goes red — the machine gate\nbehind your human review. A policy check also flags secret-looking or oversized files and\nscans each changed file's content for real credentials (provider token shapes —\nAWS/OpenAI/Anthropic/GitHub/Slack/… — plus high-entropy values on secret-named keys), so\na token committed inside an ordinary file is caught even though its filename is innocuous\n(override with `--force`). Merge refuses if your own tree is dirty.\n\nMerging lands work and then offers to delete the fork, so it asks first. In a\nnon-interactive shell (CI, a pipe) there's no one to answer, and merge refuses\nrather than landing on the default — pass `--yes` (`-y`) to confirm landing and\nremoval up front. `--yes` also skips the prompts interactively.\n\nMerging lands code, not queue state. A fork's loop works a *copy* of the task queue\n(`.agent/` is gitignored, seeded when the fork is created), so after a merge the parent's\n`.agent/tasks/00_todo/` still lists the tasks the fork finished. Mark them done or remove\nthem — `coop tasks done \u003cid\u003e` / `coop tasks rm \u003cid\u003e` — so a later `coop loop` doesn't\nre-claim finished work (redoing a done task makes an empty commit, which fails). While forks\nrun, `coop tasks watch` shows the deduped truth across the parent and its forks.\n\n## Agents \u0026 config\n\nOne box, four agents. Each reads its config and credentials from\n`~/.config/coop/agents/\u003cname\u003e/`, mounted into the box at `~/.claude`, `~/.codex`,\n`~/.gemini`, and `~/.grok`. That directory lives outside any repo, so credentials never land in git —\nedit those files on the host and they take effect in the box. The folders appear on\nfirst run, and each tool's normal user-level config works there as-is —\n`claude/settings.json`, `codex/config.toml`, `gemini/settings.json`,\n`grok/config.toml`. Only the *active*\ncredential is mounted, so a running agent sees just the account it's using, not the\nwhole vault.\n\nEach run mounts only the **launched agent's** credentials: `coop claude` mounts\n`~/.claude` (and that agent's API key from the env file), never the Codex or Gemini ones.\nThe exceptions are the modes where the lead is explicitly told to call its peers —\n`coop fusion \u003cgov\u003e --peer …` and `coop \u003cagent\u003e --peer \u003cpeer\u003e…` (and forks) — which\nalso mount the **named** peers so they can be consulted read-only (only those you\nname, never everyone signed in). Raw runs (`coop run`,\n`coop shell`) and maintenance runs (the merge gate, `coop doctor`) mount no agent\ncredentials at all. `coop login \u003cagent\u003e` mounts only the agent being signed in.\n\n\u003e **Blast radius.** That credential dir is mounted read-write — the agent must write its\n\u003e session history, and OAuth refresh rewrites the token in place. So a prompt-injected\n\u003e agent can (a) read its own credentials and try to exfiltrate them — set\n\u003e `COOP_EGRESS=none` to cut the box off the network — and (b) write config its CLI\n\u003e auto-loads next launch (e.g. a `settings.json` hook), which then runs in future boxes\n\u003e for that credential. (b) stays *inside* the container — not a host escape — but it's a\n\u003e durable foothold. A fuller fix (copy credentials into an ephemeral in-box location,\n\u003e persist nothing host-side) is planned; for now, `COOP_EGRESS=none` covers the exfil half.\n\n### Authentication\n\n```bash\ncoop login claude     # interactive login; the token persists in agents/claude/\ncoop login codex      # device-code login (the box has no browser for an OAuth redirect)\ncoop login gemini     # or it logs in on first use\n```\n\n…or use API keys — drop them in the env file, passed into every box:\n\n```bash\necho 'ANTHROPIC_API_KEY=sk-…'  \u003e\u003e ~/.config/coop/agents/env\necho 'GEMINI_API_KEY=AIza…'    \u003e\u003e ~/.config/coop/agents/env\n```\n\nA run only sees the API keys of the agents in its credential scope (above): a `coop\nclaude` box gets `ANTHROPIC_API_KEY` but not `OPENAI_API_KEY` / `GEMINI_API_KEY`, which\nare filtered out before the env file is passed in. Any other variable in the file (a\n`DATABASE_URL`, `COOP_NO_ASDF`, …) is a shared runtime var and reaches every box.\n\nOn first run the box pre-answers each agent's setup prompts — Claude's\ntheme/folder-trust/bypass warnings, Codex's \"trust this directory?\", and Gemini's\nfolder-trust — so a fresh install goes straight from login to work. (The box is the\nsandbox, so trusting the one mounted repo is the intended posture.)\n\n### Multiple subscriptions, with failover\n\nOne agent can hold several accounts as named **credentials** — each a stored login and\nits own rate-limit pool — so a long unattended run can ride through a subscription's cap\ninstead of parking on it (`coop profiles` was the pre-v3 name for the same thing):\n\n```bash\ncoop login claude@work        # a second account…\ncoop login claude@personal    # …and a third\ncoop credentials              # list them and which are signed in\n```\n\nWhen `coop loop` (or a `coop fork --loop`) hits a rate/usage limit it switches to the\nnext target and keeps going, only waiting once every target is limited. A [Zed (ACP)\nsession](#drive-it-from-zed-acp) does the same transparently — rotate, re-send your\nprompt, move the toolbar dropdown; or wait for the nearest reset when every account is\ncooling. There is no persistent pool to configure: the rotation *is* the model-first\n`agent:` ladder of the loop's lead. With no preset it rotates the agent's default model across every signed-in\naccount; a bare model in a ladder does the same, while a pinned `model@account` runs just\none. Limits are tracked per (model, account), so `claude-opus-4-8@personal` stays usable\nwhile `claude-opus-4-8@work` cools down. A ladder gives you **fallbacks** in the order you\nwrite them — step to a cheaper model, another account, or both:\n\n```yaml\n# .agent/presets/frontier/preset.yaml — coop presets init scaffolds this\nlead:\n  # agent: is a target, or a fallback LADDER — opus on all accounts, then fable on work\n  agent: [claude:claude-opus-4-8, claude:claude-fable-5@work]\n```\n\n```bash\ncoop loop frontier             # rotates that ladder; coop presets shows every recipe\ncoop loop claude:opus@work     # or a one-off single target, no preset\n```\n\nWhich credential a plain interactive `coop claude` uses is a mark you set, not a magic\nname — so you can name them all meaningfully. Credential properties are edited as a path\n(`coop credentials \u003cagent\u003e \u003ccredential\u003e \u003cattribute\u003e [value]`):\n\n```bash\ncoop credentials claude personal default  # `coop claude` now runs on the personal account\n```\n\nCredentials live in the vault (`~/.config/coop/agents/\u003cagent\u003e/profiles/\u003cname\u003e/`), never in\nthe repo, and only the active one is mounted into the box — so a running agent sees just\nthe account it's using, not your whole vault. Switching accounts loses no work: each loop\niteration is a fresh run, and the queue plus git carry the progress.\n\n### Picking models\n\nEvery launch names its model in the target (`\u003cagent\u003e:\u003cmodel\u003e`) — pick the model per run, on any path:\n\n```bash\ncoop claude:opus                                    # one big-model interactive session\ncoop fusion claude:fable --peer codex --peer gemini # the governor's model (peers keep their own)\ncoop loop claude:haiku                              # a cheap overnight grind\ncoop fork risky claude:opus                         # a careful fork on the big model\ncoop acp claude:sonnet                              # pin an editor entry's model\n```\n\nFor a *standing* model you don't retype, put it in a\n[preset](#presets-the-whole-arrangement-in-one-yaml-file): the lead's `agent:` ladder is\nthe model (and, on a loop, the rotation across your accounts), and each role names its own.\nPick the model in the target (`\u003cagent\u003e:\u003cmodel\u003e`) or a preset — a credential is just an account (which\nsubscription); the model is a separate axis:\n\n```bash\ncoop models                        # the model menu per agent\ncoop claude:opus                   # one run on the big model\ncoop claude:opus/xhigh             # …at maximum reasoning effort (low·medium·high·xhigh·max)\ncoop frontier                      # a standing lead model + roles, from the preset (its lead leads)\n```\n\nOne env knob rounds it out: `COOP_\u003cAGENT\u003e_MODEL` (e.g. `COOP_CLAUDE_MODEL=fable`) is the\nagent-wide default. For the loop, put the per-step model in\n[`.agent/loop.yaml`](#run-it-unattended) — `work.agent` for the iterations, `signoff.agent` for a\nstronger final reviewer over the cheaper work loop. In a fleet, give a fork its own with `agent:` (a\ntarget — `provider[:model][@account]`) in `.agent/fleet.yaml`. Precedence, most specific first: the\ntarget's `:model` › the preset ladder's active entry › `COOP_\u003cAGENT\u003e_MODEL` › a model baked into\n`COOP_\u003cAGENT\u003e_CMD` › the agent CLI's own default.\n\n**Reasoning effort** is a sibling axis on the same target — `/effort` after the model:\n`coop codex:gpt-5.6-sol/high`, `coop claude:opus/xhigh`, `coop loop claude:opus/low`. Levels are\n`low` · `medium` · `high` · `xhigh` · `max`; coop passes the level straight to the agent's CLI\n(Claude's `--effort`, Codex's `model_reasoning_effort`, Grok's `--reasoning-effort`), so a bad one\nfails in the agent's own error — and Gemini, which has no effort control, rejects a `/effort` up\nfront. It mirrors the model's tiers, and one axis carries both — a target's `:model/effort`,\n`COOP_\u003cAGENT\u003e_MODEL`, and a loop.yaml step's `agent:` all take `model[/effort]` (e.g.\n`signoff.agent: [claude:opus/xhigh]` signs off at xhigh while the work loop grinds low).\n\nThe chosen model reaches consult peers and fusion advisors too (each peer resolves its\nown default), and `coop loop`'s live view prints the model each iteration actually ran —\nthe agent's own init report, so it's ground truth, not coop's guess. coop never validates\na model id: `coop models` shows *examples*, ids churn, and whatever the agent CLI accepts\nworks — a bad one fails loudly in the agent's own error. (One gap: codex under ACP reads\nits model from its own `config.toml`; its adapter takes no flags and codex has no model\nenv var.)\n\n### The orchestrator pattern\n\nPut your strongest model in charge and let it spend the cheap tokens: the lead plans,\ndecomposes, and synthesizes, while pinned subagents execute and cross-vendor peers give\nindependent opinions. Everything below composes from pieces coop already has — no plugins.\n\n```bash\ncoop claude:claude-fable-5 --peer codex --peer gemini   # run it; --peer mounts the named peers\n```\n\nFor a *standing* arrangement (a lead model + its roles you don't retype), put it in a\n[preset](#presets-the-whole-arrangement-in-one-yaml-file) and run `coop \u003cname\u003e` (or `coop loop \u003cname\u003e`).\n\n- **Tiered subagents** — `coop init` scaffolds `.claude/agents/deep-reasoner.md` (pinned\n  to Opus: architecture, complex debugging, algorithm design) and `fast-worker.md`\n  (pinned to Sonnet: boilerplate, tests, mechanical edits). They're native Claude Code\n  subagents: the lead auto-delegates on their descriptions, each turn bills at *its*\n  model, and the lead's context stays lean. Commit them; edit them freely.\n- **Peer engineers, not reviewers** — with `--peer \u003cpeer\u003e…` (or fusion), the lead can ask\n  the named peers (e.g. codex, gemini) read-only via `coop-consult \u003cpeer\u003e`: different training, different\n  blind spots. **The `--peer` flag matters** — a plain `coop claude` deliberately\n  doesn't mount peer credentials, so peers only answer in a consult/fusion box.\n- **High-stakes calls** — task deep-reasoner *and* a peer on the same problem in\n  parallel, without showing either the other's answer, then synthesize. (This is the\n  move coop's fusion directive already teaches its governor.)\n\nThe same arrangement runs unattended: `coop loop claude:claude-fable-5 --peer codex --peer gemini`\nmakes every iteration orchestrate this way — the pinned subagents ride along in the\nrepo, and `--peer` mounts the named peers into each iteration's box (fork loops take it\ntoo: `coop fork \u003cname\u003e claude --loop --peer codex --peer gemini`). Prefer\n`coop-consult` over vendor cross-agent plugins in the box:\nnothing to install, peers stay read-only (one writer per tree), and the credential\nscoping is already handled.\n\n### Presets: the whole arrangement in one YAML file\n\nThe orchestrator pattern above is assembled by hand — a `:model` here, a `--peer \u003cpeer\u003e`\nthere. A **preset** declares the whole arrangement once, as a runtime recipe under\n`.agent/presets/\u003cname\u003e/`: who leads, and which **roles** it routes work to. Three role\nmodes cover the spectrum: `native` (a Claude subagent inside the lead's session),\n`consult` (a read-only peer via `coop-consult`), and `delegate` (a **write-capable**\ndelegate via `coop-delegate`). `.agent/presets/frontier/preset.yaml`:\n\n```yaml\nlead:\n  # agent: is a TARGET, or a fallback LADDER (model-first, even cross-provider). A bare\n  # provider:model runs on EVERY signed-in account (rotating on rate limit); @account pins\n  # one. On a loop it rotates top-to-bottom (running each rung's agent); a single run uses\n  # the first. models:/model:/credentials: are retired — the model+account ride agent:.\n  agent: [claude:claude-fable-5, claude:claude-opus-4-8@work]\n  prompt: roles/lead.md           # optional Markdown, appended to the generated contract\n\nroles:\n  thinker:                        # deep thinking + review, in the lead's own session\n    mode: native\n    agent: claude:claude-opus-4-8 # a target — the model rides agent: (coop generates coop-thinker)\n    when: [architecture, debugging, code-review, before-commit]\n    prompt: roles/thinker.md      # its system prompt (or set subagent: \u003cname\u003e to reuse one)\n\n  critic:                         # independent critique from another vendor, read-only\n    mode: consult\n    agent: codex:gpt-5.5          # a role runs on its agent's default account (no @account)\n    when: [plan-review, security, tradeoffs]\n\n  fast:                           # cheap mechanical work, write-capable\n    mode: delegate\n    agent: gemini:gemini-3.5-flash\n    when: [boilerplate, bulk-edits, test-scaffolding, repo-survey]\n    commit: never                 # it edits; the LEAD reviews the diff, gates, commits\n    concurrent: never             # delegate runs are serialized\n```\n\nRun it by naming the preset in the who-runs slot — its lead leads; a target in that same\nslot (`\u003cagent\u003e[:model][@account]`) runs the agent directly instead:\n\n```bash\ncoop presets init                # scaffold the recipe + starter prompt files, ready to edit\ncoop frontier                    # interactive lead with the full routing contract\ncoop loop frontier               # unattended: lead credentials rotate, roles ride along\ncoop fusion frontier             # council governed by the preset's lead + its consult roles\n```\n\nPresets resolve from **two locations, repo wins**: the repo's `.agent/presets/\u003cname\u003e/`\nfirst, then a per-user global dir `~/.config/coop/presets/` (`COOP_PRESETS_DIR` overrides\nit) — so a recipe like `frontier` applies across every repo without symlinking. A repo\npreset shadows a same-named global one (no merging); `coop presets` tags a global-sourced\none `(global)`. `coop presets init` scaffolds into the repo; author a global preset by hand.\n\ncoop generates the lead's routing contract from the YAML — each role, when to use it,\nand its role-addressed invocation (`@coop-thinker`, `coop-consult critic --fresh \"…\"`, or a\n`coop-delegate fast \u003c\u003c'EOF' … EOF` heredoc) — and mounts the wrappers. A **native** role\ngenerates its Claude subagent in the box — `coop-\u003crole\u003e`, from the role's model + `when` +\nprompt, never written to your repo (`.gitignore` keeps the overlay out of commits); set\n`subagent: \u003cname\u003e` to reference an existing `.claude/agents/` subagent instead. A **consult**\nrole runs its agent on the role's model, and the role's prompt (if any) is the persona the\npeer adopts — so two consult roles on one agent stay distinct. Native roles run inside the\nlead's session, so under a `codex`/`gemini` lead they **degrade to exactly such a consult** —\nsame model and persona, `coop-consult \u003crole\u003e` instead of in-session — so a non-Claude lead\nstill gets Claude's deep reasoning.\n`coop presets init` scaffolds starter `roles/lead.md`, `roles/thinker.md`, and `roles/fast.md` (usable\ndefaults, not placeholders); their Markdown feeds the generated text — never replacing the\nsafety/routing rules — and you edit or delete them freely.\n\n`coop-delegate` is the write-capable counterpart of the read-only `coop-consult`: the\ndelegate may edit the shared worktree, runs are serialized, and it must **not** commit —\nthe wrapper compares `HEAD` before and after and fails loud if the delegate committed.\nThe lead then reviews `git diff`, runs the gate, fixes or reverts what falls short, and\nmakes the commit itself. That keeps one reviewer — your strongest model — accountable\nfor everything that lands, while the cheap tokens do the typing.\n\n### Instructions, one source of truth\n\n`coop init` wires a tool-neutral setup so every agent reads the same instructions:\n`CLAUDE.md` and `GEMINI.md` symlink to a canonical `AGENTS.md`, and Codex shares\nClaude's skills directory. A real (non-symlink) instruction file you already have is\nleft untouched. A shared `~/.config/coop/agents/INSTRUCTIONS.md` is also wired into each\nagent's global instruction path in the box (`~/.claude/CLAUDE.md`, `~/.codex/AGENTS.md`,\n`~/.gemini/GEMINI.md`, `~/.grok/AGENTS.md`) — machine-level guidance that follows you\nacross repos, while project rules stay in the repo's own `AGENTS.md`. A per-agent file\nwins over the shared one: drop a `CLAUDE.md` into `~/.config/coop/agents/claude/` (or\n`AGENTS.md` into `codex/`, …) and that agent uses it instead.\n\n**`.agent/` is the only cornerstone.** `coop init` scaffolds the per-agent dirs\n(`.claude/`/`.codex/`/`.gemini/`) only for the agents you're signed in to — or the\n`--agents claude,codex` (or `all`) you name. A repo that never uses an agent can just\ndelete its dir: when a box runs an agent whose dir is absent, coop synthesizes the\nworkflow skills from the shared `.agent/skills` at the box's user-level\n`~/.\u003cagent\u003e/skills` (a writable copy — the host `.agent/` stays untouched). The one\nthing that needs the dir back is running that agent's OWN CLI on the host; boxes don't.\nWhen a repo *has* the per-agent dir, that committed copy wins and is used as-is (so a box\ncan still self-improve it).\n\n### MCP servers, defined once\n\n`coop init` seeds an empty `~/.config/coop/agents/mcp.json` (the standard\n`{ \"mcpServers\": { ... } }` shape). An empty one wires up nothing, so drop your servers\nin and every agent picks them up:\n\n```json\n{\n  \"mcpServers\": {\n    \"playwright\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@playwright/mcp@latest\", \"--headless\", \"--no-sandbox\"]\n    },\n    \"github\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@modelcontextprotocol/server-github\"],\n      \"env\": { \"GITHUB_PERSONAL_ACCESS_TOKEN\": \"ghp_replace_with_your_token\" }\n    },\n    \"sentry\": {\n      \"type\": \"http\",\n      \"url\": \"https://mcp.sentry.dev/mcp\",\n      \"bearer_token_env_var\": \"SENTRY_TOKEN\"\n    }\n  }\n}\n```\n\n`coop` wires that one file into each agent's native mechanism on launch: Claude via\n`--mcp-config`, Gemini merged into its `settings.json`, Codex — and Grok, same schema —\nconverted to `[mcp_servers.*]` in its `config.toml`. The generated versions are laid\nread-only on top of your existing config (pure Go, no extra tooling) — your own files\nare never touched, and servers from `mcp.json` win on a name clash.\n\nAn `env` block on a command server (`github` above) reaches that server under every\nagent, verbatim — values are literal strings, no `$VAR` substitution. To keep a token\nout of `mcp.json`, point `bearer_token_env_var` at a variable (`sentry` above) and put\nthe value in the env file: `echo 'SENTRY_TOKEN=…' \u003e\u003e ~/.config/coop/agents/env`. An\nHTTP server's `headers` work for Claude and Gemini; Codex and Grok authenticate only\nvia `bearer_token_env_var`.\n\nThe example's Playwright server works in the box out of the box: Chromium's system\nlibraries are baked into the image, the browser binary downloads to the cache volume on\nfirst use, and the server runs `--headless --no-sandbox` (the box is already the sandbox).\n\n## Fusion: a governed council\n\nOne model leads (the *governor*) and does the real work; the peers you name with `--peer` advise\nread-only; the leader synthesizes the best of all three. A council that argues\nbefore it commits beats any of its members working alone — the synthesized answer\noutperforms even the single strongest model on its own, Fable 5 included. You stop\nbetting the run on one model's blind spots. It's a mode like any other agent —\ninteractive, headless, or in Zed:\n\n```bash\ncoop fusion codex --peer claude --peer gemini    # codex governs (named explicitly); named peers advise\ncoop fusion claude --peer codex --peer gemini    # claude governs instead\ncoop fusion claude --peer codex --peer gemini -- -p \"Design the retry strategy\"   # headless; args after -- pass to the leader\n```\n\nNo extra service or protocol behind it: the leader is just that agent running\nnormally (it edits, runs the gate, streams), plus a fusion instruction injected into\nthe leader's instruction file only. For a non-trivial question it consults its peers\nread-only and in parallel through a small mounted wrapper:\n\n```bash\ncoop-consult claude --fresh    \"\u003cprompt\u003e\"   # new read-only session; never edits\ncoop-consult gemini --continue \"\u003cprompt\u003e\"   # resume the peer's thread; send only the delta\n```\n\nThe peers are read-only advisors: they analyze and report, and the leader makes\nevery change itself — even when the task *is* a change, it consults on the thinking\nand does the writing. A peer has none of the leader's conversation, so the leader\ncomposes a self-contained prompt rather than forwarding your message verbatim — a\nfollow-up like \"fix the second one\" means nothing to a peer that never saw the\nthread. `coop-consult` adds optional continuity: `--fresh` starts a new session,\n`--continue` resumes the peer's own prior consult so a follow-up can send just the\ndelta instead of re-pasting context (it prints whether it continued or fell back to\nfresh, so the leader knows when to resend). It hides the per-agent session-id\nmechanics — claude/gemini start under a generated id, codex's is captured from its\nJSON stream. The leader then merges the strongest parts, resolves disagreements by\nverification, and proceeds. Because the instruction lands only on the leader, the\npeers it spawns read their normal instructions and never recurse into a council of\ntheir own. Each consultation is two extra read-only runs, so it's for decisions and\nhard problems, not every keystroke — the leader is told to skip the council for\ntrivial steps.\n\n**In Zed:** add one entry per leader and pick who governs from the agent dropdown:\n\n```json\n\"agent_servers\": {\n  \"coop fusion (codex)\":  { \"command\": \"coop\", \"args\": [\"acp\", \"fusion\", \"codex\", \"--peer\", \"claude\", \"--peer\", \"gemini\"] },\n  \"coop fusion (claude)\": { \"command\": \"coop\", \"args\": [\"acp\", \"fusion\", \"claude\", \"--peer\", \"codex\", \"--peer\", \"gemini\"] },\n  \"coop fusion (gemini)\": { \"command\": \"coop\", \"args\": [\"acp\", \"fusion\", \"gemini\", \"--peer\", \"claude\", \"--peer\", \"codex\"] }\n}\n```\n\n### Second opinions (`--peer`)\n\nOutside fusion, name peers with `--peer \u003cagent\u003e` (repeatable) on a normal run —\n`coop claude --peer codex --peer gemini` (or a `codex`/`gemini` lead; in Zed,\n`coop acp claude --peer codex`) — for a lighter version of the same idea: on a\ngenuinely hard or risky call the agent may consult those peers read-only and in\nparallel (through the same `coop-consult` wrapper) to catch blind spots, then decide.\nIt's off by default and, unlike fusion, optional — no synthesis mandate, not for\nroutine work; it defaults to `--fresh` so each hard call gets an independent second\nopinion. Only the peers you name are consulted — there's no implicit \"consult\neveryone signed in\", and only a named peer's credentials mount (read-only). And it's\nscoped to the agent you launched, so peers it spawns never recurse.\n\n## Drive it from Zed (ACP)\n\nThe box can act as an [ACP](https://agentclientprotocol.com) agent, so you steer the\nsandboxed agent from Zed's own agent panel — Zed is the cockpit, the box stays the\ncage. Four steps:\n\n**1. Install** (once). The [install one-liner](#install) puts `coop` on your `PATH` and\nbuilds the image with the ACP adapters baked in. Check it resolves:\n\n```bash\ncommand -v coop      # e.g. /Users/you/.local/bin/coop\n```\n\n**2. Authenticate** the agent you'll use (see [Authentication](#authentication)):\n\n```bash\ncoop login claude    # or codex / gemini\n```\n\n**3. Register it in Zed.** In the agent panel use Add Custom Agent, or edit\n`settings.json` directly:\n\n```jsonc\n{\n  \"agent_servers\": {\n    \"coop\": {\n      \"type\": \"custom\",\n      \"command\": \"coop\",                                // absolute path if Zed's PATH lacks ~/.local/bin\n      \"args\": [\"acp\", \"claude\"]                         // one agent; pin the target: \"claude:opus/xhigh@work\"\n    },\n    \"coop · fusion\": {\n      \"type\": \"custom\",\n      \"command\": \"coop\",\n      \"args\": [\"acp\", \"fusion\", \"claude:opus/xhigh\"]    // a council; the governor's model/effort ride the target\n    }\n  }\n}\n```\n\n**Pin the model, reasoning effort, and account in coop's target inside `args`** —\n`provider[:model][/effort][@account]` — *not* the editor's own per-option defaults. So a\ncouncil led by Opus at extra-high reasoning effort is `[\"acp\",\"fusion\",\"claude:opus/xhigh\"]`,\nand a solo run is `[\"acp\",\"claude:opus/xhigh@work\"]`. The toolbar dropdowns come up reflecting the\ntarget and stay switchable mid-thread, and coop ignores any editor permission-`mode` setting —\nevery session runs yolo (the box is the boundary). **One caveat:** a **codex** governor (or lead)\ntakes its model from codex's own `config.toml` — coop can't set it over ACP — so choose a\n`claude` or `gemini` governor when you want coop to pick the model.\n\n\u003e GUI apps don't always inherit your shell's `PATH`. If Zed can't find `coop`, use the\n\u003e absolute path from step 1 as `command`.\n\n**4. Use it.** Open the agent panel, pick coop from the dropdown, and start a\nthread. Zed launches `coop acp \u003cagent\u003e` with the project as cwd; the agent runs in the\nbox and edits your files over ACP. Tool calls never prompt: coop runs every editor\nsession in yolo, whatever the provider's own settings — the box is the boundary, so\npermission theater would only slow it down.\n\nUnder the hood `coop acp [claude|codex|gemini|grok|fusion]` runs the matching adapter\n(`@agentclientprotocol/claude-agent-acp`, `@agentclientprotocol/codex-acp`, `gemini --acp`)\ninside the box over stdio. The repo mounts at its real host path — the same path\n`coop` and `coop loop` use — so Zed's absolute paths resolve *and* the session history\nlines up: a thread you started with `coop loop` is there to resume in Zed.\n\ncoop's proxy sits between the editor and the box and owns the session:\n\n- **A `coop` dropdown in the toolbar** lists your credentials and the repo's\n  same-provider [presets](#presets-the-whole-arrangement-in-one-yaml-file). Switching\n  restarts the box on the new identity and replays the session — the conversation\n  survives, because ACP transcripts live on a shared, credential-independent store; a\n  switch made mid-turn re-sends the in-flight prompt, so the turn completes on the new\n  target instead of erroring. The model dropdown defaults to the target's `:model`/config\n  (still switchable in-editor); under a preset the native model/effort dropdowns are\n  hidden — the preset's ladder and roles pin them; the permission-mode dropdown is gone\n  (always yolo).\n- **Rate limits are handled for you.** When a turn hits the provider's limit, coop\n  swallows the error, rotates to your next signed-in account, re-sends your prompt, and\n  moves the dropdown — the turn just completes on the backup credential. With nothing\n  free, it posts `Waiting for a reset on credential X in MM:SS (at \u003ctime\u003e)` and sends\n  automatically when the limit lifts.\n- **Restarts don't drop the editor.** A box death (`coop build`/`coop update`, an OOM,\n  Docker restarting) respawns the box and replays the handshake transparently — even a\n  thread you hadn't messaged yet survives. (`--supervise` is accepted for older configs\n  but no longer needed — supervision is always on.)\n- **Dev servers are reachable.** With `serve.ports` in\n  [`.agent/project.yaml`](#see-the-dev-server-in-your-browser), the thread announces\n  the stable `http://localhost:\u003cport\u003e` URLs the box's ports are published at.\n- **A misbehaving session can be traced.** `touch ~/.config/coop/acp-debug` (or set\n  `COOP_ACP_TRACE=1` in the agent server's env) and every `coop acp` server appends the\n  editor↔box ACP wire to `~/.config/coop/acp-trace-\u003cpid\u003e.log`. The sentinel works on an\n  already-running server, so you can switch tracing on after a session already went\n  weird. It carries prompts and file contents — treat it as sensitive; see\n  [Troubleshooting](#troubleshooting).\n\nTo steer a [**fork**](#forks-hand-off-work-like-a-pr) from Zed instead of your working tree,\npoint the adapter at it: `coop fork \u003cname\u003e acp [agent]` — same ACP, but the agent works the\nthrowaway clone (nothing to push, secrets never came along), and you still review and land it.\n\n\u003e **Services** work too — if the repo has a `.agent/compose.yml`, run `coop up` first and\n\u003e the ACP box joins the same network.\n\u003e **Custom images** must carry the ACP adapters: `coop init` scaffolds them in; for an\n\u003e older/hand-written `Dockerfile.agent`, add `@agentclientprotocol/claude-agent-acp@latest`\n\u003e and `@agentclientprotocol/codex-acp@latest` to its `npm install -g` line (else `coop acp` fails with\n\u003e `codex-acp: not found`).\n\n\u003e **One caveat on the boundary.** ACP has a second channel: the *editor* services\n\u003e `fs/read_text_file`, `fs/write_text_file`, and `terminal/*` requests **host-side**, and\n\u003e coop's proxy forwards them to Zed unfiltered (Claude's adapter uses client-side `fs` in\n\u003e normal operation). So over ACP the box's isolation is only as strong as your editor's own\n\u003e fs/terminal sandbox — a prompt-injected agent could ask the editor to read or write an\n\u003e absolute host path outside the repo. The `coop loop`/`coop claude` path has no such channel\n\u003e (the box is the only boundary that matters there); this applies specifically to driving an\n\u003e agent from an editor over ACP.\n\n## Run it unattended\n\n### The loop\n\n```bash\ncoop init                 # scaffold AGENTS.md, the .agent/ working folder, and the hooks\ncoop tasks add \"...\"      # add a task (a folder under .agent/tasks/00_todo/)\ncoop loop claude          # disposable agents work the queue until it's done, then sign off\ncoop loop codex           # …or name the agent: claude, codex, or gemini (or a preset name)\n```\n\nA task is a **folder** under `.agent/tasks/`, and its state is which directory it sits\nin: `00_todo/` · `10_in_progress/` · `50_blocked/` · `99_done/` (the numeric prefix sorts\n`ls` in lifecycle order; `coop tasks` shows the clean names). `loop` starts a fresh agent\nper iteration (no context rot), claims the next task from `00_todo/` (or resumes one left\nin `10_in_progress/`), and won't quit while either has work. Name the agent\n(`claude`/`codex`/`gemini`, or a preset name whose lead supplies it); loop.yaml `work.command` still overrides the whole\niteration command if you need something custom. When the queue empties, a fresh, **demanding\nsignoff** pass (a senior reviewer's bar) re-checks each shipped task: goal met (every acceptance\ncriterion and subtask), standards followed (`AGENTS.md` + `.agent/rules`, no scope creep),\nthe **failure path** tested, the change polished (docs/CHANGELOG updated), plus bookkeeping\n(every `99_done/` task has an implementing commit and a final `state.md`) — then it runs the\nrepo's gate **once** across the whole repo and reopens anything short of \"merge with no\nchanges\". If the signoff reopened work, the loop drains and signs off **again** — repeating\nuntil a signoff reopens nothing (verified done) or it hits the round cap, at which point the\ntask it keeps reopening is blocked for a human rather than reported as done. The cap\n**scales with the batch**: half the tasks worked this run, clamped to\n`[3, signoff.rounds]` (default `5`) — a small batch still gets a few tries, a big\novernight batch can't ping-pong one stuck task forever.\n\nTune the loop in one committed **`.agent/loop.yaml`** — a section per step (`preflight` /\n`work` / `between` / `signoff`), each with its own `agent:` model ladder and a prompt: between\nis the per-task reviewer, signoff the final one. Prompts never *replace* a coop built-in:\n**`signoff.prompt`** and **`preflight.prompt`** *append* extra checks/instructions to theirs (so\nthe signoff still reopens a shipped task that fails one — e.g. the CHANGELOG gained an entry, the\ndocs were regenerated), while **`between.prompt`** *sets* an opt-in per-task audit that runs after\neach completed task and may reopen it (between has no built-in, so it's off unless enabled + set;\ncoop names the just-finished task in the prompt). Each step's `agent:` is a ladder of targets\n(`provider[:model][/effort][@account]`) or preset names — so **`signoff.agent`** can review on a\nstronger model than the cheaper `work.agent` loop. Settings live here too: `signoff.rounds`,\n`preflight.enabled`, `work.command`. Every field is optional (a missing file = the built-in\ndefaults), and `coop init` scaffolds a fully-commented starter.\n\n\u003e The old `.agent/loop/*.md` files (`review.md`/`audit.md`/`between.md`) and the legacy\n\u003e `.agent/audit.md` are retired — fold them into `.agent/loop.yaml` (`signoff.prompt` gains\n\u003e `review.md` + `audit.md`; `between.prompt` gains `between.md`). coop warns once if one lingers and\n\u003e no longer reads it.\n\n**Exit codes.** A cron job or CI can branch on the loop's outcome without parsing output: `0` the\nqueue is verified done; `1` a failure; `2` a usage error; `3` the loop stopped with a task blocked\non a human decision — including one the review kept reopening past the round cap (resolve with `coop\ntasks decisions`, then re-run).\n\nAdd `--preflight` (or set `COOP_PREFLIGHT=1`) to run one cleanup pass *before* the loop\nstarts working: it unblocks any `50_blocked/` task whose `decision.md` now has an answer —\nso a fresh run starts from a tidy queue. It works no\ntask and makes no commits, and it's the symmetric front bookend to the review pass. Off\nby default.\n\n`init` also installs a `Stop` hook (won't let a session end with work outstanding) and a\nfast Claude commit-gate hook. Because those are Claude-only, `init` *also* installs a\ntracked git pre-commit gate (`.githooks/pre-commit`) and points `core.hooksPath` at\nit, so the format check runs for *every* committer — Codex, Gemini, and a plain\n`git commit` — and rides along on a fresh clone. A custom `core.hooksPath` is left\nuntouched; skip the gate once with `git commit --no-verify`.\n\nIf the model hits a rate or usage limit mid-run, the loop doesn't treat it as a\nfailure: it reads the reset time from the agent's own output, waits it out with a\ncountdown, and resumes the same item once the limit clears — so an overnight run rides\nthrough the daily cap instead of burning retries against it.\n\n`init` also installs generic workflow skills into `.claude/skills/` (shared with\nCodex): `/spec` a multi-file change, `/work` it step-by-step against the gate, `/sweep`\nto drain `.agent/tasks/`, `/investigate` to root-cause a failure, `/verify-api`\nbefore calling anything you're unsure of, and `/review-board` for a thorough\nmulti-hat review before landing. Edit them freely — `init` won't overwrite a\nskill you've changed.\n\n### The `.agent/` working folder\n\n`init` creates a tool-neutral working folder the agent reads back on every boot (and\nafter each compaction). Everything here is local working state and git-ignored —\nexcept the knowledge (`rules/`, `skills/`, `presets/`) and `project.yaml`, which are\ncommitted.\n\n| File | What it's for |\n|---|---|\n| `tasks/` | the work queue — one folder per task under `00_todo/`/`10_in_progress/`/`50_blocked/`/`99_done/`; a task's state is its directory, and `coop tasks` moves it. Each folder carries its own `spec.md`/`log.md`/`state.md`/`decision.md` as needed. The loop reads `00_todo/`+`10_in_progress/`. |\n| the backlog | unscheduled ideas, as task folders in the `tasks/xx_backlog/` drawer (`coop backlog`) — outside the lifecycle, so never auto-worked and never nagged by the Stop hook; `coop backlog promote \u003cid\u003e` moves one into `tasks/00_todo/` when it's ready |\n| `rules/` | the taste knowledge base — corrections graduate into rules here (committed) |\n| `project.yaml` | the committed per-project config: a monorepo's [`subprojects:`](#monorepos), the [`serve:` ports](#see-the-dev-server-in-your-browser), the box **policy** (`box:` — egress, resource caps, `auto_up`/`network`), and the merge `gate:`. `box:` and `gate:` fall *below* an explicit `COOP_*` env/conf setting, and — being committed and host-read — can only ever *tighten* your posture (egress pins to `none`, never widens; `no_new_privileges` isn't settable here) |\n\nUpgrading a repo that still has a single `.agent/TASKS.md`? Convert it to the folder format\nby pasting the prompt in [MIGRATING.md](MIGRATING.md) to any coding agent in the repo.\n\n### Monorepos\n\nOne repo, several components, each with its own work? List them once in the top-level\n`.agent/project.yaml`:\n\n```yaml\n# .agent/project.yaml — committed with the repo\nsubprojects: [runner, packs, portal, mcp]\n```\n\nand coop aggregates every member's `.agent/tasks` automatically — `coop tasks` rolls\nthem up under per-queue headers, one `coop loop` drains them all, `coop prompt` counts\nacross them, and the id commands (`claim`/`done`/…) find a task in whichever queue holds\nit. No more hand-maintaining `COOP_TASKS` (an explicit `COOP_TASKS`/`--tasks` still\noverrides). Members keep their **own** queues for their own work; the root keeps one too,\nfor changes that span members.\n\n`coop init` at the root detects the members (direct child dirs that have a `.agent/`),\nwrites the `project.yaml`, and scaffolds each member with just its own task queue —\nmembers share the root's AGENTS.md, skills, rules, and box, though a large member may\ncommit its own `rules/` if it wants them. `coop tasks queues` prints the resolved queue\npaths when a script (or the Stop hook) needs them.\n\n### A fleet\n\nRun several models at once, each looping unattended in its own [fork](#forks-hand-off-work-like-a-pr).\nSplit the work into separate task trees and hand each fork one with `--tasks`:\n\n```bash\ncoop fork perf codex  --loop -d --tasks .agent/tasks.perf   # codex loops the perf slice, detached\ncoop fork deps gemini --loop -d --tasks .agent/tasks.deps   # gemini takes the deps slice\ncoop fork docs claude --loop -d --tasks .agent/tasks.docs   # claude takes the docs\n\ncoop fleet watch       # live board: every fork's progress (done/total), blockers, the task each is on\ncoop fork ls           # snapshot: who's running, how big the diff, last activity\ncoop fork logs -f      # tail every fork at once (compose-style, prefixed)\ncoop fork stop perf    # halt one; coop fork logs perf -f to watch just it\n```\n\n`--tasks \u003cpath\u003e` is optional — with none, the fork seeds *every* queue coop knows about:\nthe repo's own `.agent/tasks`, plus each [monorepo](#monorepos) subproject's queue, each at\nits own relative path, so the in-fork loop aggregates them just like `coop loop` does. Pass\n`--tasks` (as above) to hand each fork one separate slice instead, named independently from\nthe fork. It seeds the fork's queue from that tree (once — a resumed loop keeps its own\nprogress) and runs the loop with the chosen model; `-d` (`--detach`) backgrounds it, capturing output to\n`../\u003crepo\u003e-forks/.coop/\u003cname\u003e.log`. When one finishes,\n[review and land it](#forks-hand-off-work-like-a-pr) like a PR, then `git push`. Add\nagents until *review*, not generation, is your bottleneck.\n\n**Land the whole fleet at once** with `coop fork merge --all` — a revalidating rebase\n*queue*: it rebases each fork onto the result of the last and re-runs `COOP_GATE`, so a\n\"green\" fork can't ride in against a base an earlier landing already changed. It stops at\nthe first conflict or red gate, leaving the rest untouched.\n\n**Declare the fleet once** in `.agent/fleet.yaml` (run `coop fleet init` for a template\nwith the format documented inline). Each fork needs `tasks:` (relative to the repo\nroot) and `agent:` — the who-runs, either a target (`provider[:model][@account]`; give each fork a\ndifferent account so they don't contend) or a preset name (an [orchestration preset](#presets-the-whole-arrangement-in-one-yaml-file)\n— its lead + ladder drive the fork). A fork takes ONE account — for a full\nrotation ladder, point it at a preset. The old per-fork `model:`/`credential:` keys are\nretired (the model+account ride `agent:`), and so is the separate `preset:` key (a preset name\ngoes in `agent:` now); a per-fork `consult: true` is refused for now (name peers explicitly — the\nfleet grammar for that is coming):\n\n```yaml\nforks:\n  core:\n    tasks: .agent/tasks.core\n    agent: frontier                  # a preset name — claude/fable lead + critic/fast roles\n  perf:\n    agent: codex:gpt-5.5@work        # a target: provider, model, account\n    tasks: .agent/tasks.perf\n  deps:\n    agent: gemini:gemini-3.5-flash\n    tasks: .agent/tasks.deps\n```\n\nThen `coop fleet up` starts them all detached, `coop fork ls` shows the board, and\n`coop fleet down` stops them. (The pre-v3 one-line `.agent/fleet` is not read — its\npresence is an error until you translate it to YAML and delete it; see MIGRATING.md.) To bootstrap the file, `coop tasks\nsplit \u003cn\u003e` mechanically round-robins your `.agent/tasks/` todo folders into per-fork\n`.agent/tasks.slice\u003cn\u003e/` slices and writes a matching `.agent/fleet.yaml` with each\nslice's explicit path (use an agent for *semantic* slicing). It won't clobber a fleet\nyou've already written — it prints the entries to reconcile instead.\n\n## Project toolchain \u0026 services\n\nReal projects need a language toolchain (Elixir, Go, …) and stateful services\n(Postgres, Redis). The agent does not install those at runtime — that's slow,\nnon-reproducible, and dies with the container. Declare them once instead. This is the\nDev Containers + Compose model, minus the ceremony.\n\n### `.tool-versions` — zero config\n\nIf your repo pins versions in a `.tool-versions` (asdf), the base box provisions that\ntoolchain at runtime — resolved from the working dir up the tree, or\n`~/.tool-versions` — and caches it in a shared volume. So a repo with *just* a\n`.tool-versions` (no `Dockerfile.agent`, no scaffolding) gets its toolchain with zero\nsetup:\n\n```bash\ncd ~/code/phoenix-app   # has a .tool-versions\ncoop claude             # provisions elixir/erlang/node/… from it, then runs the agent\n```\n\nThe first install of a new toolchain can be slow (e.g. Erlang compiles), then it's\nreused across runs and repos. Set `COOP_NO_ASDF=1` (in `agents/env`) to skip provisioning\nfrom `.tool-versions`; coop still repairs a stale persisted Node shim when needed so the\nagent CLIs keep running. For a baked, fully-reproducible image instead,\n`coop init --stack asdf` scaffolds an asdf `Dockerfile.agent` that installs the same\n`.tool-versions` at build time.\n\n### `Dockerfile.agent` — a per-project image\n\n```bash\ncoop init --stack asdf   # writes an asdf Dockerfile.agent (from .tool-versions)\ncoop build               # builds it, tagged coop-\u003crepo-name\u003e — its own image\n```\n\nA repo with its own `Dockerfile.agent` gets its own image tag, so projects never\ncollide, and every `coop`, `coop loop`, `coop fork`, `coop acp` in that repo uses it.\nThe scaffolded one is the asdf image — it bakes in the exact `.tool-versions`\ntoolchain (versions live there, not in the Dockerfile). For anything more exotic,\nhand-write a `Dockerfile.agent` (see the box contract below). When the agent needs a new\nsystem package, add it to the `RUN` line and `coop build` again — the dependency\n*graduates into the image* instead of being installed each run. If you change\n`Dockerfile.agent` or `.tool-versions` but forget to rebuild, `coop` notices on the next\nrun and reminds you to `coop build` (it records the image's inputs at build time).\n\n\u003cdetails\u003e\u003csummary\u003e\u003cb\u003eThe box contract (build any base)\u003c/b\u003e\u003c/summary\u003e\n\nAn image is a valid agent box when:\n\n1. It runs as a non-root user — Claude Code refuses `--dangerously-skip-permissions` as root.\n2. That user's home is `/home/node` — the `agents/` auth mounts land at `$HOME/.claude`, `$HOME/.codex`, `$HOME/.gemini`. (Different base? Set `COOP_HOME_IN_BOX=/home/\u003cuser\u003e`.)\n3. `claude`, `codex`, `gemini` are on `PATH` (so it needs Node) — plus the ACP adapters if you want `coop acp`.\n4. **`git config --system --add safe.directory '*'`** — git works on the host-owned bind mount (which lives at the repo's real path, not a fixed `/workspace`).\n\ncoop sets the working directory itself, so no `WORKDIR` is required. A skeleton:\n\n```dockerfile\nFROM \u003cyour-language-base\u003e\nRUN \u003cinstall your toolchain\u003e \\\n \u0026\u0026 npm install -g @anthropic-ai/claude-code@latest @openai/codex@latest @google/gemini-cli@latest \\\n      @agentclientprotocol/claude-agent-acp@latest @agentclientprotocol/codex-acp@latest \\\n \u0026\u0026 git config --system --add safe.directory '*' \\\n \u0026\u0026 id -u node \u003e/dev/null 2\u003e\u00261 || useradd -m -u 1000 -s /bin/bash node\nUSER node\n```\n\n(If the base lacks Node, install it first — the asdf template uses NodeSource.)\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003e\u003cb\u003eReusing an existing devcontainer\u003c/b\u003e\u003c/summary\u003e\n\nIf a repo already has a `.devcontainer/`, reuse its image as your base and add the agent\nlayer on top:\n\n```dockerfile\nFROM your-devcontainer-image          # the team's source of truth for the env\nRUN npm install -g @anthropic-ai/claude-code@latest @openai/codex@latest @google/gemini-cli@latest \\\n      @agentclientprotocol/claude-agent-acp@latest @agentclientprotocol/codex-acp@latest \\\n \u0026\u0026 git config --system --add safe.directory '*'\nUSER \u003cthe devcontainer's non-root user\u003e\n# If that user's home isn't /home/node, run with COOP_HOME_IN_BOX=/home/\u003cuser\u003e.\n```\n\nDivision of labour: devcontainer = what's in the environment (toolchain, features,\nreproducibility); Coop = running an untrusted agent in it safely (secret shadowing,\nthe container boundary, the queue + foreman). Don't lean on the devcontainer as the\nsecurity boundary — by itself it mounts your whole workspace (`.env` included), and a\nmalicious project under `--dangerously-skip-permissions` can exfiltrate `~/.claude`. The\nshadowing and the box are what Coop adds on top.\n\u003c/details\u003e\n\n### Services\n\nSibling services are opt-in: `coop init` asks which to add (or pass\n`--services postgres,redis`), scaffolding a `.agent/compose.yml` — none by default.\n\n```bash\ncoop up        # starts Postgres + Redis from .agent/compose.yml, waits until healthy\ncoop claude    # the box reaches them by name (db, redis)\ncoop down -v   # stop services and wipe their throwaway data\n```\n\nServices run as their own containers on a private network the box joins — connect with\ne.g. `DATABASE_URL=postgres://postgres:postgres@db:5432/app_dev` (put it in `agents/env`).\nThe agent never installs or hosts a database, so it can't corrupt one, and `coop down -v`\nresets to a clean slate. A shared `coop-cache` volume at `~/.cache` keeps disposable runs\nfrom re-downloading the world.\n\n`.agent/compose.yml` runs on your **host** daemon (that's how a service becomes a real\ncontainer), so coop **validates it before every run** — `coop up` and each networked launch\nalike. Only plain sibling-service directives pass: an `image`, inline `environment`, named\nvolumes or repo-relative binds, `healthcheck`, `depends_on`, and **loopback-only** published\nports. Anything that would reach past a repo-scoped container is refused with the exact reason —\n`privileged`, `cap_add`, a host bind like `/:/host` or `/var/run/docker.sock`, `network_mode:\nhost`, `env_file`, `build`, a `0.0.0.0` port, an escaping symlink. So the file is safe to\nauto-run no matter who wrote it: an agent can scaffold services for you, and a prompt-injected\none still can't turn `.agent/compose.yml` into host root. (Need something outside that subset?\nRun it yourself — coop only auto-runs the safe subset.)\n\n### See the dev server in your browser\n\nThe agent built a website in the box — now open it. List the port(s) the dev server\nlistens on in `.agent/project.yaml`:\n\n```yaml\nserve:\n  ports: [5173]        # what the server binds INSIDE the box\n```\n\nand every box for this repo (`coop acp`, `coop run`) publishes each to a **stable host\nport**, derived from the repo path — the same URL every launch, a different one per\nproject, so one shared Zed agent definition serves all your projects without port\ncollisions. In an ACP thread coop announces the mapping (`🌐 box :5173 →\nhttp://localhost:24187`); on a terminal run it's printed on stderr.\n\nTwo things to know: the dev server must bind `0.0.0.0` inside the box (`vite --host`,\n`next dev -H 0.0.0.0`, …) — a container-localhost server isn't reachable through the\nmapping — and ports bind to *your* localhost only (never the LAN). Publishing needs\nnetwork egress (`COOP_EGRESS=open`, the default); a host port already in use is skipped\nwith a note. A box restart (credential switch, rebuild) restarts the dev server's world —\nthe URL stays the same, just re-run the server.\n\n### Keeping the box current\n\n```bash\ncoop update              # self-update coop, then rebuild the image fresh\ncoop update --self-only  # just upgrade the coop binary\ncoop update --box-only   # just rebuild the image (the old behavior)\n```\n\n`coop update` first replaces the `coop` binary with the latest GitHub release —\nfetched and verified exactly as `install.sh` does (checksum + cosign signature),\nthen swapped in atomically so replacing the running binary is safe — then rebuilds the\nbox image. A dev/source build, an already-current binary, or a coop installed somewhere\nunwritable (a package-manager prefix) skips the self-update with a note and still\nrebuilds the image. The binary swap takes effect on your next `coop` run.\n\n**Stable vs fresh.** `coop build` is the *stable* path: it pins the base image to a\nspecific Node digest, so a rebuild gets the same OS/runtime every time, and the cache\nholds the agent CLIs steady between builds. `coop update` is the *fresh* path: it floats\nthe base back to the `node:24` tag and rebuilds with `--pull --no-cache`, so the base and\nthe agent CLIs + ACP adapters all jump to latest (they ship features often). To move the\npinned base permanently, bump `pinnedNodeImage` in `internal/box/image.go`.\n\n**Agent package updates.** The built-in package specs follow npm's stable `latest` tag\n(`@anthropic-ai/claude-code@latest`, `@openai/codex@latest`, `@google/gemini-cli@latest`,\n`@agentclientprotocol/claude-agent-acp@latest`, and `@agentclientprotocol/codex-acp@latest`),\nso `coop update` can pick up agent fixes without a coop source change. Coop also applies a\nbest-effort SQLite trigger to the active Codex profile before launch so inserts into the\n`logs_2.sqlite` feedback-log table are ignored; session history, auth, MCP config, and\nmemories are not touched.\n\nFor a fully reproducible image, also pin the tool versions: set\n`COOP_AGENT_PACKAGES` to exact specs and `coop build`, e.g.\n`COOP_AGENT_PACKAGES=\"@anthropic-ai/claude-code@2.1.186 @openai/codex@0.142.0 …\"` (the full\nlist is in `internal/agent/*.go`).\n\n## Configuration\n\nSet via environment variables, or `~/.config/coop/coop.conf` (`KEY=VALUE` lines, same\nnames — the environment wins over the file). Toggles default on; set `0`/`false` to\nturn them off.\n\n**Box \u0026 runtime**\n\n| Var | Default | |\n|---|---|---|\n| `COOP_RUNTIME` | auto | `container` / `docker` / `podman` |\n| `COOP_IMAGE` | (auto) | force a specific image (overrides `Dockerfile.agent` detection) |\n| `COOP_BASE_IMAGE` | `coop-box` | the shared base image tag |\n| `COOP_AGENT_PACKAGES` | (latest) | pin the global agent + ACP npm specs for a reproducible `coop build` |\n| `COOP_REPO` | (git toplevel) | the repo to operate on, overriding cwd detection |\n| `COOP_WORKDIR` | (real path) | where the repo mounts in the box |\n| `COOP_HOME_IN_BOX` | `/home/node` | where auth + instructions mount in the box |\n| `COOP_RUN_ARGS` | — | extra args passed straight to the container runtime |\n| `COOP_PIDS` | `4096` | box pids-limit (fork-bomb cap); `0`/`unlimited`/empty turns it off |\n| `COOP_MEMORY` · `COOP_CPUS` | — | box memory / CPU caps (e.g. `4g`, `2`); unset by default |\n| `COOP_NO_NEW_PRIVILEGES` | `1` | `--security-opt no-new-privileges` on the box |\n| `COOP_HOMES` | `1` | mount your per-agent home dirs (auth + settings) into the box; `COOP_HOMES=0` keeps them out, so an agent can't read your host agent configs |\n| `COOP_EGRESS` | `open` | `none` cuts the box off the network (`--network none`) — no outbound, so a prompt-injected agent can't exfiltrate the repo, secrets, or its credentials. Breaks installs / the model API, so it's opt-in; the default keeps full outbound. |\n| `COOP_NO_ASDF` | (off) | skip runtime `.tool-versions` provisioning; stale Node shim repair still runs. Read **in the box** — set it in `agents/env` (forwarded into the box), not your host shell |\n| `COOP_NETWORK` · `COOP_CACHE` | `1` | join the services network · mount the cache volume |\n| `COOP_AUTO_UP` | `1` | auto-start sibling services (`compose up`) before every box when a `.agent/compose.yml` is present, so any mode (agent, fusion, acp, loop, fork) can reach them; `0` to manage them with `coop up`/`coop down` yourself |\n| `COOP_SERVICES_NET` | (auto) | services network to join (let a fleet share one db) |\n\nThe resource/privilege caps (`COOP_PIDS` / `COOP_MEMORY` / `COOP_CPUS` /\n`COOP_NO_NEW_PRIVILEGES`) apply on docker and podman; Apple's `container` CLI differs,\nso they're skipped there for now. On docker/podman the box also runs with **all Linux\ncapabilities dropped** (`--cap-drop ALL`) — the agent workloads need none, and it keeps\nroot-in-container (a repo `Dockerfile.agent` that does `USER root`) from holding\n`CAP_DAC_OVERRIDE` / `CAP_NET_RAW` / `CAP_MKNOD` and friends.\n\n**Agents \u0026 config**\n\n| Var | Default | |\n|---|---|---|\n| `COOP_CONFIG_DIR` | `~/.config/coop/agents` | per-agent auth + settings folder |\n| `COOP_CONF` | `\u003cconfig\u003e/coop.conf` | relocate the `coop.conf` file coop reads its `COOP_*` defaults from |\n| `NO_COLOR` | — | present at any value (even empty) disables ANSI color everywhere ([no-color.org](https://no-color.org)) |\n| `COOP_\u003cAGENT\u003e_CMD` (e.g. `COOP_CLAUDE_CMD`) | autonomous default | override an agent's base command |\n| `COOP_\u003cAGENT\u003e_MODEL` (e.g. `COOP_CLAUDE_MODEL`) | (CLI default) | agent-wide default model, everywhere that agent runs (see [Picking models](#picking-models)) |\n| `COOP_CONSULT_TIMEOUT` | `1800` | per-peer `coop-consult` timeout in seconds; a peer that doesn't answer in time is skipped so the lead synthesizes from whoever did |\n| `COOP_MCP_FILE` | `\u003cconfig\u003e/mcp.json` | the one MCP source of truth |\n| `COOP_SHELL` | `bash` | the shell `coop shell` opens |\n| `COOP_NO_UPDATE_CHECK` | (off) | set to opt out of the once-a-day \"a newer coop/box is available\" check |\n\n**Forks \u0026 loop**\n\n| Var | Default | |\n|---|---|---|\n| `COOP_GATE` | — | gate re-run in the box before a fork merge lands (e.g. `make check`) |\n| `COOP_EDITOR` | (detected) | editor for `coop fork review --open` |\n| `COOP_REVIEW_CMD` | — | full override for `coop fork review` (`sh -c`) |\n| `COOP_LOOP_CMD` | — | override the loop's per-iteration command |\n| `COOP_LOOP_MODEL` | — | `model[/effort]` for loop iterations (e.g. `opus/low` — overnight runs on a cheaper/lighter setting than interactive) |\n| `COOP_REVIEW_MODEL` | — | `model[/effort]` for `coop loop`'s review pass + between-tasks audit — a stronger model/effort reviews the cheaper loop's work (unset = the loop's) |\n| `COOP_MAX_REVIEW_ROUNDS` | `5` | the ceiling for `coop loop`'s work→review rounds before blocking a task the review keeps reopening; the actual cap scales with the batch (`clamp(tasks/2, 3, this)`) |\n| `COOP_TASKS` | (derived) | explicit task queue dir(s) for `coop tasks` and the loop (space-separated for several). Unset, the queues come from `.agent/project.yaml` — a [monorepo's](#monorepos) subproject queues — else `.agent/tasks`. `--tasks` **replaces** this for a run (it doesn't merge) |\n| `COOP_PREFLIGHT` | `0` | run a cleanup pass (log/tasks/decisions) before `coop loop` (like `--preflight`) |\n| `COOP_CAFFEINATE` | `1` | while a loop runs, hold a system sleep inhibitor so the machine doesn't idle-sleep mid-drain (macOS `caffeinate`; released when the loop ends). `0`/`false` to disable |\n\nCommand-valued settings — `COOP_GATE`, `COOP_LOOP_CMD`, `COOP_RUN_ARGS`, and the\n`COOP_\u003cAGENT\u003e_CMD` overrides — are split into `argv` with shell quoting (single/double\nquotes group, `\\` escapes), but no shell runs them (no globbing or `$VAR`). So quotes\ngroup as you'd expect — `COOP_GATE='bash -lc \"make check \u0026\u0026 make lint\"'` is three args, not\nfive — but a bare `\u0026\u0026`/`|`/`$VAR` is a literal argument: wrap those in `bash -lc \"…\"`.\n(`COOP_REVIEW_CMD` is the exception — it *is* run via `sh -c`.)\n\n**Exit codes.** Every command follows one contract, so CI and scripts can branch without parsing\noutput: `0` success · `1` a failure (or findings — e.g. `coop check-secrets` on a hit) · `2` a usage\nerror (unknown command/flag or bad arguments). `coop loop` adds `3` — it stopped with a task blocked\non a human decision (see [Exit codes](#the-loop) above).\n\n**Why no `--json`?** coop's stdout is for a human at a terminal; its *exit codes* are the machine\ncontract. The structured data a script would want already lives in files it can read directly — the\ntask queue is folders under `.agent/tasks/`, a fork's state is its git worktree — so a `--json`\nsurface would just be a second, drifting copy. Branch on exit codes; read the files.\n\n## Troubleshooting\n\n| Symptom | Fix |\n|---|---|\n| **\"no container runtime found\"** | Install Apple [`container`](https://github.com/apple/container) (macOS 26+), Docker, or Podman, then `coop build \u0026\u0026 coop doctor`. Force one with `COOP_RUNTIME=docker`. |\n| **\"image … isn't built — run 'coop build'\"** | `coop build` (shared base), or `coop build` in a repo with a `Dockerfile.agent` (its own image). |\n| **Login hangs or \"usage limit reached\"** | `coop login \u003cagent\u003e` re-runs the sign-in (paste-code, no browser). Hit a subscription limit? It resets on a schedule — wait, or `coop login` into another account. The unattended loop waits out the reset on its own; a [Zed session](#drive-it-from-zed-acp) rotates to your next signed-in account and re-sends by itself. |\n| **Agent seems stuck / a detached loop won't quit** | `coop fork logs \u003cname\u003e -f` to watch it; `coop fork stop \u003cname\u003e` to stop a detached loop. A foreground run is just Ctrl-C. |\n| **\"permission denied\" writing `~/.cache` / build or test caches** | The shared cache volume initialized root-owned. Recreate it: `docker volume rm coop-cache` (or your runtime's equivalent), then `coop build`. |\n| **`go`/`gofmt`: \"No version is set for command go\"** | The box provisions toolchains from `.tool-versions` via asdf — add the toolchain there (e.g. `golang 1.26.4`) so it's installed and shimmed. Set `COOP_NO_ASDF=1` to skip provisioning. |\n| **A pinned `.tool-versions` tool (`go`, `ruby`, …) is installed yet \"not found\" in a *login* shell** | asdf's shims sit on PATH via the image's `ENV`, which only reaches the agent process and non-login shells. A login shell (`sh -lc`, `bash -l`) sources `/etc/profile`, which resets PATH and drops the shims. The base box adds an `/etc/profile.d` drop-in to re-add them; rebuild an older box with `coop build` to pick it up. |\n| **Zed (ACP) can't find the agent** | Zed must launch `coop` from a shell where it's on `PATH` (the installer puts it in `~/.local/bin`). Point Zed's ACP command at the absolute path if needed, and confirm `coop acp \u003cagent\u003e` runs in a terminal first. |\n| **An editor (ACP) session misbehaves** | Turn on wire tracing: set `COOP_ACP_TRACE=1` in the agent server's `env`, or `touch ~/.config/coop/acp-debug` (works on an already-running server). coop appends the editor↔box traffic to `~/.config/coop/acp-trace-\u003cpid\u003e.log` (size-bounded, auto-pruned). It carries prompts and file contents — treat it as sensitive. |\n| **A merge refuses** | Dirty tree → commit/stash first. Policy flagged a secret/large file → review, then `--force`. Non-interactive shell → pass `--yes`. Gate (`COOP_GATE`) went red on the rebased tree → it rolled back; fix and re-run. |\n| **Secrets still visible / a custom secret isn't hidden** | Run `coop doctor` to see what's shadowed. Add repo-specific paths to a `.coopignore` (see [Secrets never enter the box](#secrets-never-enter-the-box)). |\n| **\"box image is stale … run 'coop build'\"** | You changed `Dockerfile.agent` or `.tool-versions` since the image was built. `coop build` to rebuild; the warning clears once the image matches. |\n| **A scaffolded `db` (postgres:18) exits 1 on `coop up`** | Scaffolds from before this fix mounted `pgdata` at `/var/lib/postgresql/data`, which postgres 18+ refuses (it wants a single mount at `/var/lib/postgresql`). Edit `.agent/compose.yml` and move the mount up one level. New scaffolds are already fixed. |\n\n## Layout \u0026 development\n\nA single static Go binary plus a config folder. A repo you work on optionally carries a\n`Dockerfile.agent` (its toolchain) and `.agent/compose.yml` (its services):\n\n```\nmain.go             entrypoint\ninternal/agent/     one file per coding agent (claude/codex/gemini): commands, resume, MCP, defaults, packages\ninternal/box/       the engine: secret-shadowing mounts, git env, image selection, container run\ninternal/acpproxy/  the ACP session proxy: survives box restarts, replays the handshake, coop's editor hooks\ninternal/fusion/    the council: peer commands + the governor instruction\ninternal/preset/    orchestration presets (.agent/presets/\u003cname\u003e/preset.yaml): roles, ladders, routing\ninternal/project/   .agent/project.yaml — a monorepo's subprojects + the serve ports\ninternal/mcp/       one mcp.json → Claude / Gemini / Codex native configs (pure Go, no Python)\ninternal/scaffold/  `coop init` templates + the workflow skills (embedded in the binary)\ninternal/cli/       command dispatch, grouped help, the fork lifecycle, doctor\ninternal/config·runtime·ui/   settings · runtime detection · terminal output\ninstall.sh          the curl one-liner: download the prebuilt binary onto PATH\n```\n\n```bash\nmake build     # build ./coop\nmake check     # gofmt + vet + staticcheck + unit tests (what CI runs; no Docker needed)\nmake doctor    # the integration check — proves isolation, needs a runtime\n```\n\n`.tool-versions` pins the Go toolchain (`golang 1.26.4`), so an asdf user — and coop's\nown box — gets the right `go`/`gofmt` automatically.\n\nThe security-critical logic — secret enumeration (`internal/box/mounts.go`) and run-arg\nassembly (`internal/box/run.go`) — is pure and unit-tested without a runtime; `coop\ndoctor` proves the whole thing end-to-end against the real box.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrewdryga%2Fcoop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandrewdryga%2Fcoop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandrewdryga%2Fcoop/lists"}