diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 3743bf8..455e0ba 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -142,6 +142,8 @@ LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainer-im org.opencontainers.image.title="devcontainer-images" \ org.opencontainers.image.url="https://github.com/gatezh/devcontainer-images" +CMD ["sleep", "infinity"] + # ─── SANDBOX — network-restricted environment ───────────────────────────────── FROM base AS sandbox @@ -168,3 +170,5 @@ LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainer-im org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.title="devcontainer-images-sandbox" \ org.opencontainers.image.url="https://github.com/gatezh/devcontainer-images" + +CMD ["sleep", "infinity"] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 681171d..361b98a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -45,7 +45,13 @@ "source.organizeImports": "explicit" }, // Show workspace folder name in window title - "window.title": "${localWorkspaceFolderBasename}" + "window.title": "${localWorkspaceFolderBasename}", + // Visual identity — Claude Dark theme with coral remote indicator + "workbench.colorTheme": "Claude Dark", + "workbench.colorCustomizations": { + "statusBarItem.remoteBackground": "#C15F3C", + "statusBarItem.remoteForeground": "#ffffff" + } } } }, @@ -60,7 +66,11 @@ "CLAUDE_CONFIG_DIR": "/home/node/.claude", "NODE_OPTIONS": "--max-old-space-size=4096" }, - // Initialize Claude Code plugins (runs once when container is created) - "postCreateCommand": "sudo chown -R node /home/node/.claude && bash /workspace/.devcontainer/init-plugins.sh", + // Fix volume ownership — must complete before extensions install. + // Do NOT run claude CLI commands here: postCreateCommand runs before + // VS Code installs extensions, so claude commands race with the + // Claude Code extension's OAuth flow and can corrupt auth state. + // Run .devcontainer/init-plugins.sh manually after first login. + "postCreateCommand": "sudo chown -R node /home/node/.claude /home/node/.local/share/fish", "waitFor": "postCreateCommand" } diff --git a/claude-code/.devcontainer/Dockerfile b/claude-code/.devcontainer/Dockerfile index be49f4f..fba3503 100644 --- a/claude-code/.devcontainer/Dockerfile +++ b/claude-code/.devcontainer/Dockerfile @@ -217,13 +217,14 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ dnsutils \ aggregate -# Firewall sudo rule for node user. -# The init-firewall.sh script is NOT baked into the image — each project -# mounts its own script via bind mount in devcontainer.json: -# "source=${localWorkspaceFolder}/.devcontainer/claude-sandbox/init-firewall.sh,target=/usr/local/bin/init-firewall.sh,type=bind" -# This allows different projects to define their own domain allowlists. -RUN echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall \ - && chmod 0440 /etc/sudoers.d/node-firewall +# Passwordless sudo for node user — needed for: +# - "sudo chown" on named volumes (node_modules isolation) +# - "sudo /usr/local/bin/init-firewall.sh" (firewall setup) +# Sandbox security comes from the network firewall, not sudo restrictions. +# The firewall script is NOT baked into the image — each project mounts its +# own script via bind mount in devcontainer.json to customize the domain allowlist. +RUN echo "node ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/node-nopasswd \ + && chmod 0440 /etc/sudoers.d/node-nopasswd USER node LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainer-images" \ diff --git a/claude-code/.devcontainer/claude-sandbox/devcontainer.json b/claude-code/.devcontainer/claude-sandbox/devcontainer.json new file mode 100644 index 0000000..783cb6a --- /dev/null +++ b/claude-code/.devcontainer/claude-sandbox/devcontainer.json @@ -0,0 +1,106 @@ +// For format details, see https://aka.ms/devcontainer.json +{ + "name": "Claude Code Sandbox", + "dockerComposeFile": "docker-compose.yml", + "service": "devcontainer", + "workspaceFolder": "/workspace", + // Capabilities required for iptables firewall setup + "capAdd": ["NET_ADMIN", "NET_RAW"], + "init": true, + "updateRemoteUserUID": true, + "remoteUser": "node", + "otherPortsAttributes": { "onAutoForward": "silent" }, + "customizations": { + "vscode": { + "extensions": [ + // **AI Agents** + // Claude Code - AI coding assistant with chat, inline edits, and terminal integration + "anthropic.claude-code", + + // **Runtime** + // Bun - Bun runtime support (debugging, lockfile viewer, bundler integration) + "oven.bun-vscode", + + // **Code Quality** + // OXC - Fast linter and formatter for JavaScript/TypeScript (Rust-based) + "oxc.oxc-vscode", + + // **Tailwind** + // Tailwind CSS IntelliSense - autocomplete, syntax highlighting, linting for Tailwind classes + "bradlc.vscode-tailwindcss", + // Tailwind Fold - collapse long Tailwind class strings in the editor for readability + "stivo.tailwind-fold", + + // **General** + // YAML - YAML language support (for workflows and docker-compose) + "redhat.vscode-yaml", + // Markdown Preview Github Styles - renders Markdown preview with GitHub's CSS + "bierner.markdown-preview-github-styles", + // Docker - Dockerfile and Compose syntax, linting, and image management + "ms-azuretools.vscode-docker" + ], + "settings": { + "terminal.integrated.defaultProfile.linux": "fish", + "terminal.integrated.profiles.linux": { + "fish": { "path": "fish" }, + "bash": { "path": "bash", "icon": "terminal-bash" } + }, + "extensions.ignoreRecommendations": true, + "editor.formatOnSave": true, + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "window.title": "${localWorkspaceFolderBasename}", + // Sandbox visual identity — Claude Dark theme with coral remote indicator + "workbench.colorTheme": "Claude Dark", + "workbench.colorCustomizations": { + "statusBarItem.remoteBackground": "#C15F3C", + "statusBarItem.remoteForeground": "#ffffff" + }, + // Allow Claude Code to skip permission prompts in sandbox + "claudeCode.allowDangerouslySkipPermissions": true + } + } + }, + // 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. + "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", + // ── Persistent config ────────────────────────────────────────────── + "source=sandbox-fish-${devcontainerId},target=/home/node/.local/share/fish,type=volume", + "source=sandbox-config-${devcontainerId},target=/home/node/.claude,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. + "source=${localWorkspaceFolder}/.devcontainer/claude-sandbox/init-firewall.sh,target=/usr/local/bin/init-firewall.sh,type=bind" + ], + "containerEnv": { + "TZ": "${localEnv:TZ:America/Edmonton}", + "DEVCONTAINER": "true", + "NODE_OPTIONS": "--max-old-space-size=4096", + "CLAUDE_CONFIG_DIR": "/home/node/.claude", + // Required — the sandbox firewall blocks OAuth login, so the token must be + // injected from the host. See "Sandbox Authentication" section in README. + "CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN}" + }, + // Runs before postStartCommand (firewall), so network is still available for browser downloads. + // The find command chowns all node_modules volume mount points in one pass. + "postCreateCommand": "sudo find /workspace -maxdepth 4 -name node_modules -type d -exec chown node {} + && sudo chown -R node /home/node/.claude && mise install && bun install && npx playwright install --only-shell", + // Firewall init — script is bind-mounted from the project + "postStartCommand": "sudo /usr/local/bin/init-firewall.sh", + "waitFor": "postStartCommand" +} diff --git a/claude-code/.devcontainer/claude-sandbox/docker-compose.yml b/claude-code/.devcontainer/claude-sandbox/docker-compose.yml new file mode 100644 index 0000000..567b430 --- /dev/null +++ b/claude-code/.devcontainer/claude-sandbox/docker-compose.yml @@ -0,0 +1,6 @@ +services: + devcontainer: + image: ghcr.io/gatezh/devcontainer-images/claude-code-sandbox:latest + pull_policy: always + volumes: + - ../..:/workspace:cached diff --git a/claude-code/.devcontainer/devcontainer.json b/claude-code/.devcontainer/devcontainer.json new file mode 100644 index 0000000..40387aa --- /dev/null +++ b/claude-code/.devcontainer/devcontainer.json @@ -0,0 +1,102 @@ +// For format details, see https://aka.ms/devcontainer.json +{ + "name": "Local Development", + "dockerComposeFile": "docker-compose.yml", + "service": "devcontainer", + "workspaceFolder": "/workspace", + "init": true, + "updateRemoteUserUID": true, + "remoteUser": "node", + "otherPortsAttributes": { "onAutoForward": "silent" }, + "customizations": { + "vscode": { + "extensions": [ + // **AI Agents** + // Claude Code - AI coding assistant with chat, inline edits, and terminal integration + "anthropic.claude-code", + + // **Runtime** + // Bun - Bun runtime support (debugging, lockfile viewer, bundler integration) + "oven.bun-vscode", + + // **Code Quality** + // OXC - Fast linter and formatter for JavaScript/TypeScript (Rust-based) + "oxc.oxc-vscode", + + // **Tailwind** + // Tailwind CSS IntelliSense - autocomplete, syntax highlighting, linting for Tailwind classes + "bradlc.vscode-tailwindcss", + // Tailwind Fold - collapse long Tailwind class strings in the editor for readability + "stivo.tailwind-fold", + + // **General** + // YAML - YAML language support (for workflows and docker-compose) + "redhat.vscode-yaml", + // Markdown Preview Github Styles - renders Markdown preview with GitHub's CSS + "bierner.markdown-preview-github-styles", + // Docker - Dockerfile and Compose syntax, linting, and image management + "ms-azuretools.vscode-docker" + ], + "settings": { + "terminal.integrated.defaultProfile.linux": "fish", + "terminal.integrated.profiles.linux": { + "fish": { "path": "fish" }, + "bash": { "path": "bash", "icon": "terminal-bash" } + }, + // Suppress extension recommendation prompts + "extensions.ignoreRecommendations": true, + // ── Formatter settings (customize per project) ────────────────── + // Change "editor.defaultFormatter" to match your tooling: + // Biome: "biomejs.biome" | Prettier: "esbenp.prettier-vscode" + // OXC: "oxc.oxc-vscode" | None: remove these three settings + "editor.formatOnSave": true, + "editor.defaultFormatter": "oxc.oxc-vscode", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + // Show workspace folder name in window title + "window.title": "${localWorkspaceFolderBasename}", + // Visual identity — Claude Dark theme with coral remote indicator + "workbench.colorTheme": "Claude Dark", + "workbench.colorCustomizations": { + "statusBarItem.remoteBackground": "#C15F3C", + "statusBarItem.remoteForeground": "#ffffff" + } + } + } + }, + // 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. + "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", + // ── 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" + ], + "containerEnv": { + "TZ": "${localEnv:TZ:America/Edmonton}", + "DEVCONTAINER": "true", + "NODE_OPTIONS": "--max-old-space-size=4096", + "CLAUDE_CONFIG_DIR": "/home/node/.claude" + }, + // sudo chown fixes volume ownership — safety net in case Docker volume population didn't apply. + // The find command chowns all node_modules volume mount points in one pass. + // mise install reads .mise.toml and installs project-specific tool versions. + // playwright install ensures the correct browser binary for the project's @playwright/test version + // (idempotent — skips download if the image's cached binary already matches). + "updateContentCommand": "sudo find /workspace -maxdepth 4 -name node_modules -type d -exec chown node {} + && sudo chown -R node /home/node/.claude && mise install && bun install && npx playwright install --only-shell", + "waitFor": "postCreateCommand" +} diff --git a/claude-code/.devcontainer/docker-compose.yml b/claude-code/.devcontainer/docker-compose.yml new file mode 100644 index 0000000..f5647c3 --- /dev/null +++ b/claude-code/.devcontainer/docker-compose.yml @@ -0,0 +1,6 @@ +services: + devcontainer: + image: ghcr.io/gatezh/devcontainer-images/claude-code:latest + pull_policy: always + volumes: + - ..:/workspace:cached diff --git a/claude-code/.devcontainer/init-plugins.sh b/claude-code/.devcontainer/init-plugins.sh new file mode 100644 index 0000000..6a013f1 --- /dev/null +++ b/claude-code/.devcontainer/init-plugins.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Claude Code plugin initialization — runs once at container creation. +# Idempotent — safe to run multiple times. +# +# Wire into postCreateCommand in your devcontainer.json: +# "postCreateCommand": "bash .devcontainer/init-plugins.sh" + +set -euo pipefail + +# Mark onboarding complete so claude CLI doesn't hang on interactive prompts +if [ -f "$HOME/.claude/.claude.json" ]; then + jq '.hasCompletedOnboarding = true' "$HOME/.claude/.claude.json" > /tmp/.claude.json \ + && mv /tmp/.claude.json "$HOME/.claude/.claude.json" +else + mkdir -p "$HOME/.claude" + echo '{"hasCompletedOnboarding":true}' > "$HOME/.claude/.claude.json" +fi + +# Install plugins (customize this list) +for plugin in \ + "frontend-design@claude-plugins-official" \ + "code-review@claude-plugins-official"; do + claude plugin install "$plugin" 2>/dev/null || true +done diff --git a/claude-code/README.md b/claude-code/README.md index a935f6b..31ffd9e 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -47,102 +47,23 @@ The image rebuilds daily at 5am MT (11:00 UTC) using native runners for both amd ### Default variant -Add these files to your project's `.devcontainer/` directory. Docker Compose with `pull_policy: always` ensures "Rebuild Without Cache" always pulls the latest image — the image name stays in one place. 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 (`${devcontainerId}`, `${localEnv:...}`). -`.devcontainer/docker-compose.yml`: +Copy these to your project's `.devcontainer/`: -```yaml -services: - devcontainer: - image: ghcr.io/gatezh/devcontainer-images/claude-code:latest - pull_policy: always - volumes: - - ..:/workspace:cached -``` +- [`.devcontainer/docker-compose.yml`](.devcontainer/docker-compose.yml) — image reference with `pull_policy: always` +- [`.devcontainer/devcontainer.json`](.devcontainer/devcontainer.json) — full config with VS Code extensions, Claude Dark theme, fish shell, OXC formatter, node_modules volume isolation, and lifecycle commands -`.devcontainer/devcontainer.json`: - -```jsonc -{ - "name": "Local Development", - "dockerComposeFile": "docker-compose.yml", - "service": "devcontainer", - "workspaceFolder": "/workspace", - "init": true, - "remoteUser": "node", - // Named volumes persist node_modules, Claude config, and fish history across rebuilds. - // Dirs are pre-created in the image with node:node ownership, so fresh volumes - // inherit correct permissions via Docker volume population. - "mounts": [ - "source=myproject-node-modules-${devcontainerId},target=/workspace/node_modules,type=volume", - "source=myproject-claude-config-${devcontainerId},target=/home/node/.claude,type=volume", - "source=myproject-fish-data-${devcontainerId},target=/home/node/.local/share/fish,type=volume" - ], - "containerEnv": { - "TZ": "${localEnv:TZ:America/Los_Angeles}", - "DEVCONTAINER": "true", - "NODE_OPTIONS": "--max-old-space-size=4096", - "CLAUDE_CONFIG_DIR": "/home/node/.claude" - }, - // sudo chown fixes volume ownership — safety net in case Docker volume population didn't apply. - // mise install reads .mise.toml and installs project-specific tool versions. - // playwright install ensures the correct browser binary for the project's @playwright/test version - // (idempotent — skips download if the image's cached binary already matches). - "updateContentCommand": "sudo chown node /workspace/node_modules && sudo chown -R node /home/node/.claude && mise install && bun install && npx playwright install --only-shell", - "waitFor": "postCreateCommand" -} -``` +**Key settings included:** Claude Dark theme with coral remote indicator, fish + bash terminal profiles, OXC formatter (with comments for switching to Biome/Prettier), node_modules/Claude config/fish history volume mounts, and `updateContentCommand` for mise/bun/Playwright setup. ### Sandbox variant -> **No node_modules volumes** — the sandbox variant does not have passwordless sudo (only `sudo /usr/local/bin/init-firewall.sh` is allowed), so the `sudo chown` used in the default variant to fix volume ownership won't work. Let node_modules live in the bind mount instead. +Copy these to your project's `.devcontainer/claude-sandbox/`: -`.devcontainer/claude-sandbox/docker-compose.yml`: +- [`.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 -```yaml -services: - devcontainer: - image: ghcr.io/gatezh/devcontainer-images/claude-code-sandbox:latest - pull_policy: always - volumes: - - ../..:/workspace:cached -``` - -`.devcontainer/claude-sandbox/devcontainer.json`: - -```jsonc -{ - "name": "Claude Code Sandbox", - "dockerComposeFile": "docker-compose.yml", - "service": "devcontainer", - "workspaceFolder": "/workspace", - // Capabilities required for iptables firewall setup - "capAdd": ["NET_ADMIN", "NET_RAW"], - "init": true, - "remoteUser": "node", - "mounts": [ - "source=sandbox-fish-${devcontainerId},target=/home/node/.local/share/fish,type=volume", - "source=sandbox-config-${devcontainerId},target=/home/node/.claude,type=volume", - // Mount project's firewall script into the expected path. - // The image provides iptables/ipset packages and sudo rule but NOT the script itself. - "source=${localWorkspaceFolder}/.devcontainer/claude-sandbox/init-firewall.sh,target=/usr/local/bin/init-firewall.sh,type=bind" - ], - "containerEnv": { - "TZ": "${localEnv:TZ:America/Edmonton}", - "DEVCONTAINER": "true", - "NODE_OPTIONS": "--max-old-space-size=4096", - "CLAUDE_CONFIG_DIR": "/home/node/.claude", - // Required — the sandbox firewall blocks OAuth login, so the token must be - // injected from the host. See "Sandbox Authentication" section below. - "CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN}" - }, - // Runs before postStartCommand (firewall), so network is still available for browser downloads. - "postCreateCommand": "mise install && bun install && npx playwright install --only-shell", - // Firewall init — script is bind-mounted from the project - "postStartCommand": "sudo /usr/local/bin/init-firewall.sh", - "waitFor": "postStartCommand" -} -``` +**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. ## Project Setup Guide @@ -150,54 +71,25 @@ Projects consuming these images need the following files in their repository. ### Required: `.mise.toml` (project root) -Only pin tools that affect project stability — dev infrastructure (rtk, ralphex, Claude Code) is pre-installed in the image at latest: - -```toml -# Pin runtime and build tools that affect project stability. -# node is provided by the base image — needed for VS Code extensions -# (OXC, Playwright, etc.) that spawn node. Do not add it here. -[tools] -bun = "1.3.8" -hugo = "0.155.1" -``` +Only pin tools that affect project stability — dev infrastructure (rtk, ralphex, Claude Code) is pre-installed in the image at latest. See [`mise.toml`](mise.toml) for a template. ### Optional: `.devcontainer/init-plugins.sh` -Claude Code plugin initialization. Runs once at container creation. Idempotent. +Claude Code plugin initialization. Runs once at container creation. Idempotent. See [`.devcontainer/init-plugins.sh`](.devcontainer/init-plugins.sh) for a template. + Wire it into `postCreateCommand` in your `devcontainer.json`: ```jsonc "postCreateCommand": "bash .devcontainer/init-plugins.sh" ``` -```bash -#!/bin/bash -set -euo pipefail - -# Mark onboarding complete so claude CLI doesn't hang on interactive prompts -if [ -f "$HOME/.claude/.claude.json" ]; then - jq '.hasCompletedOnboarding = true' "$HOME/.claude/.claude.json" > /tmp/.claude.json \ - && mv /tmp/.claude.json "$HOME/.claude/.claude.json" -else - mkdir -p "$HOME/.claude" - echo '{"hasCompletedOnboarding":true}' > "$HOME/.claude/.claude.json" -fi - -# Install plugins (customize this list) -for plugin in \ - "frontend-design@claude-plugins-official" \ - "code-review@claude-plugins-official"; do - claude plugin install "$plugin" 2>/dev/null || true -done -``` - Mark as executable: `chmod +x init-plugins.sh` ### Sandbox-only: `.devcontainer/claude-sandbox/init-firewall.sh` Default-deny iptables firewall. The image provides the packages and sudo rule; the project provides this script via bind mount. Customize the domain allowlist for your project. -See the [devcontainer-claude-bun firewall script](../devcontainer-claude-bun/.devcontainer/init-firewall.sh) for a complete example. +See the [repo's own sandbox firewall script](../.devcontainer/claude-sandbox/init-firewall.sh) for a complete example. The script should: preserve Docker internal DNS rules, allow DNS/SSH/localhost, fetch GitHub IP ranges via `curl -s https://api.github.com/meta`, resolve additional allowed domains (npm, Anthropic API, VS Code marketplace, etc.) via `dig`, set default DROP policies, allow established connections and the ipset allowlist, then verify by confirming `example.com` is blocked and `api.github.com` is reachable. Mark as executable and ensure git tracks the executable bit: @@ -214,9 +106,9 @@ git add .devcontainer/claude-sandbox/init-firewall.sh # ensures git tracks +x ### Sandbox-only: Claude Code skill for fetching docs -The sandbox firewall blocks vendor doc sites, so Claude Code can't `WebFetch` or `WebSearch` as it normally would. The image includes a [sandbox-fetch-docs](.claude/skills/sandbox-fetch-docs.md) skill that teaches Claude Code how to look up library documentation using only allowed network paths (node_modules, raw.githubusercontent.com, GitHub Contents API, npm registry). +The sandbox firewall blocks vendor doc sites, so Claude Code can't `WebFetch` or `WebSearch` as it normally would. The image includes a [sandbox-fetch-docs](.claude/skills/sandbox-fetch-docs/SKILL.md) skill that teaches Claude Code how to look up library documentation using only allowed network paths (node_modules, raw.githubusercontent.com, GitHub Contents API, npm registry). -Copy `.claude/skills/sandbox-fetch-docs.md` into your project's `.claude/skills/` directory so Claude Code picks it up automatically. +Copy `.claude/skills/sandbox-fetch-docs/` into your project's `.claude/skills/` directory so Claude Code picks it up automatically. ### Sandbox Authentication @@ -283,7 +175,8 @@ Add `.env.local` to `.gitignore`. Note: Docker Compose fails to start if `.env.l └── .env.local ← actual auth token (gitignored) .claude/ └── skills/ - └── sandbox-fetch-docs.md ← teaches Claude Code to fetch docs within sandbox firewall + └── sandbox-fetch-docs/ + └── SKILL.md ← teaches Claude Code to fetch docs within sandbox firewall ``` ## Workspace Directory Layout diff --git a/claude-code/mise.toml b/claude-code/mise.toml new file mode 100644 index 0000000..80cc1ce --- /dev/null +++ b/claude-code/mise.toml @@ -0,0 +1,6 @@ +# Pin runtime and build tools that affect project stability. +# node is provided by the base image — needed for VS Code extensions +# (OXC, Playwright, etc.) that spawn node. Do not add it here. +[tools] +bun = "1.3.8" +hugo = "0.155.1"