Skip to content

Gate the repository a gh pr create is actually for - #49

Merged
alexander-yevsyukov merged 18 commits into
masterfrom
gate-cross-repo-pr
Oct 19, 2026
Merged

alexander-yevsyukov merged 18 commits into
masterfrom
gate-cross-repo-pr

Conversation

@alexander-yevsyukov

@alexander-yevsyukov alexander-yevsyukov commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

The blind spot

pre-pr-gate.sh blocks gh pr create unless /pre-pr wrote a pre-pr.ok
sentinel for the current HEAD. It resolved which repository to gate from the
session's work tree only — the hook payload's cwd, mapped through
session-work-tree.sh — and never inspected the command for a -R/--repo
argument. So a session working in repository A that ran
gh pr create -R owner/B was asked for A's sentinel: an attestation of
checks that were never run against B, and that could not meaningfully be run
against B from A at all.

Observed

On 2026-10-16, a session whose work tree was a compiler worktree opened a
config pull request. The gate demanded

<compiler worktree>/.git/worktrees/<worktree>/pre-pr.ok

Producing that file would have attested compiler for a config pull request.
The session instead created its pull requests with
gh api -X POST repos/<slug>/pulls, a workaround that bypasses this gate for
every repository, the session's own included — the failure mode this change
is really about.

This is not a repeat of the pre-d0abf64 worktree bug: the cwd → work tree
mapping behaved exactly as intended and correctly named the session's own
worktree git dir.

The design chosen, and why

Two designs were on the table. (a) resolve the target slug to a local clone
and check its sentinel; (b) treat the session's sentinel as inapplicable
and let the command through with an unmistakable note. This PR implements (b),
justified in the script's header comment.

(a) looks stricter but is unsatisfiable in practice: /pre-pr only ever writes
$(git rev-parse --absolute-git-dir)/pre-pr.ok for the work tree it ran in,
so while the session is in A there is no way to produce B's sentinel. The gate
would block every cross-repo pull request, and the escape hatch agents already
know is gh api — which removes the gate everywhere, not just for B. Failing
open with a loud note keeps the gate's real job (the session's own repository)
intact and keeps agents on gh pr create.

The note is a non-blocking error (exit 1), the convention
publish-version-gate.sh already uses where it cannot gate: stderr from a hook
that exits 0 is never shown at all, while a non-zero, non-2 exit puts the
first stderr line in the transcript and still runs the command.

pre-PR checks NOT verified for SpineEventEngine/config: 'gh pr create' names a repository other than this session's (…/worktrees/loving-gauss-55c0c5), whose pre-PR sentinel attests nothing about it.
Nothing in this session built, checked or reviewed SpineEventEngine/config, and the pre-PR
checklist cannot be run for it from here. Run /pre-pr in a session working in
that repository, or state in the pull request that the checks were not run.

What changed

Eight review rounds went into this, almost all of them into the one path that
does not gate. Two rules came out of it, and they divide the work:

Detection is as wide as possible. The exemption is as narrow as possible.

Detection is one rule: a create is detected when it starts a segment of
the command as received, or of the text from any one line onward with shell
line continuations removed. What defeats detection is always what precedes the
create, because a blanket join of continuations glues that text onto it:

# note \            : \                  printf 'x\\'
gh pr \             gh pr \              gh pr create --title x
create --title x    create --title x

The shell runs a create in each: a \ ending a comment does not continue the
line, nor does one escaping another \. Yet read as received no line starts
with gh pr create, and read with every continuation removed the create is
swallowed by the comment, by :, or by the printf. Starting the join at each
line in turn needs no judgement about which line-endings continue. Before this,
each such composition made the hook exit 0, gating nothing at all.

Detection may overshoot — it only gates — but never undershoot. The narrow
reading belongs to the exemption, below.

