Repository navigation
ci(deep-review): restore fork PR reviews and keep the reviewer read-only #2924
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -123,12 +123,37 @@ jobs: | |
|
|
||
| # Check out the PR head so reviewer sub-agents can read the actual code. | ||
| # Works for both same-repo and forked PRs. | ||
| # | ||
| # `allow-unsafe-pr-checkout` is REQUIRED for the fork case. checkout | ||
| # v6.1.0 (2026-07-20) backported it as an explicitly BREAKING change | ||
| # defaulting to false, so checking out fork code from a | ||
| # `pull_request_target` workflow now hard-fails. Because we track the | ||
| # moving `v6` tag, that arrived with no change on our side -- and every | ||
| # fork-PR review failed at this step until this flag was set. Bumping to | ||
| # v7 is not an escape: v7.0.1 ships the same input with the same default. | ||
| # https://github.blog/changelog/2026-06-18-safer-pull_request_target-defaults-for-github-actions-checkout/ | ||
| # | ||
| # The flag's warning is about executing fork code in a credentialed | ||
| # context. This job never executes it: no `yarn install`, no build, no | ||
| # Edit/Write tools, `contents: read`, and `--setting-sources user` below | ||
| # so the fork's `.claude/settings.json` cannot widen permissions. Note | ||
| # the checkout widens the prompt-injection surface from "the diff" to | ||
| # "all files" but does not create it -- `Bash(gh pr diff:*)` already | ||
| # feeds untrusted fork text to the model with no checkout at all. | ||
| - name: Checkout PR head | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| repository: ${{ steps.pr.outputs.head_repo }} | ||
| ref: ${{ steps.pr.outputs.head_ref }} | ||
| fetch-depth: 0 | ||
| allow-unsafe-pr-checkout: true | ||
| # Keep the base repo's GITHUB_TOKEN out of `.git/config` inside an | ||
| # untrusted worktree, where the reviewer's `Bash(git:*)` grant could | ||
| # read it back via `git config --get http.<url>.extraheader`. | ||
| # Nothing downstream needs git credentials: the base fetch below | ||
| # uses a public HTTPS URL, and every `gh` call takes its token from | ||
| # the environment rather than from git config. | ||
| persist-credentials: false | ||
|
|
||
| # Make the PR base SHA reachable AND fetch enough history that | ||
| # `git merge-base HEAD <base_sha>` succeeds. The ce-code-review skill | ||
|
|
@@ -260,6 +285,15 @@ jobs: | |
| repository: EveryInc/compound-engineering-plugin | ||
| ref: ${{ env.PLUGIN_REF }} | ||
| path: ce-plugin | ||
| # Same reason as the PR-head checkout above, and it matters even | ||
| # though this repo is not the untrusted one: `ce-plugin` is a | ||
| # subdirectory of the very worktree holding fork code, so a | ||
| # credential left in `ce-plugin/.git/config` is reachable from the | ||
| # same reviewer that reads that code. Nothing here needs it -- the | ||
| # plugin is consumed only as a local marketplace path | ||
| # (`plugin_marketplaces: ./ce-plugin`) and the repo is public, so | ||
| # even a re-fetch would not need auth. | ||
| persist-credentials: false | ||
|
|
||
| # --- CLI pin (TEMPORARY) ---------------------------------------------- | ||
| # claude-code-action hardcodes the Claude Code CLI version it installs | ||
|
|
@@ -319,6 +353,28 @@ jobs: | |
| fi | ||
| echo "PINNED_CLAUDE=$CLAUDE_BIN" >> "$GITHUB_ENV" | ||
|
|
||
| # Backstop for the two `persist-credentials: false` settings above, and | ||
| # the last step before the reviewer runs -- so it covers every worktree | ||
| # any earlier step created, not just the two checkouts we know about. | ||
| # The reviewer's git grant reads from an untrusted worktree, so a | ||
| # checkout added later without the flag (or an upstream default that | ||
| # flips back) must fail loud here rather than leak quietly. | ||
| - name: Assert no persisted git credentials in the workspace | ||
| if: steps.gate.outputs.should_review == 'true' | ||
| run: | | ||
| set -euo pipefail | ||
| # `-name config -path '*/.git/*'` also catches submodule configs at | ||
| # `.git/modules/<name>/config`. `|| true` is required: `grep -l` | ||
| # exits 1 on no match, which pipefail would turn into a job failure | ||
| # on the healthy path. | ||
| LEAKED=$(find "$GITHUB_WORKSPACE" -type f -name config -path '*/.git/*' -print0 \ | ||
| | xargs -0 -r grep -lEi 'extraheader|sshCommand' || true) | ||
| if [ -n "$LEAKED" ]; then | ||
| echo "::error::Persisted git credentials found in: $(echo "$LEAKED" | tr '\n' ' '). Add 'persist-credentials: false' to the checkout that created it -- the reviewer's git grant can read it back out of an untrusted worktree." | ||
| exit 1 | ||
| fi | ||
| echo "No persisted git credentials under $GITHUB_WORKSPACE." | ||
|
|
||
| - name: Run deep review | ||
| id: review | ||
| if: steps.gate.outputs.should_review == 'true' | ||
|
|
@@ -489,9 +545,58 @@ jobs: | |
| 3. Do NOT post the review yourself with `gh` or any comment tool -- | ||
| the workflow posts the structured output as a sticky comment. | ||
|
|
||
| # Deliberately no `Bash(gh api:*)` in the allowlist below. | ||
| # --allowedTools matches by command prefix and cannot constrain an | ||
| # HTTP method, so granting `gh api` grants POST/PATCH/DELETE against a | ||
| # token holding `pull-requests: write` + `issues: write` -- a | ||
| # repo-write channel driven by a model reading untrusted fork code. | ||
| # In the pinned plugin only ce-previous-comments-reviewer reaches for | ||
| # it, and that agent already prefers | ||
| # `gh pr view <n> --json reviews,comments` (still allowed via | ||
| # `Bash(gh pr view:*)`); the sole loss is inline-comment path/line | ||
| # metadata. | ||
| # | ||
| # git is granted per read-only subcommand rather than as `Bash(git:*)` | ||
| # for the same reason: a wildcard git grant is a credential-read | ||
| # channel (`git config --file <any>/.git/config --list`) and a network | ||
| # egress channel (`git fetch`/`ls-remote`/`archive --remote` to an | ||
| # attacker URL), both driven by a model reading untrusted fork code. | ||
| # The allowlist is the primary control: a denylist cannot enumerate | ||
| # every subcommand that reaches the network or a credential | ||
| # (`git archive --remote`, `git credential fill`, `git submodule | ||
| # update --remote`), whereas an allowlist has no such tail. The | ||
| # `--disallowedTools` line is belt-and-suspenders, because | ||
| # --allowedTools only PRE-APPROVES -- an un-approved call fails here | ||
| # only because a `-p` run cannot answer the prompt -- while | ||
| # --disallowedTools is a real deny. | ||
| # | ||
| # Verified against the pinned plugin ref. Of the 21 personas in | ||
| # ce-code-review/references/persona-catalog.md, 20 invoke git zero | ||
| # times; references/diff-scope.md says why -- "The scope step in the | ||
| # SKILL.md handles discovery and passes you the resolved diff." The | ||
| # exception, ce-schema-drift-detector, is Rails-only (spawned solely | ||
| # for `db/migrate/*.rb` / `db/schema.rb`, which do not exist in this | ||
| # repo) and its reads are plain `git diff`; its one write, | ||
| # `git checkout <base> -- db/schema.rb`, is a working-tree mutation | ||
| # `mode:report-only` forbids regardless. | ||
| # | ||
| # The orchestrator's own surface is the `base:<sha>` fast path | ||
| # (SKILL.md "Skip all base-branch detection, remote resolution, and | ||
| # merge-base computation"), which needs only | ||
| # merge-base/diff/ls-files/rev-parse/log. The skill's `git fetch` | ||
| # fallbacks live in scripts/resolve-base.sh, which needs | ||
| # `Bash(bash:*)` -- never granted -- and which `base:` skips anyway; | ||
| # `git checkout` / `gh pr checkout` are already blocked by | ||
| # `mode:report-only`. `git -c <cfg> diff` cannot slip past the | ||
| # allowlist either: `-c` precedes the subcommand, so the command | ||
| # string starts `git -c` and matches no prefix. | ||
| # | ||
| # Comments must stay OUT of the block scalar below -- every line in it | ||
| # is passed to the CLI verbatim as an argument. | ||
| claude_args: | | ||
| --setting-sources user | ||
| --allowedTools "Bash(git:*),Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr list:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh search:*),Bash(gh api:*)" | ||
| --allowedTools "Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git blame:*),Bash(git merge-base:*),Bash(git rev-parse:*),Bash(git ls-files:*),Bash(git cat-file:*),Bash(git status:*),Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr list:*),Bash(gh issue view:*),Bash(gh issue list:*),Bash(gh search:*)" | ||
| --disallowedTools "Bash(git config:*),Bash(git credential:*),Bash(git fetch:*),Bash(git pull:*),Bash(git clone:*),Bash(git ls-remote:*),Bash(git remote:*),Bash(git push:*),Bash(git archive:*),Bash(git submodule:*)" | ||
|
Comment on lines
+598
to
+599
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a prompt-injected reviewer invokes the allowed |
||
| --json-schema '{"type":"object","properties":{"review":{"type":"string","description":"Complete markdown review starting with <!-- deep-review --> on the first line and ## Deep Review on the second line"}},"required":["review"]}' | ||
|
|
||
| # --- Sandbox smoke check ---------------------------------------------- | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.