Skip to content

Repository files navigation

agentspace

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.


Quick start

1. Prerequisites

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.

2. Install

npm i -g agentspace-cli      # installs the `agentspace` command
agentspace --version         # print the installed version (also -v, version)

3. Spawn and talk to the agent

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 attach

spawn 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 agent

4. Review and promote

agentspace 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 branch

Use 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.

5. Clean up

agentspace my-task purge          # remove the container and its workspace + sessions volumes

purge tears the task down completely once you're done with it.

Docker support

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

The 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.

Preview in a browser

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.


How it works

  • 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.

Git writes are off-limits to agents, on purpose

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 PreToolUse hook (/etc/claude-code/) that allows read-only git but hard-blocks any mutating command. It fires even under --dangerously-skip-permissions, backed by deny rules for the mutating subcommands and a ~/.claude/CLAUDE.md instruction.
  • Codex uses a system-managed PreToolUse hook (/etc/codex/) that applies the same allowlist, plus a ~/.codex/AGENTS.md instruction. This avoids overlapping execpolicy rules, whose most-restrictive-match behavior would deny read-only git commands too.
  • Vibe uses a pre_tool hook (/etc/vibe/) on its shell tools, registered from ~/.vibe/hooks.toml. It applies the same allowlist, fires under --yolo, and is strict (a hook that fails denies rather than waves through), plus a ~/.vibe/AGENTS.md instruction.

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.

Ejecting

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_IMAGE at 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).


Dependencies install automatically

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:

  1. .agentspace/setup.sh in 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.
  2. 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.


Language runtimes

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 go

The 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 claude

Building 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 support

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.


Command reference

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.

Spawn a workspace

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 vars

By 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/app

The 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=debug

A recorded env file that has since moved or been deleted is skipped with a warning rather than blocking the restart.

Work with a running agent

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.

Review and promote

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 step

check 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.

Preview results in a browser

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 container

Because 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, not localhost. A server listening only on 127.0.0.1 inside 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, Django runserver 0.0.0.0:8000, or HOST=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.

Run Docker inside a 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 / builds

The 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:8005

Adding 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 ports

This 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.

Security: what --docker does and doesn't protect against

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).

Sharing image pulls across tasks (optional cache)

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 with agentspace cache.
  • Optional AGENTSPACE_DOCKERHUB_USER / AGENTSPACE_DOCKERHUB_PASSWORD let the cache authenticate its own upstream pulls for a higher rate limit.

Naming

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).


Updating the agent image

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 confirmation

The 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:latest by the Publish agent image workflow (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.


Configuration

Bring your own skills, commands, and settings

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.

Environment overrides

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 codex

The GHCR package must be public for unauthenticated docker pull, or run docker login ghcr.io first.


Limitations & contributing

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.io pull directly.
  • --docker is 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.


Development

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 oxfmt

License

MIT.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages