cd /news/ai-agents/never-again-so-your-ai-agent-stops-r… · home topics ai-agents article
[ARTICLE · art-131175] src=github.com ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Never-again – so your AI agent stops repeating mistakes you fixed

A new MIT-licensed tool called never-again converts each bug a developer fixes into either a blocking hook or a one-line rule so AI coding agents stop repeating the same mistakes. The tool runs locally with no account, tracking, or network calls, and requires git, bash, and Python 3.7 or newer on macOS, Linux, WSL, or Windows via Git for Windows. Its installer must be run manually from a terminal because Claude Code's auto mode will not run an unseen installer or edit its own .claude/settings.json or .git/hooks/ files.

read15 min views1 publishedSep 16, 2026
Never-again – so your AI agent stops repeating mistakes you fixed
Image: Michielbdejong (auto-discovered)

Your coding agent keeps making the same mistake. Stop writing it a reminder. Take the mistake away.

never-again catches each bug you fix and asks one question: can a script stop this next time? If it can, it writes a hook that blocks the wrong action. If it cannot, it writes one short line in a rules file. That is all.

MIT licensed. Runs on your machine. No account, no tracking, no network calls.

macOS, Linux, WSL: needs git, bash and Python 3.7 or newer.

git clone --depth 1 https://github.com/malaysherasia-ai/claude-never-again.git
bash claude-never-again/install.sh .

Windows: needs Git for Windows and Python 3.7 or newer.

git clone --depth 1 https://github.com/malaysherasia-ai/claude-never-again.git
& "C:\Program Files\Git\bin\bash.exe" claude-never-again/install.sh .

Run it with bash, not ./install.sh. A ZIP download loses the file's run permission, and Windows checkouts are unreliable about it. bash always works.

Run the installer yourself, from a terminal. Claude Code's auto mode will not run an installer it has never seen. It also will not edit its own .claude/settings.json or write into .git/hooks/, even when you say yes. That is Claude Code protecting you, not a bug. If you must install from inside Claude Code, turn auto mode off first. Otherwise, add these two pieces by hand afterwards:

