#!/usr/bin/env bash # foxhunt-audit-router.sh # # PostToolUse hook router. Reads Claude Code tool-input JSON from stdin, # inspects the file path being edited, and emits at most one one-line hint # to stdout suggesting which foxhunt-* auditor agents to dispatch. # # Contract: # - Completes in <200 ms. # - Never spawns agents itself. # - Always exits 0 (never blocks). # - One suggestion per (path, agent) per session, tracked in # .claude/.foxhunt-audit-state (gitignored, cleared at SessionStart). set -u # not -e; we want to exit 0 even on errors REPO_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}" STATE_FILE="$REPO_ROOT/.claude/.foxhunt-audit-state" mkdir -p "$(dirname "$STATE_FILE")" 2>/dev/null || exit 0 touch "$STATE_FILE" 2>/dev/null || exit 0 # Read tool input from stdin; bail if not JSON or no file_path INPUT="$(cat 2>/dev/null || true)" [ -z "$INPUT" ] && exit 0 # Extract file path. Claude Code sends keys like "tool_input": {"file_path": "..."} # Be lenient: try multiple JSON shapes via grep/sed (avoid jq dep). PATH_FOUND="$(printf '%s' "$INPUT" | grep -oE '"file_path"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1 | sed -E 's/.*"file_path"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')" [ -z "$PATH_FOUND" ] && exit 0 # Make path repo-relative if absolute case "$PATH_FOUND" in "$REPO_ROOT"/*) REL="${PATH_FOUND#$REPO_ROOT/}" ;; /*) REL="$PATH_FOUND" ;; *) REL="$PATH_FOUND" ;; esac # Routing table: build SUGGEST list of agent names SUGGEST=() case "$REL" in *.cu) SUGGEST+=("gpu-contract-auditor") ;; *cuda/*.rs|*/cuda/*) SUGGEST+=("gpu-contract-auditor") ;; crates/*/build.rs) SUGGEST+=("gpu-contract-auditor") ;; esac case "$REL" in */experience_kernels.cu|*reward*.rs|*reward*.cu|*controller*.rs|*trader_*.rs) SUGGEST+=("reward-controller-auditor") ;; esac case "$REL" in *sp*_isv_slots.rs) SUGGEST+=("isv-discipline-auditor") ;; crates/ml-*/*.rs|crates/ml-*/**/*.rs) SUGGEST+=("isv-discipline-auditor") ;; esac case "$REL" in docs/superpowers/specs/*.md|docs/superpowers/plans/*.md|docs/plans/*.md) SUGGEST+=("sp-critical-reviewer") ;; esac case "$REL" in MEMORY.md|*memory/pearl_*.md|*memory/feedback_*.md) SUGGEST+=("memory-curator") ;; esac # Default for any rust file in crates/ services/ bin/ that didn't match above case "$REL" in crates/*.rs|crates/**/*.rs|services/*.rs|services/**/*.rs|bin/*.rs|bin/**/*.rs) # Add code-hygiene unconditionally for Rust source SUGGEST+=("code-hygiene-auditor") ;; esac [ "${#SUGGEST[@]}" -eq 0 ] && exit 0 # Dedupe SUGGEST and filter against state file (one suggestion per (path, agent) per session) EMIT=() for AGENT in "${SUGGEST[@]}"; do KEY="$REL|$AGENT" if ! grep -Fxq "$KEY" "$STATE_FILE" 2>/dev/null; then echo "$KEY" >> "$STATE_FILE" # Dedupe within this invocation too case " ${EMIT[*]:-} " in *" $AGENT "*) ;; *) EMIT+=("$AGENT") ;; esac fi done [ "${#EMIT[@]}" -eq 0 ] && exit 0 # Emit single-line hint, prefixed for grep JOINED="$(printf '%s, ' "${EMIT[@]}")"; JOINED="${JOINED%, }" echo "[foxhunt-audit] $REL — suggest: $JOINED" exit 0