Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions claude-code/.claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"permissions": {
"allow": [
"Bash(bun:*)",
"Bash(npx:*)",
"Bash(git:*)",
"Bash(mise:*)",
"Bash(gh:*)",
"Bash(curl:*)",
"Bash(ls:*)",
"Bash(mkdir:*)",
"Bash(cat:*)",
"Bash(find:*)"
]
}
}
25 changes: 10 additions & 15 deletions claude-code/.devcontainer/claude-sandbox/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,24 +65,19 @@
}
},
// Named volumes keep node_modules OFF the host machine and persist across rebuilds.
// Each workspace with a package.json needs its own volume mount — without one,
// node_modules lands in the bind mount and shows up on the host filesystem.
// Dirs are pre-created in the image with node:node ownership, so fresh volumes
// inherit correct permissions via Docker volume population.
//
// Customize the monorepo mounts below to match your project structure.
// Remove any that don't exist in your project.
// Uses ${localWorkspaceFolderBasename} so default and sandbox variants share volumes.
// Replace "myproject" with your project name (must match across both variants).
"mounts": [
// ── node_modules isolation (one per workspace) ─────────────────────
"source=sandbox-node-modules-root-${devcontainerId},target=/workspace/node_modules,type=volume",
"source=sandbox-node-modules-api-${devcontainerId},target=/workspace/services/api/node_modules,type=volume",
"source=sandbox-node-modules-app-${devcontainerId},target=/workspace/services/app/node_modules,type=volume",
"source=sandbox-node-modules-www-${devcontainerId},target=/workspace/services/www/node_modules,type=volume",
"source=sandbox-node-modules-shared-${devcontainerId},target=/workspace/packages/shared/node_modules,type=volume",
"source=sandbox-node-modules-database-${devcontainerId},target=/workspace/packages/database/node_modules,type=volume",
// Each workspace with a package.json needs its own volume mount.
// Without one, node_modules lands in the bind mount and shows up on the host.
"source=myproject-node-modules-root-${localWorkspaceFolderBasename},target=/workspace/node_modules,type=volume",
// ── Monorepo: uncomment and customize for your structure ──────────
// "source=myproject-node-modules-api-${localWorkspaceFolderBasename},target=/workspace/services/api/node_modules,type=volume",
// "source=myproject-node-modules-web-${localWorkspaceFolderBasename},target=/workspace/apps/web/node_modules,type=volume",
// ── Persistent config ──────────────────────────────────────────────
"source=sandbox-fish-${devcontainerId},target=/home/node/.local/share/fish,type=volume",
"source=sandbox-config-${devcontainerId},target=/home/node/.claude,type=volume",
"source=myproject-claude-config-${localWorkspaceFolderBasename},target=/home/node/.claude,type=volume",
"source=myproject-fish-data-${localWorkspaceFolderBasename},target=/home/node/.local/share/fish,type=volume",
// ── Firewall script ────────────────────────────────────────────────
// The image provides iptables/ipset packages and sudo rule but NOT the script itself.
// Each project provides its own script via bind mount to customize the domain allowlist.
Expand Down
2 changes: 2 additions & 0 deletions claude-code/.devcontainer/claude-sandbox/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Change "myproject" to match your default variant's project name
name: myproject-sandbox
services:
devcontainer:
image: ghcr.io/gatezh/devcontainer-images/claude-code-sandbox:latest
Expand Down
25 changes: 10 additions & 15 deletions claude-code/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,24 +67,19 @@
}
},
// Named volumes keep node_modules OFF the host machine and persist across rebuilds.
// Each workspace with a package.json needs its own volume mount — without one,
// node_modules lands in the bind mount and shows up on the host filesystem.
// Dirs are pre-created in the image with node:node ownership, so fresh volumes
// inherit correct permissions via Docker volume population.
//
// Customize the monorepo mounts below to match your project structure.
// Remove any that don't exist in your project.
// Uses ${localWorkspaceFolderBasename} so default and sandbox variants share volumes.
// Replace "myproject" with your project name (must match across both variants).
"mounts": [
// ── node_modules isolation (one per workspace) ─────────────────────
"source=myproject-node-modules-root-${devcontainerId},target=/workspace/node_modules,type=volume",
"source=myproject-node-modules-api-${devcontainerId},target=/workspace/services/api/node_modules,type=volume",
"source=myproject-node-modules-app-${devcontainerId},target=/workspace/services/app/node_modules,type=volume",
"source=myproject-node-modules-www-${devcontainerId},target=/workspace/services/www/node_modules,type=volume",
"source=myproject-node-modules-shared-${devcontainerId},target=/workspace/packages/shared/node_modules,type=volume",
"source=myproject-node-modules-database-${devcontainerId},target=/workspace/packages/database/node_modules,type=volume",
// Each workspace with a package.json needs its own volume mount.
// Without one, node_modules lands in the bind mount and shows up on the host.
"source=myproject-node-modules-root-${localWorkspaceFolderBasename},target=/workspace/node_modules,type=volume",
// ── Monorepo: uncomment and customize for your structure ──────────
// "source=myproject-node-modules-api-${localWorkspaceFolderBasename},target=/workspace/services/api/node_modules,type=volume",
// "source=myproject-node-modules-web-${localWorkspaceFolderBasename},target=/workspace/apps/web/node_modules,type=volume",
// ── Persistent config ──────────────────────────────────────────────
"source=myproject-claude-config-${devcontainerId},target=/home/node/.claude,type=volume",
"source=myproject-fish-data-${devcontainerId},target=/home/node/.local/share/fish,type=volume"
"source=myproject-claude-config-${localWorkspaceFolderBasename},target=/home/node/.claude,type=volume",
"source=myproject-fish-data-${localWorkspaceFolderBasename},target=/home/node/.local/share/fish,type=volume"
],
"containerEnv": {
"TZ": "${localEnv:TZ:America/Edmonton}",
Expand Down
2 changes: 2 additions & 0 deletions claude-code/.devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Change "myproject" to your project name — must be unique across projects
name: myproject
services:
devcontainer:
image: ghcr.io/gatezh/devcontainer-images/claude-code:latest
Expand Down
27 changes: 23 additions & 4 deletions claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ The image rebuilds daily at 5am MT (11:00 UTC) using native runners for both amd

### Default variant

Copy the example files into your project's `.devcontainer/` directory and customize as needed. Docker Compose with `pull_policy: always` ensures "Rebuild Without Cache" always pulls the latest image. All other config stays in `devcontainer.json` using cross-orchestrator properties (`mounts`, `containerEnv`, `capAdd`, `init`) so you keep devcontainer variable substitution (`${devcontainerId}`, `${localEnv:...}`).
Copy the example files into your project's `.devcontainer/` directory and customize as needed. Docker Compose with `pull_policy: always` ensures "Rebuild Without Cache" always pulls the latest image. All other config stays in `devcontainer.json` using cross-orchestrator properties (`mounts`, `containerEnv`, `capAdd`, `init`) so you keep devcontainer variable substitution (`${localWorkspaceFolderBasename}`, `${localEnv:...}`).

**After copying:** replace `myproject` with your project name in `docker-compose.yml` (the `name:` field) and `devcontainer.json` (volume mount prefixes). This must match across both variants if using the sandbox.

Copy these to your project's `.devcontainer/`:

Expand All @@ -63,7 +65,9 @@ Copy these to your project's `.devcontainer/claude-sandbox/`:
- [`.devcontainer/claude-sandbox/docker-compose.yml`](.devcontainer/claude-sandbox/docker-compose.yml) — sandbox image reference
- [`.devcontainer/claude-sandbox/devcontainer.json`](.devcontainer/claude-sandbox/devcontainer.json) — full config with `NET_ADMIN`/`NET_RAW` capabilities, Claude Dark theme, `claudeCode.allowDangerouslySkipPermissions`, node_modules volume isolation, firewall script bind mount, and `CLAUDE_CODE_OAUTH_TOKEN` injection

**Sandbox differences from default:** `capAdd` for iptables, `postStartCommand` runs the firewall script, `claudeCode.allowDangerouslySkipPermissions` enabled, and OAuth token must be injected from the host (see [Sandbox Authentication](#sandbox-authentication)). Both variants use the same node_modules volume isolation pattern.
**Sandbox differences from default:** `capAdd` for iptables, `postStartCommand` runs the firewall script, `claudeCode.allowDangerouslySkipPermissions` enabled, and OAuth token must be injected from the host (see [Sandbox Authentication](#sandbox-authentication)).

**Shared volumes:** Both variants use `${localWorkspaceFolderBasename}` in volume names, so they share node_modules, Claude config, and fish history. Install packages in one variant and both benefit. Docker named volumes support multi-container access, so both can run simultaneously — just avoid running `bun install` in both at the same time.

## Project Setup Guide

Expand Down Expand Up @@ -160,6 +164,18 @@ CLAUDE_CODE_OAUTH_TOKEN=your-token-here

Add `.env.local` to `.gitignore`. Note: Docker Compose fails to start if `.env.local` doesn't exist when using `env_file` (set `required: false` in compose to make it optional).

### Recommended additional extensions

The template includes core extensions (Claude Code, Bun, OXC, Tailwind, YAML, Docker, Markdown Preview). These are commonly added by consumer projects:

| Extension | Purpose |
|-----------|---------|
| `streetsidesoftware.code-spell-checker` | Catch typos |
| `christian-kohler.npm-intellisense` | Autocomplete npm imports |
| `mattpocock.ts-error-translator` | Human-readable TS errors |
| `eamodio.gitlens` | Git blame, history, annotations |
| `ms-playwright.playwright` | Playwright test runner |

### Complete file structure

```
Expand All @@ -174,14 +190,15 @@ Add `.env.local` to `.gitignore`. Note: Docker Compose fails to start if `.env.l
├── .env.example ← template for auth token (checked in)
└── .env.local ← actual auth token (gitignored)
.claude/
├── settings.json ← permission allowlists for common dev commands
└── skills/
└── sandbox-fetch-docs/
└── SKILL.md ← teaches Claude Code to fetch docs within sandbox firewall
```

## Workspace Directory Layout

The image pre-creates these directories with `node:node` ownership so Docker's volume population seeds fresh named volumes with correct permissions:
The image pre-creates a common monorepo directory structure with `node:node` ownership so Docker's volume population seeds fresh named volumes with correct permissions:

```
/workspace/
Expand All @@ -195,7 +212,9 @@ The image pre-creates these directories with `node:node` ownership so Docker's v
└── database/node_modules/
```

If your project has additional services, add volume mounts in `devcontainer.json` `mounts` — Docker creates directories at container start. The `sudo chown` in `updateContentCommand` fixes ownership.
The template only mounts root `node_modules` by default. For monorepo projects, uncomment and customize the additional volume mounts in `devcontainer.json` to match your structure. The pre-created directories ensure correct ownership when you add mounts.

The `sudo find` in `updateContentCommand` chowns all `node_modules` directories in one pass, so additional mounts are handled automatically.

## Playwright Version Strategy

Expand Down