# DevCage AI Sandbox

A Docker-based sandboxed environment for running AI coding agents securely. Supports Claude Code, Gemini CLI, Codex CLI, Aider, GitHub Copilot CLI, and Goose — all lazily installed on first use. Provides an isolated container with pre-installed language runtimes (Node.js, Python, Java, Go, Rust, PHP, .NET, C/C++), dev tools, and an optional Docker-in-Docker sidecar — hardened with dropped capabilities, UID matching, and LAN blocking.

## Prerequisites

- **Docker** (Docker Desktop on macOS/Windows, or Docker Engine on Linux) — preferred
- **Podman** (v3+ with `podman compose`) — supported as automatic fallback when Docker is not installed
- No admin/root/sudo required — just Docker or Podman access
  - **macOS/Windows**: Docker Desktop (any user that can launch it)
  - **Linux**: user must be in the `docker` group (`sudo usermod -aG docker $USER`), or have Podman installed

## Installation

Install once to get a global `devcage` command — no admin/root/sudo required.

### After Installation

Open a **new terminal** and use:
```bash
devcage              # bash shell inside sandbox
devcage claude       # launch Claude Code
devcage gemini       # launch Gemini CLI
devcage codex        # launch Codex CLI
devcage aider        # launch Aider
devcage copilot      # launch GitHub Copilot CLI
devcage goose        # launch Goose
```

To uninstall:
```bash
# macOS / Linux (script installer)
./installers/scripting/uninstall-macos.sh

# macOS (.pkg installer)
~/.devcage/uninstall-pkg.sh          # user-scoped
/usr/local/share/devcage/uninstall-pkg.sh  # system-wide (sudo)

# Windows (PowerShell, script installer)
.\installers\scripting\uninstall-windows.ps1

# Windows (MSI) — use Add/Remove Programs or msiexec
```

> **Note:** Installation is optional. You can also run directly from a cloned repo with `devcage` as shown below.

## Quick Start

```bash
# When installed or portable in Windows
devcage

# or when running portable in MacOS/Linux
./devcage.sh

# Launch AI coding agents
devcage claude                # Claude Code
devcage gemini                # Gemini CLI
devcage codex                 # Codex CLI
devcage aider                 # Aider
devcage copilot               # GitHub Copilot CLI
devcage goose                # Goose

# Launch in auto-approve mode (append -yolo to any agent name)
devcage claude-yolo

# Pass arguments to an agent
devcage claude -p "explain this"

# Run any command
devcage python3 --version

# Use a specific directory as workspace
devcage --path ~/my-project claude
```

> **Note:** Do not run `docker compose up` directly — use the launcher scripts which detect the container engine and set required environment variables (`WORKSPACE`, `HOST_UID`).
>
> **Podman:** If Docker is not installed, the launchers automatically fall back to Podman. The DinD sidecar is auto-skipped (Docker-specific). When both are installed, Docker takes priority.

## Options

