Go, Rust and C/C++

Compiled languages share a shape that scripting languages don't: there are two things to run, and they sandbox differently.

  1. The toolchaingo build, cargo build, cmake, ninja. Needs to write your working tree, and usually needs the network to fetch dependencies.
  2. The binary it produced — needs neither, usually. It is a file, and boks can execute it directly.

Almost every difference between these languages under boks comes from how well the second half works.

Read the three levels first if you haven't.

Toolchain capabilities differ, and it matters

ToolDefault capabilitiesConsequence
gorwboks go build just works.
cmake, ninjarwSame.
cargo, rustcnoneEvery boks cargo build needs --cap rw by hand at level 1.

The rw grants exist on the reasoning that a build tool inherently produces output. Rust's toolchain missing it is the single best argument for a project .boksrc in this whole section — one capabilities: [rw] line removes a flag from every command you will ever type in that project.

Dependencies need net on the first build:

boks --cap rw --cap net cargo build
boks --cap net go build ./...

boks declares a host-backed ~/.cargo/registry cache for cargo/rustc, so crates already fetched are reused across builds and across projects. Go's module cache behaves similarly.

Running what you built

boks ./some-binary — any tool-name argument containing a / — means "run this local file", not "look up this tool name". boks validates the ELF header and executes it against boks-base-static-ubuntu, its own minimal base image. A bare hello with no / is a tool-name lookup and will fail; write ./hello the same way your shell makes you.

Whether that works depends on how your binary was linked:

Linkingboks ./binary
Gostatic by default (CGO off)works
Rustdynamic against libgcc_s.so.1fails today — the static base ships libc6/libm, no libgcc
C/C++depends on your flagsworks if statically linked

The Rust failure looks like this:

./target/debug/hello: error while loading shared libraries: libgcc_s.so.1: cannot open shared object file: No such file or directory

Use cargo run instead, which executes the binary inside cargo's own image where those libraries exist — and which is the ordinary Rust workflow anyway.

The binary's own capabilities

A path key in .boksrc is a full citizen: it takes capabilities, pass_env, set_env, dotenv, and subcommand_index/subcommands, exactly like a named tool.

tools:
  ./hello:
    capabilities: [rw]
    subcommands:
      serve:
        ports: ["8080"]
        args: ["-addr", "0.0.0.0:8080"]

boks ./hello serve publishes the port and gets the bind address; boks ./hello gets neither.

The match is exact and unnormalised. ./hello, hello, target/release/app and ./target/release/app are four different keys — the key has to match however your scripts actually invoke the binary.

Level 2 — where the subcommand actually is

The one thing worth thinking about is subcommand_index. subcommands matches exactly one argument, and the same word lands in different places depending on how you start the program:

boks go run . serve      args = ["run", ".", "serve"]     index 2
boks cargo run -- serve  args = ["run", "--", "serve"]    index 2
boks ./hello serve       args = ["serve"]                 index 0

So a project that can be started either way declares both:

tools:
  go:
    subcommand_index: 2
    subcommands:
      serve:
        ports: ["8080"]
        args: ["-addr", "0.0.0.0:8080"]

  ./hello:
    capabilities: [rw]
    subcommands:
      serve:
        ports: ["8080"]
        args: ["-addr", "0.0.0.0:8080"]

The trust prompt tells you which index each entry uses — port(serve@arg2) versus a bare port(serve) for the default 0:

▍               ./hello: +rw, port(serve) 8080, args(serve) -addr 0.0.0.0:8080
▍               go: port(serve@arg2) 8080, args(serve@arg2) -addr 0.0.0.0:8080

A key that matches nothing is silently inert — no warning, no error. When a port isn't being published, that review line is the fastest way to find out why.

Cargo makes this especially easy to check, because it echoes spliced arguments back:

     Running `target/debug/hello_cargo serve --addr '0.0.0.0:8090'`

Level 3 — and its hard limit

boks -i go
boks -i cargo
boks -i cmake
$ go version
go version go1.26.5 linux/arm64

linux/arm64 on a macOS host is the whole point in one line: there is no toolchain installed at all.

A compiled binary cannot be shimmed. A shim is a symlink named after a PATH-stable tool name, and ./hello means something different from every directory:

$ boks --install ./hello
▍  ■ ERROR      --install doesn't apply to a path ('./hello') -- shims are for
▍               named tools

And on a non-Linux host it cannot run outside boks either, because the toolchain that built it ran on Linux:

$ ./hello
zsh:1: exec format error: ./hello

So compiled languages reach level 3 for the toolchain and stay at level 1–2 for the output. On a Linux host ./hello runs natively — with all of your own privileges and no sandbox at all, which is a trade-off to make on purpose rather than by accident.

To get a host-native binary, cross-compile and say so:

boks -E GOOS=darwin -E GOARCH=arm64 go build -o hello-darwin .

That produces a genuine Mach-O 64-bit executable arm64, which then runs on your host like any other program you built.

Caveats per language

  • Rust: ~/.local/bin/cargo is very likely a rustup shim already. boks -i cargo replaces it and rustup will not know. If you use rustup for anything, stay at level 2 here.
  • Go: boks -i go shadows any host Go, including for editor tooling (gopls and friends) that shells out to go.
  • C/C++: cmake/ninja shim cleanly; the compiler you invoke through them lives inside the image.

CI

Level 1, explicitly:

- run: boks --non-interactive --cap net go test ./...
- run: boks --non-interactive --cap rw --cap net cargo test

Read a repository's build.rs or go:generate directives before granting net on a project you don't know — build scripts run arbitrary code at build time, which is exactly the moment those capabilities are live.

Worked examples

  • go-simple — all three levels, both subcommand indices, cross-compilation, and a binary that does run standalone.
  • cargo-rust-simple — all three levels, --cap rw disappearing into a project file, and the libgcc limitation with its failing transcripts.
  • cargo-project-config — a RUST_LOG=debug transcript proving an overlay grant reaches the real podman invocation.
  • cmake-c-simple, ninja-cpp-simple — C and C++ build tools.
  • haskell-simple — GHC, same shape again.