npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

claudecode-linter

v2.1.177

Published

Standalone linter for Claude Code plugins and configuration files

Readme

claudecode-linter

CI npm version license Socket Badge

Standalone linter for Claude Code plugins and configuration files.

Validates plugin.json, SKILL.md, agent/command markdown, hooks.json, mcp.json, settings.json, CLAUDE.md, .lsp.json, and monitors/monitors.json files. plugin.json, settings.json, the agent/skill/command frontmatter, .lsp.json and monitors.json are checked against JSON Schemas auto-extracted from Claude Code's runtime Zod validators — failures the linter reports are the same failures Claude Code would raise at session start.

demo

Install

npm install -g claudecode-linter

Or run directly:

npx claudecode-linter ~/projects/my-plugin/

Or build it from a clone of this repository — there is no dist/ until you build:

git clone https://github.com/retif/claudecode-linter
cd claudecode-linter
npm ci && npm run build      # install dependencies, compile to dist/
node dist/index.js path/to/plugin/

Commands elsewhere in this README are written as claudecode-linter … (the global / npx install); from a clone, that is node dist/index.js ….

Usage

Lint

Check plugin artifacts for errors without modifying files. This is the default mode — --lint is optional:

# Lint a plugin directory
claudecode-linter --lint path/to/plugin/
claudecode-linter path/to/plugin/  # same thing

# Lint multiple paths
claudecode-linter plugin-a/ plugin-b/

# JSON output
claudecode-linter --output json path/to/plugin/

# Errors only
claudecode-linter --quiet path/to/plugin/

# Filter by rule
claudecode-linter --rule plugin-json/name-kebab-case path/to/plugin/

# Enable/disable specific rules
claudecode-linter --enable skill-md/word-count --disable claude-md/no-todos path/to/plugin/

# List all available rules
claudecode-linter --list-rules

Format

Reformat all artifacts for consistent style (sorted keys, normalized indentation, trailing whitespace, kebab-case names, quoted YAML values). No lint output — just formats and reports what changed:

# Format all artifacts in place
claudecode-linter --format path/to/plugin/

Fix

Fix lint violations in place, then lint the result — output shows only issues that remain after fixing:

# Fix issues in place
claudecode-linter --fix path/to/plugin/

# Preview fixes without writing (shows diff)
claudecode-linter --fix-dry-run path/to/plugin/

Detect

Print which Claude Code artifact types a path contains — one machine-readable type per line — and set the exit code (0 if any found, 1 if none). Intended for a generic git hook that gates the linter on "is this repo a Claude Code plugin?":

# one artifact type per line
claudecode-linter --detect path/to/repo/

# JSON array
claudecode-linter --detect --output json path/to/repo/

# git hook: only lint repos that actually contain Claude Code artifacts
claudecode-linter --detect . >/dev/null 2>&1 && claudecode-linter .

Example Output

$ claudecode-linter my-plugin/

my-plugin/skills/example/SKILL.md
  warn   Body has 117 words (recommended: 500-5000)  skill-md/body-word-count:5

my-plugin/.claude/settings.json
  error  "settings.json" should only exist at user level (~/.claude/).
         Use "settings.local.json" for project-level settings  settings-json/scope-file-name
  warn   "env" is a user-level field — it has no effect in
         project-level settings.local.json  settings-json/scope-field:9:3

1 error, 2 warnings
$ claudecode-linter --fix-dry-run my-plugin/

--- my-plugin/skills/deploy/SKILL.md
+++ my-plugin/skills/deploy/SKILL.md (fixed)
-name: My Deploy Skill
+name: my-deploy-skill
-description: Use when the user asks to "deploy": handles both cases.
+description: "Use when the user asks to \"deploy\": handles both cases."
$ claudecode-linter my-plugin/
No issues found.

Artifact Types

