Why I Check Description Overlap Before Turning On inheritCustomizations
CLI 1.1.14 collapsed markdown agent inheritance into a single inheritCustomizations switch. Here is what happened when I actually inventoried the 47 skills in my workspace and decided the switch on description overlap rather than on context size.
I was reading through the CLI 1.1.14 notes from August 18 when inheritCustomizations stopped me. One line: markdown-defined agents now inherit skills, rules, plugins, subagents, and MCP servers through a single switch.
Before this, the defaults differed per category. I had been caught by that more than once — believing something was inherited when only part of it actually was. Consolidating it is a real improvement. But the moment it consolidated, a decision that used to be fuzzy landed squarely back in my hands.
Pass everything through, or turn it off and name what I need explicitly.
My first instinct was "I have too many skills, this will be heavy, set it to false." When I actually counted, that instinct turned out to be wrong.
One switch made the decision harder, not easier
When the defaults varied by category, my mistakes were partial. An MCP server would quietly fail to be inherited, I would notice, and I would fix that one thing.
With a single switch, the mistakes are wholesale. Set it true and a whole group goes through; set it false and I list things one by one. Either way the effect reaches every agent that inherits from that definition.
So before deciding, I wanted to see what I actually had. I work as an indie developer, so app maintenance and site operations share one workspace, and the skills directory had been growing by accretion for a long time without a single audit.
Sixty lines to inventory the inheritance candidates
I wrote a script that takes skill directories and reports body size and description health. Nothing clever: read description out of the SKILL.md frontmatter, split it into tokens, and look at the overlap.
Because my descriptions mix Japanese and English, I treat both alphanumeric words and CJK bigrams as tokens. A morphological analyzer would be more accurate, but I only needed enough signal to answer "are these two too similar."
#!/usr/bin/env python3"""Inventory the skills that are candidates for inheritance.Usage: python3 skill_inventory.py <skills-directory> [...]Each <name>/SKILL.md directly under a given directory counts as one skill."""import reimport sysimport statisticsfrom pathlib import Pathfrom collections import Counter, defaultdictFM = re.compile(r"\A---\r?\n(.*?)\r?\n---\r?\n", re.S)DESC = re.compile(r"^description:\s*(.*)$", re.M)# Handle mixed-language descriptions: alphanumeric words plus CJK bigramsWORD = re.compile(r"[A-Za-z][A-Za-z0-9_-]{2,}")CJK = re.compile(r"[ぁ-んァ-ヶ一-龠]{2,}")def tokens(text): t = {w.lower() for w in WORD.findall(text)} for run in CJK.findall(text): for i in range(len(run) - 1): t.add(run[i:i + 2]) return tdef collect(roots): skills = [] for root in roots: for path in sorted(Path(root).glob("*/SKILL.md")): raw = path.read_text(encoding="utf-8", errors="replace") fm = FM.match(raw) desc = "" if fm: m = DESC.search(fm.group(1)) if m: # Strip YAML quotes; plenty of files do not use them at all desc = m.group(1).strip().strip('"').strip("'") skills.append({"name": path.parent.name, "body": len(raw), "desc": desc}) return skillsdef main(roots): skills = collect(roots) if not skills: print("No SKILL.md found") return 1 body = [s["body"] for s in skills] desc_total = sum(len(s["desc"]) for s in skills) dead = [s["name"] for s in skills if not s["desc"]] print(f"skills : {len(skills)}") print(f"body total : {sum(body):,} chars") print(f"body median / max : {int(statistics.median(body)):,} / {max(body):,} chars") # Guard against division by zero when every description is missing ratio = sum(body) // desc_total if desc_total else 0 print(f"description total : {desc_total:,} chars (1/{ratio} of body)") print(f"missing desc : {len(dead)} {dead}") tok = {s["name"]: tokens(s["desc"]) for s in skills if s["desc"]} freq = Counter() for t in tok.values(): freq.update(t) overlap = defaultdict(list) for a, ta in tok.items(): for b, tb in tok.items(): if a >= b or not ta or not tb: continue j = len(ta & tb) / len(ta | tb) if j >= 0.25: overlap[round(j, 2)].append((a, b)) print("\n--- pairs with 25%+ description overlap ---") if not overlap: print("none") for j in sorted(overlap, reverse=True): for a, b in overlap[j]: print(f" {j:.2f} {a} <-> {b}") print("\n--- tokens appearing in 5+ descriptions (useless for selection) ---") noisy = [(w, c) for w, c in freq.most_common(24) if c >= 5] print(" " + ", ".join(f"{w}({c})" for w, c in noisy)) return 0if __name__ == "__main__": sys.exit(main(sys.argv[1:] or ["."]))
The a >= b check keeps each pair from being counted twice. The 0.25 threshold is simply the lowest value where almost no unrelated pairs showed up in my directory — adjust it for yours.
✦
Thank you for reading this far.
Continue Reading
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 will be able to decide inheritance from measured numbers in your own skill directory instead of guessing
✦You will catch the case where an agent picks the wrong sibling skill before it happens, rather than after you have spent half a day tracing it
✦You will understand why rewriting descriptions only moves overlap from 0.89 to 0.77, so you can skip the rewrite entirely
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.
Running it over 47 skills produced a number I did not expect
I pointed it at two locations: my general-purpose skills directory and the per-site one.
Metric
Measured
Skills
47
Total SKILL.md body
524,316 chars
Body median / max
7,043 / 47,520 chars
Total description
11,849 chars (1/44 of body)
Skills with an empty description
4
Pairs overlapping 0.25 or more
12
Half a million characters of body text. Looking at that alone, inheriting everything sounds reckless.
But the body is not what gets carried. What an agent reads to decide which skill to open is the description. The body is opened only once a skill has been chosen. The standing cost was not 524,316 characters — it was 11,849, one forty-fourth of it.
That is where my first assumption broke. I had been planning to disable inheritance because of weight, and weight was never the deciding factor.
The four skills with no description will never be selected anyway
The same run surfaced four skills with an empty description. Opening them, each was a tombstone containing the word "DEPRECATED" and a forwarding path. They were leftovers from a move, with the frontmatter stripped and the directory still sitting there.
This sits underneath the inheritance question entirely. A skill without a description never appears as an option to the agent. Setting inheritCustomizations: true will not summon those four; they stay unreachable. Which also means the reverse is a useful debugging habit: if something is "inherited but never fires," check for a description before you suspect the switch.
Of the twelve overlapping pairs, the top ones were all siblings. My per-site skills — one per domain — overlap each other between 0.77 and 0.95.
The reason was obvious once I read them side by side. Every one of them says some variation of "site build, content management, and operations skill for [domain]. Use for site construction, theme management, and i18n work." Only the domain name differs, because I wrote the first one and copied it to make the rest.
An agent does not choose a skill by its directory name. It chooses by reading the description. Line up four nearly identical sentences and the choice becomes close to a coin flip. Enabling inheritance meant handing the agent that four-way flip on every run.
I measured how far a rewrite would actually take me
The obvious response is "write better descriptions." I tried exactly that: rewrote all four, then measured again.
import refrom itertools import combinationsWORD = re.compile(r"[A-Za-z][A-Za-z0-9_-]{2,}")CJK = re.compile(r"[ぁ-んァ-ヶ一-龠]{2,}")def tokens(t): s = {w.lower() for w in WORD.findall(t)} for run in CJK.findall(t): for i in range(len(run) - 1): s.add(run[i:i + 2]) return sdef jaccard(a, b): ta, tb = tokens(a), tokens(b) return len(ta & tb) / len(ta | tb)def report(label, d): vals = [(a, b, jaccard(d[a], d[b])) for a, b in combinations(sorted(d), 2)] print(f"--- {label} ---") for a, b, j in sorted(vals, key=lambda x: -x[2]): print(f" {j:.2f} {a} <-> {b}") print(f" max {max(v[2] for v in vals):.2f} / mean {sum(v[2] for v in vals) / len(vals):.2f}")
The rewritten version named the target repository, listed the categories it owns, and added an explicit exclusion clause. I expected to land somewhere in the 0.4 range.
Version
How the descriptions were written
Max overlap
Mean overlap
BEFORE
One template with the domain name swapped
0.93
0.89
AFTER
Target, categories, and exclusion clause spelled out
0.82
0.77
From 0.89 to 0.77. Barely moved.
The cause was in the rewrite itself. Phrases like "operates only on the [domain] repository" and "do not use this when touching another domain" went into all four files, so my careful prose became the new shared vocabulary. The common scaffolding I added outweighed the distinct nouns I added.
Explaining more made them more alike.
Only dropping the shared prose brought it to 0.22
So I went the other way. I gave up on readable sentences and kept nothing but the identifying nouns: the domain, and the category slugs that site owns. As prose it is unpleasant.
Version
Max overlap
Mean overlap
vs BEFORE
BEFORE
0.93
0.89
—
AFTER
0.82
0.77
0.87x
MINIMAL
0.27
0.22
0.25x
A mean of 0.22. What a careful rewrite moved by 13 percent, deleting the shared prose cut to a quarter.
The lesson I took is that a description is an identifier, not an explanation. The more you polish it as human-readable copy, the closer it drifts to its neighbors. Put only the words its neighbors do not have.
Here is the order I now use for inheritCustomizations. I evaluate groups of skills, not individual agents.
Groups with empty descriptions do not enter the discussion. Fix them or delete them first. Leaving them in place creates the hardest state to diagnose: present in the listing, impossible to invoke.
Any group containing a pair above 0.5 does not get inherited. Set it to false and pass the one skill you need explicitly at call time. Deciding at the call site beats making the agent draw from an ambiguous set every run.
Groups with destructive operations or project-specific assumptions do not get inherited. If a procedure includes deletes, pushes, or deploys, narrow who can reach it while narrowing is cheap. It is the same reasoning behind 1.1.14 making paths outside the workspace read-only by default, applied one layer up.
General-purpose skills whose meaning is self-contained can be inherited. Things like "verify before declaring done" or "debug systematically" do not compete with siblings and mean the same thing to every agent. This is where true pays off cleanly.
In my workspace that resolved to true for the general-purpose group and false for the per-site group, with the one relevant site skill named at call time. A single switch does not require a single answer for the whole workspace — decide per agent definition, based on what that definition is carrying.
What to do next
If you are also sitting on a skills directory that only ever grew, run the inventory script once before you touch inheritCustomizations. It takes a couple of minutes.
Look at two numbers, not the body total: how many descriptions are empty, and how many pairs exceed 0.5. If both are zero, turn inheritance on without hesitation. If they are not, you already have the cleanup list in front of you, and the switch can wait until that list is empty.
I had never actually counted my own environment until this audit turned up four tombstones. If you have been putting off the same count, I hope this saves you a step.
Share
Thank You for Reading
Antigravity 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.