Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 72 additions & 4 deletions .claude/hooks/honest-eta.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,29 @@

set -euo pipefail

_HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "$_HOOK_DIR/../lib/packs.sh" ]; then
# shellcheck source=../lib/packs.sh
source "$_HOOK_DIR/../lib/packs.sh"
fi

_load_or_fallback() {
local section="$1" fallback="$2" loaded=""
if declare -F load_locale_section >/dev/null 2>&1; then
loaded="$(load_locale_section "$section" 2>/dev/null)"
fi
if [ -z "$loaded" ]; then
printf '%s' "$fallback"
else
printf '%s' "$loaded"
fi
}

ETA_CONTEXT_RE="$(_load_or_fallback eta_context '\b([0-9]+([-.][0-9]+)?)[[:space:]]*(min|minute|hour|hr|day|week|wk|month|mo|year|yr|sprint)s?\b|\bETA[: ]|\bestimated time|\btime to (deliver|ship|complete|implement|finish|land)|\bshould take (about|around|roughly|approximately)?[[:space:]]?[0-9]|\bwill take (about|around|roughly|approximately)?[[:space:]]?[0-9]|\bcompletion in [0-9]|\bready in (about|around)?[[:space:]]?[0-9]')"
LINEAR_SCALING_RE="$(_load_or_fallback eta_linear_scaling '\b[0-9]+x[[:space:]]+(faster|speedup|speed-?up)|with[[:space:]]+[0-9]+[[:space:]]+(agents|lanes|workers).*[0-9]+x|linear(ly)?[[:space:]]+scal(es|ing|able)|divid(ed|ing)[[:space:]]+by[[:space:]]+(lane|agent)[[:space:]]+count|per[- ]lane[[:space:]]+speedup|N[[:space:]]+agents[[:space:]]*=[[:space:]]*N x')"
AGENT_NATIVE_RE="$(_load_or_fallback eta_agent_native 'agent[[:space:]_]wall[[:space:]_]clock|agent[[:space:]_]hours|human[[:space:]_]touch[[:space:]_]time|calendar[[:space:]_]blockers|critical[[:space:]_]path|estimate[[:space:]_]type[: ]+(agent-native|human-equivalent|blocked|unknown)|optimistic[[:space:],/]+(.*)?[[:space:]]?likely[[:space:],/]+(.*)?[[:space:]]?pessimistic|confidence[: ]+(high|medium|low|unknown)([[:space:],]+with[[:space:]]+downgrade)?|insufficient_data')"
HEDGE_RANGE_RE="$(_load_or_fallback eta_hedge_range '\b(optimistic|likely|pessimistic|worst[- ]case|best[- ]case|p50|p90|range)[: ]+|approximately[[:space:]]+[0-9]+[[:space:]]*-[[:space:]]*[0-9]+|\bsomewhere between[[:space:]]+[0-9]+|\bcould be anywhere from')"

INPUT="$(cat)"

if ! command -v jq >/dev/null 2>&1; then
Expand All @@ -24,6 +47,47 @@ 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/honest_eta.yaml" ]; then
TMP_INPUT="$(mktemp)"; printf '%s' "$INPUT" > "$TMP_INPUT"
VERDICT_JSON="$(agentcloseout-physics scan --category honest_eta --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 // "honest_eta"' 2>/dev/null)"
EVIDENCE="$(printf '%s' "$VERDICT_JSON" | jq -r '.redacted_evidence[0] // ""' 2>/dev/null)"
if [ "$RULE" = "honest_eta.linear_scaling_claim" ]; then
echo "BLOCKED: linear-scaling claim in time estimate — agents don't divide work by lane count." >&2
else
echo "BLOCKED: time estimate without Agent-Native Estimate shape or honest hedge range." >&2
fi
echo "Matched rule: $RULE" >&2
[ -n "$EVIDENCE" ] && echo "Evidence: $EVIDENCE" >&2
echo "" >&2
echo "Repair guidance:" >&2
echo "- Use the Agent-Native Estimate shape: estimate type, agent_wall_clock optimistic/likely/pessimistic, critical_path, confidence." >&2
echo "- Or an honest hedge range (optimistic/likely/pessimistic, or 'somewhere between X and Y')." >&2
echo "- If capacity is genuinely unknown, say 'estimate type: blocked/unknown' instead of inventing a number." >&2
exit 2
fi
if [ "$DECISION" = "pass" ]; then
exit 0
fi
fi
fi
fi

