plugin — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited plugin (Plugin) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Structural validation and token-efficiency tooling for Claude Code skills. Catches the bugs that Claude tolerates but your users pay for.
Jump to: Installation · Quick Start · What It Checks · Repository Formats · Configuration · CI Integration · Changelog
Claude Code skills load into the context window on every invocation. When a skill references a context file that doesn't exist, Claude doesn't throw an error. It hallucinates the missing context and keeps going. When two skills resolve to the same install path, one silently overwrites the other. When a context file is loaded but nothing references it, you're burning tokens for nothing.
These aren't hypothetical failure modes. They're what we found running claude-skill-lint against a real 139-file production suite:
Caught in under two seconds. No LLM calls. Deterministic.
"Tolerate" is not "work correctly."
This repo also includes /sk-review, an LLM-powered deep audit skill that goes beyond structural validation — analyzing signal density, cache-stability, progressive disclosure, instruction quality, and model/effort routing. It produces a scored assessment (0-24) with a prioritized, leverage-ranked optimization plan.
claude-skill-lint catches structural bugs deterministically in CI. /sk-review is an on-demand complement for deeper optimization.
It ships as a standard Agent Skill directory (SKILL.md plus references/). Install it as a user skill:
base=https://raw.githubusercontent.com/shinytoyrobots/claude-skills-linter/main/skills/sk-review
mkdir -p ~/.claude/skills/sk-review/references
curl -fsSo ~/.claude/skills/sk-review/SKILL.md "$base/SKILL.md"
for f in suite-mode opus-5-5-checks report-template; do
curl -fsSo ~/.claude/skills/sk-review/references/$f.md "$base/references/$f.md"
done$ claude-skill-lint graph .
✖ 43 errors and 11 warnings in 54 files (139 files checked)The headline: inv-output-conventions.md was renamed to inv-output-patterns.md at some point. Nine invention skills still referenced the old name. They silently got no output formatting guidance. A rename bug, invisible at authoring time, caught in seconds.
$ claude-skill-lint graph ~/Development/anthropic-skills/
✖ 14 errors and 24 warnings in 37 files (72 files checked)Name collisions in the claude-api skill — five language-specific claude-api.md files (PHP, Java, Ruby, Go, C#) all resolve to the same canonical name, plus Python/TypeScript duplicates for tool-use.md, streaming.md, files-api.md, and batches.md. One genuinely broken ./README.md reference. Orphaned theme files in theme-factory (loaded dynamically, not via static references) and shared context files referenced only from other context files. All legitimate structural findings.
$ claude-skill-lint lint ~/Development/work/ai-plugins/
✖ 3 warnings in 3 files (45 files checked)Three unlisted plugins (valid plugin.json but not declared in the root marketplace.json). Graph validation: clean — zero errors across 45 files. The multi-plugin format's relative path resolution (../../context/foo.md) works correctly.
The lint pass found 34 type errors across the same 139-file suite — argument-hint values parsed as YAML arrays instead of strings, tools fields as comma-separated strings instead of arrays.
Claude Code tolerates all of them. Skills work fine. The linter is being opinionated about structure, enforcing that frontmatter conforms to a schema. That matters when you're sharing skills, publishing to a marketplace, or building tooling that expects consistent types. For personal skills that just work, graph validation is where the real value lives.
The 4 YAML parse errors, on the other hand, are genuinely broken. The frontmatter can't be read at all.
npm install -g claude-skill-lintOr directly:
npx claude-skill-lint lint .Requires Node.js 20+.
claude-skill-lint init # Auto-detect format, generate config
claude-skill-lint graph . # Cross-file references — the high-value bugs
claude-skill-lint lint . # Frontmatter structureBuilds a dependency graph from cross-file references. Finds:
Resolves both installed paths (~/.claude/commands/context/foo.md) and relative paths (../../context/foo.md, ./reference/guide.md, agents/scanner.md), with automatic fallback between resolution strategies.
Validates YAML frontmatter against file-type schemas:
| File Type | Required Fields | Optional Fields |
|---|---|---|
| Command | description | model, allowed-tools, argument-hint, context, agent, effort, hooks, compatibility, metadata |
| Agent | name, description | model, tools, context, agent, effort, hooks, compatibility, metadata |
| Skill | name, description | when_to_use, argument-hint, arguments, disable-model-invocation, user-invocable, background, model, context, agent, effort, hooks, shell, paths, allowed-tools, disallowed-tools, license, compatibility, metadata |
| Context | (none) | — |
#### Modern Frontmatter Fields
These fields are supported across all file types (command, agent, skill):
| Field | Type | Description |
|---|---|---|
context | string | Execution context for the skill (e.g. fork to run in a separate process) |
agent | string | Agent mode or name to delegate execution to |
effort | string | Reasoning effort level — controls how much thinking the model applies |
hooks | object | Lifecycle hooks triggered before/after skill execution |
compatibility | string | Compatibility requirements or version constraints |
metadata | object | Arbitrary key-value metadata for tooling and marketplace use |
allowed-tools | array or string | Tools the skill can use. Supports glob patterns like mcp__* and Bash(*) for broad matching, or specific tool names for fine-grained control |
At Level 1: model enum validation, known tool verification (including Bash(python*) pattern syntax), tool-to-body consistency, file size limits, effort value validation, skill name format and length (64 characters max).
Skills also get two warnings that explain how Claude Code will actually treat them:
description + when_to_use over 1,536 characters. Claude scans that combined text to decide when to auto-invoke; the excess is wasted or truncated.name differs from the skill's directory. The message says which identifier governs invocation in the detected repo format.#### Portable mode
Claude Code accepts more than the open Agent Skills spec does. --portable flags what won't survive an upload to claude.ai or the Skills API:
name, description, license, compatibility, metadata, allowed-tools.anthropic or claude, which those upload paths reject. Claude Code itself loads them, so this is off unless you ask for portability.Validates marketplace.json and plugin.json structure, source path resolution, name consistency, and missing skill files. A .claude-plugin/ directory may contain only plugin.json and marketplace.json (claude-plugin-contents): a stray skills/ or commands/ there silently breaks plugin loading.
The plugin and marketplace schema (reserved plugin names, MCP server paths, insecure URLs, dependency sources) is owned by Anthropic's official validator, claude plugin validate, and is deliberately not reimplemented here. Run it alongside the linter, or let the linter run it for you and merge the findings into one report:
claude-skill-lint lint . --run-plugin-validate # requires the Claude Code CLI on PATH; plugin repos onlyIf the CLI is missing or returns something unparseable, the run fails with a claude-plugin-validate-unavailable error rather than silently skipping the check.
Skills mature. The quality bar should mature with them.
| Level | What It Adds | When |
|---|---|---|
| 0 | Valid YAML, required fields, non-empty body | New skills, prototyping |
| 1 | Model enum, known tools, tool-in-body check, file size limits, skill name format and length | Established skills, shared suites |
Declare per file:
---
name: my-skill
description: Does something useful
quality_level: 1
---Or set directory defaults in .skill-lint.yaml:
default_level: 0
levels:
commands/: 1
agents/: 1Effective level: max(file declaration, directory default, --level flag). The highest value wins. You can raise the floor but never lower a file's declared level.
claude-skill-lint lint . --ratchet --base origin/mainCompares each file's quality_level against the base branch. If any level decreased, the build fails. Quality improvements become permanent. That's the point.
claude-skill-lint auto-detects your repository structure. Five formats are supported:
| Format | Structure | Detection Signal |
|---|---|---|
| legacy-commands | commands/, agents/, context/ at repo root | No .claude-plugin/ directory |
| project-skills | .claude/skills/{name}/SKILL.md | .claude/skills/ with SKILL.md files |
| plugin | skills/{name}/SKILL.md with marketplace manifest, or a single plugin with a root SKILL.md | .claude-plugin/marketplace.json at root, or .claude-plugin/plugin.json plus a root SKILL.md / skills/ |
| multi-plugin | plugins/{name}/skills/{skill}/SKILL.md | Plugin subdirectories with .claude-plugin/plugin.json |
| suite-monorepo | {suite}/commands/*.md and {suite}/skills/{name}/SKILL.md, side by side | Never detected; set format: suite-monorepo |
Detection priority: config override > multi-plugin > plugin > project-skills > legacy-commands. The first match wins.
.claude/skills/ FormatThe project-skills format uses .claude/skills/{name}/SKILL.md — the same structure Claude Code uses for project-scoped skills. Each skill lives in its own directory under .claude/skills/.
claude-skill-lint discovers skills in nested .claude/skills/ directories automatically. In monorepo setups where multiple packages each have their own .claude/skills/ directory, point the linter at the repo root and it finds them all.
Hybrid repos work too. A repo with both .claude/skills/ and legacy commands/ directories — or a published plugin that also has project-level skills — gets everything linted in a single run. No configuration needed.
For repos transitioning from legacy commands to modern skills, claude-skill-lint validates both locations in a single run. Set the format explicitly in .skill-lint.yaml if auto-detection picks the wrong one, or omit it and let detection handle the transition — legacy-commands is the fallback when no modern format signals are found.
SKILL.mdFor a repo of suites moving from {suite}/commands/{name}.md to {suite}/skills/{name}/SKILL.md one skill at a time, where installed context and agents are also moving from ~/.claude/commands/ to ~/.claude/. Auto-detection can't identify this mid-migration, so set it explicitly. Add **/.claude/** to ignore if .claude/skills/ holds symlinks to the same skills.
~/.claude/commands/context/foo.md, ~/.claude/context/foo.md and ~/.claude/skills/{name}/SKILL.md all resolve.skills/{name}/references/ belong to that skill. Another skill can't reference them, and same-named files in two skills don't collide. They need no frontmatter, and one with no reader in its skill is reported as orphaned.SKILL.md resolves references/foo.md, ${CLAUDE_SKILL_DIR}/references/foo.md, ./foo.md and markdown links against its own directory. A path that leaves the directory is broken, because it won't resolve once installed. Commands get none of this, so their relative links behave as they do in legacy-commands.SKILL.md files count as referrers for orphan detection.name must equal the directory at every level (name-dir-mismatch is an error here).Not every repo follows a standard layout. claude-skill-lint provides three configuration levers for non-standard structures:
skills_rootIf your skill files live in a subdirectory rather than the repo root:
skills_root: "packages/my-plugin"All path resolution starts from this root. Useful for monorepos where skills are nested deep.
format OverrideAuto-detection works for standard layouts. When it doesn't — or when your repo is mid-migration between formats — set the format explicitly:
format: plugin # Force plugin format detection
# format: legacy-commands | plugin | multi-plugin | project-skills | suite-monorepoignore PatternsExclude paths that look like skills but aren't:
ignore:
- "**/README.md"
- "**/CLAUDE.md"
- "node_modules/**"
- "docs/**/*.md"
- "archive/**"Glob patterns, matched against file paths relative to skills_root. node_modules/ is always excluded.
claude-skill-lint graph [paths...]claude-skill-lint graph . # Full graph analysis
claude-skill-lint graph . --format json # JSON output
claude-skill-lint graph . --format github # GitHub annotations
claude-skill-lint graph . --strict # Orphan warnings become errorsclaude-skill-lint lint [paths...]claude-skill-lint lint . # Lint everything
claude-skill-lint lint . --level 1 # Enforce Level 1
claude-skill-lint lint . --strict # Warnings become errors
claude-skill-lint lint . --ratchet # Prevent quality regression
claude-skill-lint lint . --changed-only --base origin/main # Only changed files
claude-skill-lint lint . --format json # JSON for tooling
claude-skill-lint lint . --format github # GitHub Actions annotations
claude-skill-lint lint . --portable # Flag Claude-Code-only fields and names that won't upload to claude.ai / Skills API
claude-skill-lint lint . --run-plugin-validate # Also run `claude plugin validate` (plugin repos)claude-skill-lint initclaude-skill-lint init # Auto-detect format, generate config
claude-skill-lint init --force # Overwrite existingExit codes: 0 clean, 1 errors found, 2 config error.
| Option | Default | Description |
|---|---|---|
--level / -l | 0 | Minimum quality level (0-3) |
--changed-only | false | Only check files changed since base ref |
--base | origin/main | Git ref for --changed-only and --ratchet |
--format / -f | terminal | Output: terminal, json, github |
--strict | false | Treat warnings as errors |
--ratchet | false | Fail if any quality_level decreased vs base |
--portable | false | Flag Claude-Code-only frontmatter fields and reserved-word skill names (anthropic, claude) that the Skills API / claude.ai reject |
--run-plugin-validate | false | Also run claude plugin validate and merge its findings (plugin repos; needs the Claude Code CLI) |
claude-skill-lint init generates this. Or create .skill-lint.yaml manually:
skills_root: "."
default_level: 0
levels:
commands/: 1
agents/: 1
# Auto-detected if omitted
# format: legacy-commands | plugin | multi-plugin | project-skills | suite-monorepo
models: [opus, sonnet, haiku]
tools:
mcp_pattern: "mcp__*"
custom: []
limits:
max_file_size: 15360
ignore:
- "**/README.md"
- "**/CLAUDE.md"
- "node_modules/**"
graph:
warn_orphans: true
detect_cycles: true
detect_duplicates: true| Code | Meaning |
|---|---|
0 | Clean — no errors (warnings don't fail by default) |
1 | Errors found (or warnings with --strict) |
2 | Configuration or git error |
By default, warnings are informational — orphaned files and minor issues appear as annotations but don't fail the build. Add --strict to treat warnings as errors and fail on any finding.
name: Skill Lint
on:
pull_request:
paths: ['**/*.md', '.skill-lint.yaml']
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm install -g claude-skill-lint
- name: Graph validation
run: claude-skill-lint graph . --format github
- name: Lint changed skills
run: claude-skill-lint lint . --format github --changed-only --base origin/${{ github.base_ref }}
- name: Quality ratchet
run: claude-skill-lint lint . --ratchet --base origin/${{ github.base_ref }} --format githubTo fail on warnings too, add --strict to any step:
- name: Graph validation (strict)
run: claude-skill-lint graph . --format github --strict--format github produces annotations that appear inline on PR diffs.
#!/bin/sh
STAGED=$(git diff --cached --name-only --diff-filter=ACM -- '*.md')
if [ -n "$STAGED" ]; then
npx claude-skill-lint lint $STAGED --level 1
fiAll core functions are exported for integration into custom tooling:
import { runLint, runGraph, loadConfig, validateFrontmatter, extractFile } from 'claude-skill-lint';runPluginValidate(rootDir) wraps the official claude plugin validate and returns findings in the same shape as the rest. See the package exports for the full API surface.
Every release is documented in CHANGELOG.md, grouped by what changed and why it matters — not by commit. For the same history with npm publish timestamps, see GitHub Releases.
MIT
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.