Files
sandcage/docs/configuration.md
T
2026-09-21 14:31:12 +02:00

15 KiB

Configuration Reference

Sandcage uses a layered configuration system. Each layer overrides the previous one:

  1. Compiled defaults — all fields unset
  2. Global config (~/.sandcage/config.toml) — user-wide preferences (TOML format)
  3. Project config (.sandcage/config.toml) — per-project settings, checked into version control (TOML format)
  4. Local config (.sandcage/local/config.local.toml) — personal overrides, gitignored via .sandcage/.gitignore (TOML format)

Later layers win. Fields not set in a layer are inherited from the layer below.


Directory Layout

.sandcage/
├── config.toml           # project config (committed)
├── overlay/              # files bind-mounted read-only over workspace files in the container
├── local/                # gitignored via .sandcage/.gitignore
│   ├── config.local.toml
│   └── overlay/          # local overlay files (also gitignored)
└── .gitignore            # contains "local/"

The .sandcage/ directory itself is invisible inside the container (shadowed by a tmpfs mount).


Config Source Resolution

When sandcage looks for the .sandcage/ directory, it checks these locations in order:

  1. SANDCAGE_CONFIG_DIR env var — explicit override, used as-is
  2. .sandcage/ in the workspace — the most common case
  3. Redirect file — a sandcage.toml at the workspace root containing a redirect key pointing to the real .sandcage/ directory
  4. Main worktree root — for git worktrees, falls back to the main worktree's .sandcage/

The first match wins. sandcage init refuses to run from linked worktrees — run it from the main worktree instead.


Fields

env

Environment variables passed into the container at runtime.

  • Type: Map of string keys to string values
  • Valid in: Global, Project, Local
  • Behavior: Passed as -e KEY=VALUE flags to docker compose run
# .sandcage/config.toml
[env]
RUST_LOG = "debug"
DATABASE_URL = "postgres://localhost:5432/dev"
# ~/.sandcage/config.toml
[env]
EDITOR = "vim"

packages

System packages to install in the container image (apt packages).

  • Type: List of strings
  • Valid in: Global, Project, Local
packages = ["ripgrep", "fd-find", "postgresql-client"]

toolchains

Language toolchain managers to enable. When enabled, the corresponding Dockerfile fragment from images/fragments/ is included during sandcage build.

  • Type: Map of string keys to boolean values
  • Valid in: Global, Project, Local
  • Supported keys: uv, nvm, rust
[toolchains]
uv = true
nvm = true
rust = true

mounts

Additional volume mounts passed to the container. Each entry must contain a : separator.

  • Type: List of strings in Docker mount format (<source>:<target>[:<options>])
  • Valid in: Global, Project, Local
  • Behavior: Tilde (~) in the source path is expanded to the host home directory. Entries without : are skipped with a warning.
mounts = [
  "/data/models:/models:ro",
  "~/.gitconfig:/home/agent/.gitconfig:ro",
  "/tmp/cache:/cache",
]

shell

Default shell inside the container.

  • Type: String
  • Valid in: Global, Project, Local
shell = "zsh"

justfile

Path to a justfile to make available in the container.

  • Type: File path (string)
  • Valid in: Global, Project, Local
justfile = "./project.justfile"

dockerfiles

Custom Dockerfile overrides for image builds. Keyed by image short name. Values can be a path to a Dockerfile or a directory containing one.

  • Type: Map of string keys to file paths
  • Valid in: Global, Project (project overrides require the project to be in trusted_projects)
  • Behavior: Project-level Dockerfile overrides are ignored unless the project directory is listed in trusted_projects in the global config.
# .sandcage/config.toml (only applied if project is trusted)
[dockerfiles]
sandcage = "./docker/Dockerfile.custom"
# ~/.sandcage/config.toml
[dockerfiles]
sandcage = "/home/user/my-dockerfiles/sandcage/"

trusted_projects

List of project directories allowed to use Dockerfile overrides.

  • Type: List of file paths
  • Valid in: Global only (a project cannot self-trust)
  • Behavior: Read exclusively from ~/.sandcage/config.toml. Projects with dockerfiles set are ignored unless their path (or a parent) appears here.
# ~/.sandcage/config.toml
trusted_projects = [
  "/home/user/projects/my-trusted-project",
  "/home/user/work",
]

container_workspace

Override the workspace path inside the container. Must be an absolute path (starting with /). Relative paths are ignored with a warning.

  • Type: String (absolute path)
  • Valid in: Global, Project, Local
  • Default: /workspace/<project-directory-name> (derived from the host workspace folder name)
