Skip to content

Commit 13f9354

Browse files
committed
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
1 parent f43971a commit 13f9354

3 files changed

Lines changed: 53 additions & 10 deletions

File tree

.devcontainer/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
- Replaced `setup-symlink-claude.sh` with `setup-migrate-claude.sh` (one-time migration)
3333
- Auto-migrates from `/workspaces/.claude/` if `.credentials.json` present
3434
- `chown` in mcp-qdrant poststart hooks now uses resolved `_USERNAME` instead of hardcoded `vscode` or `$(id -un)`
35+
- **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
36+
- **`.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
3537

3638
#### Documentation
3739
- All docs now reference `~/.claude` as default config path

.devcontainer/scripts/setup-migrate-claude.sh

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@
33
# Migrates config, credentials, and rules from the old bind-mount location
44
# to the new home directory (Docker named volume).
55
#
6-
# Safety: uses cp -rn --no-dereference to avoid following symlinks and
7-
# prevent overwriting files already in the destination.
6+
# Uses cp -a (archive) for a faithful copy that preserves permissions,
7+
# timestamps, symlinks, and directory structure. Migration is one-time
8+
# (marker-gated), so overwrite is safe — the old directory is authoritative.
89

910
OLD_DIR="/workspaces/.claude"
1011
_USERNAME="${SUDO_USER:-${USER:-vscode}}"
1112
_USER_HOME=$(getent passwd "$_USERNAME" 2>/dev/null | cut -d: -f6)
1213
_USER_HOME="${_USER_HOME:-/home/$_USERNAME}"
1314
NEW_DIR="${CLAUDE_CONFIG_DIR:-${_USER_HOME}/.claude}"
15+
MARKER="$NEW_DIR/.migrated-from-workspaces"
1416

1517
# Nothing to migrate if old directory doesn't exist
1618
if [ ! -d "$OLD_DIR" ]; then
@@ -22,8 +24,8 @@ if [ -z "$(ls -A "$OLD_DIR" 2>/dev/null)" ]; then
2224
exit 0
2325
fi
2426

25-
# Idempotency: skip if destination already has content (migration already done)
26-
if [ -d "$NEW_DIR" ] && [ -n "$(ls -A "$NEW_DIR" 2>/dev/null)" ]; then
27+
# Idempotency: skip if migration already completed
28+
if [ -f "$MARKER" ]; then
2729
exit 0
2830
fi
2931

@@ -36,11 +38,36 @@ fi
3638
echo "[setup-migrate] Migrating /workspaces/.claude → $NEW_DIR ..."
3739
mkdir -p "$NEW_DIR"
3840

39-
# --no-dereference: copy symlinks as symlinks (don't follow them)
40-
# -n: no-clobber (don't overwrite existing files)
41-
# -r: recursive
42-
if cp -rn --no-dereference "$OLD_DIR/." "$NEW_DIR/" 2>/dev/null; then
43-
echo "[setup-migrate] Migration complete. You can safely remove /workspaces/.claude/"
41+
# -a: archive mode (-dR --preserve=all) — preserves permissions, timestamps,
42+
# symlinks, ownership, and directory structure faithfully.
43+
# Errors logged explicitly (no 2>/dev/null) so failures are visible.
44+
if cp -a "$OLD_DIR/." "$NEW_DIR/"; then
45+
# Fix ownership — source files may be owned by a different uid from a
46+
# previous container lifecycle. chown everything to the current user.
47+
chown -R "$(id -un):$(id -gn)" "$NEW_DIR/" 2>/dev/null || true
48+
49+
# Verify critical files arrived
50+
MISSING=""
51+
[ ! -f "$NEW_DIR/.claude.json" ] && [ -f "$OLD_DIR/.claude.json" ] && MISSING="$MISSING .claude.json"
52+
[ ! -d "$NEW_DIR/plugins" ] && [ -d "$OLD_DIR/plugins" ] && MISSING="$MISSING plugins/"
53+
[ ! -f "$NEW_DIR/.credentials.json" ] && [ -f "$OLD_DIR/.credentials.json" ] && MISSING="$MISSING .credentials.json"
54+
if [ -n "$MISSING" ]; then
55+
echo "[setup-migrate] WARNING: Migration incomplete — missing:$MISSING"
56+
echo "[setup-migrate] Old directory preserved at $OLD_DIR for manual recovery"
57+
exit 1
58+
fi
59+
60+
# Mark migration complete
61+
date -Iseconds > "$MARKER"
62+
63+
# Rename old directory to .bak
64+
if mv "$OLD_DIR" "${OLD_DIR}.bak" 2>/dev/null; then
65+
echo "[setup-migrate] Migration complete. Old directory moved to ${OLD_DIR}.bak"
66+
else
67+
echo "[setup-migrate] Migration complete. Could not rename old directory — remove /workspaces/.claude/ manually"
68+
fi
4469
else
45-
echo "[setup-migrate] WARNING: Some files may not have been copied — verify $NEW_DIR before removing /workspaces/.claude/"
70+
echo "[setup-migrate] ERROR: cp failed — check output above for details"
71+
echo "[setup-migrate] Old directory preserved at $OLD_DIR"
72+
exit 1
4673
fi

.devcontainer/scripts/setup.sh

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ if [ -f "$ENV_FILE" ]; then
1212
set +a
1313
fi
1414

15+
# Deprecation guard: .env may still set CLAUDE_CONFIG_DIR=/workspaces/.claude
16+
# (pre-v2.0 default). Since .env is gitignored, PR updates can't fix it.
17+
# Override with warning so all child scripts use the correct home location.
18+
if [ "$CLAUDE_CONFIG_DIR" = "/workspaces/.claude" ]; then
19+
echo "[setup] WARNING: CLAUDE_CONFIG_DIR=/workspaces/.claude is deprecated (moved to home dir in v2.0)"
20+
echo "[setup] Updating .devcontainer/.env automatically."
21+
CLAUDE_CONFIG_DIR="$HOME/.claude"
22+
# Fix the file on disk so subsequent restarts don't trigger this guard
23+
if [ -f "$ENV_FILE" ]; then
24+
sed -i 's|^CLAUDE_CONFIG_DIR=.*/workspaces/\.claude.*|# CLAUDE_CONFIG_DIR removed (v2.0: now uses $HOME/.claude)|' "$ENV_FILE"
25+
echo "[setup] .env updated — CLAUDE_CONFIG_DIR line commented out."
26+
fi
27+
fi
28+
1529
# Apply defaults for any unset variables
1630
: "${CLAUDE_CONFIG_DIR:=$HOME/.claude}"
1731
: "${CONFIG_SOURCE_DIR:=$DEVCONTAINER_DIR/config}"

0 commit comments

Comments
 (0)