From e67eb23d5d87aa240dc68b521df9ac4625a9c6d3 Mon Sep 17 00:00:00 2001 From: AnExiledDev Date: Tue, 24 Feb 2026 09:03:17 +0000 Subject: [PATCH 1/6] Move .claude config from /workspaces to home directory with named volume - Change CLAUDE_CONFIG_DIR from /workspaces/.claude to ~/.claude - Add Docker named volume (per-instance via ${devcontainerId}) for persistence - Add CLAUDE_AUTH_TOKEN secret support with auto .credentials.json creation - Replace setup-symlink-claude.sh with setup-migrate-claude.sh (one-time migration) - Fix named volume root ownership via sudo chown on startup - Update scope guard allowlist to resolve $HOME dynamically - Update protected-files guard regex to cover .credentials.json - Update all feature heredocs, poststart hooks, and docs --- .devcontainer/.env.example | 5 ++- .devcontainer/.secrets.example | 3 ++ .devcontainer/CHANGELOG.md | 19 ++++++++++ .devcontainer/CLAUDE.md | 4 +-- .devcontainer/README.md | 21 +++++++++-- .devcontainer/devcontainer.json | 14 +++++++- .devcontainer/docs/configuration-reference.md | 7 ++-- .devcontainer/docs/keybindings.md | 2 +- .devcontainer/docs/troubleshooting.md | 7 +++- .devcontainer/features/ccstatusline/README.md | 4 +-- .../features/ccstatusline/install.sh | 2 +- .../claude-session-dashboard/README.md | 4 +-- .devcontainer/features/mcp-qdrant/CHANGES.md | 6 ++-- .devcontainer/features/mcp-qdrant/install.sh | 2 +- .../features/mcp-qdrant/poststart-hook.sh | 2 +- .../scripts/guard-protected-bash.py | 2 +- .../scripts/guard-protected.py | 2 +- .../plugins/workspace-scope-guard/README.md | 2 +- .../scripts/guard-workspace-scope.py | 3 +- .devcontainer/scripts/check-setup.sh | 4 +-- .devcontainer/scripts/setup-auth.sh | 29 +++++++++++++++ .devcontainer/scripts/setup-migrate-claude.sh | 20 +++++++++++ .devcontainer/scripts/setup-symlink-claude.sh | 36 ------------------- .devcontainer/scripts/setup.sh | 8 +++-- .../docs/customization/configuration.md | 4 +-- docs/src/content/docs/customization/rules.md | 2 +- .../docs/customization/system-prompts.md | 12 +++---- .../docs/plugins/workspace-scope-guard.md | 2 +- .../content/docs/reference/architecture.md | 2 +- docs/src/content/docs/reference/changelog.md | 22 ++++++++++++ .../src/content/docs/reference/environment.md | 6 ++-- 31 files changed, 177 insertions(+), 81 deletions(-) create mode 100755 .devcontainer/scripts/setup-migrate-claude.sh delete mode 100755 .devcontainer/scripts/setup-symlink-claude.sh diff --git a/.devcontainer/.env.example b/.devcontainer/.env.example index 7e51083..1533e10 100644 --- a/.devcontainer/.env.example +++ b/.devcontainer/.env.example @@ -1,9 +1,8 @@ # CodeForge Environment Configuration # Copy to .env and customize. .env is gitignored. -# Paths -CLAUDE_CONFIG_DIR=/workspaces/.claude -# CONFIG_SOURCE_DIR is derived from script location; uncomment to override: +# Paths (defaults shown — uncomment to override) +# CLAUDE_CONFIG_DIR=$HOME/.claude # CONFIG_SOURCE_DIR=/custom/path/to/config # Setup: copy config files to CLAUDE_CONFIG_DIR (per config/file-manifest.json) diff --git a/.devcontainer/.secrets.example b/.devcontainer/.secrets.example index eedf89e..122ddba 100644 --- a/.devcontainer/.secrets.example +++ b/.devcontainer/.secrets.example @@ -10,3 +10,6 @@ GH_EMAIL= # NPM auth token for registry.npmjs.org NPM_TOKEN= + +# Claude long-lived auth token (from 'claude setup-token') +CLAUDE_AUTH_TOKEN= diff --git a/.devcontainer/CHANGELOG.md b/.devcontainer/CHANGELOG.md index a6161e3..15506de 100644 --- a/.devcontainer/CHANGELOG.md +++ b/.devcontainer/CHANGELOG.md @@ -9,9 +9,26 @@ ### Changed +#### Configuration +- Moved `.claude` directory from `/workspaces/.claude` to `~/.claude` (home directory) +- Added Docker named volume for persistence across rebuilds (per-instance isolation via `${devcontainerId}`) +- `CLAUDE_CONFIG_DIR` now defaults to `/home/vscode/.claude` + +#### Authentication +- Added `CLAUDE_AUTH_TOKEN` support in `.secrets` for long-lived tokens from `claude setup-token` +- Auto-creates `.credentials.json` from token on container start (skips if already exists) +- Added `CLAUDE_AUTH_TOKEN` to devcontainer.json secrets declaration + +#### Security +- Protected-files-guard now covers `.credentials.json` (leading dot) + #### Status Bar - **ccstatusline line 1** — distinct background colors for each token widget (blue=input, magenta=output, yellow=cached, green=total), bold 2-char labels (In, Ou, Ca, Tt) fused to data widgets, `rawValue: true` on model widget to strip "Model:" prefix, restored spacing between token segments +#### Scripts +- Replaced `setup-symlink-claude.sh` with `setup-migrate-claude.sh` (one-time migration) +- Auto-migrates from `/workspaces/.claude/` if `.credentials.json` present + ### Fixed #### Plugin Marketplace @@ -29,6 +46,8 @@ ### Removed +- `setup-symlink-claude.sh` — no longer needed with native home directory location + #### VS Code Extensions - **Todo+** (`fabiospampinato.vscode-todo-plus`) — removed from devcontainer extensions diff --git a/.devcontainer/CLAUDE.md b/.devcontainer/CLAUDE.md index fd2a04a..071d8aa 100644 --- a/.devcontainer/CLAUDE.md +++ b/.devcontainer/CLAUDE.md @@ -31,7 +31,7 @@ CodeForge devcontainer for AI-assisted development with Claude Code. | `devcontainer.json` | Container definition: image, features, mounts | | `.env` | Boolean flags controlling setup steps | -Config files deploy via `file-manifest.json` on every container start. Most deploy to `/workspaces/.claude/`; ccstatusline config deploys to `~/.config/ccstatusline/`. Each entry supports `overwrite`: `"if-changed"` (default, sha256), `"always"`, or `"never"`. Supported variables: `${CLAUDE_CONFIG_DIR}`, `${WORKSPACE_ROOT}`, `${HOME}`. +Config files deploy via `file-manifest.json` on every container start. Most deploy to `~/.claude/`; ccstatusline config deploys to `~/.config/ccstatusline/`. Each entry supports `overwrite`: `"if-changed"` (default, sha256), `"always"`, or `"never"`. Supported variables: `${CLAUDE_CONFIG_DIR}`, `${WORKSPACE_ROOT}`, `${HOME}`. ## Commands @@ -76,7 +76,7 @@ Rules in `config/defaults/rules/` deploy to `.claude/rules/` on every container | Variable | Value | |----------|-------| -| `CLAUDE_CONFIG_DIR` | `/workspaces/.claude` | +| `CLAUDE_CONFIG_DIR` | `/home/vscode/.claude` | | `ANTHROPIC_MODEL` | `claude-opus-4-6` | | `WORKSPACE_ROOT` | `/workspaces` | | `TERM` | `${localEnv:TERM:xterm-256color}` (via `remoteEnv` — forwards host TERM, falls back to 256-color) | diff --git a/.devcontainer/README.md b/.devcontainer/README.md index ff2cb9b..c7c45d2 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -40,7 +40,20 @@ Get an API key from [console.anthropic.com](https://console.anthropic.com/). ### Credential Persistence -Authentication credentials are stored in `/workspaces/.claude/` and persist across container rebuilds. +Authentication credentials are stored in `~/.claude/` and persist across container rebuilds via a Docker named volume. + +### Long-Lived Token Authentication + +For headless or automated environments, you can use a long-lived auth token instead of browser login: + +1. Generate a token: `claude setup-token` +2. Add to `.devcontainer/.secrets`: + ```bash + CLAUDE_AUTH_TOKEN=sk-ant-oat01-your-token-here + ``` +3. On next container start, `setup-auth.sh` will create `~/.claude/.credentials.json` automatically. + +You can also set `CLAUDE_AUTH_TOKEN` as a Codespaces secret for cloud environments. For more options, see the [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code). @@ -111,7 +124,7 @@ Expected output shows your authenticated account and token scopes. ### Credential Persistence -GitHub CLI credentials are automatically persisted across container rebuilds. The container is configured to store credentials in `/workspaces/.gh/` (via `GH_CONFIG_DIR`), which is part of the bind-mounted workspace. +GitHub CLI credentials are automatically persisted across container rebuilds. The container is configured to store credentials in `/workspaces/.gh/` (via `GH_CONFIG_DIR`), which is part of the bind-mounted workspace. Claude Code credentials persist via a Docker named volume mounted at `~/.claude/`. **You only need to authenticate once.** After running `gh auth login` or configuring `.secrets`, your credentials will survive container rebuilds and be available in future sessions. @@ -199,7 +212,7 @@ Copy `.devcontainer/.env.example` to `.devcontainer/.env` and customize: | Variable | Default | Description | |----------|---------|-------------| -| `CLAUDE_CONFIG_DIR` | `/workspaces/.claude` | Claude configuration directory | +| `CLAUDE_CONFIG_DIR` | `/home/vscode/.claude` | Claude configuration directory | | `SETUP_CONFIG` | `true` | Copy config files during setup (per `file-manifest.json`) | | `SETUP_ALIASES` | `true` | Add `cc`/`claude`/`ccraw` aliases to shell | | `SETUP_AUTH` | `true` | Configure Git/NPM auth from `.secrets` | @@ -301,6 +314,8 @@ Three methods for providing GitHub/NPM credentials, in order of precedence: All methods persist across container rebuilds via the bind-mounted `/workspaces/.gh/` directory. +4. **`.secrets` file with `CLAUDE_AUTH_TOKEN`** — Long-lived Claude auth token from `claude setup-token`. Auto-creates `~/.claude/.credentials.json` on container start. + ## Agents & Skills Agents and skills are distributed across focused plugins (replacing the former `code-directive` monolith). diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 27226fd..ae37aff 100755 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,9 +5,17 @@ "workspaceFolder": "/workspaces", "workspaceMount": "source=${localWorkspaceFolder},target=/workspaces,type=bind", + "mounts": [ + { + "source": "codeforge-claude-config-${devcontainerId}", + "target": "/home/vscode/.claude", + "type": "volume" + } + ], + "remoteEnv": { "WORKSPACE_ROOT": "/workspaces", - "CLAUDE_CONFIG_DIR": "/workspaces/.claude", + "CLAUDE_CONFIG_DIR": "/home/vscode/.claude", "GH_CONFIG_DIR": "/workspaces/.gh", "TMPDIR": "/workspaces/.tmp", "TERM": "${localEnv:TERM:xterm-256color}", @@ -29,6 +37,10 @@ }, "GH_EMAIL": { "description": "GitHub email for git config (optional)" + }, + "CLAUDE_AUTH_TOKEN": { + "description": "Claude long-lived auth token from 'claude setup-token' (optional - sk-ant-oat01-*)", + "documentationUrl": "https://docs.anthropic.com/en/docs/claude-code/cli-reference#claude-setup-token" } }, diff --git a/.devcontainer/docs/configuration-reference.md b/.devcontainer/docs/configuration-reference.md index df8994f..e8e8b94 100644 --- a/.devcontainer/docs/configuration-reference.md +++ b/.devcontainer/docs/configuration-reference.md @@ -23,7 +23,7 @@ These control what `setup.sh` does on each container start. Copy `.env.example` | Variable | Default | Description | |----------|---------|-------------| -| `CLAUDE_CONFIG_DIR` | `/workspaces/.claude` | Where Claude Code config files are stored | +| `CLAUDE_CONFIG_DIR` | `/home/vscode/.claude` | Where Claude Code config files are stored | | `CONFIG_SOURCE_DIR` | `(auto-detected)` | Source directory for config defaults | | `SETUP_CONFIG` | `true` | Copy config files per `file-manifest.json` | | `SETUP_ALIASES` | `true` | Add cc/claude/ccraw/cc-tools aliases to shell | @@ -42,7 +42,7 @@ These environment variables are set in every terminal session inside the contain | Variable | Value | Description | |----------|-------|-------------| | `WORKSPACE_ROOT` | `/workspaces` | Workspace root directory | -| `CLAUDE_CONFIG_DIR` | `/workspaces/.claude` | Claude Code config directory | +| `CLAUDE_CONFIG_DIR` | `/home/vscode/.claude` | Claude Code config directory | | `GH_CONFIG_DIR` | `/workspaces/.gh` | GitHub CLI config directory | | `TMPDIR` | `/workspaces/.tmp` | Temporary files directory | | `CLAUDECODE` | `null` (unset) | Unsets the variable to allow nested Claude Code sessions (claude-in-claude) | @@ -88,6 +88,9 @@ GH_TOKEN=ghp_your_token_here GH_USERNAME=your-github-username GH_EMAIL=your-email@example.com NPM_TOKEN=npm_your_token_here +CLAUDE_AUTH_TOKEN=sk-ant-oat01-your-token-here ``` +The `CLAUDE_AUTH_TOKEN` is a long-lived token from `claude setup-token`. When set, `setup-auth.sh` creates `~/.claude/.credentials.json` on container start (skips if already exists). + Environment variables with the same names take precedence over `.secrets` file values (useful for Codespaces). diff --git a/.devcontainer/docs/keybindings.md b/.devcontainer/docs/keybindings.md index 8cc2346..33a3c68 100644 --- a/.devcontainer/docs/keybindings.md +++ b/.devcontainer/docs/keybindings.md @@ -78,7 +78,7 @@ Edit `config/defaults/keybindings.json` to remap Claude Code actions to non-conf } ``` -The keybindings file is copied to `/workspaces/.claude/keybindings.json` on container start (controlled by `file-manifest.json`). +The keybindings file is copied to `~/.claude/keybindings.json` on container start (controlled by `file-manifest.json`). ## Claude Code Keybinding Reference diff --git a/.devcontainer/docs/troubleshooting.md b/.devcontainer/docs/troubleshooting.md index fc45179..815b6c4 100644 --- a/.devcontainer/docs/troubleshooting.md +++ b/.devcontainer/docs/troubleshooting.md @@ -32,6 +32,11 @@ Common issues and solutions for the CodeForge devcontainer. - Or configure `.devcontainer/.secrets` with `GH_TOKEN` for automatic auth on container start. - Credentials persist in `/workspaces/.gh/` across rebuilds. +**Problem**: Claude auth token not taking effect in Codespaces. + +- When `CLAUDE_AUTH_TOKEN` is set via Codespaces secrets, it persists as an environment variable for the entire container lifetime. The `unset` in `setup-auth.sh` only clears it in the child process. This is a Codespaces platform limitation. +- If `.credentials.json` already exists, the token injection is skipped (idempotent). Delete `~/.claude/.credentials.json` to force re-creation from the token. + **Problem**: Git push fails with permission error. - Run `gh auth status` to verify authentication. @@ -119,7 +124,7 @@ Common issues and solutions for the CodeForge devcontainer. ## How to Reset to Defaults -1. **Reset config files**: Delete `/workspaces/.claude/` and restart the container. `setup-config.sh` will recopy all files from `config/defaults/`. +1. **Reset config files**: Delete `~/.claude/` and restart the container. `setup-config.sh` will recopy all files from `config/defaults/`. 2. **Reset aliases**: Delete the `# Claude Code environment and aliases` block from `~/.bashrc` and `~/.zshrc`, then run `bash /workspaces/.devcontainer/scripts/setup-aliases.sh`. diff --git a/.devcontainer/features/ccstatusline/README.md b/.devcontainer/features/ccstatusline/README.md index 0975809..da58b3c 100644 --- a/.devcontainer/features/ccstatusline/README.md +++ b/.devcontainer/features/ccstatusline/README.md @@ -105,7 +105,7 @@ You should see formatted output with powerline styling. **3. Check Claude Code integration:** ```bash -cat /workspaces/.claude/settings.json | jq '.statusLine' +cat ~/.claude/settings.json | jq '.statusLine' ``` Should show: @@ -204,7 +204,7 @@ cat ~/.config/ccstatusline/settings.json | jq . echo '{"model":{"display_name":"Test"}}' | npx -y ccstatusline@latest # 3. Check Claude Code settings -cat /workspaces/.claude/settings.json | jq '.statusLine' +cat ~/.claude/settings.json | jq '.statusLine' # 4. Manually run auto-config if needed configure-ccstatusline-auto diff --git a/.devcontainer/features/ccstatusline/install.sh b/.devcontainer/features/ccstatusline/install.sh index 0d16f01..5044270 100755 --- a/.devcontainer/features/ccstatusline/install.sh +++ b/.devcontainer/features/ccstatusline/install.sh @@ -190,7 +190,7 @@ if ! command -v jq &>/dev/null; then exit 1 fi -SETTINGS_FILE="${WORKSPACE_ROOT:-/workspaces}/.claude/settings.json" +SETTINGS_FILE="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" # Use SUDO_USER since _REMOTE_USER isn't set in post-start hooks USERNAME="${SUDO_USER:-vscode}" diff --git a/.devcontainer/features/claude-session-dashboard/README.md b/.devcontainer/features/claude-session-dashboard/README.md index 0a7a7a2..34512f4 100644 --- a/.devcontainer/features/claude-session-dashboard/README.md +++ b/.devcontainer/features/claude-session-dashboard/README.md @@ -33,8 +33,8 @@ claude-dashboard -p 8080 claude-dashboard --help ``` -The dashboard reads session data from `~/.claude/projects/` (symlinked to `/workspaces/.claude/projects/` in this devcontainer). +The dashboard reads session data from `~/.claude/projects/`. ## How persistence works -Dashboard settings and cache are stored at `~/.claude-dashboard/`. Since the home directory is ephemeral in devcontainers, a poststart hook symlinks `~/.claude-dashboard` → `/workspaces/.claude-dashboard/`, which is bind-mounted and survives rebuilds. +Dashboard settings and cache are stored at `~/.claude-dashboard/`. A poststart hook symlinks `~/.claude-dashboard` → `/workspaces/.claude-dashboard/`, which is bind-mounted and survives rebuilds. diff --git a/.devcontainer/features/mcp-qdrant/CHANGES.md b/.devcontainer/features/mcp-qdrant/CHANGES.md index 5230361..821e5ef 100644 --- a/.devcontainer/features/mcp-qdrant/CHANGES.md +++ b/.devcontainer/features/mcp-qdrant/CHANGES.md @@ -259,11 +259,11 @@ MCP_CONFIG_DIR="${USER_HOME}/.config/mcp" **Current Behavior:** - Feature creates: `~/.config/mcp/qdrant-config.json` -- Helper script (`configure-qdrant-mcp`) can update: `/workspaces/.claude/settings.json` +- Helper script (`configure-qdrant-mcp`) can update: `~/.claude/settings.json` - User must manually run helper script **Not Implemented (by request):** -- Automatic injection into `/workspaces/.claude/settings.json` during installation +- Automatic injection into `~/.claude/settings.json` during installation - This will be discussed separately --- @@ -378,7 +378,7 @@ Based on comprehensive review, the following fixes were applied: 2. ✅ Fixed credentials leak - Added cleanup trap, secure temp file handling ### High Priority Fixes -3. ✅ Removed unused config directory (~/.config/mcp) - Target is /workspaces/.claude/settings.json +3. ✅ Removed unused config directory (~/.config/mcp) - Target is ~/.claude/settings.json 4. ✅ Consolidated helper scripts - Removed duplicate manual helper, kept auto-config only 5. ✅ Fixed redundant redirections - Changed `&>/dev/null 2>&1` to `&>/dev/null` 6. ✅ Fixed hardcoded workspace paths - Now uses `${WORKSPACE_ROOT:-/workspaces}` diff --git a/.devcontainer/features/mcp-qdrant/install.sh b/.devcontainer/features/mcp-qdrant/install.sh index aadd44c..997f20e 100755 --- a/.devcontainer/features/mcp-qdrant/install.sh +++ b/.devcontainer/features/mcp-qdrant/install.sh @@ -189,7 +189,7 @@ else fi # Ensure settings.json exists -SETTINGS_FILE="/workspaces/.claude/settings.json" +SETTINGS_FILE="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" if [ ! -f "$SETTINGS_FILE" ]; then echo "[mcp-qdrant] ERROR: $SETTINGS_FILE not found" exit 1 diff --git a/.devcontainer/features/mcp-qdrant/poststart-hook.sh b/.devcontainer/features/mcp-qdrant/poststart-hook.sh index c42cf96..8649aea 100755 --- a/.devcontainer/features/mcp-qdrant/poststart-hook.sh +++ b/.devcontainer/features/mcp-qdrant/poststart-hook.sh @@ -57,7 +57,7 @@ else fi # Ensure settings.json exists -SETTINGS_FILE="/workspaces/.claude/settings.json" +SETTINGS_FILE="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" if [ ! -f "$SETTINGS_FILE" ]; then echo "[mcp-qdrant] ERROR: $SETTINGS_FILE not found" exit 1 diff --git a/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected-bash.py b/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected-bash.py index d630e2d..0c74255 100644 --- a/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected-bash.py +++ b/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected-bash.py @@ -36,7 +36,7 @@ (r"\.crt$", "Blocked: .crt certificate files should not be edited directly"), (r"\.p12$", "Blocked: .p12 files contain sensitive cryptographic material"), (r"\.pfx$", "Blocked: .pfx files contain sensitive cryptographic material"), - (r"(^|/)credentials\.json$", "Blocked: credentials.json contains secrets"), + (r"(^|/)\.?credentials\.json$", "Blocked: credentials.json contains secrets"), (r"(^|/)secrets\.yaml$", "Blocked: secrets.yaml contains secrets"), (r"(^|/)secrets\.yml$", "Blocked: secrets.yml contains secrets"), (r"(^|/)secrets\.json$", "Blocked: secrets.json contains secrets"), diff --git a/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py b/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py index eb521d0..6fb2ef3 100644 --- a/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py +++ b/.devcontainer/plugins/devs-marketplace/plugins/protected-files-guard/scripts/guard-protected.py @@ -40,7 +40,7 @@ (r"\.p12$", "Blocked: .p12 files contain sensitive cryptographic material"), (r"\.pfx$", "Blocked: .pfx files contain sensitive cryptographic material"), # Credential files - (r"(^|/)credentials\.json$", "Blocked: credentials.json contains secrets"), + (r"(^|/)\.?credentials\.json$", "Blocked: credentials.json contains secrets"), (r"(^|/)secrets\.yaml$", "Blocked: secrets.yaml contains secrets"), (r"(^|/)secrets\.yml$", "Blocked: secrets.yml contains secrets"), (r"(^|/)secrets\.json$", "Blocked: secrets.json contains secrets"), diff --git a/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/README.md b/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/README.md index 9013b1f..26bf983 100644 --- a/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/README.md +++ b/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/README.md @@ -27,7 +27,7 @@ These paths are always permitted regardless of working directory: | Path | Reason | |------|--------| -| `/workspaces/.claude/` | Claude config, plans, rules | +| `~/.claude/` | Claude config, plans, rules | | `/tmp/` | System temp directory | ### CWD Context Injection diff --git a/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/guard-workspace-scope.py b/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/guard-workspace-scope.py index 2c95080..a6bda4f 100755 --- a/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/guard-workspace-scope.py +++ b/.devcontainer/plugins/devs-marketplace/plugins/workspace-scope-guard/scripts/guard-workspace-scope.py @@ -28,8 +28,9 @@ ] # Paths always allowed regardless of working directory +_home = os.environ.get("HOME", "/home/vscode") ALLOWED_PREFIXES = [ - "/workspaces/.claude/", # Claude config, plans, rules + f"{_home}/.claude/", # Claude config, plans, rules "/tmp/", # System scratch ] diff --git a/.devcontainer/scripts/check-setup.sh b/.devcontainer/scripts/check-setup.sh index 34e51cc..f13e458 100644 --- a/.devcontainer/scripts/check-setup.sh +++ b/.devcontainer/scripts/check-setup.sh @@ -36,8 +36,8 @@ echo "Core:" check "Claude Code installed" "command -v claude" warn_check "Claude native binary" "[ -x ~/.local/bin/claude ] || [ -x /usr/local/bin/claude ]" check "cc alias configured" "grep -q 'alias cc=' ~/.bashrc 2>/dev/null || grep -q 'alias cc=' ~/.zshrc 2>/dev/null" -check "Config directory exists" "[ -d '${CLAUDE_CONFIG_DIR:-/workspaces/.claude}' ]" -check "Settings file exists" "[ -f '${CLAUDE_CONFIG_DIR:-/workspaces/.claude}/settings.json' ]" +check "Config directory exists" "[ -d '${CLAUDE_CONFIG_DIR:-$HOME/.claude}' ]" +check "Settings file exists" "[ -f '${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json' ]" echo "" echo "Authentication:" diff --git a/.devcontainer/scripts/setup-auth.sh b/.devcontainer/scripts/setup-auth.sh index 7df38d1..3c55156 100755 --- a/.devcontainer/scripts/setup-auth.sh +++ b/.devcontainer/scripts/setup-auth.sh @@ -65,6 +65,35 @@ else echo "[setup-auth] NPM_TOKEN not set, skipping NPM auth" fi +# --- Claude auth token (from 'claude setup-token') --- +# Long-lived tokens only — generated via: claude setup-token +CLAUDE_CRED_FILE="$HOME/.claude/.credentials.json" +if [ -n "$CLAUDE_AUTH_TOKEN" ]; then + if [ -f "$CLAUDE_CRED_FILE" ]; then + echo "[setup-auth] .credentials.json already exists, skipping token injection" + else + echo "[setup-auth] Creating .credentials.json from CLAUDE_AUTH_TOKEN..." + mkdir -p "$HOME/.claude" + # Write credentials with restrictive permissions from the start (no race window) + ( umask 077; cat > "$CLAUDE_CRED_FILE" </dev/null || true +echo "[setup-migrate] Migration complete. You can safely remove /workspaces/.claude/" diff --git a/.devcontainer/scripts/setup-symlink-claude.sh b/.devcontainer/scripts/setup-symlink-claude.sh deleted file mode 100755 index 29d2298..0000000 --- a/.devcontainer/scripts/setup-symlink-claude.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash -# Symlink $HOME/.claude → $CLAUDE_CONFIG_DIR so third-party tools -# (ccburn, ccusage, etc.) that hardcode ~/.claude can find auth and config. - -CLAUDE_DIR="${CLAUDE_CONFIG_DIR:=/workspaces/.claude}" -HOME_CLAUDE="$HOME/.claude" - -echo "[setup-symlink] Ensuring $HOME_CLAUDE → $CLAUDE_DIR ..." - -# Already a correct symlink — nothing to do -if [ -L "$HOME_CLAUDE" ]; then - CURRENT_TARGET="$(readlink "$HOME_CLAUDE")" - if [ "$CURRENT_TARGET" = "$CLAUDE_DIR" ]; then - echo "[setup-symlink] Symlink already correct, skipping" - exit 0 - fi - # Points somewhere else — remove stale symlink - echo "[setup-symlink] Removing stale symlink ($CURRENT_TARGET)" - rm "$HOME_CLAUDE" -fi - -# Real directory exists — merge contents into target, then remove -if [ -d "$HOME_CLAUDE" ]; then - echo "[setup-symlink] Moving existing $HOME_CLAUDE contents into $CLAUDE_DIR" - mkdir -p "$CLAUDE_DIR" - # Copy contents preserving attributes; skip files that already exist in target - cp -rn "$HOME_CLAUDE/." "$CLAUDE_DIR/" 2>/dev/null || true - rm -rf "$HOME_CLAUDE" -fi - -# Ensure target exists -mkdir -p "$CLAUDE_DIR" - -# Create symlink -ln -s "$CLAUDE_DIR" "$HOME_CLAUDE" -echo "[setup-symlink] Created symlink: $HOME_CLAUDE → $CLAUDE_DIR" diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index 8c541e9..091b426 100755 --- a/.devcontainer/scripts/setup.sh +++ b/.devcontainer/scripts/setup.sh @@ -13,7 +13,7 @@ if [ -f "$ENV_FILE" ]; then fi # Apply defaults for any unset variables -: "${CLAUDE_CONFIG_DIR:=/workspaces/.claude}" +: "${CLAUDE_CONFIG_DIR:=$HOME/.claude}" : "${CONFIG_SOURCE_DIR:=$DEVCONTAINER_DIR/config}" : "${SETUP_CONFIG:=true}" : "${SETUP_ALIASES:=true}" @@ -26,6 +26,10 @@ fi export CLAUDE_CONFIG_DIR CONFIG_SOURCE_DIR SETUP_CONFIG SETUP_ALIASES SETUP_AUTH SETUP_PLUGINS SETUP_UPDATE_CLAUDE SETUP_PROJECTS SETUP_TERMINAL SETUP_POSTSTART +# Fix named volume ownership — Docker creates named volumes as root:root +# regardless of remoteUser. This is the only setup script requiring sudo. +sudo chown "$(id -un):$(id -gn)" "$HOME/.claude" 2>/dev/null || true + SETUP_START=$(date +%s) SETUP_RESULTS=() @@ -88,7 +92,7 @@ run_poststart_hooks() { fi } -run_script "$SCRIPT_DIR/setup-symlink-claude.sh" "true" +run_script "$SCRIPT_DIR/setup-migrate-claude.sh" "true" run_script "$SCRIPT_DIR/setup-auth.sh" "$SETUP_AUTH" run_script "$SCRIPT_DIR/setup-config.sh" "$SETUP_CONFIG" run_script "$SCRIPT_DIR/setup-aliases.sh" "$SETUP_ALIASES" diff --git a/docs/src/content/docs/customization/configuration.md b/docs/src/content/docs/customization/configuration.md index ccf2616..97eea6c 100644 --- a/docs/src/content/docs/customization/configuration.md +++ b/docs/src/content/docs/customization/configuration.md @@ -116,7 +116,7 @@ The `statusLine` block configures the terminal status bar: ## file-manifest.json -The file manifest at `.devcontainer/config/file-manifest.json` controls which configuration files are deployed to `.claude/` and how they are updated. Each entry specifies a source file, a destination, and an overwrite strategy: +The file manifest at `.devcontainer/config/file-manifest.json` controls which configuration files are deployed to `~/.claude/` and how they are updated. Each entry specifies a source file, a destination, and an overwrite strategy: ```json [ @@ -155,7 +155,7 @@ If you customize a deployed file (like `settings.json` or the system prompt) and ### Adding a New Config File -To deploy a new file to `.claude/` automatically: +To deploy a new file to `~/.claude/` automatically: 1. Place the file in `.devcontainer/config/defaults/` 2. Add an entry to `file-manifest.json` diff --git a/docs/src/content/docs/customization/rules.md b/docs/src/content/docs/customization/rules.md index e9114ea..f92ee6f 100644 --- a/docs/src/content/docs/customization/rules.md +++ b/docs/src/content/docs/customization/rules.md @@ -11,7 +11,7 @@ Rules are Markdown files that define hard constraints applied to every Claude Co Rule files are Markdown documents placed in `.claude/rules/`. Claude Code loads every `.md` file in this directory at session start and treats their contents as mandatory instructions. The filename is descriptive but does not affect loading -- all files are loaded equally. -Rules are deployed from `.devcontainer/config/defaults/rules/` to `.claude/rules/` via the file manifest on every container start. You can also add rules directly to `.claude/rules/` in your project. +Rules are deployed from `.devcontainer/config/defaults/rules/` to `~/.claude/rules/` via the file manifest on every container start. You can also add rules directly to `.claude/rules/` in your project. ### Rule Precedence diff --git a/docs/src/content/docs/customization/system-prompts.md b/docs/src/content/docs/customization/system-prompts.md index 9a238be..cf2fdfc 100644 --- a/docs/src/content/docs/customization/system-prompts.md +++ b/docs/src/content/docs/customization/system-prompts.md @@ -12,7 +12,7 @@ System prompts define how Claude Code behaves during your sessions -- its coding The main system prompt is loaded for every `cc` or `claude` session. It is the single most influential file in shaping how Claude works with your code. **Location:** `.devcontainer/config/defaults/main-system-prompt.md` -**Deployed to:** `.claude/main-system-prompt.md` +**Deployed to:** `~/.claude/main-system-prompt.md` ### What It Controls @@ -53,7 +53,7 @@ Each section is self-contained. You can edit, remove, or add sections independen The writing system prompt is activated when you launch Claude with the `ccw` command. It replaces the development-focused prompt with one tuned for creative fiction writing. **Location:** `.devcontainer/config/defaults/writing-system-prompt.md` -**Deployed to:** `.claude/writing-system-prompt.md` +**Deployed to:** `~/.claude/writing-system-prompt.md` ### Key Differences from Main Prompt @@ -70,9 +70,9 @@ Use `cc` for coding sessions and `ccw` for writing sessions. Both are shell alia ### Editing the Main Prompt -To change development behavior, edit `.devcontainer/config/defaults/main-system-prompt.md`. Your changes are deployed to `.claude/` on the next container start via the file manifest. +To change development behavior, edit `.devcontainer/config/defaults/main-system-prompt.md`. Your changes are deployed to `~/.claude/` on the next container start via the file manifest. -For changes to take effect immediately (without restarting the container), edit the deployed copy at `.claude/main-system-prompt.md` directly. Be aware that this copy will be overwritten on the next container rebuild unless you change the overwrite mode in `file-manifest.json`. +For changes to take effect immediately (without restarting the container), edit the deployed copy at `~/.claude/main-system-prompt.md` directly. Be aware that this copy will be overwritten on the next container rebuild unless you change the overwrite mode in `file-manifest.json`. ### What to Customize @@ -165,7 +165,7 @@ Rules override the system prompt when they conflict. CLAUDE.md provides context ## Deployment and File Manifest -Both system prompts are listed in `file-manifest.json` and deployed to `.claude/` on every container start: +Both system prompts are listed in `file-manifest.json` and deployed to `~/.claude/` on every container start: ```json { @@ -179,7 +179,7 @@ Both system prompts are listed in `file-manifest.json` and deployed to `.claude/ The `if-changed` mode means your deployed copy is only overwritten when the source file's SHA-256 hash changes. If you want to make persistent local edits to the deployed prompt, change the overwrite mode to `"never"` so your changes survive container rebuilds. :::note[Two Copies] -The source file at `.devcontainer/config/defaults/main-system-prompt.md` is the canonical version. The deployed copy at `.claude/main-system-prompt.md` is what Claude Code actually loads. Edits to the source are deployed on next container start. Edits to the deployed copy take effect immediately but may be overwritten. +The source file at `.devcontainer/config/defaults/main-system-prompt.md` is the canonical version. The deployed copy at `~/.claude/main-system-prompt.md` is what Claude Code actually loads. Edits to the source are deployed on next container start. Edits to the deployed copy take effect immediately but may be overwritten. ::: ## Related diff --git a/docs/src/content/docs/plugins/workspace-scope-guard.md b/docs/src/content/docs/plugins/workspace-scope-guard.md index 5e8fe60..bdab9dc 100644 --- a/docs/src/content/docs/plugins/workspace-scope-guard.md +++ b/docs/src/content/docs/plugins/workspace-scope-guard.md @@ -74,7 +74,7 @@ A minimal set of paths are always allowed: | Allowed Path | Reason | |-------------|--------| -| `/workspaces/.claude/` | Claude config, plans, rules | +| `~/.claude/` | Claude config, plans, rules | | `/tmp/` | System temp directory | ## Bash Enforcement diff --git a/docs/src/content/docs/reference/architecture.md b/docs/src/content/docs/reference/architecture.md index 3d52cb7..06fea16 100644 --- a/docs/src/content/docs/reference/architecture.md +++ b/docs/src/content/docs/reference/architecture.md @@ -199,7 +199,7 @@ devcontainer.json | v [postStartCommand] setup.sh orchestrates: - 1. setup-symlink-claude.sh -- Symlink Claude config directory + 1. setup-migrate-claude.sh -- Migrate Claude config from old location 2. setup-auth.sh -- Git/NPM authentication 3. setup-config.sh -- Deploy settings, prompts, rules via file-manifest.json 4. setup-aliases.sh -- Write shell aliases to .bashrc/.zshrc diff --git a/docs/src/content/docs/reference/changelog.md b/docs/src/content/docs/reference/changelog.md index 4de958a..7a91bb3 100644 --- a/docs/src/content/docs/reference/changelog.md +++ b/docs/src/content/docs/reference/changelog.md @@ -49,11 +49,33 @@ For minor and patch updates, you can usually just rebuild the container. Check t ## Unreleased +### Changed + +#### Configuration +- Moved `.claude` directory from `/workspaces/.claude` to `~/.claude` (home directory) +- Added Docker named volume for persistence across rebuilds (per-instance isolation via `${devcontainerId}`) +- `CLAUDE_CONFIG_DIR` now defaults to `/home/vscode/.claude` + +#### Authentication +- Added `CLAUDE_AUTH_TOKEN` support in `.secrets` for long-lived tokens from `claude setup-token` +- Auto-creates `.credentials.json` from token on container start (skips if already exists) +- Added `CLAUDE_AUTH_TOKEN` to devcontainer.json secrets declaration + +#### Security +- Protected-files-guard now covers `.credentials.json` (leading dot) + +#### Scripts +- Replaced `setup-symlink-claude.sh` with `setup-migrate-claude.sh` (one-time migration) +- Auto-migrates from `/workspaces/.claude/` if `.credentials.json` present + ### Removed +- `setup-symlink-claude.sh` — no longer needed with native home directory location #### VS Code Extensions - **Todo+** (`fabiospampinato.vscode-todo-plus`) — removed from devcontainer extensions +--- + ## v1.14.2 **Release date:** 2026-02-24 diff --git a/docs/src/content/docs/reference/environment.md b/docs/src/content/docs/reference/environment.md index f6afe75..77525da 100644 --- a/docs/src/content/docs/reference/environment.md +++ b/docs/src/content/docs/reference/environment.md @@ -17,7 +17,7 @@ Variables that control Claude Code's core behavior inside the CodeForge containe | `ANTHROPIC_DEFAULT_OPUS_MODEL` | Opus model ID | `claude-opus-4-6` | settings.json | | `ANTHROPIC_DEFAULT_SONNET_MODEL` | Sonnet model ID | `claude-sonnet-4-5-20250929` | settings.json | | `ANTHROPIC_DEFAULT_HAIKU_MODEL` | Haiku model ID | `claude-haiku-4-5-20251001` | settings.json | -| `CLAUDE_CONFIG_DIR` | Claude Code configuration directory | `/workspaces/.claude` | devcontainer.json | +| `CLAUDE_CONFIG_DIR` | Claude Code configuration directory | `/home/vscode/.claude` | devcontainer.json | | `CLAUDE_CODE_MAX_OUTPUT_TOKENS` | Maximum tokens per response | `64000` | settings.json | | `MAX_THINKING_TOKENS` | Maximum tokens for extended thinking | `63999` | settings.json | | `CLAUDE_CODE_SHELL` | Shell used for Bash tool execution | `zsh` | settings.json | @@ -71,7 +71,7 @@ Variables set by the DevContainer environment that define workspace paths. | Variable | Description | Default | Set In | |----------|-------------|---------|--------| | `WORKSPACE_ROOT` | Workspace root directory | `/workspaces` | devcontainer.json | -| `CLAUDE_CONFIG_DIR` | Claude configuration directory | `/workspaces/.claude` | devcontainer.json | +| `CLAUDE_CONFIG_DIR` | Claude configuration directory | `/home/vscode/.claude` | devcontainer.json | | `GH_CONFIG_DIR` | GitHub CLI configuration directory | `/workspaces/.gh` | devcontainer.json | | `TMPDIR` | Temporary files directory | `/workspaces/.tmp` | devcontainer.json | | `CLAUDECODE` | Set to `null` to unset the detection flag, enabling nested Claude Code sessions | `null` | devcontainer.json | @@ -112,7 +112,7 @@ Applied when the container is created. Persists across all sessions. { "remoteEnv": { "WORKSPACE_ROOT": "/workspaces", - "CLAUDE_CONFIG_DIR": "/workspaces/.claude" + "CLAUDE_CONFIG_DIR": "/home/vscode/.claude" } } ``` From 2d39a30e63942298fcfa4370c0544acd612b6c89 Mon Sep 17 00:00:00 2001 From: AnExiledDev Date: Tue, 24 Feb 2026 21:47:32 +0000 Subject: [PATCH 2/6] Harden auth token handling, migration, and volume ownership MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security and robustness fixes from PR review: - setup-auth.sh: Replace unquoted heredoc with printf '%s' to prevent shell injection via CLAUDE_AUTH_TOKEN metacharacters (H1) - setup-auth.sh: Add sk-ant-* format validation on token (L1) - setup-auth.sh: Check/fix 600 permissions on existing .credentials.json (L2) - setup-auth.sh: Use ${CLAUDE_CONFIG_DIR:-$HOME/.claude} pattern consistently with other scripts (M3) - setup-auth.sh: Document /proc token visibility limitation (M2) - setup-migrate-claude.sh: Add idempotency check — skip silently when destination already has content (H2) - setup-migrate-claude.sh: Broaden trigger — migrate all content, not just when .credentials.json exists (L3) - setup-migrate-claude.sh: Add symlink protection on old directory (M1) - setup-migrate-claude.sh: Use --no-dereference with cp (M1) - setup-migrate-claude.sh: Use ${CLAUDE_CONFIG_DIR} pattern (L4) - setup.sh: Log warning on sudo chown failure instead of silent suppression (M4) --- .devcontainer/scripts/setup-auth.sh | 34 +++++++++++-------- .devcontainer/scripts/setup-migrate-claude.sh | 32 +++++++++++++---- .devcontainer/scripts/setup.sh | 4 ++- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/.devcontainer/scripts/setup-auth.sh b/.devcontainer/scripts/setup-auth.sh index 3c55156..8a741af 100755 --- a/.devcontainer/scripts/setup-auth.sh +++ b/.devcontainer/scripts/setup-auth.sh @@ -67,25 +67,29 @@ fi # --- Claude auth token (from 'claude setup-token') --- # Long-lived tokens only — generated via: claude setup-token -CLAUDE_CRED_FILE="$HOME/.claude/.credentials.json" +# Note: After unset, the token remains visible in /proc//environ for the +# lifetime of this process. This is a platform limitation of environment variables. +CLAUDE_CRED_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" +CLAUDE_CRED_FILE="$CLAUDE_CRED_DIR/.credentials.json" if [ -n "$CLAUDE_AUTH_TOKEN" ]; then - if [ -f "$CLAUDE_CRED_FILE" ]; then + # Validate token format (claude setup-token produces sk-ant-* tokens) + if [[ ! "$CLAUDE_AUTH_TOKEN" =~ ^sk-ant- ]]; then + echo "[setup-auth] WARNING: CLAUDE_AUTH_TOKEN doesn't match expected format (sk-ant-*), skipping" + elif [ -f "$CLAUDE_CRED_FILE" ]; then echo "[setup-auth] .credentials.json already exists, skipping token injection" + # Verify permissions haven't been tampered with + perms=$(stat -c %a "$CLAUDE_CRED_FILE" 2>/dev/null) + if [ -n "$perms" ] && [ "$perms" != "600" ]; then + echo "[setup-auth] WARNING: .credentials.json has permissions $perms (expected 600), fixing" + chmod 600 "$CLAUDE_CRED_FILE" + fi else echo "[setup-auth] Creating .credentials.json from CLAUDE_AUTH_TOKEN..." - mkdir -p "$HOME/.claude" - # Write credentials with restrictive permissions from the start (no race window) - ( umask 077; cat > "$CLAUDE_CRED_FILE" < "$CLAUDE_CRED_FILE" ) echo "[setup-auth] Claude auth token configured" AUTH_CONFIGURED=true fi diff --git a/.devcontainer/scripts/setup-migrate-claude.sh b/.devcontainer/scripts/setup-migrate-claude.sh index 1619d72..59924fb 100755 --- a/.devcontainer/scripts/setup-migrate-claude.sh +++ b/.devcontainer/scripts/setup-migrate-claude.sh @@ -1,20 +1,40 @@ #!/bin/bash # One-time migration: /workspaces/.claude → $HOME/.claude -# Only migrates if old location has .credentials.json (real auth data). +# Migrates config, credentials, and rules from the old bind-mount location +# to the new home directory (Docker named volume). +# +# Safety: uses cp -rn --no-dereference to avoid following symlinks and +# prevent overwriting files already in the destination. OLD_DIR="/workspaces/.claude" -NEW_DIR="$HOME/.claude" +NEW_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" +# Nothing to migrate if old directory doesn't exist if [ ! -d "$OLD_DIR" ]; then exit 0 fi -if [ ! -f "$OLD_DIR/.credentials.json" ]; then - echo "[setup-migrate] /workspaces/.claude exists but has no .credentials.json, skipping migration" +# Skip if old directory is empty (nothing worth migrating) +if [ -z "$(ls -A "$OLD_DIR" 2>/dev/null)" ]; then exit 0 fi -echo "[setup-migrate] Migrating /workspaces/.claude → $HOME/.claude ..." +# Idempotency: skip if destination already has content (migration already done) +if [ -d "$NEW_DIR" ] && [ -n "$(ls -A "$NEW_DIR" 2>/dev/null)" ]; then + exit 0 +fi + +# Symlink protection: verify OLD_DIR itself is a real directory, not a symlink +if [ -L "$OLD_DIR" ]; then + echo "[setup-migrate] WARNING: /workspaces/.claude is a symlink, skipping migration for safety" + exit 0 +fi + +echo "[setup-migrate] Migrating /workspaces/.claude → $NEW_DIR ..." mkdir -p "$NEW_DIR" -cp -rn "$OLD_DIR/." "$NEW_DIR/" 2>/dev/null || true + +# --no-dereference: copy symlinks as symlinks (don't follow them) +# -n: no-clobber (don't overwrite existing files) +# -r: recursive +cp -rn --no-dereference "$OLD_DIR/." "$NEW_DIR/" 2>/dev/null || true echo "[setup-migrate] Migration complete. You can safely remove /workspaces/.claude/" diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index 091b426..8b03936 100755 --- a/.devcontainer/scripts/setup.sh +++ b/.devcontainer/scripts/setup.sh @@ -28,7 +28,9 @@ export CLAUDE_CONFIG_DIR CONFIG_SOURCE_DIR SETUP_CONFIG SETUP_ALIASES SETUP_AUTH # Fix named volume ownership — Docker creates named volumes as root:root # regardless of remoteUser. This is the only setup script requiring sudo. -sudo chown "$(id -un):$(id -gn)" "$HOME/.claude" 2>/dev/null || true +if ! sudo chown "$(id -un):$(id -gn)" "$HOME/.claude" 2>/dev/null; then + echo "[setup] WARNING: Could not fix volume ownership on $HOME/.claude — subsequent scripts may fail" +fi SETUP_START=$(date +%s) SETUP_RESULTS=() From ed60a168bb661485a0449ddfd718eedaa40ff727 Mon Sep 17 00:00:00 2001 From: AnExiledDev Date: Tue, 24 Feb 2026 22:02:38 +0000 Subject: [PATCH 3/6] Address CodeRabbit review findings (1, 2, 6, 7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CHANGELOG.md: Add #### Documentation subsection under Changed, add #### Scripts subsection under Removed for consistent structure - CLAUDE.md: Document CLAUDE_AUTH_TOKEN, .credentials.json auto-creation, skip-if-exists behavior, sk-ant-* validation, and named volume persistence - setup-auth.sh: Detect printf subshell write failure — report warning instead of false success when .credentials.json write fails - setup-migrate-claude.sh: Verify cp exit status before printing success — warn if copy failed instead of unconditional "Migration complete" - docs/reference/changelog.md: Mirror CHANGELOG structure fixes Findings 3-5 (feature $HOME fallback) confirmed as false positives: postStartCommand runs as vscode user, CLAUDE_CONFIG_DIR is exported by setup.sh before hooks execute. --- .devcontainer/CHANGELOG.md | 5 +++++ .devcontainer/CLAUDE.md | 7 +++++++ .devcontainer/scripts/setup-auth.sh | 9 ++++++--- .devcontainer/scripts/setup-migrate-claude.sh | 7 +++++-- docs/src/content/docs/reference/changelog.md | 6 ++++++ 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/.devcontainer/CHANGELOG.md b/.devcontainer/CHANGELOG.md index 15506de..871e2ec 100644 --- a/.devcontainer/CHANGELOG.md +++ b/.devcontainer/CHANGELOG.md @@ -29,6 +29,10 @@ - Replaced `setup-symlink-claude.sh` with `setup-migrate-claude.sh` (one-time migration) - Auto-migrates from `/workspaces/.claude/` if `.credentials.json` present +#### Documentation +- All docs now reference `~/.claude` as default config path +- Added `CLAUDE_AUTH_TOKEN` setup flow to README, configuration reference, and troubleshooting + ### Fixed #### Plugin Marketplace @@ -46,6 +50,7 @@ ### Removed +#### Scripts - `setup-symlink-claude.sh` — no longer needed with native home directory location #### VS Code Extensions diff --git a/.devcontainer/CLAUDE.md b/.devcontainer/CLAUDE.md index 071d8aa..59a4c52 100644 --- a/.devcontainer/CLAUDE.md +++ b/.devcontainer/CLAUDE.md @@ -77,6 +77,7 @@ Rules in `config/defaults/rules/` deploy to `.claude/rules/` on every container | Variable | Value | |----------|-------| | `CLAUDE_CONFIG_DIR` | `/home/vscode/.claude` | +| `CLAUDE_AUTH_TOKEN` | Long-lived token from `claude setup-token` (optional, via `.secrets` or Codespaces secrets) | | `ANTHROPIC_MODEL` | `claude-opus-4-6` | | `WORKSPACE_ROOT` | `/workspaces` | | `TERM` | `${localEnv:TERM:xterm-256color}` (via `remoteEnv` — forwards host TERM, falls back to 256-color) | @@ -84,6 +85,12 @@ Rules in `config/defaults/rules/` deploy to `.claude/rules/` on every container All experimental feature flags are in `settings.json` under `env`. Setup steps controlled by boolean flags in `.env`. +## Authentication & Persistence + +The `~/.claude/` directory is backed by a Docker named volume (`codeforge-claude-config-${devcontainerId}`), persisting config, credentials, and session data across container rebuilds. Each devcontainer instance gets an isolated volume. + +**Token authentication:** Set `CLAUDE_AUTH_TOKEN` in `.devcontainer/.secrets` (or as a Codespaces secret) with a long-lived token from `claude setup-token`. On container start, `setup-auth.sh` auto-creates `~/.claude/.credentials.json` with `600` permissions. If `.credentials.json` already exists, token injection is skipped (idempotent). Tokens must match `sk-ant-*` format. + ## Modifying Behavior 1. **Change model**: Edit `config/defaults/settings.json` → `"model"` field diff --git a/.devcontainer/scripts/setup-auth.sh b/.devcontainer/scripts/setup-auth.sh index 8a741af..0fa9ec6 100755 --- a/.devcontainer/scripts/setup-auth.sh +++ b/.devcontainer/scripts/setup-auth.sh @@ -89,9 +89,12 @@ if [ -n "$CLAUDE_AUTH_TOKEN" ]; then # Write credentials with restrictive permissions from the start (no race window). # Uses printf '%s' to avoid shell expansion of token value (defense against # metacharacters in the token string — backticks, $(), quotes). - ( umask 077; printf '{\n "claudeAiOauth": {\n "accessToken": "%s",\n "refreshToken": "%s",\n "expiresAt": 9999999999999,\n "scopes": ["user:inference", "user:profile"]\n }\n}\n' "$CLAUDE_AUTH_TOKEN" "$CLAUDE_AUTH_TOKEN" > "$CLAUDE_CRED_FILE" ) - echo "[setup-auth] Claude auth token configured" - AUTH_CONFIGURED=true + if ( umask 077; printf '{\n "claudeAiOauth": {\n "accessToken": "%s",\n "refreshToken": "%s",\n "expiresAt": 9999999999999,\n "scopes": ["user:inference", "user:profile"]\n }\n}\n' "$CLAUDE_AUTH_TOKEN" "$CLAUDE_AUTH_TOKEN" > "$CLAUDE_CRED_FILE" ); then + echo "[setup-auth] Claude auth token configured" + AUTH_CONFIGURED=true + else + echo "[setup-auth] WARNING: Failed to write .credentials.json — check permissions on $CLAUDE_CRED_DIR" + fi fi unset CLAUDE_AUTH_TOKEN else diff --git a/.devcontainer/scripts/setup-migrate-claude.sh b/.devcontainer/scripts/setup-migrate-claude.sh index 59924fb..878f1f0 100755 --- a/.devcontainer/scripts/setup-migrate-claude.sh +++ b/.devcontainer/scripts/setup-migrate-claude.sh @@ -36,5 +36,8 @@ mkdir -p "$NEW_DIR" # --no-dereference: copy symlinks as symlinks (don't follow them) # -n: no-clobber (don't overwrite existing files) # -r: recursive -cp -rn --no-dereference "$OLD_DIR/." "$NEW_DIR/" 2>/dev/null || true -echo "[setup-migrate] Migration complete. You can safely remove /workspaces/.claude/" +if cp -rn --no-dereference "$OLD_DIR/." "$NEW_DIR/" 2>/dev/null; then + echo "[setup-migrate] Migration complete. You can safely remove /workspaces/.claude/" +else + echo "[setup-migrate] WARNING: Some files may not have been copied — verify $NEW_DIR before removing /workspaces/.claude/" +fi diff --git a/docs/src/content/docs/reference/changelog.md b/docs/src/content/docs/reference/changelog.md index 7a91bb3..07d1029 100644 --- a/docs/src/content/docs/reference/changelog.md +++ b/docs/src/content/docs/reference/changelog.md @@ -68,7 +68,13 @@ For minor and patch updates, you can usually just rebuild the container. Check t - Replaced `setup-symlink-claude.sh` with `setup-migrate-claude.sh` (one-time migration) - Auto-migrates from `/workspaces/.claude/` if `.credentials.json` present +#### Documentation +- All docs now reference `~/.claude` as default config path +- Added `CLAUDE_AUTH_TOKEN` setup flow to README, configuration reference, and troubleshooting + ### Removed + +#### Scripts - `setup-symlink-claude.sh` — no longer needed with native home directory location #### VS Code Extensions From 7219cc94e045dfe1a22015f1835c3c1eb0be687f Mon Sep 17 00:00:00 2001 From: AnExiledDev Date: Wed, 25 Feb 2026 03:57:46 +0000 Subject: [PATCH 4/6] Address remaining CodeRabbit review findings (3, 4, 5, 8, 9, 10) - Fix hardcoded /home/vscode/.claude in changelog, use portable ~/.claude - Remove implementation detail "(leading dot)" from changelog entry - Set AUTH_CONFIGURED=true when credentials already exist (fixes false "No tokens provided" summary) - Update docs site settings.json deployment path to ~/.claude - Harden $HOME fallback across all scripts: resolve target user's home via SUDO_USER/USER/vscode chain instead of relying on $HOME (guards against root context in feature installs and hooks) - Add CLAUDE_CONFIG_DIR documentation to ccstatusline and mcp-qdrant feature READMEs - Fix stale .claude/settings.json references in ccstatusline README --- .devcontainer/CHANGELOG.md | 4 ++-- .devcontainer/features/ccstatusline/README.md | 9 +++++---- .devcontainer/features/ccstatusline/install.sh | 3 ++- .devcontainer/features/mcp-qdrant/README.md | 1 + .devcontainer/features/mcp-qdrant/install.sh | 6 +++++- .devcontainer/features/mcp-qdrant/poststart-hook.sh | 6 +++++- .devcontainer/scripts/setup-auth.sh | 5 ++++- .devcontainer/scripts/setup-migrate-claude.sh | 4 +++- docs/src/content/docs/customization/configuration.md | 2 +- 9 files changed, 28 insertions(+), 12 deletions(-) diff --git a/.devcontainer/CHANGELOG.md b/.devcontainer/CHANGELOG.md index 871e2ec..2f11adc 100644 --- a/.devcontainer/CHANGELOG.md +++ b/.devcontainer/CHANGELOG.md @@ -12,7 +12,7 @@ #### Configuration - Moved `.claude` directory from `/workspaces/.claude` to `~/.claude` (home directory) - Added Docker named volume for persistence across rebuilds (per-instance isolation via `${devcontainerId}`) -- `CLAUDE_CONFIG_DIR` now defaults to `/home/vscode/.claude` +- `CLAUDE_CONFIG_DIR` now defaults to `~/.claude` #### Authentication - Added `CLAUDE_AUTH_TOKEN` support in `.secrets` for long-lived tokens from `claude setup-token` @@ -20,7 +20,7 @@ - Added `CLAUDE_AUTH_TOKEN` to devcontainer.json secrets declaration #### Security -- Protected-files-guard now covers `.credentials.json` (leading dot) +- Protected-files-guard now blocks modifications to `.credentials.json` #### Status Bar - **ccstatusline line 1** — distinct background colors for each token widget (blue=input, magenta=output, yellow=cached, green=total), bold 2-char labels (In, Ou, Ca, Tt) fused to data widgets, `rawValue: true` on model widget to strip "Model:" prefix, restored spacing between token segments diff --git a/.devcontainer/features/ccstatusline/README.md b/.devcontainer/features/ccstatusline/README.md index da58b3c..129037f 100644 --- a/.devcontainer/features/ccstatusline/README.md +++ b/.devcontainer/features/ccstatusline/README.md @@ -45,7 +45,7 @@ All widgets connected with powerline arrows (monokai theme). - **ccstatusline npm package**: Installed on-demand via `npx` (not globally) - **Configuration file**: `~/.config/ccstatusline/settings.json` with powerline theme -- **Claude Code integration**: Automatically updates `.claude/settings.json` +- **Claude Code integration**: Automatically updates `~/.claude/settings.json` - **Disk Usage**: Minimal (~2MB when cached by npx) ## Requirements @@ -75,9 +75,10 @@ The feature will validate these are present and exit with an error if missing. - ✅ **Session Resume**: Copyable `cc --resume {sessionId}` command via custom-command widget - ✅ **Burn Rate Tracking**: Live ccburn compact output showing pace indicators (🧊/🔥/🚨) - ✅ **ANSI Colors**: High-contrast colors optimized for dark terminals -- ✅ **Automatic Integration**: Auto-configures `.claude/settings.json` +- ✅ **Automatic Integration**: Auto-configures `~/.claude/settings.json` - ✅ **Idempotent**: Safe to run multiple times - ✅ **Multi-user**: Automatically detects container user +- ✅ **Config-aware**: Respects `CLAUDE_CONFIG_DIR` environment variable (defaults to `~/.claude`) ## Post-Installation Steps @@ -85,7 +86,7 @@ The feature will validate these are present and exit with an error if missing. This feature automatically: 1. Creates `~/.config/ccstatusline/settings.json` with powerline configuration -2. Configures `.claude/settings.json` to use ccstatusline +2. Configures `~/.claude/settings.json` to use ccstatusline **No manual steps required!** @@ -258,7 +259,7 @@ configure-ccstatusline-auto npm install -g ccstatusline@latest ``` -Then update `.claude/settings.json`: +Then update `~/.claude/settings.json`: ```json { "statusLine": { diff --git a/.devcontainer/features/ccstatusline/install.sh b/.devcontainer/features/ccstatusline/install.sh index 5044270..774b40a 100755 --- a/.devcontainer/features/ccstatusline/install.sh +++ b/.devcontainer/features/ccstatusline/install.sh @@ -190,9 +190,10 @@ if ! command -v jq &>/dev/null; then exit 1 fi -SETTINGS_FILE="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" # Use SUDO_USER since _REMOTE_USER isn't set in post-start hooks USERNAME="${SUDO_USER:-vscode}" +_USER_HOME=$(eval echo "~${USERNAME}") +SETTINGS_FILE="${CLAUDE_CONFIG_DIR:-${_USER_HOME}/.claude}/settings.json" # Ensure directory exists mkdir -p "$(dirname "${SETTINGS_FILE}")" diff --git a/.devcontainer/features/mcp-qdrant/README.md b/.devcontainer/features/mcp-qdrant/README.md index c3d2986..a56920b 100644 --- a/.devcontainer/features/mcp-qdrant/README.md +++ b/.devcontainer/features/mcp-qdrant/README.md @@ -154,6 +154,7 @@ The feature will validate these are present and exit with an error if missing. - ✅ **Cloud or Local**: Supports both Qdrant Cloud and local instances - ✅ **Idempotent**: Safe to run multiple times - ✅ **Multi-user**: Automatically detects container user +- ✅ **Config-aware**: Respects `CLAUDE_CONFIG_DIR` environment variable (defaults to `~/.claude`) - ✅ **Native mcpServers**: Uses VS Code's native devcontainer mcpServers support (declarative configuration) - ✅ **Dynamic Configuration**: Environment variables loaded from `/workspaces/.qdrant-mcp.env` file - ✅ **Secure**: API keys protected with 600 permissions on env file diff --git a/.devcontainer/features/mcp-qdrant/install.sh b/.devcontainer/features/mcp-qdrant/install.sh index 997f20e..c696348 100755 --- a/.devcontainer/features/mcp-qdrant/install.sh +++ b/.devcontainer/features/mcp-qdrant/install.sh @@ -188,8 +188,12 @@ else QDRANT_LOCAL_PATH="${QDRANT_LOCAL_PATH:-/workspaces/.qdrant/storage}" fi +# Resolve target user's home (guards against $HOME=/root during feature install) +_USERNAME="${SUDO_USER:-${USER:-vscode}}" +_USER_HOME=$(eval echo "~${_USERNAME}") + # Ensure settings.json exists -SETTINGS_FILE="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" +SETTINGS_FILE="${CLAUDE_CONFIG_DIR:-${_USER_HOME}/.claude}/settings.json" if [ ! -f "$SETTINGS_FILE" ]; then echo "[mcp-qdrant] ERROR: $SETTINGS_FILE not found" exit 1 diff --git a/.devcontainer/features/mcp-qdrant/poststart-hook.sh b/.devcontainer/features/mcp-qdrant/poststart-hook.sh index 8649aea..1190a75 100755 --- a/.devcontainer/features/mcp-qdrant/poststart-hook.sh +++ b/.devcontainer/features/mcp-qdrant/poststart-hook.sh @@ -56,8 +56,12 @@ else QDRANT_LOCAL_PATH="${QDRANT_LOCAL_PATH:-/workspaces/.qdrant/storage}" fi +# Resolve target user's home (guards against $HOME=/root when hook runs as root) +_USERNAME="${SUDO_USER:-${USER:-vscode}}" +_USER_HOME=$(eval echo "~${_USERNAME}") + # Ensure settings.json exists -SETTINGS_FILE="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" +SETTINGS_FILE="${CLAUDE_CONFIG_DIR:-${_USER_HOME}/.claude}/settings.json" if [ ! -f "$SETTINGS_FILE" ]; then echo "[mcp-qdrant] ERROR: $SETTINGS_FILE not found" exit 1 diff --git a/.devcontainer/scripts/setup-auth.sh b/.devcontainer/scripts/setup-auth.sh index 0fa9ec6..ef6db55 100755 --- a/.devcontainer/scripts/setup-auth.sh +++ b/.devcontainer/scripts/setup-auth.sh @@ -69,7 +69,9 @@ fi # Long-lived tokens only — generated via: claude setup-token # Note: After unset, the token remains visible in /proc//environ for the # lifetime of this process. This is a platform limitation of environment variables. -CLAUDE_CRED_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" +_USERNAME="${SUDO_USER:-${USER:-vscode}}" +_USER_HOME=$(eval echo "~${_USERNAME}") +CLAUDE_CRED_DIR="${CLAUDE_CONFIG_DIR:-${_USER_HOME}/.claude}" CLAUDE_CRED_FILE="$CLAUDE_CRED_DIR/.credentials.json" if [ -n "$CLAUDE_AUTH_TOKEN" ]; then # Validate token format (claude setup-token produces sk-ant-* tokens) @@ -83,6 +85,7 @@ if [ -n "$CLAUDE_AUTH_TOKEN" ]; then echo "[setup-auth] WARNING: .credentials.json has permissions $perms (expected 600), fixing" chmod 600 "$CLAUDE_CRED_FILE" fi + AUTH_CONFIGURED=true else echo "[setup-auth] Creating .credentials.json from CLAUDE_AUTH_TOKEN..." mkdir -p "$CLAUDE_CRED_DIR" diff --git a/.devcontainer/scripts/setup-migrate-claude.sh b/.devcontainer/scripts/setup-migrate-claude.sh index 878f1f0..80c97cf 100755 --- a/.devcontainer/scripts/setup-migrate-claude.sh +++ b/.devcontainer/scripts/setup-migrate-claude.sh @@ -7,7 +7,9 @@ # prevent overwriting files already in the destination. OLD_DIR="/workspaces/.claude" -NEW_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}" +_USERNAME="${SUDO_USER:-${USER:-vscode}}" +_USER_HOME=$(eval echo "~${_USERNAME}") +NEW_DIR="${CLAUDE_CONFIG_DIR:-${_USER_HOME}/.claude}" # Nothing to migrate if old directory doesn't exist if [ ! -d "$OLD_DIR" ]; then diff --git a/docs/src/content/docs/customization/configuration.md b/docs/src/content/docs/customization/configuration.md index 97eea6c..aca08c7 100644 --- a/docs/src/content/docs/customization/configuration.md +++ b/docs/src/content/docs/customization/configuration.md @@ -9,7 +9,7 @@ CodeForge configuration is spread across three files that each control a differe ## settings.json -The primary configuration file lives at `.devcontainer/config/defaults/settings.json`. It is deployed to `.claude/settings.json` on every container start and controls Claude Code's runtime behavior. +The primary configuration file lives at `.devcontainer/config/defaults/settings.json`. It is deployed to `~/.claude/settings.json` on every container start and controls Claude Code's runtime behavior. ### Core Settings From f43971a674e1c88a15bd1a73e3fef429b7a6be87 Mon Sep 17 00:00:00 2001 From: AnExiledDev Date: Wed, 25 Feb 2026 04:33:58 +0000 Subject: [PATCH 5/6] Harden shell scripts and fix stale docs from CodeRabbit review - Replace eval tilde expansion with getent passwd lookup in all PR-scoped scripts (setup-auth, setup-migrate, ccstatusline, mcp-qdrant install + poststart hook) to prevent shell injection via SUDO_USER/USER environment variables - JSON-escape auth token value before writing .credentials.json - Create credential directory with umask 077 (was default 755) - Fix mcp-qdrant chown to use resolved _USERNAME instead of hardcoded vscode or $(id -un) - Update ccstatusline README verification commands to respect CLAUDE_CONFIG_DIR environment variable - Sync docs site changelog with devcontainer CHANGELOG fixes (~/. claude path, remove "(leading dot)" aside) --- .devcontainer/CHANGELOG.md | 5 +++++ .devcontainer/features/ccstatusline/README.md | 6 +++--- .devcontainer/features/ccstatusline/install.sh | 3 ++- .devcontainer/features/mcp-qdrant/install.sh | 5 +++-- .devcontainer/features/mcp-qdrant/poststart-hook.sh | 5 +++-- .devcontainer/scripts/setup-auth.sh | 11 ++++++++--- .devcontainer/scripts/setup-migrate-claude.sh | 3 ++- docs/src/content/docs/reference/changelog.md | 9 +++++++-- 8 files changed, 33 insertions(+), 14 deletions(-) diff --git a/.devcontainer/CHANGELOG.md b/.devcontainer/CHANGELOG.md index 2f11adc..f68df43 100644 --- a/.devcontainer/CHANGELOG.md +++ b/.devcontainer/CHANGELOG.md @@ -21,6 +21,9 @@ #### Security - Protected-files-guard now blocks modifications to `.credentials.json` +- Replaced `eval` tilde expansion with `getent passwd` lookup across all scripts (prevents shell injection via `SUDO_USER`/`USER`) +- Auth token value is now JSON-escaped before writing to `.credentials.json` +- Credential directory created with restrictive umask (700) matching credential file permissions (600) #### Status Bar - **ccstatusline line 1** — distinct background colors for each token widget (blue=input, magenta=output, yellow=cached, green=total), bold 2-char labels (In, Ou, Ca, Tt) fused to data widgets, `rawValue: true` on model widget to strip "Model:" prefix, restored spacing between token segments @@ -28,10 +31,12 @@ #### Scripts - Replaced `setup-symlink-claude.sh` with `setup-migrate-claude.sh` (one-time migration) - Auto-migrates from `/workspaces/.claude/` if `.credentials.json` present +- `chown` in mcp-qdrant poststart hooks now uses resolved `_USERNAME` instead of hardcoded `vscode` or `$(id -un)` #### Documentation - All docs now reference `~/.claude` as default config path - Added `CLAUDE_AUTH_TOKEN` setup flow to README, configuration reference, and troubleshooting +- ccstatusline README verification commands now respect `CLAUDE_CONFIG_DIR` ### Fixed diff --git a/.devcontainer/features/ccstatusline/README.md b/.devcontainer/features/ccstatusline/README.md index 129037f..2722d0d 100644 --- a/.devcontainer/features/ccstatusline/README.md +++ b/.devcontainer/features/ccstatusline/README.md @@ -106,7 +106,7 @@ You should see formatted output with powerline styling. **3. Check Claude Code integration:** ```bash -cat ~/.claude/settings.json | jq '.statusLine' +cat "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" | jq '.statusLine' ``` Should show: @@ -205,7 +205,7 @@ cat ~/.config/ccstatusline/settings.json | jq . echo '{"model":{"display_name":"Test"}}' | npx -y ccstatusline@latest # 3. Check Claude Code settings -cat ~/.claude/settings.json | jq '.statusLine' +cat "${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json" | jq '.statusLine' # 4. Manually run auto-config if needed configure-ccstatusline-auto @@ -259,7 +259,7 @@ configure-ccstatusline-auto npm install -g ccstatusline@latest ``` -Then update `~/.claude/settings.json`: +Then update `${CLAUDE_CONFIG_DIR:-~/.claude}/settings.json`: ```json { "statusLine": { diff --git a/.devcontainer/features/ccstatusline/install.sh b/.devcontainer/features/ccstatusline/install.sh index 774b40a..f9cecb8 100755 --- a/.devcontainer/features/ccstatusline/install.sh +++ b/.devcontainer/features/ccstatusline/install.sh @@ -192,7 +192,8 @@ fi # Use SUDO_USER since _REMOTE_USER isn't set in post-start hooks USERNAME="${SUDO_USER:-vscode}" -_USER_HOME=$(eval echo "~${USERNAME}") +_USER_HOME=$(getent passwd "$USERNAME" 2>/dev/null | cut -d: -f6) +_USER_HOME="${_USER_HOME:-/home/$USERNAME}" SETTINGS_FILE="${CLAUDE_CONFIG_DIR:-${_USER_HOME}/.claude}/settings.json" # Ensure directory exists diff --git a/.devcontainer/features/mcp-qdrant/install.sh b/.devcontainer/features/mcp-qdrant/install.sh index c696348..06f03b7 100755 --- a/.devcontainer/features/mcp-qdrant/install.sh +++ b/.devcontainer/features/mcp-qdrant/install.sh @@ -190,7 +190,8 @@ fi # Resolve target user's home (guards against $HOME=/root during feature install) _USERNAME="${SUDO_USER:-${USER:-vscode}}" -_USER_HOME=$(eval echo "~${_USERNAME}") +_USER_HOME=$(getent passwd "$_USERNAME" 2>/dev/null | cut -d: -f6) +_USER_HOME="${_USER_HOME:-/home/$_USERNAME}" # Ensure settings.json exists SETTINGS_FILE="${CLAUDE_CONFIG_DIR:-${_USER_HOME}/.claude}/settings.json" @@ -261,7 +262,7 @@ fi # Set proper permissions chmod 644 "$SETTINGS_FILE" -chown "$(id -un):$(id -gn)" "$SETTINGS_FILE" 2>/dev/null || true +chown "${_USERNAME}:${_USERNAME}" "$SETTINGS_FILE" 2>/dev/null || true echo "[mcp-qdrant] ✓ Configuration complete" HOOK_EOF diff --git a/.devcontainer/features/mcp-qdrant/poststart-hook.sh b/.devcontainer/features/mcp-qdrant/poststart-hook.sh index 1190a75..e754b19 100755 --- a/.devcontainer/features/mcp-qdrant/poststart-hook.sh +++ b/.devcontainer/features/mcp-qdrant/poststart-hook.sh @@ -58,7 +58,8 @@ fi # Resolve target user's home (guards against $HOME=/root when hook runs as root) _USERNAME="${SUDO_USER:-${USER:-vscode}}" -_USER_HOME=$(eval echo "~${_USERNAME}") +_USER_HOME=$(getent passwd "$_USERNAME" 2>/dev/null | cut -d: -f6) +_USER_HOME="${_USER_HOME:-/home/$_USERNAME}" # Ensure settings.json exists SETTINGS_FILE="${CLAUDE_CONFIG_DIR:-${_USER_HOME}/.claude}/settings.json" @@ -129,6 +130,6 @@ fi # Set proper permissions chmod 644 "$SETTINGS_FILE" -chown vscode:vscode "$SETTINGS_FILE" 2>/dev/null || true +chown "${_USERNAME}:${_USERNAME}" "$SETTINGS_FILE" 2>/dev/null || true echo "[mcp-qdrant] ✓ Configuration complete" diff --git a/.devcontainer/scripts/setup-auth.sh b/.devcontainer/scripts/setup-auth.sh index ef6db55..e0dc36e 100755 --- a/.devcontainer/scripts/setup-auth.sh +++ b/.devcontainer/scripts/setup-auth.sh @@ -70,7 +70,8 @@ fi # Note: After unset, the token remains visible in /proc//environ for the # lifetime of this process. This is a platform limitation of environment variables. _USERNAME="${SUDO_USER:-${USER:-vscode}}" -_USER_HOME=$(eval echo "~${_USERNAME}") +_USER_HOME=$(getent passwd "$_USERNAME" 2>/dev/null | cut -d: -f6) +_USER_HOME="${_USER_HOME:-/home/$_USERNAME}" CLAUDE_CRED_DIR="${CLAUDE_CONFIG_DIR:-${_USER_HOME}/.claude}" CLAUDE_CRED_FILE="$CLAUDE_CRED_DIR/.credentials.json" if [ -n "$CLAUDE_AUTH_TOKEN" ]; then @@ -88,11 +89,15 @@ if [ -n "$CLAUDE_AUTH_TOKEN" ]; then AUTH_CONFIGURED=true else echo "[setup-auth] Creating .credentials.json from CLAUDE_AUTH_TOKEN..." - mkdir -p "$CLAUDE_CRED_DIR" + # Create directory with restrictive permissions (matches credential file at 600) + ( umask 077; mkdir -p "$CLAUDE_CRED_DIR" ) + # Escape JSON-special characters in token value (defense against malformed JSON + # if a token ever contains " or \ — unlikely with sk-ant-* but closes the gap) + ESCAPED_TOKEN=$(printf '%s' "$CLAUDE_AUTH_TOKEN" | sed 's/\\/\\\\/g; s/"/\\"/g') # Write credentials with restrictive permissions from the start (no race window). # Uses printf '%s' to avoid shell expansion of token value (defense against # metacharacters in the token string — backticks, $(), quotes). - if ( umask 077; printf '{\n "claudeAiOauth": {\n "accessToken": "%s",\n "refreshToken": "%s",\n "expiresAt": 9999999999999,\n "scopes": ["user:inference", "user:profile"]\n }\n}\n' "$CLAUDE_AUTH_TOKEN" "$CLAUDE_AUTH_TOKEN" > "$CLAUDE_CRED_FILE" ); then + if ( umask 077; printf '{\n "claudeAiOauth": {\n "accessToken": "%s",\n "refreshToken": "%s",\n "expiresAt": 9999999999999,\n "scopes": ["user:inference", "user:profile"]\n }\n}\n' "$ESCAPED_TOKEN" "$ESCAPED_TOKEN" > "$CLAUDE_CRED_FILE" ); then echo "[setup-auth] Claude auth token configured" AUTH_CONFIGURED=true else diff --git a/.devcontainer/scripts/setup-migrate-claude.sh b/.devcontainer/scripts/setup-migrate-claude.sh index 80c97cf..8f07ebb 100755 --- a/.devcontainer/scripts/setup-migrate-claude.sh +++ b/.devcontainer/scripts/setup-migrate-claude.sh @@ -8,7 +8,8 @@ OLD_DIR="/workspaces/.claude" _USERNAME="${SUDO_USER:-${USER:-vscode}}" -_USER_HOME=$(eval echo "~${_USERNAME}") +_USER_HOME=$(getent passwd "$_USERNAME" 2>/dev/null | cut -d: -f6) +_USER_HOME="${_USER_HOME:-/home/$_USERNAME}" NEW_DIR="${CLAUDE_CONFIG_DIR:-${_USER_HOME}/.claude}" # Nothing to migrate if old directory doesn't exist diff --git a/docs/src/content/docs/reference/changelog.md b/docs/src/content/docs/reference/changelog.md index 07d1029..6ef6048 100644 --- a/docs/src/content/docs/reference/changelog.md +++ b/docs/src/content/docs/reference/changelog.md @@ -54,7 +54,7 @@ For minor and patch updates, you can usually just rebuild the container. Check t #### Configuration - Moved `.claude` directory from `/workspaces/.claude` to `~/.claude` (home directory) - Added Docker named volume for persistence across rebuilds (per-instance isolation via `${devcontainerId}`) -- `CLAUDE_CONFIG_DIR` now defaults to `/home/vscode/.claude` +- `CLAUDE_CONFIG_DIR` now defaults to `~/.claude` #### Authentication - Added `CLAUDE_AUTH_TOKEN` support in `.secrets` for long-lived tokens from `claude setup-token` @@ -62,15 +62,20 @@ For minor and patch updates, you can usually just rebuild the container. Check t - Added `CLAUDE_AUTH_TOKEN` to devcontainer.json secrets declaration #### Security -- Protected-files-guard now covers `.credentials.json` (leading dot) +- Protected-files-guard now blocks modifications to `.credentials.json` +- Replaced `eval` tilde expansion with `getent passwd` lookup across all scripts (prevents shell injection via `SUDO_USER`/`USER`) +- Auth token value is now JSON-escaped before writing to `.credentials.json` +- Credential directory created with restrictive umask (700) matching credential file permissions (600) #### Scripts - Replaced `setup-symlink-claude.sh` with `setup-migrate-claude.sh` (one-time migration) - Auto-migrates from `/workspaces/.claude/` if `.credentials.json` present +- `chown` in mcp-qdrant poststart hooks now uses resolved `_USERNAME` instead of hardcoded `vscode` or `$(id -un)` #### Documentation - All docs now reference `~/.claude` as default config path - Added `CLAUDE_AUTH_TOKEN` setup flow to README, configuration reference, and troubleshooting +- ccstatusline README verification commands now respect `CLAUDE_CONFIG_DIR` ### Removed From 13f9354785280b56e9c2e41aa19e155ec0c5af59 Mon Sep 17 00:00:00 2001 From: AnExiledDev Date: Thu, 26 Feb 2026 02:44:13 +0000 Subject: [PATCH 6/6] fix(migration): harden migration script and add .env deprecation guard Migration script: - Switch from cp -rn to cp -a (archive mode) for faithful copy - Marker-based idempotency instead of checking destination contents - Verify critical files (.claude.json, plugins/, .credentials.json) - Fix ownership after copy (source may have different uid) - Rename old directory to .bak on success Setup.sh: - Detect stale CLAUDE_CONFIG_DIR=/workspaces/.claude in .env - Override to $HOME/.claude with warning - Auto-comment the stale line on disk --- .devcontainer/CHANGELOG.md | 2 + .devcontainer/scripts/setup-migrate-claude.sh | 47 +++++++++++++++---- .devcontainer/scripts/setup.sh | 14 ++++++ 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/.devcontainer/CHANGELOG.md b/.devcontainer/CHANGELOG.md index f68df43..8e779af 100644 --- a/.devcontainer/CHANGELOG.md +++ b/.devcontainer/CHANGELOG.md @@ -32,6 +32,8 @@ - Replaced `setup-symlink-claude.sh` with `setup-migrate-claude.sh` (one-time migration) - Auto-migrates from `/workspaces/.claude/` if `.credentials.json` present - `chown` in mcp-qdrant poststart hooks now uses resolved `_USERNAME` instead of hardcoded `vscode` or `$(id -un)` +- **Migration script hardened** — switched from `cp -rn` to `cp -a` (archive mode); added marker-based idempotency, critical file verification, ownership fixup, and old-directory rename +- **`.env` deprecation guard** — `setup.sh` detects stale `CLAUDE_CONFIG_DIR=/workspaces/.claude` in `.env`, overrides to `$HOME/.claude`, and auto-comments the line on disk #### Documentation - All docs now reference `~/.claude` as default config path diff --git a/.devcontainer/scripts/setup-migrate-claude.sh b/.devcontainer/scripts/setup-migrate-claude.sh index 8f07ebb..51acb3a 100755 --- a/.devcontainer/scripts/setup-migrate-claude.sh +++ b/.devcontainer/scripts/setup-migrate-claude.sh @@ -3,14 +3,16 @@ # Migrates config, credentials, and rules from the old bind-mount location # to the new home directory (Docker named volume). # -# Safety: uses cp -rn --no-dereference to avoid following symlinks and -# prevent overwriting files already in the destination. +# Uses cp -a (archive) for a faithful copy that preserves permissions, +# timestamps, symlinks, and directory structure. Migration is one-time +# (marker-gated), so overwrite is safe — the old directory is authoritative. OLD_DIR="/workspaces/.claude" _USERNAME="${SUDO_USER:-${USER:-vscode}}" _USER_HOME=$(getent passwd "$_USERNAME" 2>/dev/null | cut -d: -f6) _USER_HOME="${_USER_HOME:-/home/$_USERNAME}" NEW_DIR="${CLAUDE_CONFIG_DIR:-${_USER_HOME}/.claude}" +MARKER="$NEW_DIR/.migrated-from-workspaces" # Nothing to migrate if old directory doesn't exist if [ ! -d "$OLD_DIR" ]; then @@ -22,8 +24,8 @@ if [ -z "$(ls -A "$OLD_DIR" 2>/dev/null)" ]; then exit 0 fi -# Idempotency: skip if destination already has content (migration already done) -if [ -d "$NEW_DIR" ] && [ -n "$(ls -A "$NEW_DIR" 2>/dev/null)" ]; then +# Idempotency: skip if migration already completed +if [ -f "$MARKER" ]; then exit 0 fi @@ -36,11 +38,36 @@ fi echo "[setup-migrate] Migrating /workspaces/.claude → $NEW_DIR ..." mkdir -p "$NEW_DIR" -# --no-dereference: copy symlinks as symlinks (don't follow them) -# -n: no-clobber (don't overwrite existing files) -# -r: recursive -if cp -rn --no-dereference "$OLD_DIR/." "$NEW_DIR/" 2>/dev/null; then - echo "[setup-migrate] Migration complete. You can safely remove /workspaces/.claude/" +# -a: archive mode (-dR --preserve=all) — preserves permissions, timestamps, +# symlinks, ownership, and directory structure faithfully. +# Errors logged explicitly (no 2>/dev/null) so failures are visible. +if cp -a "$OLD_DIR/." "$NEW_DIR/"; then + # Fix ownership — source files may be owned by a different uid from a + # previous container lifecycle. chown everything to the current user. + chown -R "$(id -un):$(id -gn)" "$NEW_DIR/" 2>/dev/null || true + + # Verify critical files arrived + MISSING="" + [ ! -f "$NEW_DIR/.claude.json" ] && [ -f "$OLD_DIR/.claude.json" ] && MISSING="$MISSING .claude.json" + [ ! -d "$NEW_DIR/plugins" ] && [ -d "$OLD_DIR/plugins" ] && MISSING="$MISSING plugins/" + [ ! -f "$NEW_DIR/.credentials.json" ] && [ -f "$OLD_DIR/.credentials.json" ] && MISSING="$MISSING .credentials.json" + if [ -n "$MISSING" ]; then + echo "[setup-migrate] WARNING: Migration incomplete — missing:$MISSING" + echo "[setup-migrate] Old directory preserved at $OLD_DIR for manual recovery" + exit 1 + fi + + # Mark migration complete + date -Iseconds > "$MARKER" + + # Rename old directory to .bak + if mv "$OLD_DIR" "${OLD_DIR}.bak" 2>/dev/null; then + echo "[setup-migrate] Migration complete. Old directory moved to ${OLD_DIR}.bak" + else + echo "[setup-migrate] Migration complete. Could not rename old directory — remove /workspaces/.claude/ manually" + fi else - echo "[setup-migrate] WARNING: Some files may not have been copied — verify $NEW_DIR before removing /workspaces/.claude/" + echo "[setup-migrate] ERROR: cp failed — check output above for details" + echo "[setup-migrate] Old directory preserved at $OLD_DIR" + exit 1 fi diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index 8b03936..d865c67 100755 --- a/.devcontainer/scripts/setup.sh +++ b/.devcontainer/scripts/setup.sh @@ -12,6 +12,20 @@ if [ -f "$ENV_FILE" ]; then set +a fi +# Deprecation guard: .env may still set CLAUDE_CONFIG_DIR=/workspaces/.claude +# (pre-v2.0 default). Since .env is gitignored, PR updates can't fix it. +# Override with warning so all child scripts use the correct home location. +if [ "$CLAUDE_CONFIG_DIR" = "/workspaces/.claude" ]; then + echo "[setup] WARNING: CLAUDE_CONFIG_DIR=/workspaces/.claude is deprecated (moved to home dir in v2.0)" + echo "[setup] Updating .devcontainer/.env automatically." + CLAUDE_CONFIG_DIR="$HOME/.claude" + # Fix the file on disk so subsequent restarts don't trigger this guard + if [ -f "$ENV_FILE" ]; then + sed -i 's|^CLAUDE_CONFIG_DIR=.*/workspaces/\.claude.*|# CLAUDE_CONFIG_DIR removed (v2.0: now uses $HOME/.claude)|' "$ENV_FILE" + echo "[setup] .env updated — CLAUDE_CONFIG_DIR line commented out." + fi +fi + # Apply defaults for any unset variables : "${CLAUDE_CONFIG_DIR:=$HOME/.claude}" : "${CONFIG_SOURCE_DIR:=$DEVCONTAINER_DIR/config}"