CLAUDE LABJP
CLI — Claude Code v2.1.261 landed on September 4. Of its 67 changes, 46 are fixes, and on the CLI side they cluster around input handling and background executionSKILLS — The new /skill-doctor lists which loaded skills go unused and how much context each one costs you every turn. As a starting point for cleanup, it is plainly usefulCOST — In one real-world setup the skill list alone consumed roughly 13,800 tokens per turn. Eighty-four skills had never been called, and duplicates from double-enabled plugins accounted for 41% of the totalLIMIT — New bashOutputMaxChars and taskOutputMaxChars settings raise how much command and background output reaches Claude inline before it is spilled to a file, up to 128K charactersBREAKING — Prompt word-editing keys now follow Bash and keybindingFlavor no longer has any effect. In auto mode, links that embed content into public diagram renderers count as uploadsSAFEGUARDS — Enterprise Frontier Safeguards was announced on September 1. The monitoring data sits in cloud infrastructure the customer controls rather than Anthropic's, and there is no extra chargeCLI — Claude Code v2.1.261 landed on September 4. Of its 67 changes, 46 are fixes, and on the CLI side they cluster around input handling and background executionSKILLS — The new /skill-doctor lists which loaded skills go unused and how much context each one costs you every turn. As a starting point for cleanup, it is plainly usefulCOST — In one real-world setup the skill list alone consumed roughly 13,800 tokens per turn. Eighty-four skills had never been called, and duplicates from double-enabled plugins accounted for 41% of the totalLIMIT — New bashOutputMaxChars and taskOutputMaxChars settings raise how much command and background output reaches Claude inline before it is spilled to a file, up to 128K charactersBREAKING — Prompt word-editing keys now follow Bash and keybindingFlavor no longer has any effect. In auto mode, links that embed content into public diagram renderers count as uploadsSAFEGUARDS — Enterprise Frontier Safeguards was announced on September 1. The monitoring data sits in cloud infrastructure the customer controls rather than Anthropic's, and there is no extra charge
Articles/Claude Code
Claude Code/2026-09-06Beginner

I counted 42 skills on my shelf, and four of them were just a note saying they had moved

Claude Code v2.1.261 added /skill-doctor. Before running it I counted my own skill shelf from the files, found four tombstones, and learned that the obvious awk one-liner for spotting duplicate plugin names quietly reports skills that are not duplicates at all.

Claude Code249Agent Skills3plugins4context budgetindie development23

I opened my skills list first thing one morning, meaning to fix the one that sorts wallpaper assets. I got a few lines down and stopped. There were more rows than I remembered.

Forty-two, once I counted. For more than half of them I could not tell you when I last called one.

I remember adding each of them. Every one made sense on the day it went in. What never happens on its own is the day you take something out. A shelf only grows, and that morning I finally admitted it about mine.

Claude Code v2.1.261, released on September 4, added /skill-doctor. It lists the skills you have loaded, which ones have never been used, and how much context each one is eating. Since it was there, I decided to count my own shelf from the files first, and then look at what the command had to say.

What rides along every turn is one line of description

Let me start with the structure, because getting this backwards makes every number afterwards mean something else.

When a skill is loaded, what sits in context on every turn is not the body of SKILL.md. It is the name and description from the frontmatter — the part that says what this skill is for. The body is only read when the skill is actually invoked.

Put another way: a skill you have never called still costs you its description on every single turn. "I'm not using it, so it's free" is not how it works.

The table /skill-doctor prints has six columns.

ColumnWhat it means
skillThe skill name
sourceWhere it comes from (user, plugin, marketplace)
contextHow much rides along each turn as the description line
7d tokensWhat it actually consumed over the last seven days
usesHow many times it was invoked
last usedWhen it was last invoked

Rows are ordered by last used, which puts everything marked never at the top. The first thing you see is the thing you least want to look at.

Counting my own shelf, from the files

Before waiting on the command, I counted what the files alone could tell me. A small script that walks the folders and adds up how many characters each description takes.

import os, re, sys
 
