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
4 changes: 4 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"]
16 changes: 13 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
},
Expand All @@ -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"
}
15 changes: 8 additions & 7 deletions claude-code/.devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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" \
Expand Down
106 changes: 106 additions & 0 deletions claude-code/.devcontainer/claude-sandbox/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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"
}
6 changes: 6 additions & 0 deletions claude-code/.devcontainer/claude-sandbox/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
services:
devcontainer:
image: ghcr.io/gatezh/devcontainer-images/claude-code-sandbox:latest
pull_policy: always
volumes:
- ../..:/workspace:cached
102 changes: 102 additions & 0 deletions claude-code/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -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"
}
6 changes: 6 additions & 0 deletions claude-code/.devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
services:
devcontainer:
image: ghcr.io/gatezh/devcontainer-images/claude-code:latest
pull_policy: always
volumes:
- ..:/workspace:cached
24 changes: 24 additions & 0 deletions claude-code/.devcontainer/init-plugins.sh
Original file line number Diff line number Diff line change
@@ -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
Loading