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

@renovate-config-debugger/cli

v0.3.1

Published

Experimental — headless Renovate config debugger: preset tree, provenance, packageRules simulator and validation, as structured data

Readme

@renovate-config-debugger/cli

[!WARNING] This package is experimental

rcd answers questions about a Renovate config: which presets it expands into, where each resolved option came from, which packageRules would match a given dependency, and whether Renovate would accept the file at all.

Install

Needs Node 24 or newer. There is nothing to install:

$ npx -y @renovate-config-debugger/cli digest renovate.json
$ npx -y @renovate-config-debugger/cli mcp

The examples below write rcd, which is what a global install puts on your PATH.

Usage

[!NOTE] It is recommended to use the MCP server as it cuts down the overhead if multiple calls are done for the same config

$ rcd digest renovate.json
✓ Renovate accepted this config. Your `config:recommended` entry expanded into
1,076 presets — only 7 of which set options, the rest are package-grouping
rules. Everything merged into 34 effective options, 6 of them overridden along
the way.

| command | question it answers | | ------------ | ----------------------------------------------------- | | digest | what happened in this run, in one paragraph | | validate | would Renovate refuse this config? (exit 2 if so) | | tree | which presets did the config pull in | | provenance | which preset set this option | | resolved | the merged config Renovate would run with | | simulate | which packageRules match a hypothetical dependency | | compare | did an edit change behavior | | group | which groups form from several updates | | run | the whole trace | | docs | what an option means, and where it may go | | extract | which dependencies Renovate would extract from a file | | mcp | all of the above over MCP stdio |

$ rcd validate renovate.json
$ rcd tree renovate.json --node "config:best-practices" --body resolved
$ rcd provenance renovate.json labels
$ rcd resolved renovate.json --mode full
$ rcd simulate renovate.json --dep '{"depName":"react","currentValue":"17.0.0","newValue":"18.0.0"}'
$ rcd compare before.json after.json --dep '{"depName":"react"}'
$ rcd group renovate.json --dep '{"depName":"react","updateType":"minor"}' --dep '{"depName":"react-dom","updateType":"minor"}'
$ rcd docs minimumReleaseAge
$ rcd extract package.json
$ echo '{"extends":["config:recommended"]}' | rcd run --stdin --format json --select status

Flags

Run rcd --help lists the commands and rcd <command> --help their flags. Every command except mcp takes --format <pretty|json>.

Every command that reads a config takes the same input flags. The config itself is a positional file path unless one of these replaces it.

| flag | effect | | -------------------------- | -------------------------------------------------------------------------- | | --stdin | read the config from stdin | | --file-name <name> | config file name, drives format detection (default renovate.json) | | --repo <owner/repo> | load the config from a repository instead of a file | | --ref <ref> | git ref for --repo | | --platform <name> | platform context for local> presets (default github) | | --endpoint <url> | API endpoint for the platform | | --platform-override | let --platform/--endpoint win over the global config (precedence only) | | --global-config <file> | self-hosted global config layer (JSON) | | --inherited <file> | inherited config layer (JSON) | | --inject <preset>=<file> | supply content for a preset no fetcher can reach (repeatable) | | --trust-endpoints | send host tokens even to an endpoint the config chose |

The rest belong to one command each.

| command | flag | effect | | ------------ | ---------------------------- | ----------------------------------------------------------------------- | | provenance | --rule <n> | one merged packageRule: its body, its layer, its index in that layer | | | --source <which> | scope the packageRules ranges: repo\|presets\|all | | tree | --node <name> | one preset node, by name or identity | | | --body <which> | fetched\|afterParams\|input\|resolved (needs --node) | | | --depth <n\|all> | levels to print below the root, or below --node (default 2) | | resolved | --mode <m> | full\|keep-internal (default keep-internal) | | | --include-defaults | write out Renovate's defaults too (--mode full only) | | simulate | --dep <json>, --dep-file | the dependency update to simulate | | | --verdict <which> | notable (default) |all|matched|no-input|no-match|error | | | --source <which> | which config level contributed the rule: repo\|presets\|all | | | --rule <n> | one merged rule's whole row, whatever the facets hide | | | --detail <which> | verdict (default) | fullfull adds the merge trace | | | --keys <a,b,…> | only these options of finalDependencyConfig | | | --config-scope <which> | package-rules (default) | full | | compare | --dep/--dep-file | the A-side dependency | | | --dep-b/--dep-b-file | the B-side dependency | | | --detail <which> | verdict (default) | rules | full | | | --keys <a,b,…> | only these options of the config delta | | | --config-scope <which> | package-rules (default) | full | | group | --dep <json> (repeatable) | one pending update per occurrence, at least two | | | --deps-file <file> | a JSON array of the same objects (strict JSON — see below) | | run | --select <a,b,…> | status\|errors\|warnings\|final\|events\|tree\|layers\|platform\|all | | | --keys <a,b,…> | only these options of --select final | | | --config-scope <which> | full (default) | package-rules | | docs | --search | list options whose name matches | | extract | --manager <name> | force this manager — the only door for a pattern-less manager |

What docs answers

Renovate's own option table for the pinned version, projected — nothing here is restated prose. Besides type, default, allowed values and deprecation:

| line | says | | ------------- | ------------------------------------------------------------------------------ | | placement | where the option may appear — including no restriction, which is a statement | | container | the options RESTRICTED to it; any unrestricted option may appear there too | | patterns | values are matched as globs, or regexes when written /…/; ! negates | | templating | the value may carry {{…}} template expressions | | stage | Renovate drops the option once the run passes this stage | | mergeable | preset and repo values merge rather than replace | | inheritable | may be set in the inherited config layer |

placement: no restriction comes from upstream's own code, not a guess: Renovate's validator enforces nesting only for options that declare parents, so an option without them is legal at the top level and inside any container.

Renovate ships no per-option version history — no since, no changelog in the package — so neither rcd docs nor get_option_docs can tell you when an option appeared or last changed. They answer only for the version pinned, which every header names.

Narrowing a config answer

Three commands answer with a config document, and two flags project it. Both only ever narrow, so any answer is a subset of the one you would have got without them, and every projected payload carries a configView saying which view produced it.

--config-scope package-rules drops the ~110 globalOnly options — the ones read from a self-hosted global config, which no packageRule can read or write. That is the default where the document is a PER-DEPENDENCY config (simulate, compare), because the class is provably inert there. It is not the default for rcd run --select final, which is the run's whole effective config: when you are debugging a global or inherited layer, those options are the answer.

--keys a,b selects top-level options by name, out of what the scope left. rcd simulate --detail full is the one exception: that level answers with the unprojected result and says so in its notes. A name the scope removed is not resurrected — it comes back in configView.withheld with the reason: global-only (the scope, and --config-scope full is the way to it), absent (the document does not carry that option), or — on compare, whose delta only lists keys that differ — identical (both sides carry it and nothing changed):

$ rcd simulate renovate.json --dep '{"depName":"react"}' --format json --keys groupName,onboardingConfig
{
  "finalDependencyConfig": { "groupName": "react monorepo" },
  "configView": {
    "scope": "package-rules",
    "keys": 1,
    "droppedGlobalOnly": 110,
    "withheld": [{ "key": "onboardingConfig", "reason": "global-only" }]
  }
}

On the fixture measured for this feature that call is 2.9 kB, against 24.5 kB for the default answer and 106 kB for simulate --detail full — which is unprojected by contract, so --keys does not shrink it and the answer's notes say so.

Which layer wrote which rule

packageRules is the one key Renovate CONCATENATES: every layer appends its own rules and none overrides another, so "who won" is the wrong question for it. rcd provenance <file> packageRules answers with one contiguous merged-index range per contributing layer, plus a one-line digest of each rule:

$ rcd provenance renovate.json packageRules
packageRules [appended] — 714 merged rules, concatenated: every layer appends, none overrides

  preset config:recommended — merged packageRules[0]–[712] (its own packageRules[0]–[712])
    0 matchPackageNames: ["*"] → semanticCommitType
    …
  repo — merged packageRules[713]–[713] (your packageRules[0]–[0])
    713 matchPackageNames: ["react"] → groupName

That is the arithmetic the other commands' indexes need: a rule's index inside its own layer is index - from, and for the repo range that is the packageRules[N] you wrote. --source repo keeps just your own ranges (the indexes do not move), and --rule <n> prints one merged rule's body with the layer that wrote it.

Two vocabularies name layers here, on purpose. A layer is printed by its own name — defaults, global, inherited, repo, preset <name> — wherever an answer says who wrote a value. --source (and the MCP source parameter) takes repo | presets | all, which is a CLASS of layer: presets is plural because it scopes to every preset at once, and it is never printed as the writer of anything. repo is the one word both use, and it means the same thing in both. To scope to ONE preset, drill down instead — --rule <n> names the layer that wrote a rule, and rcd tree --node reads that preset's own contribution.

