Skip to main content

external-pr-trust

Authorship as a precondition of merging — bot vs stranger classification, what a merge executes, the merge-guard hook. Use when bulk-merging PRs, or when a merge is denied for a non-self author.

来源信息

仓库
laurigates/claude-plugins
最近来源活动
2026年9月24日 20:02
检测到的 SKILL.md 语言
英语
星标
59
分支
6

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
external-pr-trust
description
Authorship as a precondition of merging — bot vs stranger classification, what a merge executes, the merge-guard hook. Use when bulk-merging PRs, or when a merge is denied for a non-self author.
allowed-tools
Read, Grep, Glob, Bash(gh pr view *), Bash(gh pr list *), Bash(gh pr diff *), Bash(gh api *), Bash(gh search *), TodoWrite
created
2026-09-24T00:00:00.000Z
modified
2026-09-24T00:00:00.000Z
reviewed
2026-09-24T00:00:00.000Z
# External-Contributor PRs — Authorship Is a Precondition of Merging Promoted from the always-loaded `external-contributor-prs.md` portfolio rule, whose stub keeps the gate lines. Public repos accept PRs from anyone. Merge tooling, though, is usually built for bulk-merging **your own and bots'** PRs: a picker such as `ghsq` → `^a` (select all) → `a` (all remaining) lands a queue of release-please and feature PRs in one pass. A stranger's PR riding that reflex is merged code nobody read. > **The law:** before merging, know **who wrote it**. `author.login == you` or a > bot is the fast path. Anything else is a review, not a merge. ## When to Use This Skill | Use this skill when... | Use something else when... | |---|---| | Clearing a queue of PRs in one pass | Checking whether a merge did what you think → `git-merge-hazards` | | The external-PR merge guard denied a `gh pr merge` | Opening a PR against someone else's repo → `upstream-pr-contribution` | | A PR adds a GitHub Action, hook, or dependency | Writing CI that runs on fork PRs → `github-actions-plugin:claude-code-github-workflows` | ## Why it is not merely "review your PRs" A PR is not inert text — merging can *execute* code: | Path touched | What merging does | |---|---| | `.github/workflows/**`, `.github/actions/**` | Runs in CI, with whatever `permissions:` the workflow grants itself | | `**/hooks/**`, `*.sh` | Runs on **your machine** (Claude Code hooks, git hooks, justfile recipes) | | `.claude/**` | Changes agent permissions, rules, or settings | | `package.json`, `pyproject.toml`, `mise.toml`, `.pre-commit-config.yaml` | Changes what gets installed and run | Two things make a third-party CI addition worse than it looks — check them explicitly on any PR that adds an action: - **A floating tag is mutable.** `uses: owner/action@v0` resolves at dispatch time, so the publisher (or whoever takes over their account) can retarget the tag after you merge and change what runs, with no diff on your side. - **`runs.using` does not tell you what it does.** A `node20` or `composite` action can download and execute a remote binary — and often does: the canonical case (claude-plugins#2231) was a `node24` action whose entire job was to resolve the `latest` release of a *different* repo at run time, fetch a native binary from it, `chmod 0755`, and execute it. Pinning the action to a SHA does not pin that second fetch. Read the action's `src`/`action.yml` at the pinned SHA rather than trusting its declared type. ## Classify the author correctly — bots render three different ways Getting this wrong in either direction breaks the guard: flag the bots and it becomes noise you train yourself to click through; miss a stranger and it does nothing. The trap is that a bot author looks different depending on which `gh` subcommand produced it: | Source | `login` | `is_bot` | `type` | |---|---|---|---| | `gh pr list` / `gh pr view` | `app/laurigates-release-please` | `true` | — | | `gh search prs` | `laurigates-release-please[bot]` | **`false`** | `Bot` | So `is_bot` **alone silently misclassifies every App bot in search results as a stranger**. Accept any of: `is_bot == true`, `type == "Bot"`, a `*[bot]` login, or an `app/*` login. (`authorAssociation` is *not* a field on `gh pr list`/`gh pr view --json` — read it from `gh api repos/O/R/pulls/N --jq .author_association`.) ## The two guards | Actor | Mechanism | Behavior | |---|---|---| | **The human**, in the shell | `_gh_trust_gate` / `_gh_confirm_external` in the dotfiles `dot_zshrc.tmpl`, used by the `ghsq` / `ghrb` / `ghrp` pickers | External PRs are **hidden from the picker** (a stderr banner counts them; `ghsq -x` includes them, marked `⚠` white-on-red). Merging one prints author + association + which touched paths execute, is **never** swept up by `a`=all, and requires typing `merge` — not a keypress. | | **Claude**, in any repo | `hooks-plugin/hooks/external-pr-merge-guard.sh` (PreToolUse) | **Denies** `gh pr merge`, `gh api .../pulls/N/merge`, and MCP `merge_pull_request` for a non-self, non-bot author. Also denies when authorship can't be established. | Both fail **closed**: if the author cannot be read, the merge is refused rather than allowed. "Cannot verify" is not "safe." ## What Claude does when the guard fires Do not look for another merge route — the hook covers `gh pr merge` in every flag order, the `gh api` path, and the MCP tool, and `--admin` does not bypass it. Instead: 1. **Report what the PR actually changes.** Read the diff. For an added action or dependency, say where the code comes from, whether the ref is a **commit SHA or a mutable tag**, and what it downloads or transmits at runtime. 2. **Hand the merge back.** `ghsq -x` (which will demand the typed confirmation), or `gh pr merge <n> --squash --delete-branch` run by the human. ## A bulk merge must issue one literal PR number per call The guard resolves the PR selector **structurally**, and an unresolvable `$var` selector lands in its "not determinable" row — which is denied, because "cannot verify" is not "safe". So the obvious way to clear a queue of bot PRs is refused before it starts, even though every PR in it is a bot's: ``` # Denied — the hook cannot tell whose PR $n is for n in 2133 2134 2135; do gh pr merge $n --squash --delete-branch; done ``` ``` # Allowed — each literal number is checked on its own gh pr merge 2133 --squash --delete-branch gh pr merge 2134 --squash --delete-branch ``` Wrapping the loop does not help: `xargs`, `find -exec`, `parallel`, `timeout`, `bash -c`, and command substitution are all resolved through to the `gh pr merge` inside them. This bites a **Renovate onboarding sweep** specifically, because the usual workflow there is bucket-then-bulk-act: categorize the PRs by check state, then merge the whole `NOCHECKS`/`PASS` bucket. Categorize in a loop all you like; emit the merges one literal call at a time. Read the buckets out first (`gh pr list --json number,author`) so the numbers are in hand as literals before the first merge. The constraint is a feature, not friction to route around: it is what stops a stranger's PR from riding the same reflex. Do not build a helper that hides the number from the hook — that is the bypass the guard exists to prevent. The denial names the fix — "resolve shell variables to their literal values first … then merge with literal values one at a time" — so the recovery costs one re-read of the error, not a diagnosis. Never prefix `CLAUDE_HOOKS_DISABLE_EXTERNAL_PR_MERGE=1` onto a command — it is only honored from the operator's own shell environment, precisely so an agent cannot self-serve the bypass (`handling-blocked-hooks.md`). ## Evidence `claude-plugins#1222` (2026-05, a `FIRST_TIME_CONTRIBUTOR`) was merged with **no review and no comments**. It rewrote a Claude Code Stop hook — code that runs locally — and shipped a broken TODO scan that had to be repaired three weeks later in #1229. It also flipped the hook's file mode `100755 → 100644`, which is still un-restored. One drive-by merge, two defects, neither noticed at merge time. That is the failure mode both guards exist to stop. ## Related - `git-plugin:git-merge-hazards` — the *mechanics* of merging (squash-merge detection, stacked-PR auto-close, `UNSTABLE` vs `BLOCKED`). This skill is the *trust* axis: that one asks "did the merge do what I think?", this one asks "should I be merging this at all?" - `git-plugin:upstream-pr-contribution` — the mirror image: the gates *others* impose on PRs you send them - `hooks-plugin/README.md` § external-pr-merge-guard.sh — the hook's own documentation
在 GitHub 查看