root = sys.argv[1] if len(sys.argv) > 1 else "."
rows = []
for name in sorted(os.listdir(root)):
    path = os.path.join(root, name, "SKILL.md")
    if not os.path.isfile(path):
        continue
    text = open(path, encoding="utf-8", errors="replace").read()
    m = re.match(r"^---\n(.*?)\n---\n", text, re.S)
    desc = ""
    if m:
        d = re.search(r"^description:\s*(.*(?:\n[ \t]+.*)*)$", m.group(1), re.M)
        if d:
            desc = re.sub(r"\s+", " ", d.group(1).strip().strip("\"'"))
    rows.append((name, m is not None, len(desc), len(text), len(text.splitlines())))
 
with_fm = [r for r in rows if r[1]]
without  = [r for r in rows if not r[1]]
desc_total = sum(r[2] for r in with_fm)
body_total = sum(r[3] for r in rows)
 
print(f"folders with SKILL.md : {len(rows)}")
print(f"with frontmatter      : {len(with_fm)}")
print(f"without frontmatter   : {len(without)}")
print(f"description total     : {desc_total:,} chars (avg {desc_total // max(len(with_fm),1)})")
print(f"body total            : {body_total:,} chars / {sum(r[4] for r in rows):,} lines")
print(f"description share     : {desc_total / body_total * 100:.2f} %")
print("longest descriptions  :")
for r in sorted(with_fm, key=lambda x: -x[2])[:3]:
    print(f"  {r[0]:<32} {r[2]:>4} chars")

Pointed at my skills folder, it printed this.

folders with SKILL.md : 42
with frontmatter      : 38
without frontmatter   : 4
description total     : 11,752 chars (avg 309)
body total            : 332,292 chars / 8,931 lines
description share     : 3.54 %
longest descriptions  :
  claude-api                        744 chars
  ai-seo                            698 chars
  seo-audit                         686 chars

There are 332,000 characters of skill bodies on that shelf, and only 3.54 percent of that — a little under twelve thousand characters — rides along every turn. Seeing the ratio was a relief. The shelf is heavy, but almost all of the weight only lands when I actually reach for something.

The average of 309 characters is not nothing, though. One skill spends 744 characters explaining itself and another gets by on 79, a spread of more than nine times on the same shelf. Some of that gap is worth closing by rewriting, not by deleting.

Four of them were only a note saying they had moved

The line that mattered most was the one counting folders with no frontmatter at all: four of them.

I opened each. They were short documents that said the file had moved, and gave the new location. Leftovers from a time I reorganised where skills live. The move itself was finished long ago; only the forwarding notes were still sitting on the shelf. On the day I wrote them, I thought they were needed.

You cannot spot these by reading names. The names look perfectly alive. You only find out by opening the thing and seeing that its job ended a while back.

I count the number of times I reached for something, not the number of things I put on the shelf. The row count of a list is not the count of what you use. I intend to keep that sentence somewhere near my desk for a while.

I try not to read never as "unnecessary"

This is the point where it gets tempting to hurry, and where it pays to stop for a moment.

last used: never means "not invoked, as far as this machine's records go." A skill you installed yesterday shows never. So does a skill you only reach for a couple of times a year. On the day it appears, it is a candidate for removal, not a confirmed one.

Among my forty-two there were a few I use once a quarter. If I had cleared them out on momentum, the day I needed one would have started with remembering what it was I deleted.

There is one more thing I keep separate. Whether trimming unused skills improves response quality or speed is not something the official notes claim. I would rather write about context cost as context cost, and leave quality as its own question. What went down was the bill, not my skill.

Is the same name arriving from two different shelves?

The other thing worth checking is whether you have the same plugin enabled from two marketplaces. Same name, different origin, and it loads twice — every one of its skills doubled.

The quick way is to dump the /skill-doctor output to a file and look for repeated names. The obvious one-liner takes the first whitespace-separated field, and that is where it trips over names containing a space. Here is exactly what I ran.

# A table with a couple of space-containing names mixed in, for testing
cat > /tmp/sd.txt <<'TABLE'
skill                     source                    context  7d tokens  uses  last used
brand voice               marketplace-a             300      0          0     never
brand guidelines          user                      280      0          0     never
seo-audit                 user                      686      3120       4     2 days ago
seo-audit                 claude-plugins-official   686      0          0     never
TABLE
 
