{"slug": "unused-mcp-plugins-are-quietly-eating-your-context-a-weekly-auto-disable", "title": "Unused MCP Plugins Are Quietly Eating Your Context: A Weekly Auto-Disable Pipeline", "summary": "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.", "body_md": "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.\n\nWhen 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.\n\nThe 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.\"\n\nAt the start of every session, Claude Code injects the tool schemas of every plugin marked `true`\n\nunder `enabledPlugins`\n\nin `settings.json`\n\ninto 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.**\n\nAfter a few months of trying things out and leaving them enabled, the count of Dormant plugins (enabled but unused) can climb past 30.\n\n```\nplugin-usage.sh       → visualize dormant plugins (manual run / weekly report)\nplugin-auto-disable.sh → auto-disable anything unused for 30 days, weekly\ncleanup-plugin-cache.sh → archive disabled caches into .disabled-cache/\n```\n\nThe tail of `plugin-auto-disable.sh apply`\n\nchain-calls `cleanup-plugin-cache.sh apply`\n\n, so there's a single entry point for launchd to start.\n\n`~/.claude/scripts/plugin-usage.sh`\n\naggregates, from the session JSONL of the past N days (14 by default), **only the plugins that were actually invoked as a tool_use**.\n\n```\n# plugin-usage.sh excerpt\nLOG_DIR=\"$HOME/.claude/projects/-Users-matsubara\"\nDAYS=\"${1:-14}\"\n\nfind \"$LOG_DIR\" -maxdepth 1 -name \"*.jsonl\" -mtime -\"$DAYS\" -print0 2>/dev/null | xargs -0 cat 2>/dev/null \\\n  | jq -R -r 'fromjson? | select(.type==\"assistant\") | .message.content[]? | select(.type==\"tool_use\")\n      | if .name==\"Skill\" then ((.input.skill // \"\") | select(contains(\":\")) | split(\":\")[0])\n        else (.name | select(startswith(\"mcp__plugin_\")) | sub(\"^mcp__plugin_\";\"\") | split(\"_\")[0]) end' 2>/dev/null \\\n  | sort | uniq -c | sort -rn > \"$TMP\"\n```\n\nIt reports the gap between the number of enabled plugins and the number of distinct invocations as Dormant.\n\n```\nTOTAL_ENABLED=$(jq -r '[.enabledPlugins // {} | to_entries[] | select(.value)] | length' \"$SETTINGS\")\n# Dormant = TOTAL_ENABLED - USED_COUNT\n```\n\nWhy 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\n\n`select(.type==\"tool_use\")`\n\nfixes this so thatonly calls that were actually invokedare counted.\n\nSample output:\n\n```\n## Summary\n- Enabled plugins: **62**\n- Distinct plugins referenced in logs: **29**\n- Dormant (enabled but never invoked): **33**\n\n## Dormant Plugins (enabled, no recent invocation)\n_Candidates for disabling to reduce context tax_\n...\n\n## Recommendation\n- 33 plugins enabled but unused in last 14 days\n- Heavy dormant load — disabling these could free significant context budget\n- To disable a plugin: edit `enabledPlugins` in ~/.claude/settings.json\n```\n\nOnce Dormant exceeds 30, it prints \"Heavy dormant load\". This threshold is hardcoded inside the script.\n\n`~/.claude/scripts/plugin-auto-disable.sh`\n\nis the core. It defaults to `dry`\n\n(no changes); pass `apply`\n\nand it actually disables plugins.\n\n```\n# plugin-auto-disable.sh excerpt\nMODE=\"${1:-dry}\"\nDAYS=\"${DAYS:-30}\"             # observation window (default 30 days)\nMIN_CACHE_MB=\"${MIN_CACHE_MB:-5}\"    # skip caches smaller than 5MB\nWEEKLY_MAX=\"${WEEKLY_MAX:-5}\"        # at most 5 per run\n```\n\nOperational flow:\n\n`true`\n\nin `enabledPlugins`\n\nvia python3`MIN_CACHE_MB`\n\n, up to `WEEKLY_MAX`\n\nentries`apply`\n\nmode, run `plugin-disable.sh apply`\n\n→ `cleanup-plugin-cache.sh apply`\n\nin sequence\n\n```\n# sort candidates by size and pick up to WEEKLY_MAX\nSIZED=()\nfor p in \"${CANDIDATES[@]}\"; do\n  size=$(du -sm \"$HOME/.claude/plugins/cache/claude-plugins-official/$p\" 2>/dev/null | awk '{print $1}')\n  size=\"${size:-0}\"\n  [ \"$size\" -lt \"$MIN_CACHE_MB\" ] && continue\n  SIZED+=(\"${size}\\t${p}\")\ndone\n\nSELECTED=()\nwhile IFS=$'\\t' read -r sz p; do\n  SELECTED+=(\"$p\")\n  [ \"${#SELECTED[@]}\" -ge \"$WEEKLY_MAX\" ] && break\ndone < <(printf '%b\\n' \"${SIZED[@]}\" | sort -rn)\n```\n\nPrioritizing by cache size descending is **so the ones that free up the most context get processed first**.\n\nThe biggest risk of auto-disabling is false positives. The PROTECTED array exists to protect plugins that are \"barely used but painful to lose.\"\n\n```\nPROTECTED=(\n  remember plugin-dev hookify skill-creator session-report\n  security-guidance superpowers context7 explanatory-output-style\n  learning-output-style code-review feature-dev claude-md-management\n  # LSPs: Claude Code may call these transparently. They don't appear as tool_use\n  typescript-lsp pyright-lsp php-lsp ruby-lsp rust-analyzer-lsp swift-lsp\n  # Process tools: may be called ad-hoc\n  code-simplifier code-modernization ralph-loop agent-sdk-dev mcp-server-dev\n  playground commit-commands pr-review-toolkit\n  # Known false positive (trouble in a past session)\n  azure-cosmos-db-assistant\n)\n```\n\nWhy each category is protected:\n\n| Category | Examples | Reason to protect |\n|---|---|---|\n| Infrastructure |\n`hookify` `superpowers` `remember`\n|\nCore of startup hooks and skill invocation. Don't appear as tool_use in the JSONL |\n| LSP |\n`typescript-lsp` , etc. |\nClaude Code itself calls them transparently, internally. Not recorded in logs |\n| Dev support |\n`code-review` `feature-dev`\n|\nUsed ad-hoc. Needed even after long stretches of no calls |\n| Known false positive | `azure-cosmos-db-assistant` |\nCaused trouble when auto-disabled in the past |\n\nIf you don't put the LSPs in PROTECTED, you jam up immediately\n\n`typescript-lsp`\n\nand 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`\n\nlogs. If they aren't in PROTECTED, they get auto-disabled while you're using them normally.\n\nEven after you disable a plugin, its cache remains under `~/.claude/plugins/cache/`\n\n. `cleanup-plugin-cache.sh`\n\nmoves those into `~/.claude/plugins/.disabled-cache/`\n\n.\n\n```\n# cleanup-plugin-cache.sh excerpt\nCACHE_DIR=\"$HOME/.claude/plugins/cache/claude-plugins-official\"\nARCHIVE_DIR=\"$HOME/.claude/plugins/.disabled-cache\"\nPURGE_DAYS=\"${PURGE_DAYS:-30}\"\n\nfor dir in \"$CACHE_DIR\"/*/; do\n  name=$(basename \"$dir\")\n  if ! echo \"$ENABLED\" | grep -qx \"$name\"; then\n    if [ \"$MODE\" = \"apply\" ]; then\n      mv \"$dir\" \"$ARCHIVE_DIR/\" && echo \"  archived: $name\"\n    fi\n  fi\ndone\n```\n\n**It never actually deletes.** It only moves things aside with `mv`\n\n, so restoring is reversible with a single `mv`\n\n.\n\nOn top of that, the `purge`\n\nsubmode can physically delete only those items left in `.disabled-cache/`\n\nfor at least `PURGE_DAYS`\n\n(default 30 days).\n\n```\n# physically delete 30 days after archiving\n~/.claude/scripts/cleanup-plugin-cache.sh purge\n```\n\nI set the weekly schedule in `~/Library/LaunchAgents/com.shun.plugin-auto-disable.plist`\n\n.\n\n```\n<key>StartCalendarInterval</key>\n<dict>\n  <key>Hour</key>\n  <integer>6</integer>\n  <key>Minute</key>\n  <integer>45</integer>\n  <key>Weekday</key>\n  <integer>0</integer>   <!-- 0 = Sunday -->\n</dict>\n```\n\nEvery Sunday at 06:45, `plugin-auto-disable.sh apply`\n\nruns, and its logs are appended to `~/.claude/logs/plugin-auto-disable.log`\n\n.\n\nRegistering and checking:\n\n```\n# register\nlaunchctl load ~/Library/LaunchAgents/com.shun.plugin-auto-disable.plist\n\n# check it's running\nlaunchctl list | grep plugin-auto-disable\n\n# manual test (no changes)\n~/.claude/scripts/plugin-auto-disable.sh dry\n\n# manual run\n~/.claude/scripts/plugin-auto-disable.sh apply\n```\n\n`type==\"tool_use\"`\n\n, the Dormant count went negative. Things like terraform showed up near the top as ghosts.`typescript-lsp`\n\nwas removed, type checking stopped working. Added it to PROTECTED and restored it.`MIN_CACHE_MB`\n\n, plugins with tiny caches become targets`WEEKLY_MAX`\n\ntoo high drags in plugins you need`plugin-disable.sh`\n\ndoesn't exist, apply is a no-op`plugin-auto-disable.sh`\n\nis a wrapper that calls this script internally. It won't work unless you have the full set of scripts in place.`plugin-usage.sh`\n\naggregates the real `tool_use`\n\nevents from session JSONL and visualizes dormant plugins`type==\"tool_use\"`\n\n`plugin-auto-disable.sh`\n\nauto-disables based on 30-day inactivity, a 5MB-plus cache, and a cap of 5 per week`cleanup-plugin-cache.sh`\n\narchives caches into `.disabled-cache/`\n\n, with physical deletion via `purge`\n\nafter 30 days`Weekday=0 / Hour=6 / Minute=45`\n\nruns it unattended every Sunday at 06:45Next time, to verify the context headroom this buys back, I'll write about the **real-time context-usage meter in the statusline** that I built.\n\n*Written by **Lily** — I ship iOS apps and automate my content stack with Claude Code.\n\nFollow along: [Portfolio](https://bokuwalily.com) · [X](https://x.com/bokuwalily) · [GitHub](https://github.com/bokuwalily)*", "url": "https://wpnews.pro/news/unused-mcp-plugins-are-quietly-eating-your-context-a-weekly-auto-disable", "canonical_source": "https://dev.to/bokuwalily/unused-mcp-plugins-are-quietly-eating-your-context-a-weekly-auto-disable-pipeline-1gl7", "published_at": "2026-07-16 05:00:06+00:00", "updated_at": "2026-07-16 05:06:24.594206+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "ai-tools"], "entities": ["Claude Code", "MCP", "launchd"], "alternates": {"html": "https://wpnews.pro/news/unused-mcp-plugins-are-quietly-eating-your-context-a-weekly-auto-disable", "markdown": "https://wpnews.pro/news/unused-mcp-plugins-are-quietly-eating-your-context-a-weekly-auto-disable.md", "text": "https://wpnews.pro/news/unused-mcp-plugins-are-quietly-eating-your-context-a-weekly-auto-disable.txt", "jsonld": "https://wpnews.pro/news/unused-mcp-plugins-are-quietly-eating-your-context-a-weekly-auto-disable.jsonld"}}