@renovate-config-debugger/cli
v0.3.1
Published
Experimental — headless Renovate config debugger: preset tree, provenance, packageRules simulator and validation, as structured data
Maintainers
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 mcpThe 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 statusFlags
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) | full — full 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"] → groupNameThat 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 jsoncarriesruleSources(the same ranges) and, on every rule that MATCHED, an inlineorigin: {layer, sourceIndex}; pretty output appends[repo packageRules[0]]to the matched lines.rcd validateadds a line under any message whosepackageRules[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 jsonand the MCP answer always carryruleFilterwithverdict/source/total/shown/hidden, and anotesentry naming the parameters that widen the view;- pretty output ends a narrowed list with how many rules it hid;
--verdict allreturns every row,--verdict matched|no-input|no-match|errorone class, and--rule <n>one merged rule's whole row (its clauses, what it merged, and theoriginthe list carries only on matched rows) whatever the facets hide.ruleSourcesis 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 mcpEverywhere 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@secustorThe 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 bundleOther 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.
