Review: Bug: merge hook false positive
Verdict: READY
Template Completeness
- [x] Type — Bug
- [x] Lineage — "Discovered during pal-e-platform PR #192 merge (2026-03-27)"
- [x] Repo —
forgejo_admin/claude-custom - [x] What Broke — clear description of false positive on successful merge
- [x] Repro Steps — 4 steps, concrete
- [x] Expected Behavior — clear
- [x] Environment — local, current main, no alerts
- [x] Acceptance Criteria — 3 criteria, all testable
- [x] Related — project + originating PR referenced
Traceability
- [ ] story:X label — Missing. However, this is foundational hooks infrastructure. Acceptable for a discovered-scope bug, but should be tagged
story:dev-execute(merge hooks serve the developer execution workflow) before moving to next_up. - [x] arch:hooks label — present, correct component
- [x] Forgejo issue —
forgejo_admin/claude-custom#173, open
File Targets
The issue does not name specific file paths, but the bug description points to the
PostToolUse:mcp__forgejo__merge_approved_pr hook. Three hooks are registered for that matcher in settings.json (lines 216-230):- [x]
hooks/remind-update-docs.sh— verified: line 20 uses.tool_response.merged. This is the hook that emits the false positive "Merge was not successful" message described in the ticket. - [x]
hooks/post-mcp-merge-rebase.sh— verified: line 12 uses identical pattern. Silently exits on false negative (no user-visible error, but skips the local fast-forward). - [x]
hooks/board-item-on-merge.sh— verified: line 36 uses identical pattern. Silently exits on false negative (skips auto-move to done).
Root Cause Analysis
All three hooks check
jq -e '.tool_response.merged // false'. The MCP tool (forgejo-mcp/src/forgejo_mcp/tools/workflows.py line 263) returns json.dumps({"merged": True, ...}) — a JSON string, not a parsed object. Claude Code's PostToolUse hook receives the MCP response as .tool_response.result (a string), not as parsed fields at .tool_response.*.Evidence:
label-on-pr.sh (lines 30-34) and label-on-branch.sh (lines 24-28) both demonstrate the correct pattern — they try .tool_response.field first, then fall back to parsing .tool_response.result as a string. The merge hooks never implemented that fallback.The fix requires either:
- Parsing
.tool_response.resultas JSON and checking.mergedinside it, OR - Using
.tool_response.result | fromjson | .mergedin the jq expression
Repo Placement
Correct. Issue filed on
forgejo_admin/claude-custom, and all three affected hooks live in ~/claude-custom/hooks/. No cross-repo concern.Dependencies
- Board item #230 (
claude-custom#134) — "Post-merge hook fires on failed merges" — is the predecessor bug (now done/closed). The fix for #134 introduced the.tool_response.mergedcheck that is now itself broken. No blocking dependency. - No in_progress items block this work.
Acceptance Criteria
- [x] "Hook correctly detects
merged: truein MCP response" — testable by running the hook script with a mock JSON input piped to stdin - [x] "Hook does NOT emit false positive failure on successful squash merge" — testable end-to-end by merging a test PR
- [x] "Hook still correctly reports actual merge failures (405, 409, etc.)" — testable with mock input containing error responses
All criteria are verifiable. No test infrastructure exists (no
tests/ directory in claude-custom), so verification will be manual stdin piping. Consider adding a test harness as discovered scope.Blast Radius
- 3 hooks affected, not 1. The ticket describes
remind-update-docs.shbut the identical bug exists inpost-mcp-merge-rebase.shandboard-item-on-merge.sh. All three must be fixed together. post-mcp-merge-rebase.shsilently failing means local main never fast-forwards after MCP merges — agents may be working on stale main.board-item-on-merge.shsilently failing means board items are NOT being auto-moved to done after MCP merges.- The
block-mcp-merge.shPreToolUse hook is NOT affected (it reads.tool_input, not.tool_response). - The
post-merge-rebase.shBash-matcher hook is NOT affected (it checks.tool_response.exitCodeforgh pr merge, a different code path).
Decomposition (5-minute rule)
- 3 file targets, 1 repo — under threshold
- 3 acceptance criteria — under threshold
- All three files need the same one-line jq fix — well under 5 minutes
- No decomposition needed
Recommendation
- Add
story:dev-executelabel to the board item before moving to next_up (traceability gap). - Update the issue body to name all 3 affected files explicitly (currently only describes the symptom from
remind-update-docs.sh). - Fix must cover all 3 hooks, not just the one producing visible output.
After those two refinements, this ticket is ready for agent execution.