Unused MCP Plugins Are Quietly Eating Your Context: A Weekly Auto-Disable Pipeline A developer built a three-stage pipeline to automatically disable MCP plugins that remain unused for 30 days, preventing context bloat in Claude Code sessions. The system detects dormant plugins, auto-disables them, and archives their caches, freeing up token budget that would otherwise be consumed by unused tool schemas. The pipeline runs weekly via launchd and includes a dry-run mode for safety. This is a continuation of my "Claude Code environment" series. After the previous post on automating git-config backups https://zenn.dev/bokuwalily/articles/claude-config-git-backup , this time I'm tackling a problem that sneaks up on you: MCP plugins you enabled once and forgot about keep chipping away at your context , and I'll walk through the three-stage pipeline that auto-disables them on a weekly schedule. When you enable a plugin, its tool schema is injected at startup even if it's never called once. As these "just leave it on" plugins pile up, the token budget Claude actually has to work with quietly shrinks. In this article I connect detection, auto-disable, and cache archiving into a single flow with real code. The motivation to install a new plugin is clear. "I want to use this tool" is an active urge. There's no equivalent motivation to remove one. You never get a moment where you notice "hmm, I haven't used that plugin lately." At the start of every session, Claude Code injects the tool schemas of every plugin marked true under enabledPlugins in settings.json into the context. The more plugins that are "enabled but haven't been invoked once in 30 days," the more the top of your context gets filled with zero-contribution schema on every single run. After a few months of trying things out and leaving them enabled, the count of Dormant plugins enabled but unused can climb past 30. plugin-usage.sh → visualize dormant plugins manual run / weekly report plugin-auto-disable.sh → auto-disable anything unused for 30 days, weekly cleanup-plugin-cache.sh → archive disabled caches into .disabled-cache/ The tail of plugin-auto-disable.sh apply chain-calls cleanup-plugin-cache.sh apply , so there's a single entry point for launchd to start. ~/.claude/scripts/plugin-usage.sh aggregates, from the session JSONL of the past N days 14 by default , only the plugins that were actually invoked as a tool use . plugin-usage.sh excerpt LOG DIR="$HOME/.claude/projects/-Users-matsubara" DAYS="${1:-14}" find "$LOG DIR" -maxdepth 1 -name " .jsonl" -mtime -"$DAYS" -print0 2 /dev/null | xargs -0 cat 2 /dev/null \ | jq -R -r 'fromjson? | select .type=="assistant" | .message.content ? | select .type=="tool use" | if .name=="Skill" then .input.skill // "" | select contains ":" | split ":" 0 else .name | select startswith "mcp plugin " | sub "^mcp plugin ";"" | split " " 0 end' 2 /dev/null \ | sort | uniq -c | sort -rn "$TMP" It reports the gap between the number of enabled plugins and the number of distinct invocations as Dormant. TOTAL ENABLED=$ jq -r ' .enabledPlugins // {} | to entries | select .value | length' "$SETTINGS" Dormant = TOTAL ENABLED - USED COUNT Why aggregate with jq instead of grepThe old implementation used grep for substring matching. It miscounted the "full list of deferred tools" lines in the JSONL a very long list of available tool names as "invocations," which made things like terraform show up near the top even though they were never called, and could even make the Dormant count go negative. Passing everything through select .type=="tool use" fixes this so thatonly calls that were actually invokedare counted. Sample output: Summary - Enabled plugins: 62 - Distinct plugins referenced in logs: 29 - Dormant enabled but never invoked : 33 Dormant Plugins enabled, no recent invocation Candidates for disabling to reduce context tax ... Recommendation - 33 plugins enabled but unused in last 14 days - Heavy dormant load — disabling these could free significant context budget - To disable a plugin: edit enabledPlugins in ~/.claude/settings.json Once Dormant exceeds 30, it prints "Heavy dormant load". This threshold is hardcoded inside the script. ~/.claude/scripts/plugin-auto-disable.sh is the core. It defaults to dry no changes ; pass apply and it actually disables plugins. plugin-auto-disable.sh excerpt MODE="${1:-dry}" DAYS="${DAYS:-30}" observation window default 30 days MIN CACHE MB="${MIN CACHE MB:-5}" skip caches smaller than 5MB WEEKLY MAX="${WEEKLY MAX:-5}" at most 5 per run Operational flow: true in enabledPlugins via python3 MIN CACHE MB , up to WEEKLY MAX entries apply mode, run plugin-disable.sh apply → cleanup-plugin-cache.sh apply in sequence sort candidates by size and pick up to WEEKLY MAX SIZED= for p in "${CANDIDATES @ }"; do size=$ du -sm "$HOME/.claude/plugins/cache/claude-plugins-official/$p" 2 /dev/null | awk '{print $1}' size="${size:-0}" "$size" -lt "$MIN CACHE MB" && continue SIZED+= "${size}\t${p}" done SELECTED= while IFS=$'\t' read -r sz p; do SELECTED+= "$p" "${ SELECTED @ }" -ge "$WEEKLY MAX" && break done < < printf '%b\n' "${SIZED @ }" | sort -rn Prioritizing by cache size descending is so the ones that free up the most context get processed first . The biggest risk of auto-disabling is false positives. The PROTECTED array exists to protect plugins that are "barely used but painful to lose." PROTECTED= remember plugin-dev hookify skill-creator session-report security-guidance superpowers context7 explanatory-output-style learning-output-style code-review feature-dev claude-md-management LSPs: Claude Code may call these transparently. They don't appear as tool use typescript-lsp pyright-lsp php-lsp ruby-lsp rust-analyzer-lsp swift-lsp Process tools: may be called ad-hoc code-simplifier code-modernization ralph-loop agent-sdk-dev mcp-server-dev playground commit-commands pr-review-toolkit Known false positive trouble in a past session azure-cosmos-db-assistant Why each category is protected: | Category | Examples | Reason to protect | |---|---|---| | Infrastructure | hookify superpowers remember | Core of startup hooks and skill invocation. Don't appear as tool use in the JSONL | | LSP | typescript-lsp , etc. | Claude Code itself calls them transparently, internally. Not recorded in logs | | Dev support | code-review feature-dev | Used ad-hoc. Needed even after long stretches of no calls | | Known false positive | azure-cosmos-db-assistant | Caused trouble when auto-disabled in the past | If you don't put the LSPs in PROTECTED, you jam up immediately typescript-lsp and friends are used internally by Claude Code itself. Since the user never explicitly invokes them as a Skill tool or MCP tool, they don't appear in the tool use logs. If they aren't in PROTECTED, they get auto-disabled while you're using them normally. Even after you disable a plugin, its cache remains under ~/.claude/plugins/cache/ . cleanup-plugin-cache.sh moves those into ~/.claude/plugins/.disabled-cache/ . cleanup-plugin-cache.sh excerpt CACHE DIR="$HOME/.claude/plugins/cache/claude-plugins-official" ARCHIVE DIR="$HOME/.claude/plugins/.disabled-cache" PURGE DAYS="${PURGE DAYS:-30}" for dir in "$CACHE DIR"/ /; do name=$ basename "$dir" if echo "$ENABLED" | grep -qx "$name"; then if "$MODE" = "apply" ; then mv "$dir" "$ARCHIVE DIR/" && echo " archived: $name" fi fi done It never actually deletes. It only moves things aside with mv , so restoring is reversible with a single mv . On top of that, the purge submode can physically delete only those items left in .disabled-cache/ for at least PURGE DAYS default 30 days . physically delete 30 days after archiving ~/.claude/scripts/cleanup-plugin-cache.sh purge I set the weekly schedule in ~/Library/LaunchAgents/com.shun.plugin-auto-disable.plist .