# Sandboxed Claude

> Create a Podman dev container that runs Claude Code inside a sandboxed environment. Use when setting up a containerized development environment for any project, when the user asks about running Claude in a container, wants to isolate Claude's filesystem access, or needs a reproducible dev setup with Podman. Also use when the user mentions dev containers, Containerfiles, or sandboxed development workflows.

- Skill: `bensimpson-ch/sandboxed-claude` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add bensimpson-ch/sandboxed-claude`
- Raw SKILL.md: https://api.skillmd.com/api/skills/bensimpson-ch/sandboxed-claude/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: bensimpson-ch (https://skillmd.com/u/bensimpson-ch)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/bensimpson-ch/sandboxed-claude

---


# Sandboxed Claude

Create a Podman dev container where Claude Code runs inside a controlled, reproducible environment.
The container bind-mounts the project source, caches dependencies in named volumes, and persists
Claude's own configuration across restarts. One script launches the whole thing.

See [reference.md](reference.md) for annotated Containerfile and dev.sh templates.

## Why Sandbox

Running Claude Code inside a container gives you three things:

1. **Reproducibility.** Every collaborator gets the same toolchain -- same Node, same JDK, same
   Maven -- without polluting their host. No "works on my machine."
2. **Isolation.** Claude operates within the container's filesystem boundaries. It can read and
   write the project (bind-mounted) but nothing else on the host.
3. **Speed.** Named Podman volumes store dependency trees (node_modules, .m2/repository) as
   Linux-native filesystems, avoiding the overhead of macOS filesystem translation on every
   file read.

## The Two-File Pattern

Every sandboxed-claude setup produces exactly two files:

```
infra/devcontainer/
├── Containerfile   # Image definition: base + runtimes + tools + non-root user
└── dev.sh          # Launch script: volumes, mounts, container lifecycle
```

The Containerfile builds once and is reused. dev.sh handles everything else: creating volumes,
building the image on first run, tearing down stale containers, and starting a fresh interactive
session.

## Workflow

### 1. Detect the Tech Stack

Read the project to identify what runtimes and tools are needed:

| Signal | Runtime to Install |
|--------|--------------------|
| `package.json` | Node.js (check `engines` field or `.nvmrc` for version) |
| `pom.xml` | Java JDK + Maven |
| `build.gradle` / `build.gradle.kts` | Java JDK + Gradle |
| `go.mod` | Go |
| `Cargo.toml` | Rust toolchain |
| `requirements.txt` / `pyproject.toml` | Python |
| `angular.json` | Node.js + Angular CLI (match project version from devDependencies) |

Most projects need one or two runtimes. A fullstack app typically needs Node + a backend runtime.

### 2. Choose the Base Image

Pick the base that gives you the most for free:

| Primary Runtime | Base Image | Why |
|-----------------|------------|-----|
| Node.js | `node:22-bookworm` | Node pre-installed, Debian for easy apt-get of other tools |
| Python | `python:3.12-bookworm` | Python pre-installed, Debian base |
| Go | `golang:1.22-bookworm` | Go pre-installed, Debian base |
| Java only | `eclipse-temurin:21-jdk-bookworm` | JDK pre-installed |
| Rust | `rust:1.78-bookworm` | Rust toolchain pre-installed |

Use `-bookworm` (Debian 12) variants. They have `apt-get` for installing additional tools.
Alpine images are smaller but cause friction with native dependencies and missing shared libraries.

### 3. Write the Containerfile

The Containerfile follows a fixed structure. Each section has a clear purpose:

```
Base image (primary runtime)
  → System dependencies (curl, git, ssh, ca-certificates, jq)
  → Secondary runtimes (JDK, Maven, etc.)
  → CLI tools (Angular CLI, Claude Code)
  → Non-root user setup
  → Working directory + config directories