json_get() {
local filter="$1"
printf '%s' "$INPUT" | jq -r "$filter // empty" 2>/dev/null || true
Expand Down Expand Up @@ -57,7 +121,8 @@ if [ -z "$message" ]; then
fi

# Step 1 — does the message contain a time-estimate claim?
ETA_CONTEXT='(\b([0-9]+([-.][0-9]+)?)[[:space:]]*(min|minute|hour|hr|day|week|wk|month|mo|year|yr|sprint)s?\b|\bETA[: ]|\bestimated time|\btime to (deliver|ship|complete|implement|finish|land)|\bshould take (about|around|roughly|approximately)?[[:space:]]?[0-9]|\bwill take (about|around|roughly|approximately)?[[:space:]]?[0-9]|\bcompletion in [0-9]|\bready in (about|around)?[[:space:]]?[0-9])'
# Vocab loaded from packs/locale/<lang>.txt section [eta_context].
ETA_CONTEXT="(${ETA_CONTEXT_RE})"

HAS_ETA=$(printf '%s\n' "$message" | grep -Eic "$ETA_CONTEXT" || true)

Expand All @@ -66,7 +131,8 @@ if [ "$HAS_ETA" -eq 0 ]; then
fi

# Step 2 — linear-scaling claims are always bad, regardless of context.
LINEAR_SCALING='(\b[0-9]+x[[:space:]]+(faster|speedup|speed-?up)|with[[:space:]]+[0-9]+[[:space:]]+(agents|lanes|workers).*[0-9]+x|linear(ly)?[[:space:]]+scal(es|ing|able)|divid(ed|ing)[[:space:]]+by[[:space:]]+(lane|agent)[[:space:]]+count|per[- ]lane[[:space:]]+speedup|N[[:space:]]+agents[[:space:]]*=[[:space:]]*N x)'
# Vocab loaded from packs/locale/<lang>.txt section [eta_linear_scaling].
LINEAR_SCALING="(${LINEAR_SCALING_RE})"

if printf '%s\n' "$message" | grep -Eiq "$LINEAR_SCALING"; then
block "linear-scaling claim in time estimate — agents don't divide work by lane count." \
Expand All @@ -79,11 +145,13 @@ if printf '%s\n' "$message" | grep -Eiq "$LINEAR_SCALING"; then
fi

# Step 3 — redemption: Agent-Native Estimate structured fields.
AGENT_NATIVE='(agent[[:space:]_]wall[[:space:]_]clock|agent[[:space:]_]hours|human[[:space:]_]touch[[:space:]_]time|calendar[[:space:]_]blockers|critical[[:space:]_]path|estimate[[:space:]_]type[: ]+(agent-native|human-equivalent|blocked|unknown)|optimistic[[:space:],/]+(.*)?[[:space:]]?likely[[:space:],/]+(.*)?[[:space:]]?pessimistic|confidence[: ]+(high|medium|low|unknown)([[:space:],]+with[[:space:]]+downgrade)?|insufficient_data)'
# Vocab loaded from packs/locale/<lang>.txt section [eta_agent_native].
AGENT_NATIVE="(${AGENT_NATIVE_RE})"

# Step 4 — also accept honest hedge ranges as partial redemption when an
# operator only asked for a rough number.
HEDGE_RANGE='(\b(optimistic|likely|pessimistic|worst[- ]case|best[- ]case|p50|p90|range)[: ]+|approximately[[:space:]]+[0-9]+[[:space:]]*-[[:space:]]*[0-9]+|\bsomewhere between[[:space:]]+[0-9]+|\bcould be anywhere from)'
# Vocab loaded from packs/locale/<lang>.txt section [eta_hedge_range].
HEDGE_RANGE="(${HEDGE_RANGE_RE})"

HAS_AGENT_NATIVE=$(printf '%s\n' "$message" | grep -Eic "$AGENT_NATIVE" || true)
HAS_HEDGE=$(printf '%s\n' "$message" | grep -Eic "$HEDGE_RANGE" || true)
Expand Down
74 changes: 68 additions & 6 deletions .claude/hooks/no-curfew.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,33 @@
# Claude Code hook: block unsolicited rest/wellness paternalism in agent-mode sessions.
# Aligned with Anthropic's stated design intent: "various forms of paternalism and
# moralizing are disrespectful." (Claude's Constitution)
#
# Vocabulary loaded from packs/locale/<lang>.txt sections [paternalism] and
# [paternalism_allow]. Inline English fallback preserves pre-pack behavior.

set -euo pipefail

_HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [ -f "$_HOOK_DIR/../lib/packs.sh" ]; then
# shellcheck source=../lib/packs.sh
source "$_HOOK_DIR/../lib/packs.sh"
fi

_load_or_fallback() {
local section="$1" fallback="$2" loaded=""
if declare -F load_locale_section >/dev/null 2>&1; then
loaded="$(load_locale_section "$section" 2>/dev/null)"
fi
if [ -z "$loaded" ]; then
printf '%s' "$fallback"
else
printf '%s' "$loaded"
fi
}

PATERNALISM_RE="$(_load_or_fallback paternalism 'go to sleep|get some sleep|get some rest|take a break|save it for tomorrow|tomorrow morning|call it a night|rest up|step away|log off|wind down|your wellbeing|your well-being|take care of yourself|burnout|burned out|you'\''ve earned (a |some )?(rest|break)|it'\''s (late|getting late)|the work can wait|come back fresh|recharge|self-care|mental health break|you should stop|sleep matters|sleep is important|grab some sleep|give yourself a break|easy on yourself|don'\''t push yourself')"
PATERNALISM_ALLOW_RE="$(_load_or_fallback paternalism_allow 'you (asked|requested) (for|about) (a break|rest|sleep|self-care)|since you (mentioned|said) you (are|were|feel) tired|as you requested .*break')"

INPUT="$(cat)"

if ! command -v jq >/dev/null 2>&1; then
Expand All @@ -16,6 +40,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_curfew.yaml" ]; then
TMP_INPUT="$(mktemp)"; printf '%s' "$INPUT" > "$TMP_INPUT"
VERDICT_JSON="$(agentcloseout-physics scan --category no_curfew --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_curfew"' 2>/dev/null)"
EVIDENCE="$(printf '%s' "$VERDICT_JSON" | jq -r '.redacted_evidence[0] // ""' 2>/dev/null)"
echo "BLOCKED: unsolicited rest/wellness paternalism in agent-mode session." >&2
echo "Matched rule: $RULE" >&2
[ -n "$EVIDENCE" ] && echo "Evidence: $EVIDENCE" >&2
echo "" >&2
echo "Repair guidance:" >&2
echo "- Drop the human-rest framing. Continue with the next concrete piece of work." >&2
echo "- If the operator did request rest advice, restate the operator's request so the allow-clause matches." >&2
echo "- Anthropic's Constitution: paternalism and moralizing are disrespectful when unsolicited." >&2
exit 2
fi
if [ "$DECISION" = "pass" ]; then
exit 0
fi
fi
fi
fi

json_get() {
local filter="$1"
printf '%s' "$INPUT" | jq -r "$filter // empty" 2>/dev/null || true
Expand Down Expand Up @@ -48,15 +109,16 @@ if [ -z "$message" ]; then
exit 0
fi

# If the operator explicitly asked for rest/break advice this turn, the model is
# allowed to provide it. We detect this by looking for a marker phrase the model
# tends to repeat back when it is genuinely responding to a wellness ask.
if printf '%s\n' "$message" | grep -Eiq '(you (asked|requested) (for|about) (a break|rest|sleep|self-care)|since you (mentioned|said) you (are|were|feel) tired|as you requested .*break)'; then
# If the operator explicitly asked for rest/break advice this turn, the
# model is allowed to provide it. The allow vocab is loaded from
# packs/locale/<lang>.txt section [paternalism_allow].
if printf '%s\n' "$message" | grep -Eiq "(${PATERNALISM_ALLOW_RE})"; then
exit 0
fi

# Trigger: unsolicited paternalism vocabulary.
PATERNALISM='(^|[^[:alpha:]])(go to sleep|get some sleep|get some rest|take a break|save it for tomorrow|tomorrow morning|call it a night|rest up|step away|log off|wind down|your wellbeing|your well-being|take care of yourself|burnout|burned out|you'\''ve earned (a |some )?(rest|break)|it'\''s (late|getting late)|the work can wait|come back fresh|recharge|self-care|mental health break|you should stop|sleep matters|sleep is important|get some sleep|grab some sleep|give yourself a break|easy on yourself|don'\''t push yourself)([^[:alpha:]]|$)'
# Trigger: unsolicited paternalism vocabulary loaded from
# packs/locale/<lang>.txt section [paternalism].
PATERNALISM="(^|[^[:alpha:]])(${PATERNALISM_RE})([^[:alpha:]]|$)"

if printf '%s\n' "$message" | grep -Eiq "$PATERNALISM"; then
block "unsolicited rest/wellness paternalism in agent-mode session." \
Expand Down
36 changes: 36 additions & 0 deletions .claude/hooks/no-disclaimer-spam.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,42 @@ INPUT="$(cat)"
if ! command -v jq >/dev/null 2>&1; then exit 0; fi
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_disclaimer_spam.yaml" ]; then
TMP_INPUT="$(mktemp)"; printf '%s' "$INPUT" > "$TMP_INPUT"
VERDICT_JSON="$(agentcloseout-physics scan --category no_disclaimer_spam --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_disclaimer_spam"' 2>/dev/null)"
EVIDENCE="$(printf '%s' "$VERDICT_JSON" | jq -r '.redacted_evidence[0] // ""' 2>/dev/null)"
echo "BLOCKED: disclaimer spam: defensive padding ('Please note', 'It's important to mention', etc.)." >&2
echo "Matched rule: $RULE" >&2
[ -n "$EVIDENCE" ] && echo "Evidence: $EVIDENCE" >&2
echo "" >&2
echo "Repair guidance:" >&2
echo "- If the noted thing is important, state it directly. Don't preamble with a meta-frame." >&2
echo "- If it's not important enough to state directly, drop it." >&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() {
echo "BLOCKED: $1" >&2
Expand Down
38 changes: 38 additions & 0 deletions .claude/hooks/no-emoji-spam.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,44 @@ INPUT="$(cat)"
if ! command -v jq >/dev/null 2>&1; then exit 0; fi
if ! printf '%s' "$INPUT" | jq -e . >/dev/null 2>&1; then exit 0; fi

# Rust path: prefer agentcloseout-physics when available.
# Note: Rust hardcodes threshold > 3; LLM_DARK_PATTERNS_EMOJI_THRESHOLD only
# honored in the bash fallback below.
if command -v agentcloseout-physics >/dev/null 2>&1 && [ -z "${LLM_DARK_PATTERNS_EMOJI_THRESHOLD:-}" ]; 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_emoji_spam.yaml" ]; then
TMP_INPUT="$(mktemp)"; printf '%s' "$INPUT" > "$TMP_INPUT"
VERDICT_JSON="$(agentcloseout-physics scan --category no_emoji_spam --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_emoji_spam"' 2>/dev/null)"
echo "BLOCKED: emoji spam: > 3 emoji codepoints (default threshold)." >&2
echo "Matched rule: $RULE" >&2
echo "" >&2
echo "Repair guidance:" >&2
echo "- Drop the emoji and use plain text. Reserve emoji for information that prose alone cannot carry." >&2
echo "- Operator can customize the threshold via env (bash path only):" >&2
echo " LLM_DARK_PATTERNS_EMOJI_THRESHOLD=10 (permissive)" >&2
echo " LLM_DARK_PATTERNS_EMOJI_THRESHOLD=0 (zero tolerance)" >&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() {
echo "BLOCKED: $1" >&2
Expand Down
34 changes: 34 additions & 0 deletions .claude/hooks/no-tldr-bait.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,40 @@ INPUT="$(cat)"
if ! command -v jq >/dev/null 2>&1; then exit 0; fi
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_tldr_bait.yaml" ]; then
TMP_INPUT="$(mktemp)"; printf '%s' "$INPUT" > "$TMP_INPUT"
VERDICT_JSON="$(agentcloseout-physics scan --category no_tldr_bait --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_tldr_bait"' 2>/dev/null)"
echo "BLOCKED: TL;DR/summary bait at message end." >&2
echo "Matched rule: $RULE" >&2
echo "" >&2
echo "Repair guidance:" >&2
echo "- Drop the TL;DR / In summary / Bottom line trailer; power users already read the body." >&2
echo "- If the message is long enough to need a summary, lead with it; do not tail it." >&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() {
echo "BLOCKED: $1" >&2
Expand Down