Skip to content

fix(ci): restore Claude PR review under subprocess env scrubbing - #1539

Merged
giswqs merged 2 commits into
mainfrom
fix/claude-review-env-scrub
Jul 29, 2026
Merged

fix(ci): restore Claude PR review under subprocess env scrubbing#1539
giswqs merged 2 commits into
mainfrom
fix/claude-review-env-scrub

Conversation

@giswqs

@giswqs giswqs commented Jul 29, 2026

Copy link
Copy Markdown
Member

The problem

Claude Code Review has posted nothing since 2026-07-20 — on fork PRs and on our own — while still spending $0.50–$1.00 per run. Runs report success, so this went unnoticed.

Run Turns Cost Denials Posted
#1534 (fork) 17 $0.64 1 nothing
internal PR 22 $0.71 5 nothing
internal PR 29 $1.03 6 nothing

The two messages surfaced in the run annotations are both red herrings: the cache-write warning is benign, and ⚠️ SECURITY WARNING: Bypassing write permission check is just the action noting it honored our allowed_non_write_users setting.

Root cause

This workflow hasn't changed since 2026-07-20. The regression arrived through the moving anthropics/claude-code-action@v1 tag (now on Claude Code 2.1.220).

Because we set allowed_non_write_users: "*", the action auto-enables subprocess env scrubbing — action.yml:290:

CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: ${{ env.CLAUDE_CODE_SUBPROCESS_ENV_SCRUB || (inputs.allowed_non_write_users != '' && '1') || '' }}

Per the action's docs/security.md, that does a "best-effort scrub of Anthropic, cloud, and GitHub Actions secrets from subprocess environments."

Our prompt drove the entire review through gh. With no token in Claude's subprocess environment, gh pr diff / gh pr view / gh pr comment all failed unauthenticated. Claude fell back to Read/Grep on the checkout (hence the turns and the cost), groped for non-allowlisted alternatives (the permission denials), and had no way to publish anything.

The fix

Opting out via CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: 0 would be two lines, but it puts the write-scoped GITHUB_TOKEN back within reach of a prompt injection carried in an untrusted fork diff — I verified the allowlist permits command substitution, so gh pr comment N --body "$(printenv GITHUB_TOKEN)" would go through. Not worth it on a public repo.

Instead, keep the hardening and route around gh:

  1. Input — a workflow step (which does have the token) stages pr-context/{diff.patch,changed-files.txt,metadata.json}; Claude reads them with Read/Grep. Runs after checkout and clears the directory first, so a PR can't pre-seed its own pr-context/. Diff is capped at 1.5 MB with an explicit truncation marker.
  2. Inline findings — unchanged, via the github_inline_comment MCP server. Its token is injected through the MCP server config rather than inherited from the environment, so it is unaffected by the scrub.
  3. Summary — Claude ends with the summary as its final message; a workflow step extracts it from the action's execution_file output and posts it with gh pr comment --body-file.

Bash is dropped from --allowedTools entirely — no token reaches the agent, so no gh invocation there could have worked regardless.

track_progress: true is not an option here: src/modes/detector.ts:84 restricts it to pull_request/issues/comment events, and this workflow triggers on pull_request_target.

