feat(hooks): stop a session following its own PR after handoff - #1649
Conversation
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>
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
This pull request has been ignored for the connected project Preview Branches by Supabase. |
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. 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 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe 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. ChangesPR handoff stop enforcement
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
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
commented
Aug 6, 2026
Addressed the remaining review notes in 5d96617 (beyond the two inline Devin threads):
Also merged |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
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>
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
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>
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
- 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>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
3403126 to
42ae4f3CompareUh oh!
There was an error while loading. Please reload this page.
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
commented
Aug 6, 2026
PR #1649 review-and-fix summaryTip: Fixed
Dispositioned (no code)
ThreadsAll review threads resolved (0 unresolved). Required CIRe-triggered by this push. Prior tip Local gates (decisive lines)
Residual risks
|
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>
Uh oh!
There was an error while loading. Please reload this page.
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
Co-authored-by: BigSimmo <BigSimmo@users.noreply.github.com>
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>
Summary
.claude/hooks/pr-handoff-stop.sh, a two-mode Claude Code hook that ends a session's involvement with its own pull request. ThePostToolUsehalf writes a session-scoped marker when a PR-creating call —gh pr createor anycreate_pull_requestMCP 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.PreToolUsehalf, 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 carrypull_request,workflow_run,workflow_job,check_run,check_suite,job_log, orupdate_branch, so a connector is not a way around the shell rule; and the loop machineryMonitor,ScheduleWakeup, andCronCreate, 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..claude/settings.jsonwith matchers narrow enough that ordinaryRead/Edit/Grepcalls never spawn the hook.## Stop when the pull request is opensection toAGENTS.mdstating the rule, its rationale, what the hook denies, and the two documented unlocks: prefix a shell command withCLAUDE_ALLOW_PR_FOLLOW=1, or delete the marker named in the deny message. Both are meant for an explicit user ask..claude/skills/handoff/SKILL.mdso 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:
Run PRsweeps,pr-ci-fixwork, and reviews of someone else's PR keep working exactly as before.(create|merge)_?pull_?request$), socreate_pull_requestandmerge_pull_requestare allowed whilecreate_pull_request_reviewis denied — replying to review bots on your own PR is part of the loop being stopped..claude/settings.jsonbinds Claude Code sessions only. Codex and Cursor agents get theAGENTS.mdprose and no hook enforcement.Verification
npm run verify:pr-localVerification not run:
npm run verify:pr-local— this branch is a fresh worktree with nonode_modules, and installing there while other worktrees hold test/build leases is the concurrent-npm cicorruption this repo has hit before. The change touches no TypeScript, no source, and no test file, solint,typecheck, and the unit suite have nothing to exercise. What was run instead, from the pushed worktree:node scripts/check-docs-links.mjs—docs link check passed: 1629 repo path references resolve.node scripts/ci-change-scope.mjs --self-test—CI change scope self-test passed.node scripts/check-gate-manifest.mjs—Gate-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.mjs—Branch review ledger guard passed: 609 live table records + 1206 archived …node scripts/check-outstanding-issues.mjs—Outstanding-issues guard passed: 253 rows (122 open, 131 archived) …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.classifyPullRequestFilesover the changed paths —clinicalRisk: false, operationalRisk: false, ragRanking: false, ui: false, so no governance preflight orRAG 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 theCLAUDE_ALLOW_PR_FOLLOW=1prefix. 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 differentsession_idpassed straight through. The jq-less fallback path produced the same JSON in both modes.npm run verify:uiwhen UI, routing, styling, browser behavior, reduced-motion, or forced-colors behavior changedUI verification not run: no UI, routing, styling, or browser behaviour is touched by this diff.
Risk and rollout
PreToolUsecall 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 aRun PRsweep 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.PreToolUse/PostToolUseblocks from.claude/settings.jsonto 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.Notes
claude/cloud-pr-loop-prevention-bc052b, but that branch was 110 commits behindmainand carried two unmerged commits from an earlier session (chore: complete sentry setup hardening,chore: complete sentry logging and environment hardening, ~389 lines acrosssrc/sentry.*.config.ts,src/lib/env.ts, andsrc/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 currentorigin/main. Those commits remain untouched on the original branch.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests