-
Notifications
You must be signed in to change notification settings - Fork 0
fix(hooks): Claude never saw hook messages — systemMessage is user-only #135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -203,8 +203,11 @@ elif [[ "$VIOLATION_COUNT" -eq 0 ]]; then | |
| echo -e "${YELLOW}WARNINGS|$WARNING_COUNT warnings (review recommended)${NC}" | ||
| exit 0 | ||
| else | ||
| echo -e "${RED}BLOCKED|$VIOLATION_COUNT violations, $WARNING_COUNT warnings${NC}" | ||
| echo "" | ||
| echo "Fix violations before committing. These patterns indicate shortcuts that will cause problems later." | ||
| exit 1 | ||
| # Exit code 2 = blocking error, stderr is fed to Claude | ||
| echo "COMMIT INTEGRITY: $VIOLATION_COUNT violations found. Fix these before committing:" >&2 | ||
| for v in "${VIOLATIONS[@]}"; do | ||
| echo " - $v" >&2 | ||
| done | ||
| echo "These patterns indicate shortcuts that will cause problems later." >&2 | ||
|
Comment on lines
+206
to
+211
|
||
| exit 2 | ||
| fi | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -164,19 +164,8 @@ CONSECUTIVE=$(cat "$STRUGGLE_COUNT_FILE" 2>/dev/null || echo "0") | |
| done | ||
| } >> "$STRUGGLE_FILE" | ||
|
|
||
| # Trigger after 2+ consecutive struggling responses OR high single score | ||
| if [[ "$CONSECUTIVE" -ge 2 ]] || [[ "$score" -gt 25 ]]; then | ||
| # Build signals list with proper escaping | ||
| SIGNALS_LIST="" | ||
| for sig in "${signals[@]}"; do | ||
| SIGNALS_LIST="${SIGNALS_LIST}- ${sig}\\n" | ||
| done | ||
|
|
||
| cat <<EOF | ||
| { | ||
| "systemMessage": "<struggle-detected score=\"$score\" consecutive=\"$CONSECUTIVE\">\\n\\nClaude appears to be struggling with this problem.\\n\\nSignals detected:\\n${SIGNALS_LIST}\\nSuggestion: Use the Task tool with subagent_type='deep-think-partner' for thorough analysis.\\n\\nExample: \\\"I'm finding this complex. Want me to spawn a deep-thinker for a more thorough analysis?\\\"\\n</struggle-detected>" | ||
| } | ||
| EOF | ||
| fi | ||
| # Note: No JSON output needed here. The struggle-inject.sh (UserPromptSubmit) | ||
| # reads .blackboard/.struggle-count and injects additionalContext to Claude | ||
| # on the next prompt. This async Stop hook only does analysis + blackboard writes. | ||
|
Comment on lines
+171
to
+173
|
||
|
|
||
| exit 0 | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,49 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #!/usr/bin/env bash | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ============================================================================= | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # STRUGGLE INJECT - Delivers struggle detection to Claude via UserPromptSubmit | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ============================================================================= | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Trigger: UserPromptSubmit (before Claude processes the next prompt) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Purpose: Read blackboard state from async struggle-detector and inject | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # suggestion as additionalContext so Claude actually sees it. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # The struggle-detector.sh (Stop, async) writes scoring data to: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # .blackboard/.struggle-count — consecutive struggling responses | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # .blackboard/.struggle-signals — detailed signal log | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # This hook reads those files and injects context when threshold is met. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # After injection, resets the counter to prevent repeated suggestions. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # ============================================================================= | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| set -euo pipefail | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-}" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [[ -z "$PLUGIN_ROOT" ]] && exit 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| BLACKBOARD="$PLUGIN_ROOT/.blackboard" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| STRUGGLE_COUNT_FILE="$BLACKBOARD/.struggle-count" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Check consecutive struggle count | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CONSECUTIVE=$(cat "$STRUGGLE_COUNT_FILE" 2>/dev/null || echo "0") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| CONSECUTIVE=$(cat "$STRUGGLE_COUNT_FILE" 2>/dev/null || echo "0") | |
| CONSECUTIVE=$(cat "$STRUGGLE_COUNT_FILE" 2>/dev/null || echo "0") | |
| # Normalize to integer; default to 0 if empty or non-numeric | |
| if ! [[ "$CONSECUTIVE" =~ ^[0-9]+$ ]]; then | |
| CONSECUTIVE=0 | |
| fi |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Preserve single-response struggle escalation
struggle-inject.sh now injects guidance only when .struggle-count is >= 2, but before this change the detector escalated on either two consecutive struggles or one very high score (score > 25). With the new split, a severe one-off struggle increments the count to 1 and no suggestion is injected on the next prompt unless a second bad response occurs, so the high-severity path was unintentionally removed.
Useful? React with 👍 / 👎.
Outdated
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The grep ' - ' command on line 33 may fail when there are no matching lines, causing the script to exit early due to set -euo pipefail on line 17. This would prevent the struggle detection message from being injected even when the count threshold is met.
Consider adding || true to handle the case when grep finds no matches, similar to how grep is used elsewhere in this codebase (e.g., ralph-loop.sh uses grep -q with proper error handling).
| RECENT_SIGNALS=$(tail -20 "$SIGNALS_FILE" | grep ' - ' | sed 's/ - //' | tr '\n' ', ' | sed 's/, $//') | |
| RECENT_SIGNALS=$((tail -20 "$SIGNALS_FILE" | grep ' - ' || true) | sed 's/ - //' | tr '\n' ', ' | sed 's/, $//') |
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While the current implementation is likely safe because the $RECENT_SIGNALS variable is constructed from known-safe strings, it's fragile. If a future change to struggle-detector.sh introduces signals containing special characters (like double quotes), it could break the JSON output. Using jq to construct the JSON is a more robust and secure approach that prevents potential injection vulnerabilities.
| cat <<EOF | |
| { | |
| "hookSpecificOutput": { | |
| "hookEventName": "UserPromptSubmit", | |
| "additionalContext": "STRUGGLE DETECTOR ($CONSECUTIVE consecutive uncertain responses): Signals — $RECENT_SIGNALS. Consider spawning a deep-think-partner agent (Task tool, subagent_type='deep-think-partner') for thorough analysis instead of continuing to iterate." | |
| } | |
| } | |
| EOF | |
| CONTEXT_MSG="STRUGGLE DETECTOR ($CONSECUTIVE consecutive uncertain responses): Signals — $RECENT_SIGNALS. Consider spawning a deep-think-partner agent (Task tool, subagent_type='deep-think-partner') for thorough analysis instead of continuing to iterate." | |
| # Use jq to safely construct the JSON output. | |
| jq -n \ | |
| --arg context "$CONTEXT_MSG" \ | |
| '{ | |
| "hookSpecificOutput": { | |
| "hookEventName": "UserPromptSubmit", | |
| "additionalContext": $context | |
| } | |
| }' |
Outdated
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The $RECENT_SIGNALS variable is directly interpolated into the JSON output without escaping. If the signals file contains special characters like quotes or newlines, this could break the JSON structure or allow injection attacks.
Consider using jq to properly escape the variable, similar to how truth-beacon.sh handles context injection (line 72 of truth-beacon.sh uses jq -n --arg ctx "$CONTEXT"). For example:
jq -n --arg consecutive "$CONSECUTIVE" --arg signals "$RECENT_SIGNALS" '{
hookSpecificOutput: {
hookEventName: "UserPromptSubmit",
additionalContext: "STRUGGLE DETECTOR (\($consecutive) consecutive uncertain responses): Signals — \($signals). Consider spawning a deep-think-partner agent (Task tool, subagent_type='\''deep-think-partner'\'') for thorough analysis instead of continuing to iterate."
}
}'| cat <<EOF | |
| { | |
| "hookSpecificOutput": { | |
| "hookEventName": "UserPromptSubmit", | |
| "additionalContext": "STRUGGLE DETECTOR ($CONSECUTIVE consecutive uncertain responses): Signals — $RECENT_SIGNALS. Consider spawning a deep-think-partner agent (Task tool, subagent_type='deep-think-partner') for thorough analysis instead of continuing to iterate." | |
| } | |
| } | |
| EOF | |
| jq -n --arg consecutive "$CONSECUTIVE" --arg signals "$RECENT_SIGNALS" '{ | |
| hookSpecificOutput: { | |
| hookEventName: "UserPromptSubmit", | |
| additionalContext: "STRUGGLE DETECTOR (\($consecutive) consecutive uncertain responses): Signals — \($signals). Consider spawning a deep-think-partner agent (Task tool, subagent_type='\''deep-think-partner'\'') for thorough analysis instead of continuing to iterate." | |
| } | |
| }' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
action: warnrules for PreToolUse are currently dropped (function returns{}for warnings whenhook_eventisPreToolUse). This makes warnings effectively no-op for the most common hook path, and contradicts the documented "warn but allow" behavior. Consider emitting a user-visible warning (e.g., viasystemMessage) or, if supported by the Claude Code hooks spec, returning a PreToolUsehookSpecificOutputwithpermissionDecision: "allow"plus a reason field so Claude can see the warning without blocking.