diff --git a/.devcontainer/claude-code/README.md b/.devcontainer/claude-code/README.md new file mode 100644 index 0000000..b82e08e --- /dev/null +++ b/.devcontainer/claude-code/README.md @@ -0,0 +1,480 @@ +# Claude Code CLI - Local Dev Container Feature + +A local Dev Container Feature that installs the Claude Code CLI and configures it with read-only mounts to your host machine's Claude configuration. + +## What This Feature Does + +This feature combines two capabilities: + +1. **CLI Installation**: Installs the `@anthropic-ai/claude-code` npm package globally +2. **Configuration Mounting**: Mounts your host machine's Claude configuration files into the container as read-only binds + +## What Gets Installed + +- **Claude Code CLI**: The `claude` command becomes available in your container +- **VS Code Extension**: Automatically installs the `anthropic.claude-code` extension +- **Configuration Directories**: Creates `.claude/` structure in the container + +## What Gets Mounted + +The following files and directories from your **host machine** are mounted into the container: + +### Read-Only Mounts (Security-Protected) +- `~/.claude/CLAUDE.md` → Global project instructions +- `~/.claude/settings.json` → Claude CLI settings +- `~/.claude/agents/` → Custom agent configurations +- `~/.claude/commands/` → Command definitions +- `~/.claude/hooks/` → Event-driven shell hooks + +These are **read-only** (`ro` flag) to prevent: +- Prompt injection attacks that could modify your Claude configuration +- Accidental modification of shared configuration from within containers +- Security issues related to hook manipulation + +### Read-Write Mounts (Authentication & State) +- `~/.claude/.credentials.json` → OAuth access/refresh tokens +- `~/.claude/.claude.json` → Account info, user ID, workspace setup tracking + +These files **must be writable** to enable: +- OAuth authentication flow and token refresh +- Workspace setup state tracking (`projectOnboardingSeenCount`) +- Session continuity across container rebuilds + +### Why These Must Be Writable + +**`.credentials.json`**: OAuth tokens need to be refreshed periodically. Claude writes updated tokens to this file. + +**`.claude.json`**: Claude tracks per-workspace setup state here. The `projectOnboardingSeenCount` field must be writable so Claude doesn't show the setup wizard on every launch. + +⚠️ **Security Note**: These files contain sensitive data and are mounted read-write by necessity. They are only accessible by the container user and stored with `600` permissions. Only use this feature with trusted repositories. + +## Usage + +### Basic Setup + +Add this feature to your `devcontainer.json`: + +```json +{ + "features": { + "./claude-code": {} + }, + "runArgs": ["--network=host"] +} +``` + +### Recommended Setup (with Node.js) + +For better compatibility, include the Node.js feature: + +```json +{ + "features": { + "ghcr.io/devcontainers/features/node:1": {}, + "./claude-code": {} + }, + "runArgs": ["--network=host"] +} +``` + +### Why `--network=host` is Required + +The `runArgs: ["--network=host"]` is **critical for OAuth authentication** to work in containers. + +**How OAuth works:** +1. You run `claude` → starts OAuth flow +2. Opens browser → you click "Authorize" +3. Browser redirects to `http://localhost:/callback` +4. OAuth server running in container receives the callback + +**The problem without host networking:** +- OAuth server runs on port X **inside container** +- Browser callback goes to port X on **host's localhost** +- ❌ Container's port is not accessible from host → **callback fails** + +**The solution:** +- With `--network=host`, container shares host's network namespace +- OAuth server on port X in container = port X on host +- ✅ Browser callback reaches the container → **authentication succeeds** + +**Security note:** Host networking gives the container full network access. Only use in trusted environments. + +**Alternative (if host networking is not acceptable):** +- Authenticate Claude on your host machine first +- Credentials in `~/.claude/.credentials.json` are automatically shared with container +- No OAuth flow needed in container + +### Build the Container + +With DevPod: +```bash +devpod up . --recreate +``` + +With VS Code: +- Open the folder in VS Code +- Run: "Dev Containers: Rebuild Container" + +## Requirements + +### Host Machine + +You should have these files/directories on your host machine (they will be created if they don't exist): + +```bash +~/.claude/ +├── CLAUDE.md # Optional: global instructions +├── settings.json # Optional: Claude settings +├── agents/ # Optional: custom agents +├── commands/ # Optional: custom commands +└── hooks/ # Optional: event hooks +``` + +**Note**: If these don't exist on your host, the container will still build successfully, but you may see mount warnings. You can create them with: + +```bash +mkdir -p ~/.claude/{agents,commands,hooks} +touch ~/.claude/CLAUDE.md +touch ~/.claude/settings.json +``` + +### Container + +- **Node.js 18+** and **npm** must be available (will be auto-installed if possible) +- Supported base images: Debian, Ubuntu, Alpine, Fedora, RHEL, CentOS + +## Assumptions + +1. **User**: The feature assumes the container user is `vscode` (standard for Dev Containers) + - Files are mounted to `/home/vscode/.claude/` + - If your container uses a different user, you may need to adjust the mount paths + +2. **HOME Environment Variable**: Must be set on the host machine (standard on Unix systems) + +3. **Persistence**: Your host machine's `~/.claude/` directory should persist across container rebuilds + +4. **Platform**: Designed for Linux/macOS hosts + - Windows WSL2 should work + - Windows native may require path adjustments + +## How to Iterate Locally + +### Quick Changes + +1. Edit files in `.devcontainer/claude-code/`: + - `devcontainer-feature.json` - Change mounts, extensions, or metadata + - `install.sh` - Modify installation logic + - `README.md` - Update documentation + +2. Rebuild the container: + ```bash + devpod up . --recreate + ``` + +### Testing Install Script + +You can test the install script standalone: + +```bash +cd .devcontainer/claude-code +sudo ./install.sh +``` + +### Debugging + +Check if Claude is installed: +```bash +claude --version +``` + +Check mounted files: +```bash +ls -la ~/.claude/ +``` + +Verify mounts are read-only: +```bash +echo "test" >> ~/.claude/CLAUDE.md # Should fail with "Read-only file system" +``` + +## Authentication + +### How It Works + +1. **Already Authenticated on Host**: If you have Claude Code set up on your host machine, credentials are automatically shared with the container +2. **First-Time Setup**: Run `claude` in the container and follow the OAuth flow: + - The CLI will provide an OAuth URL + - Open the URL in your browser (on your host machine) + - Click "Authorize" + - The callback should complete automatically, or you may need to paste the code + - Credentials are saved to `~/.claude/.credentials.json` on your host + +### OAuth Callback Behavior + +The OAuth flow opens a local callback server. In containers, this can behave differently: +- **VS Code Dev Containers**: Usually handles port forwarding automatically +- **DevPod**: May require manual code pasting if callback doesn't complete +- **SSH/Remote**: Callback URL opens in your local browser + +### Troubleshooting Authentication + +**"Paste code here" prompt hangs forever:** +- Check that `~/.claude/.credentials.json` exists on your host with proper permissions (`600`) +- Try authenticating on your host machine first, then rebuild the container +- If the callback fails, look for the authorization code in the URL after clicking "Authorize" + +**Credentials not persisting:** +- Ensure the `.credentials.json` file exists on your host before rebuilding +- Check file permissions: `chmod 600 ~/.claude/.credentials.json` + +**Setup wizard runs on every rebuild (theme selection, OAuth):** + +This happens because Claude tracks setup completion **per-workspace**, not globally. + +**Quick fix:** +```bash +# On your HOST machine: +# Set the onboarding flag for your workspace +jq '.projects["/workspaces/pythontemplate"].projectOnboardingSeenCount = 1' ~/.claude/.claude.json > ~/.claude/.claude.json.tmp +mv ~/.claude/.claude.json.tmp ~/.claude/.claude.json + +# Also ensure themeMode is set (if needed) +jq '. + {themeMode: "dark"}' ~/.claude/.claude.json > ~/.claude/.claude.json.tmp +mv ~/.claude/.claude.json.tmp ~/.claude/.claude.json + +# Rebuild container +devpod up . --recreate +``` + +**Root cause:** Claude tracks setup wizard completion per-workspace in `.claude.json` under `.projects["/workspaces/pythontemplate"].projectOnboardingSeenCount`. When this is `0`, the setup wizard runs. Set it to `1` to mark setup as complete. + +**For future workspaces:** Replace `/workspaces/pythontemplate` with your actual container workspace path. + +## Modifying Configuration + +Configuration files (except credentials) are read-only. You **cannot** modify Claude settings from within the container. + +To change configuration: + +1. Edit files on your **host machine**: `~/.claude/settings.json`, `~/.claude/CLAUDE.md`, etc. +2. Restart or rebuild the container to see changes + +This is by design for security (prevents prompt injection attacks). + +## What Would Change Before Publishing to GHCR + +If you wanted to publish this feature to GitHub Container Registry later: + +### 1. Repository Structure + +Move from `.devcontainer/claude-code/` to a dedicated repo: + +``` +anthropics/devcontainer-features/ +└── src/ + └── claude-code/ + ├── devcontainer-feature.json + ├── install.sh + └── README.md +``` + +### 2. Metadata Updates + +In `devcontainer-feature.json`: + +```json +{ + "id": "claude-code", + "version": "1.0.0", // Semantic versioning + "documentationURL": "https://github.com/anthropics/devcontainer-features/tree/main/src/claude-code", + // ... rest of config +} +``` + +### 3. Testing Infrastructure + +Add GitHub Actions workflow (`.github/workflows/test.yaml`): + +```yaml +- name: "Create test prerequisites" + run: | + mkdir -p ~/.claude/agents + mkdir -p ~/.claude/commands + mkdir -p ~/.claude/hooks + touch ~/.claude/settings.json + touch ~/.claude/CLAUDE.md +``` + +### 4. Publishing Workflow + +Add release workflow to build and push to `ghcr.io/anthropics/devcontainer-features/claude-code:1` + +### 5. Reference Change + +Users would then reference it as: + +```json +{ + "features": { + "ghcr.io/anthropics/devcontainer-features/claude-code:1": {} + } +} +``` + +Instead of `"./claude-code": {}` + +## Optional: Future Composition + +### Splitting into Modular Features + +This feature could be split into: + +1. **`claude-code-core`**: Just CLI installation, no mounts + ```json + { + "features": { + "./claude-code-core": {} + } + } + ``` + +2. **`claude-code-mounts`**: Just configuration mounts (requires `claude-code-core`) + ```json + { + "features": { + "./claude-code-core": {}, + "./claude-code-mounts": {} + } + } + ``` + +Benefits: +- Users can install CLI without mounts (useful for Codespaces or CI) +- More flexible composition +- Easier to maintain and test separately + +### Composition with Custom Features + +You could create a personal feature that extends this: + +```json +// .devcontainer/my-claude-setup/devcontainer-feature.json +{ + "id": "my-claude-setup", + "installsAfter": ["./claude-code"], + "customizations": { + "vscode": { + "settings": { + "claude.someCustomSetting": "value" + } + } + } +} +``` + +Then use both: + +```json +{ + "features": { + "./claude-code": {}, + "./my-claude-setup": {} + } +} +``` + +## Troubleshooting + +### "Node.js not found" error + +**Solution**: Add the Node.js feature explicitly: + +```json +{ + "features": { + "ghcr.io/devcontainers/features/node:1": {}, + "./claude-code": {} + } +} +``` + +### OAuth callback hangs at "Paste code here" + +**Problem**: Browser clicks "Authorize" but container never receives the callback. + +**Solution**: Add `--network=host` to your `devcontainer.json`: + +```json +{ + "runArgs": ["--network=host"] +} +``` + +See "Why `--network=host` is Required" section above for details. + +### Interactive `claude` asks for authentication but `claude --print` works + +**Problem**: You're authenticated (credentials mounted) but interactive mode prompts for login. + +**Root cause**: Without `--network=host`, OAuth callbacks can't reach the container. + +**Solution**: Add `"runArgs": ["--network=host"]` to devcontainer.json. + +### VS Code extensions don't install with `--network=host` + +**Known Issue**: [Using runArg network=host prevents extensions from installing](https://github.com/microsoft/vscode-remote-release/issues/9212) + +**Workarounds:** +1. **Rebuild without runArgs first**, let extensions install, then add runArgs (extensions persist) +2. **Authenticate on host**, mount credentials, remove runArgs (no OAuth needed in container) +3. **Manually install extensions** after container starts + +### Mount warnings about missing files + +**Solution**: Create the directories on your host: + +```bash +mkdir -p ~/.claude/{agents,commands,hooks} +touch ~/.claude/CLAUDE.md ~/.claude/settings.json +``` + +## Security Notes + +This implementation makes conscious security trade-offs to enable OAuth authentication and persistent setup state: + +### What's Protected (Read-Only Mounts) +- **CLAUDE.md**: Prevents prompt injection attacks that could modify your global instructions +- **settings.json**: Prevents config tampering +- **agents/**, **commands/**, **hooks/**: Prevents malicious code execution through modified hooks + +### What's Writable (Necessary Trade-off) +- **`.credentials.json`**: OAuth tokens must be writable for token refresh to work +- **`.claude.json`**: Workspace state must be writable to persist `projectOnboardingSeenCount` and other setup tracking + +### Security Mitigations +- Files have `600` permissions (user-only access) +- Only use this feature in **trusted repositories** +- Container user isolation provides some protection +- Writable files are limited to authentication/state only +- All configuration and code execution files remain read-only + +### Known Risks +- A malicious process in the container could exfiltrate OAuth tokens from `.credentials.json` +- A malicious process could modify workspace state in `.claude.json` +- **Recommendation**: Only use in repositories you trust, as you would with any dev container configuration + +See related security discussions: +- [anthropics/claude-code#4478](https://github.com/anthropics/claude-code/issues/4478) +- [anthropics/claude-code#2350](https://github.com/anthropics/claude-code/issues/2350) +- Original read-only approach: [PR #25](https://github.com/anthropics/devcontainer-features/pull/25) + +## Reference + +- **Dev Container Features Spec**: https://containers.dev/implementers/features/ +- **Local Features**: https://containers.dev/implementers/features/#local-features +- **Based on PR**: https://github.com/anthropics/devcontainer-features/pull/25 +- **Upstream Features**: https://github.com/anthropics/devcontainer-features + +## License + +Based on the Anthropic devcontainer-features repository (MIT License). diff --git a/.devcontainer/claude-code/TROUBLESHOOTING.md b/.devcontainer/claude-code/TROUBLESHOOTING.md new file mode 100644 index 0000000..a4b0f00 --- /dev/null +++ b/.devcontainer/claude-code/TROUBLESHOOTING.md @@ -0,0 +1,385 @@ +# Claude Code Dev Container Feature - Troubleshooting Guide + +## Quick Reference + +### Files That Must Exist on Host + +```bash +~/.claude/ +├── .credentials.json # OAuth tokens (must be writable) +├── .claude.json # Account info, setup state (must be writable) +├── CLAUDE.md # Global instructions (read-only) +├── settings.json # Settings (read-only) +├── agents/ # Custom agents (read-only) +├── commands/ # Custom commands (read-only) +└── hooks/ # Event hooks (read-only) +``` + +### Critical Configuration in devcontainer.json + +```json +{ + "features": { + "ghcr.io/devcontainers/features/node:1": {}, + "./claude-code": {} + }, + "runArgs": ["--network=host"], + "containerEnv": { + "CLAUDE_CONFIG_DIR": "/home/vscode/.claude", + "XDG_CONFIG_HOME": "/home/vscode/.config", + "XDG_CACHE_HOME": "/home/vscode/.cache", + "XDG_DATA_HOME": "/home/vscode/.local/share" + } +} +``` + +## Common Issues and Solutions + +### Issue 1: Setup Wizard Runs on Every Container Rebuild + +**Symptoms:** +- Interactive `claude` shows theme selection screen +- After selecting theme, asks for OAuth authentication +- Happens every time you rebuild the container + +**Root Cause:** +Claude tracks setup completion per-workspace in `.claude.json`: +```json +{ + "projects": { + "/workspaces/pythontemplate": { + "projectOnboardingSeenCount": 0 // ← This! + } + } +} +``` + +**Solution:** +```bash +# On HOST machine, set a high count to skip wizard +jq '.projects["/workspaces/pythontemplate"].projectOnboardingSeenCount = 999' \ + ~/.claude/.claude.json > ~/.claude/.claude.json.tmp +mv ~/.claude/.claude.json.tmp ~/.claude/.claude.json + +# Also ensure themeMode is set (global setting) +jq '. + {themeMode: "dark"}' ~/.claude/.claude.json > ~/.claude/.claude.json.tmp +mv ~/.claude/.claude.json.tmp ~/.claude/.claude.json + +# Rebuild container +devpod up . --recreate +``` + +**Why 999?** The field is `projectOnboardingSeenCount` - it increments each time you see the wizard. Setting it high tells Claude "this workspace has been onboarded many times, skip the wizard." + +**Verification:** +```bash +# In container +devpod ssh pythontemplate +claude # Should go straight to interactive mode without wizard +``` + +### Issue 2: OAuth Callback Hangs at "Paste code here" + +**Symptoms:** +- Browser opens, you click "Authorize" +- CLI shows "Paste code here >" and waits forever +- Browser callback URL fails to connect + +**Root Cause:** +OAuth callback server runs inside container on a random port (e.g., `localhost:35673`). Your browser tries to connect to that port on the HOST, but the container's port isn't accessible. + +**Solution:** +Add `--network=host` to devcontainer.json: + +```json +{ + "runArgs": ["--network=host"] +} +``` + +This makes the container share the host's network namespace, so ports inside the container are accessible from the host browser. + +**Trade-off:** +Using `--network=host` gives the container full network access and may prevent VS Code extensions from installing (known issue: [#9212](https://github.com/microsoft/vscode-remote-release/issues/9212)). + +**Workaround if you can't use --network=host:** +Authenticate on your host machine first, then credentials are shared via mounts. + +### Issue 3: `claude --print` Works But Interactive `claude` Asks for Login + +**Symptoms:** +- `echo "test" | claude --print` works without authentication +- Running just `claude` shows setup wizard or login prompt + +**Root Cause:** +Two different issues: +1. **Setup wizard** (theme/onboarding) - see Issue 1 +2. **Print mode skips workspace trust dialogs** - expected behavior + +**Solution:** +- For setup wizard: See Issue 1 +- For workspace trust: Use `--dangerously-skip-permissions` in trusted containers + +### Issue 4: Authentication Doesn't Persist After Container Rebuild + +**Symptoms:** +- You authenticate in the container +- Rebuild the container +- Have to authenticate again + +**Root Cause:** +`.credentials.json` or `.claude.json` is not mounted, or is mounted read-only. + +**Solution:** + +1. **Verify mounts in container:** + ```bash + devpod ssh pythontemplate + mount | grep claude + ``` + + Should show: + ``` + /dev/... on /home/vscode/.claude/.credentials.json type ext4 (rw,...) + /dev/... on /home/vscode/.claude/.claude.json type ext4 (rw,...) + ``` + +2. **Check files exist on host:** + ```bash + ls -la ~/.claude/.credentials.json ~/.claude/.claude.json + ``` + +3. **Verify files are writable (not ro):** + The mounts MUST be read-write for auth to persist. + +### Issue 5: "Read-only file system" Error + +**Symptoms:** +- Error when trying to write to `~/.claude/CLAUDE.md` or similar +- Operations fail with "Read-only file system" + +**Expected Behavior:** +This is intentional! Security files are mounted read-only: +- `CLAUDE.md`, `settings.json`, `agents/`, `commands/`, `hooks/` → Read-only + +**Why?** +Prevents prompt injection attacks that could modify your Claude configuration. + +**Solution:** +Edit these files on your HOST machine, then restart/rebuild the container. + +Only `.credentials.json` and `.claude.json` are read-write (needed for auth and state). + +### Issue 6: File Permission Errors (600 vs 664) + +**Symptoms:** +- Cannot read credentials file +- Permission denied errors + +**Solution:** +```bash +# On HOST +chmod 600 ~/.claude/.credentials.json +chmod 600 ~/.claude/.claude.json +``` + +These files contain sensitive data and should only be readable by you. + +## Debugging Commands + +### Check Authentication Status + +```bash +# In container +cat ~/.claude/.credentials.json | jq '.claudeAiOauth.accessToken' | head -c 30 +# Should show: sk-ant-oat01-... + +cat ~/.claude/.claude.json | jq '.oauthAccount.emailAddress' +# Should show your email +``` + +### Verify Mounts + +```bash +# In container +mount | grep claude +# Should show all mounted files/directories + +ls -la ~/.claude/ +# Should show files from your host +``` + +### Check Environment Variables + +```bash +# In container +env | grep -E "(CLAUDE|XDG)" | sort +``` + +Should show: +``` +CLAUDE_CONFIG_DIR=/home/vscode/.claude +XDG_CACHE_HOME=/home/vscode/.cache +XDG_CONFIG_HOME=/home/vscode/.config +XDG_DATA_HOME=/home/vscode/.local/share +``` + +### Test Claude Without Authentication + +```bash +# This should work if you're authenticated +echo "what is 2+2" | claude --print +``` + +### Check Setup State + +```bash +# On HOST +cat ~/.claude/.claude.json | jq '.projects["/workspaces/pythontemplate"]' +``` + +Look for: +- `projectOnboardingSeenCount`: Should be > 0 (e.g., 999) +- Check your actual workspace path matches + +### Verify Network Mode + +```bash +# On HOST +docker inspect | jq '.[0].HostConfig.NetworkMode' +# Should show: "host" +``` + +## Complete Setup Checklist + +When setting up a new workspace: + +- [ ] Node.js feature added to devcontainer.json +- [ ] `./claude-code` feature added +- [ ] `runArgs: ["--network=host"]` added +- [ ] Environment variables added (CLAUDE_CONFIG_DIR, XDG_*) +- [ ] Files exist on host: `.credentials.json`, `.claude.json` +- [ ] File permissions: `chmod 600` on sensitive files +- [ ] `projectOnboardingSeenCount` set to 999 in `.claude.json` +- [ ] `themeMode` set (e.g., "dark") in `.claude.json` +- [ ] Container rebuilt: `devpod up . --recreate` +- [ ] Test: `claude --print "test"` works +- [ ] Test: `claude` goes to interactive mode without wizard + +## File Explanation + +### `.credentials.json` +Contains OAuth access and refresh tokens. Format: +```json +{ + "claudeAiOauth": { + "accessToken": "sk-ant-oat01-...", + "refreshToken": "sk-ant-ort01-...", + "expiresAt": 1234567890000 + } +} +``` + +**Why writable:** Tokens need to be refreshed periodically. + +### `.claude.json` +Contains account info, feature flags, and per-workspace state. Key fields: +```json +{ + "oauthAccount": { ... }, + "userID": "...", + "themeMode": "dark", + "projects": { + "/workspaces/pythontemplate": { + "projectOnboardingSeenCount": 999, + "hasTrustDialogAccepted": false, + ... + } + } +} +``` + +**Why writable:** Claude updates `projectOnboardingSeenCount` and other workspace state. + +## Advanced Debugging + +### Capture Complete Claude Startup + +```bash +# In container +script -qec "timeout 3 claude 2>&1" /tmp/claude-startup.log +cat /tmp/claude-startup.log +``` + +### Compare Config Before/After + +```bash +# Before operation +cp ~/.claude/.claude.json ~/.claude/.claude.json.before + +# Do operation (e.g., run claude) + +# After +diff <(jq -S . ~/.claude/.claude.json.before) <(jq -S . ~/.claude/.claude.json) +``` + +### Check What Changed on Host + +```bash +# On HOST, monitor file changes +watch -n 1 'stat ~/.claude/.claude.json | grep Modify' +``` + +## Security Considerations + +### What's Protected (Read-Only) +- `CLAUDE.md` - Prevents prompt injection +- `settings.json` - Prevents config tampering +- `agents/`, `commands/`, `hooks/` - Prevents malicious modifications + +### What's Writable (Necessary Risk) +- `.credentials.json` - OAuth tokens (necessary for auth) +- `.claude.json` - Setup state (necessary to skip wizard) + +### Mitigation +- Only use in trusted repositories +- Files have `600` permissions (user-only access) +- Container user isolation +- Regular review of `.claude.json` changes + +## Known Limitations + +1. **VS Code extensions may not install with --network=host** + - Issue: https://github.com/microsoft/vscode-remote-release/issues/9212 + - Workaround: Build without runArgs first, then add it + +2. **Per-workspace setup tracking** + - Each workspace path needs its own `projectOnboardingSeenCount` + - Renaming workspace requires updating the flag + +3. **No credential isolation** + - All containers share same host credentials + - Can't use different Claude accounts per container + +4. **OAuth callback browser routing** + - Requires `--network=host` or manual code pasting + - May not work in some network environments + +## Getting Help + +If issues persist: + +1. Check `/tmp/claude/debug.log` in container +2. Run `claude --debug` for verbose output +3. Review this guide with an AI agent: + - Share: `.devcontainer/claude-code/TROUBLESHOOTING.md` + - Include: Output of debugging commands above + - Describe: Exact symptoms and when they occur + +## References + +- Dev Container Features: https://containers.dev/implementers/features/ +- Claude Code Docs: https://code.claude.com/docs/ +- deps_rocker reference: https://github.com/blooop/deps_rocker +- OAuth callback issue: https://github.com/anthropics/claude-code/issues/1529 +- Network=host issue: https://github.com/microsoft/vscode-remote-release/issues/9212 diff --git a/.devcontainer/claude-code/devcontainer-feature.json b/.devcontainer/claude-code/devcontainer-feature.json new file mode 100644 index 0000000..00fcc71 --- /dev/null +++ b/.devcontainer/claude-code/devcontainer-feature.json @@ -0,0 +1,31 @@ +{ + "name": "Claude Code CLI", + "id": "claude-code", + "version": "0.1.0", + "description": "Installs Claude Code CLI globally and mounts configuration directories", + "options": {}, + "documentationURL": "https://github.com/anthropics/devcontainer-features", + "licenseURL": "https://github.com/anthropics/devcontainer-features/blob/main/LICENSE", + "customizations": { + "vscode": { + "extensions": [ + "anthropic.claude-code" + ] + } + }, + "containerEnv": { + "CLAUDE_CONFIG_DIR": "/home/vscode/.claude" + }, + "installsAfter": [ + "ghcr.io/devcontainers/features/node" + ], + "mounts": [ + "source=${localEnv:HOME}/.claude/CLAUDE.md,target=/home/vscode/.claude/CLAUDE.md,type=bind,ro", + "source=${localEnv:HOME}/.claude/settings.json,target=/home/vscode/.claude/settings.json,type=bind,ro", + "source=${localEnv:HOME}/.claude/.credentials.json,target=/home/vscode/.claude/.credentials.json,type=bind", + "source=${localEnv:HOME}/.claude/.claude.json,target=/home/vscode/.claude/.claude.json,type=bind", + "source=${localEnv:HOME}/.claude/agents,target=/home/vscode/.claude/agents,type=bind,ro", + "source=${localEnv:HOME}/.claude/commands,target=/home/vscode/.claude/commands,type=bind,ro", + "source=${localEnv:HOME}/.claude/hooks,target=/home/vscode/.claude/hooks,type=bind,ro" + ] +} diff --git a/.devcontainer/claude-code/install.sh b/.devcontainer/claude-code/install.sh new file mode 100755 index 0000000..81a42fe --- /dev/null +++ b/.devcontainer/claude-code/install.sh @@ -0,0 +1,230 @@ +#!/bin/sh +set -eu + +# Claude Code CLI Local Feature Install Script +# Based on: https://github.com/anthropics/devcontainer-features/pull/25 +# Combines CLI installation with configuration directory setup + +# Function to detect the package manager and OS type +detect_package_manager() { + for pm in apt-get apk dnf yum; do + if command -v $pm >/dev/null; then + case $pm in + apt-get) echo "apt" ;; + *) echo "$pm" ;; + esac + return 0 + fi + done + echo "unknown" + return 1 +} + +# Function to install packages using the appropriate package manager +install_packages() { + local pkg_manager="$1" + shift + local packages="$@" + + case "$pkg_manager" in + apt) + apt-get update + apt-get install -y $packages + ;; + apk) + apk add --no-cache $packages + ;; + dnf|yum) + $pkg_manager install -y $packages + ;; + *) + echo "WARNING: Unsupported package manager. Cannot install packages: $packages" + return 1 + ;; + esac + + return 0 +} + +# Function to install Node.js +install_nodejs() { + local pkg_manager="$1" + + echo "Installing Node.js using $pkg_manager..." + + case "$pkg_manager" in + apt) + # Debian/Ubuntu - install more recent Node.js LTS + install_packages apt "ca-certificates curl gnupg" + mkdir -p /etc/apt/keyrings + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list + apt-get update + apt-get install -y nodejs + ;; + apk) + # Alpine + install_packages apk "nodejs npm" + ;; + dnf) + # Fedora/RHEL + install_packages dnf "nodejs npm" + ;; + yum) + # CentOS/RHEL + curl -sL https://rpm.nodesource.com/setup_18.x | bash - + yum install -y nodejs + ;; + *) + echo "ERROR: Unsupported package manager for Node.js installation" + return 1 + ;; + esac + + # Verify installation + if command -v node >/dev/null && command -v npm >/dev/null; then + echo "Successfully installed Node.js and npm" + return 0 + else + echo "Failed to install Node.js and npm" + return 1 + fi +} + +# Function to install Claude Code CLI +install_claude_code() { + echo "Installing Claude Code CLI globally..." + + # Install with npm + npm install -g @anthropic-ai/claude-code + + # Verify installation + if command -v claude >/dev/null; then + echo "Claude Code CLI installed successfully!" + claude --version + return 0 + else + echo "ERROR: Claude Code CLI installation failed!" + return 1 + fi +} + +# Function to create Claude configuration directories +# These directories will be mounted from the host, but we create them +# in the container to ensure they exist and have proper permissions +create_claude_directories() { + echo "Creating Claude configuration directories..." + + # Determine the target user's home directory + # $_REMOTE_USER is set by devcontainer, fallback to 'vscode' or current user + local target_home="${_REMOTE_USER_HOME:-/home/${_REMOTE_USER:-vscode}}" + local target_user="${_REMOTE_USER:-vscode}" + + echo "Target home directory: $target_home" + echo "Target user: $target_user" + + # Create the main .claude directory + mkdir -p "$target_home/.claude" + mkdir -p "$target_home/.claude/agents" + mkdir -p "$target_home/.claude/commands" + mkdir -p "$target_home/.claude/hooks" + + # Create empty config files if they don't exist + # This ensures the bind mounts won't fail if files are missing on host + if [ ! -f "$target_home/.claude/.credentials.json" ]; then + echo "{}" > "$target_home/.claude/.credentials.json" + chmod 600 "$target_home/.claude/.credentials.json" + fi + + if [ ! -f "$target_home/.claude/.claude.json" ]; then + echo "{}" > "$target_home/.claude/.claude.json" + chmod 600 "$target_home/.claude/.claude.json" + fi + + # Set proper ownership + # Note: These will be overridden by bind mounts from the host, + # but this ensures the directories exist with correct permissions + # if the mounts fail or for non-mounted directories + if [ "$(id -u)" -eq 0 ]; then + chown -R "$target_user:$target_user" "$target_home/.claude" || true + fi + + echo "Claude directories created successfully" +} + +# Print error message about requiring Node.js feature +print_nodejs_requirement() { + cat </dev/null || ! command -v npm >/dev/null; then + echo "Node.js or npm not found, attempting to install automatically..." + install_nodejs "$PKG_MANAGER" || print_nodejs_requirement + else + echo "Node.js and npm are already installed" + node --version + npm --version + fi + + # Install Claude Code CLI + # Check if already installed to make this idempotent + if command -v claude >/dev/null; then + echo "Claude Code CLI is already installed" + claude --version + else + install_claude_code || exit 1 + fi + + # Create Claude configuration directories + create_claude_directories + + echo "=========================================" + echo "Claude Code feature activated successfully!" + echo "=========================================" + echo "" + echo "Configuration files mounted from host:" + echo " Read-Write (auth & state):" + echo " - ~/.claude/.credentials.json (OAuth tokens)" + echo " - ~/.claude/.claude.json (account, setup tracking)" + echo "" + echo " Read-Only (security-protected):" + echo " - ~/.claude/CLAUDE.md" + echo " - ~/.claude/settings.json" + echo " - ~/.claude/agents/" + echo " - ~/.claude/commands/" + echo " - ~/.claude/hooks/" + echo "" + echo "Authentication:" + echo " - If you're already authenticated on your host, credentials are shared" + echo " - Otherwise, run 'claude' and follow the OAuth flow" + echo " - The OAuth callback may open in your host browser" + echo " - Credentials are stored on your host at ~/.claude/.credentials.json" + echo "" + echo "To modify config files, edit on your host machine and rebuild the container." + echo "" +} + +# Execute main function +main diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 53825d6..3a27238 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,11 +21,22 @@ } }, "features": { + "ghcr.io/devcontainers/features/node:1": {}, + "./claude-code": {} // "ghcr.io/devcontainers/features/docker-in-docker:2": {} // "ghcr.io/devcontainers/features/common-utils:2": {}, // "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {}, // "ghcr.io/jsburckhardt/devcontainer-features/codex:latest": {} }, + "runArgs": [ + "--network=host" + ], + "containerEnv": { + "CLAUDE_CONFIG_DIR": "/home/vscode/.claude", + "XDG_CONFIG_HOME": "/home/vscode/.config", + "XDG_CACHE_HOME": "/home/vscode/.cache", + "XDG_DATA_HOME": "/home/vscode/.local/share" + }, "mounts": [ "source=${localWorkspaceFolderBasename}-pixi,target=${containerWorkspaceFolder}/.pixi,type=volume" ], diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 61f349d..8252d16 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -139,19 +139,19 @@ "reveal": "always" }, }, - { - "label": "set git config recurse submodules", - "type": "shell", - "linux": { - "command": [" git config submodule.recurse true"], - }, - "runOptions": { - "runOn": "folderOpen" - }, - "presentation": { - "reveal": "silent" - }, - }, + // { + // "label": "set git config recurse submodules", + // "type": "shell", + // "linux": { + // "command": [" git config submodule.recurse true"], + // }, + // "runOptions": { + // "runOn": "folderOpen" + // }, + // "presentation": { + // "reveal": "silent" + // }, + // }, ], "inputs": [ {