TLDR; Claude Code repeats mistakes that CLAUDE.md already warns about, because an instruction is a suggestion and a long session dilutes it. hamster, my MIT-licensed plugin, automates the move: it logs every failed tool call, asks for the lesson after a run of errors, and turns that lesson into a
PreToolUsehook rule. Each rule carries tests that prove it catches the bad shape and passes the good one, and a rule that never fires is listed for deletion.
My global CLAUDE.md has carried a line about zsh globs for months: an unmatched glob such as --include=*.md aborts the whole command with no matches found. Claude reads that line at the start of every session. It still wrote unquoted --include=*.md often enough that, once I turned the lesson into a hook rule on 15 October, the rule caught 229 commands in its first four days.
This guide covers that gap between knowing and doing. The hooks section of the extensibility guide puts it in two short sentences: “The instruction discourages. The hook prevents.” hamster is the machinery I built to move lessons from the first category to the second without the rule set rotting.
Why lessons in CLAUDE.md fade
CLAUDE.md and auto memory are prose. The model weighs them against everything else in its context, and as a session fills, a single line among 200 loses that contest more often. I’ve watched it happen in the same session where the model quoted the rule back to me an hour earlier.
Adding the lesson again, in capitals, buys a few days. Then the file grows, and every line costs tokens on every session whether or not it applies. A hook costs no tokens until it fires, and when it fires it runs in the harness, outside the model’s judgement.
The catch with hooks is that each one is code you have to write, test and eventually delete. I wrote three by hand and stopped. hamster makes writing one about as cheap as writing the CLAUDE.md line.
The loop
The repository’s diagram is the shortest description:
run ──► fail ──► capture ──► retro ──► lesson ──► promote ──► rule + keep-test ──► prune
▲ │ │
└──────────── handoff carries state ─────┘ rule-guard blocks the next repeat
Four pieces do the work:
- Capture. A
PostToolUseFailurehook writes one JSONL line per failed tool call to~/.claude/hamster/session-errors/. My machine holds 375 of those logs from six weeks of work. - Retro. A
Stophook counts new errors. After eight, it blocks Claude’s first attempt to stop and asks for the one durable lesson. A fingerprint ledger counts the earlier sessions and projects where the same error turned up; an error seen across several projects is a habit. - Promote.
/hamster:promotetakes a lesson from auto memory, a stored note or plain text, and drafts a rule with tests. It installs only when the tests are green. - Guard. A
PreToolUsehook on Bash, Edit and Write applies every rule and counts each fire.
The retro is the step I’d keep if I could keep only one. Eight errors is roughly where a session has gone sideways enough that the lesson is clear and still fresh. Ask at the end of every session and you get noise; never ask and the lesson dies with the context.
A hook rule example
A rule is a JSON file. This one came from a mistake Claude made once that I didn’t want to see again:
{
"id": "no-cd-into-worktree",
"tool": "Bash",
"field": "command",
"pattern": "(^|[;&|]\\s*)cd\\s+\\S*\\.worktrees/",
"action": "block",
"message": "Use absolute paths; cd into a linked worktree drifts cwd for every later call.",
"tests": [
{"input": "cd .worktrees/x && ls", "expect": "block"},
{"input": "ls .worktrees/x", "expect": "pass"}
]
}
The tests array is the keep-test. scripts/rules.py add refuses a block or rewrite rule that has no test pair or whose tests fail. A rule file dropped into a rules folder by hand, without the pair, still loads, but only as a warning, so a careless pattern can’t silently stop good commands.
An optional when gate scopes the rule before the pattern runs: files_exist for a stack check (a Gemfile means Ruby), path_glob to confine an Edit or Write rule to a folder, branch or branch_not to tighten it on main. Project rules live in the repo under .claude/hamster/rules/ and are meant to be committed, so the whole team gets the lesson.
Block or rewrite
My first rules all blocked. Blocking works, but each block costs a turn: Claude reads the message, rewrites the command and tries again. When the fix is mechanical, that turn is waste.
So hamster now also has rewrite rules. The pattern’s groups feed a replacement template, the hook hands Claude Code the corrected call, and the call runs as rewritten. Claude sees the rule’s message and the command that ran. The zsh glob rule is one:
{
"id": "unquoted-include-glob",
"tool": "Bash", "field": "command",
"pattern": "((?:^|\\s)--(?:include|exclude|exclude-dir|glob)=)(?!['\"])([^\\s'\"\\\\{}]*[*?\\[][^\\s'\"\\\\{}]*)",
"action": "rewrite",
"replace": "\\1\"\\2\"",
"message": "zsh globs an unquoted --include=*.md; quoted for you.",
"tests": [
{"input": "grep -rn --include=*.md foo .", "expect": "rewrite", "output": "grep -rn --include=\"*.md\" foo ."},
{"input": "grep -rn --include='*.md' foo .", "expect": "pass"}
]
}
Its tests state the exact output the rewrite must produce. hamster’s rewriter skips quoted strings, comments and heredoc bodies that are data, and the rewritten command passes through the block rules again before it runs.
The split I use: rewrite when a script could make the fix with no judgement; block when someone has to change course. A force-push to main is a block. A missing quote is a rewrite.
My own fire counts are why the rewrite action exists. I run RTK, which rewrites shell commands to filter their output and save tokens; its find rejects compound predicates, so those calls need rtk proxy find. Three of my rules started as blocks, and the history on the RTK one reads “325 blocks in three weeks, every one re-issued as rtk proxy find”. Each time, Claude made the same mechanical fix. On 18 October I switched all three to rewrites:
| Rule | Created | Blocked, then rewritten from 18 October |
|---|---|---|
Use rtk proxy find for compound find predicates |
26 September | 325 blocks, 45 rewrites |
Quote --include and --glob patterns |
15 October | 162 blocks, 67 rewrites |
Add the --actor flag to task-store writes |
29 September | 76 blocks, 15 rewrites |
That’s 563 blocked calls, each costing a turn to re-issue, before the switch, and 127 rewrites since, each of which ran first time. The block rules that remain fire rarely: force-push to a protected branch three times, cd into a linked worktree once.
Pruning: a rule that never fires
The cd rule has fired once in six weeks. Either Claude stopped producing that command, or the situation stopped coming up. Both mean the rule is close to overhead: one more pattern on every Bash call.
hamster lists a user or project rule for deletion once it is 60 days old and has never fired, and /hamster:rules shows the candidates. Deletion still needs my click. Bundled rules (rm -rf on root or home, chmod 777, force-push to protected branches) and rules marked keep: true are exempt: a hard-safety rule that never fires is doing its job.
The fire count is the honest measure of a rule set. Rules are easy to add and nobody remembers to remove them; a count makes the dead ones visible. One gap: the check looks for zero lifetime fires, not a long silence, so the cd rule’s single fire in September keeps it off the list for good. Spotting a rule that has gone quiet is still my job.
What else is in the box
hamster grew around the loop. Three pieces sit beside it; the README lists the rest, including a decision log.
- Handoffs. The
/handoffworkflow from the session craft guide ships here, formerly the separate claude-handoff repo. - Cache guards.
wake-guardstops me once before I message a sub-agent that has been idle past its prompt cache with more than 60,000 tokens of context, andstale-guarddoes the same for a chat idle past its one-hour cache with more than 100,000. Each states the cache-write cost and lets the second attempt through. - A local-model slot. The retro’s error grouping and the pre-compaction summary run on a local model through any OpenAI-compatible server, with Apple’s on-device model as the fallback. In my own blind evaluation, with Opus as judge (the README records the result), a Qwen 3.6 35B mixture-of-experts model matched Haiku 4.5 on a yes/no task at 0.4 seconds per call. Rule authoring never goes through the local model. Separately, I wrote up the trade-offs of a local model in the coding loop in August.
Limits
- A rule only catches a shape it has seen. hamster learns from failures that already happened.
- Patterns are regular expressions over the raw command. Heredocs and quoting are handled, but a determined enough shell construction can slip past, so don’t mistake the bundled safety rules for a sandbox.
- When another hook such as RTK also rewrites the same call, the hooks reference says matching hooks run in parallel and gives no order for competing input rewrites. On version 2.1.302 hamster’s won every time; that’s what I observed, and nothing guarantees it.
- hamster also ships as a package for the pi coding agent, whose bridge replays the PreToolUse, Stop and SessionStart hooks. The bridge applies blocks but not rewrites, so under pi a rewrite rule’s correction doesn’t happen. The error capture and pre-compaction snapshot need Claude Code’s
PostToolUseFailureandPreCompactevents, so neither runs there.
Install
git clone https://github.com/kam/hamster.git && cd hamster
./install.sh
It needs python3 and git, then a restart of Claude Code. This clone install names the commands with a hyphen (/hamster-test, /hamster-rules, /hamster-promote); installed from a plugin marketplace they take the colon form (/hamster:test). Start with the bundled rules, let the retro run for a week, and promote the first lesson it asks you for. /hamster-test runs every rule’s keep-tests.
The same principle carries into client work: the Applied AI agents I build keep their hard limits in code the model can’t skip.
Related reading
- Skills, hooks, agents, and commands: how to use each, where hooks fit among Claude Code’s primitives
- Session craft: when to start fresh, resume, or compact, the handoff workflow hamster bundles
- Quality gates: session reviews, fact-checking, and code review, hooks that review work rather than block it
- Local audio transcription with speaker labels in Claude Code, my open-source transcription skill
Sources
- Hooks reference, Claude Code Docs, hook events, exit codes and
updatedInput. Documentation - hamster on GitHub, the plugin’s source, rule format and tests. Repository
- RTK, the command-rewriting token filter referred to above. Tool
Common questions
What are hooks in Claude Code?
Hooks are commands Claude Code runs automatically on events such as PreToolUse, before a tool call, or Stop, when Claude finishes a turn. A PreToolUse hook can inspect a Bash command and block it (exit code 2) or hand back a corrected version.
How do you stop Claude Code repeating the same mistake?
Turn the lesson into a PreToolUse hook rule rather than another line in CLAUDE.md. A rule is a pattern over the tool call plus an action: block it, rewrite it, or warn. The harness runs it on every call, so it holds even when the model forgets the instruction.
What is a keep-test for a Claude Code hook rule?
A pair of examples stored with the rule: one input the rule must catch and one it must let through. hamster refuses to install a blocking rule until both pass, and runs a rule without them as a warning only. The test is what stops a pattern from quietly blocking good commands.
Should a hook block a mistake or fix it?
Fix it when the correction is mechanical, such as quoting a glob or adding a missing flag; the call runs as rewritten and Claude is told what ran. Block it when a person or the model has to change course, such as a force-push to main.
How do you keep hook rules from piling up?
Count every fire and retire the rules that never fire. hamster lists a user or project rule for deletion once it is 60 days old with no fires; bundled safety rules and rules marked keep are exempt, because a hard-safety rule that never fires is doing its job.