Skip to main content

whyskill

Your skill isn't broken. It's invisible.

You wrote a skill. Claude ignores it. There's no error, no warning, nothing in the logs — it just never fires, and you have no idea why.

Usually it's something you cannot see by looking at the file:

  • a blank line above ---, so the frontmatter was never read
  • your trigger phrases sitting past a 1,536-character cap that cuts them off
  • a skill in ~/.claude/skills quietly overriding your project's one
  • two skills described so alike that Claude picks the wrong one

whyskill finds all of it. There is nothing to install to try it:

git clone https://github.com/hjalti-hub/whyskill && cd whyskill
python3 -m whyskill ~/.claude/skills

No API key. No model calls. No dependencies. Just python3.

Once you want it to run without being asked, install it properly and it hooks itself in — after that you never type it again.

$ whyskill

.claude/skills/deploy-staging/SKILL.md
  1: error LOAD001  Blank line(s) before the `---` on line 2; frontmatter is only read when `---` is line 1
      fix: Delete the blank line(s) so `---` is the first line.

.claude/skills/release-notes/SKILL.md
  4: error LIST004  Listing text is 1674 characters, 138 over the 1536-character cap - and every trigger phrase is in the truncated part
      fix: Move the trigger phrases to the front of `description`. Dropped text begins:
      'k procedure, and the on-call escalation path. Use when the user asks to deplo'

.claude/skills/git-helper/SKILL.md
  3: warning COLLIDE005  'git-helper' and 'git-assistant' describe themselves 59% alike; routing between them is unreliable
      fix: Make the descriptions disjoint. Both currently claim: branches, changes, git,
      commit, message, write. State what each one is *not* for, or name the other skill
      explicitly to turn the ambiguity into a routing rule.

3 error(s) · 4 warning(s) across 22 skills
Run with --explain to see why each one matters.

And the question you actually have:

$ whyskill why deploy-staging

deploy-staging  .claude/skills/deploy-staging/SKILL.md
  source: project
  Claude cannot auto-invoke this skill.

  error LOAD001  Blank line(s) before the `---` on line 2
      why: Claude Code reads frontmatter only when the opening `---` is the file's first
           line. Otherwise it treats the whole file, `---` markers included, as skill
           content - so the skill has no description to match on.
      fix: Delete the blank line(s) so `---` is the first line.

Running itself

Nobody remembers to run a linter for a bug they don't know they have. So the normal way to use whyskill is to never type it:

whyskill install

This adds two hooks to .claude/settings.json (use --user for every project, --local to keep it out of the repository). The harness runs hooks — Claude does not choose to. That distinction matters here more than usual: a skill has to be selected to run, and being selected is precisely the thing that fails silently. A hook fires whether or not anyone thought about it.

PostToolUse fires the instant a SKILL.md is written, by you or by Claude. If the skill has errors, the hook exits 2, which puts its output in front of Claude — so a skill written broken gets reported and fixed inside the same turn:

whyskill: SKILL.md was written with 1 error(s) that will make it fail silently.   .claude/skills/deploy/SKILL.md:1     LOAD001 (error): Blank line(s) before the --- on line 2     fix: Delete the blank line(s) so --- is the first line.

SessionStart fires when a session opens and reports skills that were already broken before today, as context Claude can act on.

Three properties keep it from becoming a nuisance:

  • It is silent when nothing is wrong. A clean run prints nothing at all, so it costs no context and never trains you to ignore it.
  • It never breaks your session. Any internal failure — a malformed payload, a bug in whyskill itself — exits 0 quietly. A linter that breaks the tool it protects has negative value.
  • It is cheap on the common path. Most edits aren't skill files; that case returns before whyskill imports anything.

SessionStart reports only errors by default, since warnings on every session open would be noise. PostToolUse includes warnings, because you're already looking at that file. Both are adjustable in the settings it writes.

whyskill install --status      # is it installed?
whyskill install --print       # show the settings.json without writing it
whyskill install --uninstall   # remove it; other hooks are left untouched

The installer merges rather than overwrites, keeps a .whyskill-backup, is idempotent, and refuses to touch a settings.json it cannot parse.

Or as a skill

.claude/skills/whyskill/SKILL.md ships in this repo, so /whyskill works and Claude can reach for it when you ask why something isn't firing. It's a convenience, not the mechanism — the hooks are what make it autonomous, and the skill is subject to every failure mode it detects.


Why another skill linter?

There are already several good SKILL.md linters. Before writing this one I checked what they cover, and the honest answer is: they check different things, and they check them one file at a time.

Two gaps motivated this tool.

1. Some widely-used checks are not real. A popular skill auditor reports that 62% of public skills are "missing the version field". version is not a Claude Code frontmatter field. It appears nowhere in the frontmatter reference; Claude Code does not read it. Adding it fixes nothing. whyskill only implements rules traceable to documented behaviour, and every finding it emits names the mechanic it comes from — so you can check the claim yourself.