| Type | Files | Rules | |------|-------|-------| | plugin-json | .claude-plugin/plugin.json | 13 | | skill-md | skills/*/SKILL.md | 16 | | agent-md | agents/*.md | 20 | | command-md | commands/*.md | 10 | | hooks-json | hooks/hooks.json | 9 | | settings-json | .claude-plugin/settings.json | 25 | | mcp-json | .claude-plugin/mcp.json | 16 | | claude-md | CLAUDE.md | 10 | | lsp-json | .lsp.json | 3 | | monitors-json | monitors/monitors.json | 3 |

Schema-derived rules

Seven */schema-valid rules — for plugin-json, settings-json, skill-md, agent-md, command-md, lsp-json and monitors-json — validate against JSON Schemas that are auto-extracted from Claude Code's cli.js bundle — the same Zod schemas the runtime calls .safeParse(content) on. This catches:

  • Missing required fields (e.g., LSP server without extensionToLanguage)
  • Wrong field types (e.g., name: 42 instead of a string)
  • Invalid enum values (e.g., transport: "websocket" when only stdio/socket are accepted)
  • Nested-shape violations in discriminated unions (e.g., mcpServers.x.type: "telegraph" when only stdio/sse/http/... are accepted)
  • Unknown fields in strict objects (e.g., filetypes/rootPatterns inside an LSP server config — common confusion with editor-LSP shapes from other ecosystems)

The schemas live at contracts/{plugin,lsp,monitors}.schema.json and are regenerated by npm run extract-contracts on every Claude Code release.

lsp-json/no-lsp-servers-wrapper catches a specific authoring mistake: putting your .lsp.json content under a top-level lspServers key. That wrapper belongs inline in plugin.json only — the dedicated file is a flat map of server-name → config.

monitors-json/unique-names enforces Claude Code's "monitor names must be unique within a plugin" check (a refine() predicate that JSON Schema can't express natively).

Configuration

Generate a config file with all rules and their default severities:

# Create .claudecode-lint.yaml in current directory
claudecode-linter --init

# Create in a specific directory
claudecode-linter --init ~/projects/my-plugin/

# Create in home directory (applies globally)
claudecode-linter --init ~

claudecode-linter looks for config in this order:

  1. .claudecode-lint.yaml or .claudecode-lint.yml in the current directory
  2. .claudecode-lint.yaml or .claudecode-lint.yml in $HOME
  3. Bundled defaults (all rules enabled at their default severity)

Example config:

rules:
  plugin-json/name-kebab-case: true
  skill-md/word-count:
    severity: warning
    min: 50
  claude-md/no-todos: false

Fixers

Both --format and --fix run the same fixers. The difference: --format only formats and reports changes, --fix also lints the result afterwards.

Formatting is powered by prettier for consistent JSON and markdown output. Custom logic handles domain-specific transformations that prettier can't (key sorting, YAML fixes, kebab-case normalization).

| Artifact | Prettier | Custom logic | |----------|----------|--------------| | plugin-json | Tab-indented JSON | Canonical key ordering | | hooks-json | 2-space JSON | Alphabetical key sorting | | mcp-json | 2-space JSON | Server name sorting, canonical field ordering | | settings-json | 2-space JSON | Canonical key ordering, permission array sorting | | skill-md / agent-md / command-md | Markdown body | Frontmatter YAML normalization, kebab-case names, pre-parse quoting | | claude-md | Markdown | Blank line before headings |

Exit Codes

| Code | Meaning | |------|---------| | 0 | No errors | | 1 | Lint errors found | | 2 | Fatal error |

Running on untrusted plugins

claudecode-linter is a static analyzer — it parses and validates the artifacts it inspects, it never executes them. There is no eval, no child_process, no declared hooks are run, and no MCP servers are spawned. Linting trusted code needs no special isolation.

For untrusted plugins — especially with --fix, which writes files back to disk — run the linter sandboxed. claudecode-linter is verified to run correctly fully confined: no network, a read-only root filesystem, all Linux capabilities dropped, no-new-privileges, a non-root UID, and only the target directory mounted.

The Docker image

Two multi-arch (linux/amd64, linux/arm64) images are published to the GitHub Container Registry — two separate packages, each with its own :latest rolling tag and :<version> tag:

