{"slug": "never-again-so-your-ai-agent-stops-repeating-mistakes-you-fixed", "title": "Never-again – so your AI agent stops repeating mistakes you fixed", "summary": "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.", "body_md": "**Your coding agent keeps making the same mistake. Stop writing it a reminder.\nTake the mistake away.**\n\n`never-again` catches each bug you fix and asks one question: can a script\nstop this next time? If it can, it writes a hook that blocks the wrong action.\nIf it cannot, it writes one short line in a rules file. That is all.\n\nMIT licensed. Runs on your machine. No account, no tracking, no network calls.\n\n**macOS, Linux, WSL:** needs git, bash and Python 3.7 or newer.\n\n```\ngit clone --depth 1 https://github.com/malaysherasia-ai/claude-never-again.git\nbash claude-never-again/install.sh .\n```\n\n**Windows:** needs Git for Windows and Python 3.7 or newer.\n\n```\ngit clone --depth 1 https://github.com/malaysherasia-ai/claude-never-again.git\n& \"C:\\Program Files\\Git\\bin\\bash.exe\" claude-never-again/install.sh .\n```\n\nRun it with `bash`, not `./install.sh`. A ZIP download loses the file's\nrun permission, and Windows checkouts are unreliable about it. `bash` always\nworks.\n\n**Run the installer yourself, from a terminal.** Claude Code's auto mode will\nnot run an installer it has never seen. It also will not edit its own\n`.claude/settings.json` or write into `.git/hooks/`, even when you say yes.\nThat is Claude Code protecting you, not a bug. If you must install from\ninside Claude Code, turn auto mode off first. Otherwise, add these two pieces\nby hand afterwards:\n\n```\n// .claude/settings.json: merge into \"hooks\"; keep everything else\n\"PostToolUse\":        [{ \"matcher\": \"Bash\", \"hooks\": [{ \"type\": \"command\", \"if\": \"Bash(git commit *)\", \"command\": \"\\\"$CLAUDE_PROJECT_DIR\\\"/.claude/hooks/na/_after.sh\" }] }],\n\"PostToolUseFailure\": [{ \"matcher\": \"Bash\", \"hooks\": [{ \"type\": \"command\", \"if\": \"Bash(git commit *)\", \"command\": \"\\\"$CLAUDE_PROJECT_DIR\\\"/.claude/hooks/na/_after.sh\" }] }]\n# .git/hooks/pre-commit: create it if you have none; otherwise add the last two lines to yours\n#!/bin/sh\nr=\"$(git rev-parse --show-toplevel)/.claude/hooks/na/pre-commit\"\n[ -f \"$r\" ] || exit 0\nexec \"$r\" \"$@\"\n```\n\nThe same goes for `na`, the stats command. In auto mode Claude Code may\nrefuse to run it. Run it from a terminal.\n\n**Three things to check in your repo.** The installer looks for each one and\ntells you. It helps to know them ahead of time:\n\n- If `.gitignore` hides the`.claude/` folder, the hooks and their settings\nstay on your machine. They do not reach git.`LESSONS.md` still does. The\ninstaller prints the lines to use if you want the hooks shared.\n- If your repo root is served as a website (Vercel, Netlify, GitHub Pages),\nthen `/LESSONS.md` and`/.claude/` become public web pages, hooks and\nsettings included. Add`.claude` ,`LESSONS.md` and`CLAUDE.md` to`.vercelignore` , publish a subfolder instead, or accept that people can read\nthem.\n- If your repo already has a `LESSONS.md` somewhere other than the root, in\nits own format, it is left alone.`na` only manages the root file and files\nthat carry the tool's marker comment.\n\nBecause `CLAUDE.md` is advice, and advice has two problems.\n\nYou **pay for it on every turn.** A 200-line instructions file is 200 lines\nof context in every message you send, forever, whether it matters right now\nor not. Prompt caching makes that cheap in money. It does not make it cheap\nin attention, and attention is what makes rules get followed.\n\nAnd it is **optional.** The longer a session runs, the more likely a rule\ngets skipped. Plenty of people have written the rule down and watched the same\nbug ship anyway.\n\nA 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.\n\n```\n             cost per turn     can be ignored\nCLAUDE.md      every line           yes\nhook            one line             no\n```\n\n`never-again` is the bridge between the two. It decides which of your lessons\nbelongs where, and it writes the hook for you.\n\n**1. You fix a bug.** Then you say \"never again\", or Claude notices the\ncorrection on its own.\n\n**2. It sorts the lesson.** Four options, first fit wins:\n\n| Already covered | a linter or tsconfig flag catches it | turn that on, file nothing | \n| Hook-shaped | you can say it as \"before X, fail if Y\" | write a hook | \n| Judgement | a script cannot make the call | one line in `LESSONS.md` | \n| Not worth it | a one-off, or already impossible | file nothing | \n\nThat last row matters. Most tools file everything. This one is allowed to say no, because every line it writes is rent you pay forever.\n\n**3. The hook starts in warn mode.** Instead of blocking, it shows you a\nprompt with the reason, and you choose. Your choice is written down without\nany work from you. If the commit went ahead, the fire is marked *proceeded*.\nIf you stopped, it is marked *declined*, and a declined fire counts as\ncorrect. You can overrule the record when you want:\n\n```\nna ok L001        # that fire was right\nna wrong L001     # false alarm; narrow the check, the streak resets\n```\n\nGrades attach to real fires. After five correct in a row, `na` tells you, and\nyou promote it yourself:\n\n```\nna promote L001\n```\n\nNever automatic. A hook that blocks wrongly on day one gets the whole tool\nuninstalled. Who may promote is a setting in `state.json`. The default is by\npull request: `na promote` refuses on the default branch, so the change gets\nreviewed like code. The other options are anyone, or a list of names.\n[`docs/TEAMS.md`](https://github.com/malaysherasia-ai/claude-never-again/blob/main/docs/TEAMS.md) covers teams: what travels with git, who\ngrades, who promotes, and packs.\n\n**4. It guards every commit, not only Claude's.** The same script runs from\ngit's own pre-commit hook. A commit from a terminal, another agent, or a\ndifferent tool meets the same rule. Warn mode prints and lets it through.\nBlock mode refuses it.\n\nA real one: the bug this tool came out of.\n\nEvery button on a page went dead. The cause was a `let` used before its\ndeclaration. That is a temporal dead zone error. It stops the script at\nstartup, so no click handler ever attaches. `node --check` passed, because it\nonly checks syntax.\n\n**What most setups do** is add a line to `CLAUDE.md`:\n\n```\nAlways test in a real browser before shipping.\n```\n\nThat 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.\n\n**What `never-again` does** is file this instead:\n\n```\n- [web] [hook] Boot the build in a real browser, not `node --check` — when: before commit (L001)\n```\n\nIt also writes `.claude/hooks/na/L001.sh`. That script compares every source\nfile against the last browser boot that passed. If anything changed, it runs\nthe boot itself. It refuses the commit only if the page does not boot.\n\nThe rule now cannot be skipped, and it costs one line to carry. The full\nworked example is in [`examples/browser-boot/`](https://github.com/malaysherasia-ai/claude-never-again/blob/main/examples/browser-boot).\n\nThe block below is **sample output from a mature install**, not results from\nthis project. It shows the shape of the report. To see what your own repo has\nrecorded, run `na` there.\n\n``` bash\n$ na\n\n  never-again\n  ----------------------------------------------\n  rules           12 across 2 files  (cap 40 per file)\n  loaded here     9  (warn above 60)\n  hooks           7  (4 blocking, 3 warn)\n  hook fires      38\n\n  prevented       23  (19 denied, 4 declined at the prompt)\n  warned past     9   (asked, and the person went ahead)\n  est. tokens     ~184,000 saved\n                  (at 8,000/repeat — edit in state.json)\n\n  per hook        fires  denied  declined  proceeded  ok  wrong  streak\n    L001             14      11         2          1   0      0       5\n    L004              6       6         0          0   0      0       6\n    L009              3       0         2          1   1      0       3\n\n  most-hit\n    L001  ████████████████··   14  Boot the build in a real browser\n    L004  ███████···········    6  Never read-modify-write settings.json\n    L009  ████··············    3  dvh, not vh, for keyboard-adjacent UI\n\n  ready to promote: L009\n    na promote L009\n```\n\nThis is a Python script counting lines in files. **No model is involved.**\nThat is the point. A tool that spends tokens to tell you how many tokens it\nsaved has argued itself out of a job.\n\n\"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.\n\nThe token figure is an estimate built on one number you control: what a\nrepeated debug-and-fix cycle costs you. The default of 8,000 is on the low\nside on purpose. Change it in `state.json`.\n\n**Nothing is sent anywhere.** If you want to share your number, copy it into\na post. There is no phone-home, and there never will be. A tool that reads\nyour repo has no business opening a network connection.\n\nLessons belong to the nearest `LESSONS.md`. A rule about `packages/api` lives\nin `packages/api/LESSONS.md` and costs nothing while you work on the web app.\n\nParallel agents each write to the file nearest their own working folder, so\ntwo writers never touch one file. Hooks are shared, so the skill re-reads\n`state.json` to claim an id, and it merges into `settings.json` instead of\nreplacing it.\n\nHooks all load one shared library, `.claude/hooks/na/na-lib.sh`, so the parts\nthat went wrong in the first release now live in one place.\n\n**It decides from the command, not from a word in it.** `git -C . commit`,\n`git  commit` and `git add -A && git commit` all count as a commit.\n`echo \"git commit\"` does not.\n\n**It never asks \"did you run X?\"** A `PreToolUse` hook sees the repo as it\nwas before the tool call. So a hook that checks for a stamp file fires every\ntime the stamp is written in the same command as the commit. Six of the first\nnine real fires were exactly that. If a rule says \"X must have run\", the hook\nruns X itself when things are stale, and fires only when X fails.\n\nIt also looks only at the files that changed, not the whole tree. It records\nwhat happened after it fired. And it stays out of `fires.log` during\nself-tests (`NA_DRY_RUN=1`).\n\n**One process, however many hooks.** Claude Code runs one entry, the\ndispatcher. It reads the commit once, finds Python once, lists the changed\nfiles once, and asks `na index` which hooks exist, in which mode, watching\nwhich file types. A hook that watches `.css` does not run for a commit that\ntouched none. Ten hooks or two hundred cost one setup. Only the checks that\napply run, and their answers come back as one decision.\n\n```\nLESSONS.md                          the rules Claude reads (small, capped, ordered)\nCLAUDE.md                           one marked block added at the end; never overwritten\n.claude/skills/never-again/         the skill\n.claude/hooks/na/L###.sh            the hook scripts\n.claude/hooks/na/dispatch           the one registered hook: runs the others from na index\n.claude/hooks/na/na-lib.sh          shared by every hook\n.claude/hooks/na/na-verify.sh       the \"X must pass before commit\" engine, plus na-manifest.py\n.claude/hooks/na/_after.sh          records that a warned commit went ahead\n.claude/hooks/na/pre-commit         runs commit hooks from git itself\n.claude/settings.json               two entries, dispatch and _after.sh; merged, never replaced\n.git/hooks/pre-commit               a short stub, only if you had none\n.claude/never-again/\n  ├── na                            the stats command  (na.cmd for PowerShell)\n  ├── state.json                    lesson index, hook modes\n  ├── archive/L###.md               the full story, read only when asked\n  ├── verified/                     one record per verify hook; local, gitignored\n  └── fires.log                     every fire, its outcome and grade; local, gitignored\n```\n\n**Already have notes?** Most repos do: a `CLAUDE.md` full of rules, a\n`NOTES.md`, a `docs/lessons.md`, a `.cursorrules`. `na import` lists them,\nwith how many lines of notes each one holds and which were imported before.\nTell Claude once, *\"import the existing notes with the never-again skill\"*.\nEach note then goes through the same sorting as a fresh bug. The ones a\nscript can check become hooks in warn mode. The judgement calls become\none-liners. The rest are skipped. The source files are never edited. The tool\nonly remembers which files were imported, so the next `na import` shows only\nwhat changed.\n\n**Already have a `LESSONS.md`?** It stays exactly as it is, and Claude keeps\nreading it. But notes in your own words are invisible to `na`: not counted,\nnot capped, not sorted, not enforced. The installer says so when it finds\nthem. To bring them in, tell Claude once: *\"read LESSONS.md and refile each\nnote through the never-again skill\"*. Each note goes through the same sorting.\nYour original stays in git history.\n\n**What a fresh clone gets, and what it does not.** `LESSONS.md`, the hook\nscripts, `state.json` and the archive travel with git. So a clone has every\nrule, in the mode the team earned. The wiring does not travel. Git never\nclones its own hooks folder, and `.claude/settings.json` is yours; it often\nholds other tools and paths that only work on one machine, so we do not ask\nyou to share it. After cloning, run the installer once. It registers every\nhook in `state.json` with Claude Code and installs the git stub. It is safe\nto run again, changes nothing that already matches, and takes a second.\n\n`install.sh` backs up `CLAUDE.md` before touching it and is safe to re-run.\n`LESSONS.md` and the hooks are meant to be committed. They are team knowledge.\nA new hire inherits every scar the team has earned.\n\n```\n.claude/never-again/na uninstall          # or: bash install.sh --uninstall .\n```\n\nIt prints exactly what it will do and waits for a yes. Pass `--yes` to skip\nthe prompt. Anything other than `y` removes nothing. A closed stdin also\nremoves nothing, so it is safe to pipe.\n\n```\n  remove   .claude/skills/never-again/        12 file(s)\n  remove   .claude/hooks/na/                  7 file(s)\n  remove   .claude/never-again/               9 file(s)\n  remove   .git/hooks/pre-commit              git pre-commit stub\n  edit     CLAUDE.md                          strip the never-again block, keep the rest\n  edit     .claude/settings.json              remove 4 hook entries, keep everything else\n  edit     .gitignore                         remove the never-again section\n  keep     LESSONS.md                         your rules outlive the tool\n```\n\nFour things it will not do. It does not rewrite `CLAUDE.md`. It only cuts\nthe block between the `never-again` markers and leaves the rest alone. It\ndoes not replace `.claude/settings.json`. It only drops the hook entries that\npoint at `.claude/hooks/na/`, so your own hooks and settings stay. It does not\ntouch a git `pre-commit` hook it did not write. And it does not delete\n`LESSONS.md`. Those rules are yours, they read fine without the tool that\nenforced them, and deleting a stranger's notes is not an uninstaller's job.\nRemove it yourself if you want it gone.\n\nRunning it twice is fine. The second run has nothing to do and says so.\n\n`tests/uninstall.sh` checks all of the above against a repo that already has\nits own `CLAUDE.md` sections, its own hooks in `settings.json`, its own git\n`pre-commit` and its own `.gitignore` entries. The point of the test is not\nthat uninstall deletes things, but that it deletes only its own.\n`tests/hooks.sh` builds a hook from the template and runs it through both\nClaude Code's payload and a real `git commit`. It checks the fire log, the\ngrading, retire, and reinstall. Run both with\n`bash tests/hooks.sh && bash tests/uninstall.sh`. Every pull request runs\nthem on Ubuntu, macOS and Windows.\n\nThe direction is well supported: the more rules a model is given at once,\nthe fewer it follows, and it drops them quietly instead of refusing. IFScale\n(Distyl AI, 2025) tested 20 models on 10 to 500 rules at once in a\nreport-writing task. Even the best reached only 68% at the top end, and rules\ngiven earlier were followed more reliably than later ones. That task is not\ncoding, and nobody has measured the same curve for rules in a `CLAUDE.md`.\nWhat `never-again` borrows is the direction and the \"earlier is better\"\neffect, not a number. The \"I wrote the rule down and it ignored it anyway\"\nexperience is real. The exact limit for your repo is not in any paper.\n\nSo `never-again` does three things the research supports, and one thing it\ndoes not claim:\n\n- **Capped.** 40 rules per file by default. Not a magic number. It is about\nthe largest file a person still reads top to bottom. Change`cap` in`state.json` if your team disagrees.\n- **Ordered.**`na sort` puts the most-fired rules first, because \"earlier is\nbetter\" is real and free.\n- **Scoped.** Rules load per package, not per repo. The number that matters\nis*rules loaded per turn* .`na` reports it as \"loaded here\" and warns above\n60 (`loadedWarn` ). When it warns, split, retire, or promote. Do not raise it.\n- **Not claimed:** that 40, or 60, is the right number for you. The evidence\nsays fewer and ordered. It does not name a limit. Yours will show up in your\nown fire log.\n\n`na` is short for `.claude/never-again/na`. Alias it.\n\n```\nna sort           # most-fired rules first, in every LESSONS.md\nna why L001       # read the full story behind a rule\nna retire L001    # drop the line, unregister the hook, keep the archive\nna demote L001    # blocking back to warn\nna import         # notes files already in the repo, and what was imported\nna index          # every live hook: mode, trigger, scope, watched extensions\n```\n\nOn Windows, `.claude\\never-again\\na.cmd` runs the same thing from PowerShell\nor cmd.\n\nThis stands on two well-known ideas and joins them.\n\nThe `lessons.md` pattern, a file the agent writes discoveries into mid-task,\nis widely used and well documented. So are Claude Code hooks, where the\nsettled wisdom is that rules shape behaviour and hooks enforce it.\n\nWhat was missing is the step between: deciding which lessons deserve enforcement, and generating the hook. That is all this does.\n\nMIT.\n\n`never-again` is an independent open-source project. It is not affiliated\nwith, endorsed by, or sponsored by Anthropic. Claude and Claude Code are\ntrademarks of Anthropic, PBC.", "url": "https://wpnews.pro/news/never-again-so-your-ai-agent-stops-repeating-mistakes-you-fixed", "canonical_source": "https://github.com/malaysherasia-ai/claude-never-again", "published_at": "2026-09-16 07:48:53+00:00", "updated_at": "2026-09-16 08:14:30.559524+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-products"], "entities": ["never-again", "Claude Code", "GitHub", "Vercel", "Netlify", "GitHub Pages", "Python 3.7", "malaysherasia-ai"], "alternates": {"html": "https://wpnews.pro/news/never-again-so-your-ai-agent-stops-repeating-mistakes-you-fixed", "markdown": "https://wpnews.pro/news/never-again-so-your-ai-agent-stops-repeating-mistakes-you-fixed.md", "text": "https://wpnews.pro/news/never-again-so-your-ai-agent-stops-repeating-mistakes-you-fixed.txt", "jsonld": "https://wpnews.pro/news/never-again-so-your-ai-agent-stops-repeating-mistakes-you-fixed.jsonld"}}