| Flag                              | Short  | Description                                                                  | Default      |
| --------------------------------- | ------ | ---------------------------------------------------------------------------- | ------------ |
| `--embed-docker`                  | `-d`   | Enable the Docker-in-Docker sidecar                                          | DinD off     |
| `--host-agents-config MODE`       | `-hac` | Agent config mount mode: `ro` (read-only overlay), `rw` (read-write), `no`   | `ro`         |
| `--memory SIZE`                   |        | Container memory limit (absolute like `8g`, percentage like `50%`, or `auto`)| `auto`       |
| `--cpus N`                        |        | Container CPU limit (absolute like `4`, percentage like `50%`, or `auto`)    | `auto`       |
| `--path DIR`                      |        | Override workspace directory (default: current directory)                    | cwd          |
| `--path-browse`                   |        | Open a folder browse dialog to select workspace (Windows only)               | —            |
| `-p` / `--port` / `--ports`       |        | Forward container ports to host (supports `3000`, `5080:80`, `0.0.0.0:5080:80`)| none       |
| `--bind ADDR`                     |        | Bind address for forwarded ports (default for simple port syntax)            | `127.0.0.1`  |
| `--gpu [TYPE]`                    |        | Enable GPU passthrough (`nvidia`, `amd`, or auto-detect)                     | off          |
| `--no-gpu`                        |        | Explicitly disable GPU (useful to override `switches.conf`)                  | —            |
| `--git-auth`                      | `-git` | Inherit host git config and credentials (read-only)                          | off          |
| `--apt-packages "PKG..."`         |        | Pre-install additional apt packages at startup (as root)                     | none         |
| `--brew-packages "PKG..."`        |        | Pre-install Homebrew packages at startup (as user)                           | none         |
| `--snap-packages "PKG..."`        |        | Pre-install snap packages at startup (--devmode)                             | none         |
| `--env KEY=VALUE`                 | `-e`   | Inject environment variable into container (repeatable)                      | none         |
| `--volume SRC:DEST[:MODE]`        | `-V`   | Mount host directory into container (repeatable; default: read-only)         | none         |
| `--script PATH`                   | `-s`   | Additional init script sourced after all config-level scripts                | none         |
| `-P` / `--profile` / `--profiles` |        | Activate config profile(s) — single or comma-separated                       | none         |
| `--yolo-config`                   | `-yc`  | Auto-approve project-level config (init.sh, crontab)                         | prompt       |
| `--no-cache-volumes`              | `-ncv` | Disable persistent per-workspace package caches (includes dir caches)        | caches on    |
| `--cache-dirs LIST`               |        | Directory names cached on Docker volumes for ext4 perf (comma-separated)     | `node_modules` |
| `--skip-build`                    |        | Skip Docker image rebuild (use cached image)                                 | auto-build   |
| `--rebuild`                       |        | Force full image rebuild ignoring cache                                      | cached build |
| `--verbose`                       |        | Trace entrypoint execution for debugging                                     | off          |
| `--clean`                         |        | Remove containers and volumes for current workspace, then exit               | —            |
| `--clean-global`                  |        | Remove ALL DevCage containers, images, volumes, and builder cache            | —            |
| `-v` / `--version`                |        | Print version and exit                                                       | —            |
| `--help`                          |        | Print usage information                                                      | —            |

### Examples

```bash
# Enable Docker-in-Docker sidecar
devcage --embed-docker

# More resources (absolute or percentage)
devcage --memory 16g --cpus 8
devcage --memory 50% --cpus 50%

# Use a specific project directory
devcage --path ~/my-project claude

# Forward dev server ports to host browser (same port)
devcage -p 3000,5173 claude

# Map container port 80 to host port 5080
devcage -p 5080:80 claude

# Map multiple ports with different host/container values
devcage -p 5080:80,8443:443 claude

# Bind forwarded ports to all interfaces (LAN accessible)
devcage -p 3000 --bind 0.0.0.0 claude

# Full Docker syntax (overrides --bind for specific port)
devcage -p 0.0.0.0:5080:80 claude

# GPU passthrough
devcage --gpu nvidia claude       # enable NVIDIA GPU
devcage --gpu amd claude          # enable AMD GPU
devcage --gpu claude              # auto-detect GPU type

# Inherit host git config and credentials
devcage --git-auth claude

# Install extra OS packages on start
devcage --apt-packages "vim strace"
devcage --brew-packages "gh lazygit"

# Persist agent config changes back to host
devcage --host-agents-config rw claude

# Skip host agent config entirely (clean state)
devcage --host-agents-config no claude

# Mount host directories into the container
devcage --volume ~/datasets:/data claude           # read-only (default)
devcage --volume ~/models:/models:rw claude        # read-write
devcage -V ~/data1:/data1 -V ~/data2:/data2 claude # multiple

# Inject environment variables
devcage --env MY_VAR=value --env OTHER=123 claude

# Config profiles
devcage -P test claude
devcage --profiles test,prod claude

# Auto-approve project-level config
devcage --yolo-config claude

# Custom directory caching (overlay Docker volumes for native I/O perf)
devcage --cache-dirs "node_modules,target" claude
devcage --cache-dirs "" claude    # disable dir caching

# Disable persistent package caches (fresh environment, also disables dir caches)
devcage --no-cache-volumes

# Skip rebuild when image hasn't changed
devcage --skip-build

# Force full rebuild (ignore Docker cache)
devcage --rebuild

# Clean up current workspace
devcage --clean

# Remove ALL DevCage resources system-wide
devcage --clean-global
```