The same numbers travel with the answers that quote an index:

  • rcd simulate --format json carries ruleSources (the same ranges) and, on every rule that MATCHED, an inline origin: {layer, sourceIndex}; pretty output appends [repo packageRules[0]] to the matched lines.
  • rcd validate adds a line under any message whose packageRules[N] it can cross-link — the validator cites the config as WRITTEN, the simulator the merged array, and for a config with presets those are different numbers. Nothing is annotated when the run cannot be attributed, or when the message came from a global/inherited layer's own validation.

A debugging session

@types/react is not getting the minimumReleaseAge you thought you set:

{
  "packageRules": [
    { "matchPackageNames": ["react", "react-dom"], "minimumReleaseAge": "7 days" },
    { "matchUpdateTypes": ["major"], "dependencyDashboardApproval": true }
  ]
}

First rule out a config Renovate would throw away entirely:

$ rcd validate renovate.json
✓ Renovate accepted this config.

Had it not, the exit code would be 2 and the output would quote Renovate's own message (Configuration Error: packageRules[0]: Each packageRule must contain at least one match* or exclude* selector). It passes, so ask what the rules actually do for the dependency in question:

$ rcd simulate renovate.json --dep '{"depName":"@types/react","updateType":"major"}'
This major update gets no special handling from your matched rules — the defaults apply.

1 of 2 packageRules matched — rule numbers are merged packageRules[N] indexes, and `--rule <n>` takes them verbatim.

  #1 matched (matchUpdateTypes=matched)
      sets dependencyDashboardApproval = true
1 of 2 rules hidden by --verdict notable — `--verdict all --source all` shows every rule.

Rule #0 is the missing one. --verdict no-match prints the clause that rejected it, and --rule 0 returns that one row whatever the facets hide:

$ rcd simulate renovate.json --dep '{"depName":"@types/react","updateType":"major"}' --verdict no-match
  #0 no-match (matchPackageNames=no-match)

So matchPackageNames is the culprit, not the update type.

Now suppose rule #0 had selected on something your --dep never mentioned — "matchSourceUrls": ["https://github.com/facebook/react"] instead of matchPackageNames. From the outside the run looks the same: a rule that reads a field you left unset fails CLOSED, which Renovate reports as an ordinary no-match, and every scoped view hides it. The missing-input line is what says so, and it is printed whatever --verdict you asked for:

$ rcd simulate sourceurl.json --dep '{"depName":"@types/react","updateType":"major"}'
This major update gets no special handling from your matched rules — the defaults apply.

1 of 2 packageRules matched — rule numbers are merged packageRules[N] indexes, and `--rule <n>` takes them verbatim.

  #1 matched (matchUpdateTypes=matched)
      sets dependencyDashboardApproval = true
1 of 2 rules hidden by --verdict notable — `--verdict all --source all` shows every rule.
1 of 2 rules could not match because the simulated dependency has no sourceUrl — Renovate treats a missing value as a non-match. Set sourceUrl on the dependency if you expected these rules to fire. `--verdict no-input` lists them.

--format json carries the same fact as missingInputs (rules, and one group per unset field set with its selectors, its rule count and up to five sampleRuleIndexes) plus the sentence in notes. It survives --verdict/--source and the MCP answer's size elision, because the rules it counts are exactly the rows a filter removes — and --rule <n> takes one of those sampleRuleIndexes and returns that row. Its sibling evaluationErrors counts the rules whose matcher THREW (the conda versioning scheme is the documented case: its ~3 MB WASM parser is excluded from the browser build, so matchCurrentVersion cannot be evaluated). Those rules also report a plain no-match, so they are kept in the default view on purpose — an answer the tool could not compute is not a verdict about your config.

Back to the real config: add @types/react to rule #0's matchPackageNames and save that as after.json, keep the original as before.json, and use compare as the oracle for the edit. On the dependency you were fixing it should report exactly the change you wanted:

$ rcd compare before.json after.json --dep '{"depName":"@types/react","updateType":"major"}'
Behavior differs between A and B — minimumReleaseAge (A=null by default, B="7 days"); 1 rule started matching.

Matched only in B:
  matchPackageNames

Config delta:
  minimumReleaseAge: null (default in A) → "7 days"

And on a dependency that already worked, nothing:

