Skip to content

nothingnesses/agent-images

Repository files navigation

agent-images

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)

Why

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.

Available Images

AI Coding Agents

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

AI Assistants

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).

Requirements

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.

Quick Start

# 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 --version

Standalone Usage

Pass 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

Verifying Container Internals

# 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

Usage with agent-box

Global Configuration

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.

Local Mode

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 --local

Worktree Mode (Sandboxed)

Creates 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 --git

Running One-Off Commands

Use --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"

Sandbox Verification

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 directory

NixOS: Rootless Podman Setup

NixOS 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" }]
}

Troubleshooting

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 < result

newuidmap: 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.

Custom Images

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" ];
      };
    };
}

Overriding Base Packages

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 ];
}

Customising User and Working Directory

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.

XDG Base Directories and Extra Writable Paths

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.

Using Nix Inside Containers

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"

Overriding the Nix 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;
};

Customising Experimental Features

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" ];
};

Adding direnv

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)".

Known Limitations

  • No build sandbox: Nix builds inside the container run with sandbox = false because 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 custom nix.conf with sandbox = relaxed or sandbox = true.
  • Image size: Enabling withNix adds the Nix CLI and its dependencies to the image. Expect roughly 50-150 MB of additional size depending on the nixpkgs pin.
  • Rootless Podman UID remapping: Rootless Podman remaps UIDs by default, which can cause permission errors when writing to /nix/store, /tmp, or $HOME inside the container. If you encounter these errors, pass --userns=keep-id to 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

Host Store Mount Optimisation

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:latest

This is useful for reducing disk usage but couples the container to the host's Nix installation.

Development

Tests

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.

nix run .#test-default                    # default image (opencode)
AGENT=codex nix run .#test-default        # or specify any agent
nix run .#test-nix                        # basic Nix checks (offline)
nix run .#test-nix-install                # runtime install + nix develop (requires network)
nix run .#test-nix-custom                 # custom user/uid/gid, experimental features, extraEnv (with Nix)
nix run .#test-custom                     # custom user/uid/gid/workingDir, extraPackages, extraEnv (without Nix)
nix run .#test-nix-userns                 # Nix with --userns=keep-id (Podman only, skipped under Docker)
nix run .#test                            # run all of the above

Formatting

nix fmt        # format all files (Nix, shell, YAML, Markdown)
nix 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 on staged files before each commit.

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-revs

Linting

nix run .#lint        # run all linters
nix run .#shellcheck  # run shellcheck across all test files
nix run .#deadnix     # find unused bindings in Nix files
nix run .#actionlint  # validate GitHub Actions workflow files

License

This project is licensed under the Blue Oak Model License 1.0.0.

About

Sandboxed OCI container images for AI coding agents, built reproducibly with Nix.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors