test-plugin — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited test-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.
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 /te-review, an LLM-powered deep audit skill that goes beyond structural validation — analyzing token efficiency, redundancy, output constraints, and instruction quality. It produces a scored assessment (0-24) with estimated token savings per fix.
claude-skill-lint catches structural bugs deterministically in CI. /te-review is an on-demand complement for deeper optimization.
curl -o ~/.claude/commands/te-review.md https://raw.githubusercontent.com/shinytoyrobots/claude-skills-linter/main/skills/te-review.md$ 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 (plugin) | name, description | invocable, argument-hint, user-invocable, allowed-tools, context, agent, effort, hooks, 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.
Validates marketplace.json and plugin.json structure, source path resolution, name consistency, and missing skill files.
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 | 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. Four 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 | .claude-plugin/marketplace.json at root |
| multi-plugin | plugins/{name}/skills/{skill}/SKILL.md | Plugin subdirectories with .claude-plugin/plugin.json |
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.
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-skillsignore 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 annotationsclaude-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 |
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
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';See the package exports for the full API surface.
See GitHub Releases for version history and release notes.
MIT
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.