{"id":47085769,"url":"https://github.com/retif/claudecode-linter","last_synced_at":"2026-06-13T03:05:36.241Z","repository":{"id":342256597,"uuid":"1173384678","full_name":"retif/claudecode-linter","owner":"retif","description":"Standalone linter and formatter for Claude Code plugins and configuration files — 90 rules across 8 artifact types with auto-fix","archived":false,"fork":false,"pushed_at":"2026-06-07T01:14:31.000Z","size":2254,"stargazers_count":2,"open_issues_count":5,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-07T03:42:17.385Z","etag":null,"topics":["anthropic","claude","claude-code","cli","linter","plugin","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/retif.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-05T10:01:22.000Z","updated_at":"2026-06-07T01:14:33.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/retif/claudecode-linter","commit_stats":null,"previous_names":["retif/claude-lint","retif/claudecode-linter"],"tags_count":100,"template":false,"template_full_name":null,"purl":"pkg:github/retif/claudecode-linter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/retif%2Fclaudecode-linter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/retif%2Fclaudecode-linter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/retif%2Fclaudecode-linter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/retif%2Fclaudecode-linter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/retif","download_url":"https://codeload.github.com/retif/claudecode-linter/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/retif%2Fclaudecode-linter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34270418,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-13T02:00:06.617Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["anthropic","claude","claude-code","cli","linter","plugin","typescript"],"created_at":"2026-03-12T07:36:50.839Z","updated_at":"2026-06-13T03:05:36.231Z","avatar_url":"https://github.com/retif.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# claudecode-linter\n\n[![CI](https://github.com/retif/claudecode-linter/actions/workflows/ci.yml/badge.svg)](https://github.com/retif/claudecode-linter/actions/workflows/ci.yml)\n[![npm version](https://img.shields.io/npm/v/claudecode-linter)](https://www.npmjs.com/package/claudecode-linter)\n[![license](https://img.shields.io/npm/l/claudecode-linter)](https://github.com/retif/claudecode-linter/blob/main/LICENSE)\n[![Socket Badge](https://socket.dev/api/badge/npm/package/claudecode-linter)](https://socket.dev/npm/package/claudecode-linter)\n\nStandalone linter for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) plugins and configuration files.\n\nValidates `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.\n\n![demo](assets/demo.gif)\n\n## Install\n\n```bash\nnpm install -g claudecode-linter\n```\n\nOr run directly:\n\n```bash\nnpx claudecode-linter ~/projects/my-plugin/\n```\n\nOr build it from a clone of this repository — there is no `dist/` until you build:\n\n```bash\ngit clone https://github.com/retif/claudecode-linter\ncd claudecode-linter\nnpm ci \u0026\u0026 npm run build      # install dependencies, compile to dist/\nnode dist/index.js path/to/plugin/\n```\n\nCommands elsewhere in this README are written as `claudecode-linter …` (the global / `npx` install); from a clone, that is `node dist/index.js …`.\n\n## Usage\n\n### Lint\n\nCheck plugin artifacts for errors without modifying files. This is the default mode — `--lint` is optional:\n\n```bash\n# Lint a plugin directory\nclaudecode-linter --lint path/to/plugin/\nclaudecode-linter path/to/plugin/  # same thing\n\n# Lint multiple paths\nclaudecode-linter plugin-a/ plugin-b/\n\n# JSON output\nclaudecode-linter --output json path/to/plugin/\n\n# Errors only\nclaudecode-linter --quiet path/to/plugin/\n\n# Filter by rule\nclaudecode-linter --rule plugin-json/name-kebab-case path/to/plugin/\n\n# Enable/disable specific rules\nclaudecode-linter --enable skill-md/word-count --disable claude-md/no-todos path/to/plugin/\n\n# List all available rules\nclaudecode-linter --list-rules\n```\n\n### Format\n\nReformat 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:\n\n```bash\n# Format all artifacts in place\nclaudecode-linter --format path/to/plugin/\n```\n\n### Fix\n\nFix lint violations in place, then lint the result — output shows only issues that remain after fixing:\n\n```bash\n# Fix issues in place\nclaudecode-linter --fix path/to/plugin/\n\n# Preview fixes without writing (shows diff)\nclaudecode-linter --fix-dry-run path/to/plugin/\n```\n\n### Detect\n\nPrint which Claude Code artifact types a path contains — one machine-readable\ntype per line — and set the exit code (`0` if any found, `1` if none). Intended\nfor a generic git hook that gates the linter on \"is this repo a Claude Code\nplugin?\":\n\n```bash\n# one artifact type per line\nclaudecode-linter --detect path/to/repo/\n\n# JSON array\nclaudecode-linter --detect --output json path/to/repo/\n\n# git hook: only lint repos that actually contain Claude Code artifacts\nclaudecode-linter --detect . \u003e/dev/null 2\u003e\u00261 \u0026\u0026 claudecode-linter .\n```\n\n### Example Output\n\n```\n$ claudecode-linter my-plugin/\n\nmy-plugin/skills/example/SKILL.md\n  warn   Body has 117 words (recommended: 500-5000)  skill-md/body-word-count:5\n\nmy-plugin/.claude/settings.json\n  error  \"settings.json\" should only exist at user level (~/.claude/).\n         Use \"settings.local.json\" for project-level settings  settings-json/scope-file-name\n  warn   \"env\" is a user-level field — it has no effect in\n         project-level settings.local.json  settings-json/scope-field:9:3\n\n1 error, 2 warnings\n```\n\n```\n$ claudecode-linter --fix-dry-run my-plugin/\n\n--- my-plugin/skills/deploy/SKILL.md\n+++ my-plugin/skills/deploy/SKILL.md (fixed)\n-name: My Deploy Skill\n+name: my-deploy-skill\n-description: Use when the user asks to \"deploy\": handles both cases.\n+description: \"Use when the user asks to \\\"deploy\\\": handles both cases.\"\n```\n\n```\n$ claudecode-linter my-plugin/\nNo issues found.\n```\n\n## Artifact Types\n\n| Type | Files | Rules |\n|------|-------|-------|\n| plugin-json | `.claude-plugin/plugin.json` | 13 |\n| skill-md | `skills/*/SKILL.md` | 16 |\n| agent-md | `agents/*.md` | 20 |\n| command-md | `commands/*.md` | 10 |\n| hooks-json | `hooks/hooks.json` | 9 |\n| settings-json | `.claude-plugin/settings.json` | 25 |\n| mcp-json | `.claude-plugin/mcp.json` | 16 |\n| claude-md | `CLAUDE.md` | 10 |\n| lsp-json | `.lsp.json` | 3 |\n| monitors-json | `monitors/monitors.json` | 3 |\n\n## Schema-derived rules\n\nSeven `*/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:\n\n- **Missing required fields** (e.g., LSP server without `extensionToLanguage`)\n- **Wrong field types** (e.g., `name: 42` instead of a string)\n- **Invalid enum values** (e.g., `transport: \"websocket\"` when only `stdio`/`socket` are accepted)\n- **Nested-shape violations** in discriminated unions (e.g., `mcpServers.x.type: \"telegraph\"` when only `stdio`/`sse`/`http`/... are accepted)\n- **Unknown fields** in strict objects (e.g., `filetypes`/`rootPatterns` inside an LSP server config — common confusion with editor-LSP shapes from other ecosystems)\n\nThe schemas live at `contracts/{plugin,lsp,monitors}.schema.json` and are regenerated by `npm run extract-contracts` on every Claude Code release.\n\n`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.\n\n`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).\n\n## Configuration\n\nGenerate a config file with all rules and their default severities:\n\n```bash\n# Create .claudecode-lint.yaml in current directory\nclaudecode-linter --init\n\n# Create in a specific directory\nclaudecode-linter --init ~/projects/my-plugin/\n\n# Create in home directory (applies globally)\nclaudecode-linter --init ~\n```\n\nclaudecode-linter looks for config in this order:\n\n1. `.claudecode-lint.yaml` or `.claudecode-lint.yml` in the current directory\n2. `.claudecode-lint.yaml` or `.claudecode-lint.yml` in `$HOME`\n3. Bundled defaults (all rules enabled at their default severity)\n\nExample config:\n\n```yaml\nrules:\n  plugin-json/name-kebab-case: true\n  skill-md/word-count:\n    severity: warning\n    min: 50\n  claude-md/no-todos: false\n```\n\n## Fixers\n\nBoth `--format` and `--fix` run the same fixers. The difference: `--format` only formats and reports changes, `--fix` also lints the result afterwards.\n\nFormatting is powered by [prettier](https://prettier.io/) for consistent JSON and markdown output. Custom logic handles domain-specific transformations that prettier can't (key sorting, YAML fixes, kebab-case normalization).\n\n| Artifact | Prettier | Custom logic |\n|----------|----------|--------------|\n| plugin-json | Tab-indented JSON | Canonical key ordering |\n| hooks-json | 2-space JSON | Alphabetical key sorting |\n| mcp-json | 2-space JSON | Server name sorting, canonical field ordering |\n| settings-json | 2-space JSON | Canonical key ordering, permission array sorting |\n| skill-md / agent-md / command-md | Markdown body | Frontmatter YAML normalization, kebab-case names, pre-parse quoting |\n| claude-md | Markdown | Blank line before headings |\n\n## Exit Codes\n\n| Code | Meaning |\n|------|---------|\n| 0 | No errors |\n| 1 | Lint errors found |\n| 2 | Fatal error |\n\n## Running on untrusted plugins\n\nclaudecode-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.\n\nFor **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.\n\n### The Docker image\n\nTwo 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 `:\u003cversion\u003e` tag:\n\n| Image | Built from | Notes |\n|-------|-----------|-------|\n| `ghcr.io/retif/node-claudecode-linter` | `Dockerfile` — `node:24-alpine` | default |\n| `ghcr.io/retif/bun-claudecode-linter` | `Dockerfile.compile` — `bun build --compile` single executable | smaller (~44 MB compressed) |\n\n**Pull a published image:**\n\n```bash\ndocker pull ghcr.io/retif/node-claudecode-linter   # default (node:24-alpine)\ndocker pull ghcr.io/retif/bun-claudecode-linter    # smaller (bun --compile)\n```\n\n**Or build it locally** from a checkout of this repo:\n\n```bash\ndocker build -t node-claudecode-linter .                       # default (Dockerfile)\ndocker build -f Dockerfile.compile -t bun-claudecode-linter .   # smaller variant\n```\n\nBoth 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.\n\n### Sandboxed invocation\n\n**Docker — read-only lint:**\n\n```bash\ndocker run --rm --network none --read-only --tmpfs /tmp \\\n  --user \"$(id -u):$(id -g)\" --cap-drop ALL --security-opt no-new-privileges \\\n  -v \"$PWD\":/work:ro -w /work  ghcr.io/retif/node-claudecode-linter  /work\n```\n\n**Docker — `--fix`:** the mount must be read-write so fixes can be written back. Otherwise identical, plus the `--fix` flag:\n\n```bash\ndocker run --rm --network none --read-only --tmpfs /tmp \\\n  --user \"$(id -u):$(id -g)\" --cap-drop ALL --security-opt no-new-privileges \\\n  -v \"$PWD\":/work -w /work  ghcr.io/retif/node-claudecode-linter  --fix /work\n```\n\nAll four recipes here are verified. On Linux without Docker, [bubblewrap](https://github.com/containers/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).\n\n**bwrap — read-only (lint):**\n\n```bash\nbwrap \\\n  --ro-bind / / --dev /dev --proc /proc --tmpfs /tmp \\\n  --unshare-all --die-with-parent \\\n  --chdir \"$PWD\" \\\n  claudecode-linter \"$PWD\"\n```\n\n**bwrap — read-write (`--fix`):** the later `--bind` overrides the read-only root for just the target directory:\n\n```bash\nbwrap \\\n  --ro-bind / / --bind \"$PWD\" \"$PWD\" \\\n  --dev /dev --proc /proc --tmpfs /tmp \\\n  --unshare-all --die-with-parent \\\n  --chdir \"$PWD\" \\\n  claudecode-linter --fix \"$PWD\"\n```\n\n`--ro-bind / /` can be replaced with explicit per-path `--ro-bind` entries (e.g. just `/usr`, `/nix`, and the target) for least-read-authority.\n\nSee [`SECURITY.md`](SECURITY.md) for the full security model, the audited input-handling hardening, and how to report a vulnerability.\n\n## Versioning\n\nThis linter's version tracks the Claude Code version it was extracted from:\n\n- **Contract sync**: version matches Claude Code exactly (e.g., `2.1.69` for Claude Code v2.1.69)\n- **Linter-only bugfix**: pre-release suffix `2.1.69-patch.1`, `2.1.69-patch.2`, etc.\n\nPre-release versions sort below the base version in npm (`2.1.69-patch.1 \u003c 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.\n\n## Development\n\n```bash\nnpm install\nnpm run build\nnpm test\n```\n\n### Updating contracts\n\nWhen a new Claude Code version is released:\n\n```bash\n# 1. Extract contracts from latest Claude Code\nnpm run extract-contracts\n\n# Or extract from a specific version\nnpm run extract-contracts -- --version 2.1.58\n\n# 2. Generate src/contracts.ts from the JSON\nnpm run generate-contracts\n\n# 3. Build and test\nnpm run build \u0026\u0026 npm test\n```\n\nThe `--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.\n\nUse `--changelog` to also write a `CHANGELOG_ENTRY.md` file with a markdown drift report (used by CI):\n\n```bash\nnpm run extract-contracts -- --changelog\n```\n\nThis is automated in CI via `.github/workflows/release.yml`.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fretif%2Fclaudecode-linter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fretif%2Fclaudecode-linter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fretif%2Fclaudecode-linter/lists"}