From 0d805f9a1ad60fee959623e16dd50d4d15c986c8 Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 15 May 2026 21:14:06 -0600 Subject: [PATCH 1/2] fix(setup): idempotent claude plugin install to stop .claude.json truncation Root cause: every `claude plugin install` call writes to ~/.claude/.claude.json (or %USERPROFILE%\.claude\.claude.json). The Claude Code CLI's deserialize-modify-serialize cycle does NOT preserve fields outside its internal struct. Re-running install for already-installed plugins silently strips: - oauthAccount.organizationType (e.g. "claude_max") - oauthAccount.organizationRateLimitTier - oauthAccount.hasExtraUsageEnabled - oauthAccount.organizationRole / organizationName - projects {} map (per-project session state) - autoUpdates, installMethod, claudeCodeFirstTokenDate, hasCompletedOnboarding, and ~50 other onboarding/UI fields Symptom observed 2026-05-15: after running setup-linux.sh, .claude.json shrank from 75475 bytes (60 keys) to 1467 bytes (8 keys). Claude Code lost the subscription marker and prompted for re-authentication in EVERY project. Backups under ~/.claude/backups/ confirmed the pre-truncation state was intact and recoverable via cp. Fix: cache `claude plugin list` output once before the loop; skip `plugin install` calls for plugins already present. Mirrors the idempotence pattern already used for MCP registration (`claude mcp get` check at line 447 / setup-windows.ps1 line 196). Bash: shell glob match + grep -qF against cached list. PowerShell: -match with [regex]::Escape against the joined output. The CLI bug itself is upstream Anthropic. Eliminating the trigger (unconditional re-install) eliminates the symptom in our environment. If Anthropic fixes the CLI later, our wrapper stays correct. Verification: - After this fix: running setup-linux.sh twice in a row leaves ~/.claude/.claude.json at the same size (no truncation). - Subscription state (organizationType: claude_max) persists across setup runs. Unrelated to PR #32 (skill-sync symlink-follow bug). Both PRs address different setup-linux data-loss bugs; they touch different sections of the same file and do not conflict. --- setup-linux.sh | 25 ++++++++++++++++++++++--- setup-windows.ps1 | 20 ++++++++++++++++++-- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/setup-linux.sh b/setup-linux.sh index 54ff8a0..8714416 100755 --- a/setup-linux.sh +++ b/setup-linux.sh @@ -457,9 +457,22 @@ else log_warning "Claude Code CLI, npx, or jq not found, skipping MCP server registration" fi -# Claude Code plugins (requires claude CLI) +# Claude Code plugins (requires claude CLI). +# Idempotent: cache the installed-plugins list ONCE before the loop and skip +# entries already present. CRITICAL: every `claude plugin install` call +# writes to ~/.claude/.claude.json. The CLI's deserialize-modify-serialize +# cycle does NOT preserve fields outside its internal struct — subscription +# metadata (`organizationType: claude_max`, `organizationRateLimitTier`), +# the `projects` map, and onboarding flags get silently dropped. Re-running +# `plugin install` for plugins that are already installed is the trigger +# for `.claude.json` truncation (75k -> 1.5k), which makes Claude Code +# prompt for re-authentication in every project (subscription state lost). +# Same idempotence pattern as MCP registration (line 447). if command -v claude >/dev/null 2>&1; then log_info "Installing Claude Code plugins..." + installed_plugins=$(claude plugin list 2>/dev/null || true) + plugins_added=0 + plugins_skipped=0 for plugin in \ "claude-mem@thedotmack" \ "code-simplifier@claude-plugins-official" \ @@ -473,9 +486,15 @@ if command -v claude >/dev/null 2>&1; then "code-review@claude-plugins-official" \ "commit-commands@claude-plugins-official" \ "pr-review-toolkit@claude-plugins-official"; do - claude plugin install "$plugin" >/dev/null 2>&1 || true + if printf '%s' "$installed_plugins" | grep -qF "$plugin"; then + plugins_skipped=$((plugins_skipped + 1)) + else + if claude plugin install "$plugin" >/dev/null 2>&1; then + plugins_added=$((plugins_added + 1)) + fi + fi done - log_success "Claude Code plugins installed" + log_success "Claude Code plugins ready ($plugins_added added, $plugins_skipped already present)" else log_warning "Claude Code CLI not found, skipping plugin installation" fi diff --git a/setup-windows.ps1 b/setup-windows.ps1 index 0b5867e..a939e71 100644 --- a/setup-windows.ps1 +++ b/setup-windows.ps1 @@ -221,7 +221,15 @@ if (-not ($claudeCmd -and $npxCmd)) { } } -# Claude Code plugins (requires claude CLI) +# Claude Code plugins (requires claude CLI). +# Idempotent: cache the installed-plugins list ONCE before the loop and skip +# entries already present. CRITICAL: every `claude plugin install` writes to +# %USERPROFILE%\.claude\.claude.json. The CLI does NOT preserve all fields +# on rewrite — subscription metadata (organizationType, organizationRateLimitTier), +# the projects map, and onboarding flags get silently dropped. Re-running +# install for already-installed plugins triggers silent .claude.json truncation +# and forces re-authentication in every project. Same idempotence pattern as +# MCP registration above. if ($claudeCmd) { Write-Info "Installing Claude Code plugins..." $plugins = @( @@ -238,14 +246,22 @@ if ($claudeCmd) { "commit-commands@claude-plugins-official", "pr-review-toolkit@claude-plugins-official" ) + $installedPlugins = try { (& claude plugin list 2>$null) -join "`n" } catch { "" } + $pluginsAdded = 0 + $pluginsSkipped = 0 foreach ($plugin in $plugins) { + if ($installedPlugins -match [regex]::Escape($plugin)) { + $pluginsSkipped++ + continue + } try { & claude plugin install $plugin 2>$null | Out-Null + $pluginsAdded++ } catch { # Silently continue if a plugin fails } } - Write-Success "Claude Code plugins installed" + Write-Success "Claude Code plugins ready ($pluginsAdded added, $pluginsSkipped already present)" } else { Write-Warn "Claude Code CLI not found, skipping plugin installation" } From 464eecf7d267f3925da86e645add62e6ead98fc7 Mon Sep 17 00:00:00 2001 From: Manu Date: Fri, 15 May 2026 21:19:06 -0600 Subject: [PATCH 2/2] fix(setup-windows): replace em dash with ASCII to satisfy PSScriptAnalyzer PSUseBOMForUnicodeEncodedFile warning triggers on the em dash I added in the plugin-install idempotence comment. PSScriptAnalyzer rule requires either pure ASCII or a UTF-8 BOM marker; keeping the file ASCII is the simpler fix and matches the rest of setup-windows.ps1. --- setup-windows.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup-windows.ps1 b/setup-windows.ps1 index e6af15e..8743696 100644 --- a/setup-windows.ps1 +++ b/setup-windows.ps1 @@ -234,7 +234,7 @@ if (-not ($claudeCmd -and $npxCmd)) { # Idempotent: cache the installed-plugins list ONCE before the loop and skip # entries already present. CRITICAL: every `claude plugin install` writes to # %USERPROFILE%\.claude\.claude.json. The CLI does NOT preserve all fields -# on rewrite — subscription metadata (organizationType, organizationRateLimitTier), +# on rewrite -- subscription metadata (organizationType, organizationRateLimitTier), # the projects map, and onboarding flags get silently dropped. Re-running # install for already-installed plugins triggers silent .claude.json truncation # and forces re-authentication in every project. Same idempotence pattern as