## Resource Allocation (Auto Mode)

When `--memory` and `--cpus` are set to `auto` (the default), the launcher divides Docker engine resources fairly across all running DevCage instances. This prevents OOM kills when multiple containers run in parallel.

### Memory & CPU Fair-Share Algorithm

```
overhead_mem = 2g  (Docker daemon, kernel, page cache, WSL2)
overhead_cpu = 2   (system services)

usable_mem = max(engine_mem - overhead_mem, 2g)
usable_cpu = max(engine_cpus - overhead_cpu, 1)

share_mem = max(floor(usable_mem / instance_count), 4g)
share_cpu = max(floor(usable_cpu / instance_count), 1)

swap = min(floor(share_mem / 2), 4g)  # 50% of memory, capped at 4g
```

**Example on a 15g / 8-CPU Docker Desktop VM:**

| Instances     | Memory each   | Swap          | CPUs each     |
| ------------- | ------------- | ------------- | ------------- |
| 1             | 13g           | +4g           | 6             |
| 2             | 6g            | +3g           | 3             |
| 3             | 4g            | +2g           | 2             |

### How It Works

1. Containers launched with `auto` resources are tagged with labels (`dev-sandbox.memory-mode=auto`, `dev-sandbox.cpu-mode=auto`)
2. On startup, the launcher counts running containers that have the matching auto-mode label
3. Computes an equal share of `(engine - overhead) / (existing + 1)`
4. **Enforces 4g floor per container** — warns if this over-commits the engine
5. Sets its own limit to that share (memory + swap with swappiness=10)
6. Runs `docker update` on existing auto-mode containers to rebalance them to the same share

Containers with explicit limits (`--memory 8g`, `--cpus 4`) are never counted or rebalanced. Override auto with explicit values anytime: `--memory 8g --cpus 4`.

For heavy builds (Cargo, large Webpack), consider `--memory 8g` or higher.

## Customization

Configuration is loaded from three levels (each overrides the previous):

| Level       | Path                                 | Scope                          |
| ----------- | ------------------------------------ | ------------------------------ |
| Global      | `config/*` (next to launcher)        | All projects                   |
| User        | `~/.devcage/config/*`                | Current user, all projects     |
| Project     | `<workspace>/.devcage/config/*`      | Current project only           |

Files at each level:

| File                | Purpose                                                                                                                                         | Merge                              |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- |
| `switches.conf`     | Persistent CLI defaults (one flag per line, e.g. `memory 16g`). CLI flags override all.                                                        | Later entries override             |
| `versions.conf`     | Pin tool versions (key=value, e.g. `NODE_VERSION=24.13.0`). Requires `--rebuild` for pre-installed; lazy-installed take effect immediately.    | Later keys override                |
| `init.sh`           | Shell script sourced at container startup. Use for env vars, aliases, etc.                                                                      | All sourced in order               |
| `.env`              | Environment variables injected into the container (KEY=VALUE). CLI `--env` overrides all.                                                       | Later values override              |
| `crontab`           | Cron entries installed as the `devcage` user's crontab. Cron daemon starts only if entries exist.                                               | Concatenated                       |
| `AGENTS.md`         | Additional instructions appended to all AI agent instruction files.                                                                              | Appended with `---` separators     |

The user-level directory (`~/.devcage/config/`) is created manually when needed. Project-level `init.sh` and `crontab` require confirmation before running (auto-approve with `--yolo-config`).

> **Do not edit files in `sandbox/`** — they are internal build files and will be overwritten on update. If you need to customize the container image, use `--apt-packages`, `--brew-packages`, or `init.sh` at any config level instead.

### Config Profiles

Profiles provide alternative configurations that can be selectively activated. They use sibling directories named `config.{profile}` alongside the base `config/`:

```
config/                       # base (always loaded)
config.test/                  # loaded when "test" profile is active
config.prod/                  # loaded when "prod" profile is active
config.test.prod/             # loaded when BOTH "test" AND "prod" are active
```

This convention applies at every scope (global, user, project). Each profile directory can contain any subset of the config files listed above.

```bash
devcage --profile test claude           # single profile
devcage --profiles test,prod claude     # multiple profiles
devcage --profile test --profile prod   # same as above (repeatable)
```

Profile names must match `[a-zA-Z0-9][a-zA-Z0-9_-]*` (no dots). Directory matching is set-based: `config.B.A/` matches the same as `config.A.B/` when both A and B are active.

### Version Pinning

Tool versions are configurable via `versions.conf` at any config level. Format: `KEY=VALUE`, `#` comments. The merged result serves two purposes:

1. **Build time** — values are passed as `--build-arg` to Docker builds (pre-installed tools like yq, shfmt, cmake, Playwright)
2. **Runtime** — the file is mounted at `/etc/devcage/versions.conf` where lazy-install wrappers read it on first use

Default (all lines commented out) = latest versions. Uncomment and set a value for reproducible installs. Per-project overrides for lazy-installed tools take effect without rebuild.

**Language selection** — use `--lang` to include only the toolsets you need (saves build time and image size):

```bash
./devcage.sh --lang java,python     # only Java and Python (others disabled)
./devcage.sh --lang none            # disable all language toolsets
```

Valid: `java`, `php`, `go` (alias: `golang`), `rust`, `cpp` (aliases: `c`, `c++`), `python` (alias: `py`), `javascript` (aliases: `ts`, `typescript`, `js`), `dotnet` (aliases: `.net`, `csharp`, `c#`, `vb`, `f#`). Case-insensitive. Omit `--lang` to include all (default); use `--lang all` to explicitly override a config-level restriction back to all. Also settable via `langs` in `switches.conf`.

> `python`, `javascript`, and `cpp` control dev tools only — Python3/pip, Node.js/npm, and gcc/g++/make remain always available.

See `config/versions.conf` for the full list of pinnable version keys.

## What's Inside

### Language Runtimes

| Runtime                | Version    | Notes                      |
| ---------------------- | ---------- | -------------------------- |
| Node.js                | 24.x       | Pre-built binary           |
| Python                 | 3.13.x     | System package             |
| Java (Temurin JDK)     | 25.x       | Pre-built binary           |
| Maven                  | 3.9.x      | Pre-built binary           |
| Gradle                 | 9.3.x      | Pre-built binary           |
| Go                     | 1.24.x     | Pre-built binary           |
| Rust                   | 1.84.x     | Via rustup                 |
| PHP                    | System     | Via apt                    |
| .NET SDK               | 10.x       | Via dotnet-install.sh      |

All runtimes are optional — select via `--lang` flag.

### Pre-installed Tools

git, Docker CLI, curl, wget, build-essential (gcc/g++/make), jq, yq, ripgrep, tree, shellcheck, shfmt, httpie, Homebrew (Linuxbrew), Playwright + Chromium

### Optional Tools (best-effort via apt)

nano, vim, neovim, micro, emacs-nox, fd, bat, fzf, tmux, htop, pandoc, watchman, wrk, git-lfs, postgresql-client

### Lazy-installed Tools

These are installed on first use via wrapper scripts — not baked into the image:

**AI CLI tools:** Claude Code, Gemini CLI, Codex CLI, Aider, GitHub Copilot CLI, Goose

**npm packages:** lighthouse, biome, typescript (tsc), tsx, serve, dotenvx, nodemon, concurrently

**Python packages (pip):** pre-commit, ruff, pytest, mypy, ipython, bashate

**Audit tools:** trivy, semgrep

**GitHub release binaries:** delta, mkcert, direnv, just, websocat, mailpit, shellharden

**Go tools:** dlv (Delve), air

**Homebrew:** gh (GitHub CLI)

**JVM tools (via SDKMAN):** sbt, groovy, kotlin

### Browsers

Chromium is pre-installed via Playwright with system dependencies. Additional browsers can be installed at runtime (`npx playwright install firefox`).

### Security Auditing

Two built-in audit commands are available inside the sandbox:

**`audit`** — Dependency and code security audit for your project. Auto-detects package managers and runs the appropriate checks:

