Skip to content

Commit 6c72403

Browse files
gatezhclaude
andauthored
fix(devcontainer): theme, login, node_modules isolation, self-contained claude-code config (#58)
* fix(devcontainer): add Claude Dark theme and fix extension login race Theme wasn't applied because no colorTheme was set. Login was disrupted because init-plugins.sh ran claude CLI commands in postCreateCommand, which executes before extensions install — racing the extension's OAuth. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(claude-code): extract configs to example files, add full VS Code setup - Move inline devcontainer.json/docker-compose configs from README into claude-code/examples/{default,sandbox}/ as actual files - Add all VS Code extensions with detailed comments (OXC, Bun, Tailwind, YAML, Docker, Markdown Preview) — OXC was referenced as formatter but missing from extensions list - Add monorepo node_modules volume mounts to both variants - Switch sandbox to passwordless sudo (security comes from the firewall) - Fix timezone from America/Los_Angeles to America/Edmonton - Fix stale cross-reference to devcontainer-claude-bun - Fix stale skill path (sandbox-fetch-docs.md → sandbox-fetch-docs/SKILL.md) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor(claude-code): move examples into .devcontainer, mise.toml to root Configs belong next to the Dockerfile they reference — no need for a separate examples/ directory. mise.toml goes in the project root where mise expects it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cd67b96 commit 6c72403

10 files changed

Lines changed: 292 additions & 134 deletions

File tree

‎.devcontainer/Dockerfile‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainer-im
142142
org.opencontainers.image.title="devcontainer-images" \
143143
org.opencontainers.image.url="https://github.com/gatezh/devcontainer-images"
144144

145+
CMD ["sleep", "infinity"]
146+
145147
# ─── SANDBOX — network-restricted environment ─────────────────────────────────
146148
FROM base AS sandbox
147149

@@ -168,3 +170,5 @@ LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainer-im
168170
org.opencontainers.image.licenses="MIT" \
169171
org.opencontainers.image.title="devcontainer-images-sandbox" \
170172
org.opencontainers.image.url="https://github.com/gatezh/devcontainer-images"
173+
174+
CMD ["sleep", "infinity"]

‎.devcontainer/devcontainer.json‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,13 @@
4545
"source.organizeImports": "explicit"
4646
},
4747
// Show workspace folder name in window title
48-
"window.title": "${localWorkspaceFolderBasename}"
48+
"window.title": "${localWorkspaceFolderBasename}",
49+
// Visual identity — Claude Dark theme with coral remote indicator
50+
"workbench.colorTheme": "Claude Dark",
51+
"workbench.colorCustomizations": {
52+
"statusBarItem.remoteBackground": "#C15F3C",
53+
"statusBarItem.remoteForeground": "#ffffff"
54+
}
4955
}
5056
}
5157
},
@@ -60,7 +66,11 @@
6066
"CLAUDE_CONFIG_DIR": "/home/node/.claude",
6167
"NODE_OPTIONS": "--max-old-space-size=4096"
6268
},
63-
// Initialize Claude Code plugins (runs once when container is created)
64-
"postCreateCommand": "sudo chown -R node /home/node/.claude && bash /workspace/.devcontainer/init-plugins.sh",
69+
// Fix volume ownership — must complete before extensions install.
70+
// Do NOT run claude CLI commands here: postCreateCommand runs before
71+
// VS Code installs extensions, so claude commands race with the
72+
// Claude Code extension's OAuth flow and can corrupt auth state.
73+
// Run .devcontainer/init-plugins.sh manually after first login.
74+
"postCreateCommand": "sudo chown -R node /home/node/.claude /home/node/.local/share/fish",
6575
"waitFor": "postCreateCommand"
6676
}

‎claude-code/.devcontainer/Dockerfile‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -217,13 +217,14 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
217217
dnsutils \
218218
aggregate
219219

220-
# Firewall sudo rule for node user.
221-
# The init-firewall.sh script is NOT baked into the image — each project
222-
# mounts its own script via bind mount in devcontainer.json:
223-
# "source=${localWorkspaceFolder}/.devcontainer/claude-sandbox/init-firewall.sh,target=/usr/local/bin/init-firewall.sh,type=bind"
224-
# This allows different projects to define their own domain allowlists.
225-
RUN echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall \
226-
&& chmod 0440 /etc/sudoers.d/node-firewall
220+
# Passwordless sudo for node user — needed for:
221+
# - "sudo chown" on named volumes (node_modules isolation)
222+
# - "sudo /usr/local/bin/init-firewall.sh" (firewall setup)
223+
# Sandbox security comes from the network firewall, not sudo restrictions.
224+
# The firewall script is NOT baked into the image — each project mounts its
225+
# own script via bind mount in devcontainer.json to customize the domain allowlist.
226+
RUN echo "node ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/node-nopasswd \
227+
&& chmod 0440 /etc/sudoers.d/node-nopasswd
227228
USER node
228229

