devcontainer-setup
Creates devcontainers with Claude Code, language-specific tooling (Python/Node/Rust/Go), and persistent volumes. Use when adding devcontainer support to a project, setting up isolated development environments, or configuring sandboxed Claude Code workspaces.
Install
npx skills add https://github.com/trailofbits/skills/tree/main/plugins/devcontainer-setup/skills/devcontainer-setup
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install trailofbits-skills@llmmart
git clone https://github.com/trailofbits/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole trailofbits/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Devcontainer Setup Skill
Creates a pre-configured devcontainer with Claude Code and language-specific tooling.
When to Use
- User asks to "set up a devcontainer" or "add devcontainer support"
- User wants a sandboxed Claude Code development environment
- User needs isolated development environments with persistent configuration
When NOT to Use
- User already has a devcontainer configuration and just needs modifications
- User is asking about general Docker or container questions
- User wants to deploy production containers (this is for development only)
Workflow
flowchart TB
start([User requests devcontainer])
recon[1. Project Reconnaissance]
detect[2. Detect Languages]
generate[3. Generate Configuration]
write[4. Write files to .devcontainer/]
done([Done])
start --> recon
recon --> detect
detect --> generate
generate --> write
write --> done
Phase 1: Project Reconnaissance
Infer Project Name
Check in order (use first match):
package.json→namefieldpyproject.toml→project.nameCargo.toml→package.namego.mod→ module path (last segment after/)- Directory name as fallback
Convert to slug: lowercase, replace spaces/underscores with hyphens.
Detect Language Stack
| Language | Detection Files |
|---|---|
| Python | pyproject.toml, *.py |
| Node/TypeScript | package.json, tsconfig.json |
| Rust | Cargo.toml |
| Go | go.mod, go.sum |
Multi-Language Projects
If multiple languages are detected, configure all of them in the following priority order:
- Python - Primary language, uses Dockerfile for uv + Python installation
- Node/TypeScript - Uses devcontainer feature
- Rust - Uses devcontainer feature
- Go - Uses devcontainer feature
For multi-language postCreateCommand, chain all setup commands:
uv run /opt/post_install.py && uv sync && npm ci
Extensions and settings from all detected languages should be merged into the configuration.
Phase 2: Generate Configuration
Start with base templates from resources/ directory. Substitute:
{{PROJECT_NAME}}→ Human-readable name (e.g., "My Project"){{PROJECT_SLUG}}→ Slug for volumes (e.g., "my-project")
Then apply language-specific modifications below.
Base Template Features
The base template includes:
- Claude Code with marketplace plugins (anthropics/skills, trailofbits/skills, trailofbits/skills-curated)
- Sandboxing via bubblewrap and socat
- Python 3.13 via uv (fast binary download)
- Node 22 via fnm (Fast Node Manager)
- ast-grep for AST-based code search
- Network isolation tools (iptables, ipset) with NET_ADMIN capability
- Security mounts:
.devcontainer/mounted read-only to prevent container escape - Token forwarding:
CLAUDE_CODE_OAUTH_TOKENandANTHROPIC_API_KEYviaremoteEnv - Modern CLI tools: ripgrep, fd, fzf, tmux, git-delta
Language-Specific Sections
Python Projects
Detection: pyproject.toml, requirements.txt, setup.py, or *.py files
Dockerfile additions:
The base Dockerfile already includes Python 3.13 via uv. If a different version is required (detected from pyproject.toml), modify the Python installation:
# Install Python via uv (fast binary download, not source compilation)
RUN uv python install <version> --default
devcontainer.json extensions:
Add to customizations.vscode.extensions:
"ms-python.python",
"ms-python.vscode-pylance",
"charliermarsh.ruff"
Add to customizations.vscode.settings:
"python.defaultInterpreterPath": ".venv/bin/python",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit"
}
}
postCreateCommand:
If pyproject.toml exists, chain commands:
rm -rf .venv && uv sync && uv run /opt/post_install.py
Node/TypeScript Projects
Detection: package.json or tsconfig.json
No Dockerfile additions needed: The base template includes Node 22 via fnm (Fast Node Manager).
devcontainer.json extensions:
Add to customizations.vscode.extensions:
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
Add to customizations.vscode.settings:
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
postCreateCommand: Detect package manager from lockfile and chain with base command:
pnpm-lock.yaml→uv run /opt/post_install.py && pnpm install --frozen-lockfileyarn.lock→uv run /opt/post_install.py && yarn install --frozen-lockfilepackage-lock.json→uv run /opt/post_install.py && npm ci- No lockfile →
uv run /opt/post_install.py && npm install
Rust Projects
Detection: Cargo.toml
Features to add:
"ghcr.io/devcontainers/features/rust:1": {}
devcontainer.json extensions:
Add to customizations.vscode.extensions:
"rust-lang.rust-analyzer",
"tamasfe.even-better-toml"
Add to customizations.vscode.settings:
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
}
postCreateCommand:
If Cargo.lock exists, use locked builds:
uv run /opt/post_install.py && cargo build --locked
If no lockfile, use standard build:
uv run /opt/post_install.py && cargo build
Go Projects
Detection: go.mod
Features to add:
"ghcr.io/devcontainers/features/go:1": {
"version": "latest"
}
devcontainer.json extensions:
Add to customizations.vscode.extensions:
"golang.go"
Add to customizations.vscode.settings:
"[go]": {
"editor.defaultFormatter": "golang.go"
},
"go.useLanguageServer": true
postCreateCommand:
uv run /opt/post_install.py && go mod download
Reference Material
For additional guidance, see:
references/dockerfile-best-practices.md- Layer optimization, multi-stage builds, architecture supportreferences/features-vs-dockerfile.md- When to use devcontainer features vs custom Dockerfile
Adding Persistent Volumes
Pattern for new mounts in devcontainer.json:
"mounts": [
"source={{PROJECT_SLUG}}-<purpose>-${devcontainerId},target=<container-path>,type=volume"
]
Common additions:
source={{PROJECT_SLUG}}-cargo-${devcontainerId},target=/home/vscode/.cargo,type=volume(Rust)source={{PROJECT_SLUG}}-go-${devcontainerId},target=/home/vscode/go,type=volume(Go)
Output Files
Generate these files in the project's .devcontainer/ directory:
Dockerfile- Container build instructionsdevcontainer.json- VS Code/devcontainer configurationpost_install.py- Post-creation setup script.zshrc- Shell configurationinstall.sh- CLI helper for managing the devcontainer (devccommand)
Validation Checklist
Before presenting files to the user, verify:
- All
{{PROJECT_NAME}}placeholders are replaced with the human-readable name - All
{{PROJECT_SLUG}}placeholders are replaced with the slugified name - JSON syntax is valid in
devcontainer.json(no trailing commas, proper nesting) - Language-specific extensions are added for all detected languages
postCreateCommandincludes all required setup commands (chained with&&)
User Instructions
After generating, inform the user:
- How to start: "Open in VS Code and select 'Reopen in Container'"
- Alternative:
devcontainer up --workspace-folder . - CLI helper: Run
.devcontainer/install.sh self-installto add thedevccommand to PATH
Files (skills)
-
agents
-
openai.yaml 239 B
interface: display_name: "Devcontainer Setup" short_description: "Set up isolated development containers and tooling" icon_small: "assets/trail-of-bits-mark.svg" icon_large: "assets/trail-of-bits-mark.svg" brand_color: "#D83A34"
-
-
assets
-
trail-of-bits-mark.svg 3 KB · in bundle
-
-
references
-
dockerfile-best-practices.md 3.3 KB
# Dockerfile Best Practices ## Quick Reference | Practice | Why | |----------|-----| | Order by change frequency | Rarely-changing layers first (base, system packages), frequently-changing last | | Combine related RUN commands | Reduces layers and ensures cache coherence | | Clean up in same layer | Don't leave apt cache in a layer | | Use multi-stage builds | Separate build dependencies from runtime, reduce final image size | | Pin versions with digests | Supply chain security: `FROM alpine:3.21@sha256:abc123...` | | Switch to non-root user last | Do root operations first, then `USER vscode` | | Use COPY over ADD | ADD has extra features you usually don't need | | Use .dockerignore | Exclude build-irrelevant files to reduce context size | ## Base Image Selection Choose minimal, trusted base images: - **Docker Official Images** - curated, documented, regularly updated - **Alpine Linux** - under 6 MB, tightly controlled - **Verified Publisher** or **Docker-Sponsored Open Source** images Pin images to specific digests for reproducible builds: ```dockerfile FROM alpine:3.21@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c ``` Avoid `latest` tag - it can change unexpectedly and cause breaking builds. ## apt-get Best Practices Always combine `update` with `install` in the same RUN statement: ```dockerfile RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ git \ vim \ && rm -rf /var/lib/apt/lists/* ``` **Why combine?** Keeping them separate causes Docker to cache the `update` layer, potentially installing outdated packages on subsequent builds. **Best practices:** - Use `--no-install-recommends` to minimize installed packages - Sort packages alphabetically within each section for easier maintenance and PR reviews - Clean up with `rm -rf /var/lib/apt/lists/*` in the same layer ## Pipe Safety When using pipes, prepend `set -o pipefail &&` to fail if any command fails: ```dockerfile RUN set -o pipefail && curl -fsSL https://example.com/install.sh | bash ``` Without this, a failed `curl` would be masked by a successful `bash`. ## Environment Variables Use `ENV` for paths, versions, and configuration: ```dockerfile ENV PYTHON_VERSION=3.13 ENV PATH=/home/vscode/.local/bin:$PATH ``` Note: `ENV` instructions add metadata, not filesystem layers like `RUN`. Multiple separate `ENV` lines are fine and often more readable than combining them. ## WORKDIR Always use absolute paths. Avoid `RUN cd ... && command` patterns: ```dockerfile # Good WORKDIR /app RUN make install # Bad RUN cd /app && make install ``` ## Architecture Support The templates support both AMD64 and ARM64 (Apple Silicon) automatically. Use `TARGETARCH` build arg for architecture-specific downloads: ```dockerfile ARG TARGETARCH RUN curl -fsSL "https://example.com/tool-${TARGETARCH}.tar.gz" | tar xz ``` ## Devcontainer-Specific Tips **Resource allocation:** Docker Desktop has limited defaults. Increase CPU/Memory in Docker settings for resource-intensive builds. **Windows/WSL2:** Use Docker Desktop's WSL 2 backend for better file sharing performance. ## Sources - [Docker Build Best Practices](https://docs.docker.com/build/building/best-practices/) - [VS Code Dev Containers Tips](https://code.visualstudio.com/docs/devcontainers/tips-and-tricks) -
features-vs-dockerfile.md 675 B
# Features vs Dockerfile ## Use devcontainer features when: - Installing standard development tools (GitHub CLI, languages, etc.) - The feature does what you need out of the box - You want automatic updates with feature version bumps ## Use Dockerfile when: - Installing specific versions of tools - Custom configuration is needed - Combining multiple tools in optimized layers - The feature doesn't exist or is poorly maintained ## Example: Python For Python, we use Dockerfile + uv instead of the Python feature because: 1. uv installs Python binaries instantly (vs compiling from source) 2. We get uv for dependency management 3. More control over the installation
-
-
resources
-
.zshrc 1.8 KB · in bundle
-
devcontainer.json 2.5 KB
{ "$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json", "name": "{{PROJECT_NAME}}", "build": { "dockerfile": "Dockerfile", "args": { "TZ": "${localEnv:TZ:UTC}", "GIT_DELTA_VERSION": "0.18.2", "ZSH_IN_DOCKER_VERSION": "1.2.1" } }, "features": { "ghcr.io/devcontainers/features/github-cli:1": {} }, "runArgs": [ "--cap-add=NET_ADMIN", "--cap-add=NET_RAW" ], "init": true, "updateRemoteUserUID": true, "customizations": { "vscode": { "extensions": [ "anthropic.claude-code" ], "settings": { "terminal.integrated.defaultProfile.linux": "zsh", "terminal.integrated.profiles.linux": { "bash": { "path": "bash", "icon": "terminal-bash" }, "zsh": { "path": "zsh" } }, "files.trimTrailingWhitespace": true, "files.insertFinalNewline": true, "files.trimFinalNewlines": true } } }, "remoteUser": "vscode", "mounts": [ "source={{PROJECT_SLUG}}-bashhistory-${devcontainerId},target=/commandhistory,type=volume", "source={{PROJECT_SLUG}}-config-${devcontainerId},target=/home/vscode/.claude,type=volume", "source={{PROJECT_SLUG}}-gh-${devcontainerId},target=/home/vscode/.config/gh,type=volume", "source=${localEnv:HOME}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,readonly", "source=${localWorkspaceFolder}/.devcontainer,target=/workspace/.devcontainer,type=bind,readonly" ], "containerEnv": { "NODE_OPTIONS": "--max-old-space-size=4096", "CLAUDE_CONFIG_DIR": "/home/vscode/.claude", "POWERLEVEL9K_DISABLE_GITSTATUS": "true", "GIT_CONFIG_GLOBAL": "/home/vscode/.gitconfig.local", "UV_LINK_MODE": "copy", "NPM_CONFIG_IGNORE_SCRIPTS": "true", "NPM_CONFIG_AUDIT": "true", "NPM_CONFIG_FUND": "false", "NPM_CONFIG_SAVE_EXACT": "true", "NPM_CONFIG_UPDATE_NOTIFIER": "false", "NPM_CONFIG_MINIMUM_RELEASE_AGE": "1440", "PYTHONDONTWRITEBYTECODE": "1", "PIP_DISABLE_PIP_VERSION_CHECK": "1" }, "remoteEnv": { "CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN:}", "ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY:}" }, "initializeCommand": "test -f \"$HOME/.gitconfig\" || touch \"$HOME/.gitconfig\"", "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated", "workspaceFolder": "/workspace", "postCreateCommand": "uv run --no-project /opt/post_install.py" } -
Dockerfile 3.6 KB · in bundle
-
install.sh 22.6 KB
#!/bin/bash set -euo pipefail # Claude Code Devcontainer CLI Helper # Provides the `devc` command for managing devcontainers # Resolve symlinks to get actual script location SOURCE="${BASH_SOURCE[0]}" while [[ -L "$SOURCE" ]]; do DIR="$(cd "$(dirname "$SOURCE")" && pwd)" SOURCE="$(readlink "$SOURCE")" [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" done SCRIPT_DIR="$(cd "$(dirname "$SOURCE")" && pwd)" SCRIPT_NAME="$(basename "$0")" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color print_usage() { cat <<EOF Usage: devc <command> [options] Commands: . Install devcontainer template to current directory and start up Start the devcontainer in current directory rebuild Rebuild the devcontainer (preserves auth volumes) down Stop the devcontainer shell Open a shell in the running container self-install Install 'devc' command to ~/.local/bin update Update devc to the latest version template [dir] Copy devcontainer template to directory (default: current) exec <cmd> Execute a command in the running container upgrade Upgrade Claude Code to latest version mount <host> <cont> Add a mount to the devcontainer (recreates container) sync [project] [--trusted] Sync sessions from devcontainers to host cp <cont> <host> Copy files/directories from container to host destroy [-f] Remove container, volumes, and image for current project help Show this help message Examples: devc . # Install template and start container devc up # Start container in current directory devc rebuild # Clean rebuild devc shell # Open interactive shell devc self-install # Install devc to PATH devc update # Update to latest version devc exec ls -la # Run command in container devc upgrade # Upgrade Claude Code to latest devc mount ~/data /data # Add mount to container devc sync # Sync sessions from all devcontainers devc sync crypto # Sync only matching devcontainer devc cp /some/file ./out # Copy a path from container to host devc destroy # Remove all project Docker resources devc destroy -f # Skip confirmation prompt EOF } log_info() { echo -e "${BLUE}[devc]${NC} $1" } log_success() { echo -e "${GREEN}[devc]${NC} $1" } log_warn() { echo -e "${YELLOW}[devc]${NC} $1" } log_error() { echo -e "${RED}[devc]${NC} $1" >&2 } check_devcontainer_cli() { if ! command -v devcontainer &>/dev/null; then log_error "devcontainer CLI not found." log_info "Install it with: npm install -g @devcontainers/cli" exit 1 fi } check_no_sys_admin() { local workspace="${1:-.}" local dc_json="$workspace/.devcontainer/devcontainer.json" [[ -f "$dc_json" ]] || return 0 if jq -e \ '.runArgs[]? | select(test("SYS_ADMIN"))' \ "$dc_json" >/dev/null 2>&1; then log_error "SYS_ADMIN capability detected in runArgs." log_error "This defeats the read-only .devcontainer mount." exit 1 fi } get_workspace_folder() { echo "${1:-$(pwd)}" } # Extract custom mounts from devcontainer.json to a temp file # Returns the temp file path, or empty string if no custom mounts # # Security: .devcontainer/ is mounted read-only inside the container to prevent # a compromised process from injecting malicious mounts or commands into # devcontainer.json that execute on the host during rebuild. This protection # requires that SYS_ADMIN is never added to runArgs (it would allow remounting # read-write). extract_mounts_to_file() { local devcontainer_json="$1" local temp_file [[ -f "$devcontainer_json" ]] || return 0 temp_file=$(mktemp) # Filter out default mounts by target path (immune to project name changes) local custom_mounts custom_mounts=$(jq -c ' .mounts // [] | map( select( (contains("target=/commandhistory,") | not) and (contains("target=/home/vscode/.claude,") | not) and (contains("target=/home/vscode/.config/gh,") | not) and (contains("target=/home/vscode/.gitconfig,") | not) and (contains("target=/workspace/.devcontainer,") | not) ) ) | if length > 0 then . else empty end ' "$devcontainer_json" 2>/dev/null) || true if [[ -n "$custom_mounts" ]]; then echo "$custom_mounts" >"$temp_file" echo "$temp_file" else rm -f "$temp_file" fi } # Merge preserved mounts back into devcontainer.json merge_mounts_from_file() { local devcontainer_json="$1" local mounts_file="$2" [[ -f "$mounts_file" ]] || return 0 [[ -s "$mounts_file" ]] || return 0 local custom_mounts custom_mounts=$(cat "$mounts_file") local updated updated=$(jq --argjson custom "$custom_mounts" ' .mounts = ((.mounts // []) + $custom | unique) ' "$devcontainer_json") echo "$updated" >"$devcontainer_json" } # Add or update a mount in devcontainer.json update_devcontainer_mounts() { local devcontainer_json="$1" local host_path="$2" local container_path="$3" local readonly="${4:-false}" local mount_str="source=${host_path},target=${container_path},type=bind" [[ "$readonly" == "true" ]] && mount_str="${mount_str},readonly" local updated updated=$(jq --arg target "$container_path" --arg mount "$mount_str" ' .mounts = ( ((.mounts // []) | map(select(contains("target=" + $target + ",") or endswith("target=" + $target) | not))) + [$mount] ) ' "$devcontainer_json") echo "$updated" >"$devcontainer_json" } cmd_template() { local target_dir="${1:-.}" target_dir="$(cd "$target_dir" 2>/dev/null && pwd)" || { log_error "Directory does not exist: $1" exit 1 } local devcontainer_dir="$target_dir/.devcontainer" local devcontainer_json="$devcontainer_dir/devcontainer.json" local preserved_mounts="" if [[ -d "$devcontainer_dir" ]]; then log_warn "Devcontainer already exists at $devcontainer_dir" read -p "Overwrite? [y/N] " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then log_info "Aborted." exit 0 fi # Preserve custom mounts before overwriting preserved_mounts=$(extract_mounts_to_file "$devcontainer_json") if [[ -n "$preserved_mounts" ]]; then log_info "Preserving custom mounts..." fi fi mkdir -p "$devcontainer_dir" # Copy template files cp "$SCRIPT_DIR/Dockerfile" "$devcontainer_dir/" cp "$SCRIPT_DIR/devcontainer.json" "$devcontainer_dir/" cp "$SCRIPT_DIR/post_install.py" "$devcontainer_dir/" cp "$SCRIPT_DIR/.zshrc" "$devcontainer_dir/" # Restore preserved mounts if [[ -n "$preserved_mounts" ]]; then merge_mounts_from_file "$devcontainer_json" "$preserved_mounts" rm -f "$preserved_mounts" log_info "Custom mounts restored" fi log_success "Template installed to $devcontainer_dir" } cmd_up() { local workspace_folder workspace_folder="$(get_workspace_folder "${1:-}")" check_devcontainer_cli check_no_sys_admin "$workspace_folder" log_info "Starting devcontainer in $workspace_folder..." devcontainer up --workspace-folder "$workspace_folder" log_success "Devcontainer started" } cmd_rebuild() { local workspace_folder workspace_folder="$(get_workspace_folder "${1:-}")" check_devcontainer_cli check_no_sys_admin "$workspace_folder" log_info "Rebuilding devcontainer in $workspace_folder..." devcontainer up \ --workspace-folder "$workspace_folder" \ --remove-existing-container log_success "Devcontainer rebuilt" } cmd_down() { local workspace_folder workspace_folder="$(get_workspace_folder "${1:-}")" check_devcontainer_cli log_info "Stopping devcontainer..." # Get container ID and stop it local container_id local label="devcontainer.local_folder=$workspace_folder" container_id=$(docker ps -q --filter "label=$label" 2>/dev/null || true) if [[ -n "$container_id" ]]; then docker stop "$container_id" log_success "Devcontainer stopped" else log_warn "No running devcontainer found for $workspace_folder" fi } cmd_shell() { local workspace_folder workspace_folder="$(get_workspace_folder)" check_devcontainer_cli log_info "Opening shell in devcontainer..." devcontainer exec --workspace-folder "$workspace_folder" zsh } cmd_exec() { local workspace_folder workspace_folder="$(get_workspace_folder)" check_devcontainer_cli devcontainer exec --workspace-folder "$workspace_folder" "$@" } cmd_upgrade() { local workspace_folder workspace_folder="$(get_workspace_folder)" check_devcontainer_cli log_info "Upgrading Claude Code..." devcontainer exec --workspace-folder "$workspace_folder" claude update log_success "Claude Code upgraded" } cmd_mount() { local host_path="${1:-}" local container_path="${2:-}" local readonly="false" if [[ -z "$host_path" ]] || [[ -z "$container_path" ]]; then log_error "Usage: devc mount <host_path> <container_path> [--readonly]" exit 1 fi [[ "${3:-}" == "--readonly" ]] && readonly="true" # Expand and validate host path host_path="$(cd "$host_path" 2>/dev/null && pwd)" || { log_error "Host path does not exist: $1" exit 1 } local workspace_folder workspace_folder="$(get_workspace_folder)" local devcontainer_json="$workspace_folder/.devcontainer/devcontainer.json" if [[ ! -f "$devcontainer_json" ]]; then log_error "No devcontainer.json found. Run 'devc template' first." exit 1 fi check_devcontainer_cli log_info "Adding mount: $host_path → $container_path" update_devcontainer_mounts "$devcontainer_json" "$host_path" "$container_path" "$readonly" log_info "Recreating container with new mount..." devcontainer up \ --workspace-folder "$workspace_folder" \ --remove-existing-container log_success "Mount added: $host_path → $container_path" } cmd_sync() { local filter="" local trusted=false while [[ $# -gt 0 ]]; do case "$1" in --trusted) trusted=true shift ;; *) filter="$1" shift ;; esac done local host_projects="${HOME}/.claude/projects" if [[ "$trusted" == false ]]; then log_warn "This copies files from devcontainers to your host filesystem." log_warn "Only proceed if you trust the container contents." log_info "Use --trusted to skip this prompt." read -p "Continue? [y/N] " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then log_info "Aborted." exit 0 fi fi # Discover all devcontainers (running + stopped) by label. local container_ids container_ids=$(docker ps -a -q \ --filter "label=devcontainer.local_folder" 2>/dev/null || true) if [[ -z "$container_ids" ]]; then log_error "No devcontainers found (running or stopped)." exit 1 fi # List discovered devcontainers. log_info "Discovered devcontainers:" local matched_any=false while IFS= read -r cid; do local name folder status name=$(sync_get_project_name "$cid") folder=$(docker inspect --format \ '{{index .Config.Labels "devcontainer.local_folder"}}' "$cid") status=$(docker inspect --format '{{.State.Status}}' "$cid") if [[ -n "$filter" ]]; then if ! echo "$name" | grep -qi "$filter"; then continue fi fi matched_any=true echo " - ${name} (${status}) ${folder}" done <<<"$container_ids" if [[ "$matched_any" == false ]]; then log_error "No devcontainers matching '${filter}'." echo "" echo "Available:" while IFS= read -r cid; do local name status name=$(sync_get_project_name "$cid") status=$(docker inspect --format '{{.State.Status}}' "$cid") echo " - ${name} (${status})" done <<<"$container_ids" exit 1 fi echo "" # Sync matching containers. while IFS= read -r cid; do local name name=$(sync_get_project_name "$cid") if [[ -n "$filter" ]]; then if ! echo "$name" | grep -qi "$filter"; then continue fi fi sync_one_container "$cid" "$host_projects" echo "" done <<<"$container_ids" log_success "Run '/insights' in Claude Code to include these sessions." } # Extract project name from devcontainer.local_folder label. sync_get_project_name() { local folder folder=$(docker inspect --format \ '{{index .Config.Labels "devcontainer.local_folder"}}' "$1") basename "$folder" } # Resolve the Claude projects dir inside a container without # docker exec (works on stopped containers too). # Reads CLAUDE_CONFIG_DIR from container env, falls back to # /home/<user>/.claude. sync_get_claude_projects_dir() { local cid="$1" local claude_dir claude_dir=$(docker inspect --format '{{json .Config.Env}}' "$cid" | tr ',' '\n' | tr -d '[]"' | grep '^CLAUDE_CONFIG_DIR=' | cut -d= -f2- || true) if [[ -n "$claude_dir" ]]; then echo "${claude_dir}/projects" return fi local user user=$(docker inspect --format '{{.Config.User}}' "$cid") if [[ -z "$user" || "$user" == "root" ]]; then echo "/root/.claude/projects" else echo "/home/${user}/.claude/projects" fi } sync_one_container() { local cid="$1" local host_projects="$2" local project_name status claude_dir folder project_name=$(sync_get_project_name "$cid") folder=$(docker inspect --format \ '{{index .Config.Labels "devcontainer.local_folder"}}' "$cid") status=$(docker inspect --format '{{.State.Status}}' "$cid") claude_dir=$(sync_get_claude_projects_dir "$cid") log_info "=== ${project_name} (${status}) ===" echo " Host path: ${folder}" echo " Container: ${cid:0:12}" # docker cp works on both running and stopped containers. local tmpdir tmpdir=$(mktemp -d) if ! docker cp "${cid}:${claude_dir}/." "$tmpdir/" 2>/dev/null; then echo " No sessions found, skipping." rm -rf "$tmpdir" return 0 fi local session_count session_count=$(find "$tmpdir" -name '*.jsonl' | wc -l | tr -d ' ') if [[ "$session_count" -eq 0 ]]; then echo " No sessions found, skipping." rm -rf "$tmpdir" return 0 fi echo " Sessions: ${session_count}" local total_copied=0 # Sync each project key subdirectory. for key_path in "$tmpdir"/*/; do [[ ! -d "$key_path" ]] && continue local key dest_key key=$(basename "$key_path") if [[ "$key" == "-workspace" ]]; then dest_key="-devcontainer-${project_name}" else dest_key="${key}" fi local dest_dir="${host_projects}/${dest_key}" mkdir -p "$dest_dir" local copied=0 while IFS= read -r -d '' file; do local rel="${file#"$key_path"}" local dest_file="${dest_dir}/${rel}" mkdir -p "$(dirname "$dest_file")" if [[ ! -e "$dest_file" ]] || [[ "$file" -nt "$dest_file" ]]; then cp -p "$file" "$dest_file" copied=$((copied + 1)) fi done < <(find "$key_path" -type f -print0) if [[ "$copied" -gt 0 ]]; then echo " Synced ${copied} file(s) -> ${dest_key}" fi total_copied=$((total_copied + copied)) done # Handle .jsonl files directly in projects/ (no subdirectory). local orphan_copied=0 local dest_dir="${host_projects}/-devcontainer-${project_name}" mkdir -p "$dest_dir" while IFS= read -r -d '' file; do local name name=$(basename "$file") local dest_file="${dest_dir}/${name}" if [[ ! -e "$dest_file" ]] || [[ "$file" -nt "$dest_file" ]]; then cp -p "$file" "$dest_file" orphan_copied=$((orphan_copied + 1)) fi done < <(find "$tmpdir" -maxdepth 1 -name '*.jsonl' -print0) if [[ "$orphan_copied" -gt 0 ]]; then echo " Synced ${orphan_copied} file(s) -> -devcontainer-${project_name}" total_copied=$((total_copied + orphan_copied)) fi rm -rf "$tmpdir" echo " Total: ${total_copied} file(s) synced." } cmd_cp() { local container_path="${1:-}" local host_path="${2:-}" if [[ -z "$container_path" ]] || [[ -z "$host_path" ]]; then log_error "Usage: devc cp <container_path> <host_path>" exit 1 fi local workspace_folder workspace_folder="$(get_workspace_folder)" # Find the running container local label="devcontainer.local_folder=$workspace_folder" local container_id container_id=$(docker ps -q --filter "label=$label" 2>/dev/null || true) if [[ -z "$container_id" ]]; then log_error "No running devcontainer found for $workspace_folder" exit 1 fi log_info "Copying $container_path → $host_path" docker cp "$container_id:$container_path" "$host_path" log_success "Copied $container_path → $host_path" } # Discovers all Docker resources associated with the current workspace. # Sets global variables: CONTAINER_ID, CONTAINER_STATUS, VOLUMES (array), IMAGE, IMAGE_UID discover_resources() { local workspace_folder="$1" local label="devcontainer.local_folder=$workspace_folder" CONTAINER_ID="" CONTAINER_STATUS="" VOLUMES=() IMAGE="" IMAGE_UID="" # Find container (any state: running, stopped, created, etc.) CONTAINER_ID=$(docker ps -aq --filter "label=$label" 2>/dev/null | head -1) if [[ -z "$CONTAINER_ID" ]]; then return 0 fi # Get container status CONTAINER_STATUS=$(docker inspect "$CONTAINER_ID" --format '{{.State.Status}}' 2>/dev/null || true) # Get volumes (docker volumes only, not bind mounts) while IFS= read -r vol; do [[ -n "$vol" ]] && VOLUMES+=("$vol") done < <(docker inspect "$CONTAINER_ID" --format '{{json .Mounts}}' 2>/dev/null | jq -r '.[] | select(.Type == "volume") | .Name' 2>/dev/null) # Get image and its -uid variant IMAGE=$(docker inspect "$CONTAINER_ID" --format '{{.Config.Image}}' 2>/dev/null || true) if [[ -n "$IMAGE" ]]; then if [[ "$IMAGE" == *-uid ]]; then IMAGE_UID="$IMAGE" IMAGE="${IMAGE%-uid}" else IMAGE_UID="${IMAGE}-uid" fi fi } print_destroy_summary() { echo "" log_warn "The following resources will be permanently removed:" echo "" if [[ -n "$CONTAINER_ID" ]]; then local container_name container_name=$(docker inspect "$CONTAINER_ID" --format '{{.Name}}' 2>/dev/null | sed 's|^/||') echo " Container: ${container_name:-$CONTAINER_ID}" if [[ "$CONTAINER_STATUS" == "running" ]]; then echo " (currently running -- will be force-stopped)" fi fi if [[ ${#VOLUMES[@]} -gt 0 ]]; then echo " Volumes:" for vol in "${VOLUMES[@]}"; do echo " $vol" done fi if [[ -n "$IMAGE" ]]; then echo " Image: $IMAGE" if docker image inspect "$IMAGE_UID" &>/dev/null; then echo " $IMAGE_UID" fi fi echo "" } cmd_destroy() { local force=false # Parse flags while [[ $# -gt 0 ]]; do case "$1" in -f | --force) force=true shift ;; *) break ;; esac done local workspace_folder workspace_folder="$(get_workspace_folder "${1:-}")" discover_resources "$workspace_folder" # No resources found (idempotent behavior) if [[ -z "$CONTAINER_ID" ]]; then log_info "No devcontainer found for $workspace_folder" return 0 fi print_destroy_summary # Running container warning if [[ "$CONTAINER_STATUS" == "running" && "$force" != true ]]; then log_warn "Container is currently running!" read -p "Force-stop the running container? [y/N] " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then log_info "Aborted." return 0 fi fi # Main confirmation prompt if [[ "$force" != true ]]; then read -p "Destroy these resources? [y/N] " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then log_info "Aborted." return 0 fi fi # Deletion, in order: stop, remove container, volumes, images if [[ -n "$CONTAINER_ID" && "$CONTAINER_STATUS" == "running" ]]; then log_info "Stopping container..." docker stop "$CONTAINER_ID" >/dev/null 2>&1 || true fi if [[ -n "$CONTAINER_ID" ]]; then log_info "Removing container..." docker rm -f "$CONTAINER_ID" >/dev/null 2>&1 || true fi for vol in "${VOLUMES[@]}"; do log_info "Removing volume: $vol" docker volume rm -f "$vol" >/dev/null 2>&1 || true done if [[ -n "$IMAGE" ]]; then log_info "Removing image: $IMAGE" docker rmi -f "$IMAGE" >/dev/null 2>&1 || true if docker image inspect "$IMAGE_UID" &>/dev/null 2>&1; then log_info "Removing image: $IMAGE_UID" docker rmi -f "$IMAGE_UID" >/dev/null 2>&1 || true fi fi log_success "All resources destroyed for $workspace_folder" } cmd_self_install() { local install_dir="$HOME/.local/bin" local install_path="$install_dir/devc" mkdir -p "$install_dir" # Create a symlink to the original script ln -sf "$SCRIPT_DIR/$SCRIPT_NAME" "$install_path" log_success "Installed 'devc' to $install_path" # Check if in PATH if [[ ":$PATH:" != *":$install_dir:"* ]]; then log_warn "$install_dir is not in your PATH" log_info "Add this to your shell profile:" echo " export PATH=\"\$HOME/.local/bin:\$PATH\"" fi } cmd_update() { log_info "Updating devc..." if ! git -C "$SCRIPT_DIR" rev-parse --is-inside-work-tree &>/dev/null; then log_error "Not a git repository: $SCRIPT_DIR" log_info "Re-clone with: rm -rf ~/.claude-devcontainer && git clone https://github.com/trailofbits/claude-code-devcontainer ~/.claude-devcontainer" exit 1 fi local before_sha after_sha before_sha=$(git -C "$SCRIPT_DIR" rev-parse HEAD) if ! git -C "$SCRIPT_DIR" pull --ff-only; then log_error "Update failed. Try: cd $SCRIPT_DIR && git pull" exit 1 fi after_sha=$(git -C "$SCRIPT_DIR" rev-parse HEAD) if [[ "$before_sha" == "$after_sha" ]]; then log_success "Already up to date" else log_success "Updated from ${before_sha:0:7} to ${after_sha:0:7}" fi } cmd_dot() { # Install template and start container in one command cmd_template "." cmd_up "." } # Main command dispatcher main() { if [[ $# -eq 0 ]]; then print_usage exit 1 fi local command="$1" shift case "$command" in .) cmd_dot ;; up) cmd_up "$@" ;; rebuild) cmd_rebuild "$@" ;; down) cmd_down "$@" ;; destroy) cmd_destroy "$@" ;; shell) cmd_shell ;; exec) [[ "${1:-}" == "--" ]] && shift cmd_exec "$@" ;; upgrade) cmd_upgrade ;; mount) cmd_mount "$@" ;; sync) cmd_sync "$@" ;; cp) cmd_cp "$@" ;; self-install) cmd_self_install ;; update) cmd_update ;; template) cmd_template "$@" ;; help | --help | -h) print_usage ;; *) log_error "Unknown command: $command" print_usage exit 1 ;; esac } main "$@" -
post_install.py 8.4 KB
#!/usr/bin/env python3 """Post-install configuration for Claude Code devcontainer. Runs on container creation to set up: - Onboarding bypass (when CLAUDE_CODE_OAUTH_TOKEN is set) - Claude settings (bypassPermissions mode) - Tmux configuration (200k history, mouse support) - Directory ownership fixes for mounted volumes """ import contextlib import json import os import subprocess import sys from pathlib import Path def setup_onboarding_bypass(): """Bypass the interactive onboarding wizard when CLAUDE_CODE_OAUTH_TOKEN is set. Runs `claude -p` to seed ~/.claude.json with auth state. The subprocess writes the config file during startup before the API call completes, so a timeout is expected and acceptable. After the subprocess finishes (or times out), we check whether ~/.claude.json was populated and only then set hasCompletedOnboarding. Workaround for https://github.com/anthropics/claude-code/issues/8938. """ token = os.environ.get("CLAUDE_CODE_OAUTH_TOKEN", "").strip() if not token: print( "[post_install] No CLAUDE_CODE_OAUTH_TOKEN set, skipping onboarding bypass", file=sys.stderr, ) return # When `CLAUDE_CONFIG_DIR` is set, as is done in `devcontainer.json`, # `claude` unexpectedly looks for `.claude.json` in *that* folder, # instead of in `~`, contradicting the documentation. # See https://github.com/anthropics/claude-code/issues/3833#issuecomment-3694918874 claude_json_dir = Path(os.environ.get("CLAUDE_CONFIG_DIR", Path.home())) claude_json = claude_json_dir / ".claude.json" print("[post_install] Running claude -p to populate auth state...", file=sys.stderr) try: result = subprocess.run( ["claude", "-p", "ok"], capture_output=True, text=True, timeout=30, ) if result.returncode != 0: print( f"[post_install] claude -p exited {result.returncode}: {result.stderr.strip()}", file=sys.stderr, ) except subprocess.TimeoutExpired: print( "[post_install] claude -p timed out (expected on cold start)", file=sys.stderr, ) except (FileNotFoundError, OSError) as e: print( f"[post_install] Warning: could not run claude ({e}) — onboarding bypass skipped", file=sys.stderr, ) return if not claude_json.exists(): print( f"[post_install] Warning: {claude_json} not created by claude -p — " "onboarding bypass skipped", file=sys.stderr, ) return config: dict = {} try: config = json.loads(claude_json.read_text()) except json.JSONDecodeError as e: print( f"[post_install] Warning: {claude_json} has invalid JSON ({e}), starting fresh", file=sys.stderr, ) config["hasCompletedOnboarding"] = True claude_json.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8") print(f"[post_install] Onboarding bypass configured: {claude_json}", file=sys.stderr) def setup_claude_settings(): """Configure Claude Code with bypassPermissions enabled.""" claude_dir = Path(os.environ.get("CLAUDE_CONFIG_DIR", Path.home() / ".claude")) claude_dir.mkdir(parents=True, exist_ok=True) settings_file = claude_dir / "settings.json" # Load existing settings or start fresh settings = {} if settings_file.exists(): with contextlib.suppress(json.JSONDecodeError): settings = json.loads(settings_file.read_text()) # Set bypassPermissions mode if "permissions" not in settings: settings["permissions"] = {} settings["permissions"]["defaultMode"] = "bypassPermissions" settings_file.write_text(json.dumps(settings, indent=2) + "\n", encoding="utf-8") print(f"[post_install] Claude settings configured: {settings_file}", file=sys.stderr) def setup_tmux_config(): """Configure tmux with 200k history, mouse support, and vi keys.""" tmux_conf = Path.home() / ".tmux.conf" if tmux_conf.exists(): print("[post_install] Tmux config exists, skipping", file=sys.stderr) return config = """\ # 200k line scrollback history set-option -g history-limit 200000 # Enable mouse support set -g mouse on # Use vi keys in copy mode setw -g mode-keys vi # Start windows and panes at 1, not 0 set -g base-index 1 setw -g pane-base-index 1 # Renumber windows when one is closed set -g renumber-windows on # Faster escape time for vim set -sg escape-time 10 # True color support set -g default-terminal "tmux-256color" set -ag terminal-overrides ",xterm-256color:RGB" # Terminal features (ghostty, cursor shape in vim) set -as terminal-features ",xterm-ghostty:RGB" set -as terminal-features ",xterm*:RGB" set -ga terminal-overrides ",xterm*:colors=256" set -ga terminal-overrides '*:Ss=\\E[%p1%d q:Se=\\E[ q' # Status bar set -g status-style 'bg=#333333 fg=#ffffff' set -g status-left '[#S] ' set -g status-right '%Y-%m-%d %H:%M' """ tmux_conf.write_text(config, encoding="utf-8") print(f"[post_install] Tmux configured: {tmux_conf}", file=sys.stderr) def fix_directory_ownership(): """Fix ownership of mounted volumes that may have root ownership.""" uid = os.getuid() gid = os.getgid() dirs_to_fix = [ Path.home() / ".claude", Path("/commandhistory"), Path.home() / ".config" / "gh", ] for dir_path in dirs_to_fix: if dir_path.exists(): try: # Use sudo to fix ownership if needed stat_info = dir_path.stat() if stat_info.st_uid != uid: subprocess.run( ["sudo", "chown", "-R", f"{uid}:{gid}", str(dir_path)], check=True, capture_output=True, ) print(f"[post_install] Fixed ownership: {dir_path}", file=sys.stderr) except (PermissionError, subprocess.CalledProcessError) as e: print( f"[post_install] Warning: Could not fix ownership of {dir_path}: {e}", file=sys.stderr, ) def setup_global_gitignore(): """Set up global gitignore and local git config. Since ~/.gitconfig is mounted read-only from host, we create a local config file that includes the host config and adds container-specific settings like core.excludesfile and delta configuration. GIT_CONFIG_GLOBAL env var (set in devcontainer.json) points git to this local config as the "global" config. """ home = Path.home() gitignore = home / ".gitignore_global" local_gitconfig = home / ".gitconfig.local" host_gitconfig = home / ".gitconfig" # Create global gitignore with common patterns patterns = """\ # Claude Code .claude/ # macOS .DS_Store .AppleDouble .LSOverride ._* # Python *.pyc *.pyo __pycache__/ *.egg-info/ .eggs/ *.egg .venv/ venv/ .mypy_cache/ .ruff_cache/ # Node node_modules/ .npm/ # Editors *.swp *.swo *~ .idea/ .vscode/ *.sublime-* # Misc *.log .env.local .env.*.local """ gitignore.write_text(patterns, encoding="utf-8") print(f"[post_install] Global gitignore created: {gitignore}", file=sys.stderr) # Create local git config that includes host config and sets excludesfile + delta # Delta config is included here so it works even if host doesn't have it configured local_config = f"""\ # Container-local git config # Includes host config (mounted read-only) and adds container settings [include] path = {host_gitconfig} [core] excludesfile = {gitignore} pager = delta [interactive] diffFilter = delta --color-only [delta] navigate = true light = false line-numbers = true side-by-side = false [merge] conflictstyle = diff3 [diff] colorMoved = default [gpg "ssh"] program = /usr/bin/ssh-keygen """ local_gitconfig.write_text(local_config, encoding="utf-8") print(f"[post_install] Local git config created: {local_gitconfig}", file=sys.stderr) def main(): """Run all post-install configuration.""" print("[post_install] Starting post-install configuration...", file=sys.stderr) setup_onboarding_bypass() setup_claude_settings() setup_tmux_config() fix_directory_ownership() setup_global_gitignore() print("[post_install] Configuration complete!", file=sys.stderr) if __name__ == "__main__": main()
-
-
SKILL.md 7.9 KB
--- name: devcontainer-setup description: Creates devcontainers with Claude Code, language-specific tooling (Python/Node/Rust/Go), and persistent volumes. Use when adding devcontainer support to a project, setting up isolated development environments, or configuring sandboxed Claude Code workspaces. --- # Devcontainer Setup Skill Creates a pre-configured devcontainer with Claude Code and language-specific tooling. ## When to Use - User asks to "set up a devcontainer" or "add devcontainer support" - User wants a sandboxed Claude Code development environment - User needs isolated development environments with persistent configuration ## When NOT to Use - User already has a devcontainer configuration and just needs modifications - User is asking about general Docker or container questions - User wants to deploy production containers (this is for development only) ## Workflow ```mermaid flowchart TB start([User requests devcontainer]) recon[1. Project Reconnaissance] detect[2. Detect Languages] generate[3. Generate Configuration] write[4. Write files to .devcontainer/] done([Done]) start --> recon recon --> detect detect --> generate generate --> write write --> done ``` ## Phase 1: Project Reconnaissance ### Infer Project Name Check in order (use first match): 1. `package.json` → `name` field 2. `pyproject.toml` → `project.name` 3. `Cargo.toml` → `package.name` 4. `go.mod` → module path (last segment after `/`) 5. Directory name as fallback Convert to slug: lowercase, replace spaces/underscores with hyphens. ### Detect Language Stack | Language | Detection Files | |----------|-----------------| | Python | `pyproject.toml`, `*.py` | | Node/TypeScript | `package.json`, `tsconfig.json` | | Rust | `Cargo.toml` | | Go | `go.mod`, `go.sum` | ### Multi-Language Projects If multiple languages are detected, configure all of them in the following priority order: 1. **Python** - Primary language, uses Dockerfile for uv + Python installation 2. **Node/TypeScript** - Uses devcontainer feature 3. **Rust** - Uses devcontainer feature 4. **Go** - Uses devcontainer feature For multi-language `postCreateCommand`, chain all setup commands: ``` uv run /opt/post_install.py && uv sync && npm ci ``` Extensions and settings from all detected languages should be merged into the configuration. ## Phase 2: Generate Configuration Start with base templates from `resources/` directory. Substitute: - `{{PROJECT_NAME}}` → Human-readable name (e.g., "My Project") - `{{PROJECT_SLUG}}` → Slug for volumes (e.g., "my-project") Then apply language-specific modifications below. ## Base Template Features The base template includes: - **Claude Code** with marketplace plugins (anthropics/skills, trailofbits/skills, trailofbits/skills-curated) - **Sandboxing** via bubblewrap and socat - **Python 3.13** via uv (fast binary download) - **Node 22** via fnm (Fast Node Manager) - **ast-grep** for AST-based code search - **Network isolation tools** (iptables, ipset) with NET_ADMIN capability - **Security mounts**: `.devcontainer/` mounted read-only to prevent container escape - **Token forwarding**: `CLAUDE_CODE_OAUTH_TOKEN` and `ANTHROPIC_API_KEY` via `remoteEnv` - **Modern CLI tools**: ripgrep, fd, fzf, tmux, git-delta --- ## Language-Specific Sections ### Python Projects **Detection:** `pyproject.toml`, `requirements.txt`, `setup.py`, or `*.py` files **Dockerfile additions:** The base Dockerfile already includes Python 3.13 via uv. If a different version is required (detected from `pyproject.toml`), modify the Python installation: ```dockerfile # Install Python via uv (fast binary download, not source compilation) RUN uv python install <version> --default ``` **devcontainer.json extensions:** Add to `customizations.vscode.extensions`: ```json "ms-python.python", "ms-python.vscode-pylance", "charliermarsh.ruff" ``` Add to `customizations.vscode.settings`: ```json "python.defaultInterpreterPath": ".venv/bin/python", "[python]": { "editor.defaultFormatter": "charliermarsh.ruff", "editor.codeActionsOnSave": { "source.organizeImports": "explicit" } } ``` **postCreateCommand:** If `pyproject.toml` exists, chain commands: ``` rm -rf .venv && uv sync && uv run /opt/post_install.py ``` --- ### Node/TypeScript Projects **Detection:** `package.json` or `tsconfig.json` **No Dockerfile additions needed:** The base template includes Node 22 via fnm (Fast Node Manager). **devcontainer.json extensions:** Add to `customizations.vscode.extensions`: ```json "dbaeumer.vscode-eslint", "esbenp.prettier-vscode" ``` Add to `customizations.vscode.settings`: ```json "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" } ``` **postCreateCommand:** Detect package manager from lockfile and chain with base command: - `pnpm-lock.yaml` → `uv run /opt/post_install.py && pnpm install --frozen-lockfile` - `yarn.lock` → `uv run /opt/post_install.py && yarn install --frozen-lockfile` - `package-lock.json` → `uv run /opt/post_install.py && npm ci` - No lockfile → `uv run /opt/post_install.py && npm install` --- ### Rust Projects **Detection:** `Cargo.toml` **Features to add:** ```json "ghcr.io/devcontainers/features/rust:1": {} ``` **devcontainer.json extensions:** Add to `customizations.vscode.extensions`: ```json "rust-lang.rust-analyzer", "tamasfe.even-better-toml" ``` Add to `customizations.vscode.settings`: ```json "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer" } ``` **postCreateCommand:** If `Cargo.lock` exists, use locked builds: ``` uv run /opt/post_install.py && cargo build --locked ``` If no lockfile, use standard build: ``` uv run /opt/post_install.py && cargo build ``` --- ### Go Projects **Detection:** `go.mod` **Features to add:** ```json "ghcr.io/devcontainers/features/go:1": { "version": "latest" } ``` **devcontainer.json extensions:** Add to `customizations.vscode.extensions`: ```json "golang.go" ``` Add to `customizations.vscode.settings`: ```json "[go]": { "editor.defaultFormatter": "golang.go" }, "go.useLanguageServer": true ``` **postCreateCommand:** ``` uv run /opt/post_install.py && go mod download ``` --- ## Reference Material For additional guidance, see: - `references/dockerfile-best-practices.md` - Layer optimization, multi-stage builds, architecture support - `references/features-vs-dockerfile.md` - When to use devcontainer features vs custom Dockerfile --- ## Adding Persistent Volumes Pattern for new mounts in `devcontainer.json`: ```json "mounts": [ "source={{PROJECT_SLUG}}-<purpose>-${devcontainerId},target=<container-path>,type=volume" ] ``` Common additions: - `source={{PROJECT_SLUG}}-cargo-${devcontainerId},target=/home/vscode/.cargo,type=volume` (Rust) - `source={{PROJECT_SLUG}}-go-${devcontainerId},target=/home/vscode/go,type=volume` (Go) --- ## Output Files Generate these files in the project's `.devcontainer/` directory: 1. `Dockerfile` - Container build instructions 2. `devcontainer.json` - VS Code/devcontainer configuration 3. `post_install.py` - Post-creation setup script 4. `.zshrc` - Shell configuration 5. `install.sh` - CLI helper for managing the devcontainer (`devc` command) --- ## Validation Checklist Before presenting files to the user, verify: 1. All `{{PROJECT_NAME}}` placeholders are replaced with the human-readable name 2. All `{{PROJECT_SLUG}}` placeholders are replaced with the slugified name 3. JSON syntax is valid in `devcontainer.json` (no trailing commas, proper nesting) 4. Language-specific extensions are added for all detected languages 5. `postCreateCommand` includes all required setup commands (chained with `&&`) --- ## User Instructions After generating, inform the user: 1. How to start: "Open in VS Code and select 'Reopen in Container'" 2. Alternative: `devcontainer up --workspace-folder .` 3. CLI helper: Run `.devcontainer/install.sh self-install` to add the `devc` command to PATH
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.