| Check                                | When                                                                   |
| ------------------------------------ | ---------------------------------------------------------------------- |
| hadolint                             | Dockerfiles found                                                      |
| shellcheck + shellharden             | `.sh` files found                                                      |
| semgrep                              | SAST static analysis                                                   |
| SpotBugs                             | Java/Maven/Gradle projects (bug detection)                             |
| PMD                                  | Java/Maven/Gradle projects (code quality)                              |
| Checkstyle                           | Java/Maven/Gradle projects (style checking)                            |
| trivy fs                             | CVEs + committed secrets                                               |
| npm / yarn / pnpm audit              | `package.json` + lockfile present                                      |
| pip-audit                            | `requirements*.txt` / `Pipfile.lock` / `pyproject.toml` present        |
| OWASP dependency-check               | Java/Maven/Gradle projects                                             |
| govulncheck                          | `go.mod` present (auto-installed on first run)                         |
| cargo audit                          | `Cargo.toml` present (auto-installed on first run)                     |
| composer audit                       | `composer.json` present                                                |
| dotnet list package --vulnerable     | `*.csproj` / `*.sln` present                                           |

```bash
audit                           # audit /workspace (recursive)
audit ~/myapp                   # audit a specific directory
audit ~/app1 ~/app2             # audit multiple directories
audit -d 1                      # only check immediate directory (no recursion)
audit --maxdepth 2              # limit to 2 levels deep
audit --level critical          # flag critical issues only
audit --fix                     # auto-fix (npm/pnpm/pip/shellharden/semgrep)
audit --no-semgrep              # skip SAST
audit --no-trivy                # skip Trivy scan
audit --no-owasp-depcheck       # skip OWASP dependency-check
audit --no-spotbugs             # skip SpotBugs static analysis
audit --no-pmd                  # skip PMD static analysis
audit --no-checkstyle           # skip Checkstyle static analysis
audit --no-hadolint             # skip Dockerfile linting
```

**`audit-sandbox`** — Security posture and CVE audit for the DevCage sandbox itself (not your project). Verifies that the container is properly hardened:

| Check                       | What it verifies                                              |
| --------------------------- | ------------------------------------------------------------- |
| UID                         | Running as non-root `devcage` user                            |
| Linux capabilities          | All caps dropped (CapEff and CapBnd = 0)                      |
| Docker socket               | Host socket not mounted                                       |
| LAN isolation               | RFC 1918 addresses blocked by iptables                        |
| SUID/SGID bits              | All stripped by image build                                   |
| System file writability     | `/etc/passwd` etc. not writable                               |
| npm globals                 | CVE audit of globally installed npm packages                  |
| OS packages                 | CVE scan of apt-installed system packages via Trivy           |

```bash
audit-sandbox                   # full sandbox audit
audit-sandbox --level critical  # flag critical issues only
audit-sandbox --no-trivy        # skip Trivy OS package scan
```

Both commands accept `--level` to set minimum severity (`critical`, `high`, `moderate`, `low` — default: `high`) and print a pass/skip/fail summary.

## Architecture

Two-container model defined in `sandbox/docker-compose.yml`:

```
Host                          Docker
┌─────────────┐          ┌──────────────────────────────┐
│ devcage.sh  │──────────│  dev-sandbox (workspace)     │
│ devcage.ps1 │          │  (unprivileged devcage       │
│             │          │   user, all caps dropped)    │
│ ~/project ──┼── rw ───▶  /workspace                  │
│ ~/.claude ──┼── ro ───▶  /agent-config/claude        │
└─────────────┘          │                              │
                         │    TLS       ┌────────────┐  │
                         │ ◀──────────▶│    dind    │  │
                         │              │  (Docker   │  │
                         │              │   daemon)  │  │
                         │              └────────────┘  │
                         └──────────────────────────────┘
```

**Startup flow:**

1. `devcage.sh` / `devcage.ps1` detects container engine (docker preferred, podman fallback), loads hierarchical config (global → user → project + profiles), scans workspace for cache-dirs targets, sets env vars, invokes `<engine> compose run`
2. `entrypoint.sh` runs as root: remaps UID, sets up `/cache` symlinks, seeds directory cache overlays from host, overlays agent config, merges AGENTS.md, sets iptables rules, installs packages, sources init scripts, installs crontabs
3. `setpriv` drops to unprivileged `devcage` user
4. Default command is `bash`; pass `claude`, `gemini`, `codex`, `aider`, `copilot`, or `goose` to launch an AI agent

### Directory Layout

```
devcage/
├── devcage.sh / devcage.ps1 / devcage.bat   # Launchers (run these)
├── VERSION.txt                              # Single source of truth for version
├── config/                                  # ✏️ YOUR configuration — edit freely
│   ├── switches.conf                        #    Persistent defaults for flags
│   ├── versions.conf                        #    Pin tool versions
│   ├── init.sh                              #    Init script (sourced at startup)
│   └── .env                                 #    Environment variables for container
└── sandbox/                                 # ⛔ INTERNAL — do not edit
    ├── docker-compose.yml                   #    Two-service compose definition
    ├── docker-compose.gpu-nvidia.yml        #    GPU overlay (NVIDIA)
    ├── docker-compose.gpu-amd.yml           #    GPU overlay (AMD)
    └── image/                           #    Docker build context (COPYed into image)
        ├── Dockerfile                       #    Container image definition
        ├── entrypoint.sh                    #    Container init script
        ├── SANDBOX_AGENTS.md               #    In-container AI agent instructions
        ├── audit / audit-sandbox            #    Security audit scripts
        └── wrappers/                        #    Lazy-install wrapper scripts
            ├── lib/                         #    Shared helpers
            └── bin/                         #    Per-tool wrappers
```

**`config/`** — Your configuration files. Edit these to customize behavior. They are never overwritten by updates (preserved during upgrades for both script and native installers).

**`sandbox/`** — Internal container build files. Do not edit — your changes will be overwritten when you update DevCage.

## Security Model

