fix(hooks): deny PR-activity subscribe, allow unsubscribe without a prompt - #497
Conversation
…rompt AGENTS.md forbids babysitting a PR, and `land` now refuses to land one whose webhook subscription is still armed (CLOUD-518's gate). But the harness arms a subscription on every PR this repo opens, and dropping it needed a human approval on every single PR — so the enforced path was also the prompting path, which is how a gate gets worked around instead of satisfied. Two permission rules make the posture the default rather than a per-PR negotiation: - deny `subscribe_pr_activity`, so the arming this repo does not want is refused rather than approved by reflex. - allow `unsubscribe_pr_activity`, so dropping one is always available without a prompt. Nothing is lost by allowing it: unsubscribing only ever narrows what reaches the session. Both are spelled for the GitHub and Claude Code Remote servers, because the same connector is exposed under more than one name and a rule matching only one spelling gates nothing (CLOUD-178's measured instability, the same reason `issue-read-guard`'s matcher is suffix-anchored). This does not close CLOUD-673 — a task still cannot authenticate to the session's own MCP endpoint, so `land` cannot unsubscribe on the agent's behalf; the drop stays a tool call the agent makes and records with `mise run pr-unsubscribed record`. Refs: CLOUD-518, CLOUD-651
CLOUD-518 Every PR this repo opens is auto-subscribed to webhook wakes, against AGENTS.md's own ban — and the envelopes render to the human as raw XML
Why AGENTS.md forbids PR-webhook babysitting twice — Two halves, measured 2026-08-13. 1. The subscription arms itself. The remote-execution harness prompt carries a standing instruction — "After creating a PR in a session, immediately call 2. The envelopes render to the human as raw XML. Reported by the repo owner mid-session: pages of
Hooks were suspected and are excluded, tested rather than argued. Exactly four Why it is worth a row rather than a shrug. The rendering half is Anthropic's and cannot be fixed here. The subscription half is ours and is a live contract violation: every PR this repo opens starts a webhook stream that AGENTS.md says must not exist, and the only remedy today is an agent remembering to call Refinement — Ready
Acceptance
Not in this issueWhether webhook-driven work is ever right. AGENTS.md says no for this repo and that is not relitigated here; this only closes the gap between the rule and its enforcement. CLOUD-673 A task cannot authenticate to the session's own MCP endpoint, so CLOUD-518's unsubscribe is unreachable from `land`
Why CLOUD-518 needs
The mechanism is visible in the environment rather than inferred: What survives from the CLOUD-518 measurement: the endpoint shape, the header names, that the surface is an Searching the container for the client's credential is deliberately not the fix. A task scraping the client's secrets would be worse than the defect it closes, even where it works. What this costs right nowPR #490 carries a complete, tested, correctly-failing implementation of CLOUD-518's mechanism, and on its own landing attempt it printed, twice per lap: So CLOUD-518's first acceptance criterion — "a PR opened by Refinement — Ready
Acceptance
Not in this issue
CLOUD-624 Evaluate token-normalized phrase matching for hook/log predicates
Why. A small class of predicates has to read assistant prose, and each one matches raw substrings. The failures witnessed so far are surface-form brittleness rather than policy error: the policy holds and the string does not. The two that carry weight are witnessed — a real turn is cited for each, on its own issue. What this issue has NOT got is a measurement, and the shape of that gap decides the work. Re-running the shipped task against those two sentences on 2026-08-18 reproduces both verdicts, and reproducing a cited miss is worth exactly one thing: confirming it is still live before building on it. It is not evidence about any proposed matcher. A pattern authored against known strings hits them by construction — the same circularity that makes the hand-authored literal set the defect in the first place. Any sentence typed at the task to see whether it fires is an invention, not a witness, and The question. Should Batten add a deterministic parsing/normalization pass for the prose half of these predicates — a token-aware pipeline built on tokenizers or any equivalent local tokenizer/parser — so a rule matches over a normalized token stream or a small token-pattern grammar instead of raw substrings? This is not a request for learned NLP, embeddings, or model judgement. The line from CLOUD-95 stays: bounded, deterministic, structural, explainable. The prose side may nominate a candidate; the verdict must still come from a typed join. One constraint is settled and is not re-litigated by the evaluation. Deterministic segmentation, lemmatization and finite-state morphology estimate nothing, so they are admissible; naming one crate does not make the category learned. A token pattern over named classes is more explainable than a regex alternation — What to evaluateFour blocks. Each carries the questions its measurement has to answer. 1. Normalization and token-pattern matching. Sentence segmentation, token boundaries, punctuation normalization, quote/code/fence stripping, inflection-safe matching. Compare today's literal alternations against token-aware patterns that express small equivalence classes —
2. Corpus-derived candidate generation. Mine the durable turn/message corpus rather than hand-authoring a phrase list. An n-gram hit may nominate a candidate rule shape; the typed join still decides.
3. Negation.
4. Substrate and join. Keep CLOUD-326's boundary: prose narrows candidates, typed facts decide. Evaluate against the existing join shapes — candidate punt / deferral phrasing, no durable write in the same turn, no owning issue named in the relevant scope.
The matcher half is not split off, and the reason is a predicateThe matcher half looks separable: the witnessed misses exist today, so a token pattern could be authored and shown to fire with no corpus at all. It is not separable, because a matcher authored against known strings hits those strings by construction — recall over them measures nothing, which is the same circularity that makes a hand-authored literal set the defect this issue exists to fix. What decides the matcher question is the false-positive class a widened pattern opens, and CLOUD-252 and CLOUD-323 are where broader shapes actually died. That measurement needs negative examples in the register the predicate reads, and this repository's durable artifacts are not that register. The one corpus run anyone has done here is the 2026-08-17 session's, recorded in A second reason that null result settles nothing, and it is independent of the register. The predicate is a conjunction — the phrasing AND no durable write in the same turn. A durable artifact contains no instance of "nothing durable was written", by construction, so a zero on that conjunct is guaranteed before anyone runs anything and discovers exactly nothing. The literal's own false-positive rate does not depend on that conjunct at all, which is why the run tells you nothing about the half it was supposed to test. Where a measurement's construction guarantees its own zero, the finding is about the measurement. Both halves therefore wait on the same corpus, and splitting would buy an unmeasurable verdict. Existing cases this should be measured against
Non-goals
Acceptance
RelatedRefinement — Ready (a matcher verdict measured on the witnessed misses, with the typed join still the decider) Refinement gate: Definition of Ready & Done. Evaluate-and-decide spike: §3 (no new command) and §4 (nothing generated) do not apply. §2 is discharged by naming the pilot predicate a positive verdict authorizes, not by shipping one.
Verdict, 2026-08-18: reject, pending corpusNot a rejection on the merits. The matcher question is real, and the substrate objections raised against it were withdrawn on evidence — The grounds are three measurements this issue already carries.
Why now rather than waiting. CLOUD-671 was rescoped on 2026-08-18 into The re-open predicate. A command and an exit code, not a judgement, and grounded in this repo's own prior measurements rather than an invented number:
Checkable today only in part, stated as such rather than dressed up: Successor. CLOUD-677, on the Lands as. The §1 Lands as clause above is what makes this verdict landable rather than tracker-only: the measured result, and the two arguments withdrawn on evidence, go into |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 3 included reviews per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe Claude settings update allows GitHub and Claude Remote PR activity unsubscription tools. It denies the corresponding subscription tools. ChangesClaude PR activity permissions
Estimated code review effort: 1 (Trivial) | ~2 minutes Merge Risk: ⚪ Minimal · up to This PR narrows PR-activity permissions by denying new subscriptions while allowing unsubscriptions without prompting. The change is localized and no actionable merge-blocking risk remains after normal checks and review. Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsThese MCP integrations need to be re-authenticated in the Integrations settings: Linear Comment |
|
|
/fast-forward |



DO-NOT-CLOSE CLOUD-518
Two permission rules in
.claude/settings.json. Nothing else changes.Why
AGENTS.md forbids babysitting a PR, and
landnow refuses to land one whosewebhook subscription is still armed. But the harness arms a subscription on
every PR this repo opens, and dropping it required a human approval on every
single PR — so the enforced path was also the prompting path. A gate whose
remedy needs a person each time is a gate that gets worked around rather than
satisfied, which is exactly what happened on the PR before this one.
What
subscribe_pr_activity— the arming this repo does not want isrefused rather than approved by reflex.
unsubscribe_pr_activity— dropping one is always available with noprompt. Nothing is risked by allowing it: unsubscribing only ever narrows what
reaches the session.
Both are spelled for the GitHub and the Claude Code Remote servers. The same
connector is exposed under more than one name, and a rule matching one spelling
gates nothing — the measured instability behind CLOUD-178, and the same reason
issue-read-guard's matcher is suffix-anchored.Why this does not complete CLOUD-518
That issue has two halves. This is the subscription half. The other half — the
<wake reason="external-event">envelopes rendering to the human as raw XML —is untouched, so the marker above declines the close rather than moving the
board on a half-done issue.
It also does not close CLOUD-673: a task still cannot authenticate to the
session's own MCP endpoint, so
landcannot drop a subscription on the agent'sbehalf. The drop stays a tool call the agent makes and records with
mise run pr-unsubscribed record. What changes is that the call no longerstops for a person.
Verification
jq -e '.permissions.deny | index("mcp__github__subscribe_pr_activity")'andthe matching allow index both resolve — valid JSON, rules present.
mcp__github__subscribe_pr_activitybecameunavailable in this session, so the deny is in force rather than merely
written.
mcp-allow-check("every allow rule namesa tool it can match, and every enabled server has a grant"),
rules-drift,hooks-wiring-check,hook-pin-checkandmcp-timeout-budget.Summary by CodeRabbit