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://mp-staging.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.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download devcontainer-setup.zip