```

Principles:

- **Pin versions.** Use build ARGs for runtime versions so they're visible and changeable at
  the top of the file. Pin CLI tools to the version matching the project.
- **Clean apt caches.** End every `apt-get install` with `&& rm -rf /var/lib/apt/lists/*` to
  keep the image small.
- **Non-root user.** Create a `dev` user and switch to it before setting up workspace directories.
  Claude Code should never run as root inside the container.
- **Prepare mount points.** Create `/home/dev/.claude` and `/home/dev/.ssh` (if needed) with
  correct permissions before the user runs anything.

### 4. Write dev.sh

dev.sh is the single entry point. It handles the full container lifecycle:

```bash
#!/usr/bin/env bash
set -euo pipefail
```

The script does five things in order:

1. **Resolve paths.** Compute PROJECT_ROOT relative to the script location so it works from
   any working directory.
2. **Create named volumes.** Use `podman volume exists || podman volume create` for each
   dependency cache. Named volumes are Linux-native filesystems inside the Podman VM -- they
   bypass macOS filesystem translation entirely.
3. **Build the image.** Only if it doesn't already exist (`podman image exists`). Rebuilding
   is explicit: the user deletes the image when they want a fresh build.
4. **Remove stale container.** `podman rm -f` the previous container if it exists. Containers
   are ephemeral; volumes persist the state that matters.
5. **Launch.** `podman run -it` with all mounts and flags.

### 5. Configure Mounts and Volumes

This is where the decisions matter. There are three categories:

**Bind mounts (source code):**
```
-v "$PROJECT_ROOT:/workspace:z"
```
The entire project tree, read-write. The `:z` suffix is required for SELinux relabeling on
Podman (harmless on systems without SELinux).

**Named volumes (dependency caches):**
```
-v "myproject-node-modules:/workspace/app/node_modules:z"
-v "myproject-m2-repo:/home/dev/.m2/repository:z"
```
Each dependency tree gets its own named volume. This serves two purposes: dependencies install
fast (native filesystem, not macOS FUSE), and they persist across container restarts without
re-downloading.

Mount the volume at the *exact path* where the package manager writes dependencies:
- npm/pnpm: `<project>/node_modules`
- Maven: `~/.m2/repository`
- Gradle: `~/.gradle/caches`
- Go: `/home/dev/go/pkg/mod`
- Cargo: `/home/dev/.cargo/registry`
- pip: varies (use a venv path)

**Claude config (persistence):**
```
-v "$HOME/.claude-container/myproject:/home/dev/.claude:z"
```
Store Claude's auth tokens, settings, and conversation history in a host directory. This
survives container rebuilds. Keep it per-project under `~/.claude-container/` so different
projects don't collide.

### 6. Essential Flags

```bash
podman run -it \
    --name myproject-dev \
    --hostname myproject-dev \
    --userns keep-id \
    ...
```

- `--userns keep-id`: Maps the container's `dev` user to your host UID. Without this, files
  created inside the container are owned by a different UID on the host, causing permission
  headaches. Required on macOS.
- `--hostname`: Sets a recognizable prompt inside the container.
- `--name`: Allows `podman rm -f` by name on next launch.

### 7. Optional: SSH Key Mounting

If the project deploys via SSH or needs git access over SSH:

```bash
-v "$HOME/.ssh/id_ed25519_myproject:/home/dev/.ssh/id_ed25519:ro,z"
-e "SSH_KEY=/home/dev/.ssh/id_ed25519"
```

Mount the key read-only (`:ro`). If the project deploys through CI/CD (GitHub Actions, etc.),
skip this entirely.

### 8. Print First-Run Instructions

dev.sh should print what the user needs to do on first launch:

```
First run: execute inside the container:
  cd /workspace/app && npm ci
  cd /workspace/services && mvn dependency:go-offline
  claude auth login
```

These commands populate the named volumes. After the first run, subsequent launches are instant.

## Adaptation Checklist

When creating a sandboxed-claude setup for a new project:

- [ ] Read package.json / pom.xml / go.mod to identify runtimes and versions
- [ ] Choose base image matching the primary runtime
- [ ] Pin CLI tool versions to match the project (e.g., Angular CLI from devDependencies)
- [ ] Create one named volume per dependency cache
- [ ] Mount node_modules at the exact path (watch for monorepo structures)
- [ ] Set `--userns keep-id` for macOS compatibility
- [ ] Include `claude auth login` in first-run instructions
- [ ] Test: build the image, launch, run `npm ci` / `mvn compile`, verify Claude Code works

## What This Skill Does Not Cover

- **Production containers.** This skill is about dev environments. Production Dockerfiles are
  multi-stage builds optimized for size; dev containers are optimized for tooling.
- **Docker Compose for services.** If the project needs a database or message broker alongside
  the dev container, that's a separate compose file. The dev container is for the human (and
  Claude) to write code in.
- **VS Code devcontainers.** The `.devcontainer/devcontainer.json` format is IDE-specific. This
  skill produces standalone Podman containers that work from any terminal.

