Crabbox on qs-mac-studio
How isolated branch boxes run tests, full stacks, screenshots, and browser tunnels on the Mac Studio · ~16 min read ~– min read · Suggested by Noam engineeringoperations
Crabbox turns qs-mac-studio into a remote box factory: each branch gets an isolated Linux container, reachable over Tailscale, with its own filesystem, processes, ports, and optional inner Docker daemon. The result is a compact ladder of interaction: run gates, boot the full app stack, then tunnel one selected box into a local browser when a human or agent needs to inspect it.
The shape of the system
Crabbox is the direct path from a laptop to isolated dev boxes on qs-mac-studio. The laptop runs the normal Crabbox CLI, the external provider SSHes to the Studio, Docker starts a per-lease container, and Crabbox syncs the current branch into that container over Tailscale.
Setup is split between the laptop and the Studio
The laptop needs Crabbox, Tailscale, SSH access, and config. The Studio needs a runner image and an hourly reaper. The config chooses provider: external, points at studio-docker-provider, sets studio_host and studio_ssh_user, and enables privileged: true when the box must run the app’s own Docker stack.
Pure CLI work is the base layer
The first level is just Crabbox. Use crabbox doctor, crabbox warmup, crabbox run --id <slug> -- just api check, crabbox ssh, and crabbox release. This level is friendly to multiple boxes because every command can target an explicit slug.
Osprey adds app helpers
The repo-level just crabbox helpers select ready boxes, list slugs, capture screenshots, and open browser tunnels. just crabbox screenshot <slug> drives Playwright inside a running app box and copies the PNG back to /tmp/screenshots/ by default.
Claude Code uses crabbox-it
Inside Claude Code, crabbox-it is the operator wrapper. It should ensure or reuse a box, run the requested gates or app commands, collect screenshots or logs, and report the slug and result. It does not change the underlying model: the box still contains a clone of the current branch.
There are three interaction levels
Level 1 runs gates. Level 2 runs the full stack and screenshot automation. Level 3 opens local SSH tunnels to one selected box at localhost:4700 and Keycloak at localhost:4710. Levels 1 and 2 work well with several boxes; Level 3 is practically one active local target per port pair.
Lifecycle is part of the contract
Warm boxes are temporary. Crabbox releases idle warm boxes, crabbox cleanup can reap expired ones, and the Studio launchd reaper enforces max_lifetime_secs as a backstop for orphaned containers.
The shape of the system
Crabbox is a remote execution surface with a deliberately small control plane. For Osprey Strike, the Crabbox setup in infrastructure/crabbox/ uses the Mac Studio as the Docker host and the developer laptop as the command center. There is no coordinator service in this path, no Cloudflare tunnel, and no AWS dependency. The laptop runs crabbox; Crabbox invokes an external provider script; that script SSHes to qs-mac-studio; Docker starts a container; Crabbox then syncs and executes against the container over SSH.
flowchart LR Laptop[Developer laptop] --> CLI[crabbox CLI] CLI --> Provider[studio-docker-provider] Provider -->|SSH over Tailscale| Studio[qs-mac-studio] Studio --> Docker[Docker] Docker --> Box[per-lease Linux container] CLI -->|rsync + SSH over Tailscale| Box
The important word is per-lease. Two branches, or two developers, do not share a working directory on the Studio. Each warm box gets its own container, its own synced checkout of the current branch, its own processes, and, in app mode, its own inner Docker data volume. That makes the box closer to an ephemeral CI worker than to a shared dev server.
Every box spun up this way starts from the branch you are on when Crabbox syncs. If you want to test a different branch, switch branches locally and create or target the corresponding box deliberately.
Setup is split between the laptop and the Studio
The laptop setup proves reachability. A developer needs the Crabbox CLI, Tailscale access to the tailnet containing qs-mac-studio, and SSH access to the Studio account that can run Docker. The source README calls out the one-time checks: tailscale status, ping qs-mac-studio, and ssh <studio_ssh_user>@qs-mac-studio echo ok.
brew install openclaw/tap/crabbox
tailscale status | grep qs-mac-studio
ssh <studio_ssh_user>@qs-mac-studio echo ok
The Studio setup has two durable pieces. build-runner.sh ships the runner build context to the Studio and overlays the live repo-root .mise.toml, so the image bakes the project toolchain from the same source developers use locally. install-reaper.sh installs an hourly launchd job that runs studio-reap.sh, removing orphaned containers that outlive their hard cap.
cd infrastructure/crabbox
./build-runner.sh <studio_ssh_user>@qs-mac-studio
./install-reaper.sh <studio_ssh_user>@qs-mac-studio
The repo-root crabbox.yaml chooses the external provider and passes the Studio-specific settings into studio-docker-provider: the host, SSH user, runner image, container user, persistent laptop keypair path, Docker binary path, and max_lifetime_secs. The app-stack path also needs privileged: true, because the container must run its own Docker daemon for the Osprey compose stack.
provider: external
external:
command: /absolute/path/to/osprey-strike/infrastructure/crabbox/studio-docker-provider
workRoot: /work/crabbox
config:
studio_host: qs-mac-studio
studio_ssh_user: q
image: crabbox-studio-runner:latest
container_user: crabbox
ssh_key: ~/.config/crabbox-studio/id_ed25519
max_lifetime_secs: 28800
docker: /usr/local/bin/docker
privileged: true
privileged: true is chosen when the box is created, not per command. Keep a plain test config for gates and a privileged app config for full-stack work if you want both modes without editing one file back and forth.
Pure CLI work is the base layer
The first useful mode does not need Claude Code or repo helpers. Once config is in place, plain Crabbox commands give you the normal lifecycle: check the provider, warm a box, run a command, SSH into it, and release it.
crabbox doctor
crabbox warmup
crabbox run -- just api check
crabbox ssh
crabbox release
When there is more than one ready box, make the slug explicit. This is the habit that keeps parallel branch work sane.
crabbox list
crabbox run --id <slug> -- just api check
crabbox run --id <slug> -- just mcp check
crabbox ssh --id <slug>
The provider makes this possible by returning normal SSH coordinates to Crabbox: user, host, published port, and key path. The container’s sshd is published on an ephemeral port on the Studio host, bound to a Tailscale-reachable address. Crabbox handles the sync and command execution after that; the provider is only the lifecycle adapter.
Level 1 work is multi-box friendly. Running gates on several branches is just a matter of keeping the slugs straight and passing --id <slug>.
Osprey adds app helpers
The Osprey helper layer starts where plain command execution stops. The infrastructure/crabbox/justfile is meant to be imported from the repo root so operators can list boxes, ensure one exists, capture screenshots, and open browser tunnels without remembering the lower-level SSH plumbing.
just crabbox ensure
just crabbox list
just crabbox screenshot <slug>
just crabbox app-tunnel <slug>
just crabbox ensure prints the slug of a ready box, reusing one if possible unless --fresh is passed. It deliberately does not start the app stack. That separation matters: a ready box means Crabbox can SSH to it; an app-ready box means just dev-headless has already brought up the Osprey services inside it.
For app work, the normal sequence is to ensure or choose a box, run the full stack inside that box, then use either screenshot automation or an SSH tunnel.
slug=$(just crabbox ensure)
crabbox run --id "$slug" -- just dev-headless
SMOKE_TARGET=/admin/tenants just crabbox screenshot "$slug"
The screenshot path runs Playwright inside the box against http://localhost:4700, logs in through Keycloak as a seeded user such as sysadmin/sysadmin, navigates to the target route, writes a PNG in the box, then uses the SSH details from crabbox ssh --id <slug> to copy the file back. By default, outputs land under /tmp/screenshots/ rather than in the repo tree.
Level 2 is also multi-box capable. You can keep several full-stack boxes alive, but commands should name the target slug because auto-selection only stays safe when exactly one ready box exists.
Claude Code uses crabbox-it
Claude Code should use Crabbox as a remote workbench, not as a mystery side channel. The crabbox-it skill is the right interface when the agent is already inside a coding task and needs an isolated box to run gates, boot the stack, inspect a page, or capture a screenshot.
The useful contract for crabbox-it is simple:
- Ensure or reuse a Crabbox for the current branch.
- Run the requested command with an explicit slug.
- If app inspection is needed, start
just dev-headlessin the box. - Use
just crabbox screenshot <slug>for agent-readable visual proof. - Report the slug, command, result, and any captured artifact.
That wrapper is valuable because it keeps the agent from mixing local host state with branch-specific app state. The box is still just a Crabbox. It still contains the current branch clone. The difference is that Claude Code can treat it as a repeatable environment instead of hand-assembling the lifecycle every time.
Use crabbox-it to run the API gate for this branch.
Use crabbox-it to boot the app and capture /admin/tenants.
Use crabbox-it to verify the fix in a fresh box.
Use the Claude Code path for tasks where the agent needs to iterate. Use pure CLI when a human operator already knows the exact command and only wants the box as remote execution capacity.
There are three interaction levels
Crabbox becomes easier to reason about when the interaction levels are explicit. The same box can support different work, but the concurrency story changes as you climb the ladder.
| Level | What you do | Example | Multiple boxes? |
|---|---|---|---|
| 1 | Run gates and tests | crabbox run --id <slug> -- just api check |
Yes. This is the best multi-branch mode. |
| 2 | Run the full stack and automate screenshots | crabbox run --id <slug> -- just dev-headless; just crabbox screenshot <slug> |
Yes, within Studio resources. Always name slugs. |
| 3 | Open local browser tunnels | just crabbox app-tunnel <slug>; open localhost:4700 |
Practically one active target per local port pair. |
Level 1 is cheap and direct. It is the right default for gates, tests, and commands that do not need an app browser. Level 2 is heavier because privileged app boxes run an inner Docker daemon and the Osprey stack. It is still parallelizable, but bounded by the Studio’s CPU and RAM. Level 3 is human-interactive: the tunnel forwards local :4700 to the app gateway and local :4710 to Keycloak because the login redirect needs both origins.
just crabbox list
just crabbox app-tunnel <slug>
# open http://localhost:4700
# Keycloak is forwarded at http://localhost:4710
Because the tunnel binds fixed local ports, treat Level 3 as a single selected box at a time on a laptop. You can stop one tunnel and open another, or choose different local ports by changing the helper, but the default operator model is one interactive box per 4700/4710 pair.
If local just dev is already using 4700 or 4710, close it before opening the tunnel. The forwarded ports must match the app and Keycloak redirect origins.
Lifecycle is part of the contract
A remote dev box that never goes away is just a shared server with extra steps. Crabbox avoids that by giving boxes a lifecycle. crabbox warmup keeps a box available for reuse. Crabbox itself releases idle warm boxes. crabbox release tears down the selected lease. crabbox cleanup lets the provider remove expired containers. The Studio reaper is the safety net when a laptop dies mid-lease.
The provider labels containers with the lease, slug, creation time, and max_lifetime_secs. In privileged mode it also creates a lease-named /var/lib/docker volume so the inner Docker daemon gets a real lifecycle: removed on release, swept by cleanup, and pruned by the reaper when orphaned.
crabbox list
crabbox release --id <slug>
crabbox cleanup
ssh qs-mac-studio 'cat ~/.crabbox-studio/reap.log'
The most common scary-looking message is not a Studio failure at all: lease <id> claim changed; retry. The Crabbox source notes this as a benign optimistic-concurrency race in the local claim file, usually right after crabbox warmup. Re-run the command.
Other failures usually sort into three buckets: the laptop cannot reach qs-mac-studio; the provider cannot find Docker because non-interactive SSH has a minimal PATH; or an app command is running in a non-privileged test box. The first is Tailscale/SSH setup. The second is the docker: absolute path in crabbox.yaml. The third is a config choice: app boxes need privileged mode before they are created.
What to take forward
The Crabbox setup is not merely faster remote hardware. It is a way to give humans and agents branch-scoped, disposable, inspectable environments without turning the Mac Studio into a shared mutable workstation. The clean mental model is: use Level 1 for gates, Level 2 for full-stack proof and screenshots, Level 3 for one selected browser session. Keep slugs explicit once more than one box exists. Rebuild the runner when the toolchain changes. Let the reaper make forgotten boxes temporary.
- Crabbox on
qs-mac-studiois direct-only: laptop CLI, SSH over Tailscale, Docker on the Studio, no coordinator. - Each box gets a synced clone of the current branch, so branch choice happens before or during box creation and sync.
- Pure CLI Crabbox commands are the base layer and work well with multiple boxes when slugs are explicit.
- Osprey's
just crabboxhelpers add ensure, list, screenshot, and browser tunnel workflows. - Claude Code should use
crabbox-itwhen the agent needs an isolated branch box for gates, app boot, screenshots, or verification. - The tunnel workflow is intentionally single-target by default because local ports
4700and4710point at one selected box.
Discussion Prompts
- Which gates should become the default Level 1 smoke set for every branch before a PR?
- Should the repo carry two named Crabbox configs, one plain test config and one privileged app config, to avoid editing
privilegedby hand? - What screenshots should
crabbox-itcapture by default when validating an app-facing change?
References
- Crabbox - The CLI and provider model used by the Studio Docker external provider.
- Tailscale: What is Tailscale? - Background on the private network layer that makes the Studio reachable without exposing it publicly.
- Docker Docs: Running containers - The underlying container lifecycle Crabbox delegates to the Studio provider.
Generated by Cairns · Agent-powered with Claude