Motir

Set up the agent sandbox

Motir asks you to hand work to a coding agent that runs unattended, with approvals off. This is where it should run: a published container that can reach your workspace tree and nothing else on your machine. This page ends when you have a working one.

What it confines — and what it does not

Surface
Filesystem
Confined. The only host surfaces inside the container are a writable /workspace and your agent’s own credential, mounted read-only. No docker socket, no other host bind.
Surface
Network
Open, by design. Every agent needs its provider API, and every dispatched item needs git remotes. This image confines the filesystem blast radius, not egress — reach for docker’s own --network controls if your threat model needs more.
Surface
Privileges
Runs as the unprivileged node user (uid 1000), so files written into the mount stay owned by you rather than by root.

Before you start

Two things, and neither is a Motir account detail — you sign in inside the container in step 4, so there is nothing to mint or copy first. You do not need the Motir CLI on this machine either; it ships in the image.

What you need
Docker, running
How to get it
Docker Desktop or any engine. The images are built for linux/amd64 and linux/arm64, so Apple Silicon is a first-class machine and nothing is emulated. There is no build step — you pull.
What you need
Your agent’s own sign-in, on this machine
How to get it
Sign in to your coding agent once, here, before you start — or have its API key in your environment. Its credential mount is read-only, so the container can use a sign-in and can never perform one.

A Motir project usually spans several repositories and the work loop runs across all of them — so what you mount is the folder that contains your checkouts, not any one of them. Start the container from there:

your machine
~/work/                 ← start the container from HERE
├── motir-core/         ← a checkout
└── motir-ai/           ← another

1Pick your profile

One image per agent, each layer fetching that CLI from its own official source at build time. Find the row for the agent you already use — you need its tag and its credential mount in step 2.

Tier is how closely we track the vendor, not whether it works. Every profile here is published, and every one is built and smoke-tested before a release ships. Tier 1 means we pin both the install source and the credential location and verify them on every change. Tier 2 installs from a vendor endpoint we do not control, so day-to-day breakage there is reported rather than blocking — it is caught at release, not necessarily the hour it happens.

Profile · binary
claude · claude
Tier
Tier 1
Installed from
npm install -g @anthropic-ai/claude-code
Credential mount (read-only)
~/.claude
Profile · binary
codex · codex
Tier
Tier 1
Installed from
npm install -g @openai/codex
Credential mount (read-only)
~/.codex
Profile · binary
opencode · opencode
Tier
Tier 1
Installed from
npm, or the OpenCode install script (opencode.ai)
Credential mount (read-only)
~/.config/opencode · ~/.local/share/opencode
Profile · binary
kimi · kimi
Tier
Tier 1
Installed from
npm (@moonshot-ai/kimi-code) — needs Node ≥ 22.19
Credential mount (read-only)
~/.kimi-code
Profile · binary
antigravity · agy
Tier
Tier 2
Installed from
curl -fsSL https://antigravity.google/cli/install.sh | bash
Credential mount (read-only)
Profile · binary
cursor · agent
Tier
Tier 2
Installed from
curl https://cursor.com/install -fsS | bash
Credential mount (read-only)
~/.local/share/cursor-agent
Profile · binary
aider · aider
Tier
Tier 2
Installed from
pip (Python) — PyPI aider-chat
Credential mount (read-only)
~/.aider.conf.yml
Profile · binary
goose · goose
Tier
Tier 2
Installed from
the Goose installer (Block)
Credential mount (read-only)
~/.config/goose
Using something else? ghcr.io/moooon-b-v/motir-sandbox:base ships the sandbox with no agent at all — bring your own binary. Motir is agent-agnostic; the profiles make 8 CLIs first-class, they do not make Motir depend on any of them.

2Start the container

Two commands: pull the image, then start it. The pull is what makes the second one a current sandbox. docker run never goes back to the registry for an image this machine already has — and neither does docker start -ai, which is how this page tells you to come back to the container later.

That matters because the profile tags move. :claude and its siblings always point at the newest release, so a copy pulled weeks ago is out of date and nothing on your machine will say so — you get an older motir with fewer commands than this guide describes. Returning to a container you set up earlier? A pull on its own is not enough: the container was made from the old image and keeps it. Pull, then docker rm motir-sandbox and run the command again (or give the run a new --name). The sign-in from step 4 lives in the old container, so expect to do that step once more.

Two of the run command’s parts come from the row you just picked; everything else is the same for every profile. It drops you into a shell in /workspace.

Note there is no --rm, and the container has a name. The sign-in in step 4 is written inside the container, so a --rm run would throw it away the moment you exit. Set it up once and come back to it with docker start -ai motir-sandbox.
Want a sandbox you can re-enter exactly? Every profile publishes an immutable :<profile>-<version> tag beside the moving one — <version> is the @motir/cli release the image was cut from — and pulling that name gets the same bytes every time. Take it when you are pinning a CI runner or reproducing a run, and the moving tag when you would rather have the newest. The published list, with the digest for each, lives beside the image in packages/cli/sandbox/README.md.
Self-hosting? The CLI talks to https://app.motir.co unless told otherwise, so nothing here names a server. Point it at your own instance by adding -e MOTIR_SERVER=https://motir.example.com — Motir is open-core and self-hostable, and that variable is the whole difference.
sh · pull the claude image first — the same tag the command below runs
docker pull ghcr.io/moooon-b-v/motir-sandbox:claude
sh · the claude row — swap the tag and the mount for yours
docker run -it --name motir-sandbox \
  -v "$PWD:/workspace" \
  -v "$HOME/.claude:/home/node/.claude:ro" \
  ghcr.io/moooon-b-v/motir-sandbox:claude

