diff --git a/setup-linux.sh b/setup-linux.sh index a1ba9ad..dc4d83e 100755 --- a/setup-linux.sh +++ b/setup-linux.sh @@ -466,9 +466,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" \ @@ -482,9 +495,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 b55030e..8743696 100644 --- a/setup-windows.ps1 +++ b/setup-windows.ps1 @@ -230,7 +230,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 = @( @@ -247,14 +255,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" }