The exemption requires the request to be, as received, one line that is one
plain gh pr create
for a repository of another name:

  • One line, one command. Anything more is gated — a second command, a subshell
    && (gh pr create …) the start-anchored match skips, a
    &> "$(gh pr create …)" whose substitution tr moves to another line. Text
    the splitting discards is text this hook never examined, and each kind of it
    proved able to hide a create that runs here. This also retires every
    continuation subtlety at once: a multi-line create is gated, not read across
    lines.
  • That line must carry nothing the shell would still change: $, a backtick,
    {, }, *, ?, [, ], ~, <, >, #, %, or a \. Each of
    those was a live finding — $EXTRA holding a second -R, -R{a,b} expanding
    to two options with the local one last, > -Rlog whose filename read as a
    target, --title a\|b leaving a fragment that parsed as a whole command.
  • Its words are read as gh pr create's grammar, not scanned for anything
    -R-shaped: each known flag's value is consumed, so
    --title '--repo=other/repo' is a title. A flag in neither the value-taking
    nor the boolean list is ambiguous and gates — a gh flag newer than this hook
    makes the gate stricter, never looser — as does a flag left without its value
    (--repo ""). The whole line is read before answering, so a repeated -R
    means the last one, as gh resolves it.
  • The target is compared by owner/repo alone, against every URL of
    every remote, fetch and push (gh falls back to the push URL, and identifies
    the repository by the last of several, so a remote fetching from a local
    mirror and pushing first to a backup and then to GitHub is this repository).
    A URL's query and fragment are dropped, since gh reads the path alone. Hosts are left
    out deliberately: gh canonicalizes them in ways a hook cannot replicate —
    ssh.github.com:443 → github.com, HostName aliases from the user's SSH
    config, www. — and every divergence would make this repository look
    foreign and buy an exemption. The cost, stated in the script: a same-named
    repository on another host is gated here rather than exempted.
  • Percent-encoding on either side withholds the exemption, since gh decodes
    URL paths and this hook does not.
  • No explicit target ⇒ no behaviour change, --head owner:branch included:
    the session's repository is gated exactly as today, same block messages,
    jq-absence block and cwd-resolution rules.

Where the hook cannot be certain, it gates. The practical cost is that a
cross-repository create which is multi-line, or carries expansion or encoding,
needs this repository's sentinel — the behaviour that predates this change.

Verification

scripts/tests/test_hooks.py grows twenty-two cases following the existing
PrePrGateTest conventions: every -R/--repo spelling plus a quoted value
(asserted to exit 1 both with and without a session sentinel, since it is
inapplicable either way); this repository named explicitly in four spellings
including HOST/OWNER/REPO; a fork head; echo "gh pr create -R other/repo"
still ignored; and one per review finding — a --repo inside a quoted title, a
flag-shaped option value, a commented-out target, an unreadable segment, a
local create beside a cross-repo one, a repeated -R, an unknown flag, a
continuation line (including one mid-word), a target on another host, and one
case per syntax the shell would still change — command and process
substitution, scalar and array expansion, brace expansion, an escaped
separator, redirection, and a flag left without its value, and a request
that is more than one create (second command, subshell, substitution moved by
the split, comment-swallowed create).

$ python3 -m unittest discover -s scripts/tests
...............................................................
----------------------------------------------------------------------
Ran 65 tests in 62.267s

OK

Also exercised live against the observed scenario from this worktree: a
-R SpineEventEngine/config create steps aside with the note above (exit 1),
while -R SpineEventEngine/agents blocks for the missing sentinel (exit 2).

publish-version-gate.sh is untouched — #48 is open against it — and
.agents/tasks/pre-pr-gate-cross-repo.md records the task per the repo's
convention.

🤖 Generated with Claude Code

`pre-pr-gate.sh` resolved the repository to gate from the session's work tree
alone and never looked at the command's target. A session working in repository
A that ran `gh pr create -R owner/B` was therefore asked for A's `pre-pr.ok` —
an attestation of checks that were never run against B, and that could not
meaningfully be run against B from A.

Observed on 2026-10-16: a session in a `compiler` worktree opening a `config`
pull request was told to produce
`compiler/.git/worktrees/recursing-wescoff-fcb09b/pre-pr.ok`, and worked around
the gate with `gh api -X POST repos/<slug>/pulls` instead — which bypasses the
gate for every repository, the session's own included.

The gate now reads `-R`/`--repo` out of the matched command segment, in all
four spellings, by word-splitting it with globbing off (never `eval`). A target
that matches any remote of the session's work tree — so `-R upstream/repo` from
a fork too — gates exactly as before. Any other target is outside what this
sentinel can attest, so the gate names it on the first stderr line and exits 1,
a non-blocking error, as `publish-version-gate.sh` already does where it cannot
gate. Demanding B's sentinel instead would be unsatisfiable from this session —
`/pre-pr` only ever writes the sentinel of the work tree it ran in — and would
simply send agents back to `gh api`.

Without an explicit target, including `--head owner:branch`, nothing changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 21:32
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-16T21:36:30.342041Z 144e9d0 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 144e9d03b1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/pre-pr-gate.sh Resolved
Comment thread scripts/pre-pr-gate.sh Resolved
Comment thread scripts/pre-pr-gate.sh Resolved

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Argument parsing and compound-command handling can bypass required sentinel checks for the session repository.

3 open issues
What changed in this PR

Updates the shared pre-PR hook to warn rather than require an unrelated session sentinel when gh pr create explicitly targets another repository.

Changes:

  • Parses repository overrides and compares them with session remotes.
  • Adds tests for explicit targets, fork heads, and ignored commands.
  • Documents the design and known limitations.
File Description
scripts/​pre-pr-gate.sh Adds target detection and cross-repository warnings.
scripts/​tests/​test_hooks.py Covers target selection and existing gating behavior.
.agents/​tasks/​pre-pr-gate-cross-repo.md Records rationale, verification, and follow-ups.

Review effort: Balanced

Comment thread scripts/pre-pr-gate.sh Resolved
Comment thread scripts/pre-pr-gate.sh Resolved
Comment thread scripts/pre-pr-gate.sh Resolved
Review on #49 found three ways the first cut of the parser could be wrong, two
of them new bypasses. Stepping aside is the only path here that does not gate,
so everything it rests on is now conservative.

Words, not raw text. `set -- $1` re-split the segment's source, so
`gh pr create --title 'Document --repo behavior'` surfaced `--repo` as a flag
and a pull request for *this* repository skipped its sentinel. `tokenize()`
splits character by character, honouring single quotes, double quotes and
backslashes, and still evaluates nothing. A segment it cannot read — an
unterminated quote, which the split on `;&|` can itself produce out of
`--title "a|b"` — is gated rather than read as naming another repository.

Every segment, not the first. `gh pr create -R other/repo && gh pr create`
classified only the first segment and let the second, ungated create run. All
creating segments are classified now: no explicit target, this repository, or an
unreadable segment means the gate applies as usual, and it steps aside only when
no segment creates a pull request here. When it gates and another target is also
named, the note is printed after the sentinel passes — the only remaining exit
that can carry it.

Every spelling. `gh pr` accepts the attached `-Rowner/repo`, so that form is
read too; it previously fell back to gating this repository, leaving the original
defect in place for that spelling.

The host is part of the identity. `gh` takes `[HOST/]OWNER/REPO`, and dropping
the host made `enterprise.example/acme/app` compare equal to the `acme/app` of a
github.com remote. `identity_of` keeps `host/owner/repo`; a target that carries
no host is compared bare, as `gh` resolves it against this repository's host.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 21:43
`-R "$REPO"` reaches the hook as the literal `$REPO` — nothing here expands
command text, by design. Compared literally it matched no remote, so it took
the non-blocking exit and skipped the sentinel, even when `$REPO` named this
very repository. An unresolved target is unknown, not different: `$`, a
backtick, a glob character or a leading `~` now sends the segment down the
gating path.

Reported by Copilot on #49.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread scripts/pre-pr-gate.sh Resolved
Comment thread scripts/pre-pr-gate.sh Resolved
Comment thread scripts/pre-pr-gate.sh Resolved
Copilot AI balanced review requested due to automatic review settings October 16, 2026 21:49
A token scan cannot be safe: any option value can be made to look like `-R`.
`--title '--repo=other/repo'` was read as a target, so a pull request for THIS
repository skipped its sentinel. The scanner now consumes each known flag's
value instead of examining it, from a value-taking and a boolean list. A flag in
neither is ambiguous — the next word may be its value or a flag of its own — and
gates the segment, so a `gh` flag newer than this hook makes the gate stricter
rather than looser.

Shell comments were read as arguments: `… --body y # --repo other/repo` took the
commented-out target for a real one. `tokenize()` ends the segment at an
unquoted `#` that starts a word, as the shell does, while `fixes #49` stays a
word.

Line continuations were dropped: a `\` at end of line left `-R` on a line the
line-wise matching never saw, and the gate demanded this repository's sentinel
for someone else's pull request. They are joined before the split; a bare
newline remains a separator.

Found while fixing those: the scanner answered at the first `-R`, so it saw
neither a flag it does not know after the target nor a repeated `-R` — and `gh`
uses the LAST `-R` (`gh pr list -R SpineEventEngine/agents -R cli/cli` lists
cli/cli). `-R other/repo -R <ours>` would have been exempted while `gh` opened
the pull request here. The whole segment is read now, last `-R` winning.

Reported by Copilot on #49.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Two remaining parsing gaps allow session-repository PR creation without sentinel verification.

5 open issues

Review effort: Balanced

Comment thread scripts/pre-pr-gate.sh Resolved
Comment thread scripts/pre-pr-gate.sh Resolved
Copilot AI balanced review requested due to automatic review settings October 16, 2026 21:55
Reported on #49: `gh pr create -R other/repo --title "$(gh pr create --title
local --body local)"` opens a pull request in THIS repository — the inner
create runs first — and then one elsewhere, while only the outer command is on
the line to classify. The cross-repository exemption therefore covered a local
create with no sentinel.

A segment carrying command or process substitution — `$(`, a backtick, `<(`,
`>(` — is now gated. Nothing here evaluates command text to find out what it
would do, so an exemption cannot rest on it.

The repeated-`-R` finding from the same review round was already fixed in
be05215, found while reading the whole segment.

Reported by Copilot on #49.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Verified shell-parsing mismatches still allow session-repository PRs to bypass sentinel checks.

4 open issues
1 previously missed issue

In code that hasn't changed since last review

Medium severity Reject unquoted redirections during argument parsing

scripts/​pre-pr-gate.sh:141

[P2] Unquoted redirection operators are treated as ordinary words, allowing their filenames to become repository flags. If directory --repo=other exists, gh pr create --title x --body y > --repo=other/repo creates a local PR; the apparent flag is only an output filename. The parser nevertheless extracts other/repo and skips the sentinel. Reject unsupported redirections during tokenization, while preserving quoted or escaped < and > as ordinary text, and add a missing-sentinel regression.

5 resolved since last review

Review effort: Balanced

