Authoritative reference for the codebox CLI surface. This document
describes the user-facing contract — invocation, flags, defaults, and exit
behaviour. The Go code in internal/cli is the
implementation of this contract; if the two ever disagree, this file is
canonical and the code should be updated.
codebox [command] [flags] [arguments]
- Running
codeboxwith no arguments prints the banner followed by the top-level help text and exits with code0. - The banner (ASCII art, version, project URL, blank line) is written to
stdout on every invocation, including
--helpand--version. --helpis accepted at every level (codebox --help,codebox <command> --help).--versionprints the version tag and exits.- Help text preserves the flag order documented in this file — flags are never alphabetised.
| Flag | Type | Default | Notes |
|---|---|---|---|
--orchestrator |
enum | podman |
One of podman, docker. |
--remote |
string | (unset) | user@host. When omitted, the orchestrator runs on the local machine. SSH is the transport; ProxyJump configured in ~/.ssh/config is honoured automatically. |
--instance-key |
path | (auto) | SSH private key used to log into the instance. When unset, codebox tries the user's default keys. |
Default flags can be set in a .codebox.conf file (YAML) so they apply
without retyping. Entries under args.all become persistent root flags;
entries under args.create are passed to create (and to workflow,
which runs create internally). Each entry is a bare flag name (claude)
or a name=value pair (python=3.14, codex=false). codebox reads the
global ~/.codebox.conf first, then the project ./.codebox.conf, then
the command line, with later sources overriding earlier ones by flag
name — so the command line always wins, the project file beats the global
file, and an explicit --claude=false overrides a claude default from
either file.
Exit codes:
| Code | Meaning |
|---|---|
0 |
Success. |
1 |
Generic failure (parse error, runtime error). |
130 |
Interrupted by signal (SIGINT/SIGTERM). |
Create a new sandbox instance.
codebox create demo \
--orchestrator=podman --remote=user@host --instance-key=~/.ssh/id_rsa \
--https-proxy=http://proxy.corp:3128 \
--os=debian_12 \
--python=3.14 --node=24 --golang=1.26.0 --dotnet=10 \
--claude --claude-credentials \
--codex --codex-credentials --opencode --opencode-credentials \
--podman --psql --tmux
Flags (in help order):
| Flag | Type | Default | Description |
|---|---|---|---|
--orchestrator |
enum | podman |
Container orchestrator (podman, docker). |
--remote |
string | (local) | Provision on a remote host (user@host). |
--instance-key |
path | (auto) | SSH key for logging into the new instance. |
--rebuild |
bool | false |
Force a rebuild of the base image even if a cached one exists. |
--https-proxy |
string | (unset) | Append export HTTPS_PROXY="<value>" to the in-container user's login profile so interactive shells route HTTPS through the configured proxy. Other build-time downloads (apt, dnf, Go, .NET, uv, nvm) still go through the builder host's network — only the operator's shell inside the running container sees the proxy. The exception is the agent install layers (--claude, --codex, --opencode), which export HTTPS_PROXY inline so curl (and any sub-downloads the install scripts perform) route through the proxy too. The value is not validated; pass it the way curl would accept it (http://proxy:3128, http://user:pw@proxy:3128, ...). |
--os |
enum | debian_13 |
Base OS image (debian_12, debian_13, ubuntu_24, ubuntu_26, redhat_10). |
--python |
enum | (none) | Install Python at 3.12, 3.13, or 3.14. |
--node |
enum | (none) | Install Node.js at major version 24, 25, or 26. |
--golang |
enum | (none) | Install Go at version 1.26.0. |
--dotnet |
enum | (none) | Install .NET at version 8 or 10. |
--claude |
bool | false |
Install Claude Code via Anthropic's native installer. Also drops /home/user/.claude.json with the onboarding flag pre-set so the CLI does not prompt on first run inside the sandbox. |
--claude-credentials |
bool | false |
After the container starts, rsync ~/.claude/.credentials.json from the operator's machine into /home/user/.claude/.credentials.json inside the instance. Ignored unless --claude is set. When --claude is set, a missing source file fails fast (before the build). Credentials are never baked into the image. |
--codex |
bool | false |
Install the OpenAI Codex CLI via its native installer (binary lands in $HOME/.local/bin, shared with Claude/uv on the login profile's PATH). When --https-proxy is set, the proxy is exported inline so the install download routes through it. Also labels the container codex=true. After the container starts, if ~/.codex/config.toml exists on the operator's machine it is copied into /home/user/.codex/config.toml inside the instance; a missing file is silently skipped. The config is never baked into the image. |
--codex-credentials |
bool | false |
After the container starts, if ~/.codex/auth.json exists on the operator's machine it is copied into /home/user/.codex/auth.json inside the instance; a missing file is silently skipped. Ignored unless --codex is set. Credentials are never baked into the image. |
--opencode |
bool | false |
Install opencode via its native installer (binary lands in $HOME/.opencode/bin, added to the login profile's PATH). Also labels the container opencode=true. After the container starts, if ~/.config/opencode/opencode.json exists on the operator's machine it is copied into /home/user/.config/opencode/opencode.json inside the instance; a missing file is silently skipped. The config is never baked into the image. |
--opencode-credentials |
bool | false |
After the container starts, if ~/.local/share/opencode/auth.json exists on the operator's machine it is copied into /home/user/.local/share/opencode/auth.json inside the instance; a missing file is silently skipped. Ignored unless --opencode is set. Credentials are never baked into the image. |
--podman |
bool | false |
Install rootless Podman (plus podman-compose and the rootless networking/storage stack, including passt for pasta networking) inside the instance, configure /etc/subuid, /etc/subgid, and the per-user containers.conf / registries.conf, start the container with the device/capability/security-opt flags nested containers need, and run podman system migrate once the container is up. |
--psql |
bool | false |
Install the psql PostgreSQL client. |
--tmux |
bool | false |
Install tmux and label the container tmux=true. Accepts --tmux or `--tmux=true |
All boolean flags are on/off toggles that accept the explicit
--flag=true|false form: --claude (equivalently --claude=true)
enables an agent, while --claude=false disables it — and the same for
--codex / --opencode. This matters together with .codebox.conf: a
default such as claude listed under args.create enables the agent for
every create/workflow in that scope, and passing --claude=false on
the command line overrides it for a single run (command line beats
project config beats global config — see
Common conventions for the precedence rules).
The same toggle/override applies to every bool flag (--podman, --tmux,
the *-credentials flags, …).
The --help output for create ends with six footer sections that
restate the legal values for each kind of flag:
- Orchestrators — values accepted by
--orchestrator. - OS — values accepted by
--os, with a human-readable label. - Languages — language runtime flags and their version values.
- Agents — coding-agent install flags.
- Tools — other tool install flags.
- Network — proxy and other network-related knobs.
Delete a sandbox instance. The container is stopped and removed.
codebox delete demo --orchestrator=podman --remote=user@host
| Flag | Type | Default | Description |
|---|---|---|---|
--orchestrator |
enum | podman |
Container orchestrator (podman, docker). |
--remote |
string | (local) | Target a remote host (user@host). |
List sandbox instances managed by the chosen orchestrator.
codebox list --orchestrator=podman --remote=user@host
| Flag | Type | Default | Description |
|---|---|---|---|
--orchestrator |
enum | podman |
Container orchestrator (podman, docker). |
--remote |
string | (local) | Target a remote host (user@host). |
Open an interactive shell into a sandbox instance over SSH. The shell
opens in ~/source — the per-sandbox checkout — falling back to the
home directory when ~/source does not exist yet.
If the instance was created with --tmux (it carries the tmux=true
container label), codebox shell launches tmux instead of a bare login
shell: a fresh session split horizontally into two panes, both rooted at
~/source. When the instance carries exactly one agent label
(claude=true, codex=true, or opencode=true, set when it was created
with --claude, --codex, or --opencode), that agent runs in the
right-hand pane — through a login shell so its install directory is on PATH —
while the left pane is an ordinary shell. When no agent is installed, or
when several are, codebox cannot pick one for the operator, so both panes
stay plain shells.
codebox shell demo \
--orchestrator=podman --remote=user@host --instance-key=~/.ssh/id_rsa \
--port=8000:3000 --port=8001:3001
| Flag | Type | Default | Description |
|---|---|---|---|
--orchestrator |
enum | podman |
Container orchestrator (podman, docker). |
--remote |
string | (local) | Target a remote host (user@host). |
--instance-key |
path | (auto) | SSH key for logging into the instance. |
--port |
L:R (repeatable) |
(none) | Forward LOCAL:REMOTE for the lifetime of the shell. Repeat for multiple forwards. |
Forward TCP ports from localhost to a sandbox instance and hold them
open until interrupted, without opening a shell. Unlike shell's
--port, the set of forwards is read from configuration rather than
flags, so the same mapping is reused every time.
The forwards come from the port-forward: list in the project
.codebox.conf (the file in the current directory; a port-forward:
entry in the global ~/.codebox.conf is ignored, since forwards are
inherently per-project). Each entry is LOCAL:REMOTE; a bare PORT
maps that port to itself (PORT:PORT):
port-forward:
- 13000:3000 # localhost:13000 -> 3000 in the instance
- 13001:3001
- 8080 # localhost:8080 -> 8080 in the instanceWhen the project config has no port-forward: list and a
compose file is present in the current directory (one of
compose.yaml, compose.yml, docker-compose.yaml,
docker-compose.yml, podman-compose.yaml, podman-compose.yml, in
that order), the command falls back to auto mode:
it parses the compose file, collects each service's published
(host-side) port, and forwards each one to itself. This maps
localhost:PORT to the same PORT the container publishes inside the
instance — its "external" port. Short (8080:80, 127.0.0.1:8080:80,
3000, 3000-3002:3000-3002, optionally /tcp|/udp) and long
(target:/published:) port forms are both understood; ranges expand
to one forward per port.
When neither source yields a port, the command fails with
no ports to forward: ... before contacting the orchestrator.
On success the mapped ports are printed and the command blocks until
Ctrl-C (or SIGTERM), which it reports as a clean shutdown:
codebox port-forward demo \
--orchestrator=podman --remote=user@host --instance-key=~/.ssh/id_rsa
Forwarding ports to instance demo:
localhost:13000 -> 3000
localhost:13001 -> 3001
Press Ctrl-C to stop.
| Flag | Type | Default | Description |
|---|---|---|---|
--orchestrator |
enum | podman |
Container orchestrator (podman, docker). |
--remote |
string | (local) | Target a remote host (user@host). |
--instance-key |
path | (auto) | SSH key for logging into the instance. |
Positional arguments:
| Argument | Required | Description |
|---|---|---|
INSTANCE |
yes | Name of the target instance. |
Execute a command inside a sandbox instance and exit with the command's
status code. The -- separator is required: it marks the end of
codebox's own flags, so COMMAND and any flag-shaped arguments after it
(e.g. -la) are forwarded verbatim to the inner command.
codebox exec demo \
--orchestrator=podman --remote=user@host --instance-key=~/.ssh/id_rsa \
-- pytest -x tests/
| Flag | Type | Default | Description |
|---|---|---|---|
--orchestrator |
enum | podman |
Container orchestrator (podman, docker). |
--remote |
string | (local) | Target a remote host (user@host). |
--instance-key |
path | (auto) | SSH key for logging into the instance. |
Positional arguments:
| Argument | Required | Description |
|---|---|---|
INSTANCE |
yes | Name of the target instance. Must appear before --. |
COMMAND |
yes | First token after --; the command to run inside the instance. |
ARGS... |
no | Remaining tokens after --, forwarded to COMMAND unchanged. |
Invocations without --, or with extra positionals before it, are
rejected with a non-zero exit code.
Copy a file or directory from a sandbox instance down to the local machine.
codebox file pull demo \
--orchestrator=podman --remote=user@host --instance-key=~/.ssh/id_rsa \
--instance-path=/workspace/out --local-path=./results
| Flag | Type | Default | Description |
|---|---|---|---|
--orchestrator |
enum | podman |
Container orchestrator (podman, docker). |
--remote |
string | (local) | Target a remote host (user@host). |
--instance-key |
path | (auto) | SSH key for logging into the instance. |
--instance-path |
path | (unset) | File or directory on the instance to copy from. |
--local-path |
path | (unset) | Local directory to copy into. |
Copy a file or directory from the local machine up to a sandbox instance.
codebox file push demo \
--orchestrator=podman --remote=user@host --instance-key=~/.ssh/id_rsa \
--local-path=./payload --instance-path=/workspace/in
| Flag | Type | Default | Description |
|---|---|---|---|
--orchestrator |
enum | podman |
Container orchestrator (podman, docker). |
--remote |
string | (local) | Target a remote host (user@host). |
--instance-key |
path | (auto) | SSH key for logging into the instance. |
--local-path |
path | (unset) | File or directory on the local machine to copy from. |
--instance-path |
path | (unset) | Directory on the instance to copy into. |
Push a ref from the operator's repo into a sandbox instance and check
the resulting branch out at ~/source inside the container. One
repository per sandbox: codebox always uses ~/source so the operator
never has to remember a per-checkout path.
codebox git push demo origin/main:issue-1234 \
--orchestrator=podman --remote=user@host --instance-key=~/.ssh/id_rsa
codebox git push demo main:issue-1234 \
--orchestrator=podman --remote=user@host --instance-key=~/.ssh/id_rsa
| Flag | Type | Default | Description |
|---|---|---|---|
--orchestrator |
enum | podman |
Container orchestrator (podman, docker). |
--remote |
string | (local) | Target a remote host (user@host). |
--instance-key |
path | (auto) | SSH key for logging into the instance. |
Positional arguments:
| Argument | Required | Description |
|---|---|---|
INSTANCE |
yes | Name of the target sandbox instance. |
REFSPEC |
no | One of two shapes (see below); target_branch is the branch name created on the instance and checked out at ~/source. Optional when git.push is set in .codebox.conf. |
REFSPEC is either:
source_remote/source_branch:target_branch— codebox runsgit fetch source_remotelocally first, then pushes the freshly fetchedsource_remote/source_branchonto the instance.local_branch:target_branch— no slash before the:. codebox skips the local fetch and pushes the named local branch directly, so this form works in repos with no remote configured.
When git.push is set in the project's .codebox.conf the source side
may be omitted: pass just target_branch (or :target_branch), or drop
REFSPEC entirely to target a branch named after INSTANCE. The
configured source is filled in — see
Configuration.
Fetch a branch from a sandbox instance into a remote-tracking ref on
the operator's machine (refs/remotes/INSTANCE/BRANCH), then print a
hint showing how to check it out locally. When BRANCH is omitted it
defaults to INSTANCE — the branch workflow/git push check out at
~/source in a sandbox of the same name.
codebox git pull demo issue-1234 \
--orchestrator=podman --remote=user@host --instance-key=~/.ssh/id_rsa
codebox git pull issue-1234 \ # branch defaults to the instance name
--orchestrator=podman --remote=user@host --instance-key=~/.ssh/id_rsa
| Flag | Type | Default | Description |
|---|---|---|---|
--orchestrator |
enum | podman |
Container orchestrator (podman, docker). |
--remote |
string | (local) | Target a remote host (user@host). |
--instance-key |
path | (auto) | SSH key for logging into the instance. |
Positional arguments:
| Argument | Required | Description |
|---|---|---|
INSTANCE |
yes | Name of the source sandbox instance. |
BRANCH |
no | Branch on the instance side to fetch; defaults to INSTANCE. |
sshfs-mount the instance's ~/source directory onto a local directory
on the operator's machine. Lets the operator open the in-container
repository in a local editor without copying it back and forth.
codebox mount demo ./mnt \
--orchestrator=podman --remote=user@host --instance-key=~/.ssh/id_rsa
| Flag | Type | Default | Description |
|---|---|---|---|
--orchestrator |
enum | podman |
Container orchestrator (podman, docker). |
--remote |
string | (local) | Target a remote host (user@host). |
--instance-key |
path | (auto) | SSH key for logging into the instance. |
Positional arguments:
| Argument | Required | Description |
|---|---|---|
INSTANCE |
yes | Name of the target instance. |
LOCAL_DIR |
no | Local directory to mount onto. Defaults to .codebox/INSTANCE/ relative to the current working directory. The directory is created if it does not exist. |
The use-case layer performs, in order:
-
sshfs preflight.
command -v sshfsis run on the operator's machine. When sshfs is not installed the command fails fast withsshfs is not installed on this machine; install it (e.g. \sudo apt install sshfs`) and try again`. -
Mount-point check.
/proc/mountsis read; if the resolvedLOCAL_DIRalready appears as a mount target the command fails withlocal directory "DIR" is already a mount point; unmount it first. Both default and explicitLOCAL_DIRvalues are resolved to absolute paths against the current working directory before the comparison. -
Existence check.
<engine> ps -a --format '{{.Names}}'is run against--remote(locally if unset). When the instance is missing, the command fails withinstance "NAME" not found. -
Host port lookup.
<engine> port NAME 2222is run on the same target; the first<addr>:<port>line is parsed. A stopped container surfacesinstance "NAME" is not exposing port 2222; is it running?. -
Local directory creation.
LOCAL_DIR(default or supplied) is created withos.MkdirAll(mode0755). -
Instance source-dir creation. A container-bound ssh hop runs
mkdir -p ~/sourceso the directory exists before sshfs binds to it. The hop reuses the same transport shape asgit push:-i KEYon the inner hop only,-J Remotewhen the orchestrator is reached via a bastion. -
sshfs mount. The sshfs command is echoed to stdout bracketed by horizontal rules (mirroring the Dockerfile, rsync and git blocks), then run locally so its progress and any permission-denied output stream straight to the operator. The invocation has the shape:
sshfs 'user@localhost:/home/user/source' 'LOCAL_DIR' \ -p PORT \ -o StrictHostKeyChecking=no \ -o fsname='codebox-INSTANCE' \ -o reconnect \ [-o IdentityFile='KEY'] \ [-o ProxyJump='Remote']fsname=codebox-INSTANCEmakes the mount identifiable in/proc/mountssocodebox deletecan find and tear it down.- sshfs failures with
permission deniedin stderr are wrapped assshfs: permission denied: ...so the operator can tell auth failures apart from generic mount errors.
Tear down an sshfs mount established by codebox mount.
codebox umount demo ./mnt \
--orchestrator=podman --remote=user@host --instance-key=~/.ssh/id_rsa
| Flag | Type | Default | Description |
|---|---|---|---|
--orchestrator |
enum | podman |
Container orchestrator (podman, docker). |
--remote |
string | (local) | Target a remote host (user@host). |
--instance-key |
path | (auto) | SSH key (parsed for symmetry with mount; not used by fusermount). |
Positional arguments:
| Argument | Required | Description |
|---|---|---|
INSTANCE |
yes | Name of the target instance. |
LOCAL_DIR |
no | Local directory to unmount. Defaults to .codebox/INSTANCE/ relative to the current working directory — the same default mount uses. |
The use-case layer performs, in order:
- Unmount.
fusermount -u 'LOCAL_DIR'is run locally without a prior/proc/mountscheck — fusermount is the source of truth, so its own stderr surfaces whenLOCAL_DIRis not actually a mount point. Other failures (busy mount, stale handle, …) surface the underlying error so the operator can resolve them and retry. - Remove if empty.
LOCAL_DIRis removed when empty so the default.codebox/INSTANCE/does not linger on disk after the mount is gone. A non-empty directory (operator dropped files inside, or the mount target was a pre-existing populated path) is left in place silently. The success lineRemoved empty DIR.is printed on removal.
codebox delete runs the same fusermount -u step automatically for
every active mount whose source column in /proc/mounts is
codebox-INSTANCE, so the operator does not need to remember to
unmount before tearing the container down — see
delete teardown.
Create an instance, push a branch into it, and open a shell — a
shortcut that chains create, git push, and shell into a single
command. REFSPEC takes the same two shapes as
codebox git push; its
target_branch doubles as the new instance name and as the branch
checked out at ~/source inside the sandbox, so it must satisfy the
same instance-name rules.
codebox workflow origin/main:issue-1234 \
--orchestrator=podman --remote=user@host --instance-key=~/.ssh/id_rsa \
--os=debian_13 --node=24 --claude --tmux
codebox workflow main:issue-1234 # local branch, no upstream fetch
codebox workflow issue-1234 # source from git.push in .codebox.conf
When git.push is set in the project's .codebox.conf, the source side
may be omitted — pass just the target_branch (or :target_branch) and
the configured source is filled in. See
Configuration.
workflow is exactly equivalent to running, in order:
codebox create target_branch [create flags]
codebox git push target_branch REFSPEC
codebox shell target_branch
Argument formats — the refspec and every flag — are validated up
front, so a malformed refspec is rejected before any container is built.
All create-time flags are forwarded verbatim to the underlying create
step; see create for their full
semantics. When --tmux is set, the final shell step launches tmux
in ~/source (with a single installed agent in the right-hand pane).
Flags (in help order):
| Flag | Type | Default | Description |
|---|---|---|---|
--orchestrator |
enum | podman |
Container orchestrator (podman, docker). |
--remote |
string | (local) | Provision on a remote host (user@host). |
--instance-key |
path | (auto) | SSH key for logging into the new instance. |
--rebuild |
bool | false |
Force a rebuild of the base image even if a cached one exists. |
--https-proxy |
string | (unset) | Append export HTTPS_PROXY="<value>" to the in-container user's login profile (see create). |
--os |
enum | debian_13 |
Base OS image (debian_12, debian_13, ubuntu_24, ubuntu_26, redhat_10). |
--python |
enum | (none) | Install Python at 3.12, 3.13, or 3.14. |
--node |
enum | (none) | Install Node.js at major version 24, 25, or 26. |
--golang |
enum | (none) | Install Go at version 1.26.0. |
--dotnet |
enum | (none) | Install .NET at version 8 or 10. |
--claude |
bool | false |
Install Claude Code. |
--claude-credentials |
bool | false |
Copy ~/.claude/.credentials.json into the instance after it starts (ignored unless --claude). |
--codex |
bool | false |
Install OpenAI Codex CLI (and copy ~/.codex/config.toml into the instance after it starts, if present). |
--codex-credentials |
bool | false |
Copy ~/.codex/auth.json into the instance after it starts, if present (ignored unless --codex). |
--opencode |
bool | false |
Install opencode (and copy ~/.config/opencode/opencode.json into the instance after it starts, if present). |
--opencode-credentials |
bool | false |
Copy ~/.local/share/opencode/auth.json into the instance after it starts, if present (ignored unless --opencode). |
--podman |
bool | false |
Install rootless Podman inside the instance. |
--psql |
bool | false |
Install the psql PostgreSQL client. |
--tmux |
bool | false |
Install tmux and label the container tmux=true; the workflow's shell step launches it in ~/source. Accepts --tmux or `--tmux=true |
Like create, the --help output for workflow ends with the same
six footer sections restating the legal values for each kind of flag.
Emit a shell-completion script. The script wires <TAB> after
codebox <cmd> to a runtime lookup of live instance names — see
Shell completion for the full contract.
codebox completion bash > /etc/bash_completion.d/codebox
codebox completion zsh > "${fpath[1]}/_codebox"
codebox completion fish > ~/.config/fish/completions/codebox.fish
codebox completion powershell | Out-String | Invoke-Expression
| Argument | Required | Description |
|---|---|---|
SHELL |
yes | One of bash, zsh, fish, powershell. |
The script is written to stdout. Banner output is suppressed for this
command so its stdout remains an evaluable script — the same rule that
applies to codebox exec and the hidden completion runtime calls.
create is fully wired today: it builds the image and starts the
container. The generated Dockerfile is printed to stdout, bracketed
by a labelled horizontal rule and a matching closing rule, so
operators can audit exactly what they are about to provision; the
engine's build progress, the success line, and the shell hint follow.
The positional INSTANCE argument must match ^[A-Za-z0-9_-]{1,32}$:
- non-empty,
- at most 32 characters,
- characters from
A-Z,a-z,0-9,_,-.
Codebox uses the instance name verbatim as the container name and the
image tag, so the cap stays comfortably inside engine-specific limits
while leaving room for descriptive suffixes (project-feature-sha).
Invalid names fail fast — no orchestrator command is issued.
For each invocation the use-case layer performs, in order:
-
Pre-existence check.
<engine> ps -a --format '{{.Names}}'is run (locally or via ssh). If a container with the requested name already exists, the command fails with a hint:Error: instance "demo" already exists; stop and delete it first: codebox delete demoThe
--orchestratorand--remoteflags are echoed back in the hint when they differ from the defaults. -
Image build. A Dockerfile is generated in memory and piped to
<engine> build -t INSTANCE -f -whose context is a freshmktemp -ddirectory (trapped for cleanup on exit). The empty context guarantees no files from the operator's working tree leak into the image.--rebuildadds--no-cache. Build output is streamed to the operator's terminal as it is produced. -
Container start.
<engine> run -d --name INSTANCE --hostname INSTANCE --label codebox=true --publish-all INSTANCE. The hostname is set so an interactive shell inside the container makes the sandbox immediately identifiable. When--podmanis set, the flags--device /dev/fuse --device /dev/net/tun --cap-add=sys_admin --cap-add=net_admin --cap-add=mknod --security-opt label=disable --security-opt unmask=ALLare added before--publish-allso the in-container rootless Podman has the devices, capabilities, and confinement relaxations it needs to run nested containers. Failures surface the engine's stderr verbatim. -
Podman migrate (only with
--podman). Once the container is confirmed running,<engine> exec --user user --env HOME=/home/user INSTANCE podman system migrateruns inside the instance to rebuild the rootless user-namespace mappings from the baked-in/etc/subuidand/etc/subgidranges. Without it the first in-sandboxpodmaninvocation fails with a UID/GID range mismatch. It runs via the engine's ownexecso it does not wait on the in-container sshd. -
Success line. A copy-paste-ready
codebox shellcommand is printed on the line after a one-line success message, indented by two spaces.--orchestrator,--remote, and--instance-keyare included only when the operator supplied a non-default value:Instance "demo" is ready. Open a shell: codebox shell demo --remote=user@host --instance-key=~/.ssh/id_rsa
When --remote=user@host is set, each step's shell command is sent via
ssh user@host '<command>'. The operator's normal SSH configuration
(~/.ssh/config, ssh-agent, default keys) is used; the
--instance-key value is not passed to ssh — it is only embedded
into the container's authorized_keys. SSH connection failures (exit
status 255) surface as a distinct error message naming the host, so
the operator can tell them apart from build or run failures.
--os |
FROM reference |
|---|---|
debian_12 |
docker.io/debian:12.13 |
debian_13 |
docker.io/debian:13.4 |
ubuntu_24 |
docker.io/ubuntu:24.04 |
ubuntu_26 |
docker.io/ubuntu:26.04 |
redhat_10 |
docker.io/redhat/ubi10:10.1 |
The Dockerfile is built in this order so that an unrelated change to a later layer does not invalidate the package install cache:
- Install base packages —
ca-certificates,nano,vim,sudo,openssl,openssh-server,rsync,git,iputils-ping/iputils,dnsutils/bind-utils,curl,net-tools. Names are remapped per distro family. The distro's build toolchain (build-essentialon apt,"Development Tools"group on dnf) is installed in the same layer. - OS-specific fixes (
debian_13,ubuntu_26,redhat_10only): overwrite/etc/pam.d/sudowith the minimal container-friendly stack. - Provision the
useraccount, then mark it unlocked withusermod -p '*NP' userso pubkey login succeeds. On Debian and Red Hat a fresh account is created withuseradd; on Ubuntu the base image's existingubuntuaccount (UID 1000) is instead renamed touser— login, primary group, and home directory — souserlands at UID 1000 on every distro. - Configure sshd: create
/run/sshd, relaxpam_loginuidtooptional, and drop a10-codebox.confinto/etc/ssh/sshd_config.dwithPort 2222,PubkeyAuthentication yes,PasswordAuthentication no,UsePAM no. - Configure sudoers: passwordless sudo for
uservia/etc/sudoers.d/user(mode 0440). - Init script
/usr/local/bin/codebox-initthat execssshdandsleep infinity. - Optional language/tool layers (see Optional toolchains). Skipped entirely when no flag is set.
- Install the operator's public key into
/home/user/.ssh/authorized_keys(mode 0600, owned byuser). EXPOSE 2222,CMD ["/usr/local/bin/codebox-init"].
Each flag emits its own layer in the slot between the init script and
the public-key install. Layers that touch system paths run as root;
home-scoped installs (uv, nvm) switch to USER user and then
back to USER root so the subsequent key install retains its
permissions.
Version values are validated against the documented sets before any
Dockerfile is emitted; an out-of-set value (e.g. --python=3.10) fails
with image: unsupported <flag> version "<value>" (known: ...) and no
orchestrator command is issued.
PATH and toolchain exports are appended to the per-family login profile
so interactive shells pick them up: /home/user/.profile on apt-family
distros (Debian, Ubuntu) and /home/user/.bash_profile on dnf-family
distros (Red Hat). Below, PROFILE refers to whichever file applies.
| Flag | Installs |
|---|---|
--psql |
postgresql-client (apt) or postgresql (dnf) via the distro package manager. |
--tmux |
tmux via the distro package manager (same name on apt and dnf). The container is additionally labelled tmux=true; codebox shell reads that label and launches tmux (horizontal split, both panes in ~/source) on connect. Installed AI agents are recorded as their own boolean labels (e.g. claude=true, codex=true, opencode=true); when exactly one is present codebox shell runs that agent in the right-hand pane via a login shell. With none or several installed it cannot choose, so both panes stay plain shells. |
--golang=VER |
Downloads https://go.dev/dl/goVER.linux-${arch}.tar.gz (arch detected from uname -m; amd64 and arm64 supported), unpacks it to /usr/local/go, and appends export PATH="/usr/local/go/bin:$PATH" to PROFILE. |
--dotnet=VER |
Runs https://dot.net/v1/dotnet-install.sh --channel VER.0 --install-dir /usr/local/dotnet, symlinks the runner to /usr/local/bin/dotnet, and appends DOTNET_ROOT, PATH, and DOTNET_CLI_TELEMETRY_OPTOUT=1 exports to PROFILE. |
--python=VER |
Runs https://astral.sh/uv/install.sh as user user, then runs uv python install VER && uv python pin --global VER to download the prebuilt CPython and set it as the global default for uv. (The export PATH="$HOME/.local/bin:$PATH" line is emitted once — see --claude.) |
--node=VER |
On dnf-family distros, first installs libatomic (UBI omits it and recent V8 binaries link against it). Then installs nvm (pinned to v0.40.1) as user user, and runs nvm install VER && nvm alias default VER. |
--claude |
Runs Anthropic's native installer (curl -fsSL https://claude.ai/install.sh | bash) as user user. The installer drops the claude binary into $HOME/.local/bin; the corresponding PATH export is emitted once when --claude, --codex, or --python is set. When --https-proxy is also set, the proxy is exported inline for this RUN (export HTTPS_PROXY='URL' && curl ...) so the install pipeline routes through it. The same layer also drops /home/user/.claude.json (owned by user) with {"hasCompletedOnboarding": true, "defaultMode": "bypassPermissions"} so the CLI skips the first-run prompt inside the sandbox. Credentials are not baked in — pass --claude-credentials to push them after the container starts. |
--codex |
Runs the OpenAI Codex CLI's native installer (curl -fsSL https://chatgpt.com/codex/install.sh | bash, which follows the redirect to the latest GitHub release) as user user. The installer drops the codex binary into $HOME/.local/bin, so it shares the single PATH export emitted for that directory (see --claude) rather than adding its own. When --https-proxy is also set, the proxy is exported inline for this RUN (export HTTPS_PROXY='URL' && curl ...) so the install download routes through it. The operator's ~/.codex/config.toml (and, with --codex-credentials, ~/.codex/auth.json) is not baked in — it is pushed after the container starts when present (see Codex config & credentials transfer). |
--opencode |
Runs opencode's native installer (curl -fsSL https://opencode.ai/install | bash) as user user. The installer drops the opencode binary into $HOME/.opencode/bin; because it only edits interactive rc files (which login shells skip), the layer appends export PATH="$HOME/.opencode/bin:$PATH" to PROFILE explicitly. When --https-proxy is also set, the proxy is exported inline for this RUN (export HTTPS_PROXY='URL' && curl ...). The operator's opencode.json (and, with --opencode-credentials, ~/.local/share/opencode/auth.json) is not baked in — it is pushed after the container starts when present (see opencode config & credentials transfer). |
--podman |
Installs podman, podman-compose, and the rootless stack (passt, uidmap — shadow-utils on dnf-family — fuse-overlayfs, nftables, aardvark-dns) as root. On Red Hat podman-compose is installed from PyPI via pip3 (it is not packaged for dnf). The rootless network backend is pasta (from passt), which is Podman's default, so no containers.conf network key is written. It replaces /etc/subuid / /etc/subgid with root:1:65535, user:1:999, user:1001:64535 (uniform across distros — user is UID 1000 everywhere, since Ubuntu's ubuntu account is renamed rather than adding a new user), and drops two files under /home/user/.config/containers/ (owned by user): containers.conf with [containers] default_sysctls = [], and registries.conf with [registries.search] registries = ['docker.io'] so unqualified podman pull names resolve against Docker Hub. The layer is emitted before any agent install so an agent can drive containers on its first task. The container is started with --device /dev/fuse --device /dev/net/tun --cap-add=sys_admin --cap-add=net_admin --cap-add=mknod --security-opt label=disable --security-opt unmask=ALL and, once running, podman system migrate is run inside it (see the create steps) so the nested rootless Podman has what it needs. |
When --https-proxy=URL is set, codebox appends the line
export HTTPS_PROXY="URL"
to the in-container user's login profile — PROFILE as defined
above (/home/user/.profile on apt-family distros,
/home/user/.bash_profile on dnf-family distros). The proxy is not
emitted as an ENV directive: most image build downloads continue to
use the builder host's normal network, and the proxy only takes effect
once a login shell sources the profile (interactive codebox shell
sessions, ssh login sessions targeting the container). The one build-time
exception is the agent install layers (--claude, --codex,
--opencode), which export HTTPS_PROXY inline for their own curl
RUN so the install download routes through the proxy.
Single quotes in the value are shell-escaped before being written so
the surrounding echo invocation survives an embedded apostrophe;
otherwise the value is passed through verbatim
(http://proxy:3128, http://user:pw@proxy:3128, ...).
--claude-credentials is ignored unless --claude is also set:
requesting credentials without the install does nothing (no error, no
push). When both flags are set, the flag is honoured after the
container's run step succeeds. The use-case layer:
stats~/.claude/.credentials.jsonon the operator's machine before any orchestrator command is issued. A missing or unreadable file fails the command with the underlying OS error (the flag name is included in the wrapper), so the operator does not have to wait for an image build to surface the problem.- Looks up the host-side port for the in-container sshd via
<engine> port INSTANCE 2222, exactly likefile push/file pull. - Echoes the assembled rsync command bracketed by horizontal rules
(mirroring the Dockerfile and
file push/file pullblocks) and runs it locally so progress streams to the operator's terminal. - If the first rsync fails (most often because sshd inside the container is still coming up), waits 2 seconds and retries exactly once. The retry message names the original error so the operator can see what is being recovered from. If the retry also fails, that error is returned.
The rsync invocation has the shape:
rsync --verbose --archive --compress --update --progress \
--mkpath --chmod=F0600 \
-e 'ssh -o StrictHostKeyChecking=no [-i KEY] [-J Remote] -p PORT' \
~/.claude/.credentials.json user@localhost:/home/user/.claude/.credentials.json
--mkpathensures/home/user/.claudeis created on the receiving side even when neither--claudenor a previous run laid it down.--chmod=F0600pins the file mode so the credentials land with the same permissions Claude expects on the host.
--codex copies the operator's ~/.codex/config.toml into the instance
after the container's run step succeeds. This is best-effort: the
config is optional, so a missing source file is silently skipped rather
than failing the create. When the file is present, the use-case layer
looks up the host-side sshd port, echoes the assembled rsync command
bracketed by horizontal rules, runs it locally, and retries exactly once
after a short wait if the in-container sshd is not yet up — the same
single-file push used for Claude credentials. The invocation has the
shape:
rsync --verbose --archive --compress --update --progress \
--mkpath --chmod=F0600 \
-e 'ssh -o StrictHostKeyChecking=no [-i KEY] [-J Remote] -p PORT' \
~/.codex/config.toml user@localhost:/home/user/.codex/config.toml
--mkpath ensures /home/user/.codex is created on the receiving side
when the install layer has not already laid it down.
--codex-credentials (ignored unless --codex is also set) adds a
second, identical push of ~/.codex/auth.json into
/home/user/.codex/auth.json. Credentials are opt-in — copied only when
the flag is set — but the push is still best-effort: a missing
auth.json is silently skipped. They are never baked into the image.
--opencode copies the operator's ~/.config/opencode/opencode.json
into the instance after the container's run step succeeds. This is
best-effort: the config is optional, so a missing source file is
silently skipped rather than failing the create. When the file is
present, the use-case layer looks up the host-side sshd port, echoes the
assembled rsync command bracketed by horizontal rules, runs it locally,
and retries exactly once after a short wait if the in-container sshd is
not yet up — the same single-file push used for Claude credentials. The
invocation has the shape:
rsync --verbose --archive --compress --update --progress \
--mkpath --chmod=F0600 \
-e 'ssh -o StrictHostKeyChecking=no [-i KEY] [-J Remote] -p PORT' \
~/.config/opencode/opencode.json user@localhost:/home/user/.config/opencode/opencode.json
--mkpath ensures /home/user/.config/opencode is created on the
receiving side when the install layer has not already laid it down.
--opencode-credentials (ignored unless --opencode is also set) adds a
second, identical push of ~/.local/share/opencode/auth.json into
/home/user/.local/share/opencode/auth.json. Credentials are opt-in —
copied only when the flag is set — but the push is still best-effort: a
missing auth.json is silently skipped. They are never baked into
the image.
| Input | Behaviour |
|---|---|
Path ending in .pub |
Read directly. |
Path not ending in .pub |
.pub is appended, then read. |
Leading ~/ or bare ~ |
Expanded against the operator's home directory. |
| Omitted | Scan ~/.ssh/ for *.pub. Exactly one match is required; zero or multiple matches return an error naming the candidates and asking the operator to pass --instance-key. |
delete is fully wired today. The use-case layer performs, in order:
- Existence check.
<engine> ps -a --format '{{.Names}}'is run (locally or via ssh). If the instance is not present, the command fails withinstance "NAME" not foundand exits non-zero. - Auto-unmount.
/proc/mountsis read locally; every entry whose source column iscodebox-NAMEis unmounted withfusermount -u 'TARGET'. Each unmount printsUnmounting TARGET...so the operator can see what is happening. Each unmounted target is then removed if empty (same rule ascodebox umount: non-empty targets are left in place silently). A failure to read/proc/mounts(non-Linux, restricted) silently yields no mounts; a failure to unmount a found entry stopsdeleteso the operator can resolve it (e.g. close open files) and re-run. - Running check.
<engine> ps --format '{{.Names}}'lists only running containers. - Stop (conditional). If the container is running, codebox prints
Stopping container "NAME"...and runs<engine> stop NAME. A failure surfaces the engine's stderr; the remove and untag steps are skipped. - Remove. Codebox prints
Deleting container "NAME"...and runs<engine> rm NAME. - Untag.
podman untag NAME(ordocker rmi NAME, since docker has nountagverb) is run silently to drop every tag on the image codebox built for the instance. - Local git remote cleanup.
git remote get-url codebox-NAMEis run in the operator's current directory; if it succeeds (i.e. the matching instance remote is still wired up from an earliercodebox git pushorgit pull), codebox printsRemoving local git remote "codebox-NAME"...and runsgit remote remove codebox-NAME. A non-git directory, or a missing remote, is treated as a silent no-op —--remotenever changes this: the cleanup is always local.
Engine stdout (which otherwise echoes the container/image name) is captured to internal buffers throughout — only the human-readable progress lines codebox prints reach the operator's terminal.
list is fully wired today. The use-case layer runs
<engine> ps -a --filter label=codebox=true \
--format '{{.Names}}|{{.CreatedAt}}|{{.Ports}}'
against the chosen target (locally or via ssh on --remote) and
prints a three-column table to stdout:
| Column | Source |
|---|---|
INSTANCE |
{{.Names}} |
AGE |
time.Now() − {{.CreatedAt}}, rendered in the largest non-zero unit (<1 min, N min, N hr, N day). Unparseable timestamps render as ?. |
SSH COMMAND |
Copy-paste shell hint targeting the container's sshd. |
The SSH COMMAND column hard-codes the in-container login (user)
and sshd port (2222). Stopped containers have no published port and
surface (stopped) in place of an unusable hint. Otherwise:
- Local:
ssh -o StrictHostKeyChecking=no user@localhost -p <host_port> - Remote (
--remote=ops@bastion):ssh -o StrictHostKeyChecking=no -J ops@bastion user@localhost -p <host_port>
When no codebox containers are present, the single line
No codebox instances found. is printed and the command exits 0.
shell is fully wired today. The use-case layer performs, in order:
-
Existence check.
<engine> ps -a --format '{{.Names}}'is run against--remote(locally if unset). When the instance is missing, the command fails withinstance "NAME" not foundand exits non-zero before any further work. -
Host port lookup.
<engine> port NAME 2222is run on the same target; the first<addr>:<port>line is parsed and the numeric port retained. A stopped container produces no mapping and surfacesinstance "NAME" is not exposing port 2222; is it running?. -
Interactive ssh. A locally-exec'd
sshconnects to the container's published port with stdin/stdout/stderr passed through unchanged, so the operator gets a real tty. The command shape is:- Local (no
--remote):ssh -o StrictHostKeyChecking=no [-i KEY] [-L L:localhost:R ...] user@localhost -p PORT - Remote (
--remote=ops@bastion):ssh -o StrictHostKeyChecking=no [-i KEY] [-L L:localhost:R ...] -J ops@bastion user@localhost -p PORT
--instance-keyis~-expanded and passed as-i; it is never passed to the orchestrator-bound ssh that ran step 1 and 2. Each--port=L:Rbecomes-L L:localhost:Rso the remote end is interpreted on the container side of any-Jjump. - Local (no
Connection-level ssh failures (exit status 255) bubble up as
ssh: could not connect to <host> so the operator can distinguish
them from a non-zero exit from the in-container shell.
port-forward is fully wired today. Port resolution happens in the CLI
layer before the use-case runs:
- Resolve forwards. The project
.codebox.confis read. When it carries aport-forward:list, each entry is normalised toLOCAL:REMOTE(a barePORTbecomesPORT:PORT), validated as a port number, and de-duplicated. Otherwise, if a compose file is present in the current directory, its published ports are detected and mapped to themselves, ordered by service name then listing order. An empty result fails withno ports to forward: ...before any orchestrator call.
The use-case layer then mirrors shell's preflight:
-
Existence check.
<engine> ps -a --format '{{.Names}}'against--remote(locally if unset); a missing instance fails withinstance "NAME" not found. -
Host port lookup.
<engine> port NAME 2222; a stopped container surfacesinstance "NAME" is not exposing port 2222; is it running?. -
Forwarding ssh. A locally-exec'd
ssh -N(no remote command, so no usable shell) opens the-Lforwards and blocks. The command shape is:- Local (no
--remote):ssh -N -o StrictHostKeyChecking=no -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 [-i KEY] [-L L:localhost:R ...] user@localhost -p PORT - Remote (
--remote=ops@bastion):ssh -N ... [-L L:localhost:R ...] -J ops@bastion user@localhost -p PORT
As with
shell,--instance-keyis~-expanded and passed as-ionly to this hop, and each forward targetslocalhost:Rso the remote end resolves on the container side of any-Jjump.ExitOnForwardFailure=yesmakes ssh fail loudly when a local port is already taken rather than connecting with a silently-dropped forward;ServerAliveInterval=30keeps the otherwise-idle session from being dropped. - Local (no
The mapped ports are printed before the connection blocks (since
ssh -N is itself silent). Ctrl-C/SIGTERM cancels the context,
which kills ssh; that is treated as a clean shutdown (the command prints
Stopped port forwarding. and exits successfully) rather than a
connection error.
exec is fully wired today. The use-case layer performs, in order:
-
Existence check.
<engine> ps -a --format '{{.Names}}'is run against--remote(locally if unset). When the instance is missing, the command fails withinstance "NAME" not foundand exits non-zero before any further work. -
Host port lookup.
<engine> port NAME 2222is run on the same target; the first<addr>:<port>line is parsed and the numeric port retained. A stopped container produces no mapping and surfacesinstance "NAME" is not exposing port 2222; is it running?. -
Remote command. A locally-exec'd
sshconnects to the container's published port with stdin/stdout/stderr passed through unchanged, so callers can pipe data in or out. The command shape is:- Local (no
--remote):ssh -o StrictHostKeyChecking=no [-i KEY] user@localhost -p PORT '<inner>' - Remote (
--remote=ops@bastion):ssh -o StrictHostKeyChecking=no [-i KEY] -J ops@bastion user@localhost -p PORT '<inner>'
--instance-keyis~-expanded and passed as-ion the container-bound ssh only; it is never passed to the orchestrator-bound ssh that ran steps 1 and 2.<inner>isCOMMANDfollowed by eachARG, single-quoted individually so the in-container login shell preserves argument boundaries (spaces and shell metacharacters survive verbatim). - Local (no
Codebox exits with the inner command's status code; connection-level ssh failures (exit status 255) surface as a distinct error naming the host so they can be told apart from a non-zero exit from the inner command.
file push and file pull share an implementation: each builds an rsync
command tunnelled over ssh and runs it locally so rsync's progress
stream reaches the operator's terminal directly. The use-case layer
performs, in order:
-
Existence check.
<engine> ps -a --format '{{.Names}}'is run against--remote(locally if unset). When the instance is missing, the command fails withinstance "NAME" not foundand exits non-zero before any further work. -
Host port lookup.
<engine> port NAME 2222is run on the same target; the first<addr>:<port>line is parsed and the numeric port retained. A stopped container produces no mapping and surfacesinstance "NAME" is not exposing port 2222; is it running?. -
Rsync command echo. The rsync invocation is printed to stdout bracketed by horizontal rules, mirroring the Dockerfile block
createemits during provisioning, so the operator can audit the exact command before it runs. -
Rsync execution. The command is executed locally (never via the orchestrator-bound ssh) so rsync's
--progressoutput streams straight through to the operator's terminal. The shape is:rsync --verbose --archive --compress --update --progress \ -e 'ssh -o StrictHostKeyChecking=no [-i KEY] [-J Remote] -p PORT' \ SRC DST--instance-keyis~-expanded and passed as-ion the inner ssh only — the orchestrator-bound ssh that ran steps 1 and 2 used the operator's normal ssh configuration.--remote=user@hostbecomes-J user@hostso the operator's bastion is interpreted on the local side and rsync connects to the container's published port through the jump.SRCandDSTare oriented per command:file pushsendsLOCAL → user@localhost:INSTANCE_PATH,file pullsendsuser@localhost:INSTANCE_PATH → LOCAL. The local path is~-expanded before being passed to rsync.
Both --local-path and --instance-path are required; omitting
either fails fast with a flag-name error before any orchestrator
command is issued.
git push and git pull share the orchestrator-level preflight
(existence + host port lookup) and the local-side remote bookkeeping.
See git.md for the user-facing walkthrough.
For each invocation the use-case layer performs, in order:
- Local git pre-check (CLI layer). Before any orchestrator work,
the CLI confirms the operator's current working directory is the
root of a git repository — that is, it contains a
.git/directory. Subdirectories of a repo, and directories that are not a repo at all, fail fast withnot a git repository: no .git directory in <cwd>. The same check applies to bothgit pushandgit pullso the operator never reaches the orchestrator with a half-set-up local repo. - Existence check.
<engine> ps -a --format '{{.Names}}'is run against--remote(locally if unset). When the instance is missing, the command fails withinstance "NAME" not foundand exits non-zero before any further work. - Host port lookup.
<engine> port NAME 2222is run on the same target; the first<addr>:<port>line is parsed and the numeric port retained. A stopped container produces no mapping and surfacesinstance "NAME" is not exposing port 2222; is it running?. - Local remote refresh. A git remote named
codebox-INSTANCEis (re)pointed atssh://user@localhost:PORT/home/user/sourceso a restarted container with a newly-assigned host port does not strand the operator with a stale URL. Thecodebox-prefix keeps codebox's auto-managed remotes from colliding with anything the operator configured by hand (e.g.origin). The path component is always/home/user/source— codebox uses one in-container directory per sandbox. The shell idiomgit remote set-url codebox-INSTANCE URL 2>/dev/null || git remote add codebox-INSTANCE URLdoes both cases in one runner call.
After the shared preflight, git push additionally:
-
Parse the refspec. The argument is split on the first
:into source and target. The source is then classified by whether it contains a/:- With a slash —
source_remote/source_branch. The first slash separates the remote from the branch, so a source branch likefeature/xis fine (origin/feature/x:work). - Without a slash —
local_branch. The local fetch step below is skipped and the named local branch is pushed directly.
- With a slash —
-
Read operator identity.
git config --get user.nameandgit config --get user.emailare run locally. Unset values become empty strings — the init step below simply skips them. -
Initialise the instance source dir. An ssh hop into the instance runs an idempotent script:
if [ ! -d ~/source/.git ]; then mkdir -p ~/source && git init -q ~/source && cd ~/source && git config receive.denyCurrentBranch updateInstead [&& git config user.name 'NAME'] [&& git config user.email 'EMAIL'] fi
The
updateInsteadsetting lets subsequent pushes update the instance's working tree atomically when it is clean (and refuse when it is dirty). Operator identity is written only at init time; it is not refreshed on later pushes. -
Local fetch (remote form only).
git fetch source_remoteis run locally so the remote-tracking refsource_remote/source_branchreflects the upstream tip before it is pushed onward. Skipped when the refspec named a bare local branch — there is no remote to fetch from. -
Push.
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no [-i KEY] [-J Remote]' git push codebox-INSTANCE SOURCE:refs/heads/target_branchis run locally, whereSOURCEis eithersource_remote/source_branchor the barelocal_branch. The remote URL stored in.git/configdoes not encode-i/-J; those options live onGIT_SSH_COMMANDso they apply only when codebox invokes git. -
Checkout. A second ssh hop runs
cd /home/user/source && git checkout target_branchso the instance has the freshly pushed branch checked out at~/source. The branch was just created by step 5, so a plaingit checkoutis enough — no-b(which would refuse with "branch already exists") and no-B(which would clobber a manually advanced branch on the instance side). -
Success line. A one-line message names the branch and
~/source.
After the shared preflight, git pull runs
GIT_SSH_COMMAND=... git fetch codebox-INSTANCE BRANCH locally,
populating refs/remotes/codebox-INSTANCE/BRANCH in the operator's
repository. A two-line hint then prints the exact local checkout
command:
Fetched "BRANCH" from instance "INSTANCE".
To check it out locally:
git checkout codebox-INSTANCE/BRANCH -b BRANCH
- The orchestrator-bound ssh used for steps 1 and 2 honours the
operator's normal ssh configuration;
--instance-keyis never passed to it. - The container-bound ssh (init / checkout / push / fetch transport)
threads
-i KEY(when--instance-keyis set) and-J Remote(when--remoteis set). - The
git push/git fetchinvocations are echoed to stdout bracketed by horizontal rules, mirroring the Dockerfile and rsync blocks emitted bycreateandfile push/file pull.
codebox completion <bash|zsh|fish|powershell> emits a shell-specific
completion script on stdout. Source it (per the snippets in the
completion command reference) and the
shell will offer tab-completion for subcommands, flag names, and
INSTANCE positional arguments.
Subcommands whose first positional is INSTANCE — delete, shell,
port-forward, exec, file pull, file push, git push,
git pull, mount, umount — surface live instance names to the shell. At each tab press the completion path runs a
single orchestrator query:
<engine> ps -a --filter label=codebox=true --format '{{.Names}}|{{.CreatedAt}}|{{.Ports}}'
and returns the Names column. The query honours partially-typed
flags from the command line:
--orchestrator=<podman|docker>picks the engine (defaultpodman).--remote=user@hostroutes the query over ssh to the orchestrator host, so completion on a remote sandbox host returns the names that actually live there.--instance-key=PATHis parsed off the partial command line but is not used by the lookup: the listing path always uses the operator's normal ssh configuration to reach the orchestrator host, never the per-instance key.
create's INSTANCE argument and workflow's REFSPEC both name a
new instance, so no instance-name completion is offered for them.
When the lookup fails (engine missing, ssh unreachable, no instances)
the completion function returns no candidates and the directive
ShellCompDirectiveNoFileComp — the shell offers nothing rather than
falling back to filename completion, and the operator can still type
the instance name manually. No error message is surfaced to the
shell, because completion runs inside an interactive read-line loop
where a stderr blob would corrupt the prompt.
The banner is suppressed for codebox completion, the hidden
__complete / __completeNoDesc runtime helpers, and codebox exec
— each of those streams is consumed by another program (the shell or
a downstream pipe). When --help / -h is present anywhere in the
arguments the suppression is cancelled, so every help path
(codebox, codebox help, codebox --help,
codebox <cmd> --help, codebox help <cmd>) renders the same banner
- help body.
create, delete, list, shell, port-forward, exec,
file push / file pull, git push / git pull, mount / umount,
workflow, and completion are all implemented end-to-end. The
behaviours described above are the
specification the implementation is held against — if the two
disagree, this file is canonical and the code should be updated.