Skip to content

CLI Reference

Because pixi discovers pixi-<command> on PATH, all verbs work as pixi sandbox <verb> too.

Terminal window
pixi-sandbox --help
pixi-sandbox --version
pixi sandbox --help # via pixi extension

pack, doctor, and publish share two diagnostics controls:

Terminal window
pixi-sandbox pack ... -v
pixi-sandbox doctor ... -vv --log-file /var/tmp/pixi-sandbox-doctor.log
pixi-sandbox publish ... --log-file ./publish-attempt.log

-v writes phase boundaries to stderr; -vv also includes reviewed context such as local paths, platform, environment count, and helper-tool paths. --log-file <PATH> creates a durable plain text log and flushes every phase plus the complete causal error chain before exit. It works with or without -v, so CI can keep normal stdout unchanged while retaining evidence. The destination must not already exist: pixi-sandbox refuses to overwrite the only copy of an earlier failure.

Diagnostics never dump argv or the environment. A configured Git remote is recorded only as <configured>, and URL credentials plus common token/password query fields are redacted from nested process errors. Choose a log path outside the transport output: the log is operator evidence, not payload.

Create transport directory from pixi envs.

Terminal window
pixi-sandbox pack \
--repo-root . \
--envs dev,docs \
--output-dir .sandbox-out \
--platform linux-64 \
--shard-limit-mib 95 \
--cargo-vendor \
--cargo-vendor-mode loose|tarballs \
--fetch-tools \
--tools-lock /path/to/reviewed-tools.lock.json \
--tools-cache ~/.cache/pixi-sandbox/tools \
--self-bin target/release/pixi-sandbox
Flag Description
--repo-root Project root (default .)
--envs Comma-separated envs
--output-dir Output transport dir
--platform Platform (e.g., linux-64)
--shard-limit-mib Shard limit MiB (default 95)
--cargo-vendor Vendor Cargo deps
--cargo-vendor-mode loose (default) or tarballs
--fetch-tools Fetch pinned tools (sha256-verified)
--tools-lock Override tools.lock.json path
--tools-cache Tools cache dir
--self-bin Standalone binary to embed only under .pixi-sandbox/tools/<platform>/pixi-sandbox (.exe on Windows) — see below

No override means embedded pins from tools.lock.json compiled into binary.

The self-bin must run standalone. A transport is executed on machines that never saw your prefix — no pixi global installation, no conda activation, no shell profile — so pack probes the embedded copy with --version under an empty environment and refuses a binary that cannot answer. The classic trap is $(command -v pixi-sandbox) after pixi global install: that is a launcher trampoline, not the binary; it looks for trampoline_configuration/pixi-sandbox.json beside the executable and dies at restore, and it passed every other check (faithful copy, static linkage — the self-bin’s content is not pinned into the manifest). Pack the checksum-verified release asset (pixi-sandbox-<target> from the release, checked against its SHA256SUMS) or a binary you built yourself. A pixi global trampoline and a managed launcher script (the ~/.local/bin/pixi-sandbox indirection a restore registers) are refused outright, before any embedding. Packing for a platform other than the host skips the probe with a printed note — doctor --verify on the target host is the same guard there.

Verify transport without writing.

Terminal window
pixi-sandbox doctor --branch-location .sandbox-out --verify
pixi-sandbox doctor --branch-location .sandbox-out --verify --envs dev
pixi-sandbox doctor --branch-location .sandbox-out --verify --json

With --verify and an embedded pixi-sandbox tool, doctor also runs the standalone probe: after — and only after — every declared byte matches the manifest, it executes the tool’s --version under an empty environment, so a hash-green branch whose bootstrap cannot run (a packed pixi global trampoline, see --self-bin above) is diagnosed here with the remedy instead of on the airlock. The probe is skipped, with a printed reason, when the hash report is not green (unverified bytes are never executed) or when the manifest platform is not this host’s; a failed probe makes doctor exit non-zero.

Verify a restored project against the manifest’s per-file list, not just the branch:

Terminal window
pixi-sandbox doctor --branch-location .sandbox-out --verify-restored /path/to/project
Flag Description
--branch-location Transport dir
--verify Verify all blobs
--verify-restored <PROJECT> Also compare the restored tree to the manifest’s per-file digests (implies --verify)
--work-dir Work dir the restore used, if not the default <project>/.pixi/.restore-work
--envs Filter envs
--json Machine-readable output

