toolsdevopsai

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.

SSH over Tailscale

rsync + SSH over Tailscale

Developer laptop

crabbox CLI

studio-docker-provider

qs-mac-studio

Docker

per-lease Linux container

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.

Key Takeaway

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.

Laptop preflight
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.

Studio image and reaper
cd infrastructure/crabbox
./build-runner.sh <studio_ssh_user>@qs-mac-studio
./install-reaper.sh <studio_ssh_user>@qs-mac-studio

The current setup pivot is where that config lives. Older Crabbox builds around 0.34.0 accepted a repo-root crabbox.yaml. Newer builds moved repository config into an untrusted domain: 0.36.0 blocked repository config from redirecting inherited provider credentials, and 0.37.0 tightened that boundary for external providers. On 0.38.0, the Studio external provider belongs in trusted user config, which crabbox config path prints on your laptop. On macOS that is usually ~/Library/Application Support/crabbox/config.yaml.

The trusted user config 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. It also has to set external.connection.ssh.trustProviderOutput: true, because the provider creates a container and reports back its ephemeral SSH host and port. Crabbox refuses to inherit personal SSH credentials into a repository-configured external provider for the same reason it refuses most credential redirection from untrusted files: a checkout should not be able to decide where your key is offered.

Config skeleton
provider: external

external:
  command: /absolute/path/to/osprey-strike/infrastructure/crabbox/studio-docker-provider
  workRoot: /work/crabbox
  connection:
    ssh:
      trustProviderOutput: true
  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 # app boxes only

The trusted config file must be locked down: chmod 0600 "$(crabbox config path)". crabbox doctor fails the file at broader permissions, and that failure is the correct place to catch setup mistakes before spending a container.

Warning

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. If you change the flag, release the old box first; just crabbox ensure will otherwise hand you the ready box created under the previous setting.

Pure CLI work is the base layer

The first useful mode does not need Claude Code or app helpers. Once config is in place, Crabbox gives you the normal lifecycle: check the provider, warm a box, name the box you are targeting, run a command, SSH into it, and release it.

Pure Crabbox lifecycle
crabbox doctor
crabbox warmup
crabbox list
BOX="<slug from list>"
crabbox run --id "$BOX" -- just api generate
crabbox run --id "$BOX" -- just api check
crabbox ssh --id "$BOX"
crabbox release --id "$BOX"

When there is more than one ready box, make the slug explicit. This is the habit that keeps parallel branch work sane.

Target one branch box
crabbox list
crabbox run --id <slug> -- just api generate
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.

Tip

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>. On a fresh box, run just api generate before just api test; gqlgen outputs are gitignored and do not sync into the container, so a fresh Crabbox has the same codegen gap as a fresh worktree.

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.

Repo helper commands
just crabbox ensure
just crabbox list
just crabbox screenshot "" <slug>
just crabbox screenshot admin-screen <slug>
just crabbox screenshot /admin/tenants <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. It also means a config change does not mutate an existing box. If you switch from a test box to a privileged app box, release the old slug before ensuring a new one.

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.

Full-stack box
# after setting privileged: true in the trusted config
crabbox release --id "$BOX"  # if an old test box exists
BOX="$(just crabbox ensure)"
echo "$BOX"  # confirm this is a new slug
crabbox run --id "$BOX" -- just dev-headless
just crabbox screenshot "" "$BOX"
just crabbox screenshot admin-screen "$BOX"

just dev-headless returns after the stack reaches STACK READY; it does not hold the terminal open. The screenshot helper now takes three positional arguments: what, slug, and out. Keep the first argument explicit when you are passing a slug. An empty what runs the built-in goto scenario at /admin/tenants, a leading-slash value runs that goto scenario at a different app path, a bare name runs a committed library scenario from infrastructure/crabbox/scripts/<name>.mjs, and an existing .mjs file runs as either an in-tree scenario or an uploaded scratch scenario. A missing .mjs path is an error, not a route to navigate to.

The screenshot path runs Playwright inside the box against http://localhost:4700, logs in through Keycloak as a seeded user such as sysadmin/sysadmin, drives the selected scenario, writes one or more PNGs in an in-box staging directory, then uses the SSH details from crabbox ssh --id <slug> to copy artifacts back. By default, outputs land under /tmp/screenshots/ rather than in the repo tree, and existing filenames get anti-clobber suffixes.

Key Takeaway

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:

  1. Ensure or reuse a Crabbox for the current branch.
  2. Run the requested command with an explicit slug.
  3. If app inspection is needed, start just dev-headless in the box.
  4. Use just crabbox screenshot "" <slug> for a route capture, or a named scenario such as just crabbox screenshot admin-screen <slug> for richer visual proof.
  5. 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.

Claude Code shape
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.
Tip

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>; just crabbox screenshot admin-screen <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.

Browser tunnel mode
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.

Warning

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, but the commands are easy to misread. crabbox warmup keeps a box available for reuse. crabbox run --id <slug> -- ... reuses that named box. A bare crabbox run -- ... leases a fresh ephemeral box and destroys it when the command exits, so every command pays the cold dependency cost again. crabbox release --id <slug> tears down the selected lease. A bare crabbox release only prints a usage line and leaves the box running. crabbox cleanup lets the provider remove expired or orphaned containers; it is not a housekeeper for a healthy warm box. 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.

Cleanup habits
crabbox list
crabbox release --id <slug>
crabbox list # confirm the box is gone
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 a few buckets: the laptop cannot reach qs-mac-studio; the provider cannot find Docker because non-interactive SSH has a minimal PATH; the trusted user config is missing external.connection.ssh.trustProviderOutput: true or is broader than 0600; a fresh box is missing gitignored gqlgen output before just api test; 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 the trusted config. The third is the config trust boundary. The fourth is crabbox run --id "$BOX" -- just api generate. The fifth is a box-type choice: app boxes need privileged mode before they are created, and changing the flag means releasing the stale box before ensuring a new one.

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.

  1. Crabbox on qs-mac-studio is direct-only: laptop CLI, SSH over Tailscale, Docker on the Studio, no coordinator.
  2. Each box gets a synced clone of the current branch, so branch choice happens before or during box creation and sync.
  3. Current Crabbox builds require the Studio external provider in trusted user config with trustProviderOutput enabled and permissions locked to 0600.
  4. Pure CLI Crabbox commands are the base layer and work well with multiple boxes when slugs are explicit.
  5. Osprey's just crabbox helpers add ensure, list, screenshot, and browser tunnel workflows.
  6. Claude Code should use crabbox-it when the agent needs an isolated branch box for gates, app boot, screenshots, or verification.
  7. The tunnel workflow is intentionally single-target by default because local ports 4700 and 4710 point 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 a clearly named user-config example, plus one plain test variant and one privileged app variant, so nobody copies provider config into the repo root?
  • What screenshots should crabbox-it capture by default when validating an app-facing change?

References

  1. Crabbox - The CLI and provider model used by the Studio Docker external provider.
  2. Constructured/osprey-strike#1031 - Setup feedback that exposed the current trusted-user-config requirements and warm-box lifecycle traps.
  3. Tailscale: What is Tailscale? - Background on the private network layer that makes the Studio reachable without exposing it publicly.
  4. Docker Docs: Running containers - The underlying container lifecycle Crabbox delegates to the Studio provider.