Skip to content

ci: least-privilege workflow tokens; pin AI thread ops to gh-thread helper - #314

Merged
allxsmith merged 2 commits into
mainfrom
security/workflow-hardening
Jul 15, 2026
Merged

allxsmith merged 2 commits into
mainfrom
security/workflow-hardening

Conversation

@allxsmith

@allxsmith allxsmith commented Jul 15, 2026 •

Copy link
Copy Markdown
Owner

Implements findings 1, 3, and 4 from the workflow prompt-injection security review (finding 2 — the deep reviewer's gh api:* — and the AI_LOOP_PAT migration are deliberately out of scope here).

1. AI agents lose raw gh api (HIGH)

The claude-pr-loop fix/verify sessions and bestaxbot-reply read entire review threads — including replies from arbitrary users on this public repo — while the fix/reply agents hold the bestaxbot PAT. Bash(gh api:*) gave a prompt-injected session read-write access to everything the token can reach, plus a public exfiltration channel.

  • New .github/scripts/gh-thread.sh: list <pr> / reply <thread-id> <body> / resolve <thread-id> — the loop's three GraphQL thread operations and nothing else.
  • Each affected job stages it onto PATH from origin/main, never from the checked-out PR branch, so older claude/* branches still work and the branch's own copy is never the one that runs. It lives under .github/, so the protected-path gate and the agents' Edit/Write deny rules already cover it.
  • Allowlists swap Bash(gh api:*) → Bash(gh-thread:*); prompts updated to match (listing, thread replies, Copilot/verify resolves).

3. ci.yml: read-only default token (MEDIUM)

build-and-test and react-compat execute PR-authored code — including AI-pushed claude/* branches — with a contents: write token persisted in .git/config. Workflow default is now contents: read (the publish job re-declares its own scopes), and both test jobs set persist-credentials: false.

4. visual-regression.yml: same treatment (MEDIUM)

The matrix job runs scaffolded-app installs with --ignore-workspace, i.e. outside pnpm-workspace.yaml's install-script blocking and release-age cooldown, while holding a write-capable persisted token. Workflow default is now contents: read + persist-credentials: false; contents: write moved down to the workflow_dispatch-gated commit-screenshots job (verified-commit uses the git data API, so that one scope suffices).

Notes for review

  • All gh api calls in trusted run: shell steps (gate, terminal-state, continue, handoff, halt) are unchanged — only the Claude session allowlists narrowed.
  • In-flight ai-loop PRs don't need a rebase: the helper is staged from origin/main, not the PR branch.
  • Defense-in-depth, not a silver bullet: an injected agent with sed -i/pnpm run can still do local mischief inside the job — the remaining hard boundary is the PAT migration (fine-grained, single-repo, no workflow scope), tracked separately.

Summary by CodeRabbit

  • New Features
    • Added a gh-thread helper CLI to list PR review threads, post replies, and resolve threads.
  • Security
    • Tightened GitHub Actions token permissions to read-only by default.
    • Disabled git credential persistence during checkout to reduce exposure.
  • Automation
    • Updated Claude-driven review workflows to use gh-thread for thread operations and adjusted tool permissions/prompts accordingly.
  • CI
    • Ensured only the screenshot baseline update job has elevated write access.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 0829f8ae-02ef-429f-9c49-0a051477aac5

📥 Commits

Reviewing files that changed from the base of the PR and between 70749eb and 99c3091.

📒 Files selected for processing (5)
  • .github/scripts/gh-thread.sh
  • .github/workflows/bestaxbot-reply.yml
  • .github/workflows/ci.yml
  • .github/workflows/claude-pr-loop.yml
  • .github/workflows/visual-regression.yml
🚧 Files skipped from review as they are similar to previous changes (4)
  • .github/workflows/ci.yml
  • .github/workflows/bestaxbot-reply.yml
  • .github/workflows/visual-regression.yml
  • .github/workflows/claude-pr-loop.yml

Walkthrough

Adds a strict gh-thread GraphQL helper, routes Claude workflows through the pinned helper, and tightens CI and visual-regression permissions while preserving scoped screenshot publishing access.

Changes

Review-thread operations and workflow security

Layer / File(s) Summary
Review-thread CLI helper
.github/scripts/gh-thread.sh
Adds validated list, reply, and resolve commands backed by GitHub GraphQL operations.
Claude workflow thread integration
.github/workflows/bestaxbot-reply.yml, .github/workflows/claude-pr-loop.yml
Installs the helper from origin/main, removes direct gh api access from Claude tool allowlists, and updates prompts to use gh-thread.
Workflow credential restrictions
.github/workflows/ci.yml, .github/workflows/visual-regression.yml
Sets read-only defaults, disables checkout credential persistence, and scopes contents write access to the gated screenshot publishing job.

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

Sequence Diagram(s)

sequenceDiagram
  participant ClaudeAgent
  participant gh-thread
  participant GitHubGraphQL
  ClaudeAgent->>gh-thread: List, reply, or resolve review thread
  gh-thread->>GitHubGraphQL: Send review-thread query or mutation
  GitHubGraphQL-->>gh-thread: Return thread data or operation result
  gh-thread-->>ClaudeAgent: Return command output
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed but does not follow the required template and omits package, issue, type, checklist, screenshots, and context sections. Rewrite it using the repo template headings, including affected package(s), related issues, type of change, checklist, screenshots/demos, and additional context.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: workflow token hardening and routing AI thread operations through gh-thread.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/workflow-hardening

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

@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://5776f645.bestax.pages.dev

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

Deep review — 0 finding(s)

Item Severity Area Finding Location
— — — No blocking defects found. —

Overall: This is a tight, well-reasoned least-privilege hardening pass and it holds up under adversarial reading. I verified the gh-thread wrapper (fixed GraphQL surface, correct -F n int vs -f string typing, numeric-PR guard, owner/repo split), the origin/main staging (fetch-depth: 0 reliably creates refs/remotes/origin/main in the fix/verify/reply jobs — and for fork PRs that ref is the trusted base main, which is exactly the intended property), and the permission narrowing (no test-matrix job used the dropped pull-requests: write; publish and commit-screenshots re-declare precisely what they need; same-run download-artifact@v8 does not require actions: read). Every remaining raw gh api is in a trusted run: step, and all three session prompts were updated to gh-thread. The riskiest surface is the origin/main staging assumption — if fetch-depth: 0 ever regressed, every AI-loop job would fail closed at the stage step, which is the safe direction. The authors already flag the real residual gap (an injected agent still has node/pnpm run/sed locally), correctly scoping the PAT migration as separate follow-up.

🏄 Dude, this one is all about paddling the token power waaay back from the sketchy waves — untrusted thread text gets its own little gh-thread kiddie pool instead of the whole open ocean of gh api. Clean lines, no wipeouts, green light to send it. 🌊

@allxsmith

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/scripts/gh-thread.sh (1)

1-68: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not submit .github/** changes through the autonomous AI loop.

The repository policy says the autonomous loop refuses PRs modifying .github/**; this PR changes all three listed paths. Use the approved non-autonomous process or obtain an explicit policy exception before proceeding.

  • .github/scripts/gh-thread.sh#L1-L68: remove this path from the autonomous-loop PR.
  • .github/workflows/bestaxbot-reply.yml#L128-L140: remove this path from the autonomous-loop PR.
  • .github/workflows/claude-pr-loop.yml#L336-L347: remove this path from the autonomous-loop PR.

As per coding guidelines, “The autonomous AI loop refuses pull requests that modify .github/**.”

🤖 Prompt for 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.

In @.github/scripts/gh-thread.sh around lines 1 - 68, Remove the autonomous-loop
changes from .github/scripts/gh-thread.sh (lines 1-68),
.github/workflows/bestaxbot-reply.yml (lines 128-140), and
.github/workflows/claude-pr-loop.yml (lines 336-347); these .github/** paths
must not be included in the autonomous-loop PR. Use the approved non-autonomous
process or obtain an explicit policy exception instead.

Source: Coding guidelines

🤖 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/scripts/gh-thread.sh:
- Around line 40-46: Update the GraphQL query and surrounding gh-thread.sh
pagination flow to paginate both the reviewThreads and nested comments
connections, adding cursors and pageInfo and repeatedly requesting pages until
each connection is exhausted. Ensure later review threads and replies are
included while preserving the existing node fields and output handling.

In @.github/workflows/bestaxbot-reply.yml:
- Around line 134-139: Bootstrap .github/scripts/gh-thread.sh on the trusted
base branch before workflows depend on it, then update the staging steps in
.github/workflows/bestaxbot-reply.yml lines 134-139,
.github/workflows/claude-pr-loop.yml lines 342-346, and
.github/workflows/claude-pr-loop.yml lines 623-627 to load the helper from
origin/main only after it is available there.

In @.github/workflows/ci.yml:
- Around line 9-14: Remove the workflow modifications at
.github/workflows/ci.yml lines 9-14 and .github/workflows/visual-regression.yml
lines 23-29, unless an explicit exception to the repository’s .github/**
modification policy has been obtained; no other changes are requested.

In @.github/workflows/claude-pr-loop.yml:
- Around line 391-395: The model value in claude_args is invalid because it
includes the stray “[1m]” suffix. Update the model setting to use the existing
claude-opus-4-8 identifier, leaving the surrounding workflow arguments
unchanged.

---

Outside diff comments:
In @.github/scripts/gh-thread.sh:
- Around line 1-68: Remove the autonomous-loop changes from
.github/scripts/gh-thread.sh (lines 1-68), .github/workflows/bestaxbot-reply.yml
(lines 128-140), and .github/workflows/claude-pr-loop.yml (lines 336-347); these
.github/** paths must not be included in the autonomous-loop PR. Use the
approved non-autonomous process or obtain an explicit policy exception instead.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f7d157fc-95f5-4f86-8f95-09c064796e18

📥 Commits

Reviewing files that changed from the base of the PR and between b0139d2 and 6b4769a.

📒 Files selected for processing (5)
  • .github/scripts/gh-thread.sh
  • .github/workflows/bestaxbot-reply.yml
  • .github/workflows/ci.yml
  • .github/workflows/claude-pr-loop.yml
  • .github/workflows/visual-regression.yml

Comment thread .github/scripts/gh-thread.sh Outdated
Comment on lines +40 to +46
gh api graphql \
-f query='query($o:String!,$r:String!,$n:Int!){
repository(owner:$o,name:$r){pullRequest(number:$n){
reviewThreads(first:100){nodes{
id isResolved path line
comments(first:50){nodes{author{login} body url}}}}}}}' \
-f o="${REPO%/*}" -f r="${REPO#*/}" -F n="$PR"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

