ci(deep-review): restore fork reviews by removing the reviewer's shell - #2938
ci(deep-review): restore fork reviews by removing the reviewer's shell#2938teeohhem wants to merge 2 commits into
Conversation
Fork-PR deep reviews have failed at "Checkout PR head" since 2026-07-20: `actions/checkout@v6.1.0` backported `allow-unsafe-pr-checkout` as a breaking change defaulting to false, and we track the moving `v6` tag, so it arrived with no change on our side. v7 is not an escape -- v7.0.1 ships the same input with the same default. Setting that flag alone would re-open stranger-triggered code execution next to ANTHROPIC_API_KEY and a token with `pull-requests: write`, so the flag and its containment land together. The containment is removing Bash, not narrowing it. `Bash(git:*)` cannot be reduced to a read-only grant: --allowedTools matches by command prefix and cannot constrain a flag, and `git log --output=<path>` is an arbitrary file write (aim it at `.git/config`, set `diff.external`, and the next `git diff` executes it) while `git diff --no-index <a> <b>` is an arbitrary file read. No subset of git subcommands is read-only. `--tools` replaces the built-in roster instead, leaving `Read, Grep, Glob, Task, Skill` -- no Bash, no Write/Edit, no WebFetch/WebSearch -- so the model cannot execute what it reads and has no network egress. Its only output channel is the review text. That works because the skill's shell use is confined to Stage 1 (scope) and Stage 2 (intent) in `mode:report-only`, which also skips run-id generation and artifact writes. A trusted step now materializes the diff, file list, commits, branch and prior review comments into RUNNER_TEMP, and the prompt points the skill at them. Prior comments are materialized so ce-previous-comments-reviewer stays on the roster rather than silently dropping when it cannot reach `gh pr view`. The prompt also demands the fan-out explicitly: without that instruction the orchestrator skips dispatch and returns a plausible single-pass review. Verified against the pinned CLI and plugin ref: the shipped claude_args tokenize to exactly `Agent, Glob, Grep, Read, Skill`, and a no-Bash run of the skill dispatched 9 sub-agents with zero tool errors and produced a real merged review.
|
The latest updates on your projects. Learn more about Vercel for GitHub. 2 Skipped Deployments
|
|
Greptile SummaryThe workflow restores deep reviews for fork pull requests while removing the reviewer's shell and network-capable tools.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| .github/workflows/deep-review.yml | Enables fork reviews, supplies precomputed review scope to a shell-less reviewer, fixes prior-comment pagination, and validates reviewer fan-out and tool restrictions. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Fork pull request] --> B[Checkout fork head]
B --> C[Validate merge base and changed files]
C --> D[Materialize trusted review scope]
D --> E[Run shell-less multi-agent reviewer]
E --> F[Validate fan-out and tool roster]
F --> G[Post structured review]
F -->|Validation fails| H[Mark reviewer unhealthy]
Reviews (2): Last reviewed commit: "ci(deep-review): close gaps the shell-le..." | Re-trigger Greptile
Deep Review✅ No critical issues found. This is a CI-only workflow change; the 🟡 P2 -- recommended
🔵 P3 nitpicks (5)
Reviewers (9): correctness, testing, maintainability, project-standards, agent-native, learnings-researcher, security, adversarial, reliability. Testing gaps:
|
Ran the no-Bash reviewer against this PR. It returned six sub-agents and flagged real defects in the change; these are the in-scope fixes. `gh api --paginate` with `--jq` applies the filter per page and concatenates the results, so a PR with 30+ review comments wrote several back-to-back `[...]` documents rather than one array -- and the prompt tells the reviewer that file is authoritative. `--slurp` is the fix but cannot be combined with `--jq`, and it wraps pages instead of flattening them, so the projection moves to a separate jq with `.[][]`. The result is now validated as a single array before dispatch, with an empty set substituted if it is not. The fan-out floor was 4 while the comment beside it stated the always-on roster is 6, so a run missing part of that roster passed and cached a valid state marker, never re-reviewing the gap. Raised to 6; a false positive costs one re-review because the marker is omitted on unhealthy runs. The no-Bash invariant is the control that makes reviewing fork code here defensible, and it rested entirely on `--tools` -- which does not validate names. Assert it from the transcript instead: fail if any tool outside Read/Grep/Glob/Task/Skill was ever called. Verified it fires by injecting Bash and WebFetch calls into a real transcript. Broadened the sensitive-path denies beyond /proc and /sys to the credential-bearing trees on a hosted runner. A `Read(<path>)` deny also constrains Grep -- verified directly rather than assumed, which is the opposite of what release.yml's note about unscoped Grep suggests, because that note is about allow rules. The comment now says which it is, and that a denylist raises cost rather than proving a boundary. Also assert all six materialized scope files exist, since the prompt hardcodes their names, and correct the gate-hash comment: it omitted that the materialized diff also passes --text.
Dogfooded this PR through the shell-less reviewerRan the exact shipped config (prompt + Result: 6 sub-agents dispatched, zero tool errors, It also found real defects in the change, which is the more useful outcome. Fixed in 68daf99:
Deliberately not fixed here
One caveat on my own validation, for the record: an earlier attempt ran unrestricted because a shell-ism silently produced an empty argument array. I caught it from the arg echo, discarded that run, and added a guard that refuses to run when the array looks wrong. The run reported above is the guarded one. |
🟢 Tier 1 — TrivialDocs, images, lock files, a dependency bump, or an automated release. No functional code changes detected. Why this tier:
Review process: Auto-merge once CI passes. No human review required. Stats
|
E2E Test Results✅ All tests passed • 300 passed • 1 skipped • 914s
Tests ran across 4 shards in parallel. |
Fork-PR deep reviews have been broken since 2026-07-20 — 18 runs, 0 successes, across all 6 fork PRs. This restores them. The flag that unblocks them and the containment that makes it safe land together, deliberately.
Problem
actions/checkout@v6.1.0backportedallow-unsafe-pr-checkoutas an explicitly breaking change defaulting tofalse, and we track the movingv6tag — so it arrived with no change on our side. v7 is not an escape: v7.0.1 ships the same input with the same default.Setting that flag alone would re-open stranger-triggered code execution next to
ANTHROPIC_API_KEYand a token withpull-requests: write. Note the current failure is fail-closed: fork PRs reach no reviewer today, so this PR is what switches the exposure on. That's why it can't ship on its own.Fix: remove Bash, don't narrow it
Bash(git:*)cannot be reduced to a read-only grant.--allowedToolsmatches by command prefix and cannot constrain a flag, and:git log --output=<path>is an arbitrary file write — aim it at.git/config, setdiff.external, and the nextgit diffexecutes it.git diff --no-index <a> <b>is an arbitrary file read.Both verified locally. No subset of git subcommands is read-only, so
--toolsreplaces the built-in roster instead, leavingRead, Grep, Glob, Task, Skill— no Bash, no Write/Edit, no WebFetch/WebSearch. The model cannot execute what it reads and has no network egress; its only output channel is the review text this workflow posts.That's viable because the skill's shell use is confined to Stage 1 (scope) and Stage 2 (intent) in
mode:report-only, which also skips run-id generation and artifact writes. A trusted step materializes the diff, file list, commits, branch and prior review comments intoRUNNER_TEMP, and the prompt points the skill at them. Prior comments are included soce-previous-comments-reviewerstays on the roster instead of silently dropping when it can't reachgh pr view.The prompt also demands the fan-out explicitly — without that instruction the orchestrator skips dispatch and returns a plausible single-pass review. #2936 adds the assertion that catches it.
Not fixed here
The job still holds
pull-requests: writeandANTHROPIC_API_KEY. With no Bash and no WebFetch the model can't reach either, so moving the posting into a separateworkflow_runjob is now defense-in-depth rather than load-bearing — worth doing, not required for this.allowed_non_write_users: '*'is kept for the same reason; there's a comment tying it to the tool restriction so the two aren't decoupled by accident.Tests
Against the pinned CLI (
2.1.215) and plugin ref (v3.6.1):claude_argstokenize to exactlyAgent, Glob, Grep, Read, Skill— verified by extracting them from this YAML and passing them through verbatim. Bash is gone.--outputwrite vector and found the.gitattributesblinding that ci(deep-review): stop the reviewer reporting clean on code it never saw #2936 fixes.prior-review-comments.jsonis a valid array.--toolsfootguns found and documented inline: it does not validate names (a typo silently yields a tool-less agent), and--tools ""does not disable everything despite its help text.Cost is unchanged from today and not small: ~$5 for a one-file diff at 9 agents.
The fork path itself can only be exercised once this is on
main(pull_request_targetloads the workflow from the base branch); marking draft fork PR #2922 ready-for-review is the cheapest trigger.CI-only change, so no changeset per
AGENTS.md.How to test on Vercel preview
N/A — non-UI change (CI workflow only).
Preview routes: N/A
Steps:
N/A
References
actions/checkout#2500(upstream backport)pull_request_targetdefaults for GitHub Actions checkout