Offline sandboxes for pixi projects β pack, publish to an orphan branch, restore on an airlock with zero network.
Pure Rust, static musl binaries, checksum-verified, Git-native dedup.
Note
Because pixi discovers pixi-<command> on $PATH, installing pixi-sandbox unlocks native subcommands: pixi sandbox pack, doctor, publish, restore, plan.
The badge above mirrors [workspace] platforms in pixi.toml. A platform only gets a published
bundle in .pixi-sandbox.toml once a native runner has passed the airlock proof (decision D11),
so "builds here" and "restores offline there" are tracked separately β
pixi run --frozen xtask check-repository keeps the badge, pixi.toml and the publish plan in agreement.
| Platform | Status | Notes |
|---|---|---|
linux-64 |
β proven, published | sandbox/developer-linux-64 is packed, verified and restored offline in CI |
osx-arm64 |
β proven, published | Packed, verified and restored on a native macos-14 runner with egress denied (sandbox-exec, network-outbound refused), then gated against the manifest; sandbox/developer-osx-arm64 is published by the same plan as linux-64 (task-1) |
linux-aarch64, osx-64 |
π‘ release binaries only | Static binaries ship with every release; no bundle published |
win-64 |
β not supported | default itself no longer needs bun (task-25 isolated it in the web environment), but pixi lock solves every environment for every platform in pixi.toml, and conda-forge has no bun build (pixi lock: "No candidates were found for bun *"), so Windows is out of [workspace] platforms and D11 still lists it as unproven. Until that changes the JS toolchain is unsupported on Windows: use the Rust CLI, and run the JS tools through your own Node.js/Bun install outside the pixi environment |
connected build machine orphan branch (Git) airlocked machine
βββββββββββββββββββββββ βββββββββββββββββββ βββββββββββββββββ
βββββββββββββββββββββββ βββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββ
β pixi project β pack β sandbox/developer-linux-64 β fetch β airlock β
β β βββββββββΊ β β βββββββββΊ β β
β pixi.toml β β .pixi-sandbox/manifest.json β β .pixi/envs/<env>/ β
β pixi.lock β β envs/<env>/pack/ β β .pixi/tools/<platform>/β
β Cargo.lock β β tools/<platform>/ β β .pixi-sandbox/vendor/ β
β .pixi-sandbox.toml β β vendor/ β β offline β β
βββββββββββββββββββββββ βββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββ
pack pixi sandbox pack --cargo-vendor --fetch-tools --self-bin <static>
fetch plain git β whole files content-addressed, 262 MB transport β ~110 MB after dedup
local ./restore.sh β verify every byte β registered pixi β pixi run offline
Generate publishing and one-command airlock restoration:
Connected hosts need Pixi installed. Install from the canonical channel, then initialise the project:
pixi global install --channel https://prefix.dev/archont561/archont561 --channel conda-forge pixi-sandbox
pixi-sandbox init
# commit .github/workflows/publish-sandbox.yml, pixi-sandbox.toml, and restore.sh (restore.ps1 on Windows)
# after transferring Git history with the sandbox branch into the airlock:
./restore.sh| Icon | Feature | Description |
|---|---|---|
| π | Zero-Network Restore | Verified restore in unshare -rn without touching conda channels or crates.io |
| π¦ | Git-Native Transport | Orphan branch, whole files content-addressed, free dedup across envs/releases |
| βοΈ | Automatic Sharding | Splits >95 MiB into .partNNN to respect GitHub 100 MiB blob limit |
| π‘οΈ | Strict Verification | SHA-256 verified against embedded pins before writing; dynamic tools rejected |
| π¦ | Cargo Vendoring | Bundles cargo vendor --versioned-dirs alongside conda envs |
| β‘ | Pure Rust CLI | Static musl Linux, native macOS/Windows, no Python; pixi sandbox extension |
| π€ | Generated Native CI | Project-owned workflow calls the installed CLI directly on native runners |
| π | Docs like astro-icon | Starlight + astro-icon + Iconify (lucide, mdi, tabler, simple-icons) |
Docs site uses Astro Icon + Iconify β 300k+ icons: https://icon-sets.iconify.design/
# Build static self-binary (or use verified release)
pixi run --frozen build-release-binary
# 1. Pack
pixi sandbox pack \
--repo-root . \
--envs dev,docs \
--output-dir .sandbox-transport \
--platform linux-64 \
--cargo-vendor \
--fetch-tools \
--self-bin target/release/pixi-sandbox
# 2. Verify (writes nothing)
pixi sandbox doctor --branch-location .sandbox-transport --verify
# 3. Publish as orphan branch (<branch_prefix>/<bundle>-<platform>, as `plan` reports it)
pixi sandbox publish \
--input-dir .sandbox-transport \
--branch-name sandbox/developer-linux-64Transfer a clone or Git bundle containing both the project and sandbox branch, then run the
project-side launcher generated by pixi-sandbox init:
./restore.sh # Linux/macOS
# .\\restore.ps1 # Windows PowerShell
# in a new shell, pixi and pixi sandbox already resolve (registered user tools);
# pixi is the sole entrypoint for environment binaries:
pixi install --frozen --offline # must be no-op
pixi run --frozen -- cargo build --offline # uses vendored cratesAfter verifying the branch and the restored tree, the restore registers managed launchers
for pixi and pixi-sandbox in ~/.local/bin (Windows: %USERPROFILE%\.pixi-sandbox\bin)
and puts that directory on the shell's persistent PATH β so pixi and pixi sandbox work
in a new shell without sourcing anything. PIXI_SANDBOX_USER_TOOLS=skip (or
--user-tools skip) leaves the home untouched for CI, shared accounts, or locked-down
airlocks; --user-bin <dir> relocates the launchers.
The launcher reads the selected config (pixi-sandbox.toml by default, with
.pixi-sandbox.toml retained as a compatibility fallback) and resolves
<branch_prefix>/<bundle>-<platform> for the host β the same branch pixi-sandbox plan gives
the publisher β so renaming a bundle or
prefix needs no regenerated launcher. It uses local git archive when the sandbox ref is
present; if a connected clone lacks refs/remotes/origin/sandbox/*, it fetches just the selected
sandbox branch unless PIXI_SANDBOX_FETCH=skip is set. It extracts the branch under
.pixi/.restore-transport and invokes .pixi-sandbox/tools/<platform>/pixi-sandbox.
PIXI_SANDBOX_BRANCH=sandbox/developer-linux-64 ./restore.sh # pin an exact branch
PIXI_SANDBOX_BUNDLE=developer ./restore.sh # several bundles cover this platformTip
After restore, .pixi/envs/* has relocated prefixes and Cargo is wired through a sandbox-owned .pixi-sandbox/cargo-home/config.toml plus Pixi activation hooks, so an existing project .cargo/config.toml is left alone. No .pixi/sandbox-env.sh is generated; run package and crate commands through pixi run ....
# pixi-sandbox.toml
schema = 1
branch_prefix = "sandbox"
cargo_vendor = true
[runners]
# linux-aarch64 = "self-hosted-arm64" # required β no hosted default
[[bundle]]
name = "developer"
environments = ["dev", "docs"]
platforms = ["linux-64", "osx-arm64", "win-64"]
[[bundle]]
name = "minimal"
environments = ["default"]
platforms = ["linux-64"]
cargo_vendor = falseValidate:
pixi-sandbox plan --config pixi-sandbox.toml
pixi-sandbox plan --config pixi-sandbox.toml --json # GitHub Actions matrixschema = 1
cargo_vendor = false
[[bundle]]
name = "python"
environments = ["dev", "test"]
platforms = ["linux-64", "win-64"]pixi-sandbox pack --repo-root . --envs dev --output-dir .sandbox-out --platform linux-64 --fetch-tools --self-bin target/release/pixi-sandboxFull guide: https://archont561.github.io/pixi-sandbox/guides/using-in-your-project/
Run pixi-sandbox init to generate a reviewed, project-owned .github/workflows/publish-sandbox.yml.
The generated workflow installs the native package from the canonical prefix.dev channel and calls
plan, pack, doctor, and publish directly on native runners. Commit that workflow with
pixi-sandbox.toml; no repository-owned composite Action or reusable workflow is required.
Migration from v0.3.x Actions: new refs no longer contain
Archont561/pixi-sandbox@β¦,Archont561/pixi-sandbox/setup@β¦,Archont561/pixi-sandbox/publish@β¦, or the reusable.github/workflows/publish-sandbox.yml. Immutable older tags and commit SHAs retain those files. Regenerate and commit the direct CLI workflow withpixi-sandbox init.
| Field | Type | Default | Description |
|---|---|---|---|
schema |
int | required 1 |
Config schema version |
branch_prefix |
string | "sandbox" |
Branch prefix: <prefix>/<bundle>-<platform> |
cargo_vendor |
bool | true |
Default vendor for bundles |
runners |
table | {} |
Platform β runner label override |
Bundle:
| Field | Required | Description |
|---|---|---|
name |
yes | Bundle name, used in branch |
environments |
yes | Explicit pixi envs, never inferred |
platforms |
yes | e.g., linux-64, osx-arm64, win-64, linux-aarch64 |
cargo_vendor |
no | Override top-level |
Runners:
| Platform | Default | Notes |
|---|---|---|
linux-64 |
ubuntu-latest |
Hosted |
linux-aarch64 |
none | Must provide [runners] self-hosted |
osx-64 |
macos-13 |
Hosted Intel |
osx-arm64 |
macos-14 |
Apple Silicon |
win-64 |
windows-latest |
Hosted |
Full reference: https://archont561.github.io/pixi-sandbox/reference/configuration/
pixi-sandbox pack --repo-root . --envs dev,docs --output-dir DIR --platform linux-64 --cargo-vendor --fetch-tools --self-bin <static>
pixi-sandbox doctor --branch-location DIR --verify [--json] [--envs a,b]
pixi-sandbox publish --input-dir DIR --branch-name NAME --remote origin [--keep N] [--dry-run]
pixi-sandbox restore --branch-location DIR --output-path DIR [--envs a,b] [--verify-only] [--force] [--no-vendor] [--work-dir DIR] [--cargo-config auto|write|print|none]
pixi-sandbox unpack --input-dir X --output-dir PREFIX [--env NAME] [--force] [--work-dir DIR]
pixi-sandbox init [--github-workflow-path PATH] [--script-path PATH] [--config PATH] [--force]
pixi-sandbox plan --config pixi-sandbox.toml [--json]
pixi-sandbox tools list [--tools-lock PATH]--output-pathhas alias--path-to-main-repo-codefor backward compat- Work dir default
.pixi/.restore-work(same FS, TMPDIR redirected) β never small/tmp publish --keep Nretains the N most recent snapshots by rebuilding the branch (blobless shallow fetch of the kept commits, then one force-push). It bounds what the branch serves, not what the server stores β dropped snapshots are unreferenced until an admin prunes
Docs: https://archont561.github.io/pixi-sandbox/reference/cli/
sandbox/<bundle>-<platform>/ # orphan branch
βββ .pixi-sandbox/
β βββ manifest.json # SHA-256 catalog, source commit, tool versions, vendor
β βββ envs/<env>/pack/ # pixi-pack --directory-only: channel/*.conda + env.yml + pixi-pack.json
β βββ tools/<platform>/ # pixi, pixi-unpack, pixi-sandbox (static, verified)
β βββ vendor/ # cargo vendor --versioned-dirs (loose, deduped by git)
βββ README.md # human restore instructions
βββ AGENTS.md # agent instructions
Only Markdown files live at the branch root. pixi-sandbox init generates exactly one minimal
launcher on the normal project branch: restore.sh on Unix or restore.ps1 on Windows. It
resolves its branch from the selected config, fetches the selected sandbox branch only when no
local copy exists and fetching is not disabled, archives that ref, and invokes its
manifest-owned binary.
Sizes (small project, 2 envs, 33 crates, linux-64):
| Part | Payload |
|---|---|
| conda envs (.conda) | 133 MB |
| cargo vendor (loose) | 33 MB |
| tools | 96 MB |
| transport dir | 262 MB |
| orphan branch after git dedup | ~110 MB |
Tools dominate small bundles β expected, git stores each tool blob once.
| Path | Description |
|---|---|
crates/pixi-sandbox-core |
Core lib: manifest format, sharding, tool pins, verification |
crates/pixi-sandbox-git |
Trait-based Git: ShellGit + FakeGit/RecordingRunner |
crates/pixi-sandbox |
CLI binary + fixtures |
crates/pixi-sandbox/tests/fixtures/ |
Synthetic transport (15 KB, split blob) β tests never point at repo root |
crates/xtask |
Typed repository automation behind one pixi run --frozen xtask <subcommand> task (check-repository, prepare-release, release artifact gates) |
.github/workflows/ci.yml |
CI: lint + test + coverage + docs build |
.github/workflows/release.yml |
Release: 5 tier-1 static binaries + prefix.dev Conda package + GitHub Release |
.github/workflows/docs.yml |
Docs β GitHub Pages |
.github/dependabot.yml |
Dependabot: cargo, gha, npm (convco prefixes) |
scripts/restore.sh |
One-liner offline reconstruction with pixi launcher registration; branch derived from .pixi-sandbox.toml; selects the user-tool registration policy explicitly and never sources an activation hook |
.pixi-sandbox.toml |
Reviewed publish plan: bundles, platforms, branch prefix, runner overrides |
.devcontainer/devcontainer.json |
Dev container: official pixi image, git/gh as pixi globals, opencode installed by .devcontainer/setup.sh with a global bun add |
lefthook.yml |
Git hooks β every hook calls a pixi task so hooks and CI cannot drift; pre-commit stays formatter-only (no cargo), every Rust gate runs once at pre-push |
docs/ |
Starlight + astro-icon + Iconify docs (12 pages) |
package.json + bun.lock + turbo.json |
Root Bun workspace and cross-language task graph: docs plus private Cargo-crate task packages; Turbo, Biome, backlog and skills are lock-pinned |
.knowledge/ |
Open Knowledge Format: decisions D1βD11, design, benchmarks |
CHANGELOG.md |
Changelog via convco |
Pixi is the only supported entrypoint for repository tooling. Do not activate .pixi or run
bare cargo, bun, rustc, taplo, or convco: use pixi run --frozen <task>. Rust crate
work goes through pixi run --frozen xtask <subcommand> for repository automation or
pixi run --frozen -- cargo <cmd> -p <crate> when you need Cargo itself; JavaScript package
work goes through pixi run bun --filter=<workspace-package> run <script> (for example,
--filter=pixi-sandbox-docs) or the root pixi run --frozen bunx <tool> task.
Tests use rstest fixtures from crates/pixi-sandbox/tests/support/mod.rs, named #[case]
parameterisation for finite inputs, and bounded proptest invariants for universal contracts. New
properties keep their .proptest-regressions seed file in version control; new integration tests
use the committed fixtures and isolated temp homes rather than this checkout.
# Lint (rustfmt, clippy -D warnings, cargo-deny, actionlint, taplo, biome)
pixi run --frozen lint
# Test (nextest, fixtures, no network)
pixi run --frozen test
pixi run --frozen test-doc
# Coverage
pixi run --frozen coverage
# Docs (Astro + Starlight + astro-icon; tasks call bun --filter=pixi-sandbox-docs)
pixi run --frozen docs dev
pixi run --frozen docs build
# Agent CLI + repo tooling (root bun workspace, through bunx)
pixi run --frozen bunx backlog # markdown backlog
pixi run --frozen bunx skills # agent skills CLI
# Transport pipeline (pure Rust self-bin)
pixi run --frozen sandbox-plan
pixi run --frozen sandbox-pack # pack + vendor + fetch-tools + self-bin target/release/pixi-sandbox
pixi run --frozen sandbox-doctor
pixi run --frozen sandbox-publish
pixi run --frozen sandbox-restore # one-liner from orphan branch
pixi run --frozen test # fixture-backed doctor β publish β offline restore lifecycle
# Generated artifacts
pixi run --frozen xtask render-relock # rewrite the committed render of .github/workflows/relock.yml
# Changelog via convco
pixi run --frozen xtask prepare-release auto # stamps CHANGELOG.md and every version reference
pixi run --frozen xtask commit-release v0.3.8 # commits, tags and pushes a prepared release (--dry-run shows the diff)
pixi run --frozen xtask airlock-matrix # the airlock proof matrix, validated and GitHub-shaped
pixi run --frozen xtask stage-release-binary # strip + stage the release asset (host triple by default)
pixi run --frozen xtask release-checksums # SHA256SUMS over the standalone binaries, verified complete
pixi run --frozen build-release-binary # cargo build -p pixi-sandbox --release (host; -- --target in CI)
# CI variants (env vars: SANDBOX_PROJECT, ENVS, TRANSPORT, PLATFORM, BRANCH, REMOTE, SELF_BIN)
pixi run --frozen sandbox-pack "$PROJECT" "$ENVS" "$TRANSPORT" "$PLATFORM" && pixi run --frozen sandbox-doctor "$TRANSPORT"Editing pixi.toml or a Cargo.toml is a text edit any disconnected host can make; the solve
behind it is not, because prefix.dev and the crates.io index are exactly what an airlock cannot
reach (and pixi add solves before it writes). So the solve belongs to the connected side:
- Edit the manifest and push a pull request with the lock left stale.
- The guard reports.
relock.yml's first job runspixi lock --checkbefore any environment is installed, so a stale lock fails with a message about the manifest rather thansetup-pixi's message about installation. - The bot relocks. When the guard fails, the second job refreshes
pixi.lockandCargo.lockonto your branch aspixi-sandbox[bot]with achore(lock):commit, then dispatchesci.ymlandpublish-sandbox.ymlexplicitly β aGITHUB_TOKENpush triggers no workflow, so those dispatches are the only verdict the lock commit gets. - Merge once CI is green.
Important
A merged lockfile does not make a dependency usable in the airlock. A restored host
builds against the packed conda environments and .pixi-sandbox/vendor, so the new crate or
package exists for it only once a transport carrying it has been packed and published
(pixi run --frozen sandbox-pack β pixi run --frozen sandbox-doctor β pixi run --frozen sandbox-publish). Until then the dependency
is connected-side only, and an offline pixi run --frozen -- cargo build --offline will still fail on it.
The workflow is generated, not hand-written: it is the render of
crates/pixi-sandbox/src/generated/relock_workflow.rs that pixi-sandbox init also writes for
consumers, byte-checked by pixi run --frozen xtask check-repository. Change the template and run
pixi run --frozen xtask render-relock; never edit .github/workflows/relock.yml directly.
Important
Integration tests run against synthetic fixtures in crates/pixi-sandbox/tests/fixtures/, never against this repo itself, ensuring hermetic offline isolation. Two tests enforce it: the fixture must not depend on pixi-pack, and no test may walk out via ../.parent() β with the single reviewed exception scripts/restore.sh, which is an artifact under test rather than fixture data (tests/restore_script.rs runs the real bootstrap against a throwaway git repo and the transport fixture).
.devcontainer/devcontainer.json is four keys and no Dockerfile β it runs the official
ghcr.io/prefix-dev/pixi image as-is. That image is Ubuntu plus the pixi binary: no git,
and no C compiler, so the post-create step delegates to a commented script,
.devcontainer/setup.sh, which provisions the host tools, materialises the
project environment, installs the bun workspace, and installs the agent CLI:
pixi global installs into /root/.pixi/bin, which the official image already has on PATH,
so git/gh are available to Source Control, the lefthook hooks, scripts/restore.sh and
the sandbox tasks.
The compiler is the non-obvious one. ring (a transitive dependency of the workspace) shells
out to cc and ar from its build script, so without a C toolchain pixi run --frozen lint and
pixi run --frozen test cannot even compile β and CI never sees this, because a hosted runner has a
system cc. It is a global pixi install on purpose: a c-compiler dependency in a feature
would put a ~200 MB toolchain into the packed sandbox branch, to build nothing an airlock
restores. ar needs the mapping (ar=x86_64-conda-linux-gnu-ar) because conda-forge ships
no unprefixed ar; pixi global list is the check that cc, gcc and ar are exposed.
The last two steps go through pixi run rather than a bare bun install: bun lives in the
materialised environment, which is on PATH inside a pixi task and nowhere else.
opencode runs last and is not a pixi task: .devcontainer/setup.sh installs it with a
global bun add opencode-ai@latest (bun from the materialised web environment), links the
binary into /usr/local/bin β bun's own global bin dir is on nobody's PATH β refreshes the
model catalogue (~/.cache/opencode/models.json, which a binary upgrade does not invalidate),
and prints the command a Codespace user starts with:
opencode -m opencode/big-pickleIt lives in the container setup rather than pixi.toml because pixi.toml describes what the
repository is built, tested and released with, and a 185 MB agent binary is none of those.
package.json at the root makes this one bun workspace with docs as a member, so
pixi run --frozen docs-install runs at the root and hoists into the root node_modules; package
scripts target the workspace explicitly (bun --filter=pixi-sandbox-docs run build/dev), and
docs/ has no lockfile of its own any more. bunfig.toml pins the install to bun's hoisted linker
because the workspace default (isolated) hides transitive platform packages that Astro
prerenders by name β see that file for the failure it prevents. The root also carries the
repo-wide dev tooling β backlog.md and skills β reached through one generic bunx task:
pixi run --frozen bunx backlog / pixi run --frozen bunx skills (and pixi run --frozen bun <args> for bun itself, in
the web environment). The task runs bun x --bun: bun x because the conda bun package
ships no bunx shim, --bun because the bins carry node shebangs and no environment here
carries node β adding one would put ~50 MB of developer tooling into the published sandbox
branch. It resolves the workspace install before the registry and depends on docs-install,
so the committed lockfile still decides what runs.
- Changelog:
CHANGELOG.mdgenerated from conventional commits bypixi run --frozen xtask prepare-release, which stamps it together with every version reference (a bare preview ispixi run --frozen -- convco changelog). - Release: Tag
v*.*.*βrelease.ymlbuilds 5 static binaries, buildspixi-sandboxas a Conda package, publishes it toarchont561/archont561with GitHub OIDC, then creates the GitHub Release. Configure prefix.dev Repository Access for this repository'srelease.ymlworkflow; no long-lived token is stored in GitHub.gh workflow run auto-release.yml -f version=vX.Y.Z
- Dependabot:
.github/dependabot.ymlfor cargo, gha, npm (docs) β weekly, groups patch/minor, prefixeschore/cito pass convco. - Pixi deps: Manual via
pixi update,pixi lock --check.
MIT β see LICENSE. Third-party packages and vendored crates retain upstream licenses.
Full docs: https://archont561.github.io/pixi-sandbox/