# The revised query should expose pageInfo/cursors for both paginated
# connections, rather than relying on only first:100 / first:50.
rg -n 'reviewThreads\(first:.*pageInfo|comments\(first:.*pageInfo' \
  .github/scripts/gh-thread.sh

Repository: allxsmith/bestax

Length of output: 154


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

sed -n '1,220p' .github/scripts/gh-thread.sh | cat -n

Repository: allxsmith/bestax

Length of output: 3109


🏁 Script executed:

rg -n '\bgh-thread list\b|gh-thread\b' .github

Repository: allxsmith/bestax

Length of output: 5580


Paginate both connections. .github/scripts/gh-thread.sh:40-46
reviewThreads(first:100) and comments(first:50) truncate the output, so later threads or replies can be missed. Add cursors/pageInfo and loop until both connections are exhausted.

🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 41-45: Expressions don't expand in single quotes, use double quotes for that.

(SC2016)

🤖 Prompt for 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.

In @.github/scripts/gh-thread.sh around lines 40 - 46, Update the GraphQL query
and surrounding gh-thread.sh pagination flow to paginate both the reviewThreads
and nested comments connections, adding cursors and pageInfo and repeatedly
requesting pages until each connection is exhausted. Ensure later review threads
and replies are included while preserving the existing node fields and output
handling.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 70749eb — gh api graphql --paginate --slurp now follows the reviewThreads cursor (output is an array of pages, documented in the usage header), and comments is raised to the per-thread API maximum of 100. Smoke-tested against a live PR.

