Skip to content

feat(hooks): stop a session following its own PR after handoff - #1649

Merged
BigSimmo merged 12 commits into
mainfrom
claude/pr-handoff-stop-hook
Aug 7, 2026
Merged

feat(hooks): stop a session following its own PR after handoff#1649
BigSimmo merged 12 commits into
mainfrom
claude/pr-handoff-stop-hook

Conversation

@BigSimmo

@BigSimmo BigSimmo commented Aug 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add .claude/hooks/pr-handoff-stop.sh, a two-mode Claude Code hook that ends a session's involvement with its own pull request. The PostToolUse half writes a session-scoped marker when a PR-creating call — gh pr create or any create_pull_request MCP tool — returns a real PR URL, and injects a "the handoff is complete, stop" note into the model's context. A failed create returns no URL and writes no marker, so a session is never stranded with no PR to hand off. The marker lives inside the worktree's git dir and is therefore never committed.
  • Add the PreToolUse half, which is where the enforcement actually lives. While the marker exists it denies three classes of call: shell CI polling (gh pr checks|status|view|diff|list, gh run watch|view|list|rerun|download, gh api …actions/runs|check-runs|check-suites|/pulls/, sync:pr-branches); GitHub MCP tools whose names carry pull_request, workflow_run, workflow_job, check_run, check_suite, job_log, or update_branch, so a connector is not a way around the shell rule; and the loop machinery Monitor, ScheduleWakeup, and CronCreate, which is how a session parks itself on a PR without running a single command. Committing, pushing, ledger appends, and PR create/merge stay allowed.
  • Register both halves in .claude/settings.json with matchers narrow enough that ordinary Read/Edit/Grep calls never spawn the hook.
  • Add the ## Stop when the pull request is open section to AGENTS.md stating the rule, its rationale, what the hook denies, and the two documented unlocks: prefix a shell command with CLAUDE_ALLOW_PR_FOLLOW=1, or delete the marker named in the deny message. Both are meant for an explicit user ask.
  • Add a closing step 8 (Stop) to .claude/skills/handoff/SKILL.md so the skill that opens the PR is also the skill that ends there.

Why: a session that opens a PR and then stays attached to it — polling checks, watching workflow runs, re-running failed jobs, re-syncing the branch, answering review bots — spends a long tail of usage on work the user did not ask for. Claude Code on the web is the worst case, because the cloud session keeps running after the PR exists and nothing naturally ends the loop. Prose alone has not held for comparable rules in this repo, so the rule ships with a hook that denies the calls.

Reviewer notes:

  • Sessions that never create a PR are untouched, because no marker is ever written in them. Run PR sweeps, pr-ci-fix work, and reviews of someone else's PR keep working exactly as before.
  • The write-tool exemption regex is anchored ((create|merge)_?pull_?request$), so create_pull_request and merge_pull_request are allowed while create_pull_request_review is denied — replying to review bots on your own PR is part of the loop being stopped.
  • Scope limit worth knowing: .claude/settings.json binds Claude Code sessions only. Codex and Cursor agents get the AGENTS.md prose and no hook enforcement.

Verification

  • npm run verify:pr-local

Verification not run: npm run verify:pr-local — this branch is a fresh worktree with no node_modules, and installing there while other worktrees hold test/build leases is the concurrent-npm ci corruption this repo has hit before. The change touches no TypeScript, no source, and no test file, so lint, typecheck, and the unit suite have nothing to exercise. What was run instead, from the pushed worktree:

  • node scripts/check-docs-links.mjsdocs link check passed: 1629 repo path references resolve.
  • node scripts/ci-change-scope.mjs --self-testCI change scope self-test passed.
  • node scripts/check-gate-manifest.mjsGate-manifest OK: all 33 verify:cheap gates are enforced in CI (static-pr + mapped jobs), and the 30 static gates are documented consistently.
  • node scripts/check-codex-cloud-setup.mjs[Codex Cloud Check] PASS: static Cloud contracts match.
  • node scripts/check-branch-review-ledger.mjsBranch review ledger guard passed: 609 live table records + 1206 archived …
  • node scripts/check-outstanding-issues.mjsOutstanding-issues guard passed: 253 rows (122 open, 131 archived) …
  • Prettier over the three formatted files (AGENTS.md, .claude/settings.json, .claude/skills/handoff/SKILL.md) — All matched files use Prettier code style!
  • bash -n .claude/hooks/pr-handoff-stop.sh — clean.
  • classifyPullRequestFiles over the changed paths — clinicalRisk: false, operationalRisk: false, ragRanking: false, ui: false, so no governance preflight or RAG impact: line is required for this diff.

