From 4617a53db3d86752de369a0e9c8bd4632fd74796 Mon Sep 17 00:00:00 2001 From: Fernando Lazzarin Date: Sat, 16 May 2026 18:50:27 -0300 Subject: [PATCH] hooks: mirror Slice 4 dual-mode rewires (4 hooks) Byte-equal mirror of the corresponding llm-dark-patterns hooks for the multi-agent family. The 2 limited hooks (no-handoff-loop, no-ownership-violation) are NOT changed because their bash logic already handles events the Rust engine cannot. Affected: - .claude/hooks/no-aggregator-hallucination.sh - .claude/hooks/no-cherry-pick-rollup.sh - .claude/hooks/no-silent-worker-success.sh - .claude/hooks/no-credential-leak-in-handoff.sh Companion PRs: - waitdeadai/agent-closeout-bench physics-engines/slice-4-multi-agent - waitdeadai/llm-dark-patterns physics-engines/slice-4-multi-agent-dual-mode Co-Authored-By: Claude Opus 4.7 --- .claude/hooks/no-aggregator-hallucination.sh | 37 ++++++++++++++++++ .claude/hooks/no-cherry-pick-rollup.sh | 37 ++++++++++++++++++ .../hooks/no-credential-leak-in-handoff.sh | 38 +++++++++++++++++++ .claude/hooks/no-silent-worker-success.sh | 37 ++++++++++++++++++ 4 files changed, 149 insertions(+) diff --git a/.claude/hooks/no-aggregator-hallucination.sh b/.claude/hooks/no-aggregator-hallucination.sh index 7a73b86..ea522b4 100755 --- a/.claude/hooks/no-aggregator-hallucination.sh +++ b/.claude/hooks/no-aggregator-hallucination.sh @@ -30,6 +30,43 @@ if ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then exit 0 fi +# Rust path: prefer agentcloseout-physics when available. +if command -v agentcloseout-physics >/dev/null 2>&1; then + RULES_DIR="${LLM_DARK_PATTERNS_RULES_DIR:-}" + if [ -z "$RULES_DIR" ]; then + for candidate in \ + "$(dirname "$0")/../../agent-closeout-bench/rules/closeout" \ + "/home/fer/Documents/agent-closeout-bench/rules/closeout" \ + "${XDG_CONFIG_HOME:-$HOME/.config}/agentcloseout-physics/rules/closeout"; do + if [ -d "$candidate" ]; then RULES_DIR="$candidate"; break; fi + done + fi + if [ -n "$RULES_DIR" ] && [ -d "$RULES_DIR" ] && [ -f "$RULES_DIR/no_aggregator_hallucination.yaml" ]; then + TMP_INPUT="$(mktemp)"; printf '%s' "$INPUT" > "$TMP_INPUT" + VERDICT_JSON="$(agentcloseout-physics scan --category no_aggregator_hallucination --rules "$RULES_DIR" --input "$TMP_INPUT" 2>/dev/null || true)" + rm -f "$TMP_INPUT" + if [ -n "$VERDICT_JSON" ]; then + DECISION="$(printf '%s' "$VERDICT_JSON" | jq -r '.decision // empty' 2>/dev/null)" + if [ "$DECISION" = "block" ]; then + RULE="$(printf '%s' "$VERDICT_JSON" | jq -r '.matched_rules[0].rule_id // "no_aggregator_hallucination"' 2>/dev/null)" + EVIDENCE="$(printf '%s' "$VERDICT_JSON" | jq -r '.redacted_evidence[0] // ""' 2>/dev/null)" + echo "BLOCKED: aggregator hallucination: synthesis claim without per-worker evidence." >&2 + echo "Matched rule: $RULE" >&2 + [ -n "$EVIDENCE" ] && echo "Evidence: $EVIDENCE" >&2 + echo "" >&2 + echo "Repair guidance:" >&2 + echo "- Quote the per-worker output that justifies the synthesis (e.g. worker_1: pass, worker_2: pass)." >&2 + echo "- Or drop the synthesis framing and report as a single agent's work." >&2 + echo "- Or close as Status: partial / Verification: pending." >&2 + exit 2 + fi + if [ "$DECISION" = "pass" ]; then + exit 0 + fi + fi + fi +fi + _HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ -f "$_HOOK_DIR/../lib/packs.sh" ]; then # shellcheck source=../lib/packs.sh diff --git a/.claude/hooks/no-cherry-pick-rollup.sh b/.claude/hooks/no-cherry-pick-rollup.sh index a78c4a6..7f74028 100755 --- a/.claude/hooks/no-cherry-pick-rollup.sh +++ b/.claude/hooks/no-cherry-pick-rollup.sh @@ -25,6 +25,43 @@ if ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then exit 0 fi +# Rust path: prefer agentcloseout-physics when available. +if command -v agentcloseout-physics >/dev/null 2>&1; then + RULES_DIR="${LLM_DARK_PATTERNS_RULES_DIR:-}" + if [ -z "$RULES_DIR" ]; then + for candidate in \ + "$(dirname "$0")/../../agent-closeout-bench/rules/closeout" \ + "/home/fer/Documents/agent-closeout-bench/rules/closeout" \ + "${XDG_CONFIG_HOME:-$HOME/.config}/agentcloseout-physics/rules/closeout"; do + if [ -d "$candidate" ]; then RULES_DIR="$candidate"; break; fi + done + fi + if [ -n "$RULES_DIR" ] && [ -d "$RULES_DIR" ] && [ -f "$RULES_DIR/no_cherry_pick_rollup.yaml" ]; then + TMP_INPUT="$(mktemp)"; printf '%s' "$INPUT" > "$TMP_INPUT" + VERDICT_JSON="$(agentcloseout-physics scan --category no_cherry_pick_rollup --rules "$RULES_DIR" --input "$TMP_INPUT" 2>/dev/null || true)" + rm -f "$TMP_INPUT" + if [ -n "$VERDICT_JSON" ]; then + DECISION="$(printf '%s' "$VERDICT_JSON" | jq -r '.decision // empty' 2>/dev/null)" + if [ "$DECISION" = "block" ]; then + RULE="$(printf '%s' "$VERDICT_JSON" | jq -r '.matched_rules[0].rule_id // "no_cherry_pick_rollup"' 2>/dev/null)" + EVIDENCE="$(printf '%s' "$VERDICT_JSON" | jq -r '.redacted_evidence[0] // ""' 2>/dev/null)" + echo "BLOCKED: cherry-pick rollup: partial worker success + positive closeout WITHOUT handling failed workers." >&2 + echo "Matched rule: $RULE" >&2 + [ -n "$EVIDENCE" ] && echo "Evidence: $EVIDENCE" >&2 + echo "" >&2 + echo "Repair guidance:" >&2 + echo "- Explicitly handle failed workers (retried, blocking, ignored-with-reason)." >&2 + echo "- Or close as Status: partial / Next step: investigate failed worker." >&2 + echo "- Or drop the rollup framing and report only the verified-succeeded portion." >&2 + exit 2 + fi + if [ "$DECISION" = "pass" ]; then + exit 0 + fi + fi + fi +fi + _HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ -f "$_HOOK_DIR/../lib/packs.sh" ]; then # shellcheck source=../lib/packs.sh diff --git a/.claude/hooks/no-credential-leak-in-handoff.sh b/.claude/hooks/no-credential-leak-in-handoff.sh index f1962d4..7f47f53 100755 --- a/.claude/hooks/no-credential-leak-in-handoff.sh +++ b/.claude/hooks/no-credential-leak-in-handoff.sh @@ -18,6 +18,44 @@ if ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then exit 0 fi +# Rust path: only for Stop/SubagentStop. TaskCreated stays on bash path +# (the Rust v0.1 engine inspects last_assistant_message only; TaskCreated +# payload requires bash jq-based extraction of .task.description / .task.prompt). +EVENT_FILTER="$(printf '%s' "$INPUT" | jq -r '.hook_event_name // empty' 2>/dev/null)" +if command -v agentcloseout-physics >/dev/null 2>&1 && { [ "$EVENT_FILTER" = "Stop" ] || [ "$EVENT_FILTER" = "SubagentStop" ]; }; then + RULES_DIR="${LLM_DARK_PATTERNS_RULES_DIR:-}" + if [ -z "$RULES_DIR" ]; then + for candidate in \ + "$(dirname "$0")/../../agent-closeout-bench/rules/closeout" \ + "/home/fer/Documents/agent-closeout-bench/rules/closeout" \ + "${XDG_CONFIG_HOME:-$HOME/.config}/agentcloseout-physics/rules/closeout"; do + if [ -d "$candidate" ]; then RULES_DIR="$candidate"; break; fi + done + fi + if [ -n "$RULES_DIR" ] && [ -d "$RULES_DIR" ] && [ -f "$RULES_DIR/no_credential_leak_in_handoff.yaml" ]; then + TMP_INPUT="$(mktemp)"; printf '%s' "$INPUT" > "$TMP_INPUT" + VERDICT_JSON="$(agentcloseout-physics scan --category no_credential_leak_in_handoff --rules "$RULES_DIR" --input "$TMP_INPUT" 2>/dev/null || true)" + rm -f "$TMP_INPUT" + if [ -n "$VERDICT_JSON" ]; then + DECISION="$(printf '%s' "$VERDICT_JSON" | jq -r '.decision // empty' 2>/dev/null)" + if [ "$DECISION" = "block" ]; then + RULE="$(printf '%s' "$VERDICT_JSON" | jq -r '.matched_rules[0].rule_id // "no_credential_leak_in_handoff"' 2>/dev/null)" + echo "BLOCKED: credential leak in closeout message." >&2 + echo "Matched rule: $RULE" >&2 + echo "" >&2 + echo "Repair guidance:" >&2 + echo "- Refer to credentials by env-var name (e.g. \$ANTHROPIC_API_KEY) instead of inlining the value." >&2 + echo "- Or have the subagent read from a secrets manager." >&2 + echo "- Reference: arXiv:2602.11510 AgentLeak benchmark." >&2 + exit 2 + fi + if [ "$DECISION" = "pass" ]; then + exit 0 + fi + fi + fi +fi + json_get() { printf '%s' "$INPUT" | jq -r "$1 // empty" 2>/dev/null || true; } block() { diff --git a/.claude/hooks/no-silent-worker-success.sh b/.claude/hooks/no-silent-worker-success.sh index d6d2af4..073cafd 100755 --- a/.claude/hooks/no-silent-worker-success.sh +++ b/.claude/hooks/no-silent-worker-success.sh @@ -25,6 +25,43 @@ if ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then exit 0 fi +# Rust path: prefer agentcloseout-physics when available. +if command -v agentcloseout-physics >/dev/null 2>&1; then + RULES_DIR="${LLM_DARK_PATTERNS_RULES_DIR:-}" + if [ -z "$RULES_DIR" ]; then + for candidate in \ + "$(dirname "$0")/../../agent-closeout-bench/rules/closeout" \ + "/home/fer/Documents/agent-closeout-bench/rules/closeout" \ + "${XDG_CONFIG_HOME:-$HOME/.config}/agentcloseout-physics/rules/closeout"; do + if [ -d "$candidate" ]; then RULES_DIR="$candidate"; break; fi + done + fi + if [ -n "$RULES_DIR" ] && [ -d "$RULES_DIR" ] && [ -f "$RULES_DIR/no_silent_worker_success.yaml" ]; then + TMP_INPUT="$(mktemp)"; printf '%s' "$INPUT" > "$TMP_INPUT" + VERDICT_JSON="$(agentcloseout-physics scan --category no_silent_worker_success --rules "$RULES_DIR" --input "$TMP_INPUT" 2>/dev/null || true)" + rm -f "$TMP_INPUT" + if [ -n "$VERDICT_JSON" ]; then + DECISION="$(printf '%s' "$VERDICT_JSON" | jq -r '.decision // empty' 2>/dev/null)" + if [ "$DECISION" = "block" ]; then + RULE="$(printf '%s' "$VERDICT_JSON" | jq -r '.matched_rules[0].rule_id // "no_silent_worker_success"' 2>/dev/null)" + EVIDENCE="$(printf '%s' "$VERDICT_JSON" | jq -r '.redacted_evidence[0] // ""' 2>/dev/null)" + echo "BLOCKED: silent worker rollup: 'all N workers completed' claim without per-worker evidence." >&2 + echo "Matched rule: $RULE" >&2 + [ -n "$EVIDENCE" ] && echo "Evidence: $EVIDENCE" >&2 + echo "" >&2 + echo "Repair guidance:" >&2 + echo "- Enumerate per-worker status (worker_1: exit=0, worker_2: exit=0, ...)." >&2 + echo "- Or report only the workers whose output was verified." >&2 + echo "- Or close as Status: partial / Verification: pending." >&2 + exit 2 + fi + if [ "$DECISION" = "pass" ]; then + exit 0 + fi + fi + fi +fi + _HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [ -f "$_HOOK_DIR/../lib/packs.sh" ]; then # shellcheck source=../lib/packs.sh