Generated banner for Devcontainer Setup
Generated logo for Devcontainer Setup

Devcontainer Setup

Trail of BitsOfficialPublished by Trail of Bits

  • Skill
  • Workflow
  • Cloud and DevOps
  • CC-BY-SA-4.0
  • Medium risk
  • Has scripts

From the author

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.
— the description by Trail of Bits

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The devcontainer-setup skill provides a structured workflow for generating isolated, reproducible development environments. It automatically detects the project's language stack (Python, Node/TypeScript, Rust, or Go) and generates a complete .devcontainer configuration. This includes a pre-configured Dockerfile, VS Code settings, persistent volumes for build caches, and integrated tooling like Claude Code, ast-grep, and various CLI utilities. The setup emphasizes security through network isolation, read-only mounts, and token forwarding, ensuring a consistent and secure workspace for developers.

What it does

  • Detects project languages and dependencies automatically.
  • Generates a .devcontainer directory with Dockerfile and configuration files.
  • Configures Claude Code and essential CLI tools within the container.
  • Sets up persistent volumes for language-specific build caches.
  • Merges language-specific extensions and settings into the configuration.
  • Provides a helper script for managing the container lifecycle.

When to use it

  • When initializing a new project and requiring a standardized development environment.
  • When you need to add devcontainer support to an existing codebase.
  • When creating a sandboxed workspace for Claude Code.
  • When you require isolated environments with persistent configuration across sessions.

Advantages

  • Ensures consistent development environments across team members.
  • Automates complex configuration tasks for multi-language projects.
  • Enhances security with built-in isolation and read-only mounts.
  • Optimizes build times using persistent volumes for language caches.
  • Includes modern, pre-configured CLI tools for improved productivity.

Usage examples

“Set up a devcontainer for this project.”
The agent analyzes the project files, detects the language stack, and generates the necessary .devcontainer configuration files.
“Add devcontainer support to my Python project.”
The agent creates a Python-optimized devcontainer with uv, Ruff, and Pylance, including a postCreateCommand to sync dependencies.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/trailofbits/devcontainer-setup/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip devcontainer-setup.zip -d .agents/skills

Documentation by Trail of Bits

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):

  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
LanguageDetection Files
Pythonpyproject.toml, *.py
Node/TypeScriptpackage.json, tsconfig.json
RustCargo.toml
Gogo.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:

# 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-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:

"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 support

  • references/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:

  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

Files

resources/post_install.py

#!/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()

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an API key, Needs Python, Needs Node.js

Security

What our scanner noticed· Medium risk

  • Instructions that use the network
  • Downloads piped into a shell
  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Trail of Bits
Publisher
Trail of Bits (Official)
Source
trailofbits/skills · plugins/devcontainer-setup/skills/devcontainer-setup · commit e4dd0bb
Licence
Creative Commons Attribution-ShareAlike 4.0 International (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Trail of Bits.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download devcontainer-setup.zip