{"slug": "show-hn-a-hook-that-stops-claude-code-from-re-trying-fixes-that-failed", "title": "Show HN: A hook that stops Claude Code from re-trying fixes that failed", "summary": "A developer released RegressionLedger, a zero-dependency hook for Claude Code that fingerprints code edits and blocks agents from re-applying fixes that previously failed, solving the common problem of coding agents repeating failed patches across sessions.", "body_md": "Stop your coding agent from resurrecting fixes that already failed.\n\nCoding agents (Claude Code, Cursor, …) lose their memory when the context window\nfills up — and then confidently re-apply a patch they already tried two prompts,\nor two sessions, ago. You watch the same wrong fix go in, the same test go red,\nand your credits evaporate in the *fix-one-thing, break-it-again* doom loop.\n\nRegressionLedger is a tiny, **zero-dependency** Claude Code hook + CLI that:\n\n**fingerprints** every edit the agent makes (normalized so cosmetic differences don't matter, but`true`\n\nvs`false`\n\nstill 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 restarts*and*context compaction, and**hard-blocks** the agent — via a`PreToolUse`\n\ndeny — the moment it tries to re-apply a patch that previously failed, telling it*exactly why*and to change strategy.\n\n```\n⛔ BLOCKED before it could waste another test cycle:\n\n   RegressionLedger: you already tried the same fix to src/auth.js 2 hours ago.\n   It failed with: AssertionError: expected 200, got 401.\n   Re-applying it will reproduce the same failure. Change strategy instead.\n   Run `rl show src/auth.js` to see the full attempt history.\n```\n\nTry 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):\n\n```\nnpx regressionledger demo\n```\n\nThe \"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:\n\n- the agent's\n**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\n**identical tool calls within one session**— they don't remember, across sessions,*which fix*was tried and*what it broke*; - general agent-memory layers only\n**advise**(\"here's a lesson learned\"), and the model routinely ignores advice it's free to ignore.\n\nRegressionLedger targets exactly that gap: a **cross-session, semantic,\noutcome-linked** ledger of fix attempts, surfaced as a **hard block** rather than\na suggestion.\n\n```\n            PreToolUse (Edit/Write/MultiEdit)\n                        │\n       fingerprint the proposed change\n                        │\n        any prior FAILED attempt ≥ threshold?\n              │                       │\n            yes                      no\n              │                       │\n   ┌──────────┴──────────┐         allow\n   │ block: deny + reason │\n   └──────────────────────┘\n\n        PostToolUse (Edit/Write/MultiEdit)  ── record attempt as \"pending\"\n        PostToolUse (Bash: npm test / pytest / …) ── resolve pending → pass | fail\n```\n\n**Fingerprint (two channels)**— the changed code is lexed into normalized token streams. The** raw channel**keeps literals intact (only whitespace and comments are stripped): matching here proves the retry is*the same code, constants included*, and it is the**only channel allowed to hard-block**. The** collapsed channel**abstracts string/number literals to`STR`\n\n/`NUM`\n\n(`return true`\n\n≠`return false`\n\n, deliberately): a collapsed-only match means \"same shape, different constants\" — often a*legitimate*next experiment (timeout`5000`\n\n→`30000`\n\n), so it produces an advisory note, never a denial.**Outcome linkage**— after the agent runs`npm test`\n\n/`pytest`\n\n/`tsc`\n\n/`cargo test`\n\n/ …, the hook parses the output and marks the edits since the last run as`pass`\n\nor`fail`\n\n, capturing the first error line as a signature. When a fix finally**passes**, any stale matching`fail`\n\nrecords are retired so they never block again.**The ledger**— a plain JSON file at`.regressionledger/ledger.json`\n\n. No database, no native module, no API key, no network. It is the thing that survives compaction.\n\nCheck it yourself in seconds — the benchmark is deterministic:\n\n```\nnpm run bench\n# 1. Cosmetic re-applies HARD-matched (raw)   : 60/60   (100.0%)\n# 2. Literal variants routed to note-not-block : 40/40   (100.0%)\n#    ...wrongly hard-blocked (false positives) : 0/40\n# 3. Distinct fixes matching either channel    : 0/190   (0.0%)\n```\n\nThree honest categories: cosmetic re-applies (whitespace/comments — the same\nfix) are hard-matched; **changed constants (timeout 5000→30000) are never\nblocked** — they're routed to an advisory note and get their own verdict; and\ngenuinely different fixes match nothing. An earlier version of this benchmark\ncounted literal changes as \"disguises to catch\", which made its zero-false-block\nnumber circular — a community stress-test caught that, and the matching design\nchanged because of it.\n\nRequires Node ≥ 18.\n\n```\n/plugin marketplace add anlor1002-alt/regressionledger\n/plugin install regressionledger@anlor1002-plugins\n```\n\nThe hooks activate automatically — no `settings.json`\n\nediting and no restart.\n\n```\n# inside your project\nnpx regressionledger init      # or: npm i -g regressionledger && rl init\n```\n\n`init`\n\nwill:\n\n- write the\n`PreToolUse`\n\n/`PostToolUse`\n\nhooks into`./.claude/settings.json`\n\n(merging, not clobbering, any hooks you already have), - create\n`.regressionledger/`\n\nwith a default`config.json`\n\n, - add\n`.regressionledger/ledger.json`\n\nto your`.gitignore`\n\n.\n\nThen restart Claude Code (or run `/hooks`\n\n) so it picks up the new hooks. That's\nit — RegressionLedger now works silently until a repeat failed fix shows up.\n\n**The escape hatch:** `rl init`\n\nto start, `rl uninstall`\n\nto leave no trace. It\nremoves exactly the hook entries `init`\n\nadded — your other hooks, permissions,\nand settings are never touched — and `rl uninstall --purge`\n\nalso deletes the\nlocal `.regressionledger/`\n\ndata.\n\nPrefer to wire it by hand?\n\n`rl init --print`\n\noutputs the exact`hooks`\n\nblock.\n\nIf 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`\n\nthen`/plugin install regressionledger@anlor1002-plugins`\n\n— your PreToolUse/PostToolUse hooks activate automatically. Alternatively run`npx regressionledger init`\n\nin the project root, then verify with`npx regressionledger doctor`\n\n(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 run`rl unblock <file>`\n\nif the context genuinely changed.\n\nIt runs itself. The commands you'll actually use:\n\n| Command | What it does |\n|---|---|\n`rl demo` |\nWatch the doom loop get cut, in a sandboxed simulation — nothing is installed or modified. |\n`rl doctor` |\nVerify the install: env checks plus live hook round-trips (a first-time edit must pass, a seeded repeat failure must be denied). |\n`rl doctor --explain \"<output>\"` |\nPaste any test/build output and see exactly how it's classified — pass, fail, or \"ambiguous, left pending\" — and which toolchain pattern decided. |\n`rl why <file>` |\nPlain-language answer to \"what have we tried here?\": blocking failures with reasons, walls (same error across attempts), retirements with receipts, passes. |\n`rl show [file]` |\nThe attempt history — failures, passes, error signatures, previews. The shareable artifact. |\n`rl show --by-error` |\nCluster failures by error signature across files — exposes \"you keep hitting the same wall from different angles\". |\n`rl report [--html]` |\nA shareable report: markdown to stdout, or a self-contained dark-mode HTML file with attempt timelines, blocked-fix counts, and error clusters. |\n`rl stats [--card]` |\nSummary counts, plus how many repeat fixes were blocked (or would have been, in warn mode). `--card` prints a shareable screenshot card. |\n`rl list [--json]` |\nFlat list of every attempt. |\n`rl config` |\nView settings. `rl config mode warn` , `rl config threshold 0.85` , … |\n`rl unblock <file>` |\nRetire recorded failures for a file when the context genuinely changed — they stop blocking but stay auditable (∅ retired, with a receipt). |\n`rl export` / `rl import <file>` |\nShare settled verdicts between machines/teammates — imported failures block here too (herd immunity). |\n`rl clear --force` |\nWipe the ledger. |\n`rl uninstall [--purge]` |\nThe escape hatch: remove exactly the hook entries `init` added, leaving the rest of `settings.json` untouched. `--purge` also deletes `.regressionledger/` . |\n\n`.regressionledger/config.json`\n\n(safe to commit — share thresholds with your team):\n\n| Key | Default | Meaning |\n|---|---|---|\n`mode` |\n`block` |\n`block` = hard-deny a repeat failed fix. `warn` = allow but inject a warning. |\n`threshold` |\n`0.9` |\nSimilarity (0–1) at which two edits count as \"the same fix\". Higher = stricter. |\n`minFailures` |\n`1` |\nA fix must have failed at least this many times before it blocks. Set `2` for an extra-cautious rollout. |\n`crossSymbol` |\n`true` |\nMatch a failed patch anywhere in the same file. Set `false` to also require the same enclosing symbol. |\n`maxLedger` |\n`5000` |\nCap on stored attempts; oldest are dropped past this. |\n\nNervous about false positives on day one? Start with `rl init --warn`\n\n. Every\nwould-have-blocked event is logged, `rl stats`\n\nshows the count, and `rl show`\n\nlets you audit each one against your own code — then flip to `block`\n\nwith\nevidence (`rl config mode block`\n\n). For an extra safety margin, require a fix to\nfail twice before it ever blocks: `rl config minFailures 2`\n\n.\n\n| Tool | Cross-session? | Outcome-linked? | Blocks (vs advises)? |\n|---|---|---|---|\n| In-session loop detectors (identical tool-call hashing) | ✗ | ✗ | warns |\n| General agent-memory (\"lessons learned\") | ✓ | ✗ | advises |\n| Self-intervention research (Wink, …) | ✗ | partial | guides |\nRegressionLedger |\n✓ |\n✓ |\nblocks |\n\nThe differentiators: a **semantic** fix fingerprint (not raw-text or tool+arg\nhashing), an **outcome** link (which fix failed, and why), **cross-session +\npost-compaction** persistence, and a **hard block** that the model can't ignore.\n\nThe long version — including what memory tools do\n\nbetterand where this tool honestly falls short:[docs/COMPARISON.md].\n\n**🧠 Session briefing.** A`SessionStart`\n\nhook injects a compact \"what already failed here\" brief every time a session starts — including**right 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 is*different*fixes 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`\n\n/`rl import`\n\nshare settled verdicts between machines and teammates:*your agent inherits the dead ends my agent already paid for.*Failures stay attributed (`importedFrom`\n\n) and auditable. Imported text is structurally neutralized and labeled`[imported verdict]`\n\nwherever it reaches agent context — but treat shared ledgers like dependencies: import from people you trust.\n\n**Isn't this just a memory MCP / lessons-learned layer?**\nNo — those store advice the model may ignore; this issues a `PreToolUse`\n\ndeny\nthe model *can't* ignore, and only when a real test run proved that exact fix\nalready failed. Memory recalls; RegressionLedger refuses. Full comparison\n(including what memory tools do better): [docs/COMPARISON.md](/anlor1002-alt/regressionledger/blob/main/docs/COMPARISON.md).\n\n**Will it block a legitimately different edit to the same function?**\nNo — matching is keyed on the *changed code itself*, not the function. A\ndifferent approach to the same bug has a different fingerprint and sails through.\n\n**What if the right fix is just a different constant — timeout 5000→30000?**\nNever blocked. Hard blocks require a\n\n**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\n\n*verbatim*is denied. The benchmark enforces this: 0/40 parameter changes blocked.\n\n**One test run settles several edits at once — isn't attribution noisy?**\nYes, and the tool says so: when N edits are failed by a single run, each record\ncarries `batchSize`\n\n, and any resulting block message discloses *\"it failed\nalongside N−1 other edits — attribution is approximate\"* with an `rl unblock`\n\npointer. Flaky tests can still blame an innocent fix; `minFailures 2`\n\nis the\nmitigation if that bites you.\n\n**What if the failed fix is actually correct and something else was broken?**\nOnce any run marks that exact fix as **passing**, the stale `fail`\n\nrecord is\nretired and it stops blocking. You can also `rl clear --force`\n\nor use `warn`\n\nmode.\n\n**Does it send my code anywhere?** No. Everything is local, deterministic, and\noffline. The ledger stores normalized tokens and a short preview, never your\nsecrets-in-context.\n\n**Cursor / Windsurf / other agents?** v1 ships for Claude Code's hook surface\n(the one place this is installable today). The engine is harness-agnostic; other\nintegrations are on the roadmap.\n\n- AST-based fingerprints via tree-sitter (more precise symbol & structure matching) as an optional upgrade, keeping the zero-dep default.\n- Adapters for other agent harnesses.\n\n```\nnpm test        # node:test, zero dependencies\nnpm run demo    # the doom-loop simulation above\n```\n\nContributions welcome — see [CONTRIBUTING.md](/anlor1002-alt/regressionledger/blob/main/CONTRIBUTING.md). The canonical\nfirst PR: **add support for your test runner** — one toolchain entry in\n[ src/signatures.js](/anlor1002-alt/regressionledger/blob/main/src/signatures.js), a real output sample in\n\n[, and a row in the table-driven](/anlor1002-alt/regressionledger/blob/main/test/fixtures)\n\n`test/fixtures/`\n\n[. Also good: add a language to the tokenizer's comment map.](/anlor1002-alt/regressionledger/blob/main/test/outcome-fixtures.test.js)\n\n`test/outcome-fixtures.test.js`", "url": "https://wpnews.pro/news/show-hn-a-hook-that-stops-claude-code-from-re-trying-fixes-that-failed", "canonical_source": "https://github.com/anlor1002-alt/regressionledger", "published_at": "2026-07-17 12:09:58+00:00", "updated_at": "2026-07-17 12:21:06.420240+00:00", "lang": "en", "topics": ["ai-tools", "developer-tools", "ai-agents"], "entities": ["Claude Code", "Cursor", "RegressionLedger"], "alternates": {"html": "https://wpnews.pro/news/show-hn-a-hook-that-stops-claude-code-from-re-trying-fixes-that-failed", "markdown": "https://wpnews.pro/news/show-hn-a-hook-that-stops-claude-code-from-re-trying-fixes-that-failed.md", "text": "https://wpnews.pro/news/show-hn-a-hook-that-stops-claude-code-from-re-trying-fixes-that-failed.txt", "jsonld": "https://wpnews.pro/news/show-hn-a-hook-that-stops-claude-code-from-re-trying-fixes-that-failed.jsonld"}}