# 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, npm - **Task runner:** just (installed from upstream binary) - **Python tooling:** uv (installed per-user at `/home/agent/.local/bin`) The image creates an `agent` user (UID 1000) with a home at `/home/agent`, zsh as the default shell, and passwordless sudo. 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--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.yml`) ```yaml 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.