Skip to content

fix(hooks): Claude never saw hook messages — systemMessage is user-only#135

Merged
ANcpLua merged 3 commits intomainfrom
fix/hook-message-visibility
Feb 25, 2026
Merged

fix(hooks): Claude never saw hook messages — systemMessage is user-only#135
ANcpLua merged 3 commits intomainfrom
fix/hook-message-visibility

Conversation

@ANcpLua
Copy link
Owner

@ANcpLua ANcpLua commented Feb 25, 2026

Summary

  • Root cause: Both hookify and metacognitive-guard used systemMessage to communicate with Claude when blocking tools. Per the Claude Code hooks spec, systemMessage is displayed to the user only — Claude never receives it. This made every blocking hook's guidance invisible to Claude.
  • hookify/rule_engine.py: Rewrote all response paths to use spec-correct fields (permissionDecisionReason, reason, additionalContext) per event type. Removed all systemMessage usage.
  • hookify/hook_runner.py: Error reporting now uses stderr + exit code 2 (Claude-visible) instead of systemMessage + exit 0.
  • metacognitive-guard/epistemic-guard.sh: Migrated from deprecated decision/reason format to modern hookSpecificOutput with permissionDecisionReason.
  • metacognitive-guard/integrity-check.sh: Fixed exit code 1 (non-blocking, commits proceed!) to exit code 2 (actually blocks commits) with stderr output.
  • metacognitive-guard/struggle-detector: Split into async Stop hook (analysis + blackboard write) and new struggle-inject.sh on UserPromptSubmit (injects additionalContext Claude sees).
  • hookify/mtp-smart-test-filtering rule: Shortened 80-line MTP docs wall to 8-line directive message.

Test plan

  • Open a new Claude session in a .NET project with hookify enabled
  • Run dotnet test without --filter- — verify Claude sees the blocking message and retries with # VERIFY or a filter
  • Write code with DateTime.Now — verify Claude sees the epistemic guard correction
  • Stage a file with #pragma warning disable and run git commit — verify commit is blocked
  • Trigger struggle detection (multiple hedging responses) — verify suggestion appears on next prompt via additionalContext

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a five-agent council plugin accessible via /council [task].
    • Background struggle-detection now injects deep-think suggestions into prompts when repeated uncertainty is detected.
  • Bug Fixes

    • Improved error reporting: crashes now exit nonzero and surface errors reliably.
    • Commit integrity checks now block problematic commits with clear violation messages.
    • Clarified test-filter guidance with explicit retry options.
    • Tool/permission responses standardized so denials and warnings surface consistently.

Root cause: Both hookify and metacognitive-guard used `systemMessage` to
communicate with Claude when blocking tools. Per Claude Code hooks spec,
`systemMessage` is "shown to the user" only. Claude receives
`permissionDecisionReason` (PreToolUse), `reason` (Stop/PostToolUse),
`additionalContext` (UserPromptSubmit/PostToolUse/SessionStart), or
stderr with exit code 2.

hookify/rule_engine.py:
- PreToolUse deny: systemMessage → permissionDecisionReason
- PostToolUse block: systemMessage → decision/reason
- UserPromptSubmit block: systemMessage → decision/reason
- Warnings: systemMessage → additionalContext where Claude can see it
- Removed all systemMessage usage (no audience)

hookify/hook_runner.py:
- Error reporting: systemMessage + exit 0 → stderr + exit 2

hookify/mtp-smart-test-filtering:
- Rewrote 80-line MTP docs wall → 8-line directive message

metacognitive-guard/epistemic-guard.sh:
- Migrated 5 blocks from deprecated decision/reason to modern
  hookSpecificOutput.permissionDecision/permissionDecisionReason

metacognitive-guard/integrity-check.sh:
- Exit code 1 (non-blocking!) → exit code 2 (actually blocks commits)
- stdout (invisible) → stderr (Claude sees it)

metacognitive-guard/struggle-detector:
- Split into two hooks: Stop (async, analysis + blackboard write) and
  new struggle-inject.sh on UserPromptSubmit (reads blackboard, injects
  additionalContext that Claude actually sees)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 25, 2026 13:36
@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Caution

Review failed

Failed to post review comments

📝 Walkthrough

Walkthrough

