Skip to content

fix(hooks): guard-governed-enqueue reads one slug, and it strips the .git a clone URL carries - #18390

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-18366-governed-enqueue-slug-git-suffix
Sep 16, 2026
Merged

os-zhuang merged 2 commits into
mainfrom
claude/issue-18366-governed-enqueue-slug-git-suffix

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

Fixes #18366

guard-governed-enqueue.sh read a checkout's origin with one sed expression written out
TWICE — once in slug_of, once inlined in the bare gh pr merge N target derivation. The path
character class owns . and is greedy, so the optional \(\.git\) group matched EMPTY and an
origin of https://github.com/objectstack-ai/cloud.git read back as the slug
objectstack-ai/cloud.git, which equals no owner/repo this guard is ever asked about. Measured
on 8b81ab60 before the fix, both halves reproduce:

  • a sibling checkout cloned with the URL git clone hands out resolved NOTHING, so the guard
    never audited it — the same fixture answers block on a bare-URL sibling and allow on a
    .git one;
  • a bare gh pr merge N (no -R) in such a clone derived a slug the API answers 404 for, and
    this guard's read-failure branch ALLOWS. On any conventionally cloned checkout that spelling
    was unguarded.

维护者速读(草稿)

改了什么 — 把 .claude/hooks/guard-governed-enqueue.sh 里重复两遍的 origin 读法收成一个
slug_of 函数(两处调用它),并让它在匹配之后剥掉结尾的 .git;自测文件新增两组用例:四种
origin 拼写(https/ssh × 有无 .git,外加结尾斜杠)的 sibling 都能被解析并被审计,以及在一个
origin 带 .git 的 checkout 里执行不带 -Rgh pr merge N 时,守卫推导出的仓名是 API 认得
的那个。除这两个文件外没有别的改动。

为什么改 — 这是一个会「放行」的守卫缺陷:git clone 默认给出的 URL 就带 .git,而守卫用
错的仓名去读 PR 会拿到 404,它的设计是读不到就放行(硬约束线在 merge queue 那一侧)。也就是说,
在任何按常规方式克隆的 checkout 上,gh pr merge N 这个写法此前是不设防的;同时带 .git
sibling 永远解析不到,受管面的判定会落在错误的树上(或没有树)。

风险与代价(含回滚) — 风险低:改动只在两个 hook 脚本里,不影响任何发布产物(.claude/**
不发布,因此打 skip-changeset)。新读法对旧的 bare 拼写字节等价,只多剥一个后缀;GitHub 本身
拒绝以 .git 结尾的仓库名,所以不会误剥合法仓名。代价是守卫会开始拦截此前被放行的
gh pr merge N —— 这正是它应该做的。回滚 = revert 本 PR 的两个 commit,守卫退回今天的行为,
merge queue 侧的硬线不受影响。

席位意见 — (留空,待席位定稿)

你要做的 — 这是规则层(.claude/hooks/**)受管面改动:PR 保持 draft,⛔ 不由任何 agent 席位
合并、入队或 arm auto-merge。需要维护者(或授权账号)的一次 APPROVED,之后由拥有它的席位落地。

The fix

One reader, two call sites, and the suffix comes off AFTER the match rather than out of the
character class — the class still has to own . (objectstack.ai is a legitimate repository
name), and which of the two wins the class-vs-group race is exactly the kind of thing GNU and
BSD sed are free to settle differently. A trailing .git is removed by the shell either way,
and nothing legitimate is lost with it: GitHub refuses a repository name that ends in .git.

The self-test pins both shapes the card asks for. The .git sibling rows are asserted as
AGREEMENT with the register (the same $sibling_want the neighbouring case derives), never as a
copied verdict. The bare gh pr merge N case runs a copy of the hook from a checkout it builds
itself, because the hook derives its repo root from its own path.

Two things that case does NOT do, both deliberate and both measured rather than assumed:

  • it asserts no allow/block verdict. OS_GOVERNED_ENQUEUE_FIXTURE answers EVERY path, so the 404
    that makes the real guard fail open cannot be reproduced without the network — under the
    fixture a wrong slug blocks exactly like a right one, and a verdict row would pass in both
    worlds. The discriminator is the slug itself, which the refusal prints as OWNER/REPO#NUMBER.
  • the two git@github.com: rows unset GIT_CONFIG_COUNT / GIT_CONFIG_PARAMETERS. This
    container injects url.https://github.com/.insteadOf git@github.com: through the environment
    and git remote get-url HONOURS it, so without the unset git hands the hook https://... and
    both ssh rows are silent duplicates of the https ones. On a machine that injects nothing,
    env -u on an unset name is a no-op.

Evidence

Self-test, bash .claude/hooks/guard-governed-enqueue.selftest.sh:

run result
baseline on origin/main 8b81ab60 (before and after pnpm install) 56 passed, 0 failed
this branch 62 passed, 0 failed

Firing controls (each: mutate, prove the mutation reached the disk by blob hash, run, restore with
git checkout HEAD -- path, prove the restore by blob hash equality plus an empty git diff HEAD):

control effect result
the pre-fix slug reader restored (old sed, no suffix strip) 57 passed, 5 failed both gh pr merge N rows red, and the three .git spelling rows red
: dropped from the [:/] separator class 60 passed, 2 failed exactly the two git@github.com: rows red

Every one of the six new rows is red under one of the two, so none of them is a phantom.

Gates — node scripts/pm/dispatch-gates.mjs --commands derived 13 families for these two paths;
all 13 ran, all exit 0, and --ran reconciles 13 derived, 13 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 13 recorded an exit code and none of them is 3).
pnpm --filter @objectstack/lint run check:doc-formula-expressions first exited 3 (PREREQUISITE
NOT MET — unbuilt @objectstack/formula / @objectstack/lint, nothing measured); it exits 0
after turbo run build for those two packages.

Also run: bash -n on both files (0, 0); the self-test under all three
OS_GOVERNED_ENQUEUE_SIBLING_ROOT states — unset, an empty directory, a directory that does not
exist — 62 passed, 0 failed each; node scripts/pm/check-governed-merges.mjs --test .claude/hooks/guard-governed-enqueue.sh exits 3 (GOVERNED, as expected for this surface);
pnpm check:pm-settings-deny-roster exits 0 (its roster lives under .claude, so its silence in
the derivation is not evidence in either direction); grep -naP for control bytes over both files
finds none. There is no shellcheck in this tree: NOT MEASURED, with bash -n plus the 62-case
matrix as what stands in for it.

Acceptance notes


Generated by Claude Code

…`.git` a clone URL carries

`slug_of` and the inlined copy in the bare `gh pr merge <n>` target derivation
ran the same sed, and the path character class owns `.` and is greedy, so the
optional `\(\.git\)` group matched EMPTY: an origin of
`https://github.com/objectstack-ai/cloud.git` read back as the slug
`objectstack-ai/cloud.git`, which equals no `owner/repo` this guard is ever
asked about. A sibling cloned with the URL `git clone` hands out therefore
resolved nothing and was never audited, and a bare `gh pr merge <n>` in such a
clone derived a slug the API answers 404 for — this guard's fail-open branch.

One reader now, called from both sites, and the suffix comes off after the
match rather than out of the character class, which still has to own `.`
(`objectstack.ai` is a legitimate repository name) and whose race with the
group GNU and BSD sed are free to settle differently.

The self-test pins both shapes: four unpinned spellings of the sibling's origin
(https/ssh x with and without `.git`, plus a trailing slash) resolve and are
audited, and a bare `gh pr merge <n>` run from a checkout whose origin carries
the suffix derives `#13794`. 56 passed / 0 failed
before, 62 / 0 after.

Claude-Session: https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt
Co-authored-by: Claude <noreply@anthropic.com>
…`git@github.com:` to https

`git remote get-url` honours `url.<base>.insteadOf`, and this container injects
one through `GIT_CONFIG_COUNT`/`GIT_CONFIG_KEY_n`, so the two `git@github.com:`
rows were handed `https://github.com/…` and were second copies of the https
rows — green over a branch of the reader they never reached. Measured, not
assumed: with the injection unset the same remote reads back as the ssh URL.

Claude-Session: https://claude.ai/code/session_01Bz6hxDBqK62NP2W1LATvnt
Co-authored-by: Claude <noreply@anthropic.com>
@os-elon-musk os-elon-musk added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 16, 2026 — with Claude

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1696da61120962477c79ca917d28cb422331fda2

① Derived judgments

  • Accept set / public surface: none — two hook scripts under .claude/hooks/**, published by no package; no contract, export, key or error code moves. The guard's behaviour moves in the safe direction only: a .git-suffixed origin now resolves (a sibling is audited; a bare gh pr merge NUMBER reads the right repository instead of 404-ing open).
  • Diff read whole by this seat: slug_of defined once beside the Bash pass and called at both sites; ${slug%.git} applied after the match, the dead optional group removed; the selftest gains the four-spelling loop (AGREEMENT with the register through the neighbouring case's $sibling_want) and the bare gh pr merge NUMBER slug-text rows run on a built checkout with the hook symlinked in.
  • Re-run by this seat at 2026-09-16T08:19Z: the selftest on this head in a detached worktree with no node_modules — 62 passed, 0 failed, exit 0; on origin/main 8b81ab6056 passed, 0 failed; bash -n on both files exit 0; check-clause2-carriers.mjs --pair 18390 exit 0; path face GOVERNED (.claude/** ×2).

② Semver level

skip-changeset — nothing published moves (.claude/** ships in no package).

③ Boundary flags

Implemented-by: claude/issue-18366-governed-enqueue-slug-git-suffix
Reviewed-by: session_01Bz6hxDBqK62NP2W1LATvnt

VERDICT: PASS

维护者速读(终稿)

改了什么.claude/hooks/guard-governed-enqueue.sh 里两处重复的 origin 读法收成一个 slug_of 函数,匹配后剥掉结尾的 .git;自测新增两组用例:四种 origin 拼写(https / ssh × 有无 .git,外加结尾斜杠)的 sibling 都能被解析并审计,以及在 origin 带 .git 的 checkout 里执行不带 -Rgh pr merge N 时,守卫推导出的仓名是 API 认得的那个。只动这两个文件。

为什么改 — 这是一个会「放行」的守卫缺陷:git clone 默认给出的 URL 就带 .git,守卫用错仓名读 PR 得到 404,而它读不到就放行(硬约束线在 merge queue 那一侧)。也就是说在常规克隆的 checkout 上,gh pr merge N 此前不设防;带 .git 的 sibling 也永远解析不到。

风险与代价(含回滚) — 低:两个 hook 脚本,不进任何发布产物(skip-changeset)。对旧的 bare 拼写字节等价,只多剥一个后缀;GitHub 拒绝以 .git 结尾的仓名,不会误剥。代价是守卫开始拦截此前被放行的 gh pr merge N,这正是它该做的。回滚 = revert 两个 commit,merge queue 侧硬线不受影响。

席位意见 — 席位在契约复审档复核 PASS;自测在本席容器复跑 62 / 0(main 56 / 0);两条申报的偏差都有实测理由。建议批准。这是同文件串行链 #18366#18367#18384 的第一棒,批得早后两张才能开工。

你要做的 — 在本 PR 上给一次 APPROVED(os-zhuang 或 hotlong 任一)。之后由本席按裁决 C 落地(清标、ready、auto-merge SQUASH);⛔ 席位不自行批。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 16, 2026 09:14
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 16, 2026
Merged via the queue into main with commit 40eaf75 Sep 16, 2026
39 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-18366-governed-enqueue-slug-git-suffix branch September 16, 2026 09:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

3 participants