●QUOTA — The 50 percent weekly usage boost for Claude Code subscribers ended on August 19. Allowances are back to standard today, so long agent runs need rethinking across the week●CONTEXT — v2.1.234 cut the built-in claude-api skill from over 200k tokens to roughly 25k by loading its reference docs on demand rather than all at once●PERMISSIONS — In v2.1.235, permission dialog text and what a grant actually covers now always match, and the don't ask again option is withheld when contents cannot be fully shown●CACHE — Fixed whole-prompt-cache invalidation when a language server disconnected or reconnected mid-session, which had been quietly hurting hit rates on long sessions●SESSION — Claude Code now continues your session automatically when a claude.ai usage limit resets. Turn it off under Continue automatically at usage limit in /config●PRICING — Claude Sonnet 5's introductory $2 per million input and $10 output ends August 31; standard $3 and $15 pricing starts September 1, eleven days out●QUOTA — The 50 percent weekly usage boost for Claude Code subscribers ended on August 19. Allowances are back to standard today, so long agent runs need rethinking across the week●CONTEXT — v2.1.234 cut the built-in claude-api skill from over 200k tokens to roughly 25k by loading its reference docs on demand rather than all at once●PERMISSIONS — In v2.1.235, permission dialog text and what a grant actually covers now always match, and the don't ask again option is withheld when contents cannot be fully shown●CACHE — Fixed whole-prompt-cache invalidation when a language server disconnected or reconnected mid-session, which had been quietly hurting hit rates on long sessions●SESSION — Claude Code now continues your session automatically when a claude.ai usage limit resets. Turn it off under Continue automatically at usage limit in /config●PRICING — Claude Sonnet 5's introductory $2 per million input and $10 output ends August 31; standard $3 and $15 pricing starts September 1, eleven days out
The Biggest Section in Your SKILL.md Is Usually the One to Keep
A skill I run every week cost about 10K tokens just to load. Measuring it section by section showed why the largest block was the one I had to leave in place, and what splitting by reach actually saved.
I invoked the skill I use to sort wallpaper source images, and the first response took noticeably longer than it used to. The sorting procedure itself had not changed. All I had added were stricter rules for the categories that keep getting mixed up, plus the steps for pushing merged categories to the API.
When I measured the file, that single SKILL.md came to roughly 10,000 tokens. All of it was in context before we had discussed a single image.
In Claude Code v2.1.234, released on August 17, the context cost of loading the built-in claude-api skill dropped from over 200K tokens to about 25K, because reference documentation is now read at the point where it is actually needed. Deciding to do the same thing to my own skills was the easy part.
The hard part was choosing which sections to move out. My first instinct was to start with the largest one, and that turned out to be wrong.
Nothing has happened yet, and the context is already spent
In Claude Code, only a skill's description stays resident. The body of SKILL.md loads when the skill is invoked. So the size of the body is not a standing cost — it is a startup cost. Getting this backwards leads you toward pruning skills you rarely use, when the thing that actually matters is how much lands the moment you invoke the one you use daily.
As an indie developer running several apps and sites in parallel, I find my skills grow on their own. The wallpaper categorization skill carries decision rules for 30 categories, and over time I bolted on stricter rules for the categories that attract misclassification. Every line got there because something went wrong once. That is exactly why I could not trim it by feel.
So I started by measuring.
A small audit script that measures section by section
The idea is simple: split SKILL.md on H2 and H3 headings, count tokens per section, and sort descending. That alone tells you where your context went.
Claude's tokenizer is not public, so this uses tiktoken with cl100k_base. The absolute numbers are not exact. Treat them as a ruler for comparing sections against each other. There is a character-class fallback so the script still runs where tiktoken isn't installed.
#!/usr/bin/env python3"""Measure the context cost of a SKILL.md section by section.Usage: python3 skill_context_audit.py path/to/SKILL.md python3 skill_context_audit.py path/to/SKILL.md --split out_dirAdd a reach annotation (an HTML comment) right after a heading to groupsections by how likely a given run is to reach them. Untagged sectionsare treated as always."""import argparse, os, re, systry: import tiktoken _ENC = tiktoken.get_encoding("cl100k_base") def count_tokens(text: str) -> int: return len(_ENC.encode(text)) TOKENIZER = "cl100k_base"except ImportError: # Fallback: CJK chars run about 0.7 tokens each, ASCII about 4 chars per token def count_tokens(text: str) -> int: cjk = sum(1 for c in text if ord(c) > 0x2E80) return int(cjk * 0.7 + (len(text) - cjk) / 4) TOKENIZER = "approx"REACH_RE = re.compile(r"<!--\s*reach:\s*([a-z0-9_]+)\s*-->")HEADING_RE = re.compile(r"(?m)^(#{2,3} .*)$")def split_sections(md: str): """Return [(heading, block-including-heading)]; the first entry is __head__.""" parts = HEADING_RE.split(md) sections = [("__head__", parts[0])] for i in range(1, len(parts), 2): body = parts[i + 1] if i + 1 < len(parts) else "" sections.append((parts[i].strip(), parts[i] + "\n" + body)) return sectionsdef reach_of(block: str) -> str: m = REACH_RE.search(block) return m.group(1) if m else "always"def audit(path: str): md = open(path, encoding="utf-8").read() sections = split_sections(md) rows = [(h, reach_of(b), count_tokens(b)) for h, b in sections] total = sum(r[2] for r in rows) print(f"file : {path}") print(f"tokenizer : {TOKENIZER}") print(f"total : {total} tok / {len(md)} chars\n") print(f"{'tok':>7} {'share':>6} reach heading") for h, reach, t in sorted(rows, key=lambda r: -r[2]): print(f"{t:7d} {t * 100 / total:5.1f}% {reach:<9} {h[:56]}") by_reach = {} for _, reach, t in rows: by_reach[reach] = by_reach.get(reach, 0) + t print("\n-- by reach --") for reach, t in sorted(by_reach.items(), key=lambda kv: -kv[1]): print(f" {reach:<10} {t:6d} tok ({t * 100 / total:4.1f}%)") resident = by_reach.get("always", 0) print(f"\nstartup cost with only always sections in the body: {resident} tok" f"({(1 - resident / total) * 100:.1f}% lower)") return rows, total
Here is the output for the wallpaper categorization skill I actually run.
14,372 characters, about 10,048 tokens. The file is mostly Japanese, so it weighs more than the character count suggests — roughly 0.7 tokens per character. If you write skills in a non-Latin script, that ratio is worth internalizing. A line-count budget will never show it to you.
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You'll be able to see, section by section, which parts of your own SKILL.md are consuming context at load time
✦You'll be able to avoid the common mistake of extracting your largest section and paying for it in repeated lookups
✦You'll be able to decide when a multi-session workflow should become two skills instead of one skill with reference files
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The largest section was the one I had to leave alone
The decision rules were the obvious target. Thirty categories across 27 sections, 4,151 tokens, 41.3% of the file. Add the mutual exclusion rules and nearly half the skill sits in one place.
Moving it out should have halved my startup cost. I tried it, and put it back within a day.
Sorting means looking at one image at a time and deciding where it goes. The decision rules are consulted across that entire stretch. Moved into a reference file, they still get read — just not once. My skill merges every 100 images, so each batch boundary triggers another lookup. The 4,151 tokens I stopped paying at startup came back, spread across repeated reads, and the total went up rather than down.
The felt experience matched: a faster first response, followed by more small pauses during the work.
What I had gotten wrong was the criterion itself. Size tells you the ceiling of what an extraction could save. It says nothing about what it will save.
Two axes: reach and lookup frequency
Here is what I use instead.
Axis
Question
Signal
Reach
Will this run definitely enter this section?
Conditional stages and next-session stages have low reach
Lookup frequency
Once inside, how often is it consulted?
Read once and done, or referenced continuously
That gives four quadrants.
Consulted repeatedly
Consulted once
Always reached
Keep in the body (extracting makes it worse)
Keep in the body (extracting saves little)
Sometimes not reached
Extract, and state the entry condition
Extract — the best case
The decision rules are always reached and consulted repeatedly, so they belong in the body. The merge and API steps only run once 100 images have accumulated, and they are read once when they do. That was the real candidate.
You tag a section by putting a reach annotation directly under its heading.
## Merge rules<!-- reach: merge -->Once 100 sorted images have accumulated, merge them into the category list.
Untagged sections stay in the body as always. I wanted something I could retrofit onto skills I had already written, so only tagged sections move.
What the split actually saved
The same script does the extraction with --split. Tagged sections move to references/{reach}.md, and the body keeps only the instruction for when to read them.
def split_out(path: str, out_dir: str): md = open(path, encoding="utf-8").read() sections = split_sections(md) os.makedirs(os.path.join(out_dir, "references"), exist_ok=True) body, buckets = [], {} for h, block in sections: reach = reach_of(block) if reach == "always": body.append(block.rstrip()) else: buckets.setdefault(reach, []).append(block.rstrip()) for reach, blocks in buckets.items(): ref_path = os.path.join(out_dir, "references", f"{reach}.md") with open(ref_path, "w", encoding="utf-8") as f: f.write("\n\n".join(blocks) + "\n") # The body keeps the entry condition. Without it, the reference is never read. body.append( f"## Reference: {reach}\n\n" f"When you enter the {reach} stage, read this at that point.\n\n" f"```bash\ncat references/{reach}.md\n```" ) main_path = os.path.join(out_dir, "SKILL.md") with open(main_path, "w", encoding="utf-8") as f: f.write("\n\n".join(body) + "\n") before = count_tokens(md) after = count_tokens(open(main_path, encoding="utf-8").read()) print(f"\n{main_path}: {before} -> {after} tok " f"({(1 - after / before) * 100:.1f}% lower at startup)") for reach in buckets: p = os.path.join(out_dir, "references", f"{reach}.md") print(f" references/{reach}.md: {count_tokens(open(p, encoding='utf-8').read())} tok")
Results across the two skills:
Skill
Before
Body after split
Moved to references
Startup reduction
Wallpaper categorization
10,048 tok
8,448 tok
merge 1,530 / rare 178
16.0%
Ukiyo-e batch processing
5,252 tok
4,597 tok
postupload 705
12.5%
Smaller than I hoped. I expected to halve these files and got 16.0% and 12.5%.
But this 16% is 16% that the current run will never touch. Nothing comes back as repeated lookups, so the saving holds. The 41.3% extraction I attempted first would have converted almost all of its saving into lookup traffic. The size of the number and the durability of the number are different properties.
A reference nobody reads is worse than no reference
My first extraction failed for a duller reason. I moved the merge section into references/merge.md and left behind a line saying "see references/merge.md for details." Claude never opened it. It improvised the merge instead — because the 100-image threshold and the requirement to push categories to the API lived only inside the file that never got read.
What the body needs to keep is not the path. It is the description of the state that should trigger the read.
## Reference: mergeWhen 100 sorted images have accumulated, read this before doing anything else.The merge procedure and the API update steps exist only in this reference.```bashcat references/merge.md```
Since making that change, the behavior has been stable. Progressive disclosure sounds like it is about splitting, but the part that determines whether it works is what you write on the side you kept.
Three smaller things tripped me up while doing this.
Tags are per heading, not per subtree
My script treats H2 and H3 as siblings. Tagging ## Merge rules alone leaves ### Automatic merge (every 100 images) and ### Applying merged categories to the API sitting in the body. In the wallpaper skill I ended up annotating all four related headings.
Walking the hierarchy and moving child sections along with the parent would be friendlier, and I deliberately did not do it. There were cases where I wanted to extract a parent while keeping a child in place. It costs a few extra annotations, and in exchange nothing moves that I did not ask to move.
Give each reference file one line of purpose
A reference file, once detached, carries no surrounding context at the moment it is read. A bare list of steps loses the reason those steps exist. I now start each reference with a single line describing what the stage is meant to complete. One line of tokens, and the behavior after the read is noticeably steadier.
If a reference gets read twice, move it back
This is the clearest operating rule I have so far. If a reference is being read more than once in a single run, that section was never a low-reach section — it was something needed continuously. That is exactly why the decision rules went back into the body. I'd recommend treating extraction as reversible and watching the read counts rather than deciding once and moving on.
If you are chaining several skills together, the handoff design in Composing Claude Code Skills covers the same problem at a larger scale. A reference file is the minimal version of that handoff.
When the session breaks, split the skill instead
The ukiyo-e skill raised a different question.
Its first half takes source images, generates the derived sizes, updates categories and the API, drafts the push notification copy, and hands everything to me. The second half happens after I upload the files to the server: reconciling the main tree, confirming the live responses, and clearing the staging directory. A human step sits between them, so the two halves are always separate sessions.
That means the 705 tokens in the second half have exactly zero reach during the first session. Moving them to a reference removes them from startup, but in the second session they get read every single time. At that point, two skills is the simpler shape.
Approach
Fits when
Extract to references
The stage is inside the same session and only entered conditionally
Split into separate skills
A human step or external process guarantees the session ends
I'm switching to the two-skill shape at the next batch of source images. Once you learn to extract references, everything starts looking like a reference — but a session boundary is a skill boundary.
Where to start
Run the script against the one skill you invoke most often. The top five sections are usually enough to tell you where your context has been going.
Then, when you pick extraction candidates, ask whether a given run might never reach the section — not whether the section is large. Changing that one question cleanly separated the sections that were worth moving from the ones that punished me for moving them.
I still write skills that grow longer than they should. What changed is that I can now see it happening.
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.