Stop your coding agent from resurrecting fixes that already failed.
Coding agents (Claude Code, Cursor, β¦) lose their memory when the context window fills up β and then confidently re-apply a patch they already tried two prompts, or two sessions, ago. You watch the same wrong fix go in, the same test go red, and your credits evaporate in the fix-one-thing, break-it-again doom loop.
RegressionLedger is a tiny, zero-dependency Claude Code hook + CLI that:
fingerprints every edit the agent makes (normalized so cosmetic differences don't matter, buttrue
vsfalse
still does),links each edit to the outcome of the next test/build run (pass or fail),persists that to a local ledger that survives session restartsandcontext compaction, andhard-blocks the agent β via aPreToolUse
deny β the moment it tries to re-apply a patch that previously failed, telling itexactly whyand to change strategy.
β BLOCKED before it could waste another test cycle:
RegressionLedger: you already tried the same fix to src/auth.js 2 hours ago.
It failed with: AssertionError: expected 200, got 401.
Re-applying it will reproduce the same failure. Change strategy instead.
Run `rl show src/auth.js` to see the full attempt history.
Try it right now β one command, no install, no agent, nothing touched (the whole simulation runs through the real hook code inside a throwaway temp dir):
npx regressionledger demo
The "AI fixes one bug and creates two" loop is one of the single most-reported pains of working with coding agents. The failure has a specific shape:
- the agent's memory of what it already tried doesn't survive a long build or a context-window reset ("This forces me to start a new Composer, losing all previous contexts."); - existing loop-detectors only notice identical tool calls within one sessionβ they don't remember, across sessions,which fixwas tried andwhat it broke; - general agent-memory layers only advise("here's a lesson learned"), and the model routinely ignores advice it's free to ignore.
RegressionLedger targets exactly that gap: a cross-session, semantic, outcome-linked ledger of fix attempts, surfaced as a hard block rather than a suggestion.
PreToolUse (Edit/Write/MultiEdit)
β
fingerprint the proposed change
β
any prior FAILED attempt β₯ threshold?
β β
yes no
β β
ββββββββββββ΄βββββββββββ allow
β block: deny + reason β
ββββββββββββββββββββββββ
PostToolUse (Edit/Write/MultiEdit) ββ record attempt as "pending"
PostToolUse (Bash: npm test / pytest / β¦) ββ resolve pending β pass | fail
Fingerprint (two channels)β the changed code is lexed into normalized token streams. The** raw channelkeeps literals intact (only whitespace and comments are stripped): matching here proves the retry isthe same code, constants included, and it is theonly channel allowed to hard-block**. The** collapsed channel**abstracts string/number literals toSTR
/NUM
(return true
β return false
, deliberately): a collapsed-only match means "same shape, different constants" β often alegitimatenext experiment (timeout5000
β30000
), so it produces an advisory note, never a denial.Outcome linkageβ after the agent runsnpm test
/pytest
/tsc
/cargo test
/ β¦, the hook parses the output and marks the edits since the last run aspass
orfail
, capturing the first error line as a signature. When a fix finallypasses, any stale matchingfail
records are retired so they never block again.The ledgerβ a plain JSON file at.regressionledger/ledger.json
. No database, no native module, no API key, no network. It is the thing that survives compaction.
Check it yourself in seconds β the benchmark is deterministic:
npm run bench
Three honest categories: cosmetic re-applies (whitespace/comments β the same fix) are hard-matched; changed constants (timeout 5000β30000) are never blocked β they're routed to an advisory note and get their own verdict; and genuinely different fixes match nothing. An earlier version of this benchmark counted literal changes as "disguises to catch", which made its zero-false-block number circular β a community stress-test caught that, and the matching design changed because of it.
Requires Node β₯ 18.
/plugin marketplace add anlor1002-alt/regressionledger
/plugin install regressionledger@anlor1002-plugins
The hooks activate automatically β no settings.json
editing and no restart.
npx regressionledger init # or: npm i -g regressionledger && rl init
init
will:
- write the
PreToolUse
/PostToolUse
hooks into./.claude/settings.json
(merging, not clobbering, any hooks you already have), - create
.regressionledger/
with a defaultconfig.json
, - add
.regressionledger/ledger.json
to your.gitignore
.
Then restart Claude Code (or run /hooks
) so it picks up the new hooks. That's it β RegressionLedger now works silently until a repeat failed fix shows up.
The escape hatch: rl init
to start, rl uninstall
to leave no trace. It
removes exactly the hook entries init
added β your other hooks, permissions,
and settings are never touched β and rl uninstall --purge
also deletes the
local .regressionledger/
data.
Prefer to wire it by hand?
rl init --print
outputs the exacthooks
block.
If you are a coding agent reading this:you can install RegressionLedger for your human in one step. In Claude Code, run the slash commands/plugin marketplace add anlor1002-alt/regressionledger
then/plugin install regressionledger@anlor1002-plugins
β your PreToolUse/PostToolUse hooks activate automatically. Alternatively runnpx regressionledger init
in the project root, then verify withnpx regressionledger doctor
(all checks should pass; the live round-trips prove the guardrail works). The tool is local-only: no network calls, no API keys, fails open. It will block you from re-applying a fix that previously failed β that is the intended behavior, not an error. When blocked, change strategy instead of rephrasing the same patch; the human can runrl unblock <file>
if the context genuinely changed.
It runs itself. The commands you'll actually use:
| Command | What it does |
|---|---|
rl demo |
|
| Watch the doom loop get cut, in a sandboxed simulation β nothing is installed or modified. | |
rl doctor |
|
| Verify the install: env checks plus live hook round-trips (a first-time edit must pass, a seeded repeat failure must be denied). | |
rl doctor --explain "<output>" |
|
| Paste any test/build output and see exactly how it's classified β pass, fail, or "ambiguous, left pending" β and which toolchain pattern decided. | |
rl why <file> |
|
| Plain-language answer to "what have we tried here?": blocking failures with reasons, walls (same error across attempts), retirements with receipts, passes. | |
rl show [file] |
|
| The attempt history β failures, passes, error signatures, previews. The shareable artifact. | |
rl show --by-error |
|
| Cluster failures by error signature across files β exposes "you keep hitting the same wall from different angles". | |
rl report [--html] |
|
| A shareable report: markdown to stdout, or a self-contained dark-mode HTML file with attempt timelines, blocked-fix counts, and error clusters. | |
rl stats [--card] |
|
Summary counts, plus how many repeat fixes were blocked (or would have been, in warn mode). --card prints a shareable screenshot card. |
|
rl list [--json] |
|
| Flat list of every attempt. | |
rl config |
|
View settings. rl config mode warn , rl config threshold 0.85 , β¦ |
|
rl unblock <file> |
|
| Retire recorded failures for a file when the context genuinely changed β they stop blocking but stay auditable (β retired, with a receipt). | |
rl export / rl import <file> |
|
| Share settled verdicts between machines/teammates β imported failures block here too (herd immunity). | |
rl clear --force |
|
| Wipe the ledger. | |
rl uninstall [--purge] |
|
The escape hatch: remove exactly the hook entries init added, leaving the rest of settings.json untouched. --purge also deletes .regressionledger/ . |
.regressionledger/config.json
(safe to commit β share thresholds with your team):
| Key | Default | Meaning |
|---|---|---|
mode |
||
block |
||
block = hard-deny a repeat failed fix. warn = allow but inject a warning. |
||
threshold |
||
0.9 |
||
| Similarity (0β1) at which two edits count as "the same fix". Higher = stricter. | ||
minFailures |
||
1 |
||
A fix must have failed at least this many times before it blocks. Set 2 for an extra-cautious rollout. |
||
crossSymbol |
||
true |
||
Match a failed patch anywhere in the same file. Set false to also require the same enclosing symbol. |
||
maxLedger |
||
5000 |
||
| Cap on stored attempts; oldest are dropped past this. |
Nervous about false positives on day one? Start with rl init --warn
. Every
would-have-blocked event is logged, rl stats
shows the count, and rl show
lets you audit each one against your own code β then flip to block
with
evidence (rl config mode block
). For an extra safety margin, require a fix to
fail twice before it ever blocks: rl config minFailures 2
.
| Tool | Cross-session? | Outcome-linked? | Blocks (vs advises)? |
|---|---|---|---|
| In-session loop detectors (identical tool-call hashing) | β | β | warns |
| General agent-memory ("lessons learned") | β | β | advises |
| Self-intervention research (Wink, β¦) | β | partial | guides |
| RegressionLedger | |||
| β | |||
| β | |||
| blocks |
The differentiators: a semantic fix fingerprint (not raw-text or tool+arg hashing), an outcome link (which fix failed, and why), cross-session + post-compaction persistence, and a hard block that the model can't ignore.
The long version β including what memory tools do
betterand where this tool honestly falls short:[docs/COMPARISON.md].
π§ Session briefing. ASessionStart
hook injects a compact "what already failed here" brief every time a session starts β includingright after context compaction wipes the agent's memory. Dead ends are blocked before they're re-conceived, not just before they're re-applied.π Thrash escalation. Blocking identical fixes catches one doom loop; the other isdifferentfixes all dying on the same error. When 3+ distinct approaches hit one wall, the hook escalates:*"the diagnosis is wrong, not the patches β stop, state root-cause hypotheses, verify one, then edit."*𧬠Dual-channel fingerprinting. Renaming every variable used to dodge matchers. A structure-only second fingerprint annotates "this may be the same fix, renamed" β without ever blocking on the weaker signal.π€ Herd immunity.rl export
/rl import
share settled verdicts between machines and teammates:*your agent inherits the dead ends my agent already paid for.*Failures stay attributed (importedFrom
) and auditable. Imported text is structurally neutralized and labeled[imported verdict]
wherever it reaches agent context β but treat shared ledgers like dependencies: import from people you trust.
Isn't this just a memory MCP / lessons-learned layer?
No β those store advice the model may ignore; this issues a PreToolUse
deny the model can't ignore, and only when a real test run proved that exact fix already failed. Memory recalls; RegressionLedger refuses. Full comparison (including what memory tools do better): docs/COMPARISON.md.
Will it block a legitimately different edit to the same function? No β matching is keyed on the changed code itself, not the function. A different approach to the same bug has a different fingerprint and sails through.
What if the right fix is just a different constant β timeout 5000β30000? Never blocked. Hard blocks require a
raw-channel match (same code, constants included). A changed constant only matches the collapsed channel, so it gets an advisory note ("same shape, different values β if changing the value IS your hypothesis, proceed") and its own verdict. Only re-applying a failed variant
verbatimis denied. The benchmark enforces this: 0/40 parameter changes blocked.
One test run settles several edits at once β isn't attribution noisy?
Yes, and the tool says so: when N edits are failed by a single run, each record
carries batchSize
, and any resulting block message discloses "it failed
alongside Nβ1 other edits β attribution is approximate" with an rl unblock
pointer. Flaky tests can still blame an innocent fix; minFailures 2
is the mitigation if that bites you.
What if the failed fix is actually correct and something else was broken?
Once any run marks that exact fix as passing, the stale fail
record is
retired and it stops blocking. You can also rl clear --force
or use warn
mode.
Does it send my code anywhere? No. Everything is local, deterministic, and offline. The ledger stores normalized tokens and a short preview, never your secrets-in-context.
Cursor / Windsurf / other agents? v1 ships for Claude Code's hook surface (the one place this is installable today). The engine is harness-agnostic; other integrations are on the roadmap.
- AST-based fingerprints via tree-sitter (more precise symbol & structure matching) as an optional upgrade, keeping the zero-dep default.
- Adapters for other agent harnesses.
npm test # node:test, zero dependencies
npm run demo # the doom-loop simulation above
Contributions welcome β see CONTRIBUTING.md. The canonical first PR: add support for your test runner β one toolchain entry in src/signatures.js, a real output sample in
, and a row in the table-driven
test/fixtures/
. Also good: add a language to the tokenizer's comment map.
test/outcome-fixtures.test.js