Refactors hook outputs to Claude Code hooks format, improves hook runner error handling to emit stderr and exit code 2 on failures, converts struggle detector to an async Stop + UserPromptSubmit inject flow via a blackboard, and adds a five-agent council plugin (/council).

Changes

Cohort / File(s) Summary
Hook core logic & runner
plugins/hookify/hookify/core/rule_engine.py, plugins/hookify/hookify/core/hook_runner.py
Responses changed to event-specific hook fields (permissionDecision/permissionDecisionReason, decision/reason, additionalContext). Error handling now prints to stderr and exits with code 2 on failures; removed unconditional exit(0).
Hookify docs & rules
plugins/hookify/CLAUDE.md, plugins/hookify/global-rules/hookify.mtp-smart-test.local.md
Updated docs to describe new hook field semantics (systemMessage is user-only). Rewrote MTP smart-test rule to an explicit "pick one and retry" set of options.
Metacognitive guard — struggle flow
plugins/metacognitive-guard/hooks/scripts/struggle-detector.sh, plugins/metacognitive-guard/hooks/scripts/struggle-inject.sh, plugins/metacognitive-guard/hooks/hooks.json, plugins/metacognitive-guard/CLAUDE.md
Split struggle behavior: Stop hook writes blackboard (no JSON/systemMessage), new UserPromptSubmit hook reads blackboard and emits hookSpecificOutput.additionalContext when threshold met; hooks.json and CLAUDE.md updated.
Metacognitive guard — policy & integrity
plugins/metacognitive-guard/hooks/scripts/epistemic-guard.sh, plugins/metacognitive-guard/hooks/scripts/integrity-check.sh
Epistemic guard now emits hookSpecificOutput for PreToolUse denials (permissionDecision & permissionDecisionReason). Integrity-check writes violations to stderr and exits 2 on blocking violations; warnings remain non-blocking.
New plugin: council
plugins/council/...
Adds a five-agent council plugin with inline agent contexts and parallel execution for some agents; exposed via /council [task].
Release notes / changelog
CHANGELOG.md
Added Unreleased fixes for hookify and metacognitive-guard and an Added entry for the council plugin.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Claude as Claude
    participant HookRunner as Hook Runner
    participant StopHook as Stop Hook (struggle-detector)
    participant Blackboard as .blackboard
    participant InjectHook as Inject Hook (struggle-inject)

    rect rgba(200,200,255,0.5)
    User->>Claude: Submit prompt
    Claude->>HookRunner: Trigger UserPromptSubmit hooks
    end

    rect rgba(200,255,200,0.5)
    HookRunner->>InjectHook: Execute struggle-inject.sh
    InjectHook->>Blackboard: Read .struggle-count & .struggle-signals
    InjectHook-->>HookRunner: Return hookSpecificOutput.additionalContext (if threshold)
    HookRunner->>Claude: Provide additionalContext for response
    end

    rect rgba(255,200,200,0.5)
    Note over StopHook,Blackboard: Asynchronous prior flow
    StopHook->>Blackboard: Write struggle signals & increment count
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the root cause (systemMessage visibility) and the main fix (correcting message placement for Claude visibility), which aligns with the primary objective of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/hook-message-visibility

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link

Summary of Changes

Hello @ANcpLua, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the reliability and effectiveness of Claude's interaction with various hooks. The core problem addressed was Claude's inability to perceive messages from blocking hooks due to incorrect message delivery mechanisms. By aligning message fields and exit codes with the Claude Code hooks specification, the system now ensures that Claude receives critical feedback, understands why actions are blocked, and gets relevant contextual information, leading to a more robust and transparent AI-assisted development workflow.

Highlights

  • Corrected Claude Hook Communication: Resolved an issue where Claude never received hook messages by switching from systemMessage (user-only) to spec-correct fields like permissionDecisionReason, reason, and additionalContext for various hook events.
  • Improved Error Reporting: Updated hookify/hook_runner.py to use stderr and exit code 2 for errors, ensuring Claude is aware of hookify crashes and import errors, rather than silently failing with systemMessage and exit code 0.
  • Metacognitive Guard Updates: Migrated epistemic-guard.sh to use modern hookSpecificOutput for permission decisions and fixed integrity-check.sh to correctly block commits with exit code 2.
  • Struggle Detector Refactor: Split the struggle-detector into an async Stop hook (for analysis and blackboard writing) and a new struggle-inject.sh hook on UserPromptSubmit to deliver additionalContext to Claude.
  • MTP Test Filtering Rule Enhancement: Shortened and clarified the mtp-smart-test-filtering rule message, making it a concise, directive message with explicit action items for the user.
