Run long-running, autonomous coding agents in "YOLO" mode, safely.
agentspace is an alternative to running coding agents inside tmux, screen, or other terminal multiplexers, and to managing a pile of git worktrees by hand. Instead of keeping a session alive in a multiplexer, it leverages Docker to give each task its own long-running container that you can detach from and reattach to at will, just like a multiplexer, but with full workspace isolation built in.
Each task gets its own disposable workspace (a Docker volume) and its own throwaway container. The agent edits, tests, and builds inside that isolation; you keep full control over review, commits, and merges. agentspace currently wraps Codex, Claude Code and Mistral Vibe.
| Requirement | Why |
|---|---|
| Node.js (current LTS, 20+) | installs and runs the agentspace CLI via npm |
| Docker | runs the agent containers and workspace volumes |
| Git | configured on the host with access to your remotes |
The agent image is pulled automatically from GitHub Container Registry on first
use, so there's no manual build needed. Each task runs on a per-language image
(--runtime, default node); see Language runtimes.
npm i -g agentspace-cli # installs the `agentspace` command
agentspace --version # print the installed version (also -v, version)Run from inside the git repository you want the agent to work on:
cd ~/code/my-project
agentspace my-task spawn claude # clone into a fresh workspace, start an agent, and attachspawn infers the repo from the current folder (its origin URL and your
checked-out branch), cuts an agent/<repo>/<task> branch in an isolated volume,
starts the agent container, and attaches you straight to it. Talk to the agent
directly in your terminal. You can also provide a repo and base branch manually.
On first spawn you can perform authentication against Claude/Codex/Vibe. The session is shared across containers so one login per tool is enough to get you going. Tested so far with Claude Max and ChatGPT Plus subs; Vibe takes a Mistral sign-in or a MISTRAL_API_KEY.
When you want to step away, detach with the Docker standard: press Ctrl-P then Ctrl-Q in succession. The agent keeps running in the background. Reattach whenever you like:
agentspace my-task attach # drop back into the running agentagentspace my-task review # status, diff stat, and the full diff
agentspace my-task commit ["optional msg"] # show diff, generate a message, commit on accept
agentspace my-task commit-push ["optional msg"] # same, then push the branch in one step
agentspace my-task commit-apply-local ["optional msg"] # same, then apply it to your current local branchUse commit to commit your changes inside the workspace, or commit-push if you
want to push your branch immediately. Both generate the commit message for you
from the staged diff (and let you edit it before committing), so run them with no
message argument. Only pass a message (agentspace my-task commit "my message")
when you specifically want to write it yourself.
Once you've pushed (via commit-push or push), the task is just a normal
agent/<repo>/<task> branch on your remote, so you can merge your work through
your normal git procedures: open a pull request or merge it locally.
If you'd rather skip the remote round-trip, apply-local (or
commit-apply-local) drops the workspace's commits straight onto the branch you
have checked out in the current folder — no push, no PR. It fast-forwards when
your checkout hasn't moved past where the workspace was seeded (keeping the exact
commits), and otherwise cherry-picks just the workspace's new commits on top of
wherever you are. Run it from a clean checkout; networked git still never enters
the container — applying is pure local git on the host. As a safety check it
refuses when the current folder's origin differs from the workspace's (so a
stray run in the wrong clone warns instead of applying there); pass --force to
override.
agentspace my-task purge # remove the container and its workspace + sessions volumespurge tears the task down completely once you're done with it.
Some projects need Docker themselves: a Postgres for local dev, a containerized
app, a docker compose stack. Spawn the task with --docker and it gets its own
isolated, nested Docker daemon running as a sidecar:
agentspace my-task spawn claude --dockerThe agent can then use docker run, docker compose, and docker build
normally, and the host's Docker is never exposed. See
Run Docker inside a task for ports, reaching
services, and the security boundary.
See the agent's work running, without committing first, by starting a preview: a sidecar container that mounts the same workspace volume, runs a dev server, and publishes a port.
agentspace my-task preview node:24-slim --port 5173 \
--cmd "npm install && npm run dev -- --host 0.0.0.0 --port 5173"Because the preview shares the workspace volume, it sees edits live and hot-reloads as the agent works. See Preview results in a browser for the full story.
That's the whole loop. Everything below is reference.
- Workspace = state. A Docker volume holds the git repo and the agent's changes.
- Container = tool. A disposable agent container runs against that volume.
- Git = promotion. Short-lived helper containers clone/diff/commit/push; the agent container never holds git credentials.
All networked git (clone, fetch, push) runs on the host with your normal git setup, so credentials, host keys, and commit signing stay native. History moves between host and volume as git bundles piped over stdin/stdout, so there's no SSH agent forwarding into containers and no host-specific socket plumbing.
Restarts resume the agent's existing session rather than starting blank, and the work is always just a normal git branch, so agentspace adds no lock-in.
agentspace is deliberately opinionated: the agent never modifies git. It
edits, tests, and builds, and it may run read-only git (status, log, diff,
show, blame, ...) to inspect the repo, but you own review, commit, push, and
merge. This keeps an autonomous YOLO-mode agent from rewriting history,
force-pushing, or leaking credentials, and it's why promotion is a separate,
human-driven step.
Enforcement lives entirely in the agent image, not the CLI:
- Claude Code uses a system-managed
PreToolUsehook (/etc/claude-code/) that allows read-only git but hard-blocks any mutating command. It fires even under--dangerously-skip-permissions, backed bydenyrules for the mutating subcommands and a~/.claude/CLAUDE.mdinstruction. - Codex uses a system-managed
PreToolUsehook (/etc/codex/) that applies the same allowlist, plus a~/.codex/AGENTS.mdinstruction. This avoids overlapping execpolicy rules, whose most-restrictive-match behavior would deny read-onlygitcommands too. - Vibe uses a
pre_toolhook (/etc/vibe/) on its shell tools, registered from~/.vibe/hooks.toml. It applies the same allowlist, fires under--yolo, and isstrict(a hook that fails denies rather than waves through), plus a~/.vibe/AGENTS.mdinstruction.
Claude's and Vibe's hooks share one policy module, so read-only git means the same thing to both. The instruction files (and Vibe's hook registration) are re-seeded into the home volume on every start, so the policy applies to login volumes created before it existed too.
Nothing locks you in, in either direction:
- Want agents that do use git? The policy is in the image, not the CLI.
Build your own image with the hook and policy files removed, then point
AGENTSPACE_IMAGEat it. - Want to drop agentspace entirely? Every task is just a standard
agent/<repo>/<task>branch. Push it and carry on with plain git and your usual PR flow; there's nothing proprietary to migrate off.
For an even harder guarantee in the other direction, you can stub out the git
binary in a custom image (not done by default, since some build tools read git
metadata).
A fresh workspace is seeded from a git bundle, so it has the tracked files but
not the (git-ignored) node_modules, .venv, and friends. Rather than make the
agent install those on every run, agentspace kicks off the install in the
background the moment the container boots, so the agent can start reading and
planning while dependencies download.
What gets run is resolved in order:
.agentspace/setup.shin the repo, if present — the repo's own setup hook. Use it for anything an install needs beyond a single package command (codegen, env scaffolding, multiple package managers, ...). It runs from the repo root.- Otherwise the package manager is auto-detected from the lockfile or manifest that's present — pnpm / yarn / bun / npm, uv / poetry / pipenv / pip, Go modules, Cargo, or Bundler.
The install is idempotent across restarts: the resolved command and a fingerprint of the dependency files are recorded, so a later boot whose fingerprint still matches a prior successful run skips the install. It only re-runs when a base update actually changed the lockfiles, not on every restart.
The agent sees this through the asp-deps helper (baked into the image):
asp-deps status # one-line state: installing / ready / failed / nothing-to-do
asp-deps log # the install output so far
asp-deps wait [secs] # block until the install settles (used before tests/builds)The agent's guidance tells it to asp-deps wait before running builds or tests
and not to install by hand unless the install failed. Log and state live under
/workspace/.agentspace/, which is excluded from the workspace's git so it never
shows up in status, diffs, or review.
Each task runs on a per-language image, chosen at spawn with --runtime
(-R), so a task pulls only the toolchain it needs instead of one image with
every runtime:
agentspace my-task spawn claude # default: node
agentspace my-py spawn claude --runtime python # or -R python
agentspace my-svc spawn claude -R goThe default is node; --runtime accepts node, python, go, rust,
ruby, or full (the all-in-one image with every runtime, for multi-language
projects). Short aliases work too: py, rb, rs, js, n. Whatever you pick
is recorded on the workspace, so restart and pull -r rebuild the container on
the same image.
Every image shares a node:24-slim base — Node is always present because the
bundled agents are npm packages — and each adds its language's toolchain, so the
automatic dependency install works out of
the box for that ecosystem:
--runtime |
Runtime/tools | Package managers and helpers on PATH |
|---|---|---|
node |
Node.js 24, Deno, Bun | npm, pnpm, yarn, bun, vp |
python |
CPython 3 (Debian) | pip, uv, poetry, pipenv |
go |
current stable (go.dev) | go modules |
rust |
stable (rustup) | cargo (with rustfmt, clippy) |
ruby |
Ruby (Debian) | bundler |
full |
all of the above | everything in every row |
The base every image shares also carries the agent CLIs and the cross-cutting
tools, so any runtime can use them: a C/C++ toolchain (build-essential) so
packages with native extensions compile, Infrastructure-as-Code CLIs (tofu,
terraform, tflint), and browser testing (below). Vibe is a Python app, so the
base installs it with uv and uv's own managed CPython under /usr/local —
independent of whichever interpreter (if any) the runtime leaf ships. Each
toolchain is pulled from a
well-maintained, org-published source (Debian apt, go.dev, rustup, Astral's uv
installer, official HashiCorp and OpenTofu apt repositories, and upstream TFLint
releases). The daily image workflow resolves current tool versions once and
rebuilds only the layers whose versions changed, so both architectures receive
the same Node/Deno/Bun/Vite+ and agent CLI versions.
Browser testing. Playwright and a Chromium build
are baked in (the playwright CLI is on PATH), so agents can run end-to-end
specs, take screenshots, and assert against a real DOM out of the box. Browsers
live in a shared PLAYWRIGHT_BROWSERS_PATH (/usr/local/ms-playwright) rather
than the home volume, and the directory is writable so a project can install its
own pinned browser build (npx playwright install) alongside the baked one. Only
Chromium is preinstalled; add Firefox/WebKit with playwright install if needed.
Need something not in that list (a JVM, a specific Python version, system
libraries)? Extend one of the runtime images yourself and point
AGENTSPACE_IMAGE at it — that env var pins a single image for
every task, overriding --runtime.
# my-agent.Dockerfile — start from whichever runtime is closest to your needs
FROM ghcr.io/imrec/agentspace:python
RUN apt-get update && apt-get install -y --no-install-recommends \
default-jdk \
&& rm -rf /var/lib/apt/lists/*docker build -t my-agent -f my-agent.Dockerfile .
AGENTSPACE_IMAGE=my-agent agentspace my-task spawn claudeBuilding FROM ghcr.io/imrec/agentspace:<runtime> keeps the bundled agents and
the read-only-git policy; the same approach lets you bake in any other tooling
your project needs. (For --docker tasks, language runtimes can instead live in
the containers the agent runs; see
Run Docker inside a task.)
| OS | Status |
|---|---|
| macOS | tested |
| Linux | tested |
| Windows | unverified, expected to work |
agentspace depends only on Git, Docker, and Node.js, all of which run on Windows, so it should work there out of the box; it just hasn't been tested yet. If you run it on Windows, reports (success or bug) are very welcome.
Run task commands from anywhere; the agent branch, its origin, and the base branch are all recorded in the workspace volume.
agentspace <setup-command> [args] # pull, refresh, list, cache
agentspace <task> <command> [args] # everything that acts on a workspace
Host-wide setup commands (pull, refresh, list, cache) come first.
Everything that acts on a workspace — including spawn, which creates it — takes
the task first, then the command, like agentspace my-task spawn claude,
agentspace my-task check, or agentspace my-task shell. tool is codex,
claude, or vibe.
agentspace my-task spawn codex # seed from your local branch, start a detached agent
agentspace my-task spawn codex --base origin/main # seed from origin instead of local
agentspace my-task spawn codex --from git@github.com:me/other.git # seed from a different repo
agentspace my-task spawn codex --from ../sibling-checkout --base develop # seed from a local path + base
agentspace my-task spawn codex --runtime python # run on the python image (default: node)
agentspace my-task spawn codex --env-file .env --env LOG_LEVEL=debug # give the agent env varsBy default spawn infers the repo from the current folder: it records its
origin URL and seeds from the branch you have checked out — the branch's
committed history, including commits you haven't pushed to origin yet.
Uncommitted or staged working-tree changes are not carried (the seed is a git
bundle, which holds commits). This local-first default suits teams whose local
branches are ahead of a shared origin; a stale origin never silently wins.
To seed from the remote copy of a branch instead, pass --base origin/<branch>
(e.g. --base origin/main) — that fetches from origin over the network. Any
other --base <branch> (including a slashed name like feature/foo) is read
from your local checkout.
Pass --from <url|path> to seed from somewhere else entirely: any clone source
git understands. A remote URL is fetched over the network; a local path
is read at its committed branch tip (not a network round-trip). The recorded
origin becomes that source, so later push and update target it — make sure
you have push access there. For a --from source, --base defaults to the
source's default branch (its HEAD); for the current folder it defaults to your
checked-out branch. With neither a --from nor a usable origin, spawn errors.
spawn creates a workspace volume agentspace-<task>-vol, seeds it from your
local branch, cuts an agent/<repo>/<task> branch, and starts a detached
container named agentspace-<task>. It also creates a per-task
agentspace-<task>-sessions volume holding the agent's conversation transcripts.
All tasks for a tool share one home volume (codex-home / claude-home /
vibe-home) for tool credentials and config.
Environment variables. spawn takes the same --env-file <file> and
--env KEY=value flags as preview, on the
same terms: both are repeatable, --env wins over --env-file, and a later
--env-file wins over an earlier one. Files are read from the host where you run
agentspace, not from inside the workspace volume. So the agent gets your
project's .env — the API keys its tests need, a database URL pointing at a
nested --docker service — without you pasting secrets into its prompt:
agentspace my-task spawn claude --docker --env-file .env \
--env DATABASE_URL=postgres://services:5432/appThe env is recorded on the workspace volume, so every later restart, pull -r,
or restart-all brings the agent back with it. Files are re-read, not
snapshotted: edit .env on the host and agentspace my-task restart picks the
new values up. To change the set, pass --env-file / --env to restart —
whatever you pass replaces what was recorded:
agentspace my-task restart --env-file .env --env LOG_LEVEL=debugA recorded env file that has since moved or been deleted is skipped with a warning rather than blocking the restart.
agentspace my-task shell # open a shell in the agent container
agentspace my-task logs # follow the container logs
agentspace my-task attach # attach to the agent (restarts it first if stopped)
agentspace my-task restart # recreate the agent on the latest image (revives it from a leftover volume)
# pass --env-file/--env to replace the env it starts with
agentspace my-task stop # stop the container
agentspace my-task rm # remove the container (its volumes survive until purge)
agentspace my-task purge # remove the container and its workspace + sessions volumes
agentspace list # list workspace tool, task, uptime, and running-agent status
agentspace list --status # also compare workspaces to origin/base for git status
agentspace restart-all # recreate every workspace's agent on the latest local image
agentspace purge-all # purge every workspace at once (prompts; -y to skip)purge is quiet by default, printing just Workspace cleared: <task>; pass
-v for a line per removed item. purge-all clears every workspace after a
single confirmation and prints a one-line summary (-v for per-task detail).
Shorthands. The binary is also installed as asp, and most commands have a
short alias, so agentspace my-task spawn claude --docker can be typed
asp my-task scl --docker. Tools accept cl/co/vi for
claude/codex/vibe anywhere a <tool> is expected. Command aliases: s
spawn (scl/sco/svi = spawn claude/codex/vibe), sh shell, l logs,
a attach, r restart, st stop,
u update, ch check, rev review, cm commit, ps push, cp commit-push,
al apply-local, cal commit-apply-local, p purge (py/pv/pyv add
-y/-v), ls list, pa purge-all (pay = purge-all -y). Run agentspace help for the full list.
restart recreates, so it picks up new images and credentials. A plain
docker restart keeps a container's original image, so restart instead removes
the container and starts a fresh one against the same workspace and sessions
volumes — the agent resumes its task, now on the latest pulled image and with
whatever credentials the shared home volume currently holds. Because it depends
only on the workspace volume, restart also revives a task that was rm'd down
to a bare volume (the one way back from rm short of re-spawning). restart-all
does the same across every workspace at once; it does not pull, so run
agentspace pull first when you want newer images.
Pulling reclaims the image it replaces. When a pull moves a tag, the image
that tag used to name is left untagged but still on disk, and docker never
collects it on its own — left alone, daily pulls accumulate a full layer set per
release. pull removes each superseded image once nothing references it. An
agent still running on the old image pins it, so pull -r (which reboots agents
onto the new image first) reclaims it in the same run, while a plain pull
leaves it for the next one. Superseded images are never force-removed, so a
container's image is never deleted out from under it.
Sessions survive restarts. Conversation transcripts live in the per-task
agentspace-<task>-sessions volume (mounted over ~/.claude/projects,
~/.codex/sessions, or ~/.vibe/logs/session), separate from the shared home volume that holds
credentials. On start the container resumes the task's most recent session if one
exists, otherwise begins fresh, so attach drops you back into your ongoing
conversation instead of a blank one.
Detaching. While attached, press Ctrl-P then Ctrl-Q in succession (the
Docker standard) to detach your terminal and leave the agent running in the
background. Run agentspace <task> attach to reattach. Ctrl-C is not forwarded
into the agent, so it won't interrupt the running turn.
agentspace my-task check # is the work committed, pushed, up to date, or already on base?
agentspace my-task review # status, diff stat, and full diff
agentspace my-task update main # rebase the workspace onto origin/main
agentspace my-task commit ["optional msg"] # show diff, generate a message, commit on accept (no push)
agentspace my-task push # push the workspace's branch to its origin
agentspace my-task commit-push ["optional msg"] # commit (same as above) and push in one step
agentspace my-task apply-local # apply the branch to your current local checkout
agentspace my-task commit-apply-local ["optional msg"] # commit (same as above) and apply in one stepcheck is a read-only health report: it fetches origin and tells you, in plain
language, whether your latest changes are committed, whether every commit is on
the remote, whether your branch is behind the base, and whether every branch
commit is already on the base, with the exact command to fix each gap. It exits
non-zero when something still needs doing, so it doubles as a pre-merge gate in
scripts.
commit and commit-push show the staged diff before committing (with your host
git identity) and pushing. By default they generate the commit message for you:
the workspace's tool drafts a one-line subject from the staged diff and drops it
into an editable prompt, so you can accept it as-is or tweak it. Pass a message
argument (commit "my message") only when you want to write it yourself; then it's
used as-is after a yes/no confirm, with no generation step.
A task is a branch (agent/<repo>/<task>). push publishes it to origin; review
and merge through your normal pull-request flow, which respects branch
protection, required checks, and reviews. For a direct local merge instead:
git switch <base>
git pull --ff-only
git merge origin/agent/<repo>/<task>
git push origin <base>apply-local collapses that local-merge dance into one command: it pulls the
workspace's commits across as a bundle and applies them to the branch checked out
in the current folder, no origin round-trip. When your checkout is still at the
commit the workspace was seeded from it fast-forwards (the exact commits, no merge
commit); when it has moved on it cherry-picks just the workspace's new commits
(basecommit..agent/<repo>/<task>) on top of HEAD, so it works from any branch.
The working tree must be clean. A cherry-pick conflict is left in place to resolve
(git cherry-pick --continue / --abort). It also refuses to run when the current
folder's origin doesn't match the workspace's — pass --force (-f) to apply
there anyway. Like every other command, the only networked git stays on the host —
landing is purely local.
See the agent's work running, without committing first, by starting a preview: a sidecar container that mounts the same workspace volume, runs a dev server, and publishes a port.
# Node example: install deps and run a dev server, published on localhost:5173
agentspace my-task preview node:24-slim --port 5173 \
--cmd "npm install && npm run dev -- --host 0.0.0.0 --port 5173"
agentspace my-task preview-logs # follow install/build output and the server URL
agentspace my-task preview-restart # recreate the preview from its saved settings
agentspace my-task preview-stop # stop and remove the preview containerBecause the preview shares the workspace volume, it sees edits live and
hot-reloads as the agent works. The runtime comes from the image you name, so
point it at python:3.12, rust:1, or anything else and supply the matching
--cmd. --port accepts 5173 (published on 127.0.0.1), 8080:80
(host:container), or 0.0.0.0:8080:80 to expose it on your LAN. Run
agentspace <task> preview with no arguments for usage; pass --replace to
recreate a running preview.
To recreate a preview without re-typing the image, ports, cmd, and env, use
agentspace <task> preview-restart. It removes the container and starts a fresh
one from the settings the preview was created with (stored on the container), so
you get a clean slate — env files are re-read, picking up any host-side changes.
Environment variables. Pass --env-file <file> to load a host file of
KEY=value lines (e.g. your project's .env) into the preview container, and
--env KEY=value to set individual vars. Both are repeatable; --env wins over
--env-file, and a later --env-file wins over an earlier one. The file is read
from the host where you run agentspace (not from inside the workspace volume),
so point it at a .env on your machine:
agentspace my-task preview node:24-slim --port 5173 --env-file .env \
--env NODE_ENV=development \
--cmd "npm install && npm run dev -- --host 0.0.0.0 --port 5173"The server must bind
0.0.0.0, notlocalhost. A server listening only on127.0.0.1inside the container is unreachable from the host. Most dev servers need a flag for this (Vite--host 0.0.0.0, Next.js-H 0.0.0.0, Djangorunserver 0.0.0.0:8000, orHOST=0.0.0.0).
When any previews exist, list shows a PREVIEW column with each task's published
port, and rm/purge tear the preview down along with the task.
Some projects need Docker themselves: a Postgres for local dev, a containerized
app, a docker compose stack. Spawn the task with --docker and it gets its
own isolated, nested Docker daemon, running as a sidecar:
agentspace my-task spawn claude --docker
agentspace my-task docker-logs # follow the daemon's startup / pulls / buildsThe agent (and any preview) can then use docker run, docker compose, and
docker build normally. This never exposes the host's Docker: the host
socket is root-equivalent and is never mounted into a task; containers the agent
starts live inside the nested daemon's namespace. Tasks spawned without
--docker get no daemon, no network, and no Docker access.
State persists in a per-task agentspace-<task>-docker-lib volume (images, build
cache, volumes, DB data), surviving restart/stop and host reboots. The sidecar
follows the agent's lifecycle, and purge removes it along with the network and
data volume. When any task has a nested daemon, list shows a DOCKER column
with the daemon's status.
Reaching services. The agent, preview, and nested daemon share a private
per-task network on which the daemon is named services. Anything the agent
publishes inside the daemon is reachable at services:<port>: e.g. run
docker run -d -p 5432:5432 postgres and point your app at services:5432.
Reaching it from your browser. Host port publishing is fixed when the daemon
starts, so name the ports you want at spawn time with --ports (same forms as
preview, plus ranges):
agentspace my-task spawn claude --docker --ports 8000-8010
# inside the agent: docker run -d -p 8005:8080 webapp -> http://localhost:8005Adding Docker to a running task. Forgot --docker at spawn, or only later
realized a task needs it? Turn it on in place — no re-seeding:
agentspace my-task restart --docker # stand up the daemon, rewire the agent
agentspace my-task restart --docker --ports 8000-8010 # ...and publish host portsThis stands up the nested daemon and recreates the agent container wired to it.
The workspace and session volumes persist, so the agent resumes its task — only
its in-flight turn is interrupted (the same tradeoff as any restart). It's a
no-op for a task that already has Docker; since published ports are fixed when
the daemon starts, changing them still means a fresh spawn --docker --ports.
Be clear-eyed about this boundary: it is weaker than a task without
--docker, and it is not a sandbox for untrusted code:
- Protected: an agent mishap. An agent acting in good faith but autonomously
(YOLO mode) can't reach your machine through the nested daemon: no host Docker
socket, no host mounts, so even
docker run -v /:/host …mounts the sidecar's filesystem, not yours. The worst it can do is wreck its own throwaway daemon and per-task volumes. ✔ - Not protected: a deliberate escape. The nested daemon runs
--privileged(Docker-in-Docker requires it) and the agent fully controls it. Code that is actively trying to break out, such as a prompt-injection payload, can start a privileged nested container with well-known paths to the host kernel. Assume an attacker who can inject instructions into the agent can reach the host. ✘
So enable --docker only for repositories and prompts you would already trust on
your machine. If you need a hard boundary against hostile code, run agentspace on
a host that provides one: a stronger container runtime such as Sysbox, or a
microVM (Kata Containers, Firecracker, gVisor).
Each task's daemon is isolated, so concurrent --docker tasks each pull the same
images independently, which is wasteful on bandwidth and Docker Hub rate-limits
when you run many at once. Set AGENTSPACE_DOCKER_MIRROR=1 to enable a shared
pull-through cache: a single registry:2 proxy every --docker task uses as
a Docker Hub mirror. The first task to need an image fetches it; the rest are
served locally.
AGENTSPACE_DOCKER_MIRROR=1 agentspace a spawn claude --docker
AGENTSPACE_DOCKER_MIRROR=1 agentspace b spawn claude --docker # b's pulls hit the cache
agentspace cache status # up | stopped | not created
agentspace cache up # pre-warm / start it
agentspace cache down # stop it (keeps cached layers in its volume)- Saves bandwidth, pull time, and rate-limit pressure (N tasks become one upstream pull). Does not save disk: each daemon still unpacks its own copy.
- Covers Docker Hub only;
ghcr.io/quay.io/etc. pull directly (which still covers most base images: postgres, redis, nginx, node, python, …). - A host-wide singleton, kept running across tasks (not removed by
purge); manage it withagentspace cache. - Optional
AGENTSPACE_DOCKERHUB_USER/AGENTSPACE_DOCKERHUB_PASSWORDlet the cache authenticate its own upstream pulls for a higher rate limit.
For a repository named example and task my-task:
| Resource | Name |
|---|---|
| Container | agentspace-my-task |
| Workspace volume | agentspace-my-task-vol |
| Sessions volume | agentspace-my-task-sessions |
| Home volume | codex-home / claude-home / vibe-home (shared per tool) |
| Branch | agent/example/my-task |
With --docker, a task also gets a daemon sidecar agentspace-my-task-docker,
a private network agentspace-my-task-net, and a data volume
agentspace-my-task-docker-lib.
The container and volume are keyed by task name only; the repo name appears
only in the branch. So task names must be unique across all your repositories.
spawn refuses a task name that already has a workspace; remove it first with
agentspace <task> rm (or purge).
The CLI keeps the agent image fresh on its own: spawn pulls it at most once a
day (tracked in ~/.agentspace/state.json), so you pick up new releases without a
registry round-trip on every run. Force a refresh anytime:
agentspace pull # update the local image only
agentspace pull -r # also reboot running agents onto it (-y to skip the prompt)A plain pull leaves running agents on their original image until they're
recreated. pull -r removes and re-creates every workspace not already on the new
image — agents running or stopped on an older one, plus tasks left as a bare
volume — against the same workspace and session volumes, so each agent resumes its
conversation where it left off, though its in-flight turn is interrupted, so it
prompts first. (To recreate every workspace regardless of image, use
agentspace restart-all.)
Refresh shared tool credentials without touching workspaces or sessions:
agentspace refresh claude
agentspace refresh claude -y # skip the restart confirmationThe refresh runs the tool's login flow in a temporary container against the
shared home volume (claude-home / codex-home / vibe-home), then restarts
the agents for that tool that were already running. Session transcripts stay in
each task's own agentspace-<task>-sessions volume, so the restarted agents
resume their existing conversations. Claude's container login follows the normal
Claude Code flow: if the browser callback cannot reach the container, copy the
login URL and paste the resulting code back into the terminal. Vibe's refresh
runs vibe --setup, which takes either a Mistral sign-in or an API key and saves
it to ~/.vibe/.env; you can skip the login entirely by handing the key to the
task instead (agentspace my-task spawn vibe --env MISTRAL_API_KEY=...).
The image is published to
ghcr.io/imrec/agentspace:latestby thePublish agent imageworkflow (multi-arch amd64/arm64). AMD64 and ARM64 build and smoke-test on native GitHub runners in parallel; public tags move only after both architectures pass. Version-keyed inline caches avoid rebuilding unchanged tool layers.
Drop your own Claude Code / Codex / Vibe customizations into ~/.agentspace and
every task picks them up automatically, with no per-spawn flags. The directory
mirrors the in-container home layout, split by tool:
~/.agentspace/
├── claude/ # overlaid onto ~/.claude in the container
│ ├── settings.json # your settings (cannot weaken the git guardrails)
│ ├── skills/ # your skills
│ ├── commands/ # your slash commands
│ ├── agents/ # your subagents
│ └── CLAUDE.md # your global memory (kept; managed note appended)
├── codex/ # overlaid onto ~/.codex in the container
│ ├── config.toml # your Codex config
│ ├── prompts/ # your saved prompts
│ └── AGENTS.md # your global guidance (kept; managed note appended)
└── vibe/ # overlaid onto ~/.vibe in the container
├── config.toml # your Vibe config (model, theme, tool permissions)
├── agents/ # your custom agent profiles
├── prompts/ # your system / compaction prompts
├── hooks.toml # your hooks (kept; the managed block-git hook is appended)
└── AGENTS.md # your global guidance (kept; managed note appended)
Because it's just a folder, it transports at scale: keep it in a dotfiles repo,
sync it across machines, or share a team baseline. Point AGENTSPACE_CONFIG_HOME
elsewhere to use a different location.
Untested. Mounting your user config into every task is a new feature that hasn't been thoroughly tested yet. It may not behave as expected; reports (success or bug) are very welcome.
On every container start the folder is mounted read-only (so a YOLO agent
can't rewrite your source) and copied into the home volume, where the tool can
read and update it. The managed "never touch git" guardrails are then
re-asserted on top, so your config can extend the environment but never drop
them. Hard enforcement lives in /etc and on the host (see
Git is off-limits), outside any volume
you can reach, so a custom settings.json cannot re-enable git. Edits apply on
the next restart, the same as a credential refresh.
| Variable | Purpose |
|---|---|
AGENTSPACE_IMAGE |
pin a single image for every task, ignoring --runtime (a version pin or locally built image). Setting it disables the daily auto-pull (you manage updates). |
AGENTSPACE_IMAGE_REPO |
registry repo the runtime tags resolve against (default ghcr.io/imrec/agentspace); the runtime name becomes the tag, e.g. <repo>:python. |
AGENTSPACE_CONFIG_HOME |
host directory for your skills/commands/settings overlay (default ~/.agentspace). |
AGENTSPACE_GIT_IMAGE |
override the git-helper image (default alpine/git:latest). |
AGENTSPACE_STATE_DIR |
override where the pull state (last-pull timestamps, superseded image IDs) is stored. |
AGENTSPACE_DOCKER_IMAGE |
override the nested Docker daemon image for --docker tasks (default docker:dind). |
AGENTSPACE_DOCKER_MIRROR |
enable the shared pull-through cache for --docker tasks. |
AGENTSPACE_REGISTRY_IMAGE |
override the cache's registry:2 image. |
AGENTSPACE_DOCKERHUB_USER / AGENTSPACE_DOCKERHUB_PASSWORD |
authenticate the cache's upstream pulls for a higher rate limit. |
To build and test the image locally:
npm run image:build # builds the node image, tagged :node and :latest
npm run image:build:all # builds every runtime image + :full via docker buildx bake
AGENTSPACE_IMAGE=ghcr.io/imrec/agentspace:node agentspace my-task spawn codexThe GHCR package must be public for unauthenticated
docker pull, or rundocker login ghcr.iofirst.
Known gaps and rough edges, contributions welcome:
- Tools: only Codex, Claude Code, and Mistral Vibe are wired up today.
- Language runtimes: the image ships with Node.js; broader runtime support is on the roadmap. For now, bring your own image.
- Windows: unverified (see above).
- Pull-through cache: mirrors Docker Hub only;
ghcr.io/quay.iopull directly. --dockeris not a security sandbox against hostile code; see the security note.
Found a bug or want a feature? Open an issue or PR at
github.com/ImreC/agentspace. For a
tour of the codebase and the conventions to follow, read
AGENTS.md.
npm run dev -- <command> # run the CLI from source with tsx
npm run build # bundle to dist/index.mjs
npm run check # oxlint + tsc
npm run test # run the Vitest suite
npm run format # format with oxfmtMIT.