Metadata-Version: 2.4
Name: claude-skill-lint
Version: 0.1.0
Summary: Lint your Claude Code skills: descriptions that never trigger, silent frontmatter typos, boot-token cost.
Author-email: Jeremy Renoult <jeremy.renoult.pro@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/trinity-organism/skill-lint
Project-URL: Repository, https://github.com/trinity-organism/skill-lint
Project-URL: Changelog, https://github.com/trinity-organism/skill-lint/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/trinity-organism/skill-lint/issues
Keywords: claude-code,claude,skills,lint,frontmatter,cli
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Utilities
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Dynamic: license-file

# skill-lint

[![CI](https://github.com/trinity-organism/skill-lint/actions/workflows/test.yml/badge.svg)](https://github.com/trinity-organism/skill-lint/actions/workflows/test.yml)
[![PyPI](https://img.shields.io/pypi/v/skill-lint)](https://pypi.org/project/skill-lint/)
[![Python versions](https://img.shields.io/pypi/pyversions/skill-lint)](https://pypi.org/project/skill-lint/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

A Claude Code skill lives or dies by one field: its `description`. Claude
reads the name + description of **every** skill at session start and decides
from that text alone whether your skill ever fires. A typo'd frontmatter key
is ignored **silently**. Malformed YAML loads the skill with **empty
metadata** — it will never auto-trigger and nothing tells you. A description
that says *what* but never *when* under-triggers. And every character of
every description is context you pay for at **every session**.

`skill-lint` is the linter for that one field and everything around it:
**one command, zero dependencies, tells you which skills are dead, which are
silently broken, and what your skills cost at boot.**

- **Dead skills** — malformed frontmatter, no description and nothing to
  fall back on: skills that can never trigger.
- **Silent typos** — `descripton:` instead of `description:` is ignored
  without a word; skill-lint catches it and suggests the fix.
- **Under-triggering descriptions** — too short, no usage cue, or truncated
  past the 1,536-character listing cap so Claude never sees the tail.
- **Boot cost** — chars and estimated tokens of names + descriptions loaded
  every session, per skill and total; manual-only skills
  (`disable-model-invocation: true`) correctly counted at zero.

## Install

```
pipx install skill-lint
```

or

```
pip install skill-lint
```

or straight from source:

```
pipx install git+https://github.com/trinity-organism/skill-lint
```

Python >= 3.9. Zero dependencies — standard library only.

## Usage

```
skill-lint                # lint $CLAUDE_CONFIG_DIR/skills, falling back to ~/.claude/skills
skill-lint --dir DIR      # lint any skills directory (a project's .claude/skills, a plugin's skills/)
skill-lint --json         # machine-readable output
```

Exit codes: `0` clean (or no skills found), `1` at least one finding,
`2` unreadable directory — so it drops straight into CI or a pre-commit hook.

## Example

Real output on a demo skills directory:

```text
$ skill-lint --dir skills
skill-lint — SKILL.md quality + boot cost
skills dir: skills

ok    deploy-staging  170 chars (~42 tok) at boot
WARN  meeting-notes   65 chars (~16 tok) at boot
      - WARN [no-when-cue]: description says what the skill does but never when to use it (no 'use/when/for/trigger...' cue): skills without usage context under-trigger
WARN  pdf-tools       47 chars (~11 tok) at boot
      - WARN [unknown-key]: frontmatter key 'descripton' is not a documented field and is ignored silently (did you mean 'description'?)
      - WARN [no-description]: no description in frontmatter: triggering relies on the first body paragraph, which was not written for matching
ERROR scratch         20 chars (~5 tok) at boot
      - ERROR [broken-frontmatter]: unparseable line 4 in frontmatter: malformed YAML means empty metadata, so the skill never auto-triggers

verdict — skills: 4 · errors: 1 · warnings: 3 · boot cost: 302 chars (~75 tokens) of names+descriptions every session
$ echo $?
1
```

And when there is nothing to lint:

```text
$ skill-lint --dir /some/empty/dir
skill-lint: no skills found in /some/empty/dir
Nothing to lint. If your skills live elsewhere, point at them with --dir DIR or $CLAUDE_CONFIG_DIR.
```

## Rules

Every rule traces to documented Claude Code behavior, not taste.

| Rule                 | Severity | Why it matters                                                                          |
| -------------------- | -------- | --------------------------------------------------------------------------------------- |
| `broken-frontmatter` | error    | Malformed YAML loads the skill with empty metadata: it never auto-triggers.              |
| `undiscoverable`     | error    | No description and an empty body: nothing for Claude to match, ever.                     |
| `no-description`     | warning  | Matching falls back to the first body paragraph, which wasn't written for matching.      |
| `no-frontmatter`     | warning  | Same fallback; the skill also loses every other frontmatter feature.                     |
| `desc-too-short`     | warning  | A handful of characters gives Claude nothing to match against.                           |
| `no-when-cue`        | warning  | Says *what* but never *when*: the documented under-triggering failure mode.              |
| `desc-truncated`     | warning  | `description` + `when_to_use` past 1,536 chars is cut in the listing — the tail is invisible. |
| `unknown-key`        | warning  | Unknown frontmatter fields are ignored silently; usually a typo (did-you-mean included). |
| `duplicate-key`      | warning  | Only one value survives a duplicated key.                                                |
| `empty-body`         | warning  | The skill triggers and delivers nothing.                                                 |
| `no-skill-md`        | warning  | A directory in the skills root with no `SKILL.md` is dead weight, never loaded.          |

Folded and literal YAML blocks (`description: >` / `|`) are parsed correctly —
the classic case where naive parsers read an empty description and report
garbage.

## Why lint descriptions at all?

Claude Code loads skill names and descriptions into a context budget
(about 1% of the model's context window). When it overflows, descriptions
get dropped. Skills that never trigger still bill you that budget at every
session — a config that only ever grows, with nothing pruning it. Lint it,
fix the dead weight, keep the budget for skills that fire.

Auditing the rest of your config (hooks, duplicate levers)? That's this
tool's sibling: [`bridle-audit`](https://github.com/trinity-organism/bridle-audit).

## Development

```
pip install -e ".[dev]"
pytest
```

The test suite fabricates synthetic skills in temp directories — it never
reads your real `~/.claude`.

## License

MIT