Reports all failures, not first. Checks size + sha256 + tool linkage (rejects dynamic). --verify-restored additionally checks every file of the restored prefix (content, symlink targets, exec bits, the fingerprint marker) against the files.json oracle the packer recorded, flags any file present that the transport never carried, and reports schema-1 environments as unverifiable rather than failed. The airlock workflow’s archived ci-feature e2e gate runs it against the restored project and extracted transport.

Publish transport as orphan branch.

Terminal window
pixi-sandbox publish \
--input-dir .sandbox-out \
--branch-name sandbox/dev-linux-64 \
--remote origin \
--keep 5 \
--dry-run
Flag Description
--input-dir Transport dir
--branch-name Orphan branch to force-push
--remote Git remote (default origin)
--keep Retain at most N snapshots on the branch (default: 1, a single-commit orphan)
--dry-run Preview git commands, don’t push

Implementation: GIT_DIR points at scratch repo next to transport (same FS, never /tmp), GIT_INDEX_FILE scratch, GIT_WORK_TREE = transport, plumbing add → write-tree → commit-tree (no -p) → update-ref → push --force. No .git in owned dir, no copy.

Without --keep (or with --keep 1) a publish is what it has always been: one parentless commit that replaces the branch. --keep N keeps the N most recent snapshots instead — the N-1 previous ones are fetched shallow and --filter=blob:none (commits and trees only, so rotating does not download the payload it is rotating), re-committed as a fresh chain, and the new snapshot goes on top. The result is force-pushed like any other publish.

Terminal window
# a branch that carries the last three builds, oldest dropped on every publish
pixi-sandbox publish --input-dir .sandbox-out --branch-name sandbox/dev-linux-64 --keep 3

Airlock restore — verify then materialize. pixi-sandbox init generates project-side restore and restore.ps1 launchers that archive the selected sandbox ref (fetching that one branch first when a connected clone lacks it, unless PIXI_SANDBOX_FETCH=skip) and call this explicit form using its nested binary:

Terminal window
/tmp/sb/.pixi-sandbox/tools/linux-64/pixi-sandbox restore \
--branch-location /tmp/sb \
--output-path /path/to/project \
--envs dev \
--verify-only \
--force \
--no-vendor \
--work-dir /path/to/project/.pixi/.restore-work \
--cargo-config auto|write|print|none \
--user-tools register|skip \
--user-bin /path/to/user/bin

When invoked with no subcommand from inside an extracted branch, the nested binary remains read-only: it runs doctor --verify and prints the explicit restore command.

Flag Description
--branch-location Fetched branch dir (read-only)
--output-path Project output (new flag)
--path-to-main-repo-code Alias for --output-path (backward compat)
--envs Partial restore (vendor always materialized)
--verify-only Verify, write nothing
--force Overwrite existing envs; also replaces unmanaged pixi/pixi-sandbox entries in the user bin directory
--no-vendor Skip vendor
--work-dir Work dir (default .pixi/.restore-work, same FS, TMPDIR redirected)
--cargo-config auto (default) / write / print / none — auto writes sandbox-owned .pixi-sandbox/cargo-home/config.toml plus Pixi activation hooks; write overwrites project .cargo/config.toml; print/none do not wire Cargo
--user-tools register (default) / skip — register pixi + pixi-sandbox for the user after both verifications pass, or touch nothing outside the project (env: PIXI_SANDBOX_USER_TOOLS)
--user-bin Per-user bin directory for the launchers (default ~/.local/bin, Windows %USERPROFILE%\.pixi-sandbox\bin; env: PIXI_SANDBOX_USER_BIN)

Order: load manifest → verify everything → materialize tools (idempotent) → preflight disk → install envs (pixi-unpack → rename) → write markers (conda-meta/pixi_env_prefix, fingerprint) → vendor + Cargo wiring (auto uses .pixi-sandbox/cargo-home and per-env activation hooks, leaving project .cargo/config.toml untouched) → remove any legacy .pixi/sandbox-env.sh → verify the restored tree against the manifest’s per-file digests → register user tools (launchers + profile/registry PATH, unless --user-tools skip) → assertions pixi install --frozen --offline no-op, and when Cargo is wired, pixi run --frozen -- cargo build --offline uses the vendor tree.

