Devcontainer Setup
Trail of BitsOfficialPublished by Trail of Bits
- Skill
- Workflow
- Cloud and DevOps
- CC-BY-SA-4.0
- Medium risk
- Has scripts
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
Download the skill as a ZIP:
https://autogineer.com/agent-skills/trailofbits/devcontainer-setup/downloadUnzip 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 --> donePhase 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 ciExtensions 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 escapeToken forwarding:
CLAUDE_CODE_OAUTH_TOKENandANTHROPIC_API_KEYviaremoteEnvModern 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> --defaultdevcontainer.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.pyNode/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 ciNo 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 --lockedIf no lockfile, use standard build:
uv run /opt/post_install.py && cargo buildGo 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": truepostCreateCommand:
uv run /opt/post_install.py && go mod downloadReference 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 nameAll
{{PROJECT_SLUG}}placeholders are replaced with the slugified nameJSON 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
- ATTRIBUTION.mdAdded by AutoGineer948 B
- LICENSELicence file added by AutoGineer19.7 KiB
- SKILL.md7.9 KiB
- agents/openai.yaml239 B
- assets/trail-of-bits-mark.svg3.0 KiB
- references/dockerfile-best-practices.md3.3 KiB
- references/features-vs-dockerfile.md675 B
- resources/.zshrc1.8 KiB
- resources/Dockerfile3.8 KiB
- resources/devcontainer.json2.5 KiB
- resources/install.shScript22.6 KiB
- resources/post_install.pyScript8.4 KiB
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 or Trail of Bits.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download devcontainer-setup.zip