15 KiB
Configuration Reference
Sandcage uses a layered configuration system. Each layer overrides the previous one:
- Compiled defaults — all fields unset
- Global config (
~/.sandcage/config.toml) — user-wide preferences (TOML format) - Project config (
.sandcage/config.toml) — per-project settings, checked into version control (TOML format) - 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:
SANDCAGE_CONFIG_DIRenv var — explicit override, used as-is.sandcage/in the workspace — the most common case- Redirect file — a
sandcage.tomlat the workspace root containing aredirectkey pointing to the real.sandcage/directory - 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=VALUEflags todocker 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_projectsin 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 withdockerfilesset 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) andidentity_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) anddirs(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 formode = "spoof")allow— list of glob patterns permitting arguments (only formode = "host")deny— list of glob patterns blocking arguments (only formode = "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) andseccomp(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/agentinside 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
Full-featured project config
# .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",
]