Sandboxed OCI container images for AI coding agents, built reproducibly with Nix.
Consumes agent packages from llm-agents.nix and produces images usable with agent-box or standalone Podman/Docker.
llm-agents.nix (packages) -> agent-images (images) -> agent-box (orchestration)
AI coding agents need access to your filesystem to be useful, but that means they can also read secrets like SSH keys, cloud credentials, and API tokens. Running agents inside containers limits what they can see. Nix makes the images reproducible and easy to customise.
| Image | Agent | Build |
|---|---|---|
amp |
Amp | nix build .#amp |
claude-code |
Claude Code | nix build .#claude-code |
cli-proxy-api |
CLI Proxy API | nix build .#cli-proxy-api |
code |
Code | nix build .#code |
codex |
Codex CLI | nix build .#codex |
copilot-cli |
Copilot CLI | nix build .#copilot-cli |
crush |
Crush | nix build .#crush |
cursor-agent |
Cursor Agent | nix build .#cursor-agent |
droid |
Droid | nix build .#droid |
eca |
ECA | nix build .#eca |
forge |
Forge | nix build .#forge |
gemini-cli |
Gemini CLI | nix build .#gemini-cli |
goose-cli |
Goose | nix build .#goose-cli |
iflow-cli |
iFlow CLI | nix build .#iflow-cli |
jules |
Jules | nix build .#jules |
kilocode-cli |
Kilocode CLI | nix build .#kilocode-cli |
letta-code |
Letta Code | nix build .#letta-code |
mistral-vibe |
Mistral Vibe | nix build .#mistral-vibe |
nanocoder |
Nanocoder | nix build .#nanocoder |
oh-my-opencode |
Oh My OpenCode | nix build .#oh-my-opencode |
omp |
OMP | nix build .#omp |
opencode |
OpenCode | nix build .#opencode |
pi |
Pi | nix build .#pi |
qoder-cli |
Qoder CLI | nix build .#qoder-cli |
qwen-code |
Qwen Code | nix build .#qwen-code |
| Image | Agent | Build |
|---|---|---|
hermes-agent |
Hermes Agent | nix build .#hermes-agent |
localgpt |
LocalGPT | nix build .#localgpt |
openclaw |
OpenClaw | nix build .#openclaw |
picoclaw |
PicoClaw | nix build .#picoclaw |
zeroclaw |
ZeroClaw | nix build .#zeroclaw |
Each image includes a default set of base packages: git, coreutils, bash, ripgrep, findutils, grep, sed, gawk, diff, jq, tar, gzip, less, curl, which, and CA certificates. These can be overridden via the basePackages parameter (see Custom Images). By default, containers run as a non-root agent user (uid 1000) with /workspace as the working directory. The image also pre-creates standard XDG base directories under $HOME (.config, .cache, .local/share, .local/state) so mounting subpaths into them does not leave root-owned parent directories behind. Both the user and working directory can be customised (see Custom Images).
- Nix with flakes enabled
- Podman or Docker for loading and running images
All other dependencies (including agent packages from llm-agents.nix) are resolved automatically by the Nix flake. NixOS users should also follow the rootless Podman setup steps below.
macOS: Images are Linux-only. On macOS, specify the target system explicitly and ensure you have a Linux remote builder configured (e.g. via Docker Desktop or nix-darwin's linux-builder):
nix build .#packages.x86_64-linux.<agent>
# or for ARM:
nix build .#packages.aarch64-linux.<agent>Dev tooling (nix fmt, nix develop, nix flake check) works natively on macOS.
# List all available images with descriptions
nix search . ^
# Replace <agent> with any image name from the table above
nix build .#<agent>
podman load < result # or: docker load < result
podman run --rm localhost/agent-images/<agent>:latest --versionPass the API key for your chosen provider:
# Claude Code (Anthropic)
podman run --rm -it \
-v ./my-project:/workspace \
-e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
localhost/agent-images/claude-code:latest
# OpenCode (OpenRouter)
podman run --rm -it \
-v ./my-project:/workspace \
-e OPENROUTER_API_KEY="$OPENROUTER_API_KEY" \
localhost/agent-images/opencode:latest# Replace <agent> with the image name used above
podman run --rm --entrypoint sh localhost/agent-images/<agent>:latest \
-c 'whoami && echo $HOME && pwd && command -v git && command -v rg'Expected output:
agent
/home/agent
/workspace
/nix/store/.../bin/git
/nix/store/.../bin/rg
Create ~/.agent-box.toml:
workspace_dir = "~/.local/agent-box/workspaces"
base_repo_dir = "~/path/to/your/projects"
[runtime]
backend = "podman"
# Replace <agent> with any image name from the table above
image = "localhost/agent-images/<agent>:latest"
env_passthrough = ["ANTHROPIC_API_KEY", "OPENROUTER_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY"]base_repo_dir must be the real (non-symlinked) parent directory containing your git repositories. Agent-box resolves symlinks, so symlinking repos into a separate directory will not work. Add any other API keys your agent requires to env_passthrough.
Mounts the current directory as-is into the container. The agent can see all files, including untracked and gitignored files.
cd ~/projects/my-repo
ab spawn --localCreates a git worktree so the agent only sees committed/tracked files. Gitignored files (like result) are not visible.
# Create a workspace (from within the repo directory)
ab new my-repo -s my-session --git
# Spawn the container
ab spawn -s my-session --gitUse --entrypoint to override the default entrypoint and -c for arguments:
# Check agent version
ab spawn --local --entrypoint <entrypoint> -c="--version"
# Read a file
ab spawn --local --entrypoint cat -c="README.md"
# Run a shell command (note: pass each arg as a separate -c)
ab spawn --local --entrypoint sh -c="-c" -c="whoami && pwd"To verify that worktree mode hides gitignored files:
# Build an image first (creates a `result` symlink, which is gitignored)
# Replace <agent> with any image name from the table above
nix build .#<agent>
# Local mode - agent CAN see result
ab spawn --local --entrypoint ls -c="-la" -c="result"
# Output: result -> /nix/store/...
# Worktree mode - agent CANNOT see result
ab new my-repo -s sandbox-test --git
ab spawn -s sandbox-test --git --entrypoint ls -c="result"
# Output: ls: cannot access 'result': No such file or directoryNixOS requires extra configuration for rootless Podman to work with these images. Add the following to your configuration.nix:
virtualisation = {
containers.enable = true;
podman = {
enable = true;
dockerCompat = true;
};
};
users.users.<USERNAME> = {
extraGroups = [ "podman" ];
subUidRanges = [{ startUid = 100000; count = 65536; }];
subGidRanges = [{ startGid = 100000; count = 65536; }];
};Then rebuild: sudo nixos-rebuild switch
Note: The sudo commands and podman load/podman system reset commands below must be run from your own terminal. Sandboxed environments (such as AI coding agents running inside containers) cannot execute sudo or access /etc/subuid due to the "no new privileges" flag.
You also need a container trust policy. Create ~/.config/containers/policy.json:
{
"default": [{ "type": "insecureAcceptAnything" }]
}Corrupted storage after failed load. If podman load fails (e.g. because /etc/subuid was missing), Podman's storage may be corrupted. Fix with:
podman system reset --force
podman load < resultnewuidmap: Too many levels of symbolic links. This happens when /etc/subuid is a symlink (e.g. from environment.etc entries). NixOS setuid wrappers cannot follow symlinks. Remove any environment.etc entries for subuid/subgid and rely solely on subUidRanges/subGidRanges, which create real files. Rebuild and then reset Podman storage.
Use mkAgentImage to build your own agent images:
{
inputs.agent-images.url = "github:nothingnesses/agent-images";
outputs = { agent-images, nixpkgs, ... }:
let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
mkAgentImage = agent-images.lib.mkAgentImage { inherit pkgs; };
in {
packages.x86_64-linux.my-agent = mkAgentImage {
name = "my-agent";
agent = my-agent-package;
entrypoint = [ "my-agent" ];
extraPackages = [ pkgs.nodejs ];
extraEnv = { MY_VAR = "value"; };
extraDirectories = [ "~/.my-agent-cache" "/opt/my-agent-cache" ];
};
};
}By default, images include a standard set of CLI tools (bash, coreutils, git, etc.). Pass basePackages to replace them entirely:
mkAgentImage {
name = "my-minimal-agent";
agent = my-agent-package;
entrypoint = [ "my-agent" ];
basePackages = with pkgs; [ bashInteractive coreutils git cacert ];
}By default, containers run as user agent (uid/gid 1000) with /workspace as the working directory. Override these with user, uid, gid, and workingDir:
mkAgentImage {
name = "my-agent";
agent = my-agent-package;
entrypoint = [ "my-agent" ];
user = "dev";
uid = 1001;
gid = 100; # defaults to uid if omitted
workingDir = "/project";
}Setting gid independently from uid is useful for rootless Podman users whose host group (e.g. users, gid 100) differs from their uid. Without it, files created inside the container may have a gid that maps to an unexpected value on the host.
mkAgentImage always creates $HOME, the working directory, and these XDG base directories owned by the runtime user:
$HOME/.config$HOME/.cache$HOME/.local/share$HOME/.local/state
The corresponding environment variables (XDG_CONFIG_HOME, XDG_CACHE_HOME, XDG_DATA_HOME, XDG_STATE_HOME) are also set to these defaults. If you override any of them via extraEnv, the default for that variable is suppressed to avoid duplicates.
This avoids a common container-runtime footgun where mounting a subdirectory such as /home/agent/.config/git causes the missing parent to be auto-created as root:root.
If you need more writable directories owned by the runtime user, pass extraDirectories as absolute container paths or ~/... paths relative to the container user's home:
mkAgentImage {
name = "my-agent";
agent = my-agent-package;
entrypoint = [ "my-agent" ];
extraDirectories = [
"~/.my-agent-cache"
"/opt/my-agent/state"
];
}Only the listed paths are chowned to the runtime user. Intermediate parent directories created by mkdir -p for paths outside $HOME remain root-owned. If you need writable intermediates, list them explicitly in extraDirectories.
To use a non-standard XDG path, combine extraDirectories (to create the directory) with extraEnv (to set the environment variable):
mkAgentImage {
name = "my-agent";
agent = my-agent-package;
entrypoint = [ "my-agent" ];
extraDirectories = [ "~/.custom-config" ];
extraEnv = { XDG_CONFIG_HOME = "/home/agent/.custom-config"; };
}XDG_RUNTIME_DIR is intentionally excluded. It is managed by pam_systemd, requires a tmpfs with strict lifecycle semantics, and cannot be meaningfully pre-created in a container image.
extraDirectories entries are validated at build time. Paths must be absolute (or use ~/), may only contain alphanumeric characters, /, _, ., +, @, and -, and must not contain .. components. System paths (/etc, /bin, /usr, /lib, /sbin, /dev, /proc, /sys, /run, /tmp, /nix, /var, /root) are rejected to prevent accidental ownership changes to critical directories.
By default, the Nix CLI is not included in images. Set withNix = true to enable Nix workflows inside the container:
mkAgentImage {
name = "my-agent";
agent = my-agent-package;
entrypoint = [ "my-agent" ];
withNix = true;
};This configures single-user Nix with nix-command and flakes experimental features enabled. Inside the container, you can run:
nix --version
nix develop
nix build
nix shell nixpkgs#hello -c hello
nix-shell -p ripgrep --command "rg --version"The Nix CLI version defaults to whatever the flake's nixpkgs pins. Pass nixPackage to use a different version:
mkAgentImage {
name = "my-agent";
agent = my-agent-package;
entrypoint = [ "my-agent" ];
withNix = true;
nixPackage = my-custom-nix;
};The default experimental features are nix-command and flakes. Override with nixExperimentalFeatures:
mkAgentImage {
name = "my-agent";
agent = my-agent-package;
entrypoint = [ "my-agent" ];
withNix = true;
nixExperimentalFeatures = [ "nix-command" "flakes" "pipe-operators" ];
};nix-ld helps run foreign dynamically linked binaries that expect a conventional system loader path such as /lib64/ld-linux-x86-64.so.2.
Enable it with withNixLd = true:
mkAgentImage {
name = "my-agent";
agent = my-agent-package;
entrypoint = [ "my-agent" ];
withNixLd = true;
};This adds pkgs.nix-ld, creates the architecture-appropriate dynamic linker symlink inside the image, and sets NIX_LD/NIX_LD_LIBRARY_PATH automatically.
By default, NIX_LD points to the system dynamic linker and NIX_LD_LIBRARY_PATH includes a default set of common libraries mirrored from the upstream NixOS programs.nix-ld module (including glibc, openssl, zlib, curl, systemd, and others). Most foreign binaries will work out of the box without any additional configuration.
The default set mirrors upstream NixOS, which includes systemd and its transitive dependencies. This ensures broad compatibility but adds significant closure size.
If your foreign binary needs additional shared libraries beyond the defaults, use extraNixLdLibraries:
mkAgentImage {
name = "my-agent";
agent = my-agent-package;
entrypoint = [ "my-agent" ];
withNixLd = true;
extraNixLdLibraries = with pkgs; [ SDL2 libGL ];
};To replace the entire default library set (for example, to minimize image size), pass nixLdLibraries. This replaces the defaults rather than extending them, so your foreign binaries must be able to find all their dependencies in the list you provide:
mkAgentImage {
name = "my-agent";
agent = my-agent-package;
entrypoint = [ "my-agent" ];
withNixLd = true;
nixLdLibraries = with pkgs; [ zlib openssl ];
};withNixLd is independent from withNix; you can enable either one or both depending on whether you need the Nix CLI, nix-ld, or both.
direnv is not included by default but can be added via extraPackages:
mkAgentImage {
name = "my-agent";
agent = my-agent-package;
entrypoint = [ "my-agent" ];
withNix = true;
extraPackages = [ pkgs.direnv pkgs.nix-direnv ];
};You will also need to wire up the shell hook. Add an extraEnv entry or configure .bashrc in the container's home directory to run eval "$(direnv hook bash)".
- No build sandbox: Nix builds inside the container run with
sandbox = falsebecause container runtimes typically restrict namespace creation. Builds are not hermetic - a derivation that succeeds in the container may fail in a sandboxed environment. If your container runs with elevated privileges, you can override this by mounting a customnix.confwithsandbox = relaxedorsandbox = true. - Image size: Enabling
withNixand/orwithNixLdadds extra runtime components to the image.withNixadds roughly 80 MB andwithNixLdwith the default library set adds roughly 40 MB (both vary with the nixpkgs pin). A customnixLdLibrarieswith fewer packages will be smaller. - Rootless Podman UID remapping: Rootless Podman remaps UIDs by default, which can cause permission errors when writing to
/nix/store,/tmp, or$HOMEinside the container. If you encounter these errors, pass--userns=keep-idto map your host UID directly into the container. Docker and rootful Podman do not have this issue.podman run --rm -it \ --userns=keep-id \ -v ./my-project:/workspace \ -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \ localhost/agent-images/claude-code:latest
If the host machine has Nix installed, you can bind-mount the host store read-only to avoid duplicating store paths:
podman run --rm -it \
--mount type=bind,src=/nix/store,dst=/nix/store,ro \
-v ./my-project:/workspace \
localhost/agent-images/my-agent:latestThis is useful for reducing disk usage but couples the container to the host's Nix installation.
All development commands are available via just. Enter the dev shell with nix develop to get just and all other tools on your PATH.
Tests are Linux-only (they build and run container images). On macOS, specify the system explicitly, e.g. nix run .#apps.x86_64-linux.test.
just test default # default image (opencode)
AGENT=codex just test default # or specify any agent
just test nix # basic Nix checks (offline)
just test nix-install # runtime install + nix develop (requires network)
just test nix-custom # custom user/uid/gid, experimental features, extraEnv, nix-ld (with Nix)
just test nix-ld # nix-ld without Nix CLI (standalone nix-ld)
just test nix-ld-minimal # nix-ld with custom minimal library set
just test minimal # minimal basePackages (bash, coreutils, cacert only)
just test custom # custom user/uid/gid/workingDir, extraPackages, extraEnv (without Nix)
just test nix-userns # Nix with --userns=keep-id (Podman only, skipped under Docker)
just test-all # run all of the abovejust fmt # format all files (Nix, shell, YAML, Markdown)
just fmt -- --ci # check without modifying (used in CI)Pre-commit hooks are set up automatically when entering the dev shell (nix develop). They run nix fmt, deadnix, and actionlint on staged files before each commit. Shellcheck runs as a pre-push hook on .bats, .bash, and .sh files.
The SHA of any bulk formatting commit should be added to .git-blame-ignore-revs and configured locally with:
git config blame.ignoreRevsFile .git-blame-ignore-revsjust lint # run all linters (shellcheck, deadnix, actionlint)
just shellcheck # run shellcheck across all test files
just deadnix # find unused bindings in Nix files
just actionlint # validate GitHub Actions workflow filesRun format check, all linters, and all tests in sequence:
just verifyThis project is licensed under the Blue Oak Model License 1.0.0.