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

578 lines
15 KiB
Markdown

# 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`
```toml
# .sandcage/config.toml
[env]
RUST_LOG = "debug"
DATABASE_URL = "postgres://localhost:5432/dev"
```
```toml
# ~/.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
```toml
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`
```toml
[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.
```toml
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
```toml
shell = "zsh"
```
---
### justfile
Path to a justfile to make available in the container.
- **Type:** File path (string)
- **Valid in:** Global, Project, Local
```toml
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.
```toml
# .sandcage/config.toml (only applied if project is trusted)
[dockerfiles]
sandcage = "./docker/Dockerfile.custom"
```
```toml
# ~/.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.
```toml
# ~/.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)
```toml
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.
```toml
# .sandcage/config.toml
[agent_args]
claude = ["--dangerously-skip-permissions"]
codex = ["--full-auto"]
```
```toml
# ~/.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. |
```toml
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
```toml
[[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.
```toml
[services.gemini]
enabled = false
[services.codex]
enabled = false
```
```toml
# ~/.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
```toml
default_services = ["claude", "shell"]
```
---
### image
The Docker image name used for building and running containers.
- **Type:** String
- **Valid in:** Global, Project, Local
- **Default:** `"sandcage"`
```toml
# .sandcage/config.toml
image = "my-custom-sandcage"
```
```toml
# ~/.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`
```toml
[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 |
```toml
[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.
```toml
[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.
```toml
[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.
```toml
[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.
```toml
# 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:
```sh
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:
```toml
[services.codex]
enabled = false
[services.gemini]
enabled = false
```
Disabled services are excluded from Docker compose generation and cannot be invoked.
---
## Examples
### Minimal project config
```toml
# .sandcage/config.toml
packages = ["ripgrep"]
[toolchains]
nvm = true
```
### Full-featured project config
```toml
# .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)
```toml
# .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
```toml
# ~/.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",
]
```