CodeyBox:MultipassExtraRuncmd is operator-supplied shell that bakes tools
into the sandbox image before work starts. Install the tools your projects
need; CodeyBox does not assume one privileged language stack.
The examples below use Multipass's historical top-level keys. Incus exposes the same explicit bake inputs under its own provider section:
| Bake input | Multipass key | Incus key |
|---|---|---|
| First-boot commands | CodeyBox:MultipassExtraRuncmd |
CodeyBox:Incus:ExtraRuncmd |
| Package-cache files or directories | CodeyBox:MultipassPackageCacheSeeds |
CodeyBox:Incus:PackageCacheSeeds |
| Host-staged executables | CodeyBox:MultipassExecutableProvisions |
CodeyBox:Incus:ExecutableProvisions |
The providers never inherit or fall back to each other's bake configuration. When switching backends, copy the intended entries deliberately and review guest paths and size limits. Both baseline paths derive their final credential-free CLI verification commands from configured agents and the shared in-VM probe catalog; a missing required CLI fails the bake before the baseline becomes cloneable. Incus applies its explicit cache/executable inputs to both a baked baseline and the full-launch path used when baseline images are disabled, so the latter does not silently lose required tools.
Ordinary unit tests do not start Incus VMs. The destructive real-host
requires_incus scenario remains opt-in and skips unless
CODEYBOX_RUN_INCUS_INTEGRATION=1 is set explicitly; configuration examples in
this document do not enable it.
If a language auditor is enabled but its tool is missing, the auditor usually emits an Info finding and skips instead of blocking the work item. The built-in security tool auditors emit Warning on missing tools so lost secret/SAST coverage is visible without hard-blocking audits by default. Install the missing tool and re-run audit to get enforcement.
An Incus seed may be one bounded file or directory. VmDestPath is a
normalized absolute non-root canonical guest directory: directory contents land beneath
it, while a file lands at VmDestPath/<source basename>. The provider stages
the source privately, assigns the installed tree to the configured guest
UID/GID, and applies the same input to a baseline bake or a full launch:
{
"CodeyBox": {
"Incus": {
"PackageCacheSeeds": [
{
"HostSourcePath": "/srv/codeybox/package-cache/nuget",
"VmDestPath": "/var/cache/codeybox/nuget",
"MaxSizeMB": 2048
}
],
"MaxPackageCacheSeedBytes": 4294967296,
"MaxAggregatePackageCacheSeedBytes": 8589934592,
"MaxPackageCacheSeedEntries": 100000
}
}
}MaxSizeMB is measured in MiB (1,048,576 bytes) and narrows the provider-wide
per-seed byte bound; it cannot enlarge it. Guest filesystem aliases and paths
under /dev, /proc, /run, or /sys are rejected. Missing files, traversal
beyond the entry limit, and byte-limit breaches
fail provisioning instead of producing a partially seeded VM.
Seeds destined for the guest NuGet home ({GuestHome}/.nuget/..., for example
/home/ubuntu/.nuget/packages) are served as a read-only shared fallback
folder instead of being copied into the per-sandbox writable package root, so a
multi-gigabyte package cache is transferred once per host rather than once per
sandbox. Full-launch VMs mount the host directory read-only and advertise it
through NUGET_FALLBACK_PACKAGES; baselines bake the content into the image so
clones inherit it, and seeds that cannot be shared (file sources, sources
outside the allowed host mount roots) keep the copy path — served from a
once-per-host archive cache under the staging directory. Restore writes only
packages the cache does not carry into the writable root, and the required
build gate reports the shared-versus-fetched split after a passing build.
Tune the folder with CodeyBox:Incus:NuGetFallbackGuestPath, or set
CodeyBox:Incus:ShareNuGetPackageSeedsAsFallback to false to keep the legacy
per-VM copy for every seed.
{
"CodeyBox": {
"MultipassExtraRuncmd": [
"apt-get update",
"apt-get install -y curl ca-certificates git python3 python3-pip python3-venv nodejs npm golang rustup",
"curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh",
"bash /tmp/dotnet-install.sh --version 10.0.301 --install-dir /usr/share/dotnet",
"ln -sf /usr/share/dotnet/dotnet /usr/local/bin/dotnet",
"dotnet --version | grep -Fx 10.0.301",
"python3 -m pip install --break-system-packages ruff mypy pytest pip-audit",
"npm install -g prettier eslint",
"go install golang.org/x/vuln/cmd/govulncheck@latest",
"cargo install cargo-audit",
"rustup component add rustfmt clippy"
]
}
}{
"CodeyBox": {
"MultipassExtraRuncmd": [
"apt-get update",
"apt-get install -y curl ca-certificates",
"curl -fsSL https://dot.net/v1/dotnet-install.sh -o /tmp/dotnet-install.sh",
"bash /tmp/dotnet-install.sh --version 10.0.301 --install-dir /usr/share/dotnet",
"ln -sf /usr/share/dotnet/dotnet /usr/local/bin/dotnet",
"dotnet --version | grep -Fx 10.0.301"
]
}
}Do not bake C# baselines from a floating .NET SDK channel such as
dotnet-install.sh --channel 10.0 or an unpinned dotnet-sdk-10.0 package.
dotnet format behavior can change across SDK feature bands, and audit-tool
sandboxes must use the same formatter version as the mechanical
dotnet-format fixer. Pin an exact SDK version in MultipassExtraRuncmd,
verify it with dotnet --version, and deliberately bump the version in both
the baseline config and any developer/CI images when you want formatter rules
to change.
{
"CodeyBox": {
"MultipassExtraRuncmd": [
"apt-get update",
"apt-get install -y python3 python3-pip python3-venv",
"python3 -m pip install --break-system-packages ruff mypy pytest pip-audit"
]
}
}{
"CodeyBox": {
"MultipassExtraRuncmd": [
"apt-get update",
"apt-get install -y nodejs npm",
"npm install -g prettier eslint"
]
}
}{
"CodeyBox": {
"MultipassExtraRuncmd": [
"apt-get update",
"apt-get install -y golang",
"go install golang.org/x/vuln/cmd/govulncheck@latest"
]
}
}{
"CodeyBox": {
"MultipassExtraRuncmd": [
"apt-get update",
"apt-get install -y rustup",
"rustup default stable",
"rustup component add rustfmt clippy",
"cargo install cargo-audit"
]
}
}Every AgentKind registered in an agent class needs its CLI baked into the
sandbox image. The orchestrator does not install agent binaries; missing
binaries fail the baseline bake during the post-install in-VM verification
(--version) checks for configured agents. Bake them at baseline time so the
first dispatch can actually run.
{
"CodeyBox": {
"MultipassExtraRuncmd": [
"apt-get update",
"apt-get install -y curl ca-certificates nodejs npm python3",
"npm install -g @anthropic-ai/claude-code @openai/codex @google/gemini-cli",
"npm install -g @qwen-code/qwen-code@0.24.0",
"npm install -g --ignore-scripts autohand-cli@0.9.7",
"npm install -g cline@3.0.62",
"npm install -g @kilocode/cli@7.7.2",
"npm install -g command-code@1.54.2",
"npm install -g @charmland/crush@0.95.0",
"curl -fsSL https://omp.sh/install | sh -s -- --binary --ref v18.2.2",
"npm install -g @continuedev/cli@1.5.47",
"mkdir -p /home/ubuntu/.continue && touch /home/ubuntu/.continue/.onboarding_complete && chown -R ubuntu:ubuntu /home/ubuntu/.continue",
"curl -fsSL https://cursor.com/install | bash",
"curl -fsSL https://aider.chat/install.sh | bash",
"UV_TOOL_BIN_DIR=/usr/local/bin uv tool install --python python3.12 aider-chat==0.86.2",
"curl -fsSL https://app.primeintellect.ai/prime-agent/install.sh | sh"
],
"MultipassExecutableProvisions": [
{
"HostSourcePath": "/home/<operator>/.codeybox/agy-seed/agy",
"VmDestPath": "/home/ubuntu/.local/bin/agy",
"VmSymlinks": ["/usr/local/bin/agy"],
"Label": "antigravity"
}
]
}
}The equivalent Incus executable provisioning is provider-local:
{
"CodeyBox": {
"SandboxProvider": "incus",
"Incus": {
"UseBaselineImages": true,
"ExtraRuncmd": [
"apt-get update",
"apt-get install -y curl ca-certificates nodejs npm python3",
"npm install -g @anthropic-ai/claude-code @openai/codex @google/gemini-cli",
"npm install -g @qwen-code/qwen-code@0.24.0",
"npm install -g --ignore-scripts autohand-cli@0.9.7",
"npm install -g cline@3.0.62",
"npm install -g @kilocode/cli@7.7.2",
"npm install -g command-code@1.54.2",
"npm install -g @charmland/crush@0.95.0",
"curl -fsSL https://omp.sh/install | sh -s -- --binary --ref v18.2.2",
"npm install -g @continuedev/cli@1.5.47",
"mkdir -p /home/ubuntu/.continue && touch /home/ubuntu/.continue/.onboarding_complete && chown -R ubuntu:ubuntu /home/ubuntu/.continue",
"curl -fsSL https://cursor.com/install | bash",
"curl -fsSL https://aider.chat/install.sh | bash",
"UV_TOOL_BIN_DIR=/usr/local/bin uv tool install --python python3.12 aider-chat==0.86.2",
"curl -fsSL https://app.primeintellect.ai/prime-agent/install.sh | sh"
],
"ExecutableProvisions": [
{
"HostSourcePath": "/home/<operator>/.codeybox/agy-seed/agy",
"VmDestPath": "/home/ubuntu/.local/bin/agy",
"VmSymlinks": ["/usr/local/bin/agy"],
"Label": "antigravity"
}
]
}
}
}Pick the subset that matches the agents you have registered. For Gemini,
reasoning level is encoded in the model id (e.g. gemini-3-flash-preview),
not a CLI flag — there is no --thinking flag to pin a version against.
The aider lines install via uv (the aider.chat/install.sh script installs
uv itself, then uv tool install aider-chat); the trailing line re-pins to
the exact verified version (aider-chat==0.86.2) so the baked binary is
deterministic even though the vendor script tracks latest — bump the pin
when re-verifying against a newer aider. UV_TOOL_BIN_DIR pins the
aider binary onto /usr/local/bin because bake runcmds run as root and
uv tool install would otherwise land it in root's home directory, off the
sandbox user's PATH.
For Goose, add the shell-installer line to the selected provider's runcmd,
pinning the installer script by tag with a SHA256 check before executing
(verified GOOSE_VERSION=v1.50.1; the installer honors GOOSE_VERSION to
pin the binary download too, with no stable fallback when it is set):
{
"CodeyBox": {
"MultipassExtraRuncmd": [
"set -eux\nGOOSE_VERSION=v1.50.1\nGOOSE_INSTALLER=/tmp/goose-download_cli.sh\ncurl -fsSL -o \"$GOOSE_INSTALLER\" \"https://github.com/aaif-goose/goose/releases/download/${GOOSE_VERSION}/download_cli.sh\"\nprintf '%s %s\\n' \"ab5ae40513348ec4e6047cc7338040aab2df5246800c111d22065766ba6013f0\" \"$GOOSE_INSTALLER\" | sha256sum -c -\nGOOSE_VERSION=\"${GOOSE_VERSION}\" bash \"$GOOSE_INSTALLER\"\nrm \"$GOOSE_INSTALLER\"",
"goose --version"
]
}
}Goose runs one-shot headless (goose run -i - --output-format stream-json --no-session); unlike the interactive TUI some third-party harnesses drive,
no PTY or keep-alive flag is needed, so nothing else must be baked for it.
Autohand runs one-shot headless (autohand -p with the prompt on stdin,
--output-format stream-json --bare --offline --yes --unrestricted); --bare
is what keeps the run from blocking on the interactive Autohand-account
device login, and no vendor account, hook toolchain, LSP server, or
AGENTS.md discovery is needed in the image — bare mode skips all of them.
The post-install in-VM verification runs autohand --version plus a
--help assertion for --output-format stream-json (the runner's only
transport; a build that dropped it is benched at bake time, not first
dispatch). Provider credentials are NOT baked: the runner seeds the guest
~/.autohand/config.json from the CODEYBOX_AUTOHAND_API_KEY bundle at
dispatch time.
For Vibe, add the installer line to the selected provider's runcmd and pin
the version for deterministic bakes (verified mistral-vibe==2.25.4 via
uv; bump the pin when re-verifying against a newer vibe — the runner
asserts --output streaming support at smoke time, so a CLI that drops the
flag benches loudly instead of dispatching into plaintext):
{
"CodeyBox": {
"MultipassExtraRuncmd": [
"set -eux\ncurl -LsSf https://mistral.ai/vibe/install.sh -o /tmp/vibe-install.sh\nuv tool install --python python3.12 mistral-vibe==2.25.4\nvibe --version",
"mkdir -p ~/.vibe && cat > ~/.vibe/config.toml <<'EOF'\nactive_model = \"nemotron-free\"\n\n[[providers]]\nname = \"openrouter\"\napi_base = \"https://openrouter.ai/api/v1\"\napi_key_env_var = \"OPENROUTER_API_KEY\"\napi_style = \"openai\"\nbackend = \"generic\"\n\n[[models]]\nname = \"nvidia/nemotron-3.5-lightning:free\"\nprovider = \"openrouter\"\nalias = \"nemotron-free\"\nEOF"
]
}
}Vibe needs no vendor account: the guest ~/.vibe/config.toml above points
at OpenRouter with the key arriving per-dispatch as OPENROUTER_API_KEY
(the shipped credential mapping), and the model alias (nemotron-free) must
match the VIBE_ACTIVE_MODEL value the runner sends
(CodeyBox:AgentDefaults[vibe]). A baked config whose alias is missing
fails loudly at dispatch (exit 1, Active model … not found), never
silently. UV_TOOL_BIN_DIR advice from the aider entry applies equally:
bake runcmds run as root, so pin the vibe binary onto the sandbox user's
PATH (e.g. /usr/local/bin) or the in-VM smoke probe's --version step
fails the bake.
Cline runs one-shot headless (cline --json -P <provider> [-m <model>] --auto-approve true "<prompt>" with a positional prompt — the only prompt
transport this CLI version accepts; piped stdin was probed and rejected).
The post-install in-VM verification runs cline --version plus a --help
assertion for --json (the runner's only transport; a build that dropped it
is benched at bake time, not first dispatch). The provider key is NOT baked:
it arrives through the CODEYBOX_CLINE_API_KEY → OPENROUTER_API_KEY
credential mapping at dispatch time (operators may additionally pre-seed
~/.cline/data/settings/providers.json during provisioning, but the env key
alone suffices — cline auth needs a TTY so it never runs at dispatch).
Kilo runs one-shot headless (kilo run --auto --format json with the prompt
on stdin, -m openai-compatible/<model-id>): --auto is what keeps the run
from auto-rejecting its own permission requests and exiting 1, and
--format json is the runner's only transport. The post-install in-VM
verification runs kilo --version plus a run --help assertion for
--format/json and --auto (a build that dropped the transport or the
autonomy flag is benched at bake time, not first dispatch). Provider
credentials are NOT baked: the runner seeds the guest
~/.config/kilo/kilo.jsonc (openai-compatible provider block plus the
mandatory models allowlist entry) from the CODEYBOX_KILO_API_KEY bundle
at dispatch time.
OMP runs one-shot headless (omp -p --mode json --no-session with the prompt
on stdin, --model <model-id>): a bare omp "prompt" is interactive, so the
-p/--print flag is the one-shot contract, and --mode json is the runner's
only transport. The post-install in-VM verification runs omp --version plus
a --help assertion for --mode/json and -p/--print (a build that
dropped the transport or the one-shot flag is benched at bake time, not
first dispatch). Pin the install (--binary --ref v18.2.2 — the oh-my-pi
binary, not the oh-omp fork) and re-verify the headless contract before
bumping. Provider credentials are NOT baked: the provider API key arrives
through the CODEYBOX_OMP_API_KEY → OPENROUTER_API_KEY credential mapping
at dispatch time.
Continue runs one-shot headless (cn --print --auto with the prompt on
stdin — no positional prompt argument): --auto allows every tool (the
sandbox VM is throwaway with no human to approve anything), and --print
is the runner's only transport — --format json is deliberately NOT passed
(it coerces the model's answer into JSON rather than framing the transport).
The post-install in-VM verification runs cn --version plus a --help
assertion for --print and --auto (a build that dropped the transport or
the autonomy flag is benched at bake time, not first dispatch). The bake
must also create the first-run onboarding marker for the sandbox user (the
provisioning runcmd runs as root, so ~ would be the wrong home):
mkdir -p /home/ubuntu/.continue && touch /home/ubuntu/.continue/.onboarding_complete && chown -R ubuntu:ubuntu /home/ubuntu/.continue. Provider credentials are NOT baked: the runner
seeds the guest ~/.continue/config.yaml (single-model openrouter entry
with apiBase + literal apiKey, mode 0600) from the
CODEYBOX_CONTINUE_API_KEY bundle at dispatch time.
Qwen runs one-shot headless (qwen --approval-mode yolo --auth-type openai --output-format stream-json -m <model-id> with the prompt on stdin — no
positional prompt argument, never the deprecated -p flag):
--approval-mode yolo auto-approves every tool call (the sandbox VM is the
permission boundary), --auth-type openai pins env-key routing (Qwen OAuth
is discontinued), and --output-format stream-json is the runner's only
transport. The post-install in-VM verification runs qwen --version plus a
--help assertion for --output-format/stream-json (a build that dropped
the transport is benched at bake time, not first dispatch). Pin the install
(@qwen-code/qwen-code@0.24.0 — needs Node.js 22+ on the image) and
re-verify the headless contract before bumping. Provider credentials are NOT
baked: the key, base URL, and model fallback arrive through the
CODEYBOX_QWEN_* → OPENAI_* credential mappings at dispatch time.
Cursor installs the binary as agent (not cursor-agent) — verify it lands
on $PATH after the bake. Prime installs a self-contained prime-agent
binary on PATH (no runtime needed); the installer resolves the latest stable
release unless PRIME_AGENT_VERSION is set, so pin it
(PRIME_AGENT_VERSION=0.9.5 curl -fsSL … | sh) for deterministic bakes and
bump the pin when re-verifying against a newer prime-agent. Antigravity (agy) is provisioned via the selected
provider's executable-provision list, NOT a curl … | bash runcmd entry: the
upstream installer URL (https://antigravity.google/cli/install.sh) no longer
returns a shell script — as of 2026-06-17 it serves the Antigravity landing
HTML, which fails bash parsing and would silently slip past a … || true
runcmd. Stage a vetted copy of the binary on the host (e.g. extracted from a
known-good Multipass VM) and point HostSourcePath at it; the provisioner
sets mode 0755 and ownership root:root deterministically via install -m 0755 -o root -g root, then drops a /usr/local/bin/agy symlink onto the
non-login sandbox PATH. The baseline bake runs the registered --version
checks for every configured CLI-backed agent before marking the image ready
to clone, so a missing agy (or a missing host-staged file) fails the bake
instead of surfacing as dispatch exit 127. The standalone Copilot
CLI (binary name
copilot) is operator-supplied; do not substitute
gh extension install github/gh-copilot, which produces a gh copilot
subcommand and will not satisfy the runner. opencode does not yet ship a
runner in this repo; operators tracking the integration can pre-stage with
curl -fsSL https://opencode.ai/install | bash, but the orchestrator will
not dispatch to it until a runner is registered. dotnet-opencode needs a
heavier baseline than most agents: the exact .NET 11 preview SDK
(11.0.100-preview.7.26381.103 — the tool disables runtime roll-forward, so
a newer 11.x runtime refuses to launch it), the prerelease global tool
itself, and ripgrep. All verified against
dotnet-opencode 0.1.0-ci.20260905083303.33955573552.1:
# .NET 11 preview SDK first (dotnet-install.sh; apt feeds do not carry previews)
dotnet-install.sh --channel 11.0 --quality preview --version 11.0.100-preview.7.26381.103 --install-dir /usr/share/dotnet
dotnet tool install --global dotnet-opencode --prerelease --version 0.1.0-ci.20260905083303.33955573552.1
export PATH="$PATH:$HOME/.dotnet/tools"
command -v rg || apt-get install -y ripgrep # or set OPENCODE_DOTNET_RIPGREP to an absolute rg pathWithout the shim on PATH every dispatch fails with exit 127; without the
pinned runtime the shim fails with "You must install or update .NET"; without
ripgrep run fails fast with "Ripgrep is unavailable on PATH". The in-VM
smoke probe checks all three (--version, run --help | grep -q -- --format,
rg presence), so a missing piece benches the member with a named cause
instead of surfacing as an empty "no changes" run. Changing bake-affecting
provisioning values changes that provider's content-addressed baseline identity, so the next
sandbox creation bakes the corresponding new image rather than reusing another
provider's baseline.
The default appsettings.json bakes pinned gitleaks and semgrep
versions into Multipass baselines. If you override MultipassExtraRuncmd,
keep equivalent pinned install steps so security:gitleaks and
security:semgrep execute instead of reporting a missing-tool Warning:
{
"CodeyBox": {
"MultipassExtraRuncmd": [
"apt-get update",
"apt-get install -y curl ca-certificates tar python3 python3-pip",
"set -eux\nGITLEAKS_VERSION=8.29.0\ncase \"$(uname -m)\" in\n x86_64|amd64) GITLEAKS_ARCH=x64; GITLEAKS_SHA256=39e07ad810336fd0ae80d0bd61c60d0521f628173e7583583b5df4a38738522c ;;\n aarch64|arm64) GITLEAKS_ARCH=arm64; GITLEAKS_SHA256=4c811a7c23296c7163ebab166ec67cd8a31eb9922caf06a7cac4d6dd872e4159 ;;\n *) echo \"unsupported gitleaks architecture: $(uname -m)\" >&2; exit 1 ;;\nesac\nGITLEAKS_TGZ=/tmp/gitleaks_${GITLEAKS_VERSION}_linux_${GITLEAKS_ARCH}.tar.gz\ncurl -fsSL -o \"$GITLEAKS_TGZ\" \"https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_${GITLEAKS_ARCH}.tar.gz\"\nprintf '%s %s\\n' \"$GITLEAKS_SHA256\" \"$GITLEAKS_TGZ\" | sha256sum -c -\ntar -xzf \"$GITLEAKS_TGZ\" -C /usr/local/bin gitleaks\nchmod 0755 /usr/local/bin/gitleaks\nrm \"$GITLEAKS_TGZ\"",
"python3 -m pip install --break-system-packages --no-cache-dir --only-binary=:all: semgrep==1.168.0",
"gitleaks version | grep -Fx 8.29.0",
"semgrep --version | grep -Fx 1.168.0"
]
}
}Command Code runs one-shot headless (cmd --local-only -p --output-format json --no-session --skip-onboarding --yolo with the prompt on stdin,
-m openrouter/<model-id>): -p is the one-shot contract (a bare
cmd "prompt" is interactive), --output-format json is the runner's only
transport, --yolo is what keeps headless mode from silently changing
nothing (it blocks writes/edits/shell by default), and --local-only keeps
BYOK traffic provider-side with no Command Code plan required. The
post-install in-VM verification runs cmd --version plus a -p --help
assertion for --output-format/json and --yolo (a build that dropped
the transport or the autonomy flag is benched at bake time, not first
dispatch). Provider credentials are NOT baked: the provider key arrives
through the CODEYBOX_CMD_API_KEY → OPENROUTER_API_KEY credential mapping
at dispatch time (resolved through the seeded providers.json
$OPENROUTER_API_KEY reference — never a raw key), and the runner seeds the
guest ~/.commandcode/ files during provisioning (providers.json:
openrouter entry plus the model metadata; auth.json: a non-credential
presence placeholder the print-mode gate requires for plan-less
--local-only runs — see CmdConfigBuilder). The baseline image should
pre-seed the same two files (same shapes, same placeholder, no secrets) so
ad-hoc runs work:
mkdir -p ~/.commandcode
cat > ~/.commandcode/providers.json << 'JSON'
{
"provider": {
"openrouter": {
"name": "OpenRouter",
"baseURL": "https://openrouter.ai/api/v1",
"apiKey": "$OPENROUTER_API_KEY",
"models": { "nvidia/nemotron-3.5-lightning:free": {} }
}
}
}
JSON
cat > ~/.commandcode/auth.json << 'JSON'
{
"apiKey": "codeybox-local-only"
}
JSON