User-tool registration: managed launchers exec the manifest-verified copies under .pixi/tools/<platform>/; a restore of another project retargets them (the most recent restore is the user-level source); unrelated existing pixi/pixi-sandbox commands are refused unless --force. There is no supported .pixi/sandbox-env.sh: pixi is the sole entrypoint, and package/crate commands should run as pixi run ... (or through the manifest-owned .pixi/tools/<platform>/pixi when registration is skipped), never as bare cargo, rustc, or bun commands from a sourced prefix. The only activation hooks restore writes are per-environment Conda/Pixi hooks that set CARGO_HOME to the sandbox-owned cargo home when vendored crates are restored in --cargo-config auto mode.

Single-env primitive, used by restore.

Terminal window
pixi-sandbox unpack \
--input-dir .sandbox-out \
--output-dir /tmp/prefix \
--env dev \
--unpacker /path/to/pixi-unpack \
--force \
--work-dir /tmp/work \
--verify-only

Accepts transport dir, .pixi-sandbox/envs/<env>/pack dir, or bare pack dir. Leaves pixi markers to restore.

Generate the reviewed project configuration, a reusable GitHub Actions publisher, a lockfile relock bot, and exactly one launcher for the current platform (restore.sh on Unix, restore.ps1 on Windows):

Terminal window
pixi-sandbox init
pixi-sandbox init \
--github-workflow-path .github/workflows/publish-sandbox.yml \
--relock-workflow-path .github/workflows/relock.yml \
--relock-ci-workflow ci.yml \
--script-path restore.sh \
--config pixi-sandbox.toml
pixi-sandbox init --branch sandbox/developer-linux-64 --force

relock.yml closes the one gap an airlock cannot close itself: a manifest edit is a text edit any disconnected host can make, but the solve behind it needs prefix.dev and the crates.io index. On a pull request its guard job runs pixi lock --check ahead of any environment install, and when the lock has drifted the second job refreshes pixi.lock (and Cargo.lock, when the plan vendors crates) onto the branch as pixi-sandbox[bot] with a conventional chore(lock): commit, then dispatches --relock-ci-workflow explicitly — a GITHUB_TOKEN push starts no workflow, so without that dispatch the lock commit would never be tested. It solves with the pixi version pinned in the embedded tool catalogue, so the lockfile it writes is one the pixi inside a transport can read. A fork pull request cannot be pushed to and the job says so instead of failing obscurely.

Without --config, init prefers pixi-sandbox.toml, falls back to an existing .pixi-sandbox.toml, and creates the preferred name when neither exists. Generated workflow and launcher markers allow safe regeneration without --force; an unmarked user-owned destination requires the explicit flag. The launcher uses only local git archive, tar, and the verified binary stored under .pixi-sandbox/tools/<platform>/ in the sandbox branch. It never downloads during restoration.

Every file init writes carries a pixi-sandbox-version: X.Y.Z line beside its ownership marker, naming the exact CLI release that rendered it.

Render every owned file fresh, write nothing, and report drift:

Terminal window
pixi-sandbox init --check

Exits 0 when the generated workflow, the relock workflow, the launcher, and the sandbox config schema all match what init would produce right now. Otherwise it names each finding on its own line with the remedy that fixes it, and exits non-zero:

  • a missing owned file — remedy: pixi-sandbox init
  • a stale owned file, present and marked but no longer matching a fresh render — remedy: pixi-sandbox init
  • a foreign file at an owned path, present but carrying no ownership marker — remedy: pixi-sandbox init --force
  • a sandbox config whose schema this CLI no longer understands — remedy: upgrade the CLI (a config older than the CLI’s current schema is accepted silently; config is reviewed data and is never rewritten by either init or --check)

--check and --force are mutually exclusive: auditing a tree and replacing a foreign file on it are different operations on purpose. This is the same check the generated workflow’s scheduled upgrade job runs against a freshly self-updated binary (see below) before it proposes anything.

Validate pixi-sandbox.toml and emit matrix.

Terminal window
pixi-sandbox plan --config pixi-sandbox.toml
pixi-sandbox plan --config pixi-sandbox.toml --json

Rejects unsafe/implicit runner selection, checks embedded tool coverage.

