We have 27 skills and slash-commands in our agent's setup, built up over
months. Each one is a tidy file that does one job well. The problem is not
in any of those files. It is in the space between them: /linkedin
and /linkedinpost both answer to a bare /linkedin,
and which one wins was never something we decided. Nobody lints for that,
because the collision does not exist in any single file. So we built the
thing that does.
The bug that lives between files
A skill or slash-command is a trigger plus an instruction. On its own it is easy to reason about. The trouble starts when you have dozens, because two of them can quietly claim overlapping ground: the same slash prefix, the same quoted phrase, the same topic. When you type the trigger, one of them fires and the other does not, and you rarely notice the one that lost. Reviewing a single skill file will never surface this, because the conflict is a property of the set, not the file. It is the exact shape of bug that only a tool looking at everything at once can see.
What we built
skill_lint.py is one stdlib-only, read-only file. It reads every
skill and command definition, pulls out what each one claims to trigger on,
and ranks the pairs most likely to collide. It scores four signals, strongest
first: a name-prefix clash where one command name is a prefix
of another, so a bare invocation is ambiguous; a shared trigger
phrase where two skills quote the same natural-language cue; a
shared slash-token where both reference the same command; and
lexical overlap, how much two descriptions lean on the same
words, as a proxy for topic overlap. It never writes anything. Point it at
your setup and it hands back a ranked list with a one-line fix for each pair.
It caught its own bug first, again
The first run reported 68 collisions, which is not a finding, it is noise.
The scanner was reading absolute file paths in the skill bodies,
/home and /workloft and /conexus, as if
they were slash-commands, so half the setup appeared to collide with the
other half over a shared home directory. A linter that cries collision on
every pair is worse than none, because you learn to ignore it. The fix was to
teach the slash matcher the difference between a command and a path segment:
reject a token mid-path or one that carries a file extension. The count
dropped from 68 to five. Worse still, a stray not the in one
description, part of "NOT the Workloft company page", was being read as a
declaration that two skills were deliberately distinct, which downgraded the
one collision that actually mattered. We tightened that so a relationship only
counts when it names the other skill. Then the real finding floated to the
top, where it belonged.
What it found on us
Across our own 27 skills, one genuine problem and a short honest tail:
severity score pair
----------------------------------------------------------------
HIGH 66 /linkedin x /linkedinpost
- name 'linkedin' is a prefix of 'linkedinpost', so a bare slash is ambiguous
- lexical overlap 18% on: first-person, linkedin, personal, playbook…
MED 35 /go x /linkedinpost
LOW 16 /pizza x /sourdough
- lexical overlap 26% on: dough, fermentation, hydration, advice…
LOW 13 /repo-status x /standup
LOW 12 /ask-larry x /screenshot-website
The /linkedin versus /linkedinpost clash is real:
one is for personal build-in-public posts, the other for day-job takes, and a
bare /linkedin could reasonably mean either. That is a rename or
a disambiguator waiting to happen. Below it, /pizza and
/sourdough lean on the same words because they are both about
dough; they are not a bug, they are neighbours, and the tool ranks them low
for exactly that reason. What we were most pleased not to see: our
/ahfu and /triage commands, which are heavy
overlaps, did not appear at all, because one explicitly says it is an alias of
the other and the linter respects a declared relationship instead of nagging
about it.
Then we pointed it at the plugins
Run with --all, it also reads the plugin and marketplace skills
you did not write but which can still shadow yours. On our machine that pulled
in 74 definitions and immediately flagged a real one at the maximum score:
three separate channel plugins each ship a /configure and an
/access skill with almost identical descriptions, 87% lexical
overlap and the same quoted trigger phrases. Three skills answering to the
same word is not a hypothetical. It is sitting in most setups that have added
more than one integration, and nobody has looked.
What it does not do
It measures overlap, not intent. It reads what a skill says it triggers on, not what the model actually picks at dispatch time, so it hands you a shortlist to review, not a verdict. Lexical overlap is a heuristic, not semantics: it will rank two skills that share vocabulary as neighbours even when a human would keep them apart, which is why topic-only overlaps land low and structural clashes land high. And it only knows about the file layer you can see. None of that is the point. The point is that the most common bug in a mature agent setup is invisible by construction, one file at a time, and five minutes of a read-only script makes it visible. We found ours. Run it and find yours.
The linter is one stdlib-only file, MIT-licensed, on our GitHub mirror.