Changelog
  • CHANGELOG.md
    • Fixed PreToolUse deny putting message in systemMessage instead of permissionDecisionReason in hookify/rule_engine.py.
    • Fixed error reporting in hookify/hook_runner.py to use exit code 2 + stderr.
    • Rewrote hookify/global-rules/mtp-smart-test-filtering rule message to be directive.
    • Migrated metacognitive-guard/epistemic-guard.sh to modern hookSpecificOutput.permissionDecision/permissionDecisionReason format.
    • Fixed metacognitive-guard/integrity-check.sh exit code from 1 to 2 for blocking violations.
    • Split metacognitive-guard/struggle-detector into two hooks: async Stop and new struggle-inject.sh on UserPromptSubmit.
Activity
  • No human activity (comments, reviews) has been recorded on this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly refactors the hook messaging to align with the Claude Code hooks specification, moving away from the user-only systemMessage to Claude-visible fields like permissionDecisionReason and additionalContext. The changes are comprehensive, covering Python rule engines, shell script hooks, and documentation. Error reporting is also improved to be visible to Claude. I've identified one area for improvement in the new struggle-inject.sh script to enhance robustness when generating JSON, but overall the changes are excellent and address a critical functionality gap.

Comment on lines +37 to +44
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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
}
}'

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78d4adb86c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +26 to +27
CONSECUTIVE=$(cat "$STRUGGLE_COUNT_FILE" 2>/dev/null || echo "0")
[[ "$CONSECUTIVE" -lt 2 ]] && exit 0

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a critical bug where Claude never received hook messages when tools were blocked. The root cause was using systemMessage (user-only display field) instead of the correct Claude-facing fields defined in the Claude Code hooks specification.

Changes:

  • Migrated all hook responses from systemMessage to spec-compliant fields: permissionDecisionReason (PreToolUse deny), reason (Stop/PostToolUse block), additionalContext (warnings)
  • Fixed hook error handling to use exit code 2 with stderr (Claude-visible) instead of exit 0 with JSON output
  • Split struggle detection into two-phase system: async Stop hook (analysis) + UserPromptSubmit hook (injection via additionalContext)

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
plugins/hookify/hookify/core/rule_engine.py Rewrote response generation to use event-specific fields per Claude Code spec; removed all systemMessage usage; split PreToolUse/PostToolUse/Stop into separate branches with correct formats
plugins/hookify/hookify/core/hook_runner.py Changed error handling from systemMessage+exit 0 to stderr+exit 2 so Claude sees hookify crashes
plugins/hookify/global-rules/hookify.mtp-smart-test.local.md Shortened message from 80 lines to 8 lines with directive action items
plugins/hookify/CLAUDE.md Updated documentation to explain correct field usage and note that systemMessage is user-only
plugins/metacognitive-guard/hooks/scripts/epistemic-guard.sh Migrated 5 blocking responses from deprecated decision/reason format to hookSpecificOutput.permissionDecision/permissionDecisionReason
plugins/metacognitive-guard/hooks/scripts/integrity-check.sh Fixed exit code 1 (non-blocking) to exit 2 (blocking) with stderr output so violations actually block commits
plugins/metacognitive-guard/hooks/scripts/struggle-detector.sh Removed systemMessage output; now only writes to blackboard for async delivery
plugins/metacognitive-guard/hooks/scripts/struggle-inject.sh New UserPromptSubmit hook that reads blackboard state and injects struggle detection via additionalContext
plugins/metacognitive-guard/hooks/hooks.json Added UserPromptSubmit hook configuration for struggle-inject.sh
plugins/metacognitive-guard/CLAUDE.md Updated to document two-part struggle detection system
CHANGELOG.md Comprehensive documentation of all fixes under Fixed section

SIGNALS_FILE="$BLACKBOARD/.struggle-signals"
RECENT_SIGNALS=""
if [[ -f "$SIGNALS_FILE" ]]; then
RECENT_SIGNALS=$(tail -20 "$SIGNALS_FILE" | grep ' - ' | sed 's/ - //' | tr '\n' ', ' | sed 's/, $//')
Copy link