// .claude/settings.json: merge into "hooks"; keep everything else
"PostToolUse":        [{ "matcher": "Bash", "hooks": [{ "type": "command", "if": "Bash(git commit *)", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/na/_after.sh" }] }],
"PostToolUseFailure": [{ "matcher": "Bash", "hooks": [{ "type": "command", "if": "Bash(git commit *)", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/na/_after.sh" }] }]
#!/bin/sh
r="$(git rev-parse --show-toplevel)/.claude/hooks/na/pre-commit"
[ -f "$r" ] || exit 0
exec "$r" "$@"

The same goes for na, the stats command. In auto mode Claude Code may refuse to run it. Run it from a terminal.

Three things to check in your repo. The installer looks for each one and tells you. It helps to know them ahead of time:

  • If .gitignore hides the.claude/ folder, the hooks and their settings stay on your machine. They do not reach git.LESSONS.md still does. The installer prints the lines to use if you want the hooks shared.
  • If your repo root is served as a website (Vercel, Netlify, GitHub Pages), then /LESSONS.md and/.claude/ become public web pages, hooks and settings included. Add.claude ,LESSONS.md andCLAUDE.md to.vercelignore , publish a subfolder instead, or accept that people can read them.
  • If your repo already has a LESSONS.md somewhere other than the root, in its own format, it is left alone.na only manages the root file and files that carry the tool's marker comment.

Because CLAUDE.md is advice, and advice has two problems.

You pay for it on every turn. A 200-line instructions file is 200 lines of context in every message you send, forever, whether it matters right now or not. Prompt caching makes that cheap in money. It does not make it cheap in attention, and attention is what makes rules get followed.

And it is optional. The longer a session runs, the more likely a rule gets skipped. Plenty of people have written the rule down and watched the same bug ship anyway.

A hook is neither. It costs one line until it fires. In block mode it says no, and the agent cannot talk its way past that. In warn mode it asks you, and it writes down what you answered.

             cost per turn     can be ignored
CLAUDE.md      every line           yes
hook            one line             no

never-again is the bridge between the two. It decides which of your lessons belongs where, and it writes the hook for you.

1. You fix a bug. Then you say "never again", or Claude notices the correction on its own.

2. It sorts the lesson. Four options, first fit wins:

| Already covered | a linter or tsconfig flag catches it | turn that on, file nothing | | Hook-shaped | you can say it as "before X, fail if Y" | write a hook | | Judgement | a script cannot make the call | one line in LESSONS.md | | Not worth it | a one-off, or already impossible | file nothing |

That last row matters. Most tools file everything. This one is allowed to say no, because every line it writes is rent you pay forever.

3. The hook starts in warn mode. Instead of blocking, it shows you a prompt with the reason, and you choose. Your choice is written down without any work from you. If the commit went ahead, the fire is marked proceeded. If you stopped, it is marked declined, and a declined fire counts as correct. You can overrule the record when you want:

na ok L001        # that fire was right
na wrong L001     # false alarm; narrow the check, the streak resets

Grades attach to real fires. After five correct in a row, na tells you, and you promote it yourself:

na promote L001

Never automatic. A hook that blocks wrongly on day one gets the whole tool uninstalled. Who may promote is a setting in state.json. The default is by pull request: na promote refuses on the default branch, so the change gets reviewed like code. The other options are anyone, or a list of names. docs/TEAMS.md covers teams: what travels with git, who grades, who promotes, and packs.

4. It guards every commit, not only Claude's. The same script runs from git's own pre-commit hook. A commit from a terminal, another agent, or a different tool meets the same rule. Warn mode prints and lets it through. Block mode refuses it.

A real one: the bug this tool came out of.

Every button on a page went dead. The cause was a let used before its declaration. That is a temporal dead zone error. It stops the script at startup, so no click handler ever attaches. node --check passed, because it only checks syntax.

What most setups do is add a line to CLAUDE.md:

Always test in a real browser before shipping.

That line gets read on every turn until the end of the project. It gets followed when the context window is short and the agent is paying attention.

What never-again does is file this instead:

- [web] [hook] Boot the build in a real browser, not `node --check` — when: before commit (L001)

It also writes .claude/hooks/na/L001.sh. That script compares every source file against the last browser boot that passed. If anything changed, it runs the boot itself. It refuses the commit only if the page does not boot.

The rule now cannot be skipped, and it costs one line to carry. The full worked example is in examples/browser-boot/.

The block below is sample output from a mature install, not results from this project. It shows the shape of the report. To see what your own repo has recorded, run na there.

$ na

  never-again
  ----------------------------------------------
  rules           12 across 2 files  (cap 40 per file)
  loaded here     9  (warn above 60)
  hooks           7  (4 blocking, 3 warn)
  hook fires      38

  prevented       23  (19 denied, 4 declined at the prompt)
  warned past     9   (asked, and the person went ahead)
  est. tokens     ~184,000 saved
                  (at 8,000/repeat — edit in state.json)

  per hook        fires  denied  declined  proceeded  ok  wrong  streak
    L001             14      11         2          1   0      0       5
    L004              6       6         0          0   0      0       6
    L009              3       0         2          1   1      0       3

  most-hit
    L001  ████████████████··   14  Boot the build in a real browser
    L004  ███████···········    6  Never read-modify-write settings.json
    L009  ████··············    3  dvh, not vh, for keyboard-adjacent UI

  ready to promote: L009
    na promote L009

This is a Python script counting lines in files. No model is involved. That is the point. A tool that spends tokens to tell you how many tokens it saved has argued itself out of a job.

"Prevented" counts block-mode denials plus warn-mode fires where the person stopped. One stopped mistake can fire twice if the agent retries, so read it as an upper bound. "Warned past" is the honest column: prompts that were approved anyway.

The token figure is an estimate built on one number you control: what a repeated debug-and-fix cycle costs you. The default of 8,000 is on the low side on purpose. Change it in state.json.

Nothing is sent anywhere. If you want to share your number, copy it into a post. There is no phone-home, and there never will be. A tool that reads your repo has no business opening a network connection.

Lessons belong to the nearest LESSONS.md. A rule about packages/api lives in packages/api/LESSONS.md and costs nothing while you work on the web app.

Parallel agents each write to the file nearest their own working folder, so two writers never touch one file. Hooks are shared, so the skill re-reads state.json to claim an id, and it merges into settings.json instead of replacing it.

Hooks all load one shared library, .claude/hooks/na/na-lib.sh, so the parts that went wrong in the first release now live in one place.

It decides from the command, not from a word in it. git -C . commit, git commit and git add -A && git commit all count as a commit. echo "git commit" does not.

It never asks "did you run X?" A PreToolUse hook sees the repo as it was before the tool call. So a hook that checks for a stamp file fires every time the stamp is written in the same command as the commit. Six of the first nine real fires were exactly that. If a rule says "X must have run", the hook runs X itself when things are stale, and fires only when X fails.

It also looks only at the files that changed, not the whole tree. It records what happened after it fired. And it stays out of fires.log during self-tests (NA_DRY_RUN=1).

One process, however many hooks. Claude Code runs one entry, the dispatcher. It reads the commit once, finds Python once, lists the changed files once, and asks na index which hooks exist, in which mode, watching which file types. A hook that watches .css does not run for a commit that touched none. Ten hooks or two hundred cost one setup. Only the checks that apply run, and their answers come back as one decision.

LESSONS.md                          the rules Claude reads (small, capped, ordered)
CLAUDE.md                           one marked block added at the end; never overwritten
.claude/skills/never-again/         the skill
.claude/hooks/na/L###.sh            the hook scripts
.claude/hooks/na/dispatch           the one registered hook: runs the others from na index
.claude/hooks/na/na-lib.sh          shared by every hook
.claude/hooks/na/na-verify.sh       the "X must pass before commit" engine, plus na-manifest.py
.claude/hooks/na/_after.sh          records that a warned commit went ahead
.claude/hooks/na/pre-commit         runs commit hooks from git itself
.claude/settings.json               two entries, dispatch and _after.sh; merged, never replaced
.git/hooks/pre-commit               a short stub, only if you had none
.claude/never-again/
  ├── na                            the stats command  (na.cmd for PowerShell)
  ├── state.json                    lesson index, hook modes
  ├── archive/L###.md               the full story, read only when asked
  ├── verified/                     one record per verify hook; local, gitignored
  └── fires.log                     every fire, its outcome and grade; local, gitignored

Already have notes? Most repos do: a CLAUDE.md full of rules, a NOTES.md, a docs/lessons.md, a .cursorrules. na import lists them, with how many lines of notes each one holds and which were imported before. Tell Claude once, "import the existing notes with the never-again skill". Each note then goes through the same sorting as a fresh bug. The ones a script can check become hooks in warn mode. The judgement calls become one-liners. The rest are skipped. The source files are never edited. The tool only remembers which files were imported, so the next na import shows only what changed.

Already have a LESSONS.md? It stays exactly as it is, and Claude keeps reading it. But notes in your own words are invisible to na: not counted, not capped, not sorted, not enforced. The installer says so when it finds them. To bring them in, tell Claude once: "read LESSONS.md and refile each note through the never-again skill". Each note goes through the same sorting. Your original stays in git history.

What a fresh clone gets, and what it does not. LESSONS.md, the hook scripts, state.json and the archive travel with git. So a clone has every rule, in the mode the team earned. The wiring does not travel. Git never clones its own hooks folder, and .claude/settings.json is yours; it often holds other tools and paths that only work on one machine, so we do not ask you to share it. After cloning, run the installer once. It registers every hook in state.json with Claude Code and installs the git stub. It is safe to run again, changes nothing that already matches, and takes a second.

install.sh backs up CLAUDE.md before touching it and is safe to re-run. LESSONS.md and the hooks are meant to be committed. They are team knowledge. A new hire inherits every scar the team has earned.

.claude/never-again/na uninstall          # or: bash install.sh --uninstall .

It prints exactly what it will do and waits for a yes. Pass --yes to skip the prompt. Anything other than y removes nothing. A closed stdin also removes nothing, so it is safe to pipe.

  remove   .claude/skills/never-again/        12 file(s)
  remove   .claude/hooks/na/                  7 file(s)
  remove   .claude/never-again/               9 file(s)
  remove   .git/hooks/pre-commit              git pre-commit stub
  edit     CLAUDE.md                          strip the never-again block, keep the rest
  edit     .claude/settings.json              remove 4 hook entries, keep everything else
  edit     .gitignore                         remove the never-again section
  keep     LESSONS.md                         your rules outlive the tool

Four things it will not do. It does not rewrite CLAUDE.md. It only cuts the block between the never-again markers and leaves the rest alone. It does not replace .claude/settings.json. It only drops the hook entries that point at .claude/hooks/na/, so your own hooks and settings stay. It does not touch a git pre-commit hook it did not write. And it does not delete LESSONS.md. Those rules are yours, they read fine without the tool that enforced them, and deleting a stranger's notes is not an uninstaller's job. Remove it yourself if you want it gone.

Running it twice is fine. The second run has nothing to do and says so.

tests/uninstall.sh checks all of the above against a repo that already has its own CLAUDE.md sections, its own hooks in settings.json, its own git pre-commit and its own .gitignore entries. The point of the test is not that uninstall deletes things, but that it deletes only its own. tests/hooks.sh builds a hook from the template and runs it through both Claude Code's payload and a real git commit. It checks the fire log, the grading, retire, and reinstall. Run both with bash tests/hooks.sh && bash tests/uninstall.sh. Every pull request runs them on Ubuntu, macOS and Windows.

The direction is well supported: the more rules a model is given at once, the fewer it follows, and it drops them quietly instead of refusing. IFScale (Distyl AI, 2025) tested 20 models on 10 to 500 rules at once in a report-writing task. Even the best reached only 68% at the top end, and rules given earlier were followed more reliably than later ones. That task is not coding, and nobody has measured the same curve for rules in a CLAUDE.md. What never-again borrows is the direction and the "earlier is better" effect, not a number. The "I wrote the rule down and it ignored it anyway" experience is real. The exact limit for your repo is not in any paper.

So never-again does three things the research supports, and one thing it does not claim:

  • Capped. 40 rules per file by default. Not a magic number. It is about the largest file a person still reads top to bottom. Changecap instate.json if your team disagrees.
  • Ordered.na sort puts the most-fired rules first, because "earlier is better" is real and free.
  • Scoped. Rules load per package, not per repo. The number that matters isrules loaded per turn .na reports it as "loaded here" and warns above 60 (loadedWarn ). When it warns, split, retire, or promote. Do not raise it.
  • Not claimed: that 40, or 60, is the right number for you. The evidence says fewer and ordered. It does not name a limit. Yours will show up in your own fire log.

na is short for .claude/never-again/na. Alias it.

na sort           # most-fired rules first, in every LESSONS.md
na why L001       # read the full story behind a rule
na retire L001    # drop the line, unregister the hook, keep the archive
na demote L001    # blocking back to warn
na import         # notes files already in the repo, and what was imported
na index          # every live hook: mode, trigger, scope, watched extensions

On Windows, .claude\never-again\na.cmd runs the same thing from PowerShell or cmd.

This stands on two well-known ideas and joins them.

The lessons.md pattern, a file the agent writes discoveries into mid-task, is widely used and well documented. So are Claude Code hooks, where the settled wisdom is that rules shape behaviour and hooks enforce it.

What was missing is the step between: deciding which lessons deserve enforcement, and generating the hook. That is all this does.

MIT.

never-again is an independent open-source project. It is not affiliated with, endorsed by, or sponsored by Anthropic. Claude and Claude Code are trademarks of Anthropic, PBC.

── more in #ai-agents 4 stories · sorted by recency
── more on @never-again 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/never-again-so-your-…] indexed:0 read:15min 2026-09-16 ·