eslint-plugin-claude
An ESLint plugin that lints Claude Code configuration: CLAUDE.md, skills, subagents, hooks, settings, permissions, MCP servers, and plugin manifests.
It checks each of these against the Claude Code docs. It runs on
@eslint/markdown and
@eslint/json.
Scope
These limits apply to each rule:
- The Claude Code docs are the source. Each rule comes from a page of the Claude Code docs. Its rule doc links to each page and heading that it uses. A practice that only other documentation gives is not a rule.
- A rule reports a measurable fault. A rule reports a condition that code can find in the files. Examples are a length, a field value or a path that is not there. A rule does not judge style or quality. A check that needs a model judgment is not a rule. A convention of one organization is not a rule.
- A rule checks the repository only. The plugin checks the Claude Code files that a git
repository holds. A rule reads no file out of the repository. It does not read user,
enterprise or other configuration out of the repository, such as
~/.claude/. A rule option can name what the repository cannot see. - An official CLI tool does not already make the check. A check that an official Claude Code
CLI tool makes on the repository files is not a rule. An example is
claude plugin validate. A CLI check is quantitative, as the rules of this plugin are. A session tool, such as/doctor, can make qualitative checks, so it does not count. A rule can report the cases that the tool does not check. When the tool starts to make the check, the rule is deprecated, and the next major release removes it.
ADR 001 (Decisions 2 and 14) records these decisions.
How rules stay current
A daily job watches the docs. It compares each docs page that a rule cites with a stored snapshot. For a changed block, it opens or updates an issue. For a moved or renamed section, it opens an issue to update the links of each rule doc. A person decides each issue with the docs watch runbook. ADR 002 records this decision.
Install
pnpm add -D eslint @eslint/markdown @eslint/json eslint-plugin-claude
Requires ESLint 10, @eslint/markdown 8, @eslint/json 2 and Node ^22.13.0 || >=24. The two
language plugins are peers, so your project and this plugin use the same copy.
Usage
// eslint.config.js
import { defineConfig } from 'eslint/config'
import claude from 'eslint-plugin-claude'
export default defineConfig([{ plugins: { claude }, extends: ['claude/recommended'] }])
Each rule brings its own files glob and language, and a rule can add a second language and
glob. The config registers markdown and json, so you need no other setup. Rule IDs take the form claude/<rule>.
You can change the severity or the options of a rule. Put the rule in rules, in the block
that extends the config:
export default defineConfig([
{
plugins: { claude },
extends: ['claude/recommended'],
rules: {
'claude/agent-body-nonempty': 'warn',
'claude/skill-description-max-length': ['warn', { listingMax: 1000 }],
},
},
])
The doc of each rule shows its entry.
Change the files of a rule
Each config is an array of blocks. The block of a rule has the name claude/<config>/<rule>, such
as claude/recommended/command-legacy-format. The two blocks that give the files of each language
have the names claude/<config>/markdown-files and claude/<config>/json-files. These names are
stable from version 1.0.0. A rule that is off in a config has no block of its own there. The
block of its language gives its files. A glob that matches each file of a language, such as
**/*.md, is not in that block. To change the files of a rule, find its block by the name, and
replace files:
// eslint.config.js
import { defineConfig } from 'eslint/config'
import claude from 'eslint-plugin-claude'
export default defineConfig(
claude.configs.recommended.map((block) =>
block.name === 'claude/recommended/command-legacy-format'
? { ...block, files: ['packages/app/.claude/commands/**/*.md'] }
: block,
),
)
A rule that reads two languages has two blocks with the same name. Compare block.language too
('markdown/gfm' or 'json/json') to change only one of them. A rule also checks the place of a
file in code, so a wider glob can give no new report. The plugin has no setting and no rule option
for files.
Configs
Each config turns on rules whose glob is **/*.md, such as the agent rules. So each config sets
markdown/gfm with YAML frontmatter on each .md file. That replaces a language that an earlier
block sets for those files. The agent rules have this glob because a plugin manifest can name an
agent file at any path in the plugin. Each rule finds its files in code. To narrow the files of
a rule, follow Change the files of a rule.
Rules
The rules are in groups by the type of file that they check. The groups follow the rule inventory.
The list marks each rule that has options. The doc of each rule gives each option, and its default if it has one.
💼 Configurations enabled in.
⚠️ Configurations set to warn in.
✅ Set in the recommended configuration.
🔒 Set in the strict configuration.
⚙️ Has configuration options.