CLI tools (rg, jq, git, curl)

Everything else in this section is about projects. This page is about the tools you reach for a hundred times a day — and it is where shims make the most sense with the least risk.

Read the three levels first if you haven't.

Why these are the best shim candidates

The level-3 caveats that matter for python or node — shadowing a version manager, breaking editor integrations, silently changing what a build script resolves — mostly don't apply here. rg is rg. Nothing else in your system depends on which rg it gets, and the containerised one has fewer powers than the host binary it replaces.

ToolDefault capabilitiesWhat that means
rgnoneReads the working directory. Cannot write. Cannot reach the network.
jqnoneSame — a pure filter.
curl, wgetnoneNo network by default. Opt in per invocation.
gitnet, rwNeeds both: clone/fetch/push talk to a remote, commit writes.

A tool with no capability line at all is the strongest default boks has: the working directory is mounted read-only and there is no network namespace to speak of.

Level 1

boks rg "TODO" .
boks rg -l "FIXME" src/

echo '{"name":"boks","ok":true}' | boks jq -r '.name'
boks jq -r '.[] | .name' data.json

boks --cap net curl -s https://api.github.com/zen
boks --cap net curl -s https://httpbin.org/ip | boks jq '.origin'

boks git log --oneline -5
boks git diff HEAD~1

Pipes work the way you expect — boks is a normal process in the pipeline, with stdin and stdout connected through.

Note the two-container pipe in that curl | jq line. curl gets network; jq does not, and does not need it. Each stage of the pipeline carries only the capability its own stage requires, which is not something a single shell with ambient permissions can express at all.

Level 3 (there is barely a level 2 here)

Ad-hoc tools rarely need project configuration — there is no dependency file and no dev server. So they tend to jump from level 1 straight to level 3:

boks -i rg
boks -i jq
boks -i git
$ rg -c "serve" main.go
7

That is a containerised ripgrep, mounted read-only, no network, invoked as if it were installed.

Exit codes are preserved

This matters more for CLI tools than anything else, because they get used in scripts and shell conditionals:

$ rg "definitely-not-in-this-file" main.go; echo $?
1

A shim reproduces the tool's own exit code exactly, so git diff --exit-code in a pre-commit hook and if rg -q pattern; then in a script both behave the way they always did.

When you do want a project overlay

The one common case is granting a tool something extra inside one project:

tools:
  curl:
    capabilities: [net]
  rg:
    capabilities: [rw]        # e.g. for `rg --files-with-matches | xargs sed -i`

Now curl inside that project needs no flag, and curl everywhere else still has no network. That is a genuinely better default than adding net to your own machine-wide boks.toml, because it is scoped, committed and reviewed.

BOKS_ARGS for one-offs

BOKS_ARGS="--cap net" curl -s https://example.com
BOKS_ARGS="--cap rw" rg --passthru -r 'new' 'old' file.txt

git deserves its own paragraph

git is the one tool here where the sandbox is doing something you should think about.

It ships with net and rw because it cannot do its job without them — which means a shimmed git is closer to your host git in power than a shimmed rg is.

Two things are handled for you:

  • ~/.gitconfig is mounted automatically, read-only, when git is actually present in the container — so your identity and aliases come along:

    $ boks git config --get user.email
    you@example.com          # the host's value

    Controlled by mount_git_config in your boks.toml, on by default. The mount is gated on git being present, so composing an unrelated tool doesn't drag a .gitconfig into a container that has no git in it.

  • SSH transport works without hand-composing. git's image ships no ssh binary — single-purpose images — but its index entry declares default_compose = ["ssh"], so boks git clone git@host:… finds one on PATH.

And one thing is not, on macOS:

  • SSH agent forwarding is Linux-host only. The code that bind-mounts $SSH_AUTH_SOCK into the container is compiled under #[cfg(target_os = "linux")], so on macOS the variable simply isn't set inside the container even with ssh composed — verified. Pushing over SSH from a macOS host therefore won't reach your agent; HTTPS with a token forwarded through pass_env is the working path there.

What git still does not get is access to anything outside the current directory, so a worktree or submodule pointing elsewhere won't be visible.

For a repository you are reading rather than pushing to, none of this matters, and boks git log/boks git diff are exactly as good as the host's.

The genuinely nice property

Running an unfamiliar tool on an unfamiliar directory is the everyday case where a sandbox pays off with no ceremony at all:

boks rg "password" .          # can read this directory, and nothing else
boks jq . suspicious.json     # cannot write, cannot phone home

No virtualenv, no --dry-run, no reading the tool's source first. The tool gets the directory you pointed it at and nothing else.

Worked examples