229230
LABEL org.opencontainers.image.source="https://github.com/gatezh/devcontainer-images" \
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// For format details, see https://aka.ms/devcontainer.json
2+
{
3+
"name": "Claude Code Sandbox",
4+
"dockerComposeFile": "docker-compose.yml",
5+
"service": "devcontainer",
6+
"workspaceFolder": "/workspace",
7+
// Capabilities required for iptables firewall setup
8+
"capAdd": ["NET_ADMIN", "NET_RAW"],
9+
"init": true,
10+
"updateRemoteUserUID": true,
11+
"remoteUser": "node",
12+
"otherPortsAttributes": { "onAutoForward": "silent" },
13+
"customizations": {
14+
"vscode": {
15+
"extensions": [
16+
// **AI Agents**
17+
// Claude Code - AI coding assistant with chat, inline edits, and terminal integration
18+
"anthropic.claude-code",
19+
20+
// **Runtime**
21+
// Bun - Bun runtime support (debugging, lockfile viewer, bundler integration)
22+
"oven.bun-vscode",
23+
24+
// **Code Quality**
25+
// OXC - Fast linter and formatter for JavaScript/TypeScript (Rust-based)
26+
"oxc.oxc-vscode",
27+
28+
// **Tailwind**
29+
// Tailwind CSS IntelliSense - autocomplete, syntax highlighting, linting for Tailwind classes
30+
"bradlc.vscode-tailwindcss",
31+
// Tailwind Fold - collapse long Tailwind class strings in the editor for readability
32+
"stivo.tailwind-fold",
33+
34+
// **General**
35+
// YAML - YAML language support (for workflows and docker-compose)
36+
"redhat.vscode-yaml",
37+
// Markdown Preview Github Styles - renders Markdown preview with GitHub's CSS
38+
"bierner.markdown-preview-github-styles",
39+
// Docker - Dockerfile and Compose syntax, linting, and image management
40+
"ms-azuretools.vscode-docker"
41+
],
42+
"settings": {
43+
"terminal.integrated.defaultProfile.linux": "fish",
44+
"terminal.integrated.profiles.linux": {
45+
"fish": { "path": "fish" },
46+
"bash": { "path": "bash", "icon": "terminal-bash" }
47+
},
48+
"extensions.ignoreRecommendations": true,
49+
"editor.formatOnSave": true,
50+
"editor.defaultFormatter": "oxc.oxc-vscode",
51+
"editor.codeActionsOnSave": {
52+
"source.fixAll": "explicit",
53+
"source.organizeImports": "explicit"
54+
},
55+
"window.title": "${localWorkspaceFolderBasename}",
56+
// Sandbox visual identity — Claude Dark theme with coral remote indicator
57+
"workbench.colorTheme": "Claude Dark",
58+
"workbench.colorCustomizations": {
59+
"statusBarItem.remoteBackground": "#C15F3C",
60+
"statusBarItem.remoteForeground": "#ffffff"
61+
},
62+
// Allow Claude Code to skip permission prompts in sandbox
63+
"claudeCode.allowDangerouslySkipPermissions": true
64+
}
65+
}
66+
},
67+
// Named volumes keep node_modules OFF the host machine and persist across rebuilds.
68+
// Each workspace with a package.json needs its own volume mount — without one,
69+
// node_modules lands in the bind mount and shows up on the host filesystem.
70+
// Dirs are pre-created in the image with node:node ownership, so fresh volumes
71+
// inherit correct permissions via Docker volume population.
72+
//
73+
// Customize the monorepo mounts below to match your project structure.
74+
// Remove any that don't exist in your project.
75+
"mounts": [
76+
// ── node_modules isolation (one per workspace) ─────────────────────
77+
"source=sandbox-node-modules-root-${devcontainerId},target=/workspace/node_modules,type=volume",
78+
"source=sandbox-node-modules-api-${devcontainerId},target=/workspace/services/api/node_modules,type=volume",
79+
"source=sandbox-node-modules-app-${devcontainerId},target=/workspace/services/app/node_modules,type=volume",
80+
"source=sandbox-node-modules-www-${devcontainerId},target=/workspace/services/www/node_modules,type=volume",
81+
"source=sandbox-node-modules-shared-${devcontainerId},target=/workspace/packages/shared/node_modules,type=volume",
82+
"source=sandbox-node-modules-database-${devcontainerId},target=/workspace/packages/database/node_modules,type=volume",
83+
// ── Persistent config ──────────────────────────────────────────────
84+
"source=sandbox-fish-${devcontainerId},target=/home/node/.local/share/fish,type=volume",
85+
"source=sandbox-config-${devcontainerId},target=/home/node/.claude,type=volume",
86+
// ── Firewall script ────────────────────────────────────────────────
87+
// The image provides iptables/ipset packages and sudo rule but NOT the script itself.
88+
// Each project provides its own script via bind mount to customize the domain allowlist.
89+
"source=${localWorkspaceFolder}/.devcontainer/claude-sandbox/init-firewall.sh,target=/usr/local/bin/init-firewall.sh,type=bind"
90+
],
91+
"containerEnv": {
92+
"TZ": "${localEnv:TZ:America/Edmonton}",
93+
"DEVCONTAINER": "true",
94+
"NODE_OPTIONS": "--max-old-space-size=4096",
95+
"CLAUDE_CONFIG_DIR": "/home/node/.claude",
96+
// Required — the sandbox firewall blocks OAuth login, so the token must be
97+
// injected from the host. See "Sandbox Authentication" section in README.
98+
"CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN}"
99+
},
100+
// Runs before postStartCommand (firewall), so network is still available for browser downloads.
101+
// The find command chowns all node_modules volume mount points in one pass.
102+
"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",
103+
// Firewall init — script is bind-mounted from the project
104+
"postStartCommand": "sudo /usr/local/bin/init-firewall.sh",
105+
"waitFor": "postStartCommand"
106+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
services:
2+
devcontainer:
3+
image: ghcr.io/gatezh/devcontainer-images/claude-code-sandbox:latest
4+
pull_policy: always
5+
volumes:
6+
- ../..:/workspace:cached
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// For format details, see https://aka.ms/devcontainer.json
2+
{
3+
"name": "Local Development",
4+
"dockerComposeFile": "docker-compose.yml",
5+
"service": "devcontainer",
6+
"workspaceFolder": "/workspace",
7+
"init": true,
8+
"updateRemoteUserUID": true,
9+
"remoteUser": "node",
10+
"otherPortsAttributes": { "onAutoForward": "silent" },
11+
"customizations": {
12+
"vscode": {
13+
"extensions": [
14+
// **AI Agents**
15+
// Claude Code - AI coding assistant with chat, inline edits, and terminal integration
16+
"anthropic.claude-code",
17+
18+
// **Runtime**
19+
// Bun - Bun runtime support (debugging, lockfile viewer, bundler integration)
20+
"oven.bun-vscode",
21+
22+
// **Code Quality**
23+
// OXC - Fast linter and formatter for JavaScript/TypeScript (Rust-based)
24+
"oxc.oxc-vscode",
25+
26+
// **Tailwind**
27+
// Tailwind CSS IntelliSense - autocomplete, syntax highlighting, linting for Tailwind classes
28+
"bradlc.vscode-tailwindcss",
29+
// Tailwind Fold - collapse long Tailwind class strings in the editor for readability
30+
"stivo.tailwind-fold",
31+
32+
// **General**
33+
// YAML - YAML language support (for workflows and docker-compose)
34+
"redhat.vscode-yaml",
35+
// Markdown Preview Github Styles - renders Markdown preview with GitHub's CSS
36+
"bierner.markdown-preview-github-styles",
37+
// Docker - Dockerfile and Compose syntax, linting, and image management
38+
"ms-azuretools.vscode-docker"
39+
],
40+
"settings": {
41+
"terminal.integrated.defaultProfile.linux": "fish",
42+
"terminal.integrated.profiles.linux": {
43+
"fish": { "path": "fish" },
44+
"bash": { "path": "bash", "icon": "terminal-bash" }
45+
},
46+
// Suppress extension recommendation prompts
47+
"extensions.ignoreRecommendations": true,
48+
// ── Formatter settings (customize per project) ──────────────────
49+
// Change "editor.defaultFormatter" to match your tooling:
50+
// Biome: "biomejs.biome" | Prettier: "esbenp.prettier-vscode"
51+
// OXC: "oxc.oxc-vscode" | None: remove these three settings
52+
"editor.formatOnSave": true,
53+
"editor.defaultFormatter": "oxc.oxc-vscode",
54+
"editor.codeActionsOnSave": {
55+
"source.fixAll": "explicit",
56+
"source.organizeImports": "explicit"
57+
},
58+
// Show workspace folder name in window title
59+
"window.title": "${localWorkspaceFolderBasename}",
60+
// Visual identity — Claude Dark theme with coral remote indicator
61+
"workbench.colorTheme": "Claude Dark",
62+
"workbench.colorCustomizations": {
63+
"statusBarItem.remoteBackground": "#C15F3C",
64+
"statusBarItem.remoteForeground": "#ffffff"
65+
}
66+
}
67+
}
68+
},
69+
// Named volumes keep node_modules OFF the host machine and persist across rebuilds.
70+
// Each workspace with a package.json needs its own volume mount — without one,
71+
// node_modules lands in the bind mount and shows up on the host filesystem.
72+
// Dirs are pre-created in the image with node:node ownership, so fresh volumes
73+
// inherit correct permissions via Docker volume population.
74+
//
75+
// Customize the monorepo mounts below to match your project structure.
76+
// Remove any that don't exist in your project.
77+
"mounts": [
78+
// ── node_modules isolation (one per workspace) ─────────────────────
79+
"source=myproject-node-modules-root-${devcontainerId},target=/workspace/node_modules,type=volume",
80+
"source=myproject-node-modules-api-${devcontainerId},target=/workspace/services/api/node_modules,type=volume",
81+
"source=myproject-node-modules-app-${devcontainerId},target=/workspace/services/app/node_modules,type=volume",
82+
"source=myproject-node-modules-www-${devcontainerId},target=/workspace/services/www/node_modules,type=volume",
83+
"source=myproject-node-modules-shared-${devcontainerId},target=/workspace/packages/shared/node_modules,type=volume",
84+
"source=myproject-node-modules-database-${devcontainerId},target=/workspace/packages/database/node_modules,type=volume",
85+
// ── Persistent config ──────────────────────────────────────────────
86+
"source=myproject-claude-config-${devcontainerId},target=/home/node/.claude,type=volume",
87+
"source=myproject-fish-data-${devcontainerId},target=/home/node/.local/share/fish,type=volume"
88+
],
89+
"containerEnv": {
90+
"TZ": "${localEnv:TZ:America/Edmonton}",
91+
"DEVCONTAINER": "true",
92+
"NODE_OPTIONS": "--max-old-space-size=4096",
93+
"CLAUDE_CONFIG_DIR": "/home/node/.claude"
94+
},
95+
// sudo chown fixes volume ownership — safety net in case Docker volume population didn't apply.
96+
// The find command chowns all node_modules volume mount points in one pass.
97+
// mise install reads .mise.toml and installs project-specific tool versions.
98+
// playwright install ensures the correct browser binary for the project's @playwright/test version
99+
// (idempotent — skips download if the image's cached binary already matches).
100+
"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",
101+
"waitFor": "postCreateCommand"
102+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
services:
2+
devcontainer:
3+
image: ghcr.io/gatezh/devcontainer-images/claude-code:latest
4+
pull_policy: always
5+
volumes:
6+
- ..:/workspace:cached
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/bin/bash
2+
# Claude Code plugin initialization — runs once at container creation.
3+
# Idempotent — safe to run multiple times.
4+
#
5+
# Wire into postCreateCommand in your devcontainer.json:
6+
# "postCreateCommand": "bash .devcontainer/init-plugins.sh"
7+
8+
set -euo pipefail
9+
10+
# Mark onboarding complete so claude CLI doesn't hang on interactive prompts
11+
if [ -f "$HOME/.claude/.claude.json" ]; then
12+
jq '.hasCompletedOnboarding = true' "$HOME/.claude/.claude.json" > /tmp/.claude.json \
13+
&& mv /tmp/.claude.json "$HOME/.claude/.claude.json"
14+
else
15+
mkdir -p "$HOME/.claude"
16+
echo '{"hasCompletedOnboarding":true}' > "$HOME/.claude/.claude.json"
17+
fi
18+
19+
# Install plugins (customize this list)
20+
for plugin in \
21+
"frontend-design@claude-plugins-official" \
22+
"code-review@claude-plugins-official"; do
23+
claude plugin install "$plugin" 2>/dev/null || true
24+
done

0 commit comments

Comments
 (0)