Skip to content

Commit 69ca626

Browse files
committed
Harden auth token handling, migration, and volume ownership
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)
1 parent 2929208 commit 69ca626

File tree

3 files changed

+48
-22
lines changed

3 files changed

+48
-22
lines changed

.devcontainer/scripts/setup-auth.sh

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,25 +67,29 @@ fi
6767

6868
# --- Claude auth token (from 'claude setup-token') ---
6969
# Long-lived tokens only — generated via: claude setup-token
70-
CLAUDE_CRED_FILE="$HOME/.claude/.credentials.json"
70+
# Note: After unset, the token remains visible in /proc/<pid>/environ for the
71+
# lifetime of this process. This is a platform limitation of environment variables.
72+
CLAUDE_CRED_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
73+
CLAUDE_CRED_FILE="$CLAUDE_CRED_DIR/.credentials.json"
7174
if [ -n "$CLAUDE_AUTH_TOKEN" ]; then
72-
if [ -f "$CLAUDE_CRED_FILE" ]; then
75+
# Validate token format (claude setup-token produces sk-ant-* tokens)
76+
if [[ ! "$CLAUDE_AUTH_TOKEN" =~ ^sk-ant- ]]; then
77+
echo "[setup-auth] WARNING: CLAUDE_AUTH_TOKEN doesn't match expected format (sk-ant-*), skipping"
78+
elif [ -f "$CLAUDE_CRED_FILE" ]; then
7379
echo "[setup-auth] .credentials.json already exists, skipping token injection"
80+
# Verify permissions haven't been tampered with
81+
perms=$(stat -c %a "$CLAUDE_CRED_FILE" 2>/dev/null)
82+
if [ -n "$perms" ] && [ "$perms" != "600" ]; then
83+
echo "[setup-auth] WARNING: .credentials.json has permissions $perms (expected 600), fixing"
84+
chmod 600 "$CLAUDE_CRED_FILE"
85+
fi
7486
else
7587
echo "[setup-auth] Creating .credentials.json from CLAUDE_AUTH_TOKEN..."
76-
mkdir -p "$HOME/.claude"
77-
# Write credentials with restrictive permissions from the start (no race window)
78-
( umask 077; cat > "$CLAUDE_CRED_FILE" <<CRED_EOF
79-
{
80-
"claudeAiOauth": {
81-
"accessToken": "$CLAUDE_AUTH_TOKEN",
82-
"refreshToken": "$CLAUDE_AUTH_TOKEN",
83-
"expiresAt": 9999999999999,
84-
"scopes": ["user:inference", "user:profile"]
85-
}
86-
}
87-
CRED_EOF
88-
)
88+
mkdir -p "$CLAUDE_CRED_DIR"
89+
# Write credentials with restrictive permissions from the start (no race window).
90+
# Uses printf '%s' to avoid shell expansion of token value (defense against
91+
# metacharacters in the token string — backticks, $(), quotes).
92+
( 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" )
8993
echo "[setup-auth] Claude auth token configured"
9094
AUTH_CONFIGURED=true
9195
fi
Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,40 @@
11
#!/bin/bash
22
# One-time migration: /workspaces/.claude → $HOME/.claude
3-
# Only migrates if old location has .credentials.json (real auth data).
3+
# Migrates config, credentials, and rules from the old bind-mount location
4+
# to the new home directory (Docker named volume).
5+
#
6+
# Safety: uses cp -rn --no-dereference to avoid following symlinks and
7+
# prevent overwriting files already in the destination.
48

59
OLD_DIR="/workspaces/.claude"
6-
NEW_DIR="$HOME/.claude"
10+
NEW_DIR="${CLAUDE_CONFIG_DIR:-$HOME/.claude}"
711

12+
# Nothing to migrate if old directory doesn't exist
813
if [ ! -d "$OLD_DIR" ]; then
914
exit 0
1015
fi
1116

12-
if [ ! -f "$OLD_DIR/.credentials.json" ]; then
13-
echo "[setup-migrate] /workspaces/.claude exists but has no .credentials.json, skipping migration"
17+
# Skip if old directory is empty (nothing worth migrating)
18+
if [ -z "$(ls -A "$OLD_DIR" 2>/dev/null)" ]; then
1419
exit 0
1520
fi
1621

17-
echo "[setup-migrate] Migrating /workspaces/.claude → $HOME/.claude ..."
22+
# Idempotency: skip if destination already has content (migration already done)
23+
if [ -d "$NEW_DIR" ] && [ -n "$(ls -A "$NEW_DIR" 2>/dev/null)" ]; then
24+
exit 0
25+
fi
26+
27+
# Symlink protection: verify OLD_DIR itself is a real directory, not a symlink
28+
if [ -L "$OLD_DIR" ]; then
29+
echo "[setup-migrate] WARNING: /workspaces/.claude is a symlink, skipping migration for safety"
30+
exit 0
31+
fi
32+
33+
echo "[setup-migrate] Migrating /workspaces/.claude → $NEW_DIR ..."
1834
mkdir -p "$NEW_DIR"
19-
cp -rn "$OLD_DIR/." "$NEW_DIR/" 2>/dev/null || true
35+
36+
# --no-dereference: copy symlinks as symlinks (don't follow them)
37+
# -n: no-clobber (don't overwrite existing files)
38+
# -r: recursive
39+
cp -rn --no-dereference "$OLD_DIR/." "$NEW_DIR/" 2>/dev/null || true
2040
echo "[setup-migrate] Migration complete. You can safely remove /workspaces/.claude/"

.devcontainer/scripts/setup.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ export CLAUDE_CONFIG_DIR CONFIG_SOURCE_DIR SETUP_CONFIG SETUP_ALIASES SETUP_AUTH
2828

2929
# Fix named volume ownership — Docker creates named volumes as root:root
3030
# regardless of remoteUser. This is the only setup script requiring sudo.
31-
sudo chown "$(id -un):$(id -gn)" "$HOME/.claude" 2>/dev/null || true
31+
if ! sudo chown "$(id -un):$(id -gn)" "$HOME/.claude" 2>/dev/null; then
32+
echo "[setup] WARNING: Could not fix volume ownership on $HOME/.claude — subsequent scripts may fail"
33+
fi
3234

3335
SETUP_START=$(date +%s)
3436
SETUP_RESULTS=()

0 commit comments

Comments
 (0)