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.
- The toolchain —
go build,cargo build,cmake,ninja. Needs to write your working tree, and usually needs the network to fetch dependencies. - 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
| Tool | Default capabilities | Consequence |
|---|---|---|
go | rw | boks go build just works. |
cmake, ninja | rw | Same. |
cargo, rustc | none | Every 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:
| Linking | boks ./binary | |
|---|---|---|
| Go | static by default (CGO off) | works |
| Rust | dynamic against libgcc_s.so.1 | fails today — the static base ships libc6/libm, no libgcc |
| C/C++ | depends on your flags | works 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/cargois very likely a rustup shim already.boks -i cargoreplaces it and rustup will not know. If you use rustup for anything, stay at level 2 here. - Go:
boks -i goshadows any host Go, including for editor tooling (goplsand friends) that shells out togo. - C/C++:
cmake/ninjashim 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 rwdisappearing into a project file, and thelibgcclimitation with its failing transcripts.cargo-project-config— aRUST_LOG=debugtranscript 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.