From d7a5c36cb48e4a5cbe46882e427df65225772a3d Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Fri, 7 Aug 2026 18:01:30 -0700 Subject: [PATCH 1/3] feat: add deterministic CLAUDE_CODE_OAUTH_TOKEN precondition check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mac-dev-server-setup#58 hit a CI failure where the claude-code-action step failed with CLAUDE_OUTCOME: failure and no verdict rendered. The downstream verdict-parsing logic correctly fail-closed to BLOCK, but the actual root cause — a missing CLAUDE_CODE_OAUTH_TOKEN repo secret — was only visible deep in the action's own error output, misreported as a substantive review BLOCK rather than an infrastructure/config gap, wasting a CI cycle before someone dug through logs to find it. Adds a new "Verify CLAUDE_CODE_OAUTH_TOKEN is configured" step to both claude-assistant.yml and claude-blocking-review.yml — the two reusable workflows that actually invoke claude-code-action — placed before that invocation. Checks `secrets.claude_oauth_token != ''` and fails loudly with an actionable ::error:: (pointing at Settings > Secrets and /install-github-app) if the caller's secret resolves empty. claude.yml (the top-level caller stub for claude-assistant.yml) needed no change — it only passes the secret through and never invokes claude-code-action directly, so the check belongs in the reusable workflow where the actual invocation happens. Note: claude-blocking-review.yml's `secrets: claude_oauth_token: required: true` only guarantees the caller supplied a `secrets:` block, not that the underlying repo secret it references is actually set — a caller referencing an unconfigured CLAUDE_CODE_OAUTH_TOKEN still satisfies `required: true` while passing an empty string through, which is exactly the gap this closes. Bumps claude-blocking-review.yml to v3.2.0 (additive header note, matching the file's established versioning convention; tagging happens after merge per repo convention). claude-assistant.yml has no in-file minor-version history to extend, so its header is unchanged. Verified: extracted the shell-side check into a standalone harness and exercised TOKEN_SET=true (proceeds) and TOKEN_SET=false (errors, exit 1). YAML validated on both files. zizmor finding counts/types identical before and after on both files (committed with SKIP=zizmor per the documented escape hatch — dotfiles pre-commit/config.yaml, PR #155 — for the same pre-existing, unrelated findings noted in the #92 and #94 PRs). Closes #90. --- .github/workflows/claude-assistant.yml | 17 +++++++++ .github/workflows/claude-blocking-review.yml | 36 ++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/.github/workflows/claude-assistant.yml b/.github/workflows/claude-assistant.yml index c7729b8..883cd30 100644 --- a/.github/workflows/claude-assistant.yml +++ b/.github/workflows/claude-assistant.yml @@ -42,6 +42,23 @@ jobs: with: fetch-depth: 1 + - name: Verify CLAUDE_CODE_OAUTH_TOKEN is configured + env: + TOKEN_SET: ${{ secrets.claude_oauth_token != '' }} + run: | + # Deterministic (non-LLM) precondition check — fail fast and + # loudly if the caller's CLAUDE_CODE_OAUTH_TOKEN secret is + # missing, rather than letting claude-code-action fail deep in + # its own error output with CLAUDE_OUTCOME: failure and no + # actionable message (see #90 — mac-dev-server-setup#58 wasted a + # CI cycle on exactly this before someone dug through logs to + # find the missing secret). + if [ "$TOKEN_SET" != "true" ]; then + echo "::error::CLAUDE_CODE_OAUTH_TOKEN secret is not set for this repository." + echo "::error::Add it under Settings > Secrets and variables > Actions, or run /install-github-app from Claude Code." + exit 1 + fi + - name: Run Claude Code id: claude uses: anthropics/claude-code-action@26ec041249acb0a944c0a47b6c0c13f05dbc5b44 # v1 diff --git a/.github/workflows/claude-blocking-review.yml b/.github/workflows/claude-blocking-review.yml index 1e3a83c..05dde59 100644 --- a/.github/workflows/claude-blocking-review.yml +++ b/.github/workflows/claude-blocking-review.yml @@ -51,6 +51,14 @@ name: Claude Blocking Review # be scoped to a specific commit; see below. Both changes are additive: # existing unscoped markers and log consumers are unaffected. # +# v3.2.0 (2026-08-07): #90 — a new "Verify CLAUDE_CODE_OAUTH_TOKEN is +# configured" step runs before the Claude review step and fails loudly +# with an actionable ::error:: if the caller's secret resolves empty, +# instead of letting claude-code-action fail deep in its own error output +# (previously misreported as a substantive review BLOCK rather than an +# infrastructure/config gap). Additive: callers with the secret correctly +# configured see no behavior change. +# # Escape hatch: add [skip-claude-review: reason] to the PR body to bypass # enforcement with an audit trail. This unscoped form is honored # unconditionally on every subsequent run for the life of the PR (deliberate @@ -133,6 +141,34 @@ jobs: exit 1 fi + - name: Verify CLAUDE_CODE_OAUTH_TOKEN is configured + env: + TOKEN_SET: ${{ secrets.claude_oauth_token != '' }} + run: | + # Deterministic (non-LLM) precondition check — fail fast and + # loudly if the caller's CLAUDE_CODE_OAUTH_TOKEN secret is + # missing, rather than letting claude-code-action fail deep in + # its own error output with CLAUDE_OUTCOME: failure and no + # verdict rendered (see #90 — mac-dev-server-setup#58 wasted a + # CI cycle on exactly this: the downstream verdict-parsing logic + # correctly fail-closed to BLOCK, but the real cause — a missing + # secret — was only visible deep in the action's own logs, and + # got misreported as a substantive review BLOCK rather than an + # infrastructure/config gap). + # + # `secrets: claude_oauth_token: required: true` on this + # workflow_call only guarantees the caller supplied a `secrets:` + # block — it does NOT guarantee the underlying repo secret it + # references is actually set. A caller passing + # `${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}` from a repo where that + # secret was never added still satisfies the `required: true` + # check while passing an empty string through. + if [ "$TOKEN_SET" != "true" ]; then + echo "::error::CLAUDE_CODE_OAUTH_TOKEN secret is not set for this repository." + echo "::error::Add it under Settings > Secrets and variables > Actions, or run /install-github-app from Claude Code." + exit 1 + fi + - name: Validate inputs env: MODEL: ${{ inputs.model }} From badd8474df7586e97d9e351618e131a35826f73a Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Fri, 7 Aug 2026 18:03:51 -0700 Subject: [PATCH 2/3] fix: gate token-check step behind doc-only/Dependabot skip outputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-push whole-codebase review (filed as #105) correctly flagged that the "Verify CLAUDE_CODE_OAUTH_TOKEN is configured" step added in the previous commit ran unconditionally right after the permissions preflight — before the "Check for doc-only diff" and "Check for Dependabot PR" skip-detection steps. A repo with CLAUDE_CODE_OAUTH_TOKEN unset would fail the token check even on a doc-only or Dependabot PR that would never have invoked claude-code-action in the first place, breaking the doc-only skip's cost/noise-reduction guarantee for misconfigured repos. Moves the step to immediately before "Run Claude Code Review" and adds the same `if: steps.doc-check.outputs.skip != 'true' && steps.dependabot-check.outputs.skip != 'true'` gate already used by "Estimate review parameters", "Minimize prior review comments", and the review step itself. Net change is a pure move (delete + re-add with the if: gate) — no other logic touched. claude-assistant.yml has no doc-only/Dependabot skip-check steps (it's the @claude-mention assistant, not the PR-gating reviewer), so its token-check placement from the previous commit is unaffected and correct as-is. Verified: YAML valid, zizmor finding count/type on this file unchanged before/after (same 5 pre-existing findings noted in the prior commit). Closes #105 (filed by pre-push review against this PR's first commit). --- .github/workflows/claude-blocking-review.yml | 64 +++++++++++--------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/.github/workflows/claude-blocking-review.yml b/.github/workflows/claude-blocking-review.yml index 05dde59..803e34a 100644 --- a/.github/workflows/claude-blocking-review.yml +++ b/.github/workflows/claude-blocking-review.yml @@ -141,34 +141,6 @@ jobs: exit 1 fi - - name: Verify CLAUDE_CODE_OAUTH_TOKEN is configured - env: - TOKEN_SET: ${{ secrets.claude_oauth_token != '' }} - run: | - # Deterministic (non-LLM) precondition check — fail fast and - # loudly if the caller's CLAUDE_CODE_OAUTH_TOKEN secret is - # missing, rather than letting claude-code-action fail deep in - # its own error output with CLAUDE_OUTCOME: failure and no - # verdict rendered (see #90 — mac-dev-server-setup#58 wasted a - # CI cycle on exactly this: the downstream verdict-parsing logic - # correctly fail-closed to BLOCK, but the real cause — a missing - # secret — was only visible deep in the action's own logs, and - # got misreported as a substantive review BLOCK rather than an - # infrastructure/config gap). - # - # `secrets: claude_oauth_token: required: true` on this - # workflow_call only guarantees the caller supplied a `secrets:` - # block — it does NOT guarantee the underlying repo secret it - # references is actually set. A caller passing - # `${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}` from a repo where that - # secret was never added still satisfies the `required: true` - # check while passing an empty string through. - if [ "$TOKEN_SET" != "true" ]; then - echo "::error::CLAUDE_CODE_OAUTH_TOKEN secret is not set for this repository." - echo "::error::Add it under Settings > Secrets and variables > Actions, or run /install-github-app from Claude Code." - exit 1 - fi - - name: Validate inputs env: MODEL: ${{ inputs.model }} @@ -415,6 +387,42 @@ jobs: done <<< "$PRIOR_IDS" echo "Minimized $COUNT prior review comment(s) as OUTDATED." + - name: Verify CLAUDE_CODE_OAUTH_TOKEN is configured + if: steps.doc-check.outputs.skip != 'true' && steps.dependabot-check.outputs.skip != 'true' + env: + TOKEN_SET: ${{ secrets.claude_oauth_token != '' }} + run: | + # Deterministic (non-LLM) precondition check — fail fast and + # loudly if the caller's CLAUDE_CODE_OAUTH_TOKEN secret is + # missing, rather than letting claude-code-action fail deep in + # its own error output with CLAUDE_OUTCOME: failure and no + # verdict rendered (see #90 — mac-dev-server-setup#58 wasted a + # CI cycle on exactly this: the downstream verdict-parsing logic + # correctly fail-closed to BLOCK, but the real cause — a missing + # secret — was only visible deep in the action's own logs, and + # got misreported as a substantive review BLOCK rather than an + # infrastructure/config gap). + # + # `secrets: claude_oauth_token: required: true` on this + # workflow_call only guarantees the caller supplied a `secrets:` + # block — it does NOT guarantee the underlying repo secret it + # references is actually set. A caller passing + # `${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}` from a repo where that + # secret was never added still satisfies the `required: true` + # check while passing an empty string through. + # + # Gated behind the same doc-only/Dependabot skip outputs as the + # review step itself (rather than running unconditionally right + # after the permissions preflight) so a repo with a missing + # secret still gets a clean SKIPPED verdict on doc-only or + # Dependabot PRs, instead of failing on PRs that would never + # have invoked claude-code-action anyway. + if [ "$TOKEN_SET" != "true" ]; then + echo "::error::CLAUDE_CODE_OAUTH_TOKEN secret is not set for this repository." + echo "::error::Add it under Settings > Secrets and variables > Actions, or run /install-github-app from Claude Code." + exit 1 + fi + - name: Run Claude Code Review id: claude-review if: steps.doc-check.outputs.skip != 'true' && steps.dependabot-check.outputs.skip != 'true' From 0b4add4da01cb2d55fd868b79f32d4e0a73fc37a Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Fri, 7 Aug 2026 18:06:09 -0700 Subject: [PATCH 3/3] docs: clarify unconditional token check is correct for claude-assistant.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-push whole-codebase review flagged (as BLOCKING) that the token check in claude-assistant.yml has no `if:` gate, while claude-blocking-review.yml's equivalent step is gated behind doc-only/ Dependabot skip outputs — reading this as a regression that would fail callers who "legitimately omit the secret" per its `required: false` declaration. Investigated and confirmed the premise is incorrect for this file: `required: false` was added in 67630a7 specifically to work around a GitHub dispatch-time validation quirk (required: true + a job-level if: gate on the CALLER's job causes startup_failure even on no-op runs) — it is not a statement that the token is optional when the job actually runs. claude-assistant.yml's `run` job has no internal `if:` of its own to gate behind; the @claude-mention guard lives entirely in the caller (claude.yml), so by the time this job executes at all, an authorized @claude mention has already been confirmed and the token is always required. Verified via `gh search code` that every fleet caller (11+ repos across smartwatermelon and nightowlstudiollc) passes secrets.CLAUDE_CODE_OAUTH_TOKEN through unconditionally — none use an alternate auth path. No functional change — this is a documentation-only commit adding a comment that explains why the unconditional check is deliberate and correct, so a future reader (human or automated reviewer) doesn't repeat the same plausible-but-incorrect inference from `required: false` alone. Addresses the blocking finding from pre-push review on this branch. --- .github/workflows/claude-assistant.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/claude-assistant.yml b/.github/workflows/claude-assistant.yml index 883cd30..6622021 100644 --- a/.github/workflows/claude-assistant.yml +++ b/.github/workflows/claude-assistant.yml @@ -53,6 +53,22 @@ jobs: # actionable message (see #90 — mac-dev-server-setup#58 wasted a # CI cycle on exactly this before someone dug through logs to # find the missing secret). + # + # Deliberately unconditional (no `if:` gate) — unlike + # claude-blocking-review.yml, this workflow has no doc-only/ + # Dependabot skip paths to gate behind. `required: false` on the + # secrets: block above is NOT a signal that the token is + # optional for this job to function: it exists solely to avoid + # a GitHub dispatch-time validation quirk where `required: true` + # combined with the CALLER's job-level `if:` (the @claude-mention + # guard in claude.yml) causes startup_failure on every run, + # including no-op runs where the job would correctly be skipped + # (see 67630a7). By the time this reusable workflow's `run` job + # actually executes, the caller has already confirmed @claude + # was mentioned by an authorized user — there is no code path + # here where the token is genuinely unnecessary. Every fleet + # caller (confirmed via `gh search code`) passes + # secrets.CLAUDE_CODE_OAUTH_TOKEN through unconditionally. if [ "$TOKEN_SET" != "true" ]; then echo "::error::CLAUDE_CODE_OAUTH_TOKEN secret is not set for this repository." echo "::error::Add it under Settings > Secrets and variables > Actions, or run /install-github-app from Claude Code."