Repository navigation
feat: require a session claim before commit/push on a PR branch (#4155) - #4156
Conversation
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#4155 Add hooks/no-pr-work-without-claim.py (PreToolUse, Bash). On a branch with an open Morrison-Lab PR it denies `git commit` / `git push` unless a claim comment (agent marker plus hold-off wording) names this session by session id or worktree path, and denies when another session's claim is newer. Before a push it warns on pushes since the claim that local history lacks, read from the forge activity endpoint. Fails open with a visible warning on any gh/network failure. Escape hatch: ALLOW_UNCLAIMED_PR_WORK=1. Adds 37 cases in hooks/test-no-pr-work-without-claim.py (8 of 8 mutants killed), registers the hook in hooks/hooks.json and the generated plugin mirror, documents it in README.md, and adds a Do/Don't pair to shared/workflow/claim-pr.md citing #4155 and PR #4134. Local half only: hooks are inert in remote/web sessions (#2004). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Resolve the directory a command runs in from earlier `cd`s and `git -C`; an unresolvable move (`cd -`, `$VAR`, `--git-dir`) is a visible fail-open. - Match a worktree path or session id only as a whole token, so a nested worktree or a longer id is not read as this session's claim. - Decode `gh api --paginate` pages one at a time instead of rewriting `][`, which corrupted comment bodies containing `] [`; read the activity endpoint as a single page. - Give the activity read its own try so a 403/404 keeps the claim outcomes. - Dry-run grammar: push `-n`, last occurrence wins for `--no-dry-run`. - A claim needs `working on this` plus hold-off wording and must not be an unclaim; a standing peer claim gets stand-down advice instead of "post a claim". - Document the known limits (claim age, release comments, fork PRs, cost). - 56 cases now; 20 of 20 mutants killed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Accept every claim wording skills/ emits: marker plus `hold off` or `paws off`, minus an unclaim and a negated "no need to hold off". Drop the `working on this` requirement that rejected the ardi, handoff and review-only wordings. - Add the `Session worktree:` line to the skills/claim-pr PR template, the one the hook matches on; the other emitters are tracked in #4160. - Worktree match is bounded on both sides: a sentence-final period is fine, an extension of the path (either end) is not. - Judge a push by its refspec's destination branch, skip tag pushes. - URL-encode the branch in the pulls and activity queries; accept a remote URL with a trailing slash. - Cap the activity SHA checks and put every subprocess under one 20s budget (PR_CLAIM_TOTAL_BUDGET) below the 30s hook timeout. - Tests log each requested forge path and assert the head= and ref= queries; 69 cases, every mutant added this round killed by one targeted case. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- A claim that names no session now WARNS instead of denying, so the claim emitters that predate the session line (#4160) keep working; only no claim, or claims naming a different session, deny. Deny text no longer implies a lapse/release check the hook does not make. - Read a Git Bash `/c/x` path as `C:/x` on both sides of the worktree match; the claim-pr template asks for `git rev-parse --show-toplevel`. - Scan commands nested in a shell's `-c`: unlocatable, so a visible fail-open rather than a silent bypass. - Skip option values (`-m`, `-am msg`, ...) when reading `--dry-run`; stop treating `--signed` / `--recurse-submodules` as value-taking. - Skip pushes the lookup cannot judge: colon-refspec deletes, `--tags` / `--all` / `--mirror`, and non-origin remotes. - Document the shared-checkout, unfetched-commit and shallow-clone limits. - 80 cases; real-commit ancestry cases replace the made-up SHA; each mutant added this round is killed by a targeted case. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- A shell-built push destination (`$BRANCH`, `$(...)`) falls back to the current branch instead of being looked up as a literal name, which found no PR and passed silently. - The push activity warning now also runs when the only claim names no session (it uses that claim's time as the baseline). - Apply claim-pr's 2-hour rule to a PEER claim through the PR's updated_at: a peer claim on a PR idle that long warns instead of denying. - Recognize "releasing my claim" style releases as non-claims. - A broken shellcmd import is a visible fail-open on a git command, not a silent no-op; accept `github.com-<alias>` SSH remotes. - skills/claim-pr/SKILL.md no longer says the hook denies a session-less claim (it warns). - 88 cases; each mutant added this round killed by a targeted case. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Use the canonical release terms (unclaim, released, PR is free, now mergeable), accept a worktree path with a trailing slash, state the subagent-worktree and busy-branch limits accurately, and reword the prose that said the hook denies a session-less claim. 94 cases; four new mutants killed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Strip the Session worktree / Session id lines before reading a claim for release terms (a worktree named after an issue slug like fix-unclaim-wording made a correct claim fail), accept the back-off wording claim-pr.md lists, and treat a missing cwd as a quiet pass. 102 cases; wrapped git invocations (/usr/bin/git, timeout, command) are now pinned by tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A bare 'released' is no longer a release term (a claim saying a fix 'can be released' was dropped and its session denied); only 'paws off released', 'claim released', 'released my claim', 'PR is free', 'now mergeable', 'unclaim' and 'releasing my claim' are. Name the reads a cloud session should run in claim-pr.md. 104 cases; three new mutants killed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 04:52 UTC |
|
Claude Code CLI (local session, Posted by Claude Code (AI agent) --- not written by a human. |
|
Claude finished review — View run Fully-clean instrument readingPer the Stop hook's requirement, I ran the authoritative instrument rather than resting the verdict on Output (exit 1, Both reasons are this review's own in-flight sibling run: the VerdictReady for merge — judged on the diff's content; the only non-clean signal from the fully-clean instrument is this review's own in-flight completion, not a defect in the change. Structured Review Data (JSON){
"schema_version": "1.1",
"reviewer": "claude",
"commit_sha": "82ee03a05c4a3da3c04bc6d66734ab224b010b8d",
"verdict": "CLEAN",
"findings": [],
"detailed_assessment": "Traced hooks/no-pr-work-without-claim.py's claim-matching, argv-parsing, and fail-open paths against its 104-case test suite, which passes in full with no uncovered failure mode found.",
"holistic_assessment": "Registration is consistent across both hooks.json files, referenced issues exist, the claim-pr skill template matches what the hook parses, and no CI check has failed."
}Stopping Point: Clean stopping point reached --- session done; review posted with no blocking findings; no follow-up items pending. Reviewed commit: 82ee03a |
Closes #4155
What
Local half of #4155: a
PreToolUsehook on Bash that makes a session claim a PR branch before it commits or pushes to it.The incident was two sessions on #4132, #4134 and #4138 with no claim comment from either (PR #4134 lost 471 lines of
memories/preferences.md).hooks/no-pr-work-without-claim.pybinds toPreToolUse(Bash) and fires ongit commitandgit pushonly (argv-split viascripts/lib/shellcmd.py, so quoted or heredoc mentions are silent;--dry-run, branch deletes, tag and ref-set pushes are skipped).When the target branch has an open PR in a Morrison-Lab repo it reads the PR's comments:
claim-prtoday, #4160)A claim is a comment with the agent marker plus
hold off/paws offwording; a session is named by aSession worktree:orSession id:line.Before a push it also reads
repos/<o>/<r>/activity?ref=refs/heads/<branch>and warns (never denies) about pushes since the claim whose commit is not in local history.Every network or
ghfailure, an unresolvablecd, a commit insidebash -c, and a spent 20 s budget are visible fail-opens (stderr plusadditionalContext).Escape hatch:
ALLOW_UNCLAIMED_PR_WORK=1(env prefix, leadingexport, or process env).Also: a
Session worktree:line in theskills/claim-prPR template, a Do/Don't pair inshared/workflow/claim-pr.mdciting #4155 and PR #4134, README rows, and thehooks/hooks.jsonentry with its generated mirror.Activation
Merging this PR activates the hook on the plugin path (the
hooks.jsonentry is the activation); nothing was registered locally withinstall-hooks.py --fix.On the non-plugin path,
post-mergestep 3.75 owns the registration.Not done here
ardi,handoff,gi,st,gip,pr-on-claim, ...) do not carry the session line yet: #4160.Verification
hooks/test-no-pr-work-without-claim.py: 104 cases against real temporary git repos with a fakegh(a fixture, so these show the hook's decisions given canned responses, not forge behaviour; the activity and pulls response shapes were checked once against the live API).adversarial-reviewer, sonnet) on the committed diff; findings addressed in the follow-up commits.The eighth round read
82ee03a0and returned CLEAN with four non-blocking notes: a session that claims in one checkout and then commits from another reads its own claim as a peer's,/mnt/c/and 8.3 path spellings do not match, the fragment's one-line summary omits the hold-off wording, and any claim containing the words "session id" counts as a peer's.82ee03a0usedALLOW_UNREVIEWED_PUSH=1: the pre-push guard looks for the verdict in theAgentcall's own result, and this harness returns a foreground reviewer's report as a message instead, so the guard could not see a verdict that exists.🤖 Generated with Claude Code