Skip to content
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.

From plugin
fableclaudemdforopus
618 skills5 hooks
Install
$ npx -y skills add altafino/FableClaudeMDForOpus --agent claude-code

Ships 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
Stats
6
Stars
0
Forks
Maintained
Maintenance
Python
Language
2mo ago
Last commit
2mo ago
Created

Repo: altafino/FableClaudeMDForOpus