Skip to content

feat(hooks): one-shot PR-merge warning per session+PR#19

Merged
tombakerjr merged 1 commit into
mainfrom
feature/git-guard-warn-once-per-session
May 23, 2026
Merged

feat(hooks): one-shot PR-merge warning per session+PR#19
tombakerjr merged 1 commit into
mainfrom
feature/git-guard-warn-once-per-session

Conversation

@tombakerjr
Copy link
Copy Markdown
Owner

Summary

Adopt the dedup pattern from anthropics/claude-code's security-guidance plugin so the merge-readiness reminder fires once per logical unit of work (session + repo + PR) instead of every single time. Before: noisy and easy to start ignoring. After: see it once, confirm, retry — quiet on subsequent attempts.

Mechanism

Mirrors plugins/security-guidance/hooks/security_reminder_hook.py:

  • State file: ~/.claude/git_guard_warnings_{session_id}.json, holding a set of warning keys already shown this session.
  • session_id pulled from the hook's stdin JSON input.
  • Warning key: merge:<owner>/<repo>/<pr_num> — extracted from the command (--repo flag if present, else current repo via gh repo view; PR number from arg, or "current" if invoked without one).
  • First detection of a key: print reminder to stderr + sys.exit(2) (BLOCK). Subsequent detections of the same key in the same session: silent allow.
  • Periodic cleanup: 10% chance per invocation removes state files older than 30 days (matches the security plugin's housekeeping).

Behavior change

The reminder output switched from additionalContext (informational, never blocks) to stderr + exit 2 (BLOCK on first attempt). This matches the reference implementation and makes "confirm and retry" the explicit gate.

What's not changed

The on-main commit-block and direct-push-to-main blocks are unchanged — those are absolute, not dedupable.

Why

additionalContext reminders fire every single invocation. When Claude (or me) makes multiple PR-merge attempts in a session — across different PRs, or retrying after fixes — every single attempt printed the same long reminder, which trains the eye to ignore it. The block-then-allow pattern forces one explicit acknowledgment per real unit of work, then gets out of the way.

Test plan

  • Merge a PR: first gh pr merge <num> invocation prints reminder and is blocked. Re-issue → succeeds silently.
  • Merge a second PR in the same session: fresh reminder + block on first attempt (different pr_num → different key). Re-issue → succeeds.
  • After 30+ days: state file from a prior session gets cleaned up on a future invocation (10% chance per run).
  • State directory: ~/.claude/git_guard_warnings_<session>.json exists after first warning and contains the key.

🤖 Generated with Claude Code

Adopt the pattern from anthropics/claude-code's security-guidance plugin so
the merge reminder fires once per logical unit of work (session + repo + PR)
instead of every single time. Before: the reminder fired on every merge
attempt, even immediately after acknowledging it — noisy and easy to ignore.
After: first attempt prints the reminder and blocks (exit 2); the user/Claude
sees it, confirms readiness, re-issues the command, and it proceeds silently.

Mechanism (matches the security-guidance reference implementation):
- Per-session state file at ~/.claude/git_guard_warnings_{session_id}.json
- session_id pulled from the hook's stdin JSON input
- Warning key: merge:<owner>/<repo>/<pr_num> — extracted from the command
  (--repo flag or current repo via `gh repo view`; PR number from arg, or
  "current" if invoked without one)
- First detection of a key: print reminder to stderr + sys.exit(2) (BLOCK).
  Subsequent detections of the same key in the same session: silent allow.
- Periodic cleanup: 10% chance per invocation removes state files older than
  30 days (matches the security plugin's housekeeping)

Behavior change for the reminder output: switched from `additionalContext`
(informational, never blocks) to stderr+exit-2 (BLOCK on first attempt).
This matches the reference and makes "confirm and retry" the explicit gate.

The on-main commit-block and direct-push-to-main blocks are unchanged —
those are absolute and shouldn't be dedupable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude Bot commented May 23, 2026

Claude finished @tombakerjr's task in 1m 19s —— View job


Reviewed commit: 4967c2e

FIX: hooks/git-guard.py:136session_id fallback is the literal string "default", so any hook invocation where Claude Code doesn't supply session_id writes to ~/.claude/git_guard_warnings_default.json. A single such invocation permanently silences the warning for every future "default" session. Use uuid.uuid4().hex (or similar) as the fallback so an ID-less invocation gets its own ephemeral bucket rather than a shared global one. Fix this →

FIX: hooks/git-guard.py:114 — the PR-number regex r'gh\s+pr\s+merge\s+(\d+)' only matches when the number directly follows merge. For gh pr merge --squash 123 (flags before number), pr_part falls back to "current". This means: first attempt gh pr merge 123 → key merge:repo/123 (blocked, saved); retry with gh pr merge --squash 123 → key merge:repo/current (not in set → blocked again). The user confirmed and retried, yet gets blocked a second time. Fix: use r'gh\s+pr\s+merge\b.*?\b(\d+)' or scan for the first standalone integer anywhere after merge. Fix this →

@tombakerjr tombakerjr merged commit a21238b into main May 23, 2026
2 checks passed
@tombakerjr tombakerjr deleted the feature/git-guard-warn-once-per-session branch May 23, 2026 21:17
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.

1 participant