Hook behaviour was exercised directly by piping representative hook payloads into the script. Denied: Monitor, ScheduleWakeup, CronCreate, mcp__github__get_pull_request, mcp__github__list_workflow_runs, mcp__github__get_job_logs, mcp__github__update_pull_request_branch, mcp__github__create_pull_request_review, gh pr checks --watch. Allowed: mcp__github__create_pull_request, mcp__github__merge_pull_request, Read, Edit, npm run ledger:append, git push, gh pr merge --squash --auto, and any command carrying the CLAUDE_ALLOW_PR_FOLLOW=1 prefix. Marker written for both a shell create and an MCP create that returned a PR URL; no marker written for a create whose output carried no URL; a different session_id passed straight through. The jq-less fallback path produced the same JSON in both modes.

  • npm run verify:ui when UI, routing, styling, browser behavior, reduced-motion, or forced-colors behavior changed

UI verification not run: no UI, routing, styling, or browser behaviour is touched by this diff.

Risk and rollout

  • Risk: the failure mode to watch is over-blocking — a denied PreToolUse call that a session legitimately needed. The deny list is deliberately narrow (PR/CI following only) and fires solely in a session that itself created a PR, so a session doing ordinary work, reviewing someone else's PR, or running a Run PR sweep is never affected. Every deny message names both unlocks. The hook exits 0 without a decision on any parse problem, so a malformed payload leaves the tool call exactly as it was rather than failing it.
  • Rollback: revert this commit, or remove the PreToolUse/PostToolUse blocks from .claude/settings.json to disable enforcement while keeping the documentation. No state outside the hook's own marker file is involved, and that file lives in the git dir and disappears with the worktree.
  • Provider or production effects: None. No application code, no schema, no provider call, no CI workflow change.

Notes

  • Branch note: this work was originally staged on claude/cloud-pr-loop-prevention-bc052b, but that branch was 110 commits behind main and carried two unmerged commits from an earlier session (chore: complete sentry setup hardening, chore: complete sentry logging and environment hardening, ~389 lines across src/sentry.*.config.ts, src/lib/env.ts, and src/lib/supabase/client.tsx). Pushing it would have bundled that provider/production work into this PR, so the change was re-applied on a fresh branch cut from current origin/main. Those commits remain untouched on the original branch.

🤖 Generated with Claude Code


Open in Devin Review

Summary by CodeRabbit

  • New Features

    • Sessions now report the pull request URL and stop follow-up monitoring by default.
    • Added safeguards against post-creation polling, CI supervision, branch synchronization, and review-bot interactions.
    • Explicit overrides remain available when continued follow-up is required.
  • Documentation

    • Documented the updated pull request handoff workflow, restrictions, and override options.
  • Tests

    • Added coverage for pull request detection, session handling, fallback behavior, cleanup, and blocked follow-up actions.

A session that opens a PR and then stays attached to it — polling `gh pr
checks`, watching workflow runs, re-running jobs, syncing the branch, replying
to review bots, or parking a Monitor/wake-up on it — burns a long tail of usage
on work nobody asked for. Claude Code on the web is the worst case: the cloud
session keeps running once the PR exists, so nothing naturally ends the loop.

Nothing in the repo said to stop, so add the rule and enforce it:

- `.claude/hooks/pr-handoff-stop.sh` writes a session-scoped marker (inside the
  worktree git dir, never committed) when a PR-creating call — `gh pr create` or
  any `create_pull_request` MCP tool — returns a real PR URL, and injects a
  "handoff complete, stop" note into context.
- While that marker exists, the PreToolUse half denies shell CI polling, GitHub
  MCP PR/CI tools, and the loop machinery (Monitor / ScheduleWakeup /
  CronCreate). Committing, pushing, ledger appends, and PR create/merge stay
  allowed.
- A failed create writes no marker, so a session is never stranded without a PR
  to hand off. Sessions that never create a PR are untouched, which leaves
  `Run PR` sweeps, pr-ci-fix work, and reviews of other people's PRs working.
- Unlock on an explicit user ask: prefix a shell command with
  `CLAUDE_ALLOW_PR_FOLLOW=1`, or delete the marker the deny message names.

AGENTS.md gains the "Stop when the pull request is open" section and the handoff
skill gains a closing "Stop" step so the rule is stated as well as enforced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@supabase

supabase Bot commented Aug 6, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project sjrfecxgysukkwxsowpy because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 46 minutes

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b1e5c794-3450-43b8-ade4-eea11a4863b9

📥 Commits

Reviewing files that changed from the base of the PR and between ed21f6f and 43250d4.

