From adbd3b63e7e6a4b9e35d2d9d9940d2a7d748ab36 Mon Sep 17 00:00:00 2001 From: Pierre Merlet Date: Thu, 30 Jul 2026 14:53:43 +0200 Subject: [PATCH 1/4] ci(security): make the routine observable and stagger the schedules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First real runs: toolbelt and agent-ruby produced valid security PRs; forestadmin-server and forestadmin failed with '1 turn, $0.00, is_error: true' after ~180s (no token ever billed → the first API request never completed), and agent-nodejs was cancelled by hand after 28 minutes of zero visible output. - show_full_output: true — the action hides all agent output by default, making a working run indistinguishable from a hung one and the failures impossible to diagnose (secrets stay masked by GitHub) - stagger the five schedules 30 min apart: five concurrent Opus sessions on one API key, the two largest-context repos starting 53s apart, is the likely trigger of the unbilled retry loop Co-Authored-By: Claude Fable 5 --- .github/workflows/security-fixes.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/security-fixes.yml b/.github/workflows/security-fixes.yml index be3b6f53d..e976b54b6 100644 --- a/.github/workflows/security-fixes.yml +++ b/.github/workflows/security-fixes.yml @@ -13,7 +13,9 @@ name: Security fixes on: schedule: - - cron: "0 11 * * 4" # Thursdays 11:00 UTC — same slot as the former routine + # Staggered per repo: 5 concurrent Opus sessions on one API key + # tripped a rate limit on 2026-07-30 (first request never billed). + - cron: "0 13 * * 4" # Thursdays 13:00 UTC workflow_dispatch: {} permissions: @@ -54,6 +56,10 @@ jobs: uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + # Without this the action hides all agent output, so a failing or + # slow run is indistinguishable from a hung one (GitHub still masks + # secrets in logs). + show_full_output: true github_token: ${{ secrets.SECURITY_GH_PAT }} prompt: "Read the file .github/security-fixes-prompt.md in this repository and execute its instructions exactly, from Preflight through the end." claude_args: | From d6056308b2b4153d823c164190682327932e0371 Mon Sep 17 00:00:00 2001 From: Pierre Merlet Date: Thu, 30 Jul 2026 16:31:57 +0200 Subject: [PATCH 2/4] ci(security): bump checkout/setup-node to v6 (Node 20 deprecation) actions/checkout@v4 and actions/setup-node@v4 ship the Node 20 runtime, which runners now warn about and force onto Node 24. v6 targets Node 24 natively and is already used elsewhere in these repos. Co-Authored-By: Claude Fable 5 --- .github/workflows/security-fixes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/security-fixes.yml b/.github/workflows/security-fixes.yml index e976b54b6..d811bf1bd 100644 --- a/.github/workflows/security-fixes.yml +++ b/.github/workflows/security-fixes.yml @@ -36,7 +36,7 @@ jobs: BASH_DEFAULT_TIMEOUT_MS: "900000" BASH_MAX_TIMEOUT_MS: "3600000" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: token: ${{ secrets.SECURITY_GH_PAT }} From 20c71d089450678b865c7c83150b53c46dc3445c Mon Sep 17 00:00:00 2001 From: Pierre Merlet Date: Thu, 30 Jul 2026 16:53:21 +0200 Subject: [PATCH 3/4] fix(ci): put the review checkboxes in a task list, not in table cells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitHub renders '- [ ]' as an interactive checkbox only at the start of a list item. Inside a table cell it stays literal text ('- [ ]', verified against GitHub's own /markdown API), and a raw is stripped by the sanitizer — so the Done and Dismissed columns rendered as '- []' text in every row. The tables are now purely informational and the PR body ends with a 'Review checklist' section holding real task lists: fixes to verify, and alerts to dismiss. Co-Authored-By: Claude Fable 5 --- .github/security-fixes-prompt.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/security-fixes-prompt.md b/.github/security-fixes-prompt.md index d72adbf51..7768d2880 100644 --- a/.github/security-fixes-prompt.md +++ b/.github/security-fixes-prompt.md @@ -102,11 +102,18 @@ The PR description must include the sections listed below. Mark the **Validation **Alert references in the body must be full Markdown links, never bare `#`.** GitHub auto-links `#` to issues/PRs in the same repo, which sends readers to the wrong page. Format every alert reference as `[#](https://github.com/$REPO/security/dependabot/)` — in every table AND in any inline mentions. -**Tables that require human action must start with a checkbox column** so first-level support can tick off each alert as they handle it. Use cell content `- [ ]` (GitHub renders it as an interactive checkbox in PR bodies, even inside table cells). Apply this to: -- **Fixed** table: add a leading `Done` column (ticked once the reviewer has verified the fix landed) -- **Ignored** table: add a leading `Dismissed` column (ticked once the reviewer has dismissed the alert in the repo's Security tab) +**Actionable checkboxes go in a task list, never in table cells.** GitHub renders `- [ ]` as an interactive checkbox only when it starts a list item; inside a table cell it stays literal text, and a raw `` is stripped by GitHub's sanitizer. So the tables carry **no** checkbox column, and the body ends with a **Review checklist** section built from real task lists: -Deferred / Security pins added / Security pins removed / Could not auto-fix are informational — no checkbox column. +``` +## Review checklist + +**Fixes to verify** — tick once you have confirmed the bump landed: +- [ ] [#](https://github.com/$REPO/security/dependabot/) — `` + +**Alerts to dismiss** — tick once dismissed in the repo's Security tab (reason in the Ignored section): +- [ ] [#](https://github.com/$REPO/security/dependabot/) — `` +``` +Omit either list if it would be empty. Deferred / Security pins added / Security pins removed / Could not auto-fix need no checkboxes — they are informational. The very first line of the PR description must be the following blockquote so first-level support knows how to review it: @@ -123,6 +130,7 @@ The very first line of the PR description must be the following blockquote so fi - **Risks**: per bump, from the upstream CHANGELOG — breaking changes touching APIs we use, tests likely to need updating. If no behavior change beyond the patched vuln, say so. - **Manual testing**: only if automated CI doesn't cover the affected paths — give concrete reproduction steps. Otherwise write "Covered by CI." - **Validation**: `⏳ Awaiting CI` for now. +- **Review checklist**: the task lists described above (this is where the checkboxes live). **8. Monitor CI and fix failures.** Poll **inside a single Bash loop per tool call** (about 10 minutes of `sleep 60` iterations per call — do NOT spend one tool call per poll, that would exhaust the turn budget). Each iteration fetches the workflow runs and the combined commit status for the PR head SHA (Actions + Commit statuses APIs only — the Checks API is not accessible to fine-grained PATs; check runs posted by GitHub Apps such as coverage bots are therefore a blind spot of this monitoring): ``` From 539a66a3522fd126a05736f1057923bacb55a12e Mon Sep 17 00:00:00 2001 From: Pierre Merlet Date: Thu, 30 Jul 2026 16:58:00 +0200 Subject: [PATCH 4/4] fix(ci): drop show_full_output, surface only the terminal error on failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Macroscope is right that show_full_output publishes the whole agent transcript — tool results, file reads, command output — to logs any repo member can download, and GitHub only masks registered secret values. On toolbelt it would also copy the full Dependabot alert JSON into logs readable by people without security_events access. Its diagnostic value was real (it is what revealed the 401 on the two repos with a stale repo-level ANTHROPIC_API_KEY) but it is far wider than needed. Replaced by a failure-only step that extracts just the terminal error fields (subtype, is_error, terminal_reason, api_error_status, turns, cost, and the first 400 chars of the result string) from the execution log. Enough to tell an auth/API failure from an application one, without publishing the transcript. Also: the workflow-file push fallback now rewrites the commit (reset --soft + restore + re-commit) instead of merely unstaging — the files were already inside the single commit, so unstaging changed nothing and the retry was rejected identically. Co-Authored-By: Claude Fable 5 --- .github/security-fixes-prompt.md | 2 +- .github/workflows/security-fixes.yml | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/security-fixes-prompt.md b/.github/security-fixes-prompt.md index 7768d2880..c454e68be 100644 --- a/.github/security-fixes-prompt.md +++ b/.github/security-fixes-prompt.md @@ -41,7 +41,7 @@ A gem is a **direct** dependency if the owning package's gemspec (or its Gemfile Use bundler-generated resolution only for verification — no `Gemfile.lock` is ever committed in this repo. -For **npm-ecosystem** alerts (the tooling `package.json`/`yarn.lock` at the root): fix with yarn (`yarn why `, bump or root-`resolutions` pin bounded to the parent-compatible major, `yarn install` to update the committed `yarn.lock`). For **github-actions-ecosystem** alerts: bump the `uses:` reference in the affected `.github/workflows/*.yml`; `SECURITY_GH_PAT` carries the `workflows:write` permission this push requires — if the push is nevertheless rejected citing workflow permissions, unstage those changes, move the alerts to "Could not auto-fix (token lacks Workflows permission)", and ship the rest. +For **npm-ecosystem** alerts (the tooling `package.json`/`yarn.lock` at the root): fix with yarn (`yarn why `, bump or root-`resolutions` pin bounded to the parent-compatible major, `yarn install` to update the committed `yarn.lock`). For **github-actions-ecosystem** alerts: bump the `uses:` reference in the affected `.github/workflows/*.yml`; `SECURITY_GH_PAT` carries the `workflows:write` permission this push requires. If the push is nevertheless rejected citing workflow permissions, the files are already **inside the commit** — unstaging alone changes nothing. Rewrite the commit without them (`git reset --soft HEAD~1`, `git restore --staged --worktree .github/workflows/`, re-commit with the same message), then push again, move those alerts to "Could not auto-fix (token lacks Workflows permission)", adjust the Summary counts, and ship the rest. **5. Audit existing security pins.** Sweep **every** `Gemfile` (root and `packages/*/Gemfile`) for entries carrying a `# security pin` comment (or clearly pin-only entries not required by the code): - **Stale** — the gem no longer appears in that package's dependency tree at all. Remove the entry, re-resolve in that package (delete its local lockfile, `bundle lock`), confirm. diff --git a/.github/workflows/security-fixes.yml b/.github/workflows/security-fixes.yml index d811bf1bd..613cb248e 100644 --- a/.github/workflows/security-fixes.yml +++ b/.github/workflows/security-fixes.yml @@ -56,10 +56,6 @@ jobs: uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} - # Without this the action hides all agent output, so a failing or - # slow run is indistinguishable from a hung one (GitHub still masks - # secrets in logs). - show_full_output: true github_token: ${{ secrets.SECURITY_GH_PAT }} prompt: "Read the file .github/security-fixes-prompt.md in this repository and execute its instructions exactly, from Preflight through the end." claude_args: | @@ -67,3 +63,18 @@ jobs: --max-turns 250 --allowedTools "Bash,Read,Write,Edit,Glob,Grep,WebFetch,WebSearch" --append-system-prompt "This is an unattended scheduled maintenance workflow. You are explicitly authorized to create and switch to the security/ branch, commit, push it to origin, and create, update and label the pull request yourself via the GitHub REST API with the token in the GH_PAT environment variable, exactly as the instructions file specifies. Never defer pushing or PR creation to a human, never stop to ask for confirmation, and do not use any built-in branch or PR helper that would override these instructions." + + # The action redacts the agent transcript on purpose (it can contain + # secrets read from files or command output). Instead of enabling + # show_full_output, surface only the terminal error fields on failure — + # enough to tell an API/auth error from an application one. + - name: Surface the agent's terminal error (no transcript) + if: failure() + run: | + F=/home/runner/work/_temp/claude-execution-output.json + [ -f "$F" ] || { echo "No execution log at $F"; exit 0; } + jq -r '[.. | objects | select(.type == "result")] | last + | {subtype, is_error, terminal_reason, api_error_status, + num_turns, total_cost_usd, + result: ((.result // "") | tostring | .[0:400])}' "$F" \ + || echo "Could not parse the execution log"