Devcontainer Claude Code Implementation Plan¶
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Enable Claude Code CLI to run entirely within a devcontainer with full MCP server support, Vault-based secret management, and portable plugin configuration.
Architecture: Devcontainer with official Anthropic Claude Code feature, uv feature for Python tooling, Docker volume for persistent config, and post-create scripts that fetch secrets from Vault at container startup.
Tech Stack: VS Code Devcontainers, Docker, HashiCorp Vault, Node.js 22, uv, ripgrep, ast-grep
Reference: docs/plans/2025-12-25-devcontainer-claude-code-design.md
Task 1: Update .gitignore¶
Files:
- Modify: .gitignore
Step 1: Add .env.devcontainer to gitignore
Add this line to .gitignore:
# Devcontainer environment file (generated by initializeCommand)
.devcontainer/.env.devcontainer
Step 2: Verify change
Run: grep -q "\.devcontainer/\.env" .gitignore && echo "OK" || echo "FAILED"
Expected: OK
Step 3: Commit
git add .gitignore
git commit -m "chore: add devcontainer env file to gitignore"
Task 2: Update Dockerfile¶
Files:
- Modify: .devcontainer/Dockerfile
Step 1: Add ripgrep and ast-grep installation
After the existing apt-get install block, add:
# Install ripgrep for fast code search (required by Serena MCP)
RUN apt-get update && apt-get install -y --no-install-recommends ripgrep \
&& rm -rf /var/lib/apt/lists/*
# Install ast-grep for structural code search
RUN curl -fsSL https://raw.githubusercontent.com/ast-grep/ast-grep/main/install.sh | bash -s -- -y \
&& mv /root/.cargo/bin/sg /usr/local/bin/ \
&& mv /root/.cargo/bin/ast-grep /usr/local/bin/
Step 2: Verify Dockerfile syntax
Run: docker build -f .devcontainer/Dockerfile --target=base -t devcontainer-test . --no-cache --progress=plain 2>&1 | tail -20
Expected: Build succeeds (or partial success if target doesn't exist)
Step 3: Commit
git add .devcontainer/Dockerfile
git commit -m "feat(devcontainer): add ripgrep and ast-grep for code search"
Task 3: Update devcontainer.json Features¶
Files:
- Modify: .devcontainer/devcontainer.json
Step 1: Add Claude Code and uv features
Update the features block to include:
"features": {
"ghcr.io/devcontainers/features/git:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"moby": false
},
"ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {
"version": "latest",
"helm": "latest",
"minikube": "none"
},
"ghcr.io/devcontainers/features/terraform:1": {
"version": "latest"
},
"ghcr.io/devcontainers/features/node:1": {
"version": "22"
},
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
"ghcr.io/jsburckhardt/devcontainer-features/uv:1": {}
}
Step 2: Update mounts to use Docker volume
Replace the mounts array with:
"mounts": [
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached,readonly",
"source=${localEnv:HOME}/.kube,target=/home/vscode/.kube,type=bind,consistency=cached",
"source=${localEnv:HOME}/.1password,target=/home/vscode/.1password,type=bind",
"source=claude-code-config-${devcontainerId},target=/home/vscode/.claude,type=volume"
]
Step 3: Add initializeCommand and runArgs
Add these fields after containerEnv:
"initializeCommand": "echo \"GH_TOKEN=$(gh auth token 2>/dev/null || echo '')\" > .devcontainer/.env.devcontainer",
"runArgs": ["--env-file", ".devcontainer/.env.devcontainer"]
Step 4: Validate JSON syntax
Run: jq . .devcontainer/devcontainer.json > /dev/null && echo "Valid JSON" || echo "Invalid JSON"
Expected: Valid JSON
Step 5: Commit
git add .devcontainer/devcontainer.json
git commit -m "feat(devcontainer): add Claude Code, uv, Node.js features
- Official Anthropic devcontainer feature for Claude Code
- uv feature for Python package management
- Node.js 22 for MCP servers
- Docker volume for .claude persistence
- initializeCommand for GH_TOKEN capture"
Task 4: Create Vault Policy¶
Files:
- Create: tf/vault/policy-claude-code.tf
Step 1: Create the Vault policy file
# SPDX-License-Identifier: MIT-0
#
# Vault policy for Claude Code devcontainer access
# Allows reading shared MCP API keys and per-user Anthropic keys
data "vault_policy_document" "claude_code" {
# Read shared MCP secrets (firecrawl, exa, notion API keys)
rule {
path = "secret/data/fzymgc-house/cluster/claude-code"
capabilities = ["read"]
description = "Read shared MCP API keys for Claude Code"
}
# Read own user's Claude secrets (Anthropic API key)
# Uses GitHub username from identity alias
rule {
path = "secret/data/fzymgc-house/users/{{identity.entity.aliases.auth_github_*.name}}/claude-code"
capabilities = ["read"]
description = "Read per-user Anthropic API key"
}
}
resource "vault_policy" "claude_code" {
name = "claude-code"
policy = data.vault_policy_document.claude_code.hcl
}
Step 2: Validate Terraform syntax
Run: terraform -chdir=tf/vault fmt -check policy-claude-code.tf && echo "OK" || terraform -chdir=tf/vault fmt policy-claude-code.tf
Expected: OK or file formatted
Step 3: Commit
git add tf/vault/policy-claude-code.tf
git commit -m "feat(vault): add claude-code policy for devcontainer secrets
- Shared MCP API keys at secret/fzymgc-house/cluster/claude-code
- Per-user Anthropic keys at secret/fzymgc-house/users/<username>/claude-code
- Uses GitHub identity alias for user scoping"
Task 5: Create Secret Fetch Script¶
Files:
- Create: .devcontainer/setup-claude-secrets.sh
Step 1: Create standalone secret fetch script
#!/usr/bin/env bash
# SPDX-License-Identifier: MIT
# Fetch Claude Code secrets from Vault and configure environment
# Run after: vault login -method=github && gh auth login
set -euo pipefail
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Check prerequisites
if ! command -v vault &> /dev/null; then
log_error "Vault CLI not installed"
exit 1
fi
if ! vault token lookup &> /dev/null; then
log_error "Not authenticated to Vault. Run: vault login -method=github"
exit 1
fi
if ! command -v gh &> /dev/null; then
log_error "GitHub CLI not installed"
exit 1
fi
GH_USER=$(gh api user --jq '.login' 2>/dev/null || echo "")
if [[ -z "$GH_USER" ]]; then
log_error "Not authenticated to GitHub. Run: gh auth login -p https -w"
exit 1
fi
log_info "Fetching Claude Code secrets for user: $GH_USER"
# Fetch shared MCP secrets
SHARED_SECRETS=$(vault kv get -format=json secret/fzymgc-house/cluster/claude-code 2>/dev/null || echo "{}")
if [[ "$SHARED_SECRETS" == "{}" ]]; then
log_warn "Shared secrets not found - MCP servers may not work"
else
log_info "✓ Fetched shared MCP secrets"
fi
# Fetch per-user Anthropic key
USER_SECRETS=$(vault kv get -format=json "secret/fzymgc-house/users/${GH_USER}/claude-code" 2>/dev/null || echo "{}")
if [[ "$USER_SECRETS" == "{}" ]]; then
log_warn "User secrets not found at secret/fzymgc-house/users/${GH_USER}/claude-code"
log_warn "Claude Code will prompt for API key on first run"
fi
# Write environment file
ENV_FILE="/home/vscode/.claude-env"
{
echo "# Claude Code environment - generated by setup-claude-secrets.sh"
echo "# $(date -Iseconds)"
echo ""
if [[ "$USER_SECRETS" != "{}" ]]; then
ANTHROPIC_KEY=$(echo "$USER_SECRETS" | jq -r '.data.data.anthropic_api_key // empty')
if [[ -n "$ANTHROPIC_KEY" ]]; then
echo "export ANTHROPIC_API_KEY='$ANTHROPIC_KEY'"
fi
fi
if [[ "$SHARED_SECRETS" != "{}" ]]; then
FIRECRAWL_KEY=$(echo "$SHARED_SECRETS" | jq -r '.data.data.firecrawl_api_key // empty')
EXA_KEY=$(echo "$SHARED_SECRETS" | jq -r '.data.data.exa_api_key // empty')
NOTION_KEY=$(echo "$SHARED_SECRETS" | jq -r '.data.data.notion_api_key // empty')
[[ -n "$FIRECRAWL_KEY" ]] && echo "export FIRECRAWL_API_KEY='$FIRECRAWL_KEY'"
[[ -n "$EXA_KEY" ]] && echo "export EXA_API_KEY='$EXA_KEY'"
[[ -n "$NOTION_KEY" ]] && echo "export NOTION_API_KEY='$NOTION_KEY'"
fi
} > "$ENV_FILE"
chmod 600 "$ENV_FILE"
# Add to .bashrc if not already present
if ! grep -q 'source ~/.claude-env' /home/vscode/.bashrc 2>/dev/null; then
echo '[ -f ~/.claude-env ] && source ~/.claude-env' >> /home/vscode/.bashrc
fi
log_info "✓ Claude Code secrets written to $ENV_FILE"
log_info ""
log_info "To activate in current shell: source ~/.claude-env"
log_info "New shells will load automatically"
Step 2: Make executable
Run: chmod +x .devcontainer/setup-claude-secrets.sh
Step 3: Commit
git add .devcontainer/setup-claude-secrets.sh
git commit -m "feat(devcontainer): add setup-claude-secrets.sh
Standalone script to fetch Claude Code secrets from Vault:
- Shared MCP API keys (firecrawl, exa, notion)
- Per-user Anthropic API key
- Writes to ~/.claude-env with restricted permissions"
Task 6: Update post-create.sh¶
Files:
- Modify: .devcontainer/post-create.sh
Step 1: Remove Claude Code curl installation
Remove this entire block (approximately lines 88-106):
# Install Claude Code CLI using native binary installer
log_info "Installing Claude Code CLI..."
# ... entire block through the closing fi
Step 2: Add git safeguards function
Add after the log_warn function definitions:
setup_git_safeguards() {
log_info "Setting up git safeguards..."
# Create wrapper to warn on --no-verify usage
cat > /usr/local/bin/git-commit-wrapper << 'WRAPPER'
#!/bin/bash
if [[ "$*" == *"--no-verify"* ]] || [[ "$*" == *"-n"* ]]; then
echo "⚠️ Warning: --no-verify bypasses pre-commit hooks"
echo " Consider fixing hook issues instead of bypassing"
fi
exec /usr/bin/git commit "$@"
WRAPPER
chmod +x /usr/local/bin/git-commit-wrapper
git config --global alias.commit '!git-commit-wrapper'
log_info "✓ Git safeguards configured"
}
Step 3: Add Claude secrets setup call
Add after the kubectl setup section:
# Set up Claude Code if authenticated
if command -v claude &> /dev/null; then
log_info "Claude Code CLI available (installed via devcontainer feature)"
if vault token lookup &> /dev/null && gh auth status &> /dev/null; then
log_info "Fetching Claude Code secrets from Vault..."
if bash .devcontainer/setup-claude-secrets.sh; then
log_info "✓ Claude Code secrets configured"
else
log_warn "Failed to fetch Claude Code secrets - run setup-claude-secrets.sh after auth"
fi
else
log_warn "Claude Code secrets not configured (requires Vault + GitHub auth)"
echo "After authenticating, run: bash .devcontainer/setup-claude-secrets.sh"
fi
fi
Step 4: Call setup_git_safeguards
Add call to setup_git_safeguards near the end of the script (after git config section):
# Set up git safeguards
setup_git_safeguards
Step 5: Update the "Available tools" section
Update the Claude Code line:
echo " - Claude Code: $(claude --version 2>/dev/null || echo 'not installed')"
Step 6: Commit
git add .devcontainer/post-create.sh
git commit -m "feat(devcontainer): update post-create for Claude Code feature
- Remove curl install (now via devcontainer feature)
- Add git safeguards (--no-verify warning)
- Call setup-claude-secrets.sh when authenticated"
Task 7: Create Project Plugin Configuration¶
Files:
- Create: .claude/plugins.json
- Create: .claude/settings.json
Step 1: Create .claude directory
Run: mkdir -p .claude
Step 2: Create plugins.json
{
"plugins": [
"superpowers@superpowers-marketplace",
"commit-commands@claude-code-plugins",
"pr-review-toolkit@claude-code-plugins",
"feature-dev@claude-code-plugins",
"elements-of-style@superpowers-marketplace"
]
}
Step 3: Create settings.json
{
"permissions": {
"allow": [
"Edit(.devcontainer/**)",
"Edit(ansible/**)",
"Edit(tf/**)",
"Edit(argocd/**)",
"Edit(docs/**)",
"Edit(.claude/**)",
"Bash(git:*)",
"Bash(terraform:*)",
"Bash(kubectl:*)",
"Bash(ansible:*)",
"Bash(vault:*)",
"Bash(gh:*)"
]
}
}
Step 4: Commit
git add .claude/
git commit -m "feat: add project-local Claude plugin configuration
- plugins.json: superpowers, commit-commands, pr-review-toolkit, feature-dev
- settings.json: project-specific permission allowlist"
Task 8: Update Container MCP Configuration¶
Files:
- Create: .claude/mcp.json
Step 1: Create container-aware MCP configuration
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]
},
"kubernetes-mcp-server": {
"command": "npx",
"args": ["-y", "kubernetes-mcp-server@latest", "--read-only"],
"env": {
"KUBECONFIG": "/home/vscode/.kube/config"
}
},
"context7": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp@latest"]
},
"firecrawl-mcp": {
"command": "npx",
"args": ["-y", "firecrawl-mcp"],
"env": {
"FIRECRAWL_API_KEY": "${FIRECRAWL_API_KEY}"
}
},
"exa": {
"command": "npx",
"args": ["-y", "exa-mcp-server"],
"env": {
"EXA_API_KEY": "${EXA_API_KEY}"
}
},
"notion": {
"command": "npx",
"args": ["-y", "@notionhq/notion-mcp-server"],
"env": {
"NOTION_API_KEY": "${NOTION_API_KEY}"
}
},
"serena": {
"command": "uvx",
"args": ["--from", "serena-mcp", "serena"],
"env": {
"SERENA_CONFIG": "/workspace/.serena/config.json"
}
}
}
}
Step 2: Validate JSON
Run: jq . .claude/mcp.json > /dev/null && echo "Valid" || echo "Invalid"
Expected: Valid
Step 3: Commit
git add .claude/mcp.json
git commit -m "feat: add container-aware MCP configuration
All paths use /workspace and /home/vscode for container context:
- filesystem: scoped to /workspace
- kubernetes: uses container kubeconfig path
- serena: uses uvx for Python MCP server
- firecrawl, exa, notion: API keys from environment"
Task 9: Update devcontainer README¶
Files:
- Modify: .devcontainer/README.md
Step 1: Add Claude Code section
Add after the existing content:
## Claude Code
Claude Code CLI is installed via the official Anthropic devcontainer feature. The container includes:
- **MCP Servers**: filesystem, kubernetes, context7, firecrawl, exa, notion, serena
- **Project Plugins**: superpowers, commit-commands, pr-review-toolkit, feature-dev
### First-Time Setup
1. Authenticate to Vault and GitHub:
```bash
vault login -method=github
gh auth login -p https -w
```
2. Fetch Claude Code secrets:
```bash
bash .devcontainer/setup-claude-secrets.sh
```
3. Run Claude Code:
```bash
claude
```
### Secret Management
Secrets are fetched from Vault:
- **Shared MCP keys**: `secret/fzymgc-house/cluster/claude-code`
- **Per-user Anthropic key**: `secret/fzymgc-house/users/<github-username>/claude-code`
If not using Vault, set environment variables directly:
```bash
export ANTHROPIC_API_KEY="your-key"
export FIRECRAWL_API_KEY="your-key"
export EXA_API_KEY="your-key"
export NOTION_API_KEY="your-key"
**Step 2: Commit**
```bash
git add .devcontainer/README.md
git commit -m "docs(devcontainer): add Claude Code usage instructions"
Task 10: Final Verification and PR¶
Step 1: Run all linters
Run: pre-commit run --all-files
Expected: All checks pass (fix any failures)
Step 2: Verify container builds
Run: docker build -f .devcontainer/Dockerfile -t test-devcontainer . 2>&1 | tail -20
Expected: Build succeeds
Step 3: Create PR
git push -u origin feat/devcontainer-claude-code
gh pr create --title "feat(devcontainer): enable Claude Code with MCP servers" \
--body "$(cat <<'EOF'
## Summary
Enable Claude Code CLI to run entirely within the devcontainer with full MCP server support.
### Changes
- Add Claude Code via official Anthropic devcontainer feature
- Add uv and Node.js features for MCP server dependencies
- Docker volume for persistent `.claude` configuration
- Vault policy for shared MCP keys and per-user Anthropic keys
- Project-local plugin configuration in `.claude/`
- Setup script to fetch secrets from Vault
### Testing
- [ ] Container builds successfully
- [ ] Claude Code runs with `claude` command
- [ ] MCP servers connect (check with `/mcp`)
- [ ] Secrets fetch works with `setup-claude-secrets.sh`
Closes #360
🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
Summary¶
| Task | Component | Purpose |
|---|---|---|
| 1 | .gitignore | Ignore generated env file |
| 2 | Dockerfile | Add ripgrep, ast-grep |
| 3 | devcontainer.json | Add features, volume, initializeCommand |
| 4 | Vault policy | claude-code policy for secrets |
| 5 | setup-claude-secrets.sh | Standalone secret fetch |
| 6 | post-create.sh | Remove curl install, add safeguards |
| 7 | .claude/ | Project plugin config |
| 8 | .claude/mcp.json | Container-aware MCP config |
| 9 | README.md | Usage documentation |
| 10 | PR | Final verification and submission |