- **Unprivileged user** — Container runs as `devcage` user, not root
- **Capabilities** — All Linux capabilities dropped; only minimal set granted to entrypoint (dropped after init)
- **Privilege escalation** — `no-new-privileges` enforced via `security_opt`
- **Network isolation** — Outbound LAN/RFC 1918 traffic blocked by iptables (IPv4 and IPv6); only Docker-internal and internet allowed
- **Host Docker socket** — Never mounted — Docker commands route through TLS-connected DinD sidecar (when enabled)
- **Config protection** — Agent config dirs mounted read-only by default (overlay-copied into container)
- **UID matching** — Container UID matched to host UID so `/workspace` files have correct ownership
- **API keys** — Passed through from host environment if set (`ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `GOOGLE_API_KEY`, `OPENAI_API_KEY`, `DEEPSEEK_API_KEY`, etc.)
- **Git credentials** — Not inherited by default — opt in with `--git-auth`
- **macOS OAuth** — Claude Code OAuth credentials extracted from Keychain at launch, mounted read-only, cleaned up on exit
- **Telemetry** — Opted out via `DISABLE_TELEMETRY=1` and `DO_NOT_TRACK=1`
- **pip** — `PIP_REQUIRE_HASHES=1` set at runtime to prevent unverified installs (override with `PIP_REQUIRE_HASHES=0` prefix)
- **DinD LAN access** — Containers spawned inside DinD bypass the workspace's iptables rules — only enable DinD when needed
- **Custom volumes** — Read-only by default; blocked destination prefixes prevent mounting over security-sensitive paths
- **Project config** — Project-level `init.sh` and `crontab` require user confirmation (auto-approve with `--yolo-config`)

## Agent Config Handling

Three modes controlled by `--host-agents-config` (`-hac`):

- **Read-only (default, `-hac ro`):** Agent config dirs are mounted under `/agent-config/` as read-only and overlay-copied into the container home. Host directories are never modified. Agent state (conversations, sessions, history) is persisted at `/workspace/.devcage/agents/<name>/` via symlinks.
- **Read-write (`-hac rw`):** Agent config dirs are mounted directly at their home paths. Settings changes persist back to the host.
- **Skip (`-hac no`):** No host agent config is mounted. The container starts with a clean agent config state.

Config directories handled: `~/.claude`, `~/.config/gemini-cli`, `~/.codex`, `~/.aider.conf.yml`, `~/.copilot`, `~/.config/goose`. If a host directory doesn't exist, a persistent Docker volume is used instead.

## GPU Passthrough

GPU passthrough is disabled by default. Use `--gpu` to opt in.

When `--gpu` is passed without a type, the launcher auto-detects the GPU runtime (`nvidia` or `amd`). On macOS, GPU detection is skipped (Docker Desktop VM has no GPU access).

GPU support is implemented via compose overlay files (`sandbox/docker-compose.gpu-nvidia.yml` or `sandbox/docker-compose.gpu-amd.yml`) merged at runtime — the base Dockerfile needs no GPU-specific layers.

```bash
devcage --gpu nvidia claude       # NVIDIA GPU
devcage --gpu amd claude          # AMD GPU
devcage --gpu claude              # auto-detect
```

Use `--no-gpu` to explicitly disable (useful to override `gpu` in `switches.conf`).

## Package Cache Volumes

Package caches are isolated per workspace. The launcher mounts a per-workspace Docker volume at `/cache`. The entrypoint symlinks standard package manager locations into it:

| Home path                          | Cache subdir       |
| ---------------------------------- | ------------------ |
| `~/.m2`                            | `maven`            |
| `~/.npm`                           | `npm`              |
| `~/.local/share/pnpm/store`        | `pnpm`             |
| `~/.cache/pip`                     | `pip`              |
| `~/.gradle`                        | `gradle`           |
| `~/.sdkman`                        | `sdkman`           |
| `~/.cache/trivy`                   | `trivy`            |
| `~/go`                             | `go`               |
| `~/.cargo`                         | `cargo`            |
| `~/.nuget`                         | `nuget`            |

On first run, image-layer content (e.g. SDKMAN) is seeded into the cache. Subsequent runs reuse cached data. Use `--no-cache-volumes` for a fresh environment. `--clean` removes the per-workspace cache volume.

## Directory Cache Volumes (cache-dirs)

When Docker Desktop on Windows mounts a host directory into a container, the path traverses two translation layers (NTFS → WSL2 VM → container bind mount). This makes small-file-heavy operations (e.g., `node_modules` with 50k+ files) 5-20x slower than native ext4. The `cache-dirs` feature mounts Docker volumes on top of these directories inside `/workspace`, giving them native ext4 performance.

```bash
devcage --cache-dirs "node_modules,target" claude   # custom cached dirs
devcage --cache-dirs "" claude                       # disable dir caching
```

**Default:** `node_modules`. Set to `""` to disable. `--no-cache-volumes` disables both package caches and dir caches.

**Auto-detection:** For `node_modules`, the launcher scans the workspace for `package.json` files (maxdepth 5, excluding paths inside `node_modules/`). Each parent directory gets a `node_modules` overlay — this handles single projects, monorepos, and nested frontends automatically. For other names (e.g., `target`), existing directories are scanned (maxdepth 3).

**Volume strategy:** Uses `volume-subpath` of the existing per-workspace cache volume (Docker 26+ / Podman 5+). Falls back to separate named volumes on older engines.

**Seeding:** The host workspace is also mounted read-only at `/workspace-host`. On first run, the entrypoint seeds each overlay from host content (a marker file `.devcage-dircache-seeded` prevents re-seeding on subsequent runs).

**Trade-off:** Overlaid directories are fast but volatile — lost on `--clean`, invisible from the host. They can always be rebuilt (`npm install`, `cargo build`, etc.).

**Status line:** When directory caches are active, the count appears in the startup status line: `Engine: docker | DinD: off | CacheDirs: 3 | Memory: 12g | CPUs: 6`

## Custom Volume Mounts

Mount arbitrary host directories into the container with `--volume` / `-V`:

```bash
devcage --volume ~/datasets:/data claude           # read-only (default)
devcage --volume ~/models:/models:rw claude        # read-write
devcage -V ~/data1:/data1 -V ~/data2:/data2 claude # multiple
```

Syntax: `SOURCE:DEST[:MODE]` where MODE is `ro` (default) or `rw`. SOURCE must be an absolute path (or `~`). Certain destination prefixes are blocked for security (`/workspace`, `/home/devcage`, `/etc`, `/proc`, `/sys`, etc.).

Volume mounts can also be configured persistently via `volume` entries in `switches.conf`. Entries accumulate across all config levels.

## Session Logging

Session logs are written to `.devcage/logs/` inside each project workspace. Each session creates a `session-YYYYMMDD-HHMMSS.log` file containing:
- Launcher metadata (version, engine, workspace, command, memory, CPUs)
- Entrypoint init diagnostics
- Post-mortem footer (exit code, status interpretation, docker events)

A `latest` symlink (bash) or `latest.txt` file (Windows) points to the most recent session. Old logs are rotated — only the last 10 are retained by default (configurable via `log-keep N` in `switches.conf`). The `.devcage/logs/` directory is auto-gitignored.

## Windows Context Menu Integration

The script installer (`installers\scripting\install-windows.ps1`) adds an Explorer context menu entry (HKCU registry, no admin):

- **"Open in DevCage"** — launches `devcage.ps1`

Appears on directory right-click and folder background right-click. Pass `-NoExplorerIntegration` to skip. On upgrades, existing entries are silently updated.

Remove with `installers\scripting\uninstall-windows.ps1`.

The MSI installer (`installers\windows\`) provides the same integration as a selectable feature.

## Image Build Strategy

AI CLI tools and most dev tools are lazily installed on first use via wrapper scripts — not baked into the image. This keeps the image small and rebuilds fast. The Dockerfile layers are ordered most-stable to least-stable to maximize Docker cache reuse.

## Troubleshooting

**"Cannot connect to docker/podman"** — Ensure Docker Desktop is running (macOS/Windows), your user is in the `docker` group (Linux), or the Podman machine is started (`podman machine start`).

**"Neither docker nor podman found"** — Install Docker (https://docs.docker.com/get-docker/) or Podman (https://podman.io/getting-started/installation).

**UID mismatch warnings** — Usually harmless on Docker Desktop (macOS/Windows) where UID mapping is handled by the VM. On Linux, the entrypoint automatically matches the container UID to your host UID.

**Missing optional tool** — Optional tools are installed best-effort. If one is missing, install it at runtime:
```bash
brew install <tool>          # system binaries (Linuxbrew, no root needed)
pip install --user <package> # Python packages (prefix with PIP_REQUIRE_HASHES=0)
npm install -g <package>     # npm packages
```

**OOM kills or over-commit warnings** — Increase Docker Desktop memory allocation:
- **macOS:** Docker Desktop > Settings > Resources > Memory (increase to 8GB+ for dev workloads)
- **Windows:** Docker Desktop > Settings > Resources, or edit `~/.wslconfig`:
  ```ini
  [wsl2]
  memory=16GB
  swap=4GB
  ```

**LAN access blocked** — By design. The sandbox blocks RFC 1918 traffic to prevent access to your local network. Internet access works normally.

**pip install fails with hash error** — `PIP_REQUIRE_HASHES=1` is set by default for security. For ad-hoc installs, prefix with `PIP_REQUIRE_HASHES=0 pip install <package>`. Projects should use virtual environments.

## License

Free for personal, educational, and evaluation use. Commercial use requires a paid license — see [devcage.com](https://devcage.com/#pricing) for details.

See [LICENSE](LICENSE) for full terms. See [THIRD-PARTY-LICENSES](THIRD-PARTY-LICENSES.md) for bundled or related component licenses.

## Trademarks

Docker is a trademark of Docker, Inc. Node.js is a trademark of the OpenJS Foundation. Java and OpenJDK are trademarks of Oracle. .NET is a trademark of Microsoft. Go is a trademark of Google. Rust is a trademark of the Rust Foundation. Ubuntu is a trademark of Canonical Ltd. Claude Code is a product of Anthropic. Gemini is a trademark of Google LLC. Codex is a trademark of OpenAI. GitHub Copilot is a product of GitHub/Microsoft. Goose is an open-source project by Block, Inc. All other trademarks are property of their respective owners. Use of these names is for identification purposes only and does not imply endorsement.

## Authors

Designed by Łukasz Nawojczyk
https://progralink.com