2. Whether your skill loads is not a property of your file. It depends on what else is installed. A skill can be flawless on its own and still never run because a same-named personal skill outranks it, or because another skill's description claims the same vocabulary. A per-file linter cannot see this, no matter how many rules it has. whyskill analyses the whole visible set at once.

It is not a replacement for the others — it does not check shell-script safety or style. It answers one question they do not: will this skill be there when you need it?


Install

To try it, a clone is enough — run it as a module from inside the checkout:

git clone https://github.com/hjalti-hub/whyskill && cd whyskill
python3 -m whyskill ~/.claude/skills

To keep it, install it so it works from any directory:

pip install .        # from the checkout
pipx install .       # or, to keep it out of your environment

That step is required before whyskill install will register the hooks, and whyskill enforces it rather than trusting you: a hook runs from whatever project you have open, so a hook that could only import whyskill from the checkout would fail there, and Claude Code would swallow the error and carry on. Rather than write a hook that never runs and never says so, whyskill install refuses and tells you to install first.

Requires Python 3.9+. No third-party packages, at runtime or otherwise.

Not on PyPI yet, so pip install whyskill by name does not work — install from the clone as above.

Use

Once installed, drop the python3 -m prefix:

whyskill install              # let Claude check skills without being asked
whyskill                      # this project's skills, plus your personal ones
whyskill ./skills             # a directory of skills you are publishing
whyskill why deploy           # explain one skill
whyskill list                 # what was discovered, and from where
whyskill rules                # every rule, grouped

Every command also works as python3 -m whyskill … from inside the checkout, except whyskill install, for the reason above.

Useful flags:

Flag Effect
--explain Show the documented mechanic behind each finding
--target spec Also check claude.ai / Skills API field limits
--json, --sarif Machine-readable output (SARIF annotates CI diffs)
--fail-on warning Treat warnings as failures too (default: error)
--disable IDS Suppress rules, e.g. --disable INVOKE002,PORT001
--no-personal Skip ~/.claude/skillsturns off shadowing detection
--overlap N Similarity threshold for COLLIDE005 (default 0.5)

Exit code is 0 when clean, 1 when something at or above --fail-on was found, 2 for a usage error.


What it checks

Every rule below is derived from documented Claude Code behaviour, and every one of them fails silently — that is the bar for inclusion. Ordinary style opinions are out of scope.

Loading — will Claude Code read this file at all?

Rule Finding
LOAD001 The opening --- is not the file's first line
LOAD002 A UTF-8 BOM sits before the ---
LOAD003 Frontmatter is opened but never closed
LOAD004 A frontmatter line cannot be parsed and its field is dropped
LOAD005 A duplicate key silently discards the earlier value

Claude Code reads the frontmatter only when the opening --- is the file's first line. Otherwise it treats the whole file, --- markers included, as skill content.

A stray blank line, or a BOM your editor never shows you, turns your entire frontmatter into prose. The skill still appears in /skills. It just has no description, so Claude never picks it.

Listing — is there anything for Claude to route on?

Rule Finding
LIST001 No description and no body paragraph to fall back on
LIST002 No description; routing falls back to the first body paragraph
LIST003 Listing text exceeds the 1,536-character cap and is truncated
LIST004 Listing text is truncated and every trigger phrase is in the cut part
LIST005 The description says what the skill does but never when to use it
LIST006 Too few distinctive words to win a routing tie

Put the key use case first: the combined description and when_to_use text is truncated at 1,536 characters in the skill listing to reduce context usage.

LIST004 is the one worth the price of admission. Long descriptions put their examples last — "…use this when the user asks to deploy" — which is exactly the text the cap removes. The skill looks thoroughly documented and is, to the router, undescribed.

Invocation — can anything actually invoke it?

Rule Finding
INVOKE001 Both model and user invocation are disabled; nothing can run it
INVOKE002 disable-model-invocation: true — Claude can never load it on its own
INVOKE003 A paths glob matches no file in the project
INVOKE004 agent/background set without context: fork, so they do nothing
INVOKE005 effort is not one of the documented levels

INVOKE003 catches a typo that is invisible by construction: paths: src/component/**/*.tsx when the directory is components. The skill is correct, loaded, and permanently unreachable in that repository.

Collision — does it survive alongside your other skills?

These are the rules a per-file linter cannot implement.

Rule Finding
COLLIDE001 Another skill's name folds to the same value; only one loads
COLLIDE002 The name contains a look-alike or invisible character
COLLIDE003 A .claude/commands/ file is shadowed by a skill of the same name
COLLIDE004 Overrides a bundled skill, but its aliases still route elsewhere
COLLIDE005 Two skills describe themselves so alike that routing is unreliable

When skills share the same name, Claude Code resolves the conflict by source: enterprise overrides personal, and personal overrides project.

Note the direction — it is the opposite of how nearly all per-project configuration behaves. A deploy skill you wrote for one repository is silently replaced by a deploy skill you wrote months ago in ~/.claude/skills, and nothing anywhere tells you which one ran.

When it compares names, Claude Code ignores case, spacing, and invisible characters, and treats compatibility forms such as fullwidth letters and dash variants as their plain equivalents […] A name that differs only by a look-alike letter from another alphabet counts as a different name.

Those two sentences cut in opposite directions, and whyskill implements both. Deploy—App and deploy-app are the same name — one of them will not load. But review and rеview (Cyrillic е) are different names, so the override you intended never happens. Both cases are invisible in every editor.

COLLIDE005 uses no model. Similarity is an inverse-document-frequency weighted Jaccard over the listing vocabulary: words every skill uses count for almost nothing, so the score reflects the distinctive terms two skills are both claiming. It is identical on every run, which is what makes it safe to gate CI on. The report names the contested words, so the fix is obvious.

Fields — do they do what you think?

Rule Finding
PORT001 A field Claude Code does not read, so it has no effect
PORT002 A field rejected by claude.ai uploads and the Skills API (--target spec)
PORT003 compatibility exceeds its 500-character limit
PORT004 metadata is dropped or reuses a frontmatter field name

PORT001 knows the common ones by name: version, author, tools, when-to-use (the field is when_to_use), allowed_tools (it is allowed-tools). Claude Code ignores unknown fields silently, so a misspelled field behaves exactly like a missing one.

PORT002 is for publishing. claude.ai uploads, the Skills API and package_skill.py accept only six fields — name, description, license, compatibility, metadata, allowed-tools — and reject the file outright otherwise. Run --target spec before you publish.


In CI

name: skills
on: [push, pull_request]

jobs:
  whyskill:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      - run: pipx install git+https://github.com/hjalti-hub/whyskill
      - run: whyskill .claude/skills --no-personal --fail-on warning

For inline annotations on the diff, emit SARIF and upload it:

      - run: whyskill .claude/skills --no-personal --sarif --fail-on never > whyskill.sarif
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: whyskill.sarif

Pass --no-personal in CI: there is no ~/.claude/skills on a runner, and without it the shadowing rules have nothing to compare against.


Examples

examples/broken/ contains one skill per failure mode, each broken on purpose and each verified by the test suite to still demonstrate its bug:

$ whyskill examples/broken --no-personal
8 error(s) · 9 warning(s) across 15 skills

examples/good/ contains one that passes. Both are worth reading — the broken ones look completely fine.

Library use

import whyskill

skills, findings = whyskill.check(project=".")
for finding in findings:
    print(finding.rule, finding.severity.value, finding.path, finding.message)

Contributing

The bar for a new rule is specific: it must describe a failure that produces no error message, and it must be traceable to documented Claude Code behaviour. Rules encoding a preference belong in one of the style-focused linters instead.

If you add one, add its documented basis to whyskill/spec.py, a broken example under examples/broken/, and a test asserting it fires there and stays silent on examples/good/.

python3 -m unittest discover -s tests -t .

Accuracy

If a rule here contradicts Claude Code's actual behaviour, that is a bug worth reporting — the value of this tool is entirely in being right about mechanics. Behaviour also changes between versions; findings are derived from the skills documentation as of September 2026.

License

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

whyskill-0.1.0.tar.gz (65.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

whyskill-0.1.0-py3-none-any.whl (53.8 kB view details)

Uploaded Python 3

File details

Details for the file whyskill-0.1.0.tar.gz.

File metadata

  • Download URL: whyskill-0.1.0.tar.gz
  • Upload date:
  • Size: 65.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for whyskill-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3822cbc1cf498cc622de2c6e49ac52d81ac64b710306aef203ccbf49a44c3157
MD5 337b52981d25675d47a7a504368f9c7c
BLAKE2b-256 17badb334e76ec3de81999633cfaa58754244c1e5939cc57ab2f7cae42abe8e0

See more details on using hashes here.

Provenance

The following attestation bundles were made for whyskill-0.1.0.tar.gz:

Publisher: release.yml on hjalti-hub/whyskill

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file whyskill-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: whyskill-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 53.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for whyskill-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a9b5dc39645cedfea04f3ec11bb980ffeb7670017e6c9587ec5276d19c541ad1
MD5 1ed19246716312f5f48f15019aa1c751
BLAKE2b-256 3e8060a4e18984faf1a38d5b6819951aac17c865e24d743ebd63dfc1f48ac2d4

See more details on using hashes here.

Provenance

The following attestation bundles were made for whyskill-0.1.0-py3-none-any.whl:

Publisher: release.yml on hjalti-hub/whyskill

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.1.1

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page