chore(claude): foxhunt audit hook router (warn-only PostToolUse)
Adds .claude/helpers/foxhunt-audit-router.sh that suggests foxhunt-* auditor agents based on edited file path. Warn-only, <200ms, never blocks. Dedup state file .claude/.foxhunt-audit-state cleared at SessionStart by sibling script. Settings.json additive merge — existing hooks preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
86
.claude/helpers/foxhunt-audit-router.sh
Executable file
86
.claude/helpers/foxhunt-audit-router.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/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
|
||||
|
||||
STATE_FILE="${CLAUDE_PROJECT_DIR:-.}/.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
|
||||
REPO_ROOT="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
||||
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/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
|
||||
5
.claude/helpers/foxhunt-audit-session-start.sh
Executable file
5
.claude/helpers/foxhunt-audit-session-start.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Clear foxhunt-audit dedup state at session start.
|
||||
STATE_FILE="${CLAUDE_PROJECT_DIR:-.}/.claude/.foxhunt-audit-state"
|
||||
: > "$STATE_FILE" 2>/dev/null || true
|
||||
exit 0
|
||||
@@ -21,6 +21,15 @@
|
||||
"timeout": 15
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/helpers/foxhunt-audit-session-start.sh\"",
|
||||
"timeout": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SessionEnd": [
|
||||
@@ -33,6 +42,18 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostToolUse": [
|
||||
{
|
||||
"matcher": "Edit|Write|MultiEdit",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/helpers/foxhunt-audit-router.sh\"",
|
||||
"timeout": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"permissions": {
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -191,3 +191,6 @@ services/*/load_tests/*.json
|
||||
!services/*/load_tests/package.json
|
||||
.playwright-mcp/
|
||||
crates/ml/ml/
|
||||
|
||||
# Foxhunt audit hook dedup state (cleared at SessionStart)
|
||||
.claude/.foxhunt-audit-state
|
||||
|
||||
Reference in New Issue
Block a user