Cindy Zhu.
← all free guides
Power-user Claude

Claude hooks that make your agent fix its own mistakes

Hey, it's Cindy ๐ŸŒฑ This is the whole build from the carousel: four tiny files that make Claude Code log every run, review its own mistakes, and load the lessons back in at boot. You fix something once and it stays fixed. My memory system is the manual version of this (every time Claude gets something wrong, I write the lesson into a file myself), so this build genuinely stung. The idea comes from the dev behind this post, whose agent broke the same way three times and had patched itself by run four. This is my rebuild in plain language, and every event name below is checked against Anthropic's official hooks reference (July 2026).
the basics

First, what a hook is ๐Ÿช

A hook is a tiny script that fires at a set moment: when Claude boots, after every tool call, when the reply finishes. You register it once in .claude/settings.json, and from then on it fires itself, every single time. No reminders, no relying on Claude to remember.

This loop uses four of those moments (the names are case-sensitive, spell them exactly like this):

๐Ÿš€ SessionStart fires when Claude boots
๐Ÿ”ง PostToolUse fires after every tool call that worked
๐Ÿ’ฅ PostToolUseFailure fires after every tool call that failed
๐Ÿ›‘ Stop fires when Claude finishes its reply

the loop

How the loop works ๐Ÿ”„

Watch โ†’ log โ†’ review โ†’ improve:

  1. ๐Ÿ‘€ Watch file 1 sits on Claude's shoulder and writes one line per tool call (which tool ran, ok or FAIL). File 2 fires when the run ends and saves how it wrapped up
  2. ๐Ÿ“ Log everything lands in one dated file per day inside .claude/hooks/logs/, completely hands off
  3. ๐Ÿ” Review every 5th run, file 3 holds the door before Claude stops and makes it read its own logs for four things: repeated failures, slow spots, bad outputs, and quality problems. The lessons get written into .claude/hooks/lessons.md as plain one-line rules
  4. ๐Ÿ” Improve file 4 fires at boot and prints the rule book straight into context, so the next run starts already knowing what broke the last three
The dev behind the original runs four separate reviewer agents at once, so one reviewer's blind spot gets caught by the next. I folded those same four angles into one review pass to keep this a one-sitting build.

Here is everything you are about to create:

๐Ÿ—‚ the file tree
your-project/
โ””โ”€โ”€ .claude/
    โ”œโ”€โ”€ settings.json          <- the hooks block goes here
    โ””โ”€โ”€ hooks/
        โ”œโ”€โ”€ log_tool.py        <- file 1, the tool watcher
        โ”œโ”€โ”€ log_run.py         <- file 2, the run watcher
        โ”œโ”€โ”€ review_trigger.py  <- file 3, the reviewer
        โ”œโ”€โ”€ load_lessons.sh    <- file 4, the reloader
        โ”œโ”€โ”€ lessons.md         <- the rule book (grows on its own)
        โ””โ”€โ”€ logs/              <- one log file per day

file one

File 1: log_tool.py ๐Ÿ‘€

Fires after every tool call and appends one line: time, ok or FAIL, tool name, and a peek at the input. One file listens to both events, because Anthropic splits success and failure into PostToolUse and PostToolUseFailure.

๐Ÿ‘€ log_tool.py
#!/usr/bin/env python3
# File 1, the tool watcher: writes one line per tool call, which tool ran and did it work
import datetime, json, os, pathlib, sys

data = json.load(sys.stdin)  # Claude hands every hook a JSON report on stdin
logs = pathlib.Path(os.environ.get("CLAUDE_PROJECT_DIR", ".")) / ".claude" / "hooks" / "logs"
logs.mkdir(parents=True, exist_ok=True)
status = "ok" if data.get("hook_event_name") == "PostToolUse" else "FAIL"  # same script listens to both events
detail = json.dumps(data.get("tool_input", {}), ensure_ascii=False)[:160]  # first 160 chars of what the tool was doing
with open(logs / f"{datetime.date.today()}.log", "a") as f:
    f.write(f"{datetime.datetime.now():%H:%M:%S} {status} {data.get('tool_name', '?')} {detail}\n")

file two

File 2: log_run.py ๐Ÿ“

Fires when the run ends and saves Claude's closing words, so each day's log reads like a story: what ran, what failed, how it ended.

๐Ÿ“ log_run.py
#!/usr/bin/env python3
# File 2, the run watcher: when a run ends, saves how it wrapped up to the same log
import datetime, json, os, pathlib, sys

data = json.load(sys.stdin)  # the Stop report includes Claude's last message
logs = pathlib.Path(os.environ.get("CLAUDE_PROJECT_DIR", ".")) / ".claude" / "hooks" / "logs"
logs.mkdir(parents=True, exist_ok=True)
ending = " ".join((data.get("last_assistant_message") or "").split())[:400]  # Claude's closing words, trimmed
with open(logs / f"{datetime.date.today()}.log", "a") as f:
    f.write(f"--- run ended {datetime.datetime.now():%H:%M:%S} --- {ending}\n\n")

file three

File 3: review_trigger.py ๐Ÿ”

Counts finished runs. Runs 1 to 4 pass straight through. On run 5 it answers with {"decision": "block"}, which is the documented way a Stop hook says "not yet": Claude stays up, reads the reason, does the review, then stops. The counter ticks to 6 on the way out, so it can never loop forever.

๐Ÿ” review_trigger.py
#!/usr/bin/env python3
# File 3, the reviewer: every 5th run, holds the door and sends Claude back to turn logs into rules
import json, os, pathlib, sys

base = pathlib.Path(os.environ.get("CLAUDE_PROJECT_DIR", ".")) / ".claude" / "hooks"
base.mkdir(parents=True, exist_ok=True)
counter = base / "run_count.txt"
count = int(counter.read_text().strip() or 0) + 1 if counter.exists() else 1  # one tick per finished run
counter.write_text(str(count))
if count % 5:  # runs 1 to 4 pass straight through
    sys.exit(0)
print(json.dumps({"decision": "block", "reason":
    "Review pass. Read today's file in .claude/hooks/logs/ and look for four things: "
    "repeated failures, slow spots, bad outputs, and quality problems. Rewrite .claude/hooks/lessons.md "
    "as plain one-line rules, most useful first, 20 rules max, keeping old rules that still apply. Then stop."}))

file four

File 4: load_lessons.sh ๐Ÿ”

Fires at boot. Whatever a SessionStart hook prints to stdout gets added to Claude's context (that is documented behavior, not a trick), so the rule book loads itself.

๐Ÿ” load_lessons.sh
#!/bin/bash
# File 4, the reloader: at boot, print the rule book so it loads straight into Claude's context
LESSONS="${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/lessons.md"
[ -s "$LESSONS" ] && echo "Lessons learned from past runs in this project, follow these:" && cat "$LESSONS"
exit 0

wire it up

The settings block โš™๏ธ

This goes in .claude/settings.json in your project root. Create the file if it doesn't exist; if you already have one, merge the "hooks" key into it:

โš™๏ธ the settings.json hooks block
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume|clear",
        "hooks": [
          { "type": "command", "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/load_lessons.sh\"" }
        ]
      }
    ],
    "PostToolUse": [
      {
        "hooks": [
          { "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/log_tool.py\"" }
        ]
      }
    ],
    "PostToolUseFailure": [
      {
        "hooks": [
          { "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/log_tool.py\"" }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/log_run.py\"" },
          { "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/review_trigger.py\"" }
        ]
      }
    ]
  }
}

Reading it back: SessionStart runs the reloader on boot, resume, and /clear (that pipe list is the matcher). Both tool events feed the same watcher. Stop takes no matcher (it fires on every finished run) and runs the run watcher and the reviewer side by side. Keeping this at project level means each project grows its own rule book.


the setup

The install ๐Ÿ› 

  1. ๐Ÿ“ In your project root, run mkdir -p .claude/hooks/logs
  2. ๐Ÿ“„ Save the four files above into .claude/hooks/ with those exact names
  3. โš™๏ธ Add the hooks block to .claude/settings.json
  4. โœ… Inside Claude Code, run /hooks and check the four events are listed (edits to settings files are picked up automatically, the /hooks menu is just your receipt)

the shortcut

Too lazy to copy files? ๐Ÿ’ฌ

Paste this into Claude Code inside your project and it builds the whole loop for you:

๐Ÿ’ฌ build the whole loop for me
Build me a self-correcting loop with Claude Code hooks, following the official hooks reference at code.claude.com/docs/en/hooks. Create .claude/hooks/ with four files: (1) log_tool.py, registered on PostToolUse and PostToolUseFailure, appends one line per tool call (time, ok or FAIL, tool name, first 160 chars of tool_input) to .claude/hooks/logs/YYYY-MM-DD.log. (2) log_run.py, registered on Stop, appends the run's closing message to the same log. (3) review_trigger.py, registered on Stop, counts finished runs in .claude/hooks/run_count.txt and on every 5th run prints {"decision": "block", "reason": "..."} telling you to read the logs for repeated failures, slow spots, bad outputs and quality problems, then rewrite .claude/hooks/lessons.md as plain one-line rules, 20 max. (4) load_lessons.sh, registered on SessionStart, prints lessons.md to stdout so it loads into context. Register everything in .claude/settings.json with $CLAUDE_PROJECT_DIR paths, show me the final hooks block, then prove logging works with one test tool call.

prove it works

Test that it caught something ๐Ÿงช

  1. โ–ถ๏ธ Ask Claude something small ("list the files in this folder"), then open .claude/hooks/logs/ and check today's file has ok lines in it
  2. ๐Ÿ’ฅ Now make it fail on purpose: ask Claude to run cat a_file_that_does_not_exist.txt. A FAIL line appears in the log, that is PostToolUseFailure catching it
  3. ๐Ÿ” Fast-forward the review instead of waiting five runs: run echo 4 > .claude/hooks/run_count.txt, finish one more request, and watch Claude hold the door, read its own logs, and write lessons.md before it stops
  4. ๐Ÿ” Start a fresh session and ask "what lessons are loaded from past runs?". It should recite the rule book, because file 4 printed it into context at boot

what to watch

Honest caveats ๐Ÿšง

  • โš ๏ธ Hooks run on YOUR machine with YOUR permissions, they are arbitrary shell commands. Never paste a hooks block you don't understand. Start read-only, exactly like this build: it reads the run report and appends to its own log folder, it never edits or deletes your files. Add write powers only after you trust the loop
  • ๐Ÿ•ต๏ธ Logs contain snippets of tool inputs (file names, commands). In a shared repo, add .claude/hooks/logs/ and run_count.txt to .gitignore, and keep lessons.md committed only if you want the team sharing one rule book
  • ๐Ÿ“ Keep the rule book short. The reviewer caps it at 20 one-line rules so your boot context stays light
  • ๐Ÿง‘โ€โš–๏ธ The review is only as good as the model doing it. Skim lessons.md the first few times and delete anything wrong, it is just a text file
  • ๐Ÿ… The power move: when a rule proves itself, move it into CLAUDE.md (the file Claude reads every session) and it sticks for good
  • ๐Ÿ—“ Verified against the official hooks reference in July 2026. Event names are case-sensitive and evolve with releases, so if something stops firing, check the reference first, then /hooks

The links ๐Ÿ”—

Follow @cindiezhu for more AI tips every single day ๐ŸŒฑ