Agent Memory
Hook
Hooks
What andon runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Where it lives
- hooks/auto_orient.pyGitHub
Read the script
#!/usr/bin/env python3 """ auto_orient.py — UserPromptSubmit hook that loads your MEMORY.md index into context at the start of a session, so the agent starts oriented instead of amnesiac. This is the other half of the anti-amnesia loop: the wrap protocol SAVES state at session end; orient LOADS it at the start. Two hooks, one loop. USAGE 1. Copy to your agent config dir (e.g. ~/.claude/hooks/). 2. Register as a UserPromptSubmit hook in settings.json — MERGE into any existing array: { "hooks": { "UserPromptSubmit": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "python3 /ABSOLUTE/PATH/TO/.claude/hooks/auto_orient.py" }] }] } } 3. It looks for `memory/MEMORY.md` (then `MEMORY.md`) in the project; override with AUTO_ORIENT_MEMORY_PATH=/abs/path/to/MEMORY.md 4. It injects the index ONCE per session (not every turn — that would waste context). Turn it off with AUTO_ORIENT=off. 5. It caps the index text at 8000 bytes (a one-line header and a truncation note are added on top); override with AUTO_ORIENT_MAX_BYTES=N. A value that is not a positive whole number falls back to 8000. Fail-safe by design: any error, a missing file, or an already-oriented session → injects nothing and never blocks the prompt. (Contract: UserPromptSubmit context injection = {"hookSpecificOutput": {"hookEventName": "UserPromptSubmit", "additionalContext": "..."}}, exit 0.) """ from __future__ import annotations import hashlib import json import os import sys import tempfile from pathlib import Path DEFAULT_MAX_BYTES = 8000 def _max_bytes(): # Parsed at call time, not import time: an int() at module level ran outside the # fail-safe try below, so AUTO_ORIENT_MAX_BYTES=abc crashed the hook (exit 1). try: n = int(os.environ.get("AUTO_ORIENT_MAX_BYTES", "")) except ValueError: return DEFAULT_MAX_BYTES return n if n > 0 else DEFAULT_MAX_BYTES # 0 / negative would keep nothing def _emit_context(text): sys.stdout.write( json.dumps( { "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": text, } } ) + "\n" ) sys.exit(0) def _noop(): sys.exit(0) def _find_memory(cwd): override = os.environ.get("AUTO_ORIENT_MEMORY_PATH") candidates = [] if override: candidates.append(Path(override).expanduser()) if cwd: candidates += [Path(cwd) / "memory" / "MEMORY.md", Path(cwd) / "MEMORY.md"] for c in candidates: try: if c.is_file(): return c except OSError: continue return None def main(): if os.environ.get("AUTO_ORIENT", "on").lower() == "off": _noop() try: data = json.loads(sys.stdin.read() or "{}") except json.JSONDecodeError: _noop() cwd = data.get("cwd") or os.getcwd() session_id = data.get("session_id") or "" mem = _find_memory(cwd) if not mem: _noop() # Read FIRST — a failed read must NOT mark the session oriented (else it never retries). try: text = mem.read_text(errors="ignore") except OSError: _noop() if not text.strip(): _noop() # Once-per-session guard, only after a successful read. Don't re-inject every prompt. if session_id: key = hashlib.sha1(f"{session_id}:{mem}".encode()).hexdigest()[:16] marker = Path(tempfile.gettempdir()) / f"auto_orient_{key}" try: if marker.exists(): _noop() marker.write_text("1") except OSError: pass # can't mark → still inject once now (safe, just not deduped) # Over the byte cap: keep whole lines (never cut mid-character). Only a first line # that is over the cap by itself gets cut mid-line, on a character boundary. max_bytes = _max_bytes() if len(text.encode()) > max_bytes: kept, total = [], 0 for ln in text.splitlines(keepends=True): b = len(ln.encode()) if total + b > max_bytes: break kept.append(ln) total += b if not kept: # The first line alone is over the cap: cut it at the cap on a character # boundary (drop a split multi-byte tail) rather than inject only the notice. kept = [text.encode()[:max_bytes].decode("utf-8", errors="ignore")] text = "".join(kept).rstrip() + "\n\n…[truncated — open the file for the rest]" # Show a path relative to the project when possible (disambiguates root vs memory/). try: display_path = mem.relative_to(Path(cwd)) except ValueError: display_path = mem.name _emit_context(f"[Project memory — loaded from {display_path} at session start]\n\n{text}") if __name__ == "__main__": try: main() except Exception: # Fail-safe: never block a prompt because the hook errored. _noop() - hooks/claim_check_hook.pyGitHub
Read the script
#!/usr/bin/env python3 """ claim_check_hook.py — Stop-hook enforcer skeleton. Blocks turn-end if the assistant claims "done / shipped / verified / complete" without a recent verification log entry that refers to that specific claim and was written in the same session. Make the agent's done-claim mean something. USAGE 1. Copy hooks/claim_check_hook.py + hooks/log_claim.py into your agent config dir (e.g. ~/.claude/hooks/). 2. Register as a Stop hook in settings.json — MERGE into any existing "Stop" array, don't overwrite it: { "hooks": { "Stop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "CLAIM_CHECK_ENFORCE_MODE=warn python3 /abs/path/to/claim_check_hook.py" }] }] } } 3. Tell your agent (in CLAUDE.md) that after it verifies a done-claim it must log it: python3 /abs/path/to/log_claim.py "what I claimed" "how I verified it" log_claim.py creates ~/.claude/state/claim_checks/log.jsonl on first run. 4. Test it fired: echo '{"transcript_path":"/dev/null"}' | python3 claim_check_hook.py → should print {"continue": true, ...} 5. Modes (default = warn; promote to block once you trust it): CLAIM_CHECK_ENFORCE_MODE=warn # surface a warning, don't block (DEFAULT) CLAIM_CHECK_ENFORCE_MODE=block # block turn-end until a fresh log entry exists CLAIM_CHECK_ENFORCE_MODE=off # silent pass-through 6. Evidence source (opt-in, v0.2; independent of the mode above): CLAIM_CHECK_EVIDENCE=log # the agent's own log_claim.py line (DEFAULT) CLAIM_CHECK_EVIDENCE=tool # a harness-recorded PASSING verification run in the # transcript, this session, inside the window CLAIM_CHECK_EVIDENCE=both # both of the above A non-blank unrecognized value is treated as "both" (strictest), not as "log"; a blank value means "log". In tool/both mode an internal error fails CLOSED. CLAIM_CHECK_VERIFY_COMMANDS="pytest,make test" overrides the allowlist. NOTE — this is a STOP hook; Stop hooks block with {"decision": "block"}. A PreToolUse hook uses a DIFFERENT schema ({"hookSpecificOutput": {"permissionDecision": "deny"}}). The two are NOT interchangeable — using the wrong one is a silent no-op. KNOWN LIMITS (binding is a forcing function, not a cryptographic guarantee): - Binding is by content-token overlap: it establishes TOPICAL correspondence, not proof. An agent that names the subject in its verification without doing the work can pass (legible on the record), and an incidental shared word — a file extension ("py"), an adverb ("successfully"), a number ("200") — can bind unrelated claim/evidence. - A truly subjectless closure ("Done." with no other content in the turn) is unbindable and degrades to a same-session freshness floor (still stronger than global freshness). - Two claims in the SAME sentence share that sentence's subject tokens (claims in DIFFERENT sentences are bound separately). - log_claim.py records the session from CLAUDE_CODE_SESSION_ID (set by Claude Code). Under a harness that does not set it, entries are session-less and cannot clear a session-bound claim — the verification must run in the same session that makes the claim. - CLAIM_CHECK_EVIDENCE=tool reads the transcript, which the agent's own Bash can still append to: forging a passing run is possible — deliberate, and the call that writes it is recorded, though the forging code can hide inside a script that call runs. A recorded run is not bound to the claim's subject (any passing pytest covers "the migration is complete"), it says nothing about whether the tests are meaningful or selected (-k, -run NOMATCH), and it can predate later edits. The allowlist credits a command LINE (the harness-recorded input.command), not the program that ran: a fake `pytest` on PATH, ./pytest, a shell function or (global) alias named pytest passes. Only the Bash tool's record shape is verified; Monitor, MCP tools, PowerShell and subagent (sidechain) runs are not credited. A block holds one turn (stop_hook_active). - "**DONE** Want me to deploy?" (a bold marker followed by a question on the same line) is exempted as a question — genuinely ambiguous; terminator-ended closures ("Done. Anything else?") are treated as completed assertions and DO gate. Plain bullet closers ("- Fixed the auth bug.") are a known detector-recall gap, tracked with issue #3. CUSTOMIZE - COMPLETION_VERBS — closure verbs that trigger the check (kept tight to avoid false-fires). - OPT_IN_VERBS — looser verbs; enable only if your domain needs them. - CLOSURE_NOUNS — subjects for the bare "Task complete!" / "Migration done." form. - CLAIM_CHECK_FRESH_MIN — "fresh" window (15 is forgiving; don't go below ~5). - OVERLAP_MIN_TOKENS — content tokens a log entry must share with the claim (default 1). - CLAIM_CHECKS_LOG_PATH env var — override the log location. """ from __future__ import annotations import json import os import re import sys from bisect import bisect_left, bisect_right from datetime import datetime, timezone from pathlib import Path # ---------- CONFIG (edit these) ---------- CLAIM_CHECKS_LOG = Path( os.environ.get( "CLAIM_CHECKS_LOG_PATH", str(Path.home() / ".claude" / "state" / "claim_checks" / "log.jsonl"), ) ).expanduser() CLAIM_CHECK_FRESH_MIN = 15 # minutes — log entries newer than this count as fresh STALE_THRESHOLD_S = 30 # transcript-flush race guard DEFAULT_MODE = "warn" # warn | block | off (start warn; promote to block once tuned) OVERLAP_MIN_TOKENS = 1 # a fresh log entry must share >=N content tokens with the claim # Unambiguous closure verbs — kept tight so everyday prose ("the data is current", # "are we done here?") does NOT false-fire. COMPLETION_VERBS = ( "ready", "shipped", "complete", "completed", " - hooks/log_claim.pyGitHub
Read the script
#!/usr/bin/env python3 """ log_claim.py — write the verification entry that claim_check_hook.py looks for. Run this right AFTER you actually verify a done-claim: python3 log_claim.py "what you claimed" "how you verified it" It creates ~/.claude/state/claim_checks/log.jsonl on first run, so the very first claim doesn't get blocked by a missing file/dir. Override the location with the CLAIM_CHECKS_LOG_PATH env var (must match the hook's). Each entry also stamps: - session_id — from CLAUDE_CODE_SESSION_ID (the current agent session), so one session's log cannot alibi another session's claim on a shared $HOME. - claim_fingerprint — content tokens of the claim (subject nouns, closure verbs stopworded), so the hook can require a fresh entry that plausibly refers to the specific claim being made rather than accepting any recent entry. See issue #2. """ from __future__ import annotations import json import os import re import sys from datetime import datetime, timezone from pathlib import Path CLAIM_CHECKS_LOG = Path( os.environ.get( "CLAIM_CHECKS_LOG_PATH", str(Path.home() / ".claude" / "state" / "claim_checks" / "log.jsonl"), ) ).expanduser() # ---- claim fingerprinting (MUST stay byte-identical to claim_check_hook.py) ---- # Closure verbs are stopworded: they appear in every done-claim, so binding on them # would be vacuous ("done" matches "done"). Only SUBJECT tokens (migration, endpoint, # paginator) carry the fingerprint. Cross-file consistency is pinned by a test. _STOPWORDS = frozenset({ "the", "a", "an", "is", "are", "was", "were", "be", "been", "being", "has", "have", "had", "this", "that", "these", "those", "and", "or", "but", "to", "of", "in", "on", "at", "it", "its", "for", "with", "now", "all", "not", "from", "into", "out", "up", "down", "so", "as", "by", "just", "then", "also", "ive", "weve", "our", "your", "my", "me", "we", "i", # closure verbs — must NOT bind "done", "complete", "completed", "completing", "ready", "verified", "verify", "fixed", "fix", "resolved", "resolve", "shipped", "ship", "live", "set", "wired", "implemented", "implement", "applied", "apply", "patched", "synced", "finished", "finish", "production", "working", # generic dev nouns — too common to bind a specific claim on their own "test", "tests", "code", "file", "files", "thing", "things", "stuff", "work", "item", "items", "run", "ran", }) def _content_tokens(text): toks = re.findall(r"[a-z0-9]+", (text or "").lower()) return {t for t in toks if len(t) >= 2 and t not in _STOPWORDS} def _fingerprint(*texts): # Fingerprint the claim AND the verification: an honest verification usually # names the artifact ("route", "alembic", "stripe"), so a claim worded # differently from the verification still binds. See issue #2 discussion. return " ".join(sorted(_content_tokens(" ".join(t for t in texts if t)))) def main(): claim = sys.argv[1] if len(sys.argv) > 1 else "" verification = sys.argv[2] if len(sys.argv) > 2 else "" if not claim or not verification: print( 'usage: python3 log_claim.py "<what you claim>" "<how you verified it>"', file=sys.stderr, ) sys.exit(2) CLAIM_CHECKS_LOG.parent.mkdir(parents=True, exist_ok=True) entry = { "timestamp": datetime.now(timezone.utc).isoformat(), "claim": claim, "verification": verification, "session_id": os.environ.get("CLAUDE_CODE_SESSION_ID", ""), "claim_fingerprint": _fingerprint(claim, verification), } with CLAIM_CHECKS_LOG.open("a", encoding="utf-8") as f: f.write(json.dumps(entry) + "\n") print(f"logged → {CLAIM_CHECKS_LOG}") if __name__ == "__main__": 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 withandon
Stop fixing the same AI mistake twice. A Lean quality system for AI-assisted work — operators turn recurring defects into countermeasures. Built by a manufacturing operator, for people who actually ship.
Get the whole plugin
Stats
167
Stars
13
Forks
Active
Maintenance
Python
Language
MIT
License
7h ago
Last commit
4mo ago
Created
4h ago
Added
In this plugin
See everything inside 
