🛰️ export from upstream (18069bd)
This commit is contained in:
@@ -0,0 +1,434 @@
|
||||
# Command Reference
|
||||
|
||||
Sandcage provides subcommands for running AI coding agents in isolated Docker containers.
|
||||
|
||||
```
|
||||
sandcage <COMMAND> [OPTIONS]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## claude
|
||||
|
||||
Run the Claude Code agent in a sandboxed container.
|
||||
|
||||
```
|
||||
sandcage claude [OPTIONS] [-- AGENT_ARGS...]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-p, --path <PATH>` | Path to the project directory (defaults to current directory) |
|
||||
| `--shell` | Drop into a shell instead of launching the agent |
|
||||
| `--worktree` | Create a git worktree and mount it instead of the project directory |
|
||||
| `--home <NAME>` | Named home profile for agent auth/config (default: "default") |
|
||||
|
||||
**Trailing arguments:** Any arguments after `--` are forwarded directly to the Claude Code binary inside the container.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
# Run Claude Code on the current directory
|
||||
sandcage claude
|
||||
|
||||
# Run Claude Code on a specific project
|
||||
sandcage claude --path /home/user/myproject
|
||||
|
||||
# Drop into a shell in the Claude container environment
|
||||
sandcage claude --shell
|
||||
|
||||
# Run in a disposable worktree copy
|
||||
sandcage claude --worktree
|
||||
|
||||
# Run with a specific home profile (e.g. work account)
|
||||
sandcage claude --home work
|
||||
|
||||
# Forward arguments to the agent
|
||||
sandcage claude -- --resume
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## codex
|
||||
|
||||
Run the OpenAI Codex agent in a sandboxed container.
|
||||
|
||||
```
|
||||
sandcage codex [OPTIONS] [-- AGENT_ARGS...]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-p, --path <PATH>` | Path to the project directory (defaults to current directory) |
|
||||
| `--shell` | Drop into a shell instead of launching the agent |
|
||||
| `--worktree` | Create a git worktree and mount it instead of the project directory |
|
||||
| `--home <NAME>` | Named home profile for agent auth/config (default: "default") |
|
||||
|
||||
**Trailing arguments:** Any arguments after `--` are forwarded directly to the Codex binary inside the container.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
# Run Codex on the current directory
|
||||
sandcage codex
|
||||
|
||||
# Run Codex in a disposable worktree
|
||||
sandcage codex --worktree
|
||||
|
||||
# Forward arguments to Codex
|
||||
sandcage codex -- --model o4-mini
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## gemini
|
||||
|
||||
Run the Gemini CLI agent in a sandboxed container.
|
||||
|
||||
```
|
||||
sandcage gemini [OPTIONS] [-- AGENT_ARGS...]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-p, --path <PATH>` | Path to the project directory (defaults to current directory) |
|
||||
| `--shell` | Drop into a shell instead of launching the agent |
|
||||
| `--worktree` | Create a git worktree and mount it instead of the project directory |
|
||||
| `--home <NAME>` | Named home profile for agent auth/config (default: "default") |
|
||||
|
||||
**Trailing arguments:** Any arguments after `--` are forwarded directly to the Gemini CLI binary inside the container.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
# Run Gemini CLI on the current directory
|
||||
sandcage gemini
|
||||
|
||||
# Run Gemini in a disposable worktree
|
||||
sandcage gemini --worktree
|
||||
|
||||
# Drop into a shell in the Gemini container
|
||||
sandcage gemini --shell
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## shell
|
||||
|
||||
Open an interactive shell (zsh) with the same sandboxed environment used by agents.
|
||||
|
||||
```
|
||||
sandcage shell [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-p, --path <PATH>` | Path to the project directory (defaults to current directory) |
|
||||
| `--home <NAME>` | Named home profile for agent auth/config (default: "default") |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
# Interactive shell in the sandbox for current project
|
||||
sandcage shell
|
||||
|
||||
# Interactive shell for a specific project
|
||||
sandcage shell --path /home/user/myproject
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## build
|
||||
|
||||
Build the container images used by sandcage services.
|
||||
|
||||
```
|
||||
sandcage build [OPTIONS] [SERVICES...]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-f, --force` | Force rebuild even if images are up to date |
|
||||
|
||||
**Positional arguments:** Optionally specify which services to build (e.g. `claude`, `codex`, `gemini`). When omitted, all enabled services are built.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
# Build all enabled service images
|
||||
sandcage build
|
||||
|
||||
# Force rebuild of all images
|
||||
sandcage build --force
|
||||
|
||||
# Build only the claude image
|
||||
sandcage build claude
|
||||
|
||||
# Build claude and gemini images
|
||||
sandcage build claude gemini
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## init
|
||||
|
||||
Initialize a `.sandcage/` directory with configuration for a project.
|
||||
|
||||
```
|
||||
sandcage init
|
||||
```
|
||||
|
||||
Scaffolds a `.sandcage/` directory in the current workspace with `sandcage.yml`, `overlay/`, `local/`, `local/overlay/`, and a `.gitignore` that excludes `local/`.
|
||||
|
||||
Refuses to run from linked git worktrees — run it from the main worktree instead.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
# Initialize sandcage config in current directory
|
||||
sandcage init
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## setup ssh
|
||||
|
||||
Configure SSH key access for containers. Copies selected SSH keys into a Docker volume accessible by sandcage containers.
|
||||
|
||||
```
|
||||
sandcage setup ssh [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--global` | Write to global config (`~/.sandcage/config.toml`) instead of project config |
|
||||
| `--yes` | Skip confirmation prompt |
|
||||
| `--refresh` | Re-populate the SSH volume using the previously saved selection |
|
||||
| `--bind` | Use legacy full bind mount instead of volume copy |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
# Interactive SSH key setup for the current project
|
||||
sandcage setup ssh
|
||||
|
||||
# Non-interactive setup writing to global config
|
||||
sandcage setup ssh --global --yes
|
||||
|
||||
# Refresh the SSH volume after adding new keys
|
||||
sandcage setup ssh --refresh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## acp
|
||||
|
||||
Manage and run ACP (Agent Control Protocol) agents. This command group requires the `bollard` feature, which is enabled by default.
|
||||
|
||||
```
|
||||
sandcage acp <SUBCOMMAND>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### acp run
|
||||
|
||||
Run a service in ACP relay mode, attaching stdin/stdout/stderr for bidirectional communication with the container. Uses the Bollard Docker API directly instead of docker compose. Designed for structured agent orchestration.
|
||||
|
||||
```
|
||||
sandcage acp run <SERVICE> [OPTIONS] [-- AGENT_ARGS...]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `-p, --path <PATH>` | Path to the project directory (defaults to current directory) |
|
||||
|
||||
**Positional arguments:** `<SERVICE>` — the service name to run (e.g. `claude`, `codex`).
|
||||
|
||||
**Trailing arguments:** Any arguments after `--` are forwarded directly to the agent binary inside the container.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
# Run the claude service in ACP relay mode
|
||||
sandcage acp run claude
|
||||
|
||||
# Run against a specific project directory
|
||||
sandcage acp run claude --path /home/user/myproject
|
||||
|
||||
# Forward arguments to the agent
|
||||
sandcage acp run claude -- --resume
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### acp install
|
||||
|
||||
Install an agent from the ACP registry. Downloads the agent binary for the current platform and caches it at `~/.sandcage/agents/<id>/<version>/`.
|
||||
|
||||
```
|
||||
sandcage acp install <AGENT_ID> [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--force` | Reinstall even if the agent is already installed |
|
||||
|
||||
**Positional arguments:** `<AGENT_ID>` — the identifier of the agent to install from the registry.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
# Install an agent from the registry
|
||||
sandcage acp install my-agent
|
||||
|
||||
# Force reinstall an already-installed agent
|
||||
sandcage acp install my-agent --force
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### acp list
|
||||
|
||||
List available agents in the ACP registry. Fetches the agent list from the CDN with a 1-hour local cache.
|
||||
|
||||
```
|
||||
sandcage acp list [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--refresh` | Bypass the local cache and fetch a fresh list from the registry |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
# List available agents (uses cache if fresh)
|
||||
sandcage acp list
|
||||
|
||||
# Force a fresh fetch from the registry
|
||||
sandcage acp list --refresh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### acp installed
|
||||
|
||||
List locally installed ACP agents.
|
||||
|
||||
```
|
||||
sandcage acp installed
|
||||
```
|
||||
|
||||
No options.
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
# Show all locally installed ACP agents
|
||||
sandcage acp installed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## clean
|
||||
|
||||
Remove shadow volumes for the current project. By default runs a dry run showing what would be removed.
|
||||
|
||||
```
|
||||
sandcage clean [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--yes` | Actually delete (default is a dry run) |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
# Preview what would be removed
|
||||
sandcage clean
|
||||
|
||||
# Remove shadow volumes
|
||||
sandcage clean --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## worktree
|
||||
|
||||
Manage sandcage-created git worktrees. Worktrees are created under `~/.sandcage/worktrees/`.
|
||||
|
||||
---
|
||||
|
||||
### worktree list
|
||||
|
||||
List all sandcage-managed worktrees.
|
||||
|
||||
```
|
||||
sandcage worktree list
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
sandcage worktree list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### worktree clean
|
||||
|
||||
Remove all sandcage-managed worktrees.
|
||||
|
||||
```
|
||||
sandcage worktree clean [OPTIONS]
|
||||
```
|
||||
|
||||
**Options:**
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--yes` | Skip confirmation prompt |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```sh
|
||||
# Interactive cleanup
|
||||
sandcage worktree clean
|
||||
|
||||
# Non-interactive cleanup
|
||||
sandcage worktree clean --yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Project path resolution:** All agent commands and `shell` accept `--path` (`-p`) to specify the project directory. When omitted, sandcage resolves the workspace from the current directory.
|
||||
|
||||
**Shell override:** The `--shell` flag on agent commands (`claude`, `codex`, `gemini`) drops you into an interactive shell inside the agent's container without launching the agent itself. Useful for debugging the container environment.
|
||||
|
||||
**Forwarding arguments to agents:** Agent commands accept trailing arguments (after `--`) that are passed through to the underlying agent binary. This allows setting agent-specific flags without sandcage needing to know about them.
|
||||
|
||||
**Available services:** The built-in services are `claude`, `codex`, `gemini`, and `shell`. Each agent service auto-installs its binary on first run if not already present in the container.
|
||||
@@ -0,0 +1,577 @@
|
||||
# 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",
|
||||
]
|
||||
```
|
||||
@@ -0,0 +1,113 @@
|
||||
# Docker Image
|
||||
|
||||
## Base image contents
|
||||
|
||||
The sandcage image is built on `debian:bookworm-slim` and includes:
|
||||
|
||||
- **Shell:** zsh (default), bash
|
||||
- **VCS:** git, openssh-client
|
||||
- **Search:** ripgrep, fd-find
|
||||
- **Utilities:** jq, curl, sudo, ca-certificates
|
||||
- **Task runner:** just (installed from upstream binary)
|
||||
|
||||
npm is not included in the base image. Node.js is available via the nvm toolchain fragment when `nvm = true` is set in config.
|
||||
|
||||
uv is not in the base image by default. It is added via the uv toolchain fragment when `uv = true` is set in config.
|
||||
|
||||
The image creates an `agent` user (UID 1000) with a home at `/home/agent` and zsh as the default shell. Sudo is disabled (the passwordless sudo line is commented out in the Dockerfile).
|
||||
|
||||
Pre-created directories: `/workspace`, `/home/agent/.claude`, `/home/agent/.codex`, `/home/agent/.gemini`.
|
||||
|
||||
## How agents are installed
|
||||
|
||||
AI coding agents (Claude Code, Codex, Gemini CLI) are **not** baked into the image. They are installed on first run via their respective entrypoint scripts. The agent home directories (`~/.claude`, `~/.codex`, `~/.gemini`) are persisted across runs through volume mounts to `~/.sandcage/` on the host, so installation only happens once.
|
||||
|
||||
Each service has its own entrypoint script installed at `/usr/local/bin/sandcage-<service>-entrypoint`.
|
||||
|
||||
## Building images
|
||||
|
||||
Build the sandcage image with:
|
||||
|
||||
```
|
||||
sandcage build
|
||||
```
|
||||
|
||||
### Force rebuild
|
||||
|
||||
To bypass the cache and rebuild unconditionally:
|
||||
|
||||
```
|
||||
sandcage build --force
|
||||
```
|
||||
|
||||
When `--force` is used, Docker's `--no-cache` flag is also passed to ensure layers are rebuilt from scratch.
|
||||
|
||||
### Service filter
|
||||
|
||||
Build only images required for specific services:
|
||||
|
||||
```
|
||||
sandcage build claude shell
|
||||
```
|
||||
|
||||
Unknown service names produce an error listing available services.
|
||||
|
||||
## Cache awareness
|
||||
|
||||
Sandcage uses hash-based rebuild detection to avoid unnecessary image builds.
|
||||
|
||||
On each build, the SHA-256 hash of the Dockerfile content (bundled or custom) is computed and compared against the stored hash in `~/.sandcage/.build-hashes`. If they match, the build is skipped with an "up to date" message.
|
||||
|
||||
After a successful build, the new hash is persisted. The hash file uses a simple `image:hash` line format.
|
||||
|
||||
This means:
|
||||
|
||||
- Editing the Dockerfile (or switching to/from a custom one) triggers a rebuild automatically.
|
||||
- The `--force` flag bypasses hash comparison entirely.
|
||||
|
||||
## Custom Dockerfiles
|
||||
|
||||
You can override the bundled Dockerfile by specifying a path in configuration.
|
||||
|
||||
### Global config (`~/.sandcage/config.toml`)
|
||||
|
||||
```toml
|
||||
[dockerfiles]
|
||||
sandcage = "/path/to/my/Dockerfile"
|
||||
```
|
||||
|
||||
### Project config (`.sandcage/config.toml`)
|
||||
|
||||
```toml
|
||||
[dockerfiles]
|
||||
sandcage = "./docker/Dockerfile.custom"
|
||||
```
|
||||
|
||||
The path can point to either:
|
||||
|
||||
- A **file** -- used as the Dockerfile with a temporary build context.
|
||||
- A **directory** -- used as the full build context (must contain a `Dockerfile`).
|
||||
|
||||
### trusted_projects requirement
|
||||
|
||||
Project-level Dockerfile overrides are only applied if the project directory is listed in the global `trusted_projects` setting. This prevents untrusted repositories from injecting arbitrary build instructions.
|
||||
|
||||
```toml
|
||||
# ~/.sandcage/config.toml
|
||||
trusted_projects = [
|
||||
"/home/user/projects/my-trusted-repo",
|
||||
"/home/user/work",
|
||||
]
|
||||
```
|
||||
|
||||
A project is considered trusted if its canonical path starts with any entry in `trusted_projects` (prefix matching). If a project specifies `dockerfiles` but is not trusted, the override is silently ignored with a warning.
|
||||
|
||||
Global config overrides (`~/.sandcage/config.toml`) are always trusted.
|
||||
|
||||
## Cross-platform notes
|
||||
|
||||
### UID/GID handling
|
||||
|
||||
On **Linux**, sandcage queries the host user's UID and GID via `id -u` / `id -g` and passes them into the container as `SANDCAGE_UID` and `SANDCAGE_GID`. This ensures files created inside the container have correct ownership on bind-mounted host directories.
|
||||
|
||||
On **Windows**, UID/GID passthrough is not meaningful. Sandcage hardcodes both to `1000`, matching the `agent` user built into the image. File ownership on Windows bind mounts is handled by Docker Desktop's filesystem sharing layer.
|
||||
@@ -0,0 +1,88 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="700" height="520" viewBox="0 0 700 520">
|
||||
<style>
|
||||
text { font-family: system-ui, -apple-system, sans-serif; }
|
||||
.title { font-size: 16px; font-weight: 600; fill: #1a1a1a; }
|
||||
.label { font-size: 13px; fill: #1a1a1a; }
|
||||
.small { font-size: 11px; fill: #555; }
|
||||
.edge { font-size: 11px; font-weight: 600; }
|
||||
.yes { fill: #2d7d46; }
|
||||
.no { fill: #c0392b; }
|
||||
.arrow { stroke: #666; stroke-width: 1.5; fill: none; marker-end: url(#arrowhead); }
|
||||
.diamond { fill: #fff3cd; stroke: #e0c36a; stroke-width: 1.5; }
|
||||
.result-native { fill: #d4edda; stroke: #7bc88f; stroke-width: 1.5; rx: 8; }
|
||||
.result-compose { fill: #cce5ff; stroke: #6cb3f5; stroke-width: 1.5; rx: 8; }
|
||||
.platform { fill: #f0f0f0; stroke: #ccc; stroke-width: 1; stroke-dasharray: 4 2; rx: 6; }
|
||||
</style>
|
||||
<defs>
|
||||
<marker id="arrowhead" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
|
||||
<path d="M0,0 L8,3 L0,6 Z" fill="#666"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<text x="350" y="30" text-anchor="middle" class="title">Interactive Mode: Backend Selection</text>
|
||||
|
||||
<!-- Config check -->
|
||||
<rect x="225" y="50" width="250" height="44" class="diamond" rx="6"/>
|
||||
<text x="350" y="69" text-anchor="middle" class="label">compose_backend = "compose"</text>
|
||||
<text x="350" y="84" text-anchor="middle" class="small">in sandcage.toml?</text>
|
||||
|
||||
<!-- Windows-only box -->
|
||||
<rect x="30" y="140" width="340" height="290" class="platform"/>
|
||||
<text x="50" y="160" class="small" font-style="italic">Windows only</text>
|
||||
|
||||
<!-- MSYSTEM check -->
|
||||
<rect x="100" y="175" width="200" height="44" class="diamond" rx="6"/>
|
||||
<text x="200" y="194" text-anchor="middle" class="label">MSYSTEM env var set?</text>
|
||||
<text x="200" y="209" text-anchor="middle" class="small">(Git Bash / MSYS2)</text>
|
||||
|
||||
<!-- Console handle check -->
|
||||
<rect x="100" y="275" width="200" height="44" class="diamond" rx="6"/>
|
||||
<text x="200" y="294" text-anchor="middle" class="label">Console handle valid?</text>
|
||||
<text x="200" y="309" text-anchor="middle" class="small">(GetConsoleMode)</text>
|
||||
|
||||
<!-- Raw mode check -->
|
||||
<rect x="225" y="380" width="250" height="44" class="diamond" rx="6"/>
|
||||
<text x="350" y="399" text-anchor="middle" class="label">Raw mode supported?</text>
|
||||
<text x="350" y="414" text-anchor="middle" class="small">(crossterm probe)</text>
|
||||
|
||||
<!-- Result: Native TTY -->
|
||||
<rect x="195" y="468" width="200" height="38" class="result-native"/>
|
||||
<text x="295" y="491" text-anchor="middle" class="label" font-weight="600">Native TTY</text>
|
||||
|
||||
<!-- Result: Compose fallback -->
|
||||
<rect x="500" y="200" width="170" height="38" class="result-compose"/>
|
||||
<text x="585" y="223" text-anchor="middle" class="label" font-weight="600">Compose fallback</text>
|
||||
|
||||
<!-- Arrows -->
|
||||
<!-- Config → MSYSTEM (No) -->
|
||||
<path d="M350,94 L350,120 L200,120 L200,175" class="arrow"/>
|
||||
<text x="280" y="115" class="edge no">no</text>
|
||||
|
||||
<!-- Config → Compose (Yes) -->
|
||||
<path d="M475,72 L585,72 L585,200" class="arrow"/>
|
||||
<text x="510" y="65" class="edge yes">yes</text>
|
||||
|
||||
<!-- MSYSTEM → Console (No) -->
|
||||
<path d="M200,219 L200,275" class="arrow"/>
|
||||
<text x="210" y="252" class="edge no">no</text>
|
||||
|
||||
<!-- MSYSTEM → Compose (Yes) -->
|
||||
<path d="M300,197 L430,197 L430,219 L500,219" class="arrow"/>
|
||||
<text x="340" y="190" class="edge yes">yes</text>
|
||||
|
||||
<!-- Console → Raw mode (Yes) -->
|
||||
<path d="M200,319 L200,355 L350,355 L350,380" class="arrow"/>
|
||||
<text x="210" y="345" class="edge yes">yes</text>
|
||||
|
||||
<!-- Console → Compose (No) -->
|
||||
<path d="M300,297 L430,297 L430,219 L500,219" class="arrow"/>
|
||||
<text x="340" y="290" class="edge no">no</text>
|
||||
|
||||
<!-- Raw mode → Native (Yes) -->
|
||||
<path d="M295,424 L295,468" class="arrow"/>
|
||||
<text x="275" y="450" class="edge yes">yes</text>
|
||||
|
||||
<!-- Raw mode → Compose (No) -->
|
||||
<path d="M475,402 L585,402 L585,238" class="arrow"/>
|
||||
<text x="510" y="395" class="edge no">no</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,91 @@
|
||||
# Interactive Mode
|
||||
|
||||
When you run `sandcage shell` or pass `--shell` to an agent command, sandcage opens an
|
||||
interactive terminal session inside the container. How that session is wired up depends on
|
||||
your terminal — sandcage picks the best available method automatically.
|
||||
|
||||
## Two backends
|
||||
|
||||
### Native TTY (default)
|
||||
|
||||
Sandcage talks directly to the Docker API, puts your terminal into raw mode, and relays
|
||||
keystrokes and output between your terminal and the container. This gives you:
|
||||
|
||||
- **Terminal resize tracking** — the container shell adjusts when you resize the window
|
||||
- **Full TTY fidelity** — colors, cursor movement, and interactive programs (vim, htop, etc.)
|
||||
work as expected
|
||||
- **Graceful shutdown** — the container stops cleanly when the session ends
|
||||
|
||||
This is the same mechanism `docker run -it` uses under the hood.
|
||||
|
||||
### Compose fallback
|
||||
|
||||
When native TTY isn't available, sandcage delegates to `docker compose run`, which manages
|
||||
the terminal connection itself. You get the same interactive shell — the difference is
|
||||
invisible in normal use. The fallback activates automatically; no configuration needed.
|
||||
|
||||
## Which backend am I using?
|
||||
|
||||
Sandcage prints a diagnostic line to stderr when it makes its choice:
|
||||
|
||||
```
|
||||
sandcage: tty probe → crossterm raw mode OK — using bollard TTY
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```
|
||||
sandcage: tty probe → MSYS2/MinGW detected (MSYSTEM set) — using compose
|
||||
```
|
||||
|
||||
These messages appear once at session start and don't affect your shell.
|
||||
|
||||
## When does the fallback activate?
|
||||
|
||||

|
||||
|
||||
The detection runs through these checks in order:
|
||||
|
||||
| Check | Triggers fallback when… |
|
||||
|---|---|
|
||||
| `compose_backend` config | Set to `"compose"` in `sandcage.toml` — forces compose unconditionally |
|
||||
| MSYSTEM environment variable (Windows) | Present — indicates Git Bash or MSYS2, whose terminal emulator can't handle raw console mode |
|
||||
| Console handle (Windows) | Invalid — stdin is piped or redirected, not a real terminal |
|
||||
| Raw mode probe (all platforms) | Fails — terminal doesn't support raw mode |
|
||||
|
||||
If none of these trigger, sandcage uses native TTY.
|
||||
|
||||
## Exiting the session
|
||||
|
||||
In native TTY mode, your keystrokes are relayed directly to the container shell. This means:
|
||||
|
||||
- **Ctrl+D** or typing `exit` ends the session (same as any shell)
|
||||
- **Ctrl+C** interrupts the current command inside the container — it does *not* kill the
|
||||
session. This matches `docker run -it` behavior.
|
||||
|
||||
In compose fallback mode, Ctrl+C ends the session immediately.
|
||||
|
||||
## Forcing compose mode
|
||||
|
||||
If you run into rendering issues in an unusual terminal, you can force the compose backend
|
||||
by adding this to your `sandcage.toml`:
|
||||
|
||||
```toml
|
||||
compose_backend = "compose"
|
||||
```
|
||||
|
||||
This bypasses all detection and always uses `docker compose run` for interactive sessions.
|
||||
|
||||
## Windows terminal compatibility
|
||||
|
||||
| Terminal | Backend used |
|
||||
|---|---|
|
||||
| Windows Terminal / PowerShell | Native TTY |
|
||||
| CMD (Command Prompt) | Native TTY |
|
||||
| Git Bash (MinTTY) | Compose fallback |
|
||||
| MSYS2 / MinGW shells | Compose fallback |
|
||||
| WSL | Native TTY |
|
||||
|
||||
On macOS and Linux, native TTY is used in all standard terminals (iTerm2, Terminal.app,
|
||||
GNOME Terminal, Alacritty, kitty, etc.). The compose fallback would only activate if stdin
|
||||
is piped or the terminal is otherwise non-interactive.
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
# SSH Key Access
|
||||
|
||||
## Overview
|
||||
|
||||
Containers launched by sandcage often need SSH access to clone private
|
||||
repositories or push code. Since the agent user inside the container does not
|
||||
have access to your host SSH keys by default, sandcage provides a setup command
|
||||
that copies or mounts the relevant keys into the container environment.
|
||||
|
||||
## Modes
|
||||
|
||||
Sandcage supports two SSH modes, configured via the `ssh_mode` field:
|
||||
|
||||
### Volume mode (recommended)
|
||||
|
||||
```
|
||||
ssh_mode: volume
|
||||
```
|
||||
|
||||
A Docker named volume (`sandcage-ssh`) is created and populated with only the
|
||||
keys required for your git remotes. The volume is mounted read-only at
|
||||
`/home/agent/.ssh` inside the container. A synthesized SSH config is written
|
||||
into the volume so the agent can connect to the correct hosts with the correct
|
||||
keys.
|
||||
|
||||
Advantages:
|
||||
|
||||
- Only selected keys are copied (not your entire `~/.ssh` directory).
|
||||
- A minimal SSH config is synthesized from your host config.
|
||||
- The volume persists across container restarts without re-reading host files.
|
||||
- Works identically on Linux, macOS, and Windows (Docker Desktop).
|
||||
|
||||
### Bind mode
|
||||
|
||||
```
|
||||
ssh_mode: bind
|
||||
```
|
||||
|
||||
Your host `~/.ssh` directory is bind-mounted read-only into the container at
|
||||
`/home/agent/.ssh:ro`. This exposes all files in that directory to the
|
||||
container.
|
||||
|
||||
Advantages:
|
||||
|
||||
- Zero setup after initial configuration.
|
||||
- Changes to host keys are immediately visible.
|
||||
|
||||
Disadvantages:
|
||||
|
||||
- Exposes all keys and config, not just the ones needed.
|
||||
- On some platforms, file permission translation can cause issues.
|
||||
|
||||
### None
|
||||
|
||||
```
|
||||
ssh_mode: none
|
||||
```
|
||||
|
||||
No SSH mount is added. Use this if you do not need SSH inside containers or
|
||||
handle key injection through other means.
|
||||
|
||||
## Setup workflow
|
||||
|
||||
Run the interactive setup:
|
||||
|
||||
```
|
||||
sandcage setup ssh
|
||||
```
|
||||
|
||||
The command:
|
||||
|
||||
1. Scans `~/.ssh/config` (including `Include` directives) for `Host` blocks
|
||||
with `User git`.
|
||||
2. Runs `git remote -v` in the current directory to discover SSH remote hosts.
|
||||
3. Merges the two sources and displays discovered host/key pairs.
|
||||
4. Prompts you to select which keys to copy.
|
||||
5. Asks whether to include `known_hosts`.
|
||||
6. Populates the `sandcage-ssh` Docker volume with the selected keys and a
|
||||
synthesized SSH config.
|
||||
7. Writes the configuration to the appropriate config file.
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Effect |
|
||||
|------|--------|
|
||||
| `--global` | Write to `~/.sandcage/config.toml` instead of project-local config |
|
||||
| `--yes` | Accept all defaults without prompting |
|
||||
| `--bind` | Use bind-mount mode instead of volume mode |
|
||||
| `--refresh` | Re-populate the volume from existing config (no interactive selection) |
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
# Interactive setup for current project
|
||||
sandcage setup ssh
|
||||
|
||||
# Non-interactive, global config
|
||||
sandcage setup ssh --global --yes
|
||||
|
||||
# Legacy bind-mount mode
|
||||
sandcage setup ssh --bind
|
||||
|
||||
# Refresh volume after adding a new key to ~/.ssh
|
||||
sandcage setup ssh --refresh
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Project config (`.sandcage/local/sandcage.local.yml` or `.sandcage/sandcage.yml`)
|
||||
|
||||
```yaml
|
||||
ssh_mode: volume
|
||||
ssh_keys:
|
||||
- host: github.com
|
||||
identity_file: ~/.ssh/id_ed25519
|
||||
- host: gitea
|
||||
identity_file: ~/.ssh/work_gitea
|
||||
```
|
||||
|
||||
### Global config (`~/.sandcage/config.toml`)
|
||||
|
||||
```toml
|
||||
ssh_mode = "volume"
|
||||
|
||||
[[ssh_keys]]
|
||||
host = "github.com"
|
||||
identity_file = "~/.ssh/id_ed25519"
|
||||
|
||||
[[ssh_keys]]
|
||||
host = "gitea"
|
||||
identity_file = "~/.ssh/work_gitea"
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `ssh_mode` | string | One of `volume`, `bind`, or `none` |
|
||||
| `ssh_keys` | array | List of host/key pairs to include in the volume |
|
||||
| `ssh_keys[].host` | string | SSH host alias (matches `Host` line in SSH config) |
|
||||
| `ssh_keys[].identity_file` | string | Path to the private key (supports `~/` expansion) |
|
||||
|
||||
Configuration layers are merged in order: global, project, local. Later layers
|
||||
override earlier ones.
|
||||
|
||||
## Refreshing keys
|
||||
|
||||
After adding or rotating SSH keys on the host, update the container volume:
|
||||
|
||||
```
|
||||
sandcage setup ssh --refresh
|
||||
```
|
||||
|
||||
This re-reads the existing `ssh_keys` configuration and the current
|
||||
`~/.ssh/config`, then repopulates the `sandcage-ssh` Docker volume with fresh
|
||||
copies.
|
||||
|
||||
For bind mode, no refresh is needed since the host directory is mounted
|
||||
directly.
|
||||
|
||||
## Security considerations
|
||||
|
||||
- **Volume mode** copies only the selected private keys into a Docker volume.
|
||||
The volume is mounted read-only into containers. Keys are owned by the
|
||||
`agent` user with `600` permissions.
|
||||
- **Bind mode** mounts the entire `~/.ssh` directory read-only. The container
|
||||
can read all keys and config present in that directory.
|
||||
- Neither mode exposes keys to other containers or Docker networks. Keys exist
|
||||
only on the filesystem inside the running container.
|
||||
- The `sandcage-ssh` volume persists on disk until explicitly removed
|
||||
(`docker volume rm sandcage-ssh`). Run `--refresh` or delete the volume when
|
||||
decommissioning keys.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Volume not found
|
||||
|
||||
```
|
||||
sandcage: SSH volume not found — run 'sandcage setup ssh --refresh' to populate it
|
||||
```
|
||||
|
||||
The `sandcage-ssh` volume does not exist or was removed. Re-run:
|
||||
|
||||
```
|
||||
sandcage setup ssh --refresh
|
||||
```
|
||||
|
||||
Or, if no config exists yet:
|
||||
|
||||
```
|
||||
sandcage setup ssh
|
||||
```
|
||||
|
||||
### Permission denied inside container
|
||||
|
||||
If the agent cannot read keys, the volume population step may have failed or
|
||||
the image does not have an `agent` user at the expected UID. Verify:
|
||||
|
||||
```
|
||||
docker run --rm -v sandcage-ssh:/home/agent/.ssh:ro sandcage:latest ls -la /home/agent/.ssh
|
||||
```
|
||||
|
||||
Private keys should be `600` and owned by `agent:agent`.
|
||||
|
||||
### No git SSH hosts found
|
||||
|
||||
```
|
||||
sandcage: no git SSH hosts found in remotes or ~/.ssh/config
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- The current directory has no SSH git remotes (all remotes use HTTPS).
|
||||
- `~/.ssh/config` has no `Host` blocks with `User git`.
|
||||
|
||||
Add an SSH remote or configure your SSH config, then re-run setup.
|
||||
|
||||
### Migration from legacy bind mount
|
||||
|
||||
If your config contains a raw `~/.ssh:/home/agent/.ssh:ro` entry in `mounts`
|
||||
without an `ssh_mode` field, sandcage detects this as a legacy configuration
|
||||
and offers to migrate to volume mode. Accepting the migration removes the old
|
||||
mount entry and writes `ssh_mode: volume` with discovered keys. Declining sets
|
||||
`ssh_mode: bind` explicitly to suppress future prompts.
|
||||
Reference in New Issue
Block a user