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

From plugin
vestige
6503 agents11 hooks

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