awk 'NF>3 {print $1}' /tmp/sd.txt | sort | uniq -d

The result:

brand
seo-audit

seo-audit is a real hit. brand is not. brand voice and brand guidelines are two different skills, but taking only the first field makes their leading word line up, and the pair gets reported as a duplicate. Names with spaces in them are common enough that this false positive is not a corner case.

Cutting the name column at the first run of two or more spaces makes the confusion go away.

awk 'NR>1 {
  if (match($0, /  +/)) {
    n = substr($0, 1, RSTART - 1)
    c[n]++
  }
} END {
  for (k in c) if (c[k] > 1) printf "%s\tx%d\n", k, c[k]
}' /tmp/sd.txt | sort
seo-audit	x2

The NR>1 is there to drop the header row, so the literal word skill is not counted as a name. In the naive version it was being counted as one.

If you would rather check the origin itself, read it from the settings file. I have seen enabledPlugins written both as an array and as an object, so this handles either shape.

import json, sys, collections
 
path = sys.argv[1] if len(sys.argv) > 1 else "settings.json"
try:
    data = json.load(open(path, encoding="utf-8"))
except FileNotFoundError:
    print(f"no settings at: {path}")
    sys.exit(0)
except json.JSONDecodeError as e:
    print(f"cannot parse JSON: {path} ({e})")
    sys.exit(2)
 
raw = data.get("enabledPlugins")
pairs = []
if isinstance(raw, list):
    for entry in raw:
        name, _, market = str(entry).partition("@")
        pairs.append((name, market or "(marketplace unknown)"))
elif isinstance(raw, dict):
    for market, names in raw.items():
        for name in names or []:
            pairs.append((str(name), market))
else:
    print("enabledPlugins is neither an array nor an object. Check the shape first.")
    sys.exit(2)
 
by_name = collections.defaultdict(list)
for name, market in pairs:
    by_name[name].append(market)
 
dups = {n: m for n, m in by_name.items() if len(m) > 1}
print(f"{len(pairs)} enabled entries / {len(by_name)} distinct names")
for name in sorted(dups):
    print("  duplicate: " + name + " <- " + " / ".join(sorted(dups[name])))
sys.exit(1 if dups else 0)

I ran it against both shapes of settings file, and both gave the same answer.

5 enabled entries / 3 distinct names
  duplicate: aws-core <- agent-toolkit-for-aws / claude-plugins-official
  duplicate: frontend-design <- claude-plugins-official / my-marketplace

It exits 1 on a hit so I can line it up with the other morning checks later. A missing file exits 0 and stays quiet; only malformed JSON returns 2. Absent and broken deserve different treatment. On the wider theme of settings that fail without saying anything, I wrote up a related case in a mistyped key in settings.json is ignored without a word.

One thing to do tomorrow morning

Just one. Run /skill-doctor once and count how many rows say never. Only count. Do not delete anything that day.

Run it again a week later, and anything that got called in the meantime will have dropped off the never list. What is still marked never on the second pass is your first real candidate for removal. Since I started doing it in two passes, I have not once regretted a deletion.

If you want to go further into what a single description line actually costs, I measured a skill section by section in the skill that cost ten thousand tokens just to start, split by reach rather than size. And for the earlier question of which skills earn a place in your daily work at all, I put my reasoning in five things I look at before a Claude Code skill stays in my daily workflow.

Counting the shelf was not a cheerful task. Even so, since seeing the number forty-two, I have felt a little closer to my own tools. Thank you for reading this far.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-09-04
The One File I Keep Out of Claude Code's Reach: project.pbxproj
With Xcode project files, the breakage that still opens costs far more than the breakage that refuses to open. Here is what I measured before moving every edit behind a script, and where the line sits today.
Claude Code2026-08-25
One Space in a Folder Name Turned 80 Checks Into Zero
An inspection loop reported 80 files checked and 0 readable. The files were fine. Here is how word splitting turns path fragments into real directories, measured side by side, plus the count assertion I now put in front of every delete-heavy batch.
Claude Code2026-08-21
My read-deny rules never reached inside the folder I had allowed
An audit of how far permissions.deny Read rules actually reach in a working tree where secrets and source code live side by side, with a script you can run today.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →