Skip to content

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