3Or start it from VS Code instead

The same confined image, as a dev container: VS Code opens /workspace inside it with the same mounts, so your editor, terminal and agent all run behind the same boundary. Three sub-steps, and they replace step 2 rather than following it — steps 4 onwards are the same either way.

1 · Install the Dev Containers extension. From the Extensions view, or from the command palette — ⇧⌘P on macOS, Ctrl+Shift+P on Windows and Linux, F1 on all three, or View → Command Palette… if you would rather not hold a chord — then Extensions: Install Extensions. The palette is where two of these three sub-steps happen, so it is worth pinning now. The extension is what starts the container on your behalf, and it drives the same Docker engine step 2 uses.

2 · Add .devcontainer/devcontainer.json to the folder you are mounting — the same one step 2 would have started from. It pins the published image and passes the mount your profile needs. Write it from that folder in one command, because a GUI file manager will not do it for you: macOS Finder and most file pickers refuse a name beginning with a dot, and they refuse it without saying why.

your machine — in the folder you are mounting
mkdir -p .devcontainer
cat > .devcontainer/devcontainer.json <<'JSON'
{
  "name": "Motir sandbox (Claude Code)",
  "image": "ghcr.io/moooon-b-v/motir-sandbox:claude",
  "workspaceFolder": "/workspace",
  "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind",
  "mounts": [
    "source=${localEnv:HOME}/.claude,target=/home/node/.claude,type=bind,readonly"
  ],
  "remoteUser": "node",
  "overrideCommand": true
}
JSON

The quotes around <<'JSON' are load-bearing: they are what stops your shell from expanding ${localWorkspaceFolder} and ${localEnv:HOME} before they reach the file. Those are Dev Containers substitutions, and the editor is what resolves them. That command writes exactly this — the same file, if you would rather create it by hand:

.devcontainer/devcontainer.json
{
  "name": "Motir sandbox (Claude Code)",
  "image": "ghcr.io/moooon-b-v/motir-sandbox:claude",
  "workspaceFolder": "/workspace",
  "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind",
  "mounts": [
    "source=${localEnv:HOME}/.claude,target=/home/node/.claude,type=bind,readonly"
  ],
  "remoteUser": "node",
  "overrideCommand": true
}

Two routes inside VS Code do accept the dot-name where Finder will not: Explorer → New File, typing the whole path .devcontainer/devcontainer.json — it creates the .devcontainer folder for you — or the palette’s Dev Containers: Add Dev Container Configuration Files…, then replace what it scaffolds with the above.

Swap :claude and the mounts entry for your row from step 1. A dev container is not torn down when you close the window, so the sign-in in step 4 persists here without any extra flag — and for exactly that reason it also keeps the image it was first created from. Dev Containers reuses a local image just as docker run does, so step 2’s docker pull is still yours to run, in a terminal on your machine, before you reopen; an existing container then needs the palette’s Dev Containers: Rebuild Container to pick the new image up. Pin the immutable :<profile>-<version> tag here instead if you would rather this folder stay on a known image.

3 · Open the folder in the container. Command palette → Dev Containers: Open Folder in Container…, and pick the folder you just wrote the file into. VS Code pulls the image and attaches; its terminal is the same shell step 2 would have dropped you into. If that folder is already open in VS Code, Dev Containers: Reopen in Container does the same attach without asking which folder — that precondition is the entire difference between the two commands, and it is why the sub-step does not lead with the one that begins “Re”.

The devcontainer.json files inside the motir-core repository are not this file. They carry a build block pointing at that repository’s own Dockerfile, because they are its dev containers and a checkout is what those are for. For your own workspace, pin the image as above.

4Sign in

From the shell inside the container. It prints a code and a URL, you approve it in a browser on any device, and the container polls until the token is minted — a device grant, headless by construction, which is exactly what a container is.

inside the container
motir login
Two other ways in, for the machines that need them. A CI runner or a brand-new box has no browser to approve anything, so pass a token straight through: -e MOTIR_TOKEN on step 2’s command, minted under Settings → Account → Tokens. And a laptop that has already run motir login can mount its credential read-only — but then motir login has nowhere to write, and says so.

6Check it

The last thing, and the one that tells you the setup actually took:

inside the container
motir doctor
It checks
the workspace
Which tells you
/workspace is writable and the link from step 5 resolves — so you mounted the right folder, not a checkout inside it.
It checks
the Motir credential
Which tells you
Step 4’s sign-in landed, and it names where the credential came from.
It checks
your agent
Which tells you
The binary is on PATH, and its own credential is present where that profile keeps it — step 2’s read-only mount actually landed.

All three green is the end of this page: you have a confined container that can see your work and hold your credentials, and nothing else on your machine.

What next

Setting the sandbox up and using it are different things, and this page only owns the first. Inside the container the CLI behaves exactly as it does on a host — motir next to take one item, motir run <key> for a specific one, or motir auto to drain the ready set unattended, which is what the confinement is really for. All three are documented with the CLI, because none of them is about the container.

The unattended loop takes your agent’s own non-interactive and auto-approve flags (motir auto --agent "<your agent’s command>"). Those drift between vendor releases, so the current set lives beside the image in packages/cli/sandbox/README.md — which is also where the digests to pin, the confinement proof, the validation harness and the escape hatch for an unlisted agent live.

Next: API reference · Getting started · Stability & deprecation