Documentation

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

The idea

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

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

Why it's built this way

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

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

Install

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

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

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

coop build && coop doctor

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

The installer verifies what it downloads. With cosign on your PATH it checks a keyless Sigstore signature; otherwise it falls back to a SHA-256 check and tells you the signature wasn't verified. Prefer Go? go install github.com/AndrewDryga/coop@latest works too.

Quickstart

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

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

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

coop fork feature claude     # agent works in a throwaway clone…
coop fork review feature     # …you read the diff…
coop fork merge feature      # …and land it on your branch

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

The sandbox

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

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

Why the path matches

A fixed /workspace mount would break tools that record absolute paths (agent session files, your editor's project state). Mounting at the real path means a session you start in the loop is the same one you can resume later from Zed.

Your git identity, not the box's

The container has no ~/.gitconfig of its own, so coop mounts a curated one: your name and email (commits are authored as you), your global gitignore, and commit.gpgsign=false. Your signing key never enters the box, so commits made inside it are unsigned — but work landed through a fork can still be signed on the host, where your key lives. More on that below.

Secrets stay out

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

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

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

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

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

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

coop check-secrets

Prove it

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

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

coop doctor

Run an agent

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

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

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

coop claude

Forks — hand off work like a PR

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

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

When to reach for a fork

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

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

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

coop fork — review & land

Fusion — a council of models

For a genuinely hard call, don't bet the run on one model's blind spots. Let one model lead and the others weigh in.

In coop fusion, one model is the governor: it does the real work — edits, runs the gate, commits. The other two are consulted read-only and in parallel on the decisions that matter, and the governor synthesizes the strongest parts of all three. The combined answer reliably beats any single model working alone.

Why it's not just three agents

There's no extra service or protocol — the leader simply asks its peers from its own shell, and only on hard questions (it's told to skip the council for trivial steps). Each consult is two quick read-only runs, so you spend the extra tokens exactly where a second opinion is worth it.

coop fusion                 # the default governor leads
coop fusion claude          # …or pick who leads
coop claude --consult       # lighter, opt-in: a second opinion on hard calls only
coop fusion claude

The loop — work the queue while you sleep

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

The problem it solves

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

You write tasks as checkboxes in .agent/TASKS.md. coop loop then starts a fresh agent for each one — so every task begins with a clean head, no leftover context to rot. The agent claims a task, does it, runs your gate, commits the result, and moves to the next. It won't stop while a single [ ] remains.

coop init             # scaffold AGENTS.md, the .agent/ folder, and the hooks
# …fill .agent/TASKS.md with checkbox tasks…
coop loop             # disposable agents drain the queue, then audit

It checks its own work. When the list is empty, coop starts one more agent as an auditor. It re-reads every task marked done and confirms the git history actually backs it up — reopening anything that doesn't. "Done" ends up meaning verified, not just claimed.

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

Good fits

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

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

The .agent/ folder

coop init creates a small, tool-neutral working folder the agent reads back on every start (and after each compaction). It's all local working state and git-ignored — except rules/, a shared knowledge base, which is committed.

File What it's for
TASKS.md the work queue — [ ] todo, [w] claimed, [x] done, [B] blocked
LOG.md the agent's running notes (what & why), so intent survives a fresh start
BACKLOG.md work spotted outside the current task — captured, not auto-worked
rules/ the taste knowledge base — corrections graduate into rules here

A fleet — many agents at once

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

Split the backlog into separate task files and hand each fork one. coop fleet up starts them all in the background; coop fleet watch is the live board, showing each fork's progress, the task it's on, and who's finished.

coop fleet up         # start every fork in .agent/fleet, detached
coop fleet watch      # the live dashboard
coop fork merge --all # land the whole fleet through a revalidating rebase
Why run a fleet

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

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

Profiles & failover

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

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

coop login claude --profile work       # a second account…
coop login claude --profile personal   # …and a third
coop profiles                          # list them and which are signed in

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

Editors (Zed)

Prefer your editor to a terminal? Drive the sandboxed agent from Zed's agent panel — Zed is the cockpit, the box stays the cage.

Coop speaks ACP, so you can register it as a custom agent and steer it from Zed while it runs in the container. Point one entry at each agent you use:

{
  "agent_servers": {
    "coop": {
      "type": "custom",
      "command": "coop",
      "args": ["acp", "claude"]   // or codex / gemini / fusion
    }
  }
}

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

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

Toolchain & services

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

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

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

coop up        # start the services, wait until healthy
coop claude    # the box reaches them by name (db, redis)
coop down -v   # stop them and wipe the throwaway data

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

Configuration

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

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

Variable What it does
COOP_GATE a command re-run in the box before a fork merge lands (e.g. make check)
COOP_EGRESS none cuts the box off the network entirely — no outbound, nothing to exfiltrate
COOP_RUNTIME force container / docker / podman instead of auto-detect
COOP_MEMORY · COOP_CPUS cap the box's resources (e.g. 4g, 2)
COOP_FUSION_GOVERNOR which model leads coop fusion by default

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

Troubleshooting

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

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

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

Stuck on something not here? Open an issue  ·  ← back home