Comment thread scripts/pre-pr-gate.sh Resolved
Comment thread scripts/pre-pr-gate.sh Resolved
Comment thread scripts/pre-pr-gate.sh Resolved
Comment thread scripts/pre-pr-gate.sh Resolved
Copilot AI balanced review requested due to automatic review settings October 16, 2026 22:02
Four more ways the parsing differed from the shell, each able to exempt a pull
request meant for this repository: `-R Spine/ma\` + `in`, an in-word
continuation the shell joins while joining with a space read `Spine/ma`;
`--title a\|b`, split at the escaped pipe into `… --title a\`, which read as a
whole command though the real one ended with a local `-R`; `$EXTRA` or
`"${extra[@]}"` carrying a second `-R`; and `-R{other/repo,ours}`, two options
with ours last.

Each is a fix in itself, but five rounds of the same shape settle the approach.
Enumerating dangerous syntax is only ever as good as one's knowledge of the
shell, and this is the one path in the hook that fails open. So the burden is
inverted: a segment is exempted only when its text cannot change under anything
the shell does after the hook sees it.

Continuations are removed as the shell removes them — the `\` and the newline,
nothing in their place — which is also what makes an in-word continuation read
as the single word the shell builds. Anything else that could change the text
gates the segment before it is parsed at all: `$`, a backtick, `{`, `}`, `*`,
`?`, `[`, `]`, `~`, `<(`, `>(`, and any `\` left once continuations are gone.
The two narrower guards this subsumes are gone with it.

The cost, stated in the header: a cross-repository create carrying any of that
syntax needs this repository's sentinel — the behaviour that predates this
change — and no exemption rests on text the hook had to guess at.

Reported by Copilot on #49.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread scripts/pre-pr-gate.sh Resolved
Comment thread scripts/pre-pr-gate.sh Resolved
Comment thread scripts/pre-pr-gate.sh Resolved
Copilot AI balanced review requested due to automatic review settings October 16, 2026 22:10
Two more segments that read as cross-repository creates while `gh` would open
the pull request here.

`gh pr create --title x --body y > -Rlog` had the redirection's *filename* read
as the target, and `<<< '--repo=other/repo'` its stdin. `<` and `>` join the
gating set, which makes the earlier `<(` and `>(` entries redundant.

`gh pr create -R other/repo --title x --repo ""` ends the segment with an empty
word, and capturing the tokenizer's output drops trailing newlines, so the
scanner stopped waiting for the final value and answered with the earlier `-R`
— while an empty override makes `gh` fall back to this repository. A
value-taking flag still waiting when the words run out now means the segment
was not understood, and an unexplained segment earns no exemption.

The brace-expansion finding from the same review round was already closed by
4f73d5c, which put `{` and `}` in the gating set; verified, not assumed.

Reported by Copilot on #49.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Combined comment and command continuations still let a local PR command bypass sentinel verification.

1 open issue
2 resolved since last review

Review effort: Balanced

Comment thread scripts/pre-pr-gate.sh Resolved
The two continuation cases compose into one neither reading can see:

    # note \
    gh pr \
    create --title local --body local

The shell ends the comment at the newline and continues line two into line
three, so the create runs. Read as received, no line starts with
`gh pr create`; read with every continuation removed, all three fold into the
comment. The hook returned 0 — gating nothing — and returns 2 now.

Detection gets a third reading: each line's `#` to its end is dropped before
continuations are removed, which leaves the create in view. The `#` goes
whether or not it is quoted, because over-dropping can only add a match, and a
detection that matches too much merely gates while one that matches too little
gates nothing at all.

A variant not in the report is covered too, found while testing: a
single-quoted newline, `: '<newline>' ; gh pr \` + `create …`.

Reported by Copilot on #49.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 23:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A verified multiline command still bypasses detection and skips sentinel verification for a local pull request.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread scripts/pre-pr-gate.sh Resolved
Round 10's composition again, with an escaped backslash ending the first
command instead of a comment:

    : \\
    gh pr \
    create --title local --body local

`\\` is a literal backslash, so that line does not continue and the shell runs
the create. The hook returned 0, gating nothing: all three readings glued the
create onto `:`.

What defeats detection is always what PRECEDES the create — a blanket join of
continuations glues it on. So the readings are replaced by one general rule: a
create is detected when it starts a segment of the command as received, or of
the text from any one line onward with continuations removed. Starting the join
at each line in turn needs no judgement about which line-endings continue, and
finds the create in every composition found so far.

This subsumes the joined reading and the comment-stripping pass added one
commit ago, both now gone: strictly stronger, and less code. The per-line loop
runs only for a multi-line command in which the plain reading found nothing, so
the common path is untouched.

Reported by Copilot on #49.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 23:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The fallback scan adds verified multi-second delays to unrelated multiline tool requests.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread scripts/pre-pr-gate.sh Resolved
The per-line scan added one commit ago ran for every multi-line request in
which the plain reading found no create: an 800-line heredoc measured 4.5s
here, paid by an agent on an unrelated tool call.

Only a `\` standing immediately before a newline can make a joined reading
differ from the plain one, and only from the line that carries it. With no such
pair every suffix reads exactly as the plain reading already read it, and a
line not ending in `\` glues nothing onto itself. So the scan runs for those
lines alone: 4.5s becomes 0.035s, while a create whose very first token is
split across lines (`g\` + `h pr create`) is still detected, so the limit costs
no detection. A test holds the 800-line request under two seconds.

Caught while verifying this, before it was committed: writing the pair as
`${suffix//$continuation/}` broke detection outright. Unquoted, the pattern's
`\` escapes the newline, so only the newline was removed and the text read
`gh pr \create`. The pattern is quoted now. `bash -x` on the failing input
found it; the suite did not, having no case for that spelling, so one covers it
now.

Reported by Copilot on #49.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 23:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new tokenizer introduces substantial delays for ordinary single-line PR bodies, including local creates.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread scripts/pre-pr-gate.sh Resolved
The word splitter was quadratic: every character copied the rest of the segment
through `${rest#?}`. Measured here, a 4 KiB pull request body took 4s and a
16 KiB one about a minute — and every create reaches the splitter, so a create
for THIS repository paid that before being gated anyway. Bodies of a few
kilobytes are ordinary, so this was a real cost on ordinary work.

It is one `awk` pass now, walking the text by index: 4 KiB takes 0.141s and
16 KiB 0.165s. `awk` is already required further down to read the sentinel, so
nothing new is needed, and the pass still evaluates nothing — no expansion
runs and no command substitution in the text can execute. Behaviour is
unchanged: the suite's quoting, escape, comment, flag-shaped-value and
unterminated-quote cases all pass.

A timing test covers 4 KiB and 16 KiB bodies for a local and a cross-repository
create, since round 12's heredoc test never reaches the splitter.

Reported by Copilot on #49.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 23:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The organization-wide non-blocking exemption retains a repository-identity gap and needs final maintainer review.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread scripts/pre-pr-gate.sh Resolved
`gh repo set-default` records the repository `gh` works against in
`remote.<name>.gh-resolved`, and it need not be any remote's own. Verified in a
throwaway repository: with `origin` at `contributor/fork` and that key set to
`upstream/project`, no upstream remote present, `gh repo set-default --view`
reports `upstream/project`.

A `-R upstream/project` in such a checkout therefore names the checkout's own
default pull-request repository, while the identity list — built from remote
URLs alone — read it as another repository and exempted the create from its
sentinel. Non-`base` `gh-resolved` values are part of the identities now, under
the same percent-encoding guard; `base` means the remote itself, which its URLs
already cover.

Reported by Copilot on #49.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 16, 2026 23:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The organization-wide exemption relies on custom shell parsing with an unresolved bypass and needs maintainer validation.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread scripts/pre-pr-gate.sh Resolved
With `extglob` enabled and a matching path, `@(OWNER)/repo` is a glob that
expands to `OWNER/repo` — confirmed with `bash -O extglob -c 'echo
@(Spine)/agents'`, which printed `Spine/agents`. The hook compared the literal
`@(…)` text, so such a target read as another repository and was exempted even
where it expands to this one.

Parentheses join the characters that withhold the exemption. That is the rule
from the one-plain-line commit — the line must carry nothing the shell would
still change — applied to a form it had missed; `!(…)` and `+(…)` fall under the
same guard. Exploitability is low, since `extglob` is off by default in
non-interactive shells and the expansion needs a matching path, and the cost is
only that a cross-repository create whose text contains parentheses needs this
repository's sentinel.

Reported by Copilot on #49.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 17, 2026 00:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The organization-wide warning-only exemption relies on custom shell and CLI parsing that warrants final maintainer review.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@alexander-yevsyukov
alexander-yevsyukov merged commit 0f760b4 into master Oct 19, 2026
1 check passed
@alexander-yevsyukov
alexander-yevsyukov deleted the gate-cross-repo-pr branch October 19, 2026 13:57
@github-project-automation github-project-automation Bot moved this from In Review to ✅ Done in v2.0 Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

3 participants