cd /news/developer-tools/unused-mcp-plugins-are-quietly-eatin… · home topics developer-tools article
[ARTICLE · art-61504] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

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.

read6 min views1 publishedJul 16, 2026

This is a continuation of my "Claude Code environment" series. After the previous post on automating git-config backups, 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.

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")

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.

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 python3MIN_CACHE_MB

, up to WEEKLY_MAX

entriesapply

mode, run plugin-disable.sh apply

cleanup-plugin-cache.sh apply

in sequence

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
  typescript-lsp pyright-lsp php-lsp ruby-lsp rust-analyzer-lsp swift-lsp
  code-simplifier code-modernization ralph-loop agent-sdk-dev mcp-server-dev
  playground commit-commands pr-review-toolkit
  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 thetool_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/

.

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).

~/.claude/scripts/cleanup-plugin-cache.sh purge

I set the weekly schedule in ~/Library/LaunchAgents/com.shun.plugin-auto-disable.plist

.

<key>StartCalendarInterval</key>
<dict>
  <key>Hour</key>
  <integer>6</integer>
  <key>Minute</key>
  <integer>45</integer>
  <key>Weekday</key>
  <integer>0</integer>   <!-- 0 = Sunday -->
</dict>

Every Sunday at 06:45, plugin-auto-disable.sh apply

runs, and its logs are appended to ~/.claude/logs/plugin-auto-disable.log

.

Registering and checking:

launchctl load ~/Library/LaunchAgents/com.shun.plugin-auto-disable.plist

launchctl list | grep plugin-auto-disable

~/.claude/scripts/plugin-auto-disable.sh dry

~/.claude/scripts/plugin-auto-disable.sh apply

type=="tool_use"

, the Dormant count went negative. Things like terraform showed up near the top as ghosts.typescript-lsp

was removed, type checking stopped working. Added it to PROTECTED and restored it.MIN_CACHE_MB

, plugins with tiny caches become targetsWEEKLY_MAX

too high drags in plugins you needplugin-disable.sh

doesn't exist, apply is a no-opplugin-auto-disable.sh

is a wrapper that calls this script internally. It won't work unless you have the full set of scripts in place.plugin-usage.sh

aggregates the real tool_use

events from session JSONL and visualizes dormant pluginstype=="tool_use"

plugin-auto-disable.sh

auto-disables based on 30-day inactivity, a 5MB-plus cache, and a cap of 5 per weekcleanup-plugin-cache.sh

archives caches into .disabled-cache/

, with physical deletion via purge

after 30 daysWeekday=0 / Hour=6 / Minute=45

runs 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.

*Written by Lily — I ship iOS apps and automate my content stack with Claude Code.

Follow along: Portfolio · X · GitHub*

── more in #developer-tools 4 stories · sorted by recency
── more on @claude code 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/unused-mcp-plugins-a…] indexed:0 read:6min 2026-07-16 ·