Development
Hook
Hooks
What fableclaudemdforopus 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 altafino/FableClaudeMDForOpus --agent claude-codeShips with fableclaudemdforopus. Installing the plugin gets these hooks.
Where it lives
- hooks/guard.pyGitHub
Read the script
#!/usr/bin/env python3 """Guardrails companion — PreToolUse enforcement (roadmap Phase B1). Mechanically enforces: iron rule 1/C1 (no Edit of an un-Read file), iron rule 2 (no Write over an existing file), C2 (no edits under generated/vendored paths), hard stop 2 (no git push without user authorization), hard stop 3 (no kill-by-image-name), SEC4 (no staged secrets at git commit). Deny = exit 2 with the reason on stderr (shown to the model). Bypass convention: GUARDRAILS_BYPASS=1 allows the call and appends one line to <cwd>/.claude/guardrails-bypass.log so the auditor can review overrides. """ import json import os import re import subprocess import sys import tempfile import time GENERATED = re.compile( r"(^|/)(dist|build|out|gen|\.next|target|node_modules|vendor|coverage)(/|$)" r"|\.min\.|\.map$" r"|(^|/)(package-lock\.json|yarn\.lock|pnpm-lock\.yaml|poetry\.lock|Cargo\.lock)$" ) # Assignment-with-value shape, not bare words — kit docs legitimately contain # the words "password|secret|token" in rule text. SECRETS = re.compile( r"(?i)(password|secret|token|api[_-]?key)\s*[:=]\s*['\"][^'\"]{8,}" r"|BEGIN [A-Z ]*PRIVATE KEY" ) KILL_BY_NAME = re.compile(r"(?i)\b(pkill|killall)\b|\btaskkill\b[^|;&]*/(im)\b") GIT_PUSH = re.compile(r"\bgit\b[^|;&]*\bpush\b") GIT_COMMIT = re.compile(r"\bgit\b[^|;&]*\bcommit\b") ALLOW_PUSH_TTL_S = 1800 # scripts/allow-push grants one push within 30 min def reads_state_path(session_id: str) -> str: return os.path.join(tempfile.gettempdir(), f"guardrails-reads-{session_id}.txt") def deny(msg: str) -> None: print(msg, file=sys.stderr) sys.exit(2) def log_bypass(cwd: str, tool: str, detail: str) -> None: try: d = os.path.join(cwd, ".claude") os.makedirs(d, exist_ok=True) with open(os.path.join(d, "guardrails-bypass.log"), "a") as f: f.write(f"{time.strftime('%Y-%m-%dT%H:%M:%S')} BYPASS {tool} {detail}\n") except OSError: pass def main() -> None: try: data = json.load(sys.stdin) except (json.JSONDecodeError, ValueError): sys.exit(0) # unparseable input: never block on our own bug tool = data.get("tool_name", "") ti = data.get("tool_input") or {} sid = data.get("session_id", "unknown") cwd = data.get("cwd") or os.getcwd() if os.environ.get("GUARDRAILS_BYPASS") == "1": log_bypass(cwd, tool, json.dumps(ti)[:200]) sys.exit(0) if tool in ("Edit", "Write"): path = ti.get("file_path", "") norm = path.replace("\\", "/") if GENERATED.search(norm): deny( f"guardrails C2: {path} looks generated/vendored — edit the source or the " "generator and re-run it instead (docs/guardrails/CODE.md C2). " "(false positive? GUARDRAILS_BYPASS=1)" ) exists = os.path.exists(path) known = False rf = reads_state_path(sid) if os.path.exists(rf): with open(rf) as f: known = path in {line.strip() for line in f} if tool == "Edit" and exists and not known: deny( f"guardrails iron rule 1 / C1: no Read of {path} recorded this session — " "Read the enclosing scope first, then retry the Edit. " "(state lost after crash/compaction? GUARDRAILS_BYPASS=1, logged)" ) if tool == "Write" and exists: deny( f"guardrails iron rule 2: {path} exists — modify with Edit, never Write. " "Sole exception: the rewrite procedure in docs/guardrails/CODE.md " "('You are rewriting instead of editing'); follow it, then GUARDRAILS_BYPASS=1." ) sys.exit(0) if tool == "Bash": cmd = ti.get("command", "") if KILL_BY_NAME.search(cmd): deny( "guardrails hard stop 3: never kill by image name -> find the PID via the " "port (lsof -ti :PORT | netstat -ano | findstr :PORT) and kill that PID. " "(bypass: GUARDRAILS_BYPASS=1)" ) if GIT_PUSH.search(cmd): flag = os.path.join(cwd, ".claude", ".allow-push") if os.path.exists(flag) and time.time() - os.path.getmtime(flag) < ALLOW_PUSH_TTL_S: try: os.remove(flag) # single-use grant except OSError: pass sys.exit(0) deny( "guardrails hard stop 2: git push blocked — needs the user's own action: " "run scripts/allow-push (grants ONE push for 30 min), then retry. " "(bypass: GUARDRAILS_BYPASS=1, logged)" ) if GIT_COMMIT.search(cmd): try: diff = subprocess.run( ["git", "-C", cwd, "diff", "--cached"], capture_output=True, text=True, timeout=15, ).stdout except (OSError, subprocess.SubprocessError): diff = "" hits = [l[:120] for l in diff.splitlines() if l.startswith("+") and SECRETS.search(l)] if hits: deny( "guardrails SEC4: staged diff matches secret patterns:\n" + "\n".join(hits[:5]) + "\nRedact/unstage, or confirm false positive with the user. " "(bypass: GUARDRAILS_BYPASS=1, logged)" ) sys.exit(0) sys.exit(0) if __name__ == "__main__": main() - hooks/rearm.pyGitHub
Read the script
#!/usr/bin/env python3 """Guardrails companion — SessionStart re-arm hook (roadmap Phase C1 + C4). On session start after a compaction or /resume, deterministically re-inject the kit's re-arm instruction (routing row 6 / SESSION.md S1) instead of relying on the CLAUDE.md footer surviving the model's attention. On every start, report docs/STATE.md freshness (C4). Silent when the cwd has no guardrails kit. stdout from a SessionStart hook is added to the session's context. """ import json import os import sys import time STALE_S = 24 * 3600 def kit_installed(cwd: str) -> bool: p = os.path.join(cwd, "CLAUDE.md") try: with open(p) as f: return "guardrails-kit" in f.readline() except OSError: return False def main() -> None: try: data = json.load(sys.stdin) except (json.JSONDecodeError, ValueError): sys.exit(0) cwd = data.get("cwd") or os.getcwd() if not kit_installed(cwd): sys.exit(0) source = data.get("source", "startup") lines = [] if source in ("compact", "resume"): lines.append( "guardrails re-arm: routing row 6 has fired — write its TRIGGER line and Read " "docs/guardrails/SESSION.md (S1 runs first: Read docs/STATE.md, run git status + " "git diff --stat HEAD, restate Goal/Next). Docs read before compaction no longer " "count as read; compaction-summary claims are UNVERIFIED until re-checked." ) state = os.path.join(cwd, "docs", "STATE.md") if not os.path.exists(state): lines.append( "guardrails C4: docs/STATE.md is missing — create it per docs/guardrails/SESSION.md " "S2 before file-modifying work." ) else: age = time.time() - os.path.getmtime(state) if age > STALE_S: lines.append( f"guardrails C4: docs/STATE.md last updated {int(age // 3600)}h ago — treat its " "Now/Next as stale until refreshed (SESSION.md S3)." ) if lines: print("\n".join(lines)) sys.exit(0) if __name__ == "__main__": main() - hooks/stop_verify.pyGitHub
Read the script
#!/usr/bin/env python3 """Guardrails companion — Stop-hook done-claim verifier (roadmap Phase B2). At turn end: if the session edited files AND the final assistant message claims done/fixed/works/passing/complete/resolved/ready WITHOUT a legal VERIFY.md form (`Verified:` / `UNVERIFIED` / V-lines / canonical statuses), block the stop and point at docs/guardrails/VERIFY.md. Match scope: assistant prose only — fenced code blocks are stripped first. Heuristic v0; bypass: GUARDRAILS_BYPASS=1. """ import json import os import re import sys CLAIM = re.compile(r"\b(done|fixed|works|passing|completed?|resolved|ready)\b", re.I) EVIDENCE = re.compile( r"Verified:|UNVERIFIED|EDITED-UNVERIFIED|NOT-DONE|CANNOT-REPRODUCE|V\d+:\s*(PASS|FAIL|N/A)" ) EDIT_TOOLS = ("Edit", "Write", "NotebookEdit") def main() -> None: try: data = json.load(sys.stdin) except (json.JSONDecodeError, ValueError): sys.exit(0) if data.get("stop_hook_active"): # loop guard: never block our own re-entry sys.exit(0) if os.environ.get("GUARDRAILS_BYPASS") == "1": sys.exit(0) tp = data.get("transcript_path") if not tp or not os.path.exists(tp): sys.exit(0) had_edit = False last_text = "" with open(tp, encoding="utf-8", errors="replace") as f: for line in f: line = line.strip() if not line: continue try: e = json.loads(line) except json.JSONDecodeError: continue content = (e.get("message") or {}).get("content") if not isinstance(content, list): continue for b in content: if isinstance(b, dict) and b.get("type") == "tool_use" and b.get("name") in EDIT_TOOLS: had_edit = True if e.get("type") == "assistant": texts = [ b.get("text", "") for b in content if isinstance(b, dict) and b.get("type") == "text" ] if texts: last_text = "\n".join(texts) if not had_edit: sys.exit(0) # nothing was edited; completion claims need no run evidence prose = re.sub(r"```.*?```", "", last_text, flags=re.S) if CLAIM.search(prose) and not EVIDENCE.search(prose): print( "guardrails VERIFY: the final message claims completion without a legal status " "form. Use `Verified: <command> -> <result line>` or `UNVERIFIED — to confirm, " "run: <command>` per docs/guardrails/VERIFY.md, or run the verification now. " "(bypass: GUARDRAILS_BYPASS=1)", file=sys.stderr, ) sys.exit(2) sys.exit(0) if __name__ == "__main__": main() - hooks/test_hooks.shGitHub
Read the script
#!/usr/bin/env bash # Guardrails companion test suite — every deny's trigger case + its bypass path. set -u HERE="$(cd "$(dirname "$0")" && pwd)" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT SID="testsess-$$" STATE="${TMPDIR:-/tmp}/guardrails-reads-$SID.txt" rm -f "$STATE" PASS=0; FAIL=0 run_guard() { # $1=json -> echoes exit code printf '%s' "$1" | python3 "$HERE/guard.py" >/dev/null 2>"$TMP/err"; echo $? } check() { # $1=name $2=expected_exit $3=actual_exit if [ "$2" = "$3" ]; then PASS=$((PASS+1)); echo " ok $1" else FAIL=$((FAIL+1)); echo "FAIL $1 (expected exit $2, got $3)"; sed 's/^/ /' "$TMP/err"; fi } j() { python3 -c 'import json,sys; print(json.dumps({"session_id":sys.argv[1],"cwd":sys.argv[2],"tool_name":sys.argv[3],"tool_input":json.loads(sys.argv[4])}))' "$SID" "$1" "$2" "$3"; } echo "== guard.py: Edit/Write ==" F="$TMP/app.py"; echo "x = 1" > "$F" check "Edit un-Read file -> deny" 2 "$(run_guard "$(j "$TMP" Edit "{\"file_path\":\"$F\"}")")" printf '%s' "$(j "$TMP" Read "{\"file_path\":\"$F\"}")" | python3 "$HERE/track.py" check "Edit after Read -> allow" 0 "$(run_guard "$(j "$TMP" Edit "{\"file_path\":\"$F\"}")")" check "Write existing file -> deny" 2 "$(run_guard "$(j "$TMP" Write "{\"file_path\":\"$F\"}")")" check "Write new file -> allow" 0 "$(run_guard "$(j "$TMP" Write "{\"file_path\":\"$TMP/new.py\"}")")" check "Edit under node_modules -> deny" 2 "$(run_guard "$(j "$TMP" Edit "{\"file_path\":\"$TMP/node_modules/a.js\"}")")" check "Write lockfile -> deny" 2 "$(run_guard "$(j "$TMP" Write "{\"file_path\":\"$TMP/package-lock.json\"}")")" echo "== guard.py: Bash ==" check "pkill by name -> deny" 2 "$(run_guard "$(j "$TMP" Bash '{"command":"pkill node"}')")" check "taskkill /IM -> deny" 2 "$(run_guard "$(j "$TMP" Bash '{"command":"taskkill /IM node.exe /F"}')")" check "kill by PID -> allow" 0 "$(run_guard "$(j "$TMP" Bash '{"command":"kill 12345"}')")" check "git push w/o flag -> deny" 2 "$(run_guard "$(j "$TMP" Bash '{"command":"git push origin dev"}')")" mkdir -p "$TMP/.claude"; touch "$TMP/.claude/.allow-push" check "git push with flag -> allow" 0 "$(run_guard "$(j "$TMP" Bash '{"command":"git push origin dev"}')")" [ ! -e "$TMP/.claude/.allow-push" ] && { PASS=$((PASS+1)); echo " ok push flag consumed (single-use)"; } || { FAIL=$((FAIL+1)); echo "FAIL push flag not consumed"; } echo "== guard.py: secret scan on commit ==" REPO="$TMP/repo"; mkdir -p "$REPO"; git -C "$REPO" init -q echo 'api_key = "sk-supersecret-12345"' > "$REPO/cfg.py"; git -C "$REPO" add cfg.py check "commit with staged secret -> deny" 2 "$(run_guard "$(j "$REPO" Bash '{"command":"git commit -m x"}')")" git -C "$REPO" reset -q; echo 'name = "hello"' > "$REPO/cfg.py"; git -C "$REPO" add cfg.py check "commit clean staged -> allow" 0 "$(run_guard "$(j "$REPO" Bash '{"command":"git commit -m x"}')")" echo "== bypass + log ==" EC=$(printf '%s' "$(j "$TMP" Write "{\"file_path\":\"$F\"}")" | GUARDRAILS_BYPASS=1 python3 "$HERE/guard.py" >/dev/null 2>&1; echo $?) check "bypass allows denied call" 0 "$EC" grep -q "BYPASS Write" "$TMP/.claude/guardrails-bypass.log" 2>/dev/null \ && { PASS=$((PASS+1)); echo " ok bypass logged"; } || { FAIL=$((FAIL+1)); echo "FAIL bypass not logged"; } echo "== stop_verify.py ==" mk_transcript() { # $1=file $2=claim-text $3=with_edit(0/1) : > "$1" [ "$3" = "1" ] && printf '%s\n' '{"type":"assistant","message":{"content":[{"type":"tool_use","name":"Edit","input":{"file_path":"a.py"}}]}}' >> "$1" python3 -c 'import json,sys; print(json.dumps({"type":"assistant","message":{"content":[{"type":"text","text":sys.argv[1]}]}}))' "$2" >> "$1" } run_stop() { printf '{"transcript_path":"%s","stop_hook_active":false}' "$1" | python3 "$HERE/stop_verify.py" >/dev/null 2>&1; echo $?; } T="$TMP/t1.jsonl"; mk_transcript "$T" "The bug is fixed now." 1 check "edited + bare done-claim -> block" 2 "$(run_stop "$T")" T="$TMP/t2.jsonl"; mk_transcript "$T" "Fixed. Verified: pytest -> 3 passed" 1 check "edited + Verified: -> allow" 0 "$(run_stop "$T")" T="$TMP/t3.jsonl"; mk_transcript "$T" "All done for today!" 0 check "no edits + done-claim -> allow" 0 "$(run_stop "$T")" T="$TMP/t4.jsonl"; mk_transcript "$T" "Edited files. EDITED-UNVERIFIED: a.py" 1 check "edited + EDITED-UNVERIFIED -> allow" 0 "$(run_stop "$T")" echo "== rearm.py ==" KITDIR="$TMP/kitproj"; mkdir -p "$KITDIR/docs" echo '<!-- guardrails-kit: v1.2 -->' > "$KITDIR/CLAUDE.md" run_rearm() { printf '{"cwd":"%s","source":"%s"}' "$1" "$2" | python3 "$HERE/rearm.py" 2>/dev/null; } OUT=$(run_rearm "$KITDIR" compact) echo "$OUT" | grep -q "routing row 6" && echo "$OUT" | grep -q "STATE.md is missing" \ && { PASS=$((PASS+1)); echo " ok compact -> re-arm + missing-STATE nudge"; } \ || { FAIL=$((FAIL+1)); echo "FAIL compact re-arm output: $OUT"; } mkdir -p "$KITDIR/docs"; echo x > "$KITDIR/docs/STATE.md" OUT=$(run_rearm "$KITDIR" startup) [ -z "$OUT" ] && { PASS=$((PASS+1)); echo " ok startup + fresh STATE -> silent"; } \ || { FAIL=$((FAIL+1)); echo "FAIL startup not silent: $OUT"; } NOKIT="$TMP/nokit"; mkdir -p "$NOKIT" OUT=$(run_rearm "$NOKIT" compact) [ -z "$OUT" ] && { PASS=$((PASS+1)); echo " ok non-kit project -> silent"; } \ || { FAIL=$((FAIL+1)); echo "FAIL non-kit not silent: $OUT"; } rm -f "$STATE" echo echo "RESULT: $PASS passed, $FAIL failed" [ "$FAIL" -eq 0 ] - hooks/track.pyGitHub
Read the script
#!/usr/bin/env python3 """Guardrails companion — PostToolUse read/edit tracker (roadmap Phase B1). Records file paths the session has Read (or successfully Edited/Written) into a per-session state file; hooks/guard.py checks it to enforce iron rule 1 / C1. """ import json import os import sys import tempfile def main() -> None: try: data = json.load(sys.stdin) except (json.JSONDecodeError, ValueError): sys.exit(0) if data.get("tool_name") not in ("Read", "Edit", "Write"): sys.exit(0) path = (data.get("tool_input") or {}).get("file_path") if not path: sys.exit(0) state = os.path.join( tempfile.gettempdir(), f"guardrails-reads-{data.get('session_id', 'unknown')}.txt" ) try: existing = set() if os.path.exists(state): with open(state) as f: existing = {line.strip() for line in f} if path not in existing: with open(state, "a") as f: f.write(path + "\n") except OSError: pass sys.exit(0) 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 withfableclaudemdforopus
A portable CLAUDE.md + documentation set that makes Claude Opus / Sonnet operate as close to frontier (Fable) level as possible inside Claude Code: fewer logic errors, fewer introduced bugs, fewer wasted tokens.
Get the whole plugin

