Skip to content
ghostship25Public

About

**Lint and auto-fix `SKILL.md` files for AI coding agents** — Claude Code, Cursor, Windsurf, Aider, Continue, and anything else that routes on skill/rule frontmatter.

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

7 Commits

Folders and files

Repository files navigation

SkillLint

CI npm version node zero dependencies license PRs welcome

ESLint, but for the SKILL.md files your AI agent reads. Catches the vague triggers, missing fields, and dangerous shell commands that quietly make Claude Code / Cursor / Windsurf / Aider skills unreliable — then fixes most of them for you.

npx skilllint .claude/skills/

SkillLint finding and fixing issues in a terminal

No install, no config, no API key — point it at a directory and read the report. If you'd rather see it before running it:

$ skilllint .claude/skills/

skills/pdf-table-extractor/SKILL.md   100/100 (A)
  ✔ no issues found

skills/general-helper/SKILL.md         55/100 (F)
  ✖ File has no YAML frontmatter block (--- or +++ delimited). [required-fields]
  ⚠ "use-when" reads as an overly broad trigger ("always"). [broad-triggers]
  ⚠ Body is very short (12 words). [body-structure]

──────────────────────────────────────────────
Files linted: 2   Average score: 78/100
1 errors   2 warnings   0 info
Fail threshold: 70/100 — 1 file(s) below it
──────────────────────────────────────────────

Why

Skill files are prompts, not just docs — a vague or overly broad use-when degrades routing for every skill in the set, not just the sloppy one. As teams accumulate dozens of skills across Claude Code, Cursor rules, and other agent tools, SkillLint gives you the same kind of guardrail ESLint gives JavaScript: a fast, CI-friendly check that catches the mistakes that are easy to make and easy to overlook in review.

Install

npm install --save-dev skilllint
# or run without installing:
npx skilllint .

SkillLint has zero runtime dependencies — the whole tool, including its YAML frontmatter parser, is pure Node.js. Requires Node 18+.

Usage

skilllint [paths...]              # lint files/directories (default: .)
skilllint --fix [paths...]        # auto-fix issues in place
skilllint --fix --dry-run [paths] # preview fixes without writing files
skilllint --check [paths...]      # exit 1 on errors / low scores — for CI
skilllint --json [paths...]       # machine-readable report
skilllint --init                  # write a starter .skilllintrc.json

Common flags:

Flag Description
--fix Apply automatic fixes and overwrite files
--dry-run With --fix, show what would change without writing
--check Exit code 1 if any file has errors or scores below the fail threshold
--json Print a machine-readable JSON report
--quiet, -q Only print the summary line
--config <path> Use a specific config file
--ignore-rule <id> Disable a rule for this run (repeatable)
--fail-threshold <n> Override the configured fail threshold (0–100)
--file-name <name> Override which filename to look for (default SKILL.md)
--spec <name> Also enforce an external spec (currently agentskills.io)
--no-delegates Disable shelling out to shellcheck/markdownlint even if installed
--max-fallbacks <n> With --check, fail if more than n delegate checks fell back to heuristics

You can point SkillLint at a single file or a whole directory tree — it recursively finds every SKILL.md (or your configured filename). Linting a path with zero matching files is treated as a configuration mistake, not a clean pass: it exits with code 3 and a loud message rather than silently reporting a perfect score.

What it checks

Rule What it catches
required-fields Missing frontmatter block, missing name / description / use-when
use-when-quality Trigger text that's too short, vague ("use when needed"), or lacks any concrete signal (file types, verbs, quoted phrases)
broad-triggers Triggers like "always", "any request", "everything" that will fire on nearly anything and crowd out more specific skills
security Dangerous patterns in the body (eval(), curl | sh, rm -rf /, chmod 777, disabled TLS verification, etc.) and likely hardcoded secrets (API keys, tokens, private keys) — optionally deepened by shellcheck on embedded shell blocks, see Delegated deep linting
body-structure Empty or extremely short/long bodies, empty sections, unclosed code fences, missing headings, unreadable wall-of-text paragraphs — optionally deepened by markdownlint, see below
duplicate-skills (cross-file) Two skills with the same or near-identical name, or trigger text that overlaps so much the router can't tell them apart
spec-conformance (opt-in, --spec agentskills.io) Non-kebab-case names, name not matching its directory, oversized descriptions, unrecognized frontmatter fields, bodies over 500 lines

Every issue includes a severity (error / warning / info), a rule ID you can reference in config, and — where useful — a concrete suggestion for fixing it. Scores start at 100 and are reduced per issue found; see src/score.js for the exact weighting.

Spec conformance (agentskills.io)

The open agentskills.io standard is accepted by Claude Code, Cursor, Gemini CLI, Codex, and others, and adds a few structural requirements SkillLint doesn't enforce by default (name must be kebab-case and match its directory, description length bounds, no unrecognized frontmatter fields). Turn it on explicitly:

skilllint --spec agentskills.io ./skills

It's opt-in rather than default because "name must equal directory name" is a real constraint some setups (Cursor rules, single-file skill collections) don't follow. --fix --spec agentskills.io will safely rename a mismatched name field to match its directory when doing so is unambiguous.

Delegated deep linting

SkillLint's built-in security and body-structure rules are zero-dependency regex heuristics — fast and dependency-free, but they can't parse real shell syntax or catch every structural Markdown issue. When shellcheck and/or markdownlint (from markdownlint-cli) are already installed on your machine or CI runner, SkillLint will automatically shell out to them for extra precision:

  • shellcheck parses ```bash / ```sh code fences in the body for real shell-injection and quoting issues the regex patterns in security.js can't catch.
  • markdownlint checks body structure (heading levels, duplicate headings, code fences missing a language) — filtered to a curated, routing-relevant rule set, not its full prose-style ruleset.

