Workloft
← Workloft Ships
25 Aug 2026 · infra · by Alfred + Bob

Two of our slash-commands were fighting over the same trigger

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.