📒 Files selected for processing (1)
  • docs/branch-review-ledger.md
📝 Walkthrough

Walkthrough

The PR adds a session-scoped hook that records successful GitHub pull request creation and blocks prohibited follow-up actions. Configuration, handoff guidance, ledger records, and Vitest coverage support the hook.

Changes

PR handoff stop enforcement

Layer / File(s) Summary
Detect PR creation and track session state
.claude/hooks/pr-handoff-stop.sh
The hook parses tool payloads, detects successful GitHub PR creation, validates session IDs, and writes session markers.
Enforce the post-PR stop state
.claude/hooks/pr-handoff-stop.sh, .claude/settings.json, AGENTS.md
Pre-tool enforcement blocks PR, CI, monitoring, polling, review, and branch-sync follow-up actions while allowing PR writes and an explicit bypass.
Document and validate handoff behavior
.claude/skills/handoff/SKILL.md, tests/pr-handoff-stop.test.ts, docs/branch-review-ledger.md
Handoff guidance defines the stop rule. Tests cover marker handling, fallback parsing, denied actions, and bypass handling. The ledger adds six PR review records.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Session
  participant pr_handoff_stop
  participant SessionMarker
  participant GitHubTools
  Session->>pr_handoff_stop: create pull request
  pr_handoff_stop->>GitHubTools: inspect PR creation output
  GitHubTools-->>pr_handoff_stop: GitHub PR URL
  pr_handoff_stop->>SessionMarker: write session marker
  Session->>pr_handoff_stop: request follow-up check
  pr_handoff_stop->>SessionMarker: inspect active marker
  pr_handoff_stop-->>Session: deny follow-up operation
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: stopping a session after it hands off its own pull request.
Description check ✅ Passed The description covers the change, verification results and exceptions, risk, rollback, effects, and notes with sufficient detail.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/pr-handoff-stop-hook

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

devin-ai-integration[bot]

This comment was marked as resolved.

cursoragent and others added 2 commits August 6, 2026 15:55
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
- jq-less post mode only scans tool_response for PR URLs, so a failed
  gh pr create whose body mentions another PR no longer writes the marker
- use --absolute-git-dir so the marker path is cwd-independent
- read PowerShell script/code/input fields; prune day-old markers;
  escape control chars in JSON; sync pr_status into the PreToolUse matcher

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
@BigSimmo

BigSimmo commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

Addressed the remaining review notes in 5d96617 (beyond the two inline Devin threads):

  • PowerShell tool payloadscommand_text now also reads .tool_input.script / .code / .input (jq path and jq-less field extract), so PowerShell create/deny detection is not limited to .command.
  • JSON escapingjson_escape folds CR/LF/tab to spaces and strips other C0 controls before quote/backslash escaping.
  • Marker accumulationpost prunes claude-pr-handoff-* files older than a day under the git dir.
  • pr_status matcher drift — added .*pr_status.* to the PreToolUse matcher in .claude/settings.json so it stays in sync with the script deny regex.
  • Shell substring deny — left as designed (cheap, no shell parse). False positives that only mention blocked tokens already have CLAUDE_ALLOW_PR_FOLLOW=1; documented that trade-off in a comment next to the filter.

Also merged origin/main (clean) so the branch is no longer behind. Prior CI reds on this PR were GitHub Actions Service Unavailable during action download (Change scope / PR policy / Semgrep), not product failures — this push should re-run them.

devin-ai-integration[bot]

This comment was marked as resolved.

Anchor create_pull_request matching so review tools do not write the
handoff marker, keep the active session marker across day-old prune,
sanitize session ids used in marker paths, and cover the contract with
focused tests.

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

When jq is missing, quote-naive JSON extraction stopped at the first
escaped quote inside tool_input.command, so a compound shell like
`git commit -m "msg" && gh pr checks` never saw the follow token.
Match shell create/follow regexes against the raw payload as well in
that path, and cover it with regression tests.

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
devin-ai-integration[bot]

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

- Restrict post-mode create-token fallback to the payload half before
  tool_response so printed docs cannot write a marker.
- Isolate jq-less tool_output from a later tool_input URL suffix.
- Fail open on missing/unsafe session ids instead of sharing
  unknown-session.
