diff --git a/claude-code/.claude/settings.json b/claude-code/.claude/settings.json new file mode 100644 index 0000000..7ae8a69 --- /dev/null +++ b/claude-code/.claude/settings.json @@ -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:*)" + ] + } +} diff --git a/claude-code/.devcontainer/claude-sandbox/devcontainer.json b/claude-code/.devcontainer/claude-sandbox/devcontainer.json index 783cb6a..839e2ad 100644 --- a/claude-code/.devcontainer/claude-sandbox/devcontainer.json +++ b/claude-code/.devcontainer/claude-sandbox/devcontainer.json @@ -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. diff --git a/claude-code/.devcontainer/claude-sandbox/docker-compose.yml b/claude-code/.devcontainer/claude-sandbox/docker-compose.yml index 567b430..5a06278 100644 --- a/claude-code/.devcontainer/claude-sandbox/docker-compose.yml +++ b/claude-code/.devcontainer/claude-sandbox/docker-compose.yml @@ -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 diff --git a/claude-code/.devcontainer/devcontainer.json b/claude-code/.devcontainer/devcontainer.json index 40387aa..f44e5cf 100644 --- a/claude-code/.devcontainer/devcontainer.json +++ b/claude-code/.devcontainer/devcontainer.json @@ -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}", diff --git a/claude-code/.devcontainer/docker-compose.yml b/claude-code/.devcontainer/docker-compose.yml index f5647c3..bcab1d7 100644 --- a/claude-code/.devcontainer/docker-compose.yml +++ b/claude-code/.devcontainer/docker-compose.yml @@ -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 diff --git a/claude-code/README.md b/claude-code/README.md index 31ffd9e..4aac3b4 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -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/`: @@ -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 @@ -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 ``` @@ -174,6 +190,7 @@ 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 @@ -181,7 +198,7 @@ Add `.env.local` to `.gitignore`. Note: Docker Compose fails to start if `.env.l ## 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/ @@ -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