Terminal window
pixi-sandbox tools list
pixi-sandbox tools list --tools-lock /path/to/tools.lock.json
# connected host: is any pin stale? downloads nothing, exits non-zero if so
pixi-sandbox tools update --check
# refresh the pins and print a candidate catalogue for review
pixi-sandbox tools update > /tmp/tools.lock.json
# refresh one file in place (atomic; unchanged on any failure)
pixi-sandbox tools update --tools-lock /path/to/tools.lock.json
pixi-sandbox tools update --tool pixi --tools-lock /path/to/tools.lock.json

tools update resolves each tool’s latest release from the repository in its own url_template, downloads the release assets, and re-derives the recorded linkage from the bytes — a pin that claims static but ships a dynamically linked binary is refused. The write happens only after every asset has been checked, so a failure leaves the file untouched.

Upstream does not publish checksums for the bare binaries this catalogue pins (see design.md §10), so each pin’s note records which guarantees actually applied: cross-checked against an upstream manifest, manifest-does-not-cover-this-asset, or no manifest. Treat a new pin as reviewed data: read the diff before committing it.

Replaces a standalone pixi-sandbox binary with a published release. Needs network; it is the one command that does.

Terminal window
# the latest release, replacing the running binary
pixi-sandbox self-update
# an exact release — what every committed workflow and transport should name
pixi-sandbox self-update --version 0.5.0
# roll back by naming an older release
pixi-sandbox self-update --version 0.4.3
# resolve and report, writing nothing
pixi-sandbox self-update --check
# CI: update a disposable binary in runner scratch, not the machine's own tool
pixi-sandbox self-update --version 0.5.0 --dest "$RUNNER_TEMP/pixi-sandbox"
flag meaning
--version X.Y.Z Install this exact release. Accepts X.Y.Z or vX.Y.Z; anything else is refused before any network access. Without it, the latest release is resolved.
--check Print the current and resolved versions and exit. Downloads no binary and writes nothing.
--dest PATH Update this path instead of the running binary.
--repo OWNER/NAME Repository publishing the release assets. Defaults to Archont561/pixi-sandbox.

The order is the point: the release is resolved and the destination is classified before anything is downloaded, so a destination this command will not touch costs no bytes. Then SHA256SUMS is fetched before the binary — there is no reason to hold bytes you cannot judge — and the asset is checked against it. A missing entry and a digest mismatch are separate refusals, and neither reaches the disk. Nothing downloaded is ever executed.

The host is mapped to one canonical asset (pixi-sandbox-<target>, with .exe on Windows); an unsupported host is an error naming the host rather than a guess.

Self-update only ever replaces a standalone binary. If pixi-sandbox was installed by a package manager, the prefix records the file’s digest, and overwriting it in place would leave the environment describing bytes that are no longer there. So the destination is classified by where it lives, and each refusal names the remedy that actually works:

destination remedy
Inside a conda/Pixi prefix (an ancestor conda-meta/) pixi update pixi-sandbox
A pixi global install trampoline pixi global update pixi-sandbox
A pixi-sandbox-managed launcher in your user bin directory restore a newer transport, or pass --dest
A restored transport tool under .pixi/tools/<platform>/ repack and republish the transport

There is no --force. An escape hatch here would reintroduce exactly the corruption the check exists to prevent.

The verified bytes are staged in the destination’s own directory — never a temp dir, so the final move is same-filesystem and therefore atomic — and only then swapped in.

On Unix the staged file is renamed straight over the destination. That is atomic, and it works even when the destination is the binary currently running: the rename swaps the directory entry and leaves the busy inode to the running process, so there is no window in which the tool is missing and no Text file busy.

Windows cannot rename onto an existing file and will not delete a running image, but it does allow renaming one. So the old binary is moved aside to a .pixi-sandbox-old-<version> sibling, the new file takes its name, and the leftover is removed at the start of the next update — deleting it immediately fails by design while the old image is still mapped. If the second rename fails, the first is undone.

Terminal window
pixi-sandbox init # connected host; commit the generated files
./restore.sh # disconnected host; local Git objects, or one sandbox-branch fetch if connected

The generated platform-appropriate launcher contains no installation logic. It archives the configured sandbox branch and delegates verification and restoration to its nested binary; if the ref is absent in a connected clone it fetches refs/heads/<branch> into origin/<branch>, while PIXI_SANDBOX_FETCH=skip keeps a fully local airlock from attempting any remote access.