- Require CLAUDE_ALLOW_PR_FOLLOW=1 as a command prefix.
- Drop the identity-dependent empty commit from hook tests.
- Record the Run PR sweep ledger row for PR #1649.

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request Aug 6, 2026
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
@cursor
cursor Bot force-pushed the claude/pr-handoff-stop-hook branch from 3403126 to 42ae4f3 Compare August 6, 2026 16:57
@BigSimmo
BigSimmo requested a lite review from Copilot August 6, 2026 17:00

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment thread docs/branch-review-ledger.md
cursoragent and others added 2 commits August 6, 2026 17:10
Bugbot found age-based sibling marker prune on every Bash PostToolUse
could disarm a long-lived handoff session that only uses Read/Edit.
Drop that prune, emit handoff context only after a successful marker
write, and deny gh pr comment/review to match the AGENTS review-bot loop.

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Supersede stacked Run PR rows that pointed at unresolvable HEADs and
record the heavy review-and-fix pass for the product fix commit.

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
@BigSimmo

BigSimmo commented Aug 6, 2026

Copy link
Copy Markdown
Owner Author

PR #1649 review-and-fix summary

Tip: 935671ca956755ce208726144bb389e01a3b94d2 (product fix 057a5579e538fcacc15feb8a0ac7fa580b3decb4)
Branch: claude/pr-handoff-stop-hookmain
Sync / merge-tree: behind 0 / ahead 9; git merge-tree --write-tree origin/main <tip> clean (exit 0). No sync needed. Auto-merge not armed.

Fixed

  • Bugbot high — cross-session prune disarm: removed age-based sibling claude-pr-handoff-* prune from post-mode (every Bash PostToolUse was able to delete another session’s day-old marker when that session only used Read/Edit and never refreshed mtime).
  • Marker write fail-open: post-mode emits handoff additionalContext only after a successful marker write ([ -f "$marker" ]); failed writes no longer claim tools are denied.
  • Review-bot shell loop: pre-mode now denies gh pr comment / gh pr review (AGENTS.md loop), with tests.
  • Devin ledger thread: superseded stacked Run PR rows that pointed at unresolvable HEADs (fff524d7 / f67e5c91 / 3403126b) via ledger:append --supersede to resolvable product tip 057a5579. Thread replied + resolved.

Dispositioned (no code)

  • Security review: no P0/P1 (path injection / JSON deny / unlock prefix already controlled).
  • Bugbot medium — incomplete gh blocklist beyond comment/review (e.g. gh workflow view, raw curl): residual policy incompleteness; substring deny is best-effort by design.
  • Prior Devin/CodeRabbit findings on this PR were already fixed on earlier heads and threads were already resolved.

Threads

All review threads resolved (0 unresolved).

Required CI

Re-triggered by this push. Prior tip 42ae4f37 had required CI still pending/queued (Actions slow / not settled) — not treated as green. Missing checks while dirty/pending ≠ green. Will not babysit the new run.

Local gates (decisive lines)

  • npx vitest run tests/pr-handoff-stop.test.tsTest Files 1 passed (1) / Tests 11 passed (11)
  • npm run verify:cheapTest Files 516 passed (516) / Tests 5457 passed | 1 skipped (5458) (exit 0)
  • npm run verify:pr-local → same unit suite pass + Offline RAG fixture and manifest validation passed (36 golden cases, 23 suites). + All matched files use Prettier code style! (exit 0)
  • bash -n .claude/hooks/pr-handoff-stop.sh → clean

Residual risks

  • Hook enforcement remains best-effort (substring deny; agents can still reach GitHub outside listed tools/commands).
  • Markers are no longer pruned across sessions (tiny files under the git dir / worktree lifetime).
  • No RAG / clinical / UI / provider surfaces touched.
  • Merge left to you.

@BigSimmo
BigSimmo enabled auto-merge (squash) August 6, 2026 17:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

cursoragent and others added 3 commits August 6, 2026 20:18
Prior CI run cancelled/timed out while queued during the GitHub
Actions major outage. Empty commit to re-fire checks on current tip.

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
@BigSimmo
BigSimmo merged commit 76169eb into main Aug 7, 2026
24 checks passed
@BigSimmo
BigSimmo deleted the claude/pr-handoff-stop-hook branch August 7, 2026 00:15
cursor Bot pushed a commit that referenced this pull request Aug 7, 2026
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request Aug 7, 2026
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request Aug 7, 2026
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request Aug 7, 2026
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request Aug 7, 2026
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request Aug 7, 2026
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request Aug 7, 2026
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request Aug 7, 2026
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request Aug 7, 2026
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request Aug 7, 2026
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
cursor Bot pushed a commit that referenced this pull request Aug 7, 2026
CodeRabbit asked that the PR #1649 late ledger checks cell list the six
named repo scripts and explicitly record incomplete handoff coverage
(verify:pr-local / verify:ui not run; no provider-backed checks) instead
of the opaque "6 repo gate scripts green" summary.

Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
BigSimmo added a commit that referenced this pull request Aug 7, 2026
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.

3 participants