| Image | Built from | Notes | |-------|-----------|-------| | ghcr.io/retif/node-claudecode-linter | Dockerfilenode:24-alpine | default | | ghcr.io/retif/bun-claudecode-linter | Dockerfile.compilebun build --compile single executable | smaller (~44 MB compressed) |

Pull a published image:

docker pull ghcr.io/retif/node-claudecode-linter   # default (node:24-alpine)
docker pull ghcr.io/retif/bun-claudecode-linter    # smaller (bun --compile)

Or build it locally from a checkout of this repo:

docker build -t node-claudecode-linter .                       # default (Dockerfile)
docker build -f Dockerfile.compile -t bun-claudecode-linter .   # smaller variant

Both images behave identically. The docker run recipes below use ghcr.io/retif/node-claudecode-linter; substitute ghcr.io/retif/bun-claudecode-linter or a locally-built tag as you prefer.

Sandboxed invocation

Docker — read-only lint:

docker run --rm --network none --read-only --tmpfs /tmp \
  --user "$(id -u):$(id -g)" --cap-drop ALL --security-opt no-new-privileges \
  -v "$PWD":/work:ro -w /work  ghcr.io/retif/node-claudecode-linter  /work

Docker — --fix: the mount must be read-write so fixes can be written back. Otherwise identical, plus the --fix flag:

docker run --rm --network none --read-only --tmpfs /tmp \
  --user "$(id -u):$(id -g)" --cap-drop ALL --security-opt no-new-privileges \
  -v "$PWD":/work -w /work  ghcr.io/retif/node-claudecode-linter  --fix /work

All four recipes here are verified. On Linux without Docker, bubblewrap (bwrap) gives the equivalent boundary: --unshare-all cuts network (confirmed: ECONNREFUSED inside the sandbox), and nothing is writable except — for --fix — the target directory (confirmed: a write outside it is refused).

bwrap — read-only (lint):

bwrap \
  --ro-bind / / --dev /dev --proc /proc --tmpfs /tmp \
  --unshare-all --die-with-parent \
  --chdir "$PWD" \
  claudecode-linter "$PWD"

bwrap — read-write (--fix): the later --bind overrides the read-only root for just the target directory:

bwrap \
  --ro-bind / / --bind "$PWD" "$PWD" \
  --dev /dev --proc /proc --tmpfs /tmp \
  --unshare-all --die-with-parent \
  --chdir "$PWD" \
  claudecode-linter --fix "$PWD"

--ro-bind / / can be replaced with explicit per-path --ro-bind entries (e.g. just /usr, /nix, and the target) for least-read-authority.

See SECURITY.md for the full security model, the audited input-handling hardening, and how to report a vulnerability.

Versioning

This linter's version tracks the Claude Code version it was extracted from:

  • Contract sync: version matches Claude Code exactly (e.g., 2.1.69 for Claude Code v2.1.69)
  • Linter-only bugfix: pre-release suffix 2.1.69-patch.1, 2.1.69-patch.2, etc.

Pre-release versions sort below the base version in npm (2.1.69-patch.1 < 2.1.69), but ^2.1.68 will still resolve them. When the next Claude Code version is released (e.g., 2.1.70), it supersedes all patches.

Development

npm install
npm run build
npm test

Updating contracts

When a new Claude Code version is released:

# 1. Extract contracts from latest Claude Code
npm run extract-contracts

# Or extract from a specific version
npm run extract-contracts -- --version 2.1.58

# 2. Generate src/contracts.ts from the JSON
npm run generate-contracts

# 3. Build and test
npm run build && npm test

The --version flag is useful for testing the CI pipeline: extract an older version, commit it, then let CI detect the newer latest version and run the full release flow.

Use --changelog to also write a CHANGELOG_ENTRY.md file with a markdown drift report (used by CI):

npm run extract-contracts -- --changelog

This is automated in CI via .github/workflows/release.yml.

License

MIT