Comment thread .github/workflows/bestaxbot-reply.yml
Comment thread .github/workflows/ci.yml
Comment on lines +9 to +14
# Read-only by default: the test jobs execute PR-authored code (pnpm install,
# tests, builds) — including AI-pushed claude/* branches — so the token they
# can reach must not be able to write. The publish job re-declares the write
# scopes semantic-release needs.
permissions:
contents: write
pull-requests: write
contents: read

@coderabbitai coderabbitai Bot Jul 15, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔴 Critical | 🏗️ Heavy lift

The PR violates the repository’s .github/** modification policy.

  • .github/workflows/ci.yml#L9-L14: obtain an explicit exception or remove this workflow change.
  • .github/workflows/visual-regression.yml#L23-L29: obtain an explicit exception or remove this workflow change.

As per coding guidelines, the autonomous AI loop refuses pull requests that modify .github/**.

📍 Affects 2 files
  • .github/workflows/ci.yml#L9-L14 (this comment)
  • .github/workflows/visual-regression.yml#L23-L29
🤖 Prompt for 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.

In @.github/workflows/ci.yml around lines 9 - 14, Remove the workflow
modifications at .github/workflows/ci.yml lines 9-14 and
.github/workflows/visual-regression.yml lines 23-29, unless an explicit
exception to the repository’s .github/** modification policy has been obtained;
no other changes are requested.

Source: Coding guidelines

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Refuting: the guideline being cited ("the autonomous AI loop refuses PRs that touch .github/**") is a constraint on the AI loop's own PRs — by design the loop cannot modify its own gates. This is a session PR authored by the repo owner, which is exactly the channel .github/** changes are supposed to go through (see CLAUDE.md: workflow changes are explicitly "session/human PR" work). No exception needed.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

@coderabbitai review

Comment thread .github/workflows/claude-pr-loop.yml
allxsmith added a commit that referenced this pull request Jul 15, 2026
CodeRabbit finding on #314: reviewThreads(first:100)/comments(first:50)
truncated silently. `gh api graphql --paginate --slurp` now follows the
thread cursor (output becomes an array of pages, documented in the
usage header); comments raised to the per-thread API maximum of 100.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://af0376f2.bestax.pages.dev

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

allxsmith and others added 2 commits July 14, 2026 21:19
…elper

Security hardening from the workflow prompt-injection review:

- claude-pr-loop (fix/verify) and bestaxbot-reply no longer allowlist
  Bash(gh api:*). These sessions ingest review-thread text that anyone on
  a public repo can reply into, and the fix/reply agents hold the
  bestaxbot PAT — raw gh api let a prompt-injected session write to
  anything the token could reach. Thread ops now go through
  .github/scripts/gh-thread.sh (list/reply/resolve only), staged onto
  PATH from origin/main so a checked-out PR branch can neither miss it
  nor substitute its own copy.
- ci.yml: workflow default drops contents:write + pull-requests:write to
  contents:read (the publish job already re-declares its own scopes);
  the jobs that execute PR-authored code no longer persist git
  credentials.
- visual-regression.yml: same read-only default — the matrix job runs
  scaffolded-app installs with --ignore-workspace, outside
  pnpm-workspace.yaml's supply-chain hardening — with contents:write
  moved down to the workflow_dispatch-gated commit-screenshots job.
CodeRabbit finding on #314: reviewThreads(first:100)/comments(first:50)
truncated silently. `gh api graphql --paginate --slurp` now follows the
thread cursor (output becomes an array of pages, documented in the
usage header); comments raised to the per-thread API maximum of 100.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

@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: 1

🤖 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/scripts/gh-thread.sh:
- Around line 19-21: Relocate the gh-thread helper out of .github/** to a
repository location permitted by the PR policy, then update every workflow or
script reference to the new path; alternatively, obtain and document an explicit
exception before merging. Preserve the helper’s behavior, including paginated
review-thread processing.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 049f8382-50a5-40fb-a611-14d734fe6868

📥 Commits

Reviewing files that changed from the base of the PR and between 6b4769a and 70749eb.

📒 Files selected for processing (1)
  • .github/scripts/gh-thread.sh

Comment thread .github/scripts/gh-thread.sh
@allxsmith
allxsmith force-pushed the security/workflow-hardening branch from 70749eb to 99c3091 Compare July 15, 2026 01:22
@github-actions

Copy link
Copy Markdown
Contributor

Preview Deployment

Preview URL: https://574dd638.bestax.pages.dev

@allxsmith
allxsmith merged commit 1c07347 into main Jul 15, 2026
22 checks passed
@allxsmith
allxsmith deleted the security/workflow-hardening branch July 15, 2026 01:46
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.3.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 5.7.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@bestax-release-bot

Copy link
Copy Markdown

🎉 This PR is included in version 1.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant