I manage a dev team that has been living inside Claude Code for the better part of a year. Not “we tried it for a sprint” living, but “our CI runs on it and our pull request templates are actually Claude prompts” living. Somewhere in that journey, I got ambitious. I built a custom set of skills for our team: `code-review`, `debug-protocol`, and `team-conventions`. They live in `.claude/skills/`, look beautiful, have all the YAML frontmatter you could dream of — and for the first few weeks, they mostly just sat there. Silent. Ignored. Like a fire alarm that only goes off when there's already a fire.
The most frustrating part? The skill files were correct. The paths were right. The markdown was clean and documented beautifully. The skill just never fired. And after months of head-scratching, I finally learned the truth: it was never the skill's fault. It was mine.
The "Skill Didn't Fire" Trap
For anyone who hasn't stared into the abyss of Claude Code skill configuration yet, here's the short version: skills are folders under `.claude/skills/
```markdown
---
name: code-reivew
description: Use when the user asks for a code review.
---
```
That looks innocent enough. But the way Claude Code actually decides whether to fire a skill is by reading *every* skill description, mixing them into its system prompt, and matching your current request against them. In other words, skill activation is essentially an intent-matching game. And if your description is written like a literal keyword trigger, the game is rigged from the start.
We spent three weeks blaming Claude, blaming the API, blaming the phase of the moon. Then we actually read the official skill documentation at [docs.anthropic.com/en/docs/claude-code/skills](https://docs.anthropic.com/en/docs/claude-code/skills) and realized something uncomfortable: the model isn't lazy, it's just literal. It can only fire a skill it *recognizes* as relevant from the description. And our descriptions were terrible.
The Real Problem: Description Mismatch
Here's the thing that took us way too long to understand: **the skill doesn't exist in a vacuum.** It exists in a soup of other skills, built-in behaviors, and whatever else is in Claude's context. When a developer types something, Claude runs a quick mental calculation: *"Does this match any skill description? If multiple match, which is closest? Should I just answer directly instead?"*
If your skill description is too narrow, too vague, or too keyword-focused, Claude will miss it. And if your description collides with another skill or a built-in command, Claude will pick the wrong one. The code in your SKILL.md is never the issue. The metadata is.
Here are the three biggest ways this bites real teams.
Scenario 1: The Code Review Skill That Kept Napping
I wrote our `code-review` skill with the world's most obvious description: *"Use when the user asks for a code review."* Simple, right? Wrong.
Our developers never said "code review." They said:
- "Can you sanity-check my PR?"
- "Look at this diff, something's off."
- "Review this before I ship it."
- "Is this over-engineered?"
Guess which one triggered the skill? None. Claude answered those directly without ever invoking the skill because the phrase "code review" never appeared in the message. The fix was embarrassingly simple. We rewrote the description to be semantic, not literal:
```yaml
description: >-
Use for reviewing code changes, pull requests, diffs, or merge requests.
Trigger on phrases like "review this PR", "check my diff", "sanity-check
this code", "is this good to merge", or "look at these changes".
Not for general debugging or explaining code.
```
We also added a "Not for" clause. That single change made the skill fire roughly 80% more often. Claude Code skills are fundamentally prompt engineering — and the prompt is the description.
Scenario 2: The Debugging Protocol That Ghosted Us
Our `debug-protocol` skill was supposed to be the hero of the team. A structured, step-by-step process for reproducing bugs, checking logs, inspecting state, and proposing fixes. It was beautiful. It also never fired.
Why? Because the description started with the word "debug." And "debug" is everywhere in Claude Code. There's a built-in debug flag, there's system-level debugging behavior, and there are probably three other skills in the same directory using similar language. When our developers said "help me debug this failing test," Claude had four plausible matches and picked whichever one had the strongest *overall* prompt relevance — usually the built-in behavior, not our custom skill.
We learned two lessons from this:
1. **Give skills unique, specific names.** `debug-protocol` is generic. `sentry-repro-protocol` or `memory-leak-hunt` would have been better. Generic names get clobbered by generic intent.
2. **Use the manual override.** In Claude Code, you can always type `/debug-protocol` to force a skill to run. We added that to our team's conventions, and suddenly the skill wasn't dead — it just needed a direct invitation.
But here's the deeper insight: the model picked the wrong skill because *we* designed a collision. If you have two skills that sound similar, Claude will guess. Make the descriptions disqualify each other. Tell Claude explicitly: "use this instead of the general debugging behavior when the issue involves server logs."
Scenario 3: The Team Conventions Skill That Forgot Everything
The weirdest failure came from our `team-conventions` skill. It held the rules for commit messages, branch naming, and PR descriptions. It worked beautifully at the start of a session and then stopped somewhere around message 20. We assumed the model was "forgetting." It wasn't a memory bug — it was context.
Claude Code dumps all skill descriptions into the system prompt. Long descriptions eat context. And when the conversation builds up, the system starts compressing or truncating the system prompt to make room. Our `team-conventions` description was a wall of text — three paragraphs of corporate jargon. It was the first thing to get thrown overboard when the context window got tight.
The fix was to slim down the description to two or three sentences and move the actual rules into a referenced file inside the skill folder. Now the description is just a signpost: *"Use for commit message, branch naming, and PR conventions. Full rules in convention.md."* The model can hold that much, and when the skill fires, it reads the full file. We also moved non-skill rules into our `CLAUDE.md` project memory file, which is loaded more reliably for team-wide instructions.
Practical Fixes You Can Start Today
After weeks of pain, here's the checklist I wish someone had handed me on day one.
Treat the Description Like a Search Query
If someone searched your skill description, would it match the thing you actually want? Write the description as a semantic search string. Include synonyms, real phrases your team uses, and explicit exclusions. Don't assume the model "knows what you meant." It only knows what you wrote.
Keep Skill Files Lean
A skill file that's 500 lines long is a liability. It eats context, gets truncated, and becomes unreliable. Keep the SKILL.md focused on the *decision* of when to fire. Push the heavy details into helper files — `prompt.md`, `criteria.md`, `checklist.md` — that Claude reads only after the skill activates.
Add Negative Examples
This sounds counterintuitive, but it works. In the description, explicitly state what the skill is *not* for. Write: *"Not for general architecture discussions. Not for syntax questions."* This reduces false positives and also helps the model disambiguate between similar skills. Negative examples are the sharpest tool in the box.
Use `/` for Manual Override
No matter how good your metadata gets, there will be a moment when the auto-detection fails. Train your team to type `/skill-name` when they need to force activation. It's not a failure — it's a fallback. The smartest teams treat automatic skill firing as a convenience, not a dependency.
Debug It Like Any Other Code
Claude Code has a `--debug` flag. Use it. Inspect the system prompt that gets assembled at the start of a session and confirm your skill descriptions are actually in there. This sounds obvious, but we discovered that a trailing comma in one of our YAML frontmatter blocks was silently invalidating the whole file. The skill never loaded. Not because of intent-matching — because of a parse error. The debug output caught it in ten seconds.
Test Across Different Phrasings
Don't just test the exact phrase from your documentation. Open a fresh session and type the messy, human, ambiguous way your team actually talks. "Is this PR gonna explode?" should trigger your review skill if your descriptions are right. If it doesn't, your description isn't good enough yet.
FAQ
Why does my skill work in a fresh session but stop firing later?
That's almost always context truncation. Long skill descriptions get dropped from the system prompt as the conversation grows. Slim down the description, or restructure with a reference file. It's not a memory problem — it's a real estate problem.
Is it better to use keywords or natural language in skill descriptions?
Both, in the right ratio. Start with a one-sentence semantic description of *intent* ("use for reviewing code changes"), then add a list of common trigger phrases. Don't rely on keywords alone, but don't be so abstract that nothing matches either.
Can a custom skill override built-in Claude Code behavior?
Not directly. Built-in behaviors are wired in at a level your skill can't override. But you can win the match by making your skill description more specific and relevant to your team's actual situation. If you're stuck, use `/skill-name` to force it, or rephrase the request so the built-in path no longer fits.
How do I know if my skill file is even loading?
Run `claude --debug` and inspect the output. You should see your skills listed in the system prompt assembly. If your skill isn't listed, check the YAML frontmatter, the folder structure, and the filename. A skill folder without a valid `SKILL.md` is just a decoration.
The Bottom Line
Here's the uncomfortable truth: when a skill doesn't fire, the skill file is almost never the problem. The problem is how well you translated the human intent — "review my work, follow our rules, fix this bug in the way we agreed" — into the metadata Claude actually reads. It's not about writing better code. It's about writing better *signposts.*
If you're frustrated with silent skills, don't rewrite your markdown from scratch. Rewrite your descriptions. Add real examples. Add exclusions. Keep the file lean. And when all else fails, just type `/skill-name` directly and move on with your day.
The skill isn't broken. And believe it or not, neither is Claude. We just forgot that the model needs to be invited to the party with more than a vague nod across the room.
Comments (0)
No comments yet. Be the first to comment!
Leave a Comment