$ rcd compare before.json after.json --dep '{"depName":"react","updateType":"major"}'
✓ No behavioral change — the same effective config results (a rule's matchPackageNames list changed), which is expected when the edit touched the very selectors that rule matches on.

Selector text changed, same effect (rule identity, not behavior):
  matchPackageNames  #0 → #0  (clause-values-changed)

That second run is the point of the two axes. Behavior is the citable claim: the per-dependency config is identical and no rule started or stopped doing something. Rule identity goes true on edits that provably change nothing, because adding an entry to the array a rule matches on rewrites that rule's selector text — and the parenthetical says WHICH kind of rewrite it was, so clause-added never reads as a pattern replacement.

Reading the comparison JSON

--format json and the MCP compare_simulations answer carry the same object. It is ordered so the answer comes first:

| field | what it says | | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | summary | the whole verdict in one line — ${verdict}: ${netEffect} | | verdict | identical, documentation-only (only prose such as description moved), or differs | | netEffect | the words after the colon, so no consumer slices the string | | mode | which axis the caller varied: config, dependency, or unspecified | | stoppedMatching / startedMatching | BEHAVIOR: effects one side produced and the other did not | | matchedInBoth | rules paired by selector signature — --detail rules and up only | | configDelta[] | changed keys, behavioral first; kind is behavioral or documentation, a/b are the two sides, inA/inB say which side has the key | | configDelta[].aInherited / bInherited | that side's value reached the final config with NO merge step writing it — a Renovate default, not a setting the config carries | | identity.* | bookkeeping about selector TEXT, not a behavior claim: changed always, counts at --detail verdict, signatureChanges[] (each with kind and keys) plus onlyInA/onlyInB from --detail rules | | notes[] | every pointer about the answer: what the detail level withheld, and each side's own missing-input / evaluation-error line |

summary, verdict and netEffect always describe the WHOLE delta, so --keys/--config-scope narrow the view without ever moving the verdict; what the view withheld is in configView.

A few things to know when you write your own --dep. The two name fields are cross-defaulted the way Renovate's fetch worker does before packageRules run (dep.packageName ??= dep.depName), so --dep '{"depName":"react"}' matches a matchPackageNames rule instead of falling through, and the run's notes say when a field was defaulted. A clause reads no-input when the field it matches on was absent from your --dep, which is a different failure from no-match — and a rule that lost only to unset input is counted in missingInputs on both commands, per side on compare (a.missingInputs/b.missingInputs, and an A — … / B — … line in pretty output). Two sides that both went blind on the same rule agree perfectly, so identical: over them says nothing about your edit. If the input config would be refused by Renovate, simulate exits 2 and says so on its own output (exitNote in JSON, a trailing note: line in pretty). compare exits 0 whenever the comparison ran — its exit code reflects the comparison, not the inputs' validity, so a proven "no behavioral change" over a config that validate rejects is still a 0; the refusal stays a named fact on the output (wouldRefuse per side, the same note:/exitNote).

Group-level answers: rcd group

simulate is one dependency at a time by construction, so "would this group actually reach its minimumGroupSize" is not a question it can answer. group takes SEVERAL updates — --dep repeatedly, or --deps-file with a JSON array of the same objects — runs the same evaluation for each, and tallies them by the groupName their matching rules produced:

$ rcd group renovate.json \
    --dep '{"depName":"react","updateType":"minor"}' \
    --dep '{"depName":"react-dom","updateType":"minor"}' \
    --dep '{"depName":"lodash","updateType":"patch"}'
1 group over 3 simulated updates (1 update ungrouped).

  "react monorepo" would WAIT: 2 updates of the 3 its minimumGroupSize requires.
      react (minor)
      react-dom (minor)

  Ungrouped — each update gets its own PR:
      lodash (patch)

Two honesty caveats travel on every answer. The tally is over the updates YOU supplied — Renovate evaluates minimumGroupSize against the repository's real pending updates at run time, so "would wait" here means "these updates alone don't reach it", never "this group can never form". And membership is by groupName as the rules resolved it: branch splitting (separateMajorMinor, custom branchName templates) is not modeled. The MCP simulate_group tool is the same answer over a held run.

One punctuation note, since the two spellings otherwise read as the same input: an inline descriptor (--dep, --dep-file, --dep-b, --dep-b-file) is parsed as JSON5 — the superset Renovate accepts for a preset file, so unquoted keys, comments and trailing commas are fine — while a --deps-file batch is parsed as strict JSON. The entries themselves are finished identically either way, updateType derivation included.

Reading the simulate and compare output

On a config:best-practices run the rule list runs to several hundred, so BOTH output formats answer with the rules that ACTED — --verdict notable: matched, not-simulated, and the rows the tool could not evaluate. Measured on a config:recommended config plus a react major, that is 2,550 bytes of rule rows against 340,843 for the whole array, which no MCP answer can carry anyway (the transport would elide it into a first/last window chosen by byte arithmetic). Nothing is withheld silently:

  • --format json and the MCP answer always carry ruleFilter with verdict/source/total/shown/hidden, and a notes entry naming the parameters that widen the view;
  • pretty output ends a narrowed list with how many rules it hid;
  • --verdict all returns every row, --verdict matched|no-input|no-match|error one class, and --rule <n> one merged rule's whole row (its clauses, what it merged, and the origin the list carries only on matched rows) whatever the facets hide. ruleSources is the legend for those indexes.

matched is a subset of notable, so rcd simulate --format json | jq '.rules[] | select(.verdict=="matched")' returns exactly what it always did.

Both output formats lead with the outcome in one sentence — verdict.text in JSON, the first line of pretty output. It is the same string the web app's verdict card renders, so a terminal and a screenshot cannot disagree about what a config does; verdict.changedKeys are the options the rules changed, and verdict.caveat appears when one of YOUR rules failed only because --dep left a field unset.

flattened says what the update-type flattening did. blocks are the major/minor/patch/pin/digest/lockFileMaintenance/replacement blocks the config carried before flattening — Renovate's defaults declare all seven, so presence alone means nothing — and authoredBlocks are the ones a human wrote. appliedBlock is null when there was no block for this update type at all, and carries changed: [] when the block was there and contributed nothing; consumedBlocks are the authored blocks dropped WITHOUT applying, which is why an option you set may be missing from the result. note states which of those four happened, in one sentence.

simulate --format json answers at --detail verdict: mergeSteps and rawFinalConfig describe how the merge proceeded — ~1 MB on a config:recommended run — and are opt-in through --detail full, which returns the whole simulation result unprojected, exactly as it comes out of the engine. The one thing --detail full does not widen is the rule LIST: which rows you asked for is --verdict's question, at every detail level. The same flag exists as detail on the MCP simulate tool; the two transports are one implementation.

compare has its own --detail, same vocabulary on both transports. At the default verdict it answers with the claim and its evidence and states the identity axis as counts (identity.counts.onlyInA/onlyInB/signatureChanges); it omits matchedInBoth — every rule that behaved the same on both sides, in a diff — and the per-rule signature strings, each a whole selector array re-serialized next to the label that already names the rule. --detail rules restores the arrays, --detail full the comparison exactly as the engine computes it. Every level carries the same summary/verdict/netEffect.

One note-shaped field, not five: each aggregate keeps its own note inside its object (missingInputs.note, evaluationErrors.note, flattened.note), and every pointer about the answer itself — the detail level, the rule filter, the ruleSources legend, the per-side input gaps on compare — is an entry in the single top-level notes array.

One more shape both commands share: description is a mergeable array, so Renovate concatenates it on nearly every merge, and a merged diff used to re-embed all of it on both sides. An append is now stated as what it appended ({"collapsed": "append", "beforeLength": 22, "afterLength": 24, "added": […]}) — a replacement still shows both sides, and the full array is one rcd provenance renovate.json description away.

Exit codes

| code | meaning | | ---- | ------------------------------------------------------------------- | | 0 | clean, Renovate accepted the config | | 2 | Renovate would refuse this config (validation or parse failure) | | 1 | infrastructure error: bad flag, unreadable file, unfetchable preset |

2 is deliberate. Claude Code hooks read exit 2 as the blocking "feed stderr back to the model and fix it" signal, so rcd validate drops into a Stop/PreToolUse hook with no wrapper around it.

Two exceptions: compare exits 0 whenever the comparison itself ran, even over an input config Renovate would refuse — its verdict is the answer, and a 2 there read as "the comparison failed" (replay-04). The refusal is still reported on the output. And extract exits 1 when no manager section produced dependencies — a section that ran cleanly and simply found nothing counts — which is its verdict rather than anything failing to load.

Credentials

Tokens come from the environment only, never from a flag, where they would land in shell history and in every process listing:

| variable | host | | ---------------------------------------------- | ------- | | RCD_GITHUB_TOKEN, GITHUB_TOKEN, GH_TOKEN | GitHub | | RCD_GITLAB_TOKEN, GITLAB_TOKEN | GitLab | | RCD_GITEA_TOKEN | Gitea | | RCD_FORGEJO_TOKEN | Forgejo |

The npm and http preset fetchers have no auth at all, the same coverage and the same gaps as the web app.

A preset fetcher sends a host's token to whatever endpoint the platform context resolves to, and a --global-config sets that context. So when the config under inspection chooses the endpoint, the CLI withholds tokens and says so on stderr. --trust-endpoints is the way out; --platform-override only changes which of the two endpoints wins, so it releases the tokens only when you named an --endpoint of your own (or the global config sets just a platform).

MCP server

rcd mcp speaks MCP over stdio — the same answers as the subcommands, better economics for a session. It takes no arguments and writes nothing but the protocol to stdout, so point any MCP-capable client at it as a stdio server. extract is extract_deps there: the one tool besides get_option_docs that takes no runId — a filename plus the file's contents in, the dependencies Renovate would extract out, shaped to feed straight into simulate. It speaks the 2026-07-28 protocol and the legacy 2025-era initialize handshake, chosen per connection.

Adding it to an agent

The command is npx -y @renovate-config-debugger/cli mcp everywhere, or rcd mcp if the package is installed globally. Claude Code and Codex write the config for you:

$ claude mcp add rcd -- npx -y @renovate-config-debugger/cli mcp
$ codex mcp add rcd -- npx -y @renovate-config-debugger/cli mcp

Everywhere else it is a config file. Most of them take the same JSON:

{
  "mcpServers": {
    "rcd": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@renovate-config-debugger/cli", "mcp"]
    }
  }
}

| harness | file | key | | -------------------- | ---------------------------------------------------- | ------------- | | Claude Code | .mcp.json, or ~/.claude.json for user scope | mcpServers | | Cursor | .cursor/mcp.json or ~/.cursor/mcp.json | mcpServers | | Gemini CLI | .gemini/settings.json or ~/.gemini/settings.json | mcpServers | | VS Code with Copilot | .vscode/mcp.json | servers | | Codex | .codex/config.toml or ~/.codex/config.toml | mcp_servers |

In Claude Code, the plugin — hosted at the root of this repository (.claude-plugin/plugin.json plus skills/debug-renovate-config) — registers this server and adds the skill that knows the call sequence. Until the secustor/claude-marketplace catalog lists it, run it from a checkout of this repository instead:

$ claude --plugin-dir .

Once listed:

$ /plugin marketplace add secustor/claude-marketplace
$ /plugin install renovate-config-debugger@secustor

The server holds a small number of recent runs (an LRU), so an agent can compare the run before an edit with the run after it. A runId that has been evicted says so, and lists the ones still held.

Working on the CLI itself

Every config above runs the published bundle. In a checkout of this repository that is the wrong one: it answers from the last release while your src/ differs. bin/rcd-dev.mjs is the same module graph served straight from src/ — no build step between an edit and the next answer — but the repo's .mcp.json cannot name it, because that file doubles as the published plugin's server config.

Claude Code takes server definitions only from .mcp.json (project scope) and ~/.claude.json (local and user scope), never from settings.json, so the override is a one-time command rather than a file in the repo. A local-scope entry shadows the project one for this project alone:

$ claude mcp add -s local rcd -- node packages/cli/bin/rcd-dev.mjs mcp
$ claude mcp remove -s local rcd   # back to the published bundle

Other harnesses take the same swap in their own config file: replace npx -y @renovate-config-debugger/cli mcp with node <repo>/packages/cli/bin/rcd-dev.mjs mcp. For one-off questions, pnpm --filter @renovate-config-debugger/cli rcd <subcommand> already runs the dev bin and needs no configuration at all.

Compatibility

Every release states the Renovate it carries. The engine and its renovate graph are inlined at build time, so a given CLI version always answers with exactly this Renovate and nothing resolves at install time. rcd --version prints both, and every published version states the same facts in a renovateCompatibility manifest field, keyed by full package name:

$ pnpm view @renovate-config-debugger/cli renovateCompatibility

| @renovate-config-debugger/cli | @renovate-config-debugger/engine | renovate | | ------------------------------- | ---------------------------------- | ---------- | | 0.3.1 | 0.1.0 | 44.82.4 | | 0.3.0 | 0.1.0 | 44.42.1 | | 0.2.0 | 0.1.0 | 44.39.1 | | 0.1.0 | 0.1.0 | 44.30.0 |

A new row is not a promise that the previous row's flags still work. scripts/stamp-compat.ts writes the field and the table while publishing, and scripts/check-compat.ts fails the build when a stamped claim stops describing it — no hand writes either, and nothing is committed, so nothing can go stale.

License

AGPL-3.0-only, like the rest of this project.