Agent Memory
Hook
Hooks
What no-no-debug runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
$ npx -y skills add summerliuuu/no-no-debug --agent claude-codeShips with no-no-debug. Installing the plugin gets these hooks.
Where it lives
- hooks/post_tool_failure.shGitHub
Read the script
#!/usr/bin/env bash # PostToolUseFailure hook — log failures + warn on consecutive same-tool failures. # # 1. Appends a log line to error_log.md (original behavior) # 2. Tracks recent failures in /tmp/.nnd_tool_fail_history # 3. If the same tool fails 2+ times within 5 minutes, outputs a warning # that tells the AI to switch to an alternative tool. # # Fails silent on any error. set -u log_dir="$HOME/.claude/memory" log_file="$log_dir/error_log.md" history_file="/tmp/.nnd_tool_fail_history" mkdir -p "$log_dir" 2>/dev/null || exit 0 touch "$log_file" 2>/dev/null || exit 0 ts="$(date '+%Y-%m-%d %H:%M')" now_epoch="$(date +%s)" if ! command -v jq >/dev/null 2>&1; then printf '[%s] TOOL_FAIL | unknown | jq_not_installed\n' "$ts" >> "$log_file" exit 0 fi payload="$(cat)" tool_name="$(printf '%s' "$payload" | jq -r '.tool_name // empty' 2>/dev/null)" || tool_name="" error_summary="$(printf '%s' "$payload" | jq -r '.error // .stderr // .message // "unknown_error"' 2>/dev/null)" || error_summary="" if [ -z "$tool_name" ]; then printf '[%s] TOOL_FAIL | unknown | parse_error\n' "$ts" >> "$log_file" exit 0 fi case "$tool_name" in Bash) category="BUILD_FAIL" ;; Write|Edit) category="FILE_FAIL" ;; *) category="TOOL_FAIL" ;; esac error_summary="$(printf '%s' "$error_summary" | tr '\n' ' ' | head -c 120)" printf '[%s] %s | %s | %s\n' "$ts" "$category" "$tool_name" "$error_summary" >> "$log_file" # --- Consecutive failure detection --- touch "$history_file" 2>/dev/null || exit 0 # Append this failure printf '%s|%s|%s\n' "$now_epoch" "$tool_name" "$error_summary" >> "$history_file" 2>/dev/null # Clean entries older than 5 minutes cutoff=$((now_epoch - 300)) if [ -f "$history_file" ]; then awk -F'|' -v cutoff="$cutoff" '$1 >= cutoff' "$history_file" > "${history_file}.tmp" 2>/dev/null && \ mv "${history_file}.tmp" "$history_file" 2>/dev/null fi # Count recent failures for this tool count=0 if [ -f "$history_file" ]; then count="$(grep -c "|${tool_name}|" "$history_file" 2>/dev/null)" || count=0 fi if [ "$count" -ge 2 ]; then case "$tool_name" in Grep|Glob) alt="Bash with 'find' and 'grep -r'" ;; Read) alt="Bash with 'ls' to verify path first, then Read" ;; mcp__playwright__*|mcp__pinchtab__*|mcp__agent-browser__*) alt="a different browser tool, or Bash with 'curl'" ;; *) alt="Bash or an alternative approach" ;; esac printf '[no-no-debug] %s has failed %d times in 5 minutes. Switch to %s.\n' "$tool_name" "$count" "$alt" fi exit 0 - hooks/review_reminder.pyGitHub
Read the script
#!/usr/bin/env python3 """UserPromptSubmit hook — remind when error review is overdue. Reads ~/.claude/memory/error_tracker.md for the Last Review Date and the configured review frequency (default: 3 days). If enough days have elapsed, prints a one-line reminder that the AI will act on to start the Mechanism 3 review cycle. A 4-hour cooldown (via a temp file) prevents the reminder from firing on every single user message within the same session. Fails silent on any error. A broken hook must never interrupt the user. """ from __future__ import annotations import sys import os from datetime import datetime from pathlib import Path TRACKER = Path.home() / ".claude" / "memory" / "error_tracker.md" COOLDOWN_FILE = Path("/tmp/.nnd_review_reminded") COOLDOWN_HOURS = 4 def main() -> int: try: sys.stdin.read() except Exception: pass if not TRACKER.exists(): return 0 if COOLDOWN_FILE.exists(): try: mtime = COOLDOWN_FILE.stat().st_mtime if (datetime.now().timestamp() - mtime) / 3600 < COOLDOWN_HOURS: return 0 except Exception: pass try: content = TRACKER.read_text(encoding="utf-8") except Exception: return 0 freq = 3 for line in content.splitlines(): if "review_frequency:" in line: try: freq = int(line.split("review_frequency:")[1].strip().rstrip(" ->")) except (ValueError, IndexError): pass for line in content.splitlines(): if line.startswith("Last Review Date:"): date_str = line.split(":", 1)[1].strip() try: last_review = datetime.strptime(date_str, "%Y-%m-%d") days_since = (datetime.now() - last_review).days if days_since >= freq: print( f"[no-no-debug] Error review overdue " f"({days_since} days since last review on {date_str}). " f"Run: error review" ) try: COOLDOWN_FILE.write_text(str(datetime.now())) except Exception: pass except ValueError: pass break return 0 if __name__ == "__main__": sys.exit(main()) - hooks/user_prompt_filter.pyGitHub
Read the script
#!/usr/bin/env python3 """UserPromptSubmit hook — log genuine user corrections to error_log.md. Claude Code delivers the UserPromptSubmit payload to this script via stdin as JSON. We extract the user's prompt, strip all XML-style context blocks (<system-reminder>, <task-notification>, <project-memory-context>, etc.) so that keywords appearing in system-injected context or past-work memory cannot false-trigger the detector, then match the remaining user-authored text against a tight list of second-person correction phrases. On a match, appends a single line to ~/.claude/memory/error_log.md: [YYYY-MM-DD HH:MM] USER_CORRECTION | <first 200 chars, whitespace flattened> Fails silent on any error. A broken hook must never interrupt the user. """ from __future__ import annotations import json import re import sys from datetime import datetime from pathlib import Path LOG_PATH = Path.home() / ".claude" / "memory" / "error_log.md" # Tight second-person correction phrases only. # Bare `不对` / `错了` / `wrong` / `again` false-trigger on memory context, # code snippets, documentation and injected system reminders that happen to # contain the word, so they are NOT included here. PATTERNS = [ r"你又错了", r"你错了", r"你搞错", r"你说错", r"不对啊", r"不对不对", r"这不对", r"这明显不对", r"不是这样", r"不是这个意思", r"不是这么", # Python 3's \b treats CJK chars as word chars, so we use ASCII-only # lookarounds instead — this avoids false matches where an English phrase # is embedded in Chinese text and vice versa. r"(?<![A-Za-z])wrong again(?![A-Za-z])", r"(?<![A-Za-z])that'?s wrong(?![A-Za-z])", r"(?<![A-Za-z])that'?s not right(?![A-Za-z])", r"(?<![A-Za-z])you already (did|said)(?![A-Za-z])", r"(?<![A-Za-z])nope(?![A-Za-z])", ] CORRECTION_RE = re.compile("|".join(PATTERNS), re.IGNORECASE) # Strip <tag>...</tag> blocks (with attributes), looping until stable so # adjacent / nested blocks all get removed, then drop any stray tags. XML_BLOCK_RE = re.compile(r"<([A-Za-z][\w:-]*)\b[^>]*>.*?</\1>", re.DOTALL) XML_STRAY_RE = re.compile(r"<[^>]+>") # Strip fenced code blocks (``` … ```) and inline code spans (` … `) so a # user pasting code that contains a correction phrase (e.g. a string literal # "wrong again") doesn't false-trigger. FENCED_CODE_RE = re.compile(r"```[\s\S]*?```", re.DOTALL) INLINE_CODE_RE = re.compile(r"`[^`\n]+`") WHITESPACE_RE = re.compile(r"\s+") # Normalise Unicode smart quotes and dashes to ASCII so macOS auto-correct # (' → ' / ’, " → “ / ”) doesn't cause matches like "that's wrong" to miss. SMART_CHARS = str.maketrans({ "\u2018": "'", "\u2019": "'", "\u201A": "'", "\u201B": "'", "\u201C": '"', "\u201D": '"', "\u201E": '"', "\u201F": '"', }) def strip_xml(text: str) -> str: prev = None while prev != text: prev = text text = XML_BLOCK_RE.sub(" ", text) text = XML_STRAY_RE.sub(" ", text) text = FENCED_CODE_RE.sub(" ", text) text = INLINE_CODE_RE.sub(" ", text) text = text.translate(SMART_CHARS) return WHITESPACE_RE.sub(" ", text).strip() def main() -> int: try: payload = json.load(sys.stdin) except Exception: return 0 prompt = payload.get("prompt") or "" if not prompt: return 0 cleaned = strip_xml(prompt) if not cleaned or not CORRECTION_RE.search(cleaned): return 0 try: LOG_PATH.parent.mkdir(parents=True, exist_ok=True) excerpt = cleaned[:200].replace("\n", " ") stamp = datetime.now().strftime("%Y-%m-%d %H:%M") with LOG_PATH.open("a", encoding="utf-8") as fh: fh.write(f"[{stamp}] USER_CORRECTION | {excerpt}\n") except Exception: return 0 return 0 if __name__ == "__main__": sys.exit(main())
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withno-no-debug
A self-evolution system for AI coding assistants "auto-triggers every 3 days" but had no enforcement mechanism — the review only ran when manually invoked, and the tracker went stale silently.
Get the whole plugin

