A self-contained AI coding-agent sandbox, packaged as a standalone Nix flake. It ships a full dev toolchain in an OCI container and runs Claude Code, Codex, and OpenCode inside it, isolated from the host.
agentboxImage— a Nix-built OCI image (no Dockerfile, no Homebrew).agentbox— a host-side CLI to drive the container (status,shell,logs,exec,start/stop/restart).nixosModules.agentbox/darwinModules.agentbox— run it as a systemdoci-containersservice (NixOS) or via manual management (macOS).
It depends on nothing but nixpkgs. No private inputs, no bundled
third-party agents — just the sandbox plus Claude Code and Codex.
| Agent | How it gets into the box | Toggle (default) |
|---|---|---|
| Codex | bundled in the image from nixpkgs (codex) |
settings.enableCodex (false) |
| OpenCode | bundled in the image from nixpkgs (opencode) |
settings.enableOpencode (true) |
| Claude Code | installed at container start by Anthropic's native installer (self-updates) | settings.enableClaudeCode (false) + claudeCodeVersion |
All three are first-class: Codex and OpenCode are baked into the image from
nixpkgs; Claude Code is runtime-installed so its own updater keeps it current.
Each toggle sets the matching ENABLE_* env the entrypoint reads. Provide
credentials through the secret environmentFile (e.g. OPENAI_API_KEY for
Codex, CLAUDE_CODE_OAUTH_TOKEN for Claude Code).
services.agentbox = {
enable = true;
environmentFile = config.sops.templates."agentbox.env".path;
settings = {
enableClaudeCode = true;
enableCodex = true;
enableOpencode = true; # on by default
};
};| Group | Packages |
|---|---|
| Dev tools | git, neovim (pre-configured), tmux, htop, tree, ripgrep, fd, fzf, jq, yq-go, curl, wget, unzip, gnumake, pkg-config, gcc, nix |
| Languages | go, nodejs 22, bun, python 3.12, uv |
| AI CLIs | codex, opencode (Claude Code is runtime-installed) |
| Language servers | gopls, nil, typescript-language-server, vscode-langservers-extracted |
| Formatters | nixfmt (RFC), prettier |
| Cloud CLIs | awscli2, kubectl, kubernetes-helm, google-cloud-sdk (+gke-gcloud-auth-plugin), docker-client |
| VCS / scanning | gh, gitleaks, openssh |
Base core/system utilities are tagged REQUIRED vs CONVENIENCE inline in
image.nix — readline and expect were dropped from the base as unneeded.
The image has no apt/dpkg (it isn't Debian). Two knobs cover the usual "let me just install a thing" workflow, both on by default:
-
settings.hardening.enableSudo— the entrypoint stages a setuid-rootsudounder/run/wrappers/binat boot (Nix store paths can't carry setuid bits, and there's nosecurity.wrappersinside the container, so plain/bin/sudocan't elevate).agentgets passwordless sudo. Turning onhardening.noNewPrivilegesdisables it at the kernel level regardless. -
settings.enableNix— bakes thenixCLI in, registers the store DB (buildLayeredImageWithNixDb), and starts a rootnix-daemonat boot so the unprivileged agent can install throwaway packages:nix profile install nixpkgs#ripgrep # persists for the container's life nix shell nixpkgs#hello -c hello # ephemeral, one command
These land in the container's writable layer and are lost on recreate — bake anything permanent into the image instead (
extraPackages, or the package lists inimage.nix). Language-level installs (bun add -g,go install,uv tool install,cargo install) work without either knob.
nix build .#agentboxImage # -> ./result (a docker image tarball)
docker load < result # loads agentbox:latest.github/workflows/build-image.yml builds the image with Nix on every push and
PR, and on pushes to main / v* tags publishes it to GHCR
(ghcr.io/<owner>/agentbox:latest and :<sha> / :<tag>). Building needs no
secrets (nixpkgs-only); only the push uses the built-in GITHUB_TOKEN.
# flake.nix (consumer)
{
inputs.agentbox.url = "github:<you>/agentbox"; # or path:./agentbox
outputs = { self, nixpkgs, agentbox, ... }: {
nixosConfigurations.host = nixpkgs.lib.nixosSystem {
modules = [
{ nixpkgs.overlays = [ agentbox.overlays.default ]; }
agentbox.nixosModules.agentbox
{
services.agentbox = {
enable = true;
user = "you";
settings.enableCodex = true;
# image/package default to pkgs.agentboxImage / pkgs.agentbox
};
}
];
};
};
}For macOS swap nixosModules → darwinModules and use darwin.lib.darwinSystem
(set image to a Linux-built image copied onto the Mac).
Both platform modules share one option schema (modules/common-options.nix,
covering hardening, enableDocker, enableNotification, cloud creds, the
dockerProxy, autoCloneRepos, …). Each platform module adds only its
OS-specific top-level options and config body (systemd vs launchd/manual).
Agentbox can expose an opt-in request inbox for a separate host collector. It does not upload transcripts and never receives object-storage credentials.
services.agentbox.settings.historyArchive = {
enable = true;
hostId = "home-macbook";
};This creates only <dataDir>/history-sync/requests and mounts that directory at
/home/agent/.agent-history/requests. The rest of a collector's spool should be
host-only and must not be mounted into Agentbox.
Explicitly invoke /archive-conversation in Claude Code or OpenCode. The command
creates a bounded intent; the next Claude Stop or terminal OpenCode idle event
resolves it with the native session ID. The request timestamp is the content
cutoff, so delayed resolution must not include later messages. Archiving more of
the session requires another explicit request. Codex support is intentionally
deferred until its skill invocation can be bound to a native thread ID before
the completion callback.
Resolved files contain identifiers, timestamps, event type, and local source context, but no message content. They are untrusted input: a host collector must still validate repository remotes and allowlists before reading or uploading a transcript. The request is an opt-in user interface, not an authentication boundary: code running inside Agentbox can forge or delete inbox files, just as it can alter its local transcript. A collector must derive host and trust-domain identity from host configuration rather than trusting request fields.
Beyond the three bundled agents, this standalone ships no others — and there is no agent-specific code in the image. To add your own you have four generic hooks, none of which require forking:
| Hook | Purpose |
|---|---|
agentboxImage.override { extraPackages = [ … ]; } |
bake the binary into the image |
services.agentbox.extraEnvironment |
pass its config/env vars |
services.agentbox.extraVolumes |
mount its data |
services.agentbox.extraActivation |
stage files on the host at activation |
services.agentbox.bootScripts |
run it at container boot (as the agent, backgrounded) |
bootScripts is what lets a daemon-style agent actually start — the entrypoint
launches each snippet in the background and keeps the container alive while it
runs (it also honors a single AGENTBOX_BOOT_CMD env var, or any executables
mounted into ~/.agentbox/boot.d).
nixpkgs.overlays = [
agentbox.overlays.default
(final: prev: {
agentboxImage = prev.agentboxImage.override { extraPackages = [ myAgent ]; };
})
];
services.agentbox = {
extraEnvironment.MY_AGENT_PORT = "1234";
bootScripts.my-agent = ''
exec my-agent serve --port "$MY_AGENT_PORT"
'';
};The image itself stays agent-agnostic; everything specific to your agent lives in your own config.