Verification

  • Confirmed execution_file holds the unredacted messages — writeExecutionFile(messages) serializes the raw array; only console output goes through sanitizeSdkOutput. The console "full output hidden for security" line is why the denials were invisible in the logs.
  • Exercised the summary-extraction jq against success, is_error: true, and missing-result-turn fixtures; the last two yield empty and hit the skip guard.
  • Dry-ran the staging step against feat(share): add access roles (view, comment, edit), link expiry, password protection, and share revocation (#1527) #1534: 37,570 bytes of diff, 6 changed files, valid metadata.json.
  • bash -n on all three run: blocks; YAML parses; pre-commit run --files clean.

Not verifiable before merge: pull_request_target workflows run from the base branch, so the end-to-end path only exercises once this lands. If the summary step misfires it fails loudly (::warning:: + captured output) rather than posting a broken comment.

Summary by CodeRabbit

  • New Features
    • Added automated pull request reviews that use staged review context for inline feedback.
    • Added a consolidated “review summary” comment to pull requests.
  • Improvements
    • Improved robustness for large review content with enforced size limits and safe truncation.
    • Prevented posting incomplete, empty, or invalid review summaries.
  • Security & Reliability
    • Tightened the review environment to avoid token exposure and disallow external command/network actions.

Claude Code Review has posted nothing since 2026-07-20 while still
burning $0.50-$1.00 per run. The workflow was unchanged; the regression
came in through the moving `anthropics/claude-code-action@v1` tag.

Because this workflow sets `allowed_non_write_users`, the action now
auto-enables `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=1` (its action.yml), which
strips GITHUB_TOKEN/GH_TOKEN from every subprocess Claude spawns. Our
prompt drove the whole review through `gh` — `gh pr diff` to read the
change and `gh pr comment` to publish it — so all of it failed
unauthenticated. Claude fell back to Read/Grep on the checkout, spent its
turns there, and had no way to publish. The run still reported success,
which is why this went unnoticed: the only visible symptoms were a benign
cache warning and the action's own "bypassing write permission check"
notice, neither of which is the cause.

Rather than opt out of the scrub (which would put the write-scoped token
back within reach of a prompt injection carried in an untrusted fork
diff), keep the hardening and route around `gh`:

  - A workflow step stages the diff, changed-file list, and PR metadata
    into `pr-context/` for Claude to Read.
  - Inline findings keep using the `github_inline_comment` MCP server; its
    token comes from the MCP server config, not the environment, so it
    survives the scrub.
  - Claude ends with the summary as its final message; a workflow step
    publishes it from the action's `execution_file` output.

Bash is dropped from --allowedTools entirely: no token reaches the agent,
so no `gh` invocation there could have worked anyway.
Copilot AI review requested due to automatic review settings July 29, 2026 14:22

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b60f474b-4c5e-4da4-aafa-5aa44eba8a76

📥 Commits

Reviewing files that changed from the base of the PR and between 108fb9e and 7242641.

📒 Files selected for processing (1)
  • .github/workflows/claude-code-review.yml

📝 Walkthrough

Walkthrough

The review workflow stages pull request data on disk, removes Claude’s Bash and network access, pins the Claude action, limits its tools to file inspection and inline comments, and posts its final structured review through a separate validated workflow step.

Changes

Claude review workflow

Layer / File(s) Summary
Stage pull request context
.github/workflows/claude-code-review.yml
The workflow documents agent restrictions, stages pull request metadata and changed-file statistics, writes a bounded diff, and directs Claude to use the staged files.
Run and publish the review
.github/workflows/claude-code-review.yml
The Claude action is pinned to a commit, its tools are restricted to Read/Grep/Glob and inline comments, and the final execution result is extracted, size-limited, and posted as one pull request comment.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions
  participant gh
  participant pr_context
  participant Claude
  participant execution_file
  participant PullRequest
  GitHubActions->>gh: fetch pull request metadata, files, and diff
  gh->>pr_context: write staged review inputs
  GitHubActions->>Claude: provide staged context with restricted tools
  Claude->>execution_file: write final review summary
  GitHubActions->>execution_file: extract and validate result
  GitHubActions->>PullRequest: post truncated Markdown summary
Loading

Poem

A bunny checked the diff by moonlight bright,
With Bash tucked safely out of sight.
Files were staged in a tidy row,
Claude wrote findings, neat as snow,
Then one small comment made them glow.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: restoring Claude PR review after subprocess environment scrubbing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/claude-review-env-scrub

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://9a08a239.geolibre-preview.pages.dev
Demo app https://9a08a239.geolibre-preview.pages.dev/demo/
Commit 7242641

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/claude-code-review.yml:
- Around line 247-267: Update the summary-file handling around the jq extraction
and truncation logic to create transient files with mktemp under $RUNNER_TEMP,
rather than using checkout-relative summary.md or summary.trimmed paths. Ensure
all subsequent size checks, reads, truncation, and gh pr comment --body-file
usage reference the generated temporary file and avoid following symlinks from
the untrusted checkout.
- Around line 9-31: Update the Run Claude Code Review step’s
anthropics/claude-code-action uses reference from the mutable v1 tag to the
exact full commit SHA for the intended action version, preserving the existing
action configuration and behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 09a7e330-af10-4e40-80f1-71e6f439d04e

📥 Commits

Reviewing files that changed from the base of the PR and between 76bfe73 and 108fb9e.

📒 Files selected for processing (1)
  • .github/workflows/claude-code-review.yml

Comment thread .github/workflows/claude-code-review.yml
Comment thread .github/workflows/claude-code-review.yml Outdated
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site https://opengeos.org/pages-preview/GeoLibre/pr-1539/
Demo app https://opengeos.org/pages-preview/GeoLibre/pr-1539/demo/
Commit 7242641

@giswqs

giswqs commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@giswqs

giswqs commented Jul 29, 2026

Copy link
Copy Markdown
Member Author

@codex review

- Write the summary to a mktemp file under $RUNNER_TEMP instead of
  `summary.md` in the workspace. The checkout is the untrusted PR head, so
  a PR shipping `summary.md` as a symlink would have the redirection follow
  it and clobber the target (or fail the step before the review posts).
- Pin `anthropics/claude-code-action` to commit be7b93b instead of the
  mutable `v1` tag. This workflow depends on action internals that `v1` has
  silently changed before — that drift is the bug this PR fixes — so a SHA
  turns the next such change into a reviewable Dependabot PR. be7b93b is
  what `v1` already resolved to, so behaviour is unchanged. Note that
  `v1` is an annotated tag, so the pin is its dereferenced commit, not the
  tag object SHA.
- Pass `github.repository` through `env:` in the two steps this PR adds
  rather than expanding `${{ }}` inside the shell body, clearing the
  template-injection warning zizmor raised on the summary step.

@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: 108fb9ed89

ℹ️ 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".

- name: Post review summary
if: ${{ steps.claude-review.outputs.execution_file != '' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Isolate summary posting from untrusted project hooks

When a PR adds a .claude/settings.json hook, Claude Code can execute that attacker-controlled shell command while reviewing the checked-out head; project hooks are explicitly shell commands according to the Claude Code hooks documentation. Although subprocess scrubbing hides the current token, the hook can persist changes on the runner—for example, placing a malicious gh earlier in the next step's PATH—so this newly added step then supplies its write-scoped GH_TOKEN to attacker-controlled code. Post the captured result from a fresh job/runner, or otherwise prevent project hooks and runner mutation before introducing a token-bearing step.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks — this is the right thing to worry about, but I believe it's already closed, and by the action itself rather than by anything in this PR. Leaving the thread open for @giswqs to weigh in since it's a P1 security call.

1. The PR's .claude/ never reaches Claude. restoreConfigFromBase (src/github/operations/restore-config.ts:88) exists for exactly this vector. Its doc comment names it:

The CLI's non-interactive mode trusts cwd: it reads .mcp.json, .claude/settings.json, and .claude/settings.local.json from the working directory and acts on them before any tool-permission gating — executing hooks (including SessionStart), setting env vars (NODE_OPTIONS, LD_PRELOAD, PATH), running apiKeyHelper/awsAuthRefresh shell commands […] When this action checks out a PR head, all of these are attacker-controlled.

It snapshots the PR's copies to .claude-pr/ (explicitly not executed), deletes them, then restores the base versions. This is observable in our own run logs:

Restoring .claude, .mcp.json, .claude.json, .gitmodules, .ripgreprc, CLAUDE.md, CLAUDE.local.md, .husky from origin/main (PR head is untrusted)
Preserved PR's sensitive paths -> .claude-pr/ for review agents (not executed)

Worth noting for this repo specifically: git ls-files .claude .mcp.json .claude.json .husky returns 0. Nothing is tracked on main, so "paths absent on base are deleted" means a PR-added .claude/settings.json is removed outright, not merely overwritten.

2. PATH and the loader env are sanitised before my step runs. action.yml:383Re-prepend system bin dirs to PATH, gated on always() && inputs.allowed_non_write_users != '', which is precisely our configuration. It re-prepends /usr/bin and /bin to $GITHUB_PATH and clears BASH_ENV, LD_PRELOAD, LD_LIBRARY_PATH, NODE_OPTIONS, and the DYLD_* set. That is the specific "malicious gh earlier in PATH" defence, and it runs inside the composite action, i.e. before the Post review summary step this PR adds.

3. The agent has no write primitive here. --allowedTools is Read,Grep,Glob,mcp__github_inline_comment__create_inline_comment — no Bash, no Write, no Edit. Claude cannot mutate the runner directly; hooks were the only execution route, and (1) closes it.

Residual risk, stated honestly: this rests on the action's SENSITIVE_PATHS list staying complete as Claude Code evolves. If a future version reads settings from a path not on that list, the gap reopens silently. That's an argument for the SHA pin added in 7242641 — the pin makes such a change arrive as a reviewable Dependabot PR instead of drifting in.

So I'd rather not add a separate posting job: it's real complexity (artifact round-trip plus a second job) against a vector with three independent mitigations. Happy to implement it if you'd prefer belt-and-braces — say the word and I'll do it in a follow-up.

@giswqs
giswqs merged commit 58725f6 into main Jul 29, 2026
37 checks passed
@giswqs
giswqs deleted the fix/claude-review-env-scrub branch July 29, 2026 14:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants