routeledger v0.3.0 · MIT · npm · source

Which model actually answered?

Claude Code can change the model underneath you. A fallback fires, a plan-mode boundary is crossed, an alias is remapped in your environment. The work continues, the answers keep coming, and nothing says a word. Your transcript recorded all of it. routeledger reads it back.

$ npx routeledger
Model routing trace of one real Claude Code session Top channel: the model that served each of 11,398 turns across sixteen days, stepping between Fable 5 and Opus 4.8 nine times. A dashed box marks a twenty-minute window near the end, and two lines fan out from it down to the middle channel, which is that same window magnified. There the routing flips three times — at 15:32 and at 15:52 with no recorded cause, and at 15:50 following a /model command. Bottom channel: the 22 subagent runs of the same session, ordered by id, with no time axis; thirteen are solid because a declared model existed to compare against, nine are hatched because nothing was declared. SESSION — 11,398 turns, 10–26 Jul 2026 fable 5 opus 4.8 DETAIL — the boxed window, magnified: 19 Jul, 15:32 to 15:52 fable 5 opus 4.8 15:32 no recorded cause 15:50 after /model 15:52 no recorded cause SUBAGENTS — 22 runs, no time axis Model routing trace of one real Claude Code session The same trace as the wide chart, laid out for a narrow screen. In both channels the upper line is Fable 5 and the lower line is Opus 4.8: 11,398 turns stepping between them nine times, then the boxed twenty-minute window on 19 July shown magnified, holding three of those flips, two of which have no recorded cause. Below them, 22 subagent runs of which nine had no declared model. SESSION — 11,398 turns upper: fable 5 · lower: opus 4.8 DETAIL — magnified 15:32 15:50 15:52 two of the three: no cause SUBAGENTS — 22 runs
model that served the turn change with no recorded cause declared model to compare against nothing declared — unverifiable

One real session, audited with routeledger: 11,398 turns over sixteen days, spanning five Claude Code versions. The model changed nine times. Seven of those changes follow a /model command in the transcript. Two do not, and both sit inside the same twenty minutes, where the routing flips three times — which is why that window is drawn again, magnified. The bottom channel has no time axis: it is the 22 subagent runs of that session, ordered by id.

Three answers, and one of them is
“I can’t tell you”

Most checkers have two states: fine, or broken. That grammar lies the moment evidence goes missing, because “nothing wrong here” and “nothing here to check with” come out looking identical. routeledger keeps them apart.

ok
A model was declared, a model answered, and they are the same family. The only case where the tool is entitled to say nothing is wrong.
observation
Something changed and the transcript does not record why. Reported as a hypothesis, with the reason it cannot be more than one. A mid-session settings edit and a resume leave no trace in the file. Absence of a cause in the record is not absence of a cause.
unverifiable
The evidence needed is not in the file. No verdict is issued, and none is implied. This is a real answer, not a failure to produce one. It appears often — nine times in the session above.

An auditor that is confidently wrong is worse than no auditor.

What it reads

Three checks, run against files already sitting under ~/.claude/projects. Each one states its own limit, because a limit you have to discover later is a defect.

Declared model versus served model

Limit. When a subagent’s meta.json declares no model, there is nothing to compare against — so the finding is unverifiable, not “fine”.
[UNVERIFIABLE] agent-a90c8e85777de7dde (general-purpose)
                no declared model; served:
                claude-fable-5, claude-opus-4-8

Every subagent run leaves a record of the model it was asked for, next to a transcript of the model that answered. routeledger joins the two. That one run was served by two different models — a fact that appears nowhere else.

Model changes inside a session

Limit. “No user action found” is an inference, and every report of it says so on the same line.
[OBSERVATION ] unexplained model change
                claude-fable-5 -> claude-opus-4-8
                @ 2026-07-19T15:32:10.329Z
                inference: no user action found; but a
                mid-session settings.json edit and a
                resume are not recorded in the
                transcript, so this is a hypothesis

It walks the served model across the session and separates the changes you asked for from the changes you did not, by looking for a preceding /model command.

Plan-mode profile

Limit. The configuration in force at session time is not recorded anywhere, so this is an observation and never a judgement.
[OBSERVATION ] plan-mode profile
                plan: claude-fable-5 | execution:
                claude-fable-5, claude-opus-4-8
                inference: claude-fable-5 served turns
                on both sides of the plan-mode
                boundary; if you expected a plan-mode
                upgrade, it may never have fired. […]

Which models served plan-mode turns, and which served the rest. The two sets are not identical here, but they overlap — Fable 5 answered on both sides, so the plan-mode boundary did not separate the models at all. A checker comparing the sets for equality would call that fine. This one does not.

Output above is verbatim, from the session drawn at the top of this page. Long lines are wrapped to fit the column, and one trailing sentence is elided at […]; nothing else is changed.

It stops asserting on a version it has not seen

The Claude Code transcript format is undocumented and it moves. While building this, a single transcript turned up spanning five CLI versions, and a record type present in one session was simply absent in another.

So routeledger records the version behind every session it reads. On a version newer than the last one it was verified against — or when no version was recorded at all — it withdraws. Every finding drops to unverifiable, the claim it would have made is kept visible in the detail line, and the models-served table is relabelled a raw, unverified count.

  ! This session is newer than the version routeledger
    was verified on (2.1.220).
    Findings are treated as unverifiable; the format
    may have changed.

  MODELS SERVED (raw count — format unverified)
    claude-opus-5                     1 turn
    claude-fable-5                    1 turn

  FINDINGS
    [UNVERIFIABLE] unexplained model change
                    claude-opus-5 -> claude-fable-5
                    (version fence: unverified version,
                    no "observation" claim made)

Real output, from the test fixture that holds this path open: a transcript recording Claude Code 9.9.9. The withdrawn word stays in the report, so you can see exactly which claim was not made.

Absence of evidence closes the fence too. If no version was recorded, routeledger does not assume it is safe — it assumes it cannot tell.

What it will not do

  • no networkEver. There is nothing to configure and nothing to opt out of.
  • writes nothingNo files, no cache, no telemetry. Read-only by construction.
  • changes nothingIt cannot switch models or touch your configuration.
  • reports no contentYour prompts and Claude’s replies are never printed, stored, or transmitted.

One narrow exception, stated here because “it does not read your content” would be false. To tell a model change you requested from one you did not, the second check scans user text in memory for the /model command marker.

That text never enters a finding and never leaves the process. The scanned value is passed to one regular expression and discarded.

Running it

Node 20 or newer, and transcripts under ~/.claude/projects. No install, no configuration, no account.

routeledger

Audits the most recent session for the current directory, falling back to the newest session anywhere — and saying which one it picked.

routeledger <session-id>

Audits a specific session. A prefix is enough.

routeledger --sessions

Lists recent sessions, newest first, so you can pick an id.

routeledger --json

The same report as machine-readable JSON, always with every finding included.

Nearby, and honestly

  • ccusage

    What your sessions cost, broken down by model. A different question, and the one most people need first.

  • fableplan

    Sets up a Fable-plans, Opus-executes routing. routeledger checks whether the routing you set up is the one you got.

Findings describe one session. There is no history, no policy file, and no CI-gate mode in this version — a finding never changes the exit code.