A Claude Code plugin that measures the shape of a change and cites where every number comes from. Lines per file, cyclomatic and cognitive complexity, Halstead difficulty, duplication, coverage per function with CRAP, and type debt, each as a read-only audit that reads source files and existing build artifacts, runs an external collector only when one already resolves, and prints a report whose references carry their provenance. It never runs tests, never edits code, never installs a tool, and never renders a keep-or-retire verdict: a reference here is a value to count against, not a bar.
| Skill | What it does |
|---|---|
/code-metrics:audit-complexity |
Per-function cyclomatic and cognitive complexity and Halstead difficulty from whichever collector resolves (lizard, radon, ESLint rules, gocyclo, gocognit, shellmetrics, multimetric), beside the ISO/IEC 5055 §8.2.117 reference of 20 with 10 and 15 selectable; cognitive and Halstead carry no standard threshold. |
/code-metrics:audit-size |
Lines per file (total, blank, comment, code through scc; total and non-blank from a bundled counter otherwise) beside a cited reference; size.mode: iso-8.2.115 adds the ISO function-percentage form. |
/code-metrics:audit-duplication |
Clone classes (the detector's pairs merged; duplicated lines and tokens, every instance's range) from jscpd, dupl, or PMD CPD, rolled up per lane and per directory, minus the replication the repository declares in a sanctioned-replication registry (a path-within-plugin, or a canonical -> copies cluster line), which is an exclusion, not a suppression. A file over the size cap is reported as skipped, never silently dropped. |
/code-metrics:audit-coverage |
Line coverage per file and per function read from the artifacts a build already produced (lcov 1.x and 2.2, Cobertura, coverage.py JSON, Go cover profile), plus CRAP per function from the complexity rows; it never runs a test, a missing artifact is a visible warning, and a function with no executable lines reports null, never zero. |
/code-metrics:audit-type-debt |
The typed-code percentage per file and per lane: type-coverage for TypeScript, mypy's --any-exprs-report for Python; no standard or CWE anchors the measure, so the reference is null by design. C# is reported as not applicable. |
/code-metrics:principles |
Metric literacy: what each measure can and cannot tell you, where every reference value came from, CRAP's corrected provenance, the cross-metric caveats (carried once, here), and gated pointers to the plugins that own mutation score, tautological tests, dead code, coupling, and lint. |
/code-metrics:setup |
check probes the interpreter, every configuration layer, and every collector; apply writes the tracked team configuration per key, idempotently, and never installs a tool. |
Lanes are detected from file extensions (TypeScript/JavaScript, Python, Bash, Go, and C#, whose
complexity lane is deferred and reported as such). Every other text file, markdown, JSON, YAML,
PowerShell, a Makefile, lands in the catch-all other lane, which carries a line count and
nothing else: audit-size measures it, and every other measure reports it as not applicable.
When the consuming repository tracks .claude/ecosystems/<lane>.yaml files, their globs
override the bundled map for that lane. Two scopes are first-class: the default is the change,
files that differ from the merge-base with the default branch plus uncommitted and untracked
files, and --all is the whole tree (every tracked or untracked-but-not-ignored file); explicit
paths narrow either. Nothing depends on a framework, a build system, or the publisher.
- Bash 4 or later and Python 3.9 or later (
python3,python, orpy -3), plusgitfor change scope. macOS ships bash 3.2, so install a current bash (brew install bash); Windows needs Git Bash. Every entry point checks the bash version and stops with that remediation. Python is required for correctness: every entry point stops with a remediation message when it is absent. - Collectors, all optional. A lane whose collector is absent reports
unavailablewith the install hint and the run continues; nothing is installed on your behalf.
| Collector | Used for | Install |
|---|---|---|
scc |
comment-aware line counts, every lane | go install github.com/boyter/scc/v3@latest, brew install scc, or a release binary |
lizard |
cyclomatic complexity and function ranges for TypeScript/JavaScript, Python, Go | pip install lizard or pipx install lizard |
radon |
Python cyclomatic complexity, Halstead, function ranges | pip install radon |
eslint with the core complexity rule |
TypeScript/JavaScript cyclomatic complexity when ESLint is wired | npm install --save-dev eslint |
eslint-plugin-sonarjs |
TypeScript/JavaScript cognitive complexity | npm install --save-dev eslint eslint-plugin-sonarjs |
gocyclo, gocognit |
Go cyclomatic and cognitive complexity | go install github.com/fzipp/gocyclo/cmd/gocyclo@latest, go install github.com/uudashr/gocognit/cmd/gocognit@latest |
shellmetrics |
Bash cyclomatic complexity | one POSIX shell script from github.com/shellspec/shellmetrics, placed on PATH |
multimetric |
Halstead difficulty in every lane; Bash cyclomatic as a labeled approximation | pip install multimetric |
jscpd |
duplication in every lane | npm install -g jscpd, or a devDependency |
dupl |
Go duplication | go install github.com/mibk/dupl@latest |
| PMD CPD | duplication for the non-shell lanes when jscpd is absent |
the PMD 7 distribution or brew install pmd (needs a JVM) |
type-coverage |
TypeScript type coverage; needs a resolvable typescript in the project |
npm install --save-dev type-coverage typescript |
mypy |
Python Any-expression report |
pip install mypy, pipx install mypy, or uv tool install mypy |
Python has no maintained cognitive-complexity collector and Bash has none for cognitive
complexity or function ranges; those rows report unavailable with the validation date, and the
run continues.
/plugin marketplace add melodic-software/claude-code-plugins
/plugin install code-metrics@melodic-softwareThis plugin has no userConfig. Everything tunable lives in the consumer's
.claude/code-metrics.yaml, layered as user-global (~/.claude/code-metrics.yaml), team
(tracked), and local overlay (.claude/code-metrics.local.yaml, gitignored; recommended line
.claude/**/*.local.*) with per-key override, and every key has a bundled default
(scripts/config-defaults.json), so the plugin works with no configuration at all; the one
opinionated default is scope.exclude, which drops node_modules, vendor, dist, and build
directories at any depth and reports what it dropped, and a team file that sets the key replaces
the list whole. The consumer's
.claude/ecosystems/<lane>.yaml files, when tracked, override lane detection with their globs
and enabled. References ship with their provenance: cyclomatic 20 cites ISO/IEC 5055:2021
§8.2.117; the 1000-line file default is the plugin's own number and says so. Files are written in
a documented YAML subset (block style, flow sequences of scalars, no flow mappings). Every key:
reference/config.md; /code-metrics:setup writes the team layer and probes the collectors.
Every audit prints one code-metrics/v2 JSON document (--json) or its markdown rendering. The
document opens with a "Coverage of this run" table naming, per lane and measure, the collector
used or the reason none did, and a status of complete, partial, or empty, so a run that
measured nothing can never read as green. The markdown table shows each function once with every
collector's values on that line, rows over a reference first, and stops at 200 rows; every
markdown run also writes the whole document under CLAUDE_PLUGIN_DATA (else
~/.claude/plugins/data/code-metrics/reports) and names the path, so the rows past the cap need no
second run. A repository that declares its deliberate replication in a registry
(scope.registries) sees each replicated function once, with the copy count beside the path.
Measured paths are relative to the document's root. Field reference:
reference/report-schema.md. Tool provenance stamps: reference/collectors.md.
audit-coverage reads what a test run already wrote and never runs one. When it finds no
artifact, every lane is unavailable, the report lists the paths searched, and its last line
points at this table. One command per lane produces an artifact the next run can read. Each row
restates a producer's documented default, verified against the page in its Basis column on
2026-09-12; the producer's page wins over the row. Recheck trigger: a release note or changelog
entry of that producer naming the flag or the output path in its row, or a read-time fetch of the
Basis page that no longer states what the row does; either re-derives the row from the page and
refreshes the date.
| Lane | Producer | Command shape | Writes | Basis |
|---|---|---|---|---|
| TypeScript/JavaScript | vitest | npx vitest run --coverage --coverage.reporter=lcov |
lcov .info at coverage/lcov.info, auto-discovered |
Vitest coverage guide, coverage.reporter |
| TypeScript/JavaScript | jest | npx jest --coverage |
lcov .info at coverage/lcov.info, auto-discovered; the default coverageReporters list carries lcov |
Jest CLI, --coverage; Jest configuration, coverageReporters default ["clover", "json", "lcov", "text"] |
| TypeScript/JavaScript | c8 or nyc, over any test runner | npx c8 --reporter=lcov <test command> |
lcov .info at coverage/lcov.info, auto-discovered |
c8 README, --reporter, which takes any Istanbul reporter, lcov among them |
| Python | coverage.py | python -m coverage run -m pytest && python -m coverage json |
coverage.py JSON at coverage.json, auto-discovered; coverage xml writes coverage.xml, also auto-discovered; coverage lcov writes coverage.lcov, which needs --artifacts |
coverage.py command pages run, json, xml, lcov |
| Python | pytest-cov | pytest --cov=<package> --cov-report=json |
coverage.py JSON at coverage.json, coverage.py's default name for the json report, auto-discovered; --cov-report=json:<path> moves it |
pytest-cov reporting, --cov-report; the default file name is coverage.py's, per its json page |
| Bash | kcov | kcov <outdir> bash <test script> |
Cobertura-compatible XML under <outdir>; pass the cobertura.xml it writes with --artifacts |
kcov README, "Kcov will also write cobertura-compatible XML output" |
| Go | go test |
go test ./... -coverprofile=coverage.out |
Go cover profile at the path -coverprofile names; coverage.out and cover.out are auto-discovered; file rows carry the statement ratio, function rows need a line artifact too |
go command, testing flags, -coverprofile |
| C# | coverlet, as the dotnet test collector |
dotnet test --collect:"XPlat Code Coverage" |
Cobertura XML at TestResults/<run id>/coverage.cobertura.xml; pass it with --artifacts. The C# complexity lane is deferred, so the lane reports file rows and no CRAP |
coverlet VSTest integration, --collect:"XPlat Code Coverage" |
Auto-discovered means the output lands on one of the well-known names the run looks for with no
--artifacts (coverage/lcov.info, lcov.info, coverage.xml, cobertura.xml, coverage.json,
coverage.out, cover.out, at most two directory levels below the repository root); anything else
is named explicitly or listed under coverage.artifacts in the configuration.
The Python suites are the test_*.py files beside the scripts they cover, and python3 -m pytest -q
from this directory runs all of them. To measure them, run the same command under coverage.py from
this directory:
python3 -m coverage run -m pytest -q && python3 -m coverage jsonThe .coveragerc here sets source = ., so every module under the plugin is reported whether or
not a test imported it, and patch = subprocess (coverage.py 7.10 or later), so the scripts the
suites drive at their command line, join.py and the parsers among them, are measured in their
child interpreters instead of reading 0 percent. The patch leaves one data file per process;
coverage json combines them on its own from coverage.py 7.14, and an older release needs
python3 -m coverage combine between the two commands. The run writes .coverage and
coverage.json into this directory, both ignored by git, and coverage.json sits two levels
below the repository root, where /code-metrics:audit-coverage plugins/code-metrics run from the
root auto-discovers it.
Every skill description in a session shares one listing budget, and Claude Code drops the
least-invoked descriptions first when it overflows. Measured with
plugins/skill-quality/scripts/check-listing-budget.sh over every marketplace plugin's skills on
2026-09-05, this plugin adds six listing-eligible descriptions (setup is model-hidden and costs
nothing) at these estimated sizes:
| Measurement | Characters |
|---|---|
| Marketplace aggregate before this plugin | 135,541 |
| Marketplace aggregate with this plugin | 141,411 |
| This plugin alone | 5,870 |
The aggregate is an upper-bound estimate against the documented 8,000-character fallback; a
consumer who installs only this plugin sits well inside it, and /doctor reports the resolved
figure for a live session.
- Bash has no collector for cognitive complexity or function ranges, and Python none for cognitive
complexity; those rows report
unavailablewith the validation date rather than a number. - C# is counted and its duplication measured, but its complexity lane is deferred to a native collector and its type debt is reported as not applicable.
- The sentences that restate the
coverage.*defaults, the duplication defaults (inaudit-duplicationand theprinciplesmeasures reference),type_debt.reference, the cyclomatic reference, and the file-length reference are pinned toscripts/config-defaults.jsonbyscripts/check-code-metrics-skill-prose.py.
MIT (SPDX-License-Identifier: MIT).