Run AI coding agents in sandboxed Docker containers with full permissions (--dangerously-skip-permissions or equivalent) without risking your host system.
Agent Box manages Git/Jujutsu workspaces and spawns isolated containers where agents can freely execute commands, modify files, and install packages - all contained within a disposable environment.
AI coding agents like Claude Code, Cursor, and others work best when given full autonomy - but running --dangerously-skip-permissions on your host machine is risky. Agents can execute arbitrary commands, install packages, modify system files, or accidentally rm -rf something important.
Agent Box solves this by:
- Sandboxing: Agents run in Docker containers with no access to your host system
- Disposable workspaces: Each session gets a fresh Git worktree or JJ workspace that can be thrown away
- Shared Nix store: Optionally share your host's Nix store for fast, reproducible tooling without rebuilding inside containers
- Easy iteration: Spawn containers, let agents go wild, review changes, discard or keep - repeat
cargo install --path .Create ~/.agent-box.toml:
workspace_dir = "~/workspaces" # Where git worktrees and jj workspaces are created
base_repo_dir = "~/repos" # Base directory for your repos (colocated jj/git repos)
context = "Optional context string available in containers"
context_path = "/tmp/context" # Where context is mounted (default: /tmp/context; try: ~/.pi/agent/APPEND_SYSTEM.md for pi)
[runtime]
backend = "docker" # Container runtime: "docker" or "podman" (default: docker)
image = "agent-box:latest"
entrypoint = "/bin/bash" # Shell-style command string (supports quotes for args with spaces)
skip_mounts = ["/nix/store/*", "/nix/var/nix"] # Glob patterns for paths to always skip
env_passthrough = ["PATH", "TERM"] # Environment variables to pass through from host
ports = ["8080:8080"] # Port mappings (Docker -p syntax)
hosts = ["host.docker.internal:host-gateway"] # Custom /etc/hosts entries
[runtime.mounts.ro]
absolute = ["/nix/store"]
home_relative = ["~/.config/git"]
[runtime.mounts.rw]
absolute = []
home_relative = ["~/.local/share"]
[runtime.mounts.o] # Overlay mounts (Podman only)
absolute = []
home_relative = []All paths support ~ expansion and will be canonicalized.
Agent Box supports layered configuration. It loads config files in the following order:
~/.agent-box.toml(global config, required)<git_root>/.agent-box.toml(repo-local config, optional)
Merge behavior:
- Scalar values (strings, numbers, booleans, including
entrypoint): repo-local overrides global - Arrays (
env, mount paths): repo-local values are appended to global values - Nested objects: merged recursively
This allows you to define global defaults in ~/.agent-box.toml and override or extend them per-repository.
Example:
Global config (~/.agent-box.toml):
workspace_dir = "~/workspaces"
base_repo_dir = "~/repos"
[runtime]
image = "default-agent:latest"
env = ["EDITOR=nvim"]
[runtime.mounts.ro]
home_relative = ["~/.config/git"]Repo-local config (~/repos/myproject/.agent-box.toml):
[runtime]
image = "myproject-agent:latest" # overrides global
entrypoint = '/bin/bash -c "nix develop"' # overrides global (shell-style parsing)
env = ["PROJECT=myproject"] # appended: ["EDITOR=nvim", "PROJECT=myproject"]
[runtime.mounts.ro]
home_relative = ["~/.ssh"] # appended to global mountsenv_passthrough allows you to pass environment variables from the host to the container. Instead of hard-coding values like env = ["PATH=/usr/bin"], you can specify variable names to be read from the host environment:
[runtime]
env_passthrough = ["PATH", "SSH_AUTH_SOCK", "TERM"]When spawning a container, Agent Box will:
- Read each variable's value from the host environment
- Pass it to the container as
VAR_NAME=value - Warn if a variable is not set in the host environment (but continue)
This is useful for:
- Passing dynamic values (like
SSH_AUTH_SOCKorGPG_AGENT_INFO) - Sharing the host's
PATHwithout hard-coding - Terminal settings (
TERM,COLORTERM)
Layering: Like env, env_passthrough arrays are concatenated across global config, repo-local config, and profiles.
Example:
# Global config
[runtime]
env_passthrough = ["PATH", "USER"]
# Profile
[profiles.dev]
env_passthrough = ["SSH_AUTH_SOCK", "GPG_TTY"]Using -p dev would passthrough: PATH, USER, SSH_AUTH_SOCK, and GPG_TTY.
The context field allows you to provide textual context that is made available to containers. This is useful for passing information to AI coding agents or other tools running inside containers.
How it works:
- Define
contextas a string at the root level and/or in profiles - When spawning a container, all context strings are collected in order:
- Root-level
context(if set) - Each applied profile's
context(following profile resolution order)
- Root-level
- Context strings are joined with newlines (
\n) and written to a temporary file - The file is mounted read-write inside the container (default:
/tmp/context)
Configuration:
# Optional: customize the mount path (defaults to /tmp/context)
# Example: mount as pi's APPEND_SYSTEM.md so context is automatically included
context_path = "~/.pi/agent/APPEND_SYSTEM.md"
# Or use a custom path
context_path = "~/.context"
# Absolute paths also work
context_path = "/tmp/context"# Root-level context - always included
context = """
This is a Rust project using workspace layout with multiple crates.
- Always run `cargo fmt` before committing
- Use `cargo clippy -- -D warnings` to check for issues
- Tests must pass with `cargo test --all-features`
- Follow the error handling patterns in src/error.rs
- New features require documentation in README.md and doc comments
"""
[profiles.rust]
context = """
Rust development environment:
- Use `nix develop` for consistent toolchain (rustc 1.75.0)
- Run `just check` before pushing (runs fmt, clippy, test)
- Prefer `eyre::Result` over `std::result::Result` for error handling
- Use `tracing` for logging, not println!
"""
[profiles.web-api]
extends = ["rust"]
context = """
Web API guidelines:
- All endpoints must have OpenAPI documentation
- Use the middleware stack defined in src/middleware.rs
- Rate limiting is configured per-endpoint in config.toml
- Authentication uses JWT tokens validated in auth middleware
- Database migrations go in migrations/ and use sqlx
"""Example usage:
# Spawn with web-api profile
ab spawn -s my-session -p web-api
# Inside the container, the context file (default: /tmp/context) contains all three context strings:
# (root context about Rust project conventions)
# (rust profile context about dev environment)
# (web-api profile context about API guidelines)Viewing context:
You can preview the resolved context before spawning:
ab dbg resolve -p devThis will show the Context section with all merged context strings.
Real-world example:
For a project with specific architecture and conventions, you might define:
context = """
This is a web service built with axum and sqlx. Architecture:
- src/main.rs: Application entry point and server setup
- src/routes/: HTTP route handlers (one file per resource)
- src/models/: Database models using sqlx
- src/services/: Business logic layer
- migrations/: SQL migrations managed by sqlx
Development workflow:
1. Create feature branch from main
2. Make changes and add tests
3. Run `just check` (runs fmt, clippy, tests)
4. Create PR and wait for CI
Code standards:
- All public functions need doc comments
- Use Result<T, ApiError> for error handling
- Database queries go in src/models/, not in handlers
- Follow RESTful conventions for API design
- Integration tests in tests/ should cover happy path and error cases
"""When an AI agent spawns with this context, it can read the context file (default: /tmp/context) to understand the project structure, workflow, and standards, allowing it to make better decisions about code organization and testing.
Notes:
- Context strings are not environment variables - they're written to a file
- If no context is defined (empty strings), no file is created and no mount is added
- The context file is temporary and cleaned up after the container exits
Tip for pi users: Set context_path = "~/.pi/agent/APPEND_SYSTEM.md" to automatically provide context to pi without needing to pass it via CLI. Pi will automatically read and include this file in its system instructions.
- Context is particularly useful for providing instructions or metadata to AI coding agents
- Use multi-line strings (""") for readable formatting
Config-defined mount paths must be absolute (/...) or home-relative (~/...).
For CLI mounts (-m/-M), relative host source paths are also accepted and resolved against the current working directory.
absolute vs home_relative:
The key difference is how single-path mounts (without explicit : mapping) handle the container path:
-
absolute: Same path on both sides
~/.config/git→/home/hostuser/.config/git:/home/hostuser/.config/git -
home_relative: Host's home prefix is replaced with container's home
~/.config/git→/home/hostuser/.config/git:/home/containeruser/.config/git
Explicit source:dest mapping:
Both support explicit mappings where ~ expands to host home for source, container home for dest:
[runtime.mounts.rw]
# Map host socket to container's ~/.gnupg/S.gpg-agent
home_relative = ["/run/user/1000/gnupg/S.gpg-agent:~/.gnupg/S.gpg-agent"]Glob expansion:
Mount paths support glob patterns (*, ?, [...]). Each matching path is mounted individually with the same mode and path derivation rules:
[runtime.mounts.rw]
# Mounts every /tmp/kitty-* directory that exists at spawn time
absolute = ["/tmp/kitty-*"]
# Each match under ~/.config/ is home-translated independently
home_relative = ["~/.config/sock-*"]home_relativetranslation applies per expanded match- Globs are not supported with explicit
src:dstmappings (ambiguous: N sources, 1 dest) - Zero matches are silently skipped, same as a non-existent literal path
- When using
-m/-Mon the CLI, quote the glob so the shell doesn't expand it first:-M '/tmp/kitty-*'
Examples:
[runtime.mounts.ro]
# Same path on both sides (stays /nix/store:/nix/store)
absolute = ["/nix/store"]
# Host ~/.config/git -> container ~/.config/git (home translated)
home_relative = ["~/.config/git"]
[runtime.mounts.rw]
# Explicit mapping with different paths
absolute = ["/host/path:/container/path"]Symlink Handling:
When a mount path contains symlinks, Agent Box automatically mounts the entire symlink chain so that paths resolve identically inside the container:
# If you have: ~/mylink -> /tmp/intermediate -> /data/actual
# Agent Box mounts all three:
# ~/mylink:/home/user/mylink:rw
# /tmp/intermediate:/tmp/intermediate:rw
# /data/actual:/data/actual:rwThis ensures symlinks work correctly in the container.
Path Coverage & Deduplication:
Agent Box automatically deduplicates mounts and skips redundant paths:
- Paths are deduplicated by their canonical (resolved) path
- Subpaths under already-mounted directories are skipped
- Example: If
/nix/storeis mounted,/nix/store/packageis redundant
Non-Existent Path Filtering:
Agent Box automatically filters out mount paths that don't exist on the host:
- Paths are checked for existence before being added to the container command
- Non-existent paths are silently filtered out with a debug message
- This prevents container spawn failures due to missing host paths
- Example: If
~/.config/nvimis in your profile but doesn't exist, it's skipped
To see filtered paths, look for DEBUG: Filtering out non-existent mount: messages when spawning containers.
Mount Coverage:
When a mount path is under an already-mounted parent directory, it is automatically skipped (unless --no-skip is used):
- Mounts are checked against existing mounts to avoid redundancy
- If a path is already covered by a parent mount, the child mount is skipped
- This applies to all mode combinations (ro/rw/overlay)
- Use
--no-skipflag to disable this behavior and mount all paths explicitly
Example:
[runtime.mounts.ro]
absolute = ["/nix/store"]
[profiles.dev.mounts.rw]
absolute = ["/nix/store/mydata"] # Skipped - already covered by parent /nix/storeSkipping Special Paths:
Agent Box can be configured to always skip certain "special" paths that should never be mounted into containers:
- Patterns in
runtime.skip_mountsare always skipped, even if explicitly configured - Supports glob patterns with
*wildcards (e.g.,/nix/store/*to skip all subdirectories) - This is useful for large system directories like
/nix/storeon NixOS - Skip patterns are checked before mount coverage and deduplication
- The
--no-skipflag does NOT affect skip_mounts - special paths are always skipped
Glob Pattern Support:
The skip_mounts option supports standard glob patterns with * wildcards (using the glob crate):
/nix/store/*- Skip/nix/store/followed by exactly one path segment (e.g.,/nix/store/package)/nix/store/**- Skip everything under/nix/storeincluding nested paths/tmp/test-*- Skip paths starting withtest-in/tmp/*/*/temp- Skip paths two levels deep ending intemp- Exact paths like
/nix/var/nix- Match that exact path only (not subdirectories unless using**)
Note: Glob patterns are matched against the full path string. For recursive matching (including all subdirectories), use ** or ensure your pattern covers all cases.
Default skip patterns (on NixOS systems):
[runtime]
skip_mounts = ["/nix/store/**", "/nix/var/nix"]To override the defaults, set an empty array or specify your own patterns:
[runtime]
skip_mounts = [] # Don't skip any special paths
# Or specify your own patterns:
skip_mounts = ["/nix/store/**", "/var/lib/**", "/usr/lib/**"]
# Skip specific subdirectories:
skip_mounts = ["/nix/store/glibc-*", "/nix/store/rustc-*"]This is particularly useful when:
- You have large read-only system directories that shouldn't be mounted
- Symlink chains resolve to system paths you want to avoid mounting
- You want to reduce container startup time by avoiding large directory mounts
- You want to skip specific subdirectory patterns (like all glibc packages) while still allowing others
Port mappings expose container ports to the host, using the same layered configuration system as mounts and environment variables.
Configuration:
[runtime]
ports = ["8080:8080", "3000:3000"]
[profiles.dev]
ports = ["9000:9000"]Format:
Port specs follow Docker's -p syntax:
CONTAINER_PORT— Expose a container port on a random host portHOST_PORT:CONTAINER_PORT— Map a specific host port to a container portHOST_IP:HOST_PORT:CONTAINER_PORT— Bind to a specific host IPHOST_PORT-END:CONTAINER_PORT-END— Port ranges
CLI:
# Expose port 8080 on host and container
ab spawn -s my-session -P 8080:8080
# Multiple ports
ab spawn -s my-session -P 8080:8080 -P 3000:3000
# Combine with profiles (profile ports + CLI ports are merged)
ab spawn -s my-session -p dev -P 9000:9000Resolution order:
runtime.ports(always applied first)default_profileports (if set)- CLI profiles (
-p) ports in the order specified - CLI ports (
-P) applied last
Duplicate port specs (exact string match) are automatically deduplicated — if the same spec appears in multiple profiles or on the CLI, only the first occurrence is kept.
Custom host-to-IP mappings are added to /etc/hosts inside the container via Docker/Podman's --add-host flag. They use the same layered configuration system as mounts, env, and ports.
Configuration:
[runtime]
hosts = ["host.docker.internal:host-gateway"]
[profiles.dev]
hosts = ["myservice:192.168.1.10", "db.local:10.0.0.5"]Format:
Each entry is HOST:IP:
myhost:192.168.1.1— resolvemyhostto192.168.1.1inside the containerhost.docker.internal:host-gateway— specialhost-gatewayvalue resolves to the host machine's IP (supported by Docker 20.10+ and Podman)
CLI:
# Add a single host entry
ab spawn -s my-session -H myhost:192.168.1.1
# Add multiple entries
ab spawn -s my-session -H myhost:10.0.0.1 -H db.local:10.0.0.5
# Combine with profiles
ab spawn -s my-session -p dev -H extra.local:172.16.0.1Resolution order:
runtime.hosts(always applied first)default_profilehosts (if set)- CLI profiles (
-p) hosts in the order specified - CLI hosts (
-H) applied last
Duplicate host entries (exact string match) are automatically deduplicated — if the same HOST:IP pair appears in multiple profiles or on the CLI, only the first occurrence is kept.
The container's network mode can be overridden at spawn time with --network=<MODE>. The value is passed directly as --network to the underlying container runtime (Docker or Podman), so any mode they support is valid:
| Mode | Description |
|---|---|
host |
Share the host's network namespace — no port mapping needed |
bridge |
Default isolated bridge network (Docker's default) |
none |
No networking at all |
<name> |
Join a named Docker/Podman network |
CLI:
# Host networking — container sees all host ports and interfaces directly
ab spawn -s my-session --network=host
# Explicitly use the default bridge network
ab spawn -s my-session --network=bridge
# No network access
ab spawn -s my-session --network=noneNote:
--network=hostand-P(port mappings) /-H(host entries) are mutually exclusive on Docker — Docker will return an error if both are supplied. Agent Box does not enforce this itself; the error comes from the container runtime.
Agent Box supports two container runtimes:
- Docker (default): Set
backend = "docker"or omit thebackendkey - Podman: Set
backend = "podman"
Differences:
- Podman uses
--userns keep-idfor better user namespace mapping - Podman supports overlay mounts (
mounts.o) with the:Oflag - Docker uses direct
--usermapping and does not support overlay mounts
Overlay mounts allow containers to write to a mounted directory without affecting the host. Changes are stored in a temporary overlay layer that is discarded when the container exits.
Profiles let you define named sets of mounts, environment variables, context, and passthrough variables that can be selectively applied when spawning containers. This enables modular, reusable configurations for different toolchains.
Basic profile definition:
# Default profile applied to all spawn commands (optional)
default_profile = "base"
[profiles.base]
env = ["EDITOR=nvim"]
context = """
Base development practices:
- Commit messages follow Conventional Commits (feat:, fix:, docs:, etc.)
- Never commit directly to main - use feature branches
- Keep commits focused and atomic
"""
[profiles.base.mounts.ro]
absolute = ["/nix/store"]
home_relative = ["~/.config/git"]
[profiles.git]
extends = ["base"] # Inherits mounts, env, and context from base
env = ["GIT_AUTHOR_NAME=You"]
context = """
Git workflow:
- Rebase instead of merge when updating branches
- Squash fixup commits before PR
- Sign commits with GPG when available
"""
[profiles.git.mounts.ro]
home_relative = ["~/.gitconfig"]
[profiles.rust]
env = ["CARGO_HOME=/home/user/.cargo"]
context = """
Rust coding standards:
- Run `cargo clippy` and fix all warnings
- Use #[must_use] on functions that return Result
- Prefer borrowing over cloning unless necessary
- Document all public APIs with doc comments
"""
[profiles.rust.mounts.ro]
home_relative = ["~/.cargo/config.toml"]
[profiles.rust.mounts.rw]
home_relative = ["~/.cargo/registry"]
[profiles.dev]
extends = ["rust"]
context = """
Local development:
- Use `cargo watch -x check -x test` for fast feedback
- Debug builds are in target/debug
- Set RUST_LOG=debug for verbose logging
"""
ports = ["8080:8080", "3000:3000"] # Port mappings (inherited by children)
hosts = ["host.docker.internal:host-gateway"] # Host entries (inherited by children)
[profiles.gpg]
context = """
GPG signing:
- GPG agent socket is forwarded from host
- Use `git config commit.gpgsign true` to sign commits
- Test with `echo test | gpg --clearsign`
"""
[profiles.gpg.mounts.o] # Overlay (Podman only)
home_relative = ["~/.gnupg"]
[profiles.gpg.mounts.rw]
home_relative = [
"/run/user/1000/gnupg/S.gpg-agent:~/.gnupg/S.gpg-agent",
]Using profiles:
# Use only default_profile (if set)
ab spawn -s my-session
# Add specific profiles on top of default
ab spawn -s my-session -p git
# Combine multiple profiles
ab spawn -s my-session -p git -p rust -p gpg
# Profiles + additional CLI mounts, ports, and host entries
ab spawn -s my-session -p rust -m ~/my-data -P 8080:8080 -H myhost:10.0.0.1Profile inheritance with extends:
Profiles can inherit from other profiles using the extends field. Parent profiles are resolved depth-first, in order:
[profiles.base]
env = ["EDITOR=nvim"]
context = """
Code quality requirements:
- All code must pass CI checks before merging
- Use conventional commits for all changes
"""
[profiles.git]
extends = ["base"]
env = ["GIT_PAGER=less -R"]
context = """
Git workflow:
- Create feature branches from main
- Keep commits focused and well-described
- Rebase to keep history clean
"""
[profiles.dev]
extends = ["git"] # Inherits from git, which inherits from base
env = ["RUST_BACKTRACE=1"]
context = """
Development environment:
- Use `cargo watch` for automatic recompilation
- Run tests frequently with `cargo test`
- Check performance with `cargo bench` when optimizing
"""Using -p dev results in all three context strings being written to the context file (default: /tmp/context), providing the agent with:
- General code quality requirements (from base)
- Git workflow guidelines (from git)
- Development environment tips (from dev)
Resolution order:
- Root-level
contextandruntime.mountsandruntime.env(always applied first) default_profile(if set)- CLI profiles (
-p) in the order specified - CLI mounts (
-m,-M) applied last
Arrays (mounts, env, ports, hosts, context) are concatenated. Context strings from the root level and each profile are collected in order and written to the context file (configured via context_path, default: /tmp/context). Duplicate mount paths, port specs, and host entries (exact string match) are automatically deduplicated - if the same spec appears in multiple profiles, only the first occurrence is kept. Circular dependencies are detected and reported as errors.
Profiles with layered configuration:
Profiles work with layered configuration. Repo-local profiles can extend profiles defined in the global config:
Global config (~/.agent-box.toml):
context = """
General development guidelines:
- Write clear, self-documenting code
- Add comments for complex logic
- Keep functions small and focused
"""
[profiles.base]
env = ["EDITOR=nvim"]
context = """
Environment setup:
- Nix store is mounted read-only at /nix/store
- Use `nix develop` for project-specific tools
"""
[profiles.base.mounts.ro]
absolute = ["/nix/store"]
[profiles.rust]
extends = ["base"]
env = ["CARGO_HOME=~/.cargo"]
context = """
Rust best practices:
- Prefer iterators over loops
- Use `?` operator for error propagation
- Run clippy with `cargo clippy -- -D warnings`
"""Repo-local config (~/repos/myproject/.agent-box.toml):
context = """
MyProject - Web service for task management:
- Main entry point: src/main.rs
- API handlers in src/handlers/
- Database models in src/models/
- Tests must cover all API endpoints
- Use the test helpers in tests/common/mod.rs
"""
# Override default profile for this repo
default_profile = "myproject-dev"
# Define a repo-specific profile that extends global "rust"
[profiles.myproject-dev]
extends = ["rust"]
env = ["PROJECT=myproject"]
context = """
MyProject development:
- Database schema in migrations/
- Run `sqlx migrate run` to apply migrations
- API docs generated with `cargo doc --open`
- Use `.env.example` as template for local config
"""
[profiles.myproject-dev.mounts.rw]
home_relative = ["~/.local/share/myproject"]
# Add to an existing global profile
[profiles.rust]
env = ["RUST_BACKTRACE=1"] # Appended to global rust.envThis allows:
- Repo-local profiles extending global profiles
- Overriding
default_profileper-repo - Adding mounts/env/context to existing global profiles (arrays are concatenated)
- Repo-local
contextoverrides globalcontext(scalar value)
Use ab dbg validate to check your profile configuration for errors:
ab dbg validateThis validates:
default_profilereferences a defined profile- All
extendsreferences point to defined profiles - No circular dependencies in
extendschains - No self-references in
extends
Example output for a valid configuration:
Configuration valid. No errors or warnings.
Profiles defined: 3
- rust (extends: base)
- base
- git (extends: base)
Default profile: base
Example output with errors:
Errors:
✗ default_profile 'nonexistent' is not defined. Available profiles: ["base", "git"]
✗ Profile 'broken': extends unknown profile 'also_nonexistent'. Available profiles: ["base", "git"]
Warnings:
⚠ Profile 'empty': profile is empty (no mounts, env, ports, hosts, or extends)
Configuration invalid: 2 error(s), 1 warning(s).
Use ab dbg resolve to see the merged configuration after applying profiles:
# Show resolved config with just default_profile (if set)
ab dbg resolve
# Show resolved config with specific profiles
ab dbg resolve -p rust -p gitThis shows the final merged mounts, environment variables, and context after:
- Starting with base
runtime.mounts,runtime.env, and root-levelcontext - Applying
default_profile(if set) - Applying CLI-specified profiles in order
Example output:
Profiles applied (in order): base → rust
Resolved config:
Mounts:
ro: /nix/store -> /nix/store:/nix/store:ro
rw: /nix/var/nix/daemon-socket/ -> /nix/var/nix/daemon-socket:/nix/var/nix/daemon-socket:rw
ro: ~/.config/git (home-relative) -> /home/user/.config/git:/home/user/.config/git:ro
rw: ~/.cargo (home-relative) -> /home/user/.cargo:/home/user/.cargo:rw
O: ~/.gnupg (home-relative) -> /home/user/.gnupg:/home/user/.gnupg:O
Environment:
NIX_REMOTE=daemon
Environment Passthrough:
PATH = /usr/local/bin:/usr/bin:/bin
TERM = xterm-256color
Ports:
8080:8080
3000:3000
Hosts:
host.docker.internal:host-gateway
Context:
General development guidelines:
- Write clear, self-documenting code
- Add comments for complex logic
- Keep functions small and focused
Environment setup:
- Nix store is mounted read-only at /nix/store
- Use `nix develop` for project-specific tools
Rust best practices:
- Prefer iterators over loops
- Use `?` operator for error propagation
- Run clippy with `cargo clippy -- -D warnings`
The output shows both the mount spec and the resolved bind string (host:container:mode).
Mounts marked (home-relative) will have their host home directory prefix mapped to the container's home directory (e.g., /home/alice/.cargo → /home/bob/.cargo).
If a mount path contains symlinks, all intermediate symlinks and the final target are mounted so that path resolution works identically in the container:
rw: ~/mylink (home-relative) ->
/home/user/mylink:/home/user/mylink:rw
/home/user/intermediate:/home/user/intermediate:rw
/data/actual:/data/actual:rw
ab infoDisplays git worktrees and jj workspaces for the current repository.
# Create jj workspace (default), prompts for session name
ab new myrepo
# Create jj workspace with session name
ab new myrepo -s feature-x
# Create git worktree instead
ab new myrepo -s feature-x --git
# Use current directory's repo
ab new -s feature-x# Spawn container for a session workspace
ab spawn -s my-session
# Specify repository
ab spawn -s my-session -r myrepo
# Create workspace and spawn container
ab spawn -s my-session -r myrepo -n
# Local mode: use current directory as workspace
ab spawn -l
# Run a command in the container (passed to entrypoint)
ab spawn -s my-session -c pi
ab spawn -s my-session -c cargo build
# Override entrypoint (bypass nix develop wrapper)
ab spawn -s my-session -e /bin/bash
# Add additional profiles
ab spawn -s my-session -p git -p rust
# Add additional mounts (home-relative with -m, absolute with -M)
ab spawn -s my-session -m ~/data -m ro:~/.config/git
ab spawn -s my-session -M /nix/store -M ro:/etc/hosts
# Mount with explicit source:dest mapping
ab spawn -s my-session -m rw:~/src:/app/src
ab spawn -s my-session -m /run/user/1000/gnupg/S.gpg-agent:~/.gnupg/S.gpg-agent
# Expose ports
ab spawn -s my-session -P 8080:8080 -P 3000
# Use host networking (share the host's network namespace)
ab spawn -s my-session --network=host
# Use a specific network mode (bridge, none, or a named network)
ab spawn -s my-session --network=bridge
# Combine profiles with additional mounts, ports, host entries, and network mode
ab spawn -s my-session -p rust -m ~/project-data -P 8080:8080 -H myhost:10.0.0.1 --network=hostSession vs Local mode:
-s/--session: Creates/uses a separate workspace directory, mounts source repo's.git/.jjseparately-l/--local: Uses current directory as both source and workspace (mutually exclusive with-s)
Additional mounts (-m and -M):
Add extra mounts beyond what's configured in ~/.agent-box.toml:
-m/--mount: Home-relative mount (container path translates~to container user's home)-M/--Mount: Absolute mount (same path on host and container)
Format: [MODE:]PATH or [MODE:]SRC:DST
MODEis optional:ro(read-only),rw(read-write, default), oro(overlay, Podman only)PATHcan be absolute (/...), home-relative (~/...), or relative (../...,./...)- Relative host source paths are resolved against the current working directory
Examples:
-m ~/data # rw mount, ~/data on host → ~/data in container
-m ro:~/.config # ro mount
-M /nix/store # rw mount, same absolute path on both sides
-M o:/tmp/cache # overlay mount (Podman only)
-m ~/src:/app # explicit mapping: ~/src on host → /app in container
-m ../pierre # relative host path resolved against current working directory
-M '/tmp/kitty-*' # glob: mounts every matching path (quote to prevent shell expansion)-
Directory Structure:
base_repo_dir: Your source repositories (colocated jj/git repos)workspace_dir/git/{repo_path}/{session}: Git worktreesworkspace_dir/jj/{repo_path}/{session}: JJ workspaces
-
New Workspace:
- For JJ: Creates a workspace from a colocated jj repo using
jj workspace add - For Git: Creates a worktree from a git repo using
git worktree add
- For JJ: Creates a workspace from a colocated jj repo using
-
Spawn Container:
- Mounts the workspace path as read-write
- In session mode: also mounts source repo's
.gitand.jjdirectories - In local mode: workspace and source are the same directory
- Adds configured mounts (ro/rw, absolute/home_relative)
- Runs as current user (uid:gid)
- Sets working directory to the workspace
- Uses the configured runtime backend (docker or podman)
-
Repository Identification:
- Repos are identified by their relative path from
base_repo_dir - Can search by full path (
fr/agent-box) or partial name (agent-box) - If multiple repos match, prompts user to select
- Repos are identified by their relative path from
To use your host's GPG keys for signing inside containers, you need to:
- Mount
~/.gnupgas an overlay (so container writes don't affect host) - Mount the GPG socket files from the host's runtime directory
Find your socket paths:
On your host, run:
gpgconf --list-dirsLook for these paths:
socketdir- where GPG expects sockets (usually~/.gnupg)agent-socket- the gpg-agent socketkeyboxd-socket- the keybox daemon socket (GPG 2.4+)
On most Linux systems with systemd, the actual sockets live in /run/user/<UID>/gnupg/.
Configuration:
[runtime.mounts.o] # Overlay mount (Podman only)
home_relative = ["~/.gnupg"]
[runtime.mounts.rw]
# Mount sockets from host's runtime dir to container's ~/.gnupg
# Replace 1000 with your UID
home_relative = [
"/run/user/1000/gnupg/S.gpg-agent:~/.gnupg/S.gpg-agent",
"/run/user/1000/gnupg/S.keyboxd:~/.gnupg/S.keyboxd",
]Why overlay mount for ~/.gnupg?
GPG creates lock files and other temporary files in ~/.gnupg. Without an overlay:
- Lock files from the host (with host PIDs) confuse the container
- Container writes would affect your host's GPG directory
The overlay mount lets the container see your keys and config but writes go to a temporary layer.
For Docker users:
Docker doesn't support overlay mounts. You can either:
- Use Podman instead (
backend = "podman") - Mount
~/.gnupgas read-write and accept that lock files may conflict
Smartcard/YubiKey users:
If your signing key is on a smartcard, also mount the scdaemon socket:
home_relative = [
"/run/user/1000/gnupg/S.gpg-agent:~/.gnupg/S.gpg-agent",
"/run/user/1000/gnupg/S.keyboxd:~/.gnupg/S.keyboxd",
"/run/user/1000/gnupg/S.scdaemon:~/.gnupg/S.scdaemon",
]Troubleshooting:
- "Connection timed out" / "waiting for lock": Stale lock files in
~/.gnupg. Use overlay mount or clean up.#lk*files. - "IPC call has been cancelled": Usually means your default key is on a smartcard that isn't connected. Specify a different key with
gpg -u <keyid>. - Verify sockets are working: Run
gpg-connect-agent 'getinfo socket_name' /bye- should show the socket path and returnOK. - List keys:
gpg --list-secret-keys- keys with>aftersecare on smartcards.
To use binaries from your host's Nix store inside containers via the daemon socket:
[docker]
env = ["NIX_REMOTE=daemon"]
[docker.mounts.ro]
absolute = ["/nix/store"]
[docker.mounts.rw]
absolute = ["/nix/var/nix/daemon-socket/"]This mounts the Nix store read-only and the daemon socket read-write, allowing the container to request builds/fetches from the host's Nix daemon.
- Rust (2024 edition)
- Git
- Jujutsu (for jj workspaces)
- Docker or Podman (for container spawning)