Neither is a hard dependency: if a tool isn't found, SkillLint silently falls back to its own heuristics, which already ran regardless. Disable delegation entirely with --no-delegates for fully reproducible, dependency-free runs. In CI, use --max-fallbacks <n> to fail the build if more than n delegate checks silently degraded to heuristics — useful if your pipeline is supposed to have these tools installed and you want to know when it doesn't:

skilllint --check --max-fallbacks 0 ./skills   # fail loud if shellcheck/markdownlint are missing

The summary line reports which delegates were used vs. unavailable:

Delegates — shellcheck: used; markdownlint: used

Auto-fix

Auto-fix is now rule-driven: each rule that can safely repair its own issue exposes a fix(), and the runner re-lints after every pass to a fixpoint, so --fix never contradicts what the linter itself would flag. skilllint --fix will:

  • Add a missing frontmatter block (--- by default) if one doesn't exist
  • Fill in a missing name (derived from the file/folder path) and description (derived from the first paragraph of the body)
  • Rewrite a use-when that's missing, too short, or uses vague phrasing, generated from verbs, file extensions, and headings found in the body — flagged with a TODO: marker so you know to review and sharpen it
  • Remove empty sections (a heading with nothing under it) and collapse excess blank lines

Every fix is verified: SkillLint re-lints the result and refuses to write a change that would leave the file's score lower than before. If a proposed fix would regress the file, it's skipped and reported instead of silently applied — you'll see (before → after) scores and a remaining fixable-issue count for every file --fix touches.

Auto-fix never invents claims about what your skill does beyond what's already in the body — generated text is a starting point, always marked with a TODO, and meant to be reviewed before you ship it.

skilllint --fix --dry-run ./skills   # see what would change first
skilllint --fix ./skills             # then apply it

Configuration

Run skilllint --init to generate a starter .skilllintrc.json, or write skilllint.config.js exporting the same shape:

{
  "ignore": ["broad-triggers"],
  "severity": { "body-structure": "warning" },
  "failThreshold": 75,
  "fileName": "SKILL.md",
  "ignorePaths": ["node_modules", ".git", "dist", "build"]
}
  • ignore — rule IDs to skip entirely.
  • severity — override a rule's severity (error / warning / info) without disabling it.
  • failThreshold — minimum score --check requires to pass.
  • fileName — change what filename SkillLint looks for when given a directory (e.g. .cursorrules.md, AGENT.md).
  • ignorePaths — directory names to skip while walking.

Config is discovered by walking up from the target directory, the same way .eslintrc resolution works, so a config at your repo root applies to every skill underneath it.

CI integration

# .github/workflows/skilllint.yml
- run: npx skilllint --check .

See .github/workflows/skilllint.yml for a complete example that also uploads a JSON report as a build artifact.

Frontmatter parsing

SkillLint ships small, dependency-free parsers purpose-built for the frontmatter shapes actually used in agent-skill files: a YAML-subset parser for --- blocks (scalars, quoted strings, inline [a, b, c] arrays, block lists, nested maps, block scalars |/>), and a TOML-lite parser for +++ blocks (flat key = value assignments, quoted strings, arrays). +++ content is sniffed for which shape it actually has — some tools use +++ with YAML content anyway — rather than assuming TOML from the fence alone. Neither is a full spec implementation; that's a deliberate scope match for what skill frontmatter actually contains, not a shortcut.

Parser honesty: if either parser hits a line it can't confidently read — unexpected indentation, no recognizable key: value / key = value shape — it does not silently drop that line and move on. It's reported as a required-fields error naming the exact line and reason, because a silent drop means the parsed object is missing data a human actually wrote into the file, and a router relying on that data would never know. As a consequence, --fix refuses to modify a file with unparseable frontmatter lines rather than risk "cleaning up" a file by discarding content it couldn't read — fix the flagged line by hand first, then re-run --fix.

Example skills

See examples/good for skills that score 100/100, and examples/bad for skills demonstrating each rule failure (missing frontmatter, vague triggers, overly broad triggers, and security smells).

Development

See CONTRIBUTING.md for the full guide, including a template for adding a new rule. Quick start:

git clone https://github.com/your-org/skilllint.git
cd skilllint
npm install
npm test              # runs Node's built-in test runner
npm run lint:self      # lints the bundled example skills

Rules live in src/rules/, each as a small self-contained module with a check(context) function returning an array of issues. Adding a rule is as simple as dropping a new file in src/rules/ and registering it in src/rules/index.js.

Robustness notes

A few things SkillLint deliberately guards against, since they came up during hardening and are easy to regress:

  • --fix preserves the original line-ending style (CRLF vs LF) and correctly escapes embedded newlines/tabs/quotes when re-serializing a frontmatter value — it never silently corrupts a field it didn't mean to touch.
  • Delegate subprocess calls (shellcheck/markdownlint) have a timeout so a hung or misbehaving binary can't stall an entire lint run.
  • Word-count checks work across scripts that don't use spaces (CJK, Japanese, Korean) via a script-aware estimator, not a raw .split(/\s+/).
  • Duplicate frontmatter keys are reported, not silently overwritten.
  • Symlinked skill directories are followed, with cycle detection so a symlink loop can't hang the file walk.

See test/robustness.test.js for the regression coverage on all of these.

License

MIT

About

**Lint and auto-fix `SKILL.md` files for AI coding agents** — Claude Code, Cursor, Windsurf, Aider, Continue, and anything else that routes on skill/rule frontmatter.

Resources

Contributing

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages