Security
Hook
Hooks
What vestige 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/cwd-state-injector.shGitHub
Read the script
#!/bin/bash # cwd-state-injector.sh — SessionStart + UserPromptSubmit hook # # HOOK #3 of the 2026-04-20 upgrade: ELIMINATE RE-EXPLORATION PENALTY. # # On every prompt, reads the current directory's git + CI + test state and # injects it as additionalContext so Claude starts every turn already knowing: # # - current git branch + HEAD commit + staged/unstaged file counts # - last commit subject + author # - last GitHub Actions run conclusion via gh CLI (if repo has remote) # - open PR + open issue counts # - recent test-suite status (cached if present) # # Saves ~500 tokens per prompt (Claude no longer asks "what state are we in?") # and prevents stale-state reasoning errors. # # Cached in /tmp/cwd-state-{hash}.json for 60s to keep hook fast. # Fails open: if gh or git unavailable, emits partial context. set -u INPUT="$(cat)" CWD="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("cwd",""))' 2>/dev/null || printf '')" # Fallback to PWD if cwd not in input if [ -z "$CWD" ] || [ ! -d "$CWD" ]; then CWD="$(pwd 2>/dev/null)" fi # Only run in git repos cd "$CWD" 2>/dev/null || exit 0 if ! /usr/bin/git rev-parse --is-inside-work-tree > /dev/null 2>&1; then exit 0 fi # Cache for 60s CACHE_KEY="$(printf '%s' "$CWD" | /usr/bin/shasum | awk '{print $1}')" CACHE_FILE="/tmp/cwd-state-${CACHE_KEY}.json" if [ -f "$CACHE_FILE" ]; then MTIME=$(stat -f %m "$CACHE_FILE" 2>/dev/null || stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0) NOW=$(date +%s) AGE=$((NOW - MTIME)) if [ "$AGE" -lt 60 ] && [ -s "$CACHE_FILE" ]; then cat "$CACHE_FILE" exit 0 fi fi # Gather state BRANCH="$(/usr/bin/git rev-parse --abbrev-ref HEAD 2>/dev/null)" HEAD_SHA="$(/usr/bin/git rev-parse --short HEAD 2>/dev/null)" HEAD_SUBJECT="$(/usr/bin/git log -1 --format='%s' 2>/dev/null | head -c 100)" HEAD_AUTHOR="$(/usr/bin/git log -1 --format='%an' 2>/dev/null)" STAGED_COUNT="$(/usr/bin/git diff --cached --name-only 2>/dev/null | /usr/bin/wc -l | awk '{print $1}')" UNSTAGED_COUNT="$(/usr/bin/git diff --name-only 2>/dev/null | /usr/bin/wc -l | awk '{print $1}')" UNTRACKED_COUNT="$(/usr/bin/git ls-files --others --exclude-standard 2>/dev/null | /usr/bin/wc -l | awk '{print $1}')" AHEAD_BEHIND="$(/usr/bin/git rev-list --left-right --count HEAD...@{upstream} 2>/dev/null | awk '{printf "ahead=%s behind=%s", $1, $2}' || echo "no-upstream")" # GitHub state (only if gh CLI available + remote configured) CI_STATE="" PR_COUNT="" ISSUE_COUNT="" if /usr/bin/which gh > /dev/null 2>&1 && /usr/bin/git config --get remote.origin.url > /dev/null 2>&1; then # Last CI run on current branch CI_JSON="$(gh run list --branch "$BRANCH" --limit 1 --json status,conclusion,name,headSha 2>/dev/null || echo '[]')" CI_STATE="$(printf '%s' "$CI_JSON" | /usr/bin/python3 -c 'import sys,json try: d=json.load(sys.stdin) if d: r=d[0]; print(f"{r.get(\"name\",\"?\")}:{r.get(\"status\",\"?\")}:{r.get(\"conclusion\") or \"...\"}") except: pass' 2>/dev/null)" PR_COUNT="$(gh pr list --state open --json number 2>/dev/null | /usr/bin/python3 -c 'import sys,json try: print(len(json.load(sys.stdin))) except: print("?")' 2>/dev/null)" ISSUE_COUNT="$(gh issue list --state open --json number 2>/dev/null | /usr/bin/python3 -c 'import sys,json try: print(len(json.load(sys.stdin))) except: print("?")' 2>/dev/null)" fi # Build context block REPO_NAME="$(/usr/bin/basename "$CWD")" CONTEXT_LINES=() CONTEXT_LINES+=("[CWD STATE — auto-injected, 60s cache]") CONTEXT_LINES+=(" repo: $REPO_NAME branch: $BRANCH HEAD: $HEAD_SHA") if [ -n "$HEAD_SUBJECT" ]; then CONTEXT_LINES+=(" last commit: \"$HEAD_SUBJECT\" by $HEAD_AUTHOR") fi CONTEXT_LINES+=(" working tree: staged=$STAGED_COUNT unstaged=$UNSTAGED_COUNT untracked=$UNTRACKED_COUNT") if [ "$AHEAD_BEHIND" != "no-upstream" ]; then CONTEXT_LINES+=(" vs upstream: $AHEAD_BEHIND") fi if [ -n "$CI_STATE" ]; then CONTEXT_LINES+=(" last CI run: $CI_STATE") fi if [ -n "$PR_COUNT" ] && [ -n "$ISSUE_COUNT" ]; then CONTEXT_LINES+=(" open: PRs=$PR_COUNT issues=$ISSUE_COUNT") fi # Format as JSON additionalContext JSON_OUT="$(/usr/bin/python3 <<PYEOF import json, os lines = """$(printf '%s\n' "${CONTEXT_LINES[@]}")""".strip().split("\n") ctx = "\n".join(lines) print(json.dumps({ "hookSpecificOutput": { "hookEventName": "UserPromptSubmit", "additionalContext": ctx } })) PYEOF )" # Cache and emit printf '%s' "$JSON_OUT" > "$CACHE_FILE" printf '%s' "$JSON_OUT" exit 0 - hooks/load-all-memory.shGitHub
Read the script
#!/bin/bash # Load ALL memory MD files on every UserPromptSubmit. # This legacy opt-in hook cats every file in the memory directory into prompt # context. It is intentionally not enabled by default. # Resolve per-user Claude Code project memory dir from $HOME. # Claude Code encodes home path as `-Users-<name>`; allow override via env. if [ -n "${VESTIGE_MEMORY_DIR:-}" ]; then MEM_DIR="$VESTIGE_MEMORY_DIR" else MEM_DIR="$HOME/.claude/projects/$(printf '%s' "$HOME" | tr '/' '-')/memory" fi if [ ! -d "$MEM_DIR" ]; then exit 0 fi echo "═══════════════════════════════════════════════════════════════" echo "[FULL MEMORY DUMP — EVERY FILE LOADED]" echo "═══════════════════════════════════════════════════════════════" echo "" # Iterate every .md file in the memory directory (not archive) for f in "$MEM_DIR"/*.md; do if [ -f "$f" ]; then filename=$(basename "$f") echo "" echo "┌─────────────────────────────────────────────────────────────" echo "│ FILE: $filename" echo "└─────────────────────────────────────────────────────────────" cat "$f" echo "" fi done echo "" echo "═══════════════════════════════════════════════════════════════" echo "[END FULL MEMORY DUMP — $(ls "$MEM_DIR"/*.md 2>/dev/null | wc -l | tr -d ' ') files loaded]" echo "═══════════════════════════════════════════════════════════════" - hooks/preflight-swarm.shGitHub
Read the script
#!/bin/bash # preflight-swarm.sh — UserPromptSubmit hook (Pre-Cognitive Triad v1) # # Spawns the Lateral Thinker subagent via Claude Code's headless mode # (`claude -p`) to generate a cross-disciplinary epiphany, then injects # it as additionalContext before Main Claude sees the prompt. # # Architectural fixes over the other-agent draft: # - Reads stdin JSON (not $1) per Claude Code UserPromptSubmit spec # - Uses `claude -p` with inlined system prompt (no --agent flag exists) # - Model: claude-haiku-4-5-20251001 (current Haiku, not Oct-2024 3.5) # - Re-entrancy guard via VESTIGE_SWARM_ACTIVE env var (prevents the # subagent from re-firing this same hook and looping forever) # - 25-char minimum + y/yes/ok/continue bypass (fast-path preserved) # - 8-second timeout (fails open if Haiku is slow) # - EMPTY mute (no injection when subagent finds no epiphany) # - Emits JSON additionalContext (no duplicate raw-prompt echo) # - POSIX-sh-safe: quoted heredoc for script body, env var pass-through # # Ship date 2026-04-20. Pairs with the veto-detector.sh Guillotine on the # Stop hook to form the Cognitive Sandwich (pre-flight Triad + post-flight # Sanhedrin). set -u # === OPT-OUT GATE === # Pre-Cognitive Triad is ON by default as of 2026-04-21 (birthday launch day). # To disable, set VESTIGE_SWARM_ENABLED=0 in your environment. Default-on # guarantees the Cognitive Sandwich fires on fresh machines, Docker # containers, GUI-launched Claude Code, and shells without .zshrc — any # case where the Claude Code process lacks a sourced profile. The # re-entrancy guard (VESTIGE_SWARM_ACTIVE) below still prevents fork-bombs # from the subagent's own UserPromptSubmit hook. if [ "${VESTIGE_SWARM_ENABLED:-1}" = "0" ]; then exit 0 fi # === RE-ENTRANCY GUARD === # If we are already inside the Lateral Thinker subagent, exit immediately # so the subagent's own UserPromptSubmit does not spawn another Lateral # Thinker. Without this guard: infinite fork-bomb. if [ "${VESTIGE_SWARM_ACTIVE:-0}" = "1" ]; then exit 0 fi # === READ PROMPT FROM STDIN JSON === INPUT="$(cat)" PROMPT="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("prompt",""))' 2>/dev/null || printf '')" SESSION_ID="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("session_id",""))' 2>/dev/null || printf '')" if [ -z "$PROMPT" ]; then exit 0 fi # === LATENCY + INTENT GATE === # Skip on very short prompts and common continuation phrases. These do not # benefit from a lateral epiphany and the 2-4s latency would be annoying. PROMPT_LEN="${#PROMPT}" if [ "$PROMPT_LEN" -lt 25 ]; then exit 0 fi LOWER_TRIMMED="$(printf '%s' "$PROMPT" | /usr/bin/tr '[:upper:]' '[:lower:]' | /usr/bin/awk '{$1=$1;print}')" case "$LOWER_TRIMMED" in y|yes|no|ok|okay|continue|proceed|go|ship|lfg|lets\ go|lets\ ship|looks\ good|thanks|thank\ you|perfect|great|awesome) exit 0 ;; esac # === VERIFY claude CLI AVAILABLE === CLAUDE_BIN="$(command -v claude 2>/dev/null || true)" if [ -z "$CLAUDE_BIN" ]; then # No claude CLI in PATH — fail open, hook does not block Claude Code itself exit 0 fi # === BUILD COMBINED PROMPT (lateral-thinker system prompt + user prompt) === # We inline the agent's system-prompt text because `claude -p` headless mode # takes a single prompt string; there is no --agent flag. PROMPT_FILE="$(mktemp -t vestige-lateral.XXXXXX)" trap 'rm -f "$PROMPT_FILE"' EXIT cat > "$PROMPT_FILE" <<'LATERAL_SYSTEM_EOF' You are the Lateral Thinker, a subconscious subagent in the Vestige OS. Your only job: surface a cross-disciplinary structural parallel from the user's Vestige memory graph that the main agent would not otherwise see. Execution protocol (do all steps silently, no narration): 1. Read the user prompt below the separator. 2. Extract the core structural pattern (race condition / state sync / retry loop / memory leak / schema migration / decoding ambiguity / rate limit / ordering guarantee / cache invalidation / etc). 3. Call mcp__vestige__explore_connections with action="bridges" OR mcp__vestige__search to find memories in a COMPLETELY UNRELATED domain that share the same structural pattern. Prefer bridges between distant clusters (e.g., React UI state <-> Rust async channel; Python DB lock <-> Git merge conflict). 4. If you find a high-confidence mechanical parallel, output EXACTLY this XML structure (nothing else, no preamble, no explanation): <lateral_epiphany> <structural_pattern>one short noun phrase naming the shared pattern</structural_pattern> <source_domain>where the user currently is</source_domain> <bridge_domain>the unrelated domain where the pattern also lives</bridge_domain> <memory_id>the Vestige node ID of the cross-domain memory, if applicable</memory_id> <insight>one sentence explaining how the unrelated memory informs the current problem mechanically, not metaphorically</insight> </lateral_epiphany> 5. If you cannot find a confident, mechanical, distinct bridge in under three tool calls, output EXACTLY the single word: EMPTY Do not apologize, do not explain, do not converse. --- USER PROMPT: LATERAL_SYSTEM_EOF printf '%s\n' "$PROMPT" >> "$PROMPT_FILE" # === SPAWN LATERAL THINKER (background with timeout) === # Set VESTIGE_SWARM_ACTIVE=1 so the subagent's own UserPromptSubmit sees # the re-entrancy guard and exits early. --permission-mode bypassPermissions # skips interactive prompts inside the subagent run (standard for headless). OUTPUT_FILE="$(mktemp -t vestige-lateral-out.XXXXXX)" trap 'rm -f "$PROMPT_FILE" "$OUTPUT_FILE"' EXIT ( VESTIGE_SWARM_ACTIVE=1 \ "$CLAUDE_BIN" \ -p "$(cat "$PROMPT_FILE")" \ --model claude-haiku-4-5-20251001 \ --allowed-tools "mcp__vestige__search,mcp__vestige__explore_connections,mcp__vestige__memory" \ < /dev/null \ > "$OUTPUT_FILE" 2>/dev/null ) & CLAUDE_PID=$! # === TIMEOUT GUARD (40 seconds) === - hooks/sanhedrin-local.pyGitHub
Read the script
#!/usr/bin/env python3 # sanhedrin-local.py — OpenAI-compatible Sanhedrin Executioner bridge. # Drop-in replacement for the Haiku 4.5 subagent that sanhedrin.sh used to spawn. # # Reads draft from stdin, prints single-line verdict to stdout: # yes # no - [Sanhedrin Veto] [CLASS]: <reason under 120 chars> # # Architecture: # stdin (draft) -> Vestige /api/deep_reference (single semantic query) # -> OpenAI-compatible chat endpoint (one-shot judgment) # -> stdout (single-line verdict) # # Fail-open: if the endpoint is unreachable, print "yes" and exit 0 (don't break # the Cognitive Sandwich on infra errors). The wrapping sanhedrin.sh maps # "yes" to exit 0, so this preserves existing fail-open semantics. from __future__ import annotations import json import os import re import sys import unicodedata import urllib.error import urllib.parse import urllib.request from dataclasses import asdict, dataclass, field, replace from pathlib import Path from typing import Any sys.path.insert(0, str(Path(__file__).resolve().parent)) try: import sanhedrin_core except Exception: sanhedrin_core = None def env_int(name: str, default: int) -> int: try: return int(os.environ.get(name, "") or default) except ValueError: return default DASHBOARD_PORT = os.environ.get("VESTIGE_DASHBOARD_PORT") or "3927" VESTIGE_BASE_URL = ( os.environ.get("VESTIGE_BASE_URL") or f"http://127.0.0.1:{DASHBOARD_PORT}" ).rstrip("/") SANHEDRIN_ENDPOINT = ( os.environ.get("VESTIGE_SANHEDRIN_ENDPOINT") or os.environ.get("MLX_ENDPOINT") or "" ) VESTIGE_ENDPOINT = ( os.environ.get("VESTIGE_DEEP_REFERENCE_ENDPOINT") or f"{VESTIGE_BASE_URL}/api/deep_reference" ) VESTIGE_HEALTH = ( os.environ.get("VESTIGE_HEALTH_ENDPOINT") or f"{VESTIGE_BASE_URL}/api/health" ) MODEL = ( os.environ.get("VESTIGE_SANHEDRIN_MODEL") or os.environ.get("VESTIGE_SANDWICH_MODEL") or "" ) SANHEDRIN_TIMEOUT = env_int("VESTIGE_SANHEDRIN_TIMEOUT", env_int("MLX_TIMEOUT", 45)) VESTIGE_TIMEOUT = env_int("VESTIGE_TIMEOUT", 5) SANHEDRIN_BACKEND = (os.environ.get("VESTIGE_SANHEDRIN_BACKEND") or "").strip().lower() THINK_RE = re.compile( r"<(?:think|thinking|reasoning)>.*?</(?:think|thinking|reasoning)>", re.DOTALL | re.IGNORECASE, ) def post_json(url: str, body: dict, timeout: int): if not url: return None data = json.dumps(body).encode("utf-8") headers = {"Content-Type": "application/json"} api_key = os.environ.get("VESTIGE_SANHEDRIN_API_KEY") if api_key and same_endpoint_origin(url, SANHEDRIN_ENDPOINT): headers["Authorization"] = f"Bearer {api_key}" req = urllib.request.Request( url, data=data, headers=headers ) try: with urllib.request.urlopen(req, timeout=timeout) as r: return json.loads(r.read()) except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError): return None def same_endpoint_origin(url: str, endpoint: str) -> bool: try: target = urllib.parse.urlsplit(url) expected = urllib.parse.urlsplit(endpoint) except ValueError: return False return ( target.scheme == expected.scheme and target.netloc == expected.netloc and target.path == expected.path ) def use_backend_extensions() -> bool: if SANHEDRIN_BACKEND in {"mlx", "vllm"}: return True if SANHEDRIN_BACKEND in {"openai", "ollama", "llama.cpp", "llamacpp", "litellm"}: return False return MODEL.startswith("mlx-community/") def sanhedrin_model_configured() -> bool: return bool(SANHEDRIN_ENDPOINT and MODEL) def sanhedrin_body( messages: list[dict[str, str]], max_tokens: int, stop: list[str] | None = None, ) -> dict[str, Any]: body: dict[str, Any] = { "model": MODEL, "messages": messages, "max_tokens": max_tokens, "temperature": 0.0, "top_p": 1.0, "stream": False, } if stop: body["stop"] = stop if use_backend_extensions(): body["top_k"] = 1 body["seed"] = 42 body["chat_template_kwargs"] = {"enable_thinking": False} return body TRUST_FLOOR = 0.55 # filter out low-trust memories that drive false-positive vetoes CLAIM_MODE_ENV = "VESTIGE_SANHEDRIN_CLAIM_MODE" OUTPUT_ENV = "VESTIGE_SANHEDRIN_OUTPUT" STAGE_FILE_ENV = "VESTIGE_SANHEDRIN_STAGE_FILE" MAX_CLAIMS = env_int("VESTIGE_SANHEDRIN_MAX_CLAIMS", 8) MAX_CLAIM_CHARS = env_int("VESTIGE_SANHEDRIN_MAX_CLAIM_CHARS", 500) MAX_EVIDENCE_CHARS = env_int("VESTIGE_SANHEDRIN_MAX_EVIDENCE_CHARS", 420) CLAIM_CLASSES = { "TECHNICAL", "BIOGRAPHICAL", "FINANCIAL", "ACHIEVEMENT", "TIMELINE", "QUANTITATIVE", "ATTRIBUTION", "CAUSAL", "COMPARATIVE", "EXISTENTIAL", "VAGUE-QUANTIFIER", "UNVERIFIED-POSITIVE", } CRITICAL_ABSENCE_CLASSES = { "BIOGRAPHICAL", "FINANCIAL", "ACHIEVEMENT", "TIMELINE", "QUANTITATIVE", "ATTRIBUTION", "VAGUE-QUANTIFIER", } STRUCTURED_VERDICTS = {"SUPPORTED", "REFUTED", "REFUTED_BY_ABSENCE", "NEI"} SEVERITY_ORDER = { "BIOGRAPHICAL": 0, "FINANCIAL": 1, "ACHIEVEMENT": 2, "ATTRIBUTION": 3, "TIMELINE": 4, "QUANTITATIVE": 5, "VAGUE-QUANTIFIER": 6, "UNVERIFIED-POSITIVE": 7, "TECHNICAL": 8, "EXISTENTIAL": 9, "CAUSAL": 10, "COMPARATIVE": 11, } USER_TERMS_RE = re.compile( r"\b(sam|sam's|the user|user's|you|your|yours|yourself)\b", re.IGNORECASE ) HYPOTHETICAL_PREFIX_RE = re.compile( r"^\s*(if|suppose|imagine|hypothetically|assume|what if)\b", re.IGNORECASE, ) SUBJECT_MODAL_PREFIX_RE = re.compile( r"^\s*(sam|sam's|the user|user's|you|your)\b\s+(would|could)\b", re.IGNORECASE, ) TRAILING_MODAL_COMMENT_RE = re.compile( r"\s*,?\s+(which|that)\s+(would|could)\b.*$", re.IGNORECASE, ) CURRENT_TURN_PREFIXES = [ re.compile(r"^\s*(per your request|as requested)\s*,?\s*", re.IGNORECASE), re.co - hooks/sanhedrin.shGitHub
Read the script
#!/bin/bash # sanhedrin.sh — Stop hook (Post-Cognitive Sanhedrin / Full Agent-Type Guillotine) # # Spawns the Executioner subagent (Haiku 4.5, fresh context, Vestige MCP # tools) to run mcp__vestige__deep_reference 8-stage contradiction analysis # on the last assistant draft. If any technical claim contradicts a # high-trust memory, exit 2 with the veto reason — forces Main Claude to # rewrite. # # Runs AFTER veto-detector.sh (fast regex against veto-tagged memories). # Sanhedrin is the deeper semantic check: it reads the draft as a real # reasoning agent, extracts claims, runs deep_reference on each. # # Architecture: # Main Claude finishes draft → Stop hook chain fires → # veto-detector.sh (50ms regex, may block) → # sanhedrin.sh (2-8s Haiku subagent, may block) → # synthesis-stop-validator.sh (existing regex hedge check, may block) # # Opt-in: set VESTIGE_SANHEDRIN_ENABLED=1 in parent shell, or install with # scripts/install-sandwich.sh --enable-sanhedrin. # Re-entrancy lock: VESTIGE_EXECUTIONER_ACTIVE=1 inside the subagent. # # Ship date 2026-04-20. set -u load_vestige_sanhedrin_env() { [ -f "$1" ] || return 0 command -v python3 >/dev/null 2>&1 || return 0 while IFS="$(printf '\t')" read -r key value; do case "$key" in VESTIGE_SANHEDRIN_ENABLED|VESTIGE_SANHEDRIN_MODEL|VESTIGE_SANHEDRIN_ENDPOINT|VESTIGE_SANHEDRIN_API_KEY|VESTIGE_SANHEDRIN_BACKEND|VESTIGE_SANHEDRIN_CLAIM_MODE|VESTIGE_SANHEDRIN_OUTPUT|VESTIGE_SANHEDRIN_PYTHON|VESTIGE_SANHEDRIN_STATE_DIR|VESTIGE_SANHEDRIN_ALLOW_COMMAND_LEDGER|VESTIGE_SANHEDRIN_ALLOW_LOOSE_LEDGER|VESTIGE_DASHBOARD_PORT) export "$key=$value" ;; esac done < <(python3 - "$1" <<'PY' import shlex import sys allowed = { "VESTIGE_SANHEDRIN_ENABLED", "VESTIGE_SANHEDRIN_MODEL", "VESTIGE_SANHEDRIN_ENDPOINT", "VESTIGE_SANHEDRIN_API_KEY", "VESTIGE_SANHEDRIN_BACKEND", "VESTIGE_SANHEDRIN_CLAIM_MODE", "VESTIGE_SANHEDRIN_OUTPUT", "VESTIGE_SANHEDRIN_PYTHON", "VESTIGE_SANHEDRIN_STATE_DIR", "VESTIGE_SANHEDRIN_ALLOW_COMMAND_LEDGER", "VESTIGE_SANHEDRIN_ALLOW_LOOSE_LEDGER", "VESTIGE_DASHBOARD_PORT", } try: lines = open(sys.argv[1], encoding="utf-8").read().splitlines() except OSError: sys.exit(0) for raw in lines: line = raw.strip() if not line or line.startswith("#"): continue try: parts = shlex.split(line, posix=True) except ValueError: continue if len(parts) != 1 or "=" not in parts[0]: continue key, value = parts[0].split("=", 1) if key in allowed and "\t" not in value and "\0" not in value: print(f"{key}\t{value}") PY ) } # === OPT-IN GATE === # Sanhedrin is opt-in and model-agnostic. It never guesses a large verifier # model; if endpoint/model are unset, the bridge fails open with telemetry. # The installer writes this env file only for --enable-sanhedrin. SANHEDRIN_ENV="${VESTIGE_SANHEDRIN_ENV:-$HOME/.claude/hooks/vestige-sanhedrin.env}" if [ -f "$SANHEDRIN_ENV" ]; then load_vestige_sanhedrin_env "$SANHEDRIN_ENV" || exit 0 fi case "${VESTIGE_SANHEDRIN_ENABLED:-0}" in 1|true|TRUE|yes|YES|on|ON) ;; *) exit 0 ;; esac # === RE-ENTRANCY GUARD === # The Executioner's own Stop hook will fire when it returns — prevent # recursive spawns that would fork-bomb the quota. if [ "${VESTIGE_EXECUTIONER_ACTIVE:-0}" = "1" ]; then exit 0 fi PYTHON_BIN="${VESTIGE_SANHEDRIN_PYTHON:-}" if [ -z "$PYTHON_BIN" ]; then PYTHON_BIN="$(command -v python3 2>/dev/null || printf '')" fi if [ -z "$PYTHON_BIN" ]; then PYTHON_BIN="/usr/bin/python3" fi if ! "$PYTHON_BIN" -c 'import sys' >/dev/null 2>&1; then exit 0 fi record_sanhedrin_fail_open() { REASON="$1" DETAIL="${2:-}" "$PYTHON_BIN" - "$REASON" "$DETAIL" <<'PY' 2>/dev/null || true import json import os import sys from datetime import datetime, timezone from pathlib import Path reason = sys.argv[1] if len(sys.argv) > 1 else "unknown" detail = sys.argv[2] if len(sys.argv) > 2 else "" state_dir = Path(os.environ.get("VESTIGE_SANHEDRIN_STATE_DIR") or Path.home() / ".vestige" / "sanhedrin") try: state_dir.mkdir(parents=True, exist_ok=True) with (state_dir / "fail-open.jsonl").open("a", encoding="utf-8") as f: f.write(json.dumps({ "timestamp": datetime.now(timezone.utc).isoformat(timespec="seconds"), "runId": os.environ.get("VESTIGE_SANHEDRIN_RUN_ID"), "reason": reason, "detail": detail[:500], "transcript": os.environ.get("TRANSCRIPT_PATH") or os.environ.get("VESTIGE_SANHEDRIN_TRANSCRIPT"), }) + "\n") except OSError: pass PY } # === READ STOP HOOK INPUT === INPUT="$(cat)" TRANSCRIPT_PATH="$(printf '%s' "$INPUT" | "$PYTHON_BIN" -c 'import sys,json;d=json.load(sys.stdin);print(d.get("transcript_path",""))' 2>/dev/null || printf '')" if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then exit 0 fi # === EXTRACT LAST ASSISTANT DRAFT === # Read the transcript JSONL, pull the last assistant message text. export TRANSCRIPT_PATH DRAFT_SCRIPT="$(mktemp -t vestige-sanhedrin-draft.XXXXXX)" trap 'rm -f "$DRAFT_SCRIPT"' EXIT cat > "$DRAFT_SCRIPT" <<'DRAFT_PYEOF' import json, os, re, sys transcript = os.environ.get("TRANSCRIPT_PATH", "") last_assistant = "" try: with open(transcript) as f: for line in f: line = line.strip() if not line: continue try: obj = json.loads(line) except Exception: continue role = obj.get("role") or obj.get("type", "") content = obj.get("message", {}).get("content", obj.get("content", "")) text = "" if isinstance(content, list): for block in content: if isinstance(block, dict) and block.get("type") == "text": text += block.get("text", "") + "\n" elif isinstance(content, str): text = content - hooks/sanhedrin_core.pyGitHub
Read the script
#!/usr/bin/env python3 """Shared Sanhedrin claim-ledger, receipt, and appeal helpers. This module is intentionally dependency-free so Stop hooks can import it inside Claude Code, Codex wrappers, and local development without installing a package. """ from __future__ import annotations import datetime as dt import hashlib import html import json import os import re from pathlib import Path from typing import Any STATE_DIR = Path( os.environ.get("VESTIGE_SANHEDRIN_STATE_DIR") or Path.home() / ".vestige" / "sanhedrin" ) RECEIPTS_DIR = STATE_DIR / "receipts" LATEST_JSON = STATE_DIR / "latest.json" LATEST_HTML = STATE_DIR / "latest.html" APPEALS_JSONL = STATE_DIR / "appeals.jsonl" COMMAND_RECEIPTS_JSONL = STATE_DIR / "command-receipts.jsonl" FAIL_OPEN_JSONL = STATE_DIR / "fail-open.jsonl" SUPPORTED_RECEIPT_SCHEMA = "vestige.sanhedrin.receipt.v1" VERIFICATION_RE = re.compile( r"\b(" r"(all\s+)?(tests?|test suite|build|lint|typecheck|checks?|cargo test|npm test|pnpm test|" r"pytest|vitest|jest|playwright|tsc|clippy)\s+" r"(passed|passes|passing|green|succeeded|succeeds|clean)|" r"(verified|validated|confirmed)\s+(with|by|via)\s+[`']?([^`'\n]+)[`']?" r")\b", re.IGNORECASE, ) VERIFICATION_HEDGE_LEFT_RE = re.compile( r"\b(" r"i\s+(think|believe|guess|suspect)|" r"maybe|might|may\s+have|possibly|probably|apparently|" r"let\s+me\s+verify|need\s+to\s+verify|will\s+verify|should\s+verify|" r"not\s+sure|unverified|without\s+running" r")\b[^.;:!?]{0,80}$", re.IGNORECASE, ) COMMAND_FAMILY_PATTERNS = { "test": re.compile(r"\b(pytest|cargo\s+test|npm\s+(run\s+)?test|pnpm\s+(run\s+)?test|vitest|jest|playwright\s+test)\b", re.I), "build": re.compile(r"\b(cargo\s+build|npm\s+(run\s+)?build|pnpm\s+(run\s+)?build|next\s+build|vite\s+build)\b", re.I), "lint": re.compile(r"\b(cargo\s+clippy|npm\s+(run\s+)?lint|pnpm\s+(run\s+)?lint|eslint|ruff|flake8)\b", re.I), "typecheck": re.compile(r"\b(tsc|svelte-check|mypy|pyright|cargo\s+check)\b", re.I), } COMMAND_EXIT_RE = re.compile( r"(exit[_ -]?code|status|returncode|rc)[\"':=\s]+(-?\d+)|" r"Error:\s*Exit code\s*(-?\d+)|" r"Process exited with code\s*(-?\d+)", re.I, ) CLAUDE_TOOL_NAME_RE = re.compile(r'"name"\s*:\s*"([^"]+)"') def now_iso() -> str: return dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds") def ensure_dirs() -> None: RECEIPTS_DIR.mkdir(parents=True, exist_ok=True) def stable_id(text: str, prefix: str = "sr") -> str: digest = hashlib.sha256(text.encode("utf-8")).hexdigest()[:16] return f"{prefix}_{digest}" def claim_fingerprint(text: str) -> str: normalized = re.sub(r"\s+", " ", text.lower()).strip() normalized = re.sub(r"[`'\"$]", "", normalized) return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16] def strip_non_assertive_regions(text: str) -> str: """Remove quoted/code regions before looking for Receipt Lock assertions.""" text = text[:16_384] text = re.sub(r"```.*?```", " ", text, flags=re.DOTALL) text = re.sub(r"`[^`\n]+`", " ", text) kept_lines = [] for line in text.splitlines(): stripped = line.lstrip() if stripped.startswith(">"): continue kept_lines.append(line) text = "\n".join(kept_lines) text = re.sub(r'(^|[\s([{])"[^"\n]+"(?=([\s.,;:!?)}\]]|$))', r"\1 ", text) return text def is_asserted_verification_claim(text: str) -> bool: match = VERIFICATION_RE.search(text) if not match: return False left_context = text[max(0, match.start() - 100) : match.start()] return VERIFICATION_HEDGE_LEFT_RE.search(left_context) is None def split_claims(draft: str) -> list[str]: cleaned = strip_non_assertive_regions(draft) chunks = re.split(r"(?<=[.!?])\s+|\n+", cleaned) claims: list[str] = [] for chunk in chunks: text = chunk.strip(" -\t") if len(text) >= 18 or is_asserted_verification_claim(text) or is_hard_user_claim(text): claims.append(text) return claims[:24] def detect_claim_type(text: str) -> str: low = text.lower() if is_asserted_verification_claim(text): return "receipt_lock" if is_hard_user_claim(text): return "hard_user_claim" if any(word in low for word in ("won", "prize", "ranked", "placed", "score", "graduated", "worked at")): return "hard_user_claim" if any(word in low for word in ("should", "could", "recommend", "plan", "target", "estimate")): return "advice" if "`" in text or "/" in text or re.search(r"\bv?\d+\.\d+", text): return "technical" return "general" def is_hard_user_claim(text: str) -> bool: if not re.search(r"\b(Sam|you|your|I|my)\b", text, re.I): return False hard_patterns = ( r"\b(attended|graduated|studied|enrolled|accepted|worked|works|employed|hired)\b", r"\b(was\s+born|born\s+in|born\s+on|birthdate|birthday)\b", r"\b(won|placed|ranked|scored|earned|raised|sold|founded|launched)\b", r"\b(prize|award|payout|grant|scholarship|degree|gpa|employer|school|university|college|birth\s+date)\b", r"\$[0-9]", ) return any(re.search(pattern, text, re.I) for pattern in hard_patterns) def new_manifest(draft: str) -> dict[str, Any]: draft_id = stable_id(draft, "draft") claims = [] for i, text in enumerate(split_claims(draft), start=1): claim_type = detect_claim_type(text) claims.append( { "id": f"c{i:03d}", "text": text, "fingerprint": claim_fingerprint(text), "class": claim_type, "subject": infer_subject(text), "risk": "hard" if claim_type == "receipt_lock" else "normal", "evidence_state": "unchecked", "decision": "pending", "precedent": [], "fix": "", "appeal": { "status - hooks/synthesis-gate.shGitHub
- hooks/synthesis-preflight.shGitHub
- hooks/synthesis-stop-validator.shGitHub
- hooks/vestige-pulse-daemon.shGitHub
- hooks/veto-detector.shGitHub
All 11 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.
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 withvestige
Vestige is a fail-closed runtime firewall for AI agents. The model proposes, a deterministic gate decides, and every action leaves a receipt. When something breaks, causal_walk finds the real cause.
Get the whole plugin
Stats
650
Stars
72
Forks
Active
Maintenance
Rust
Language
AGPL-3.0
License
3d ago
Last commit
8mo ago
Created
4h ago
Added
Repo: samvallad33/vestige