container_workspace = "/workspace/my-app"

agent_args

Default arguments passed to agents on every invocation. Keyed by service name. These are appended after the service name but before any extra CLI arguments.

  • Type: Map of service name to list of strings
  • Valid in: Global, Project, Local
  • Behavior: Only the args for the invoked service are used. When running with --shell, agent_args are skipped entirely.
# .sandcage/config.toml
[agent_args]
claude = ["--dangerously-skip-permissions"]
codex = ["--full-auto"]
# ~/.sandcage/config.toml
[agent_args]
claude = ["--dangerously-skip-permissions"]

ssh_mode

Controls how SSH keys are made available inside the container.

  • Type: String — one of "volume", "bind", or "none"
  • Valid in: Global, Project, Local
  • Default: No SSH mount (equivalent to "none")
Value Behavior
volume Mounts a named Docker volume (sandcage-ssh) at /home/agent/.ssh (read-only). Populate it with sandcage setup ssh.
bind Bind-mounts ~/.ssh from the host at /home/agent/.ssh (read-only).
none No SSH directory is mounted.
ssh_mode = "volume"

When using volume mode, run sandcage setup ssh --refresh after changing keys on the host.


ssh_keys

Declares which SSH keys to copy into the volume when using sandcage setup ssh. Each entry specifies a host and identity file.

  • Type: List of objects with host (string) and identity_file (string) fields
  • Valid in: Global, Project, Local
[[ssh_keys]]
host = "github.com"
identity_file = "~/.ssh/id_ed25519"

[[ssh_keys]]
host = "gitea.internal"
identity_file = "~/.ssh/work_gitea"

services

Override the enabled state of built-in services. Keyed by service name.

  • Type: Map of service name to object with optional enabled (boolean) field
  • Valid in: Global, Project, Local
  • Behavior: Disabled services cannot be started with sandcage <service> and are excluded from generated compose files.
[services.gemini]
enabled = false

[services.codex]
enabled = false
# ~/.sandcage/config.toml
[services.codex]
enabled = false

default_services

Controls which services sandcage build prepares by default (when no service names are given on the CLI).

  • Type: List of strings (service names)
  • Valid in: Global, Project, Local
default_services = ["claude", "shell"]

image

The Docker image name used for building and running containers.

  • Type: String
  • Valid in: Global, Project, Local
  • Default: "sandcage"
# .sandcage/config.toml
image = "my-custom-sandcage"
# ~/.sandcage/config.toml
image = "my-custom-sandcage"

overlay.enabled

Controls whether overlay mounts are active. When enabled, files in .sandcage/overlay/ (and .sandcage/local/overlay/) are bind-mounted read-only over the corresponding workspace files inside the container.

  • Type: Boolean
  • Valid in: Global, Project, Local
  • Default: true
[overlay]
enabled = false

Local overlays (.sandcage/local/overlay/) take precedence over project overlays (.sandcage/overlay/).


shadow

Shadow directories redirect build artifact and dependency directories away from the workspace bind-mount into faster, container-local storage. This prevents large directories like target/, .venv/, or node_modules/ from thrashing the host filesystem.

  • Type: Object with optional backend (string) and dirs (map) fields
  • Valid in: Global, Project, Local
  • Default backend: "volume" (named Docker volume)

Each entry under [shadow.dirs] can override the global backend and set shared = true to share the volume across containers for the same project.

Backend Behavior
volume Named Docker volume (sandcage-<project>-<dir-hash>), persisted across runs
directory Host directory under ~/.sandcage/shadow/, persisted
tmpfs In-memory tmpfs mount, lost when the container stops
[shadow]
backend = "volume"

[shadow.dirs]
".venv" = {}
"target" = { backend = "directory" }
".cargo/registry" = { shared = true }
"node_modules" = { backend = "tmpfs" }

Use sandcage clean to remove shadow volumes for a project.


tools

Tool mode declarations control how specific CLI tools behave inside the container. Each entry under [tools.<name>] configures one tool.

  • Type: Map of tool name to object
  • Valid in: Project, Local
  • Fields:
    • mode — "in-cage" (default), "spoof", or "host"
    • message — message printed by a spoof script (only for mode = "spoof")
    • allow — list of glob patterns permitting arguments (only for mode = "host")
    • deny — list of glob patterns blocking arguments (only for mode = "host")
Mode Behavior
in-cage Tool runs normally inside the container (default)
spoof Replaced by a script that prints message and exits. Prevents agents from running the tool.
host Forwarded to the host via the forwarding daemon. Requires trusted_projects.

Allow/deny patterns use simple * wildcard glob matching on the full argument string. Deny patterns are checked first and override allow patterns. If any allow patterns are specified, the command must match at least one.

[tools.cargo]
mode = "spoof"
message = "Use `just build` to compile this project."

[tools.docker]
mode = "host"
allow = ["compose up *", "compose down"]
deny = ["* prune *", "* rm -f *"]

[tools.uv]
mode = "in-cage"

tasks

Named operations that can run on the host or inside the container. Host tasks are forwarded via the forwarding daemon and thumper; in-cage tasks run directly inside the container.

  • Type: Map of task name to object
  • Valid in: Project, Local
  • Fields:
    • where — "host" or "in-cage" (where the task executes)
    • run — the command to execute (string, split on whitespace)

Host tasks (where = "host") require the project to be in trusted_projects. A Justfile overlay is auto-generated for host tasks, making them available as just <task-name> inside the container.

[tasks.build]
where = "host"
run = "cargo build --release"

[tasks.test]
where = "host"
run = "cargo test"

[tasks.lint]
where = "in-cage"
run = "clippy"

profiles

Security profiles applied to the container via --security-opt. Profile files are loaded from .sandcage/profiles/.

  • Type: Object with optional apparmor (string) and seccomp (string) fields
  • Valid in: Project, Local
  • Behavior: Paths are relative to the project root. Missing files are skipped with a warning. Applied on both the bollard and compose backends.

If a seccomp profile contains SCMP_ACT_NOTIFY, sandcage detects this as a fermata hook point and logs accordingly.

[profiles]
apparmor = ".sandcage/profiles/apparmor.conf"
seccomp = ".sandcage/profiles/seccomp.json"

home

Named home profile for agent auth and configuration. Each profile is a separate directory under ~/.sandcage/homes/, containing its own agent credentials, settings, and session history.

  • Type: String (profile name)
  • Valid in: Global, Project, Local
  • Default: "default"
  • CLI override: --home <NAME> on any agent or shell subcommand
  • Behavior: Resolves to ~/.sandcage/homes/<name>/, which is mounted as /home/agent inside the container. The directory is created automatically on first use. CLI flag takes precedence over config.
# Use a specific home profile for this project
home = "work"

This allows switching between multiple agent identities (e.g., personal and work Claude accounts) without re-authenticating:

sandcage claude --home personal   # uses ~/.sandcage/homes/personal/
sandcage claude --home work       # uses ~/.sandcage/homes/work/
sandcage claude                   # uses ~/.sandcage/homes/default/

Migration: If a legacy ~/.sandcage/home/ directory exists from before named profiles were introduced, it is automatically moved to ~/.sandcage/homes/default/ on first run.


Reserved Sections

The following top-level config section is parsed and validated but not yet active:

  • forward — port/socket forwarding rules (the forwarding daemon uses internal configuration; this section is for future user-facing forwarding config)

Setting this field will not produce errors, but it has no runtime effect yet.


Services

Sandcage ships with four built-in services, all enabled by default:

Service Description Config directory
claude Claude Code agent .claude
codex Codex agent .codex
gemini Gemini CLI agent .gemini
shell Interactive zsh shell (none)

Agent services are installed on first run into the persistent home directory (~/.sandcage/homes/<profile>/). They auto-update on subsequent runs.

To disable a service you do not use:

[services.codex]
enabled = false

[services.gemini]
enabled = false

Disabled services are excluded from Docker compose generation and cannot be invoked.


Examples

Minimal project config

# .sandcage/config.toml
packages = ["ripgrep"]

[toolchains]
nvm = true
# .sandcage/config.toml
packages = ["ripgrep", "fd-find", "postgresql-client"]
mounts = ["/data/models:/models:ro"]
shell = "zsh"

[env]
RUST_LOG = "debug"
NODE_ENV = "development"

[toolchains]
rust = true
nvm = true

[agent_args]
claude = ["--dangerously-skip-permissions"]

[services.gemini]
enabled = false

Local overrides (gitignored)

# .sandcage/local/config.local.toml
ssh_mode = "volume"
mounts = ["~/.aws:/home/agent/.aws:ro"]

[env]
SECRET_API_KEY = "sk-local-dev-key"

[[ssh_keys]]
host = "github.com"
identity_file = "~/.ssh/id_ed25519"

Global config

# ~/.sandcage/config.toml
shell = "zsh"

[env]
EDITOR = "vim"

[agent_args]
claude = ["--dangerously-skip-permissions"]

[services.codex]
enabled = false

trusted_projects = [
  "/home/user/work/trusted-project",
]