Copilot AI Feb 25, 2026

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).

Suggested change
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/, $//')

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +44
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
Copy link

Copilot AI Feb 25, 2026

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."
  }
}'
Suggested change
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."
}
}'

Copilot uses AI. Check for mistakes.
ANcpLua and others added 2 commits February 25, 2026 15:11
Review feedback: the empty hookSpecificOutput with only hookEventName
serves no purpose — decision/reason are top-level fields for PostToolUse.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…wers

- grep under set -e: add || true to prevent exit 1 when no signal matches
- JSON escaping: use jq instead of raw heredoc for safe JSON construction
- Lost high-severity path: score > 25 now sets count to 2 immediately,
  preserving single-response escalation that was lost in the split

Flagged by: Gemini, Copilot, Codex (ChatGPT)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 25, 2026 14:15
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 5 comments.

Comment on lines +36 to +38
# Inject as additionalContext — use jq for safe JSON construction
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."
jq -n --arg ctx "$MSG" '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":$ctx}}'
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

struggle-inject.sh assumes jq is available. If jq is missing, the script will exit non-zero under set -e and the hook won’t inject any additionalContext. Other hooks in this plugin (e.g., truth-beacon.sh) include a jq/no-jq fallback—consider adding the same here so the hook degrades gracefully.

Suggested change
# Inject as additionalContext — use jq for safe JSON construction
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."
jq -n --arg ctx "$MSG" '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":$ctx}}'
# Inject as additionalContext — prefer jq for safe JSON construction, but degrade gracefully if missing
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."
if command -v jq >/dev/null 2>&1; then
jq -n --arg ctx "$MSG" '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":$ctx}}'
else
# Fallback: emit JSON using basic shell escaping when jq is not available
ESCAPED_MSG=$(printf '%s' "$MSG" | sed 's/\\/\\\\/g; s/"/\\"/g')
printf '{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"%s"}}\n' "$ESCAPED_MSG"
fi

Copilot uses AI. Check for mistakes.
STRUGGLE_COUNT_FILE="$BLACKBOARD/.struggle-count"

# Check consecutive struggle count
CONSECUTIVE=$(cat "$STRUGGLE_COUNT_FILE" 2>/dev/null || echo "0")
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CONSECUTIVE is read from a file and then compared with -lt. If the file content is empty or non-numeric, the comparison returns an error and the script may proceed as if threshold was met. Consider normalizing CONSECUTIVE to an integer (default 0) before the numeric comparison.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +206 to +211
# 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
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the blocking path, the hook relies on exit 2 + stderr for Claude-visible output, but it currently only prints the compact VIOLATIONS[@] entries to stderr. The more detailed context emitted earlier in the script goes to stdout and may not be visible to Claude when blocking. Consider writing the key violation details/locations to stderr as well (or redirecting stdout to stderr in the violation case) so Claude gets actionable guidance.

Copilot uses AI. Check for mistakes.
Comment on lines +171 to +173
# 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.
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section now intentionally produces no JSON output, but the surrounding section header/comments still describe "OUTPUT SUGGESTION IF THRESHOLD MET". Updating the header/comments to match the new behavior will avoid confusion when maintaining the hook.

Copilot uses AI. Check for mistakes.
Comment on lines +133 to +144
# Use additionalContext where Claude can see it
if hook_event in ['PostToolUse', 'UserPromptSubmit', 'SessionStart']:
return {
"hookSpecificOutput": {
"hookEventName": hook_event,
"additionalContext": combined_warning
}
}
else:
# PreToolUse warnings can't reach Claude without blocking
# No audience for the message — skip
return {}
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

action: warn rules for PreToolUse are currently dropped (function returns {} for warnings when hook_event is PreToolUse). 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., via systemMessage) or, if supported by the Claude Code hooks spec, returning a PreToolUse hookSpecificOutput with permissionDecision: "allow" plus a reason field so Claude can see the warning without blocking.

Copilot uses AI. Check for mistakes.
@ANcpLua ANcpLua merged commit ee94c88 into main Feb 25, 2026
13 of 14 checks passed
@ANcpLua ANcpLua deleted the fix/hook-message-visibility branch February 25, 2026 14:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants