Skip to content

chore(release): promote next to production (2026-06-25)#542

Merged
wkoutre merged 38 commits into
mainfrom
release/next-to-main-20260625-180504
Jun 25, 2026
Merged

chore(release): promote next to production (2026-06-25)#542
wkoutre merged 38 commits into
mainfrom
release/next-to-main-20260625-180504

Conversation

@hello-happy-puppy

@hello-happy-puppy hello-happy-puppy commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

Production Deployment

This PR promotes the next branch to main for production release.

Commits included (38):

6e50003 Merge remote-tracking branch 'origin/next' into nickkoutrelakos/sync-main-to-next
9bde236 chore(sync): merge main into next to resolve promotion drift
a1dd445 chore(release): @uniswap/ai-toolkit-notion-publisher@0.0.19-next.0
b2e9047 chore(release): @uniswap/ai-toolkit-linear-task-utils@0.0.22-next.0
f62f9f0 chore(release): @uniswap/ai-toolkit-claude-mcp-helper@1.0.25-next.0
d1bfbf0 chore(release): @uniswap/ai-toolkit-nx-claude@0.5.38-next.1
b259724 Merge pull request #536 from Uniswap/feat/skill-management-plugin
999bfc2 Merge branch 'next' into feat/skill-management-plugin
7978a9b Merge pull request #526 from Uniswap/chore/model-update-opus-4-8-20260601
388a753 Merge pull request #525 from Uniswap/nickkoutrelakos/security-scan-oauth-fixes
53c0c91 fix(security): inline OAuth cookie flags for SAST visibility; drop speculative XSS suppressions
2781d57 fix(security): suppress false-positive direct-response-write XSS findings
c9176a1 fix(security): bind OAuth state to initiating browser (double-submit cookie)
06b996c fix(security): close 6 OAuth backend + nx-claude script scan findings
2e13ea0 chore(plugins): bump plugin versions for claude-opus-4-8 model update
412d0cd chore: update model references to claude-opus-4-8
92541aa Merge pull request #539 from Uniswap/nickkoutrelakos/remove-oauth-scope-logging
6f189a5 Merge branch 'next' into nickkoutrelakos/remove-oauth-scope-logging
f886fd7 chore(slack-oauth-backend): remove OAuth scope diagnostic logging
8364bc8 chore(deps): bump claude-code-action to v1.0.133 and model versions (#527)
6c0d982 feat(rules): add response-links-summary output convention (#537)
c42a517 fix(skill-management): trigger PR hook on gt submit/gt ss, not gt create
b458487 fix(workflows): harden reusable Claude workflows vs silent failures (#524)
a10b8f8 feat(skill-management): add skill-management plugin
882b69e docs(create-pr): read live PR body before editing in update mode (#531)
31c6c56 feat(development-pr-workflow): add /backtest-change command (#532)
27e7e2d fix(slack-oauth-backend): use fresh bot token, drop SLACK_BOT_TOKEN (#523)
8356485 fix(workflows): tighten bullfrog allowlists per workflow (#516)
776e81f fix(workflows): allowlist toolkit_ref to block fork-branch RCE (#510)
dab0e80 fix(workflows): set BUN_CONFIG_FILE=/dev/null in remaining workflows (#511)
09491fb fix(claude-task-worker): harden against prompt injection (#514)
c431f5b fix(workflows): replace sed-with-pipe-delimiter with python str.replace (#513)
0e09ace chore(sync): [skip ci] sync next with main
07fdbd9 chore(sync): [skip ci] sync next with main
80ac06e chore(slack-oauth-backend): log OAuth request and Slack-granted scopes (#520)
7d0d6a3 chore(sync): [skip ci] sync next with main
a48592e fix(slack-oauth-backend): drop incoming-webhook bot scope (#518)
e1f1cf6 feat(slack-oauth-backend): expand OAuth scope requests to match app manifest (#517)

Merge Strategy

Using merge commit to preserve full commit history for changelog generation.

Temporary Branch

This PR is created from a temporary branch release/next-to-main-20260625-180504 that will be deleted after merge.


This PR was automatically created by the Update Production workflow.


Changes Summary

✨ Features (4)

  • Add skill-management plugin for skill discovery and management
  • Add /backtest-change command to development-pr-workflow
  • Add response-links-summary output convention to rules
  • Expand OAuth scope requests to match app manifest

🐛 Bug Fixes (10)

  • Inline OAuth cookie flags for SAST visibility; drop speculative XSS suppressions
  • Suppress false-positive direct-response-write XSS findings
  • Bind OAuth state to initiating browser using double-submit cookie pattern
  • Close 6 OAuth backend and nx-claude script scan findings
  • Trigger skill-management PR hook on gt submit/gt ss, not gt create
  • Harden reusable Claude workflows against silent failures
  • Use fresh bot token and drop SLACK_BOT_TOKEN in slack-oauth-backend
  • Tighten bullfrog allowlists per workflow
  • Allowlist toolkit_ref to block fork-branch RCE
  • Harden claude-task-worker against prompt injection

📝 Documentation (1)

  • Read live PR body before editing in update mode for create-pr

🔧 Maintenance (11)

  • Remove OAuth scope diagnostic logging from slack-oauth-backend
  • Bump claude-code-action to v1.0.133 and update model versions
  • Update model references to claude-opus-4-8
  • Bump plugin versions for claude-opus-4-8 model update
  • Set BUN_CONFIG_FILE=/dev/null in remaining workflows
  • Replace sed-with-pipe-delimiter with python str.replace in workflows
  • Drop incoming-webhook bot scope from slack-oauth-backend
  • Sync next with main (multiple syncs)
  • Log OAuth request and Slack-granted scopes for diagnostics

Full Commit List

6e50003 Merge remote-tracking branch 'origin/next' into nickkoutrelakos/sync-main-to-next
9bde236 chore(sync): merge main into next to resolve promotion drift
b259724 Merge pull request #536 from Uniswap/feat/skill-management-plugin
999bfc2 Merge branch 'next' into feat/skill-management-plugin
7978a9b Merge pull request #526 from Uniswap/chore/model-update-opus-4-8-20260601
388a753 Merge pull request #525 from Uniswap/nickkoutrelakos/security-scan-oauth-fixes
53c0c91 fix(security): inline OAuth cookie flags for SAST visibility; drop speculative XSS suppressions
2781d57 fix(security): suppress false-positive direct-response-write XSS findings
c9176a1 fix(security): bind OAuth state to initiating browser (double-submit cookie)
06b996c fix(security): close 6 OAuth backend + nx-claude script scan findings
2e13ea0 chore(plugins): bump plugin versions for claude-opus-4-8 model update
412d0cd chore: update model references to claude-opus-4-8
92541aa Merge pull request #539 from Uniswap/nickkoutrelakos/remove-oauth-scope-logging
6f189a5 Merge branch 'next' into nickkoutrelakos/remove-oauth-scope-logging
f886fd7 chore(slack-oauth-backend): remove OAuth scope diagnostic logging
8364bc8 chore(deps): bump claude-code-action to v1.0.133 and model versions (#527)
6c0d982 feat(rules): add response-links-summary output convention (#537)
c42a517 fix(skill-management): trigger PR hook on gt submit/gt ss, not gt create
b458487 fix(workflows): harden reusable Claude workflows vs silent failures (#524)
a10b8f8 feat(skill-management): add skill-management plugin
882b69e docs(create-pr): read live PR body before editing in update mode (#531)
31c6c56 feat(development-pr-workflow): add /backtest-change command (#532)
27e7e2d fix(slack-oauth-backend): use fresh bot token, drop SLACK_BOT_TOKEN (#523)
8356485 fix(workflows): tighten bullfrog allowlists per workflow (#516)
776e81f fix(workflows): allowlist toolkit_ref to block fork-branch RCE (#510)
dab0e80 fix(workflows): set BUN_CONFIG_FILE=/dev/null in remaining workflows (#511)
09491fb fix(claude-task-worker): harden against prompt injection (#514)
c431f5b fix(workflows): replace sed-with-pipe-delimiter with python str.replace (#513)
0e09ace chore(sync): [skip ci] sync next with main
07fdbd9 chore(sync): [skip ci] sync next with main
80ac06e chore(slack-oauth-backend): log OAuth request and Slack-granted scopes (#520)
7d0d6a3 chore(sync): [skip ci] sync next with main
a48592e fix(slack-oauth-backend): drop incoming-webhook bot scope (#518)
e1f1cf6 feat(slack-oauth-backend): expand OAuth scope requests to match app manifest (#517)

wkoutre and others added 30 commits May 28, 2026 18:23
…anifest (#517)

Sync the bot and user scope arrays in createOAuthHandler() with the
current Slack app manifest so granted tokens carry all configured
permissions.

Bot scopes: 14 to 15 (adds incoming-webhook).
User scopes: 14 to 39 (adds search:read family, canvases, bookmarks,
links, reminders, pins, files read/write, emoji:read, channels:write,
groups:write, mpim:write, users:read.email).

Existing tokens retain their previous narrower scopes; users must
re-authorize to receive the new permissions.
The scope was added in PR #517 to mirror the Slack app manifest, but
the OAuth callback handler discards tokenResponse.incoming_webhook
(only forwards team, enterprise, scopes, bot_user_id). The app does
not consume webhook URLs, so requesting the scope adds a channel-
picker step to install for no functional benefit.

Manifest should be updated separately to remove incoming-webhook from
oauth_config.scopes.bot so the two stay aligned.
#520)

* chore(slack-oauth-backend): log OAuth request scopes and Slack-granted scopes

Adds two diagnostic logger.info calls to surface what we ask Slack for
and what Slack returns on each install, so future scope-grant mismatches
can be debugged against Vercel logs instead of trial-and-error reinstalls.

handler.ts (after exchangeCodeForToken): logs botScopesGranted,
userScopesGranted, presence flags for bot/user access and refresh
tokens, token types, team id, enterprise id. Token values themselves
are not logged.

routes/oauth.ts (in /authorize handler): logs the full redirect URL
including scope and user_scope query params, so we can verify the
request side independently of Slack's response.

Temporary instrumentation for the ongoing org-install debugging; safe
to leave in but a follow-up can prune if these become noisy.

* chore(slack-oauth-backend): scope-only log + drop "Debug" framing

Addresses two review comments on #520:

routes/oauth.ts: parse the authorize URL and log only the scope,
user_scope, and redirect_uri params instead of the full URL. The full
URL contains the `state` CSRF nonce, which would otherwise leak into
Vercel logs and weaken the CSRF protection validated on callback.

handler.ts + routes/oauth.ts: drop the "Debug:" framing from both log
comments. The logs fire at info level in production and the data they
capture (scope strings, presence flags) is permanent OAuth observability,
not temporary instrumentation, so the comments now reflect that intent.
…ce (#513)

_claude-docs-check.yml's Build Docs Check Prompt step substitutes 8
placeholders into the prompt template via `sed -i "s|\${VAR}|$VALUE|g"`
with `|` as the sed delimiter. The escape applied to $CHANGED_FILES
on line 520 is `sed 's/[&/\]/\\&/g'` — it escapes `&`, `/`, `\` but
NOT `|` (the actual delimiter).

A filename in $CHANGED_FILES containing `|` (legal on Linux; not
quoted by git's default core.quotepath since `|` is in the printable
ASCII range) breaks the substitution into garbage flags and causes
the docs-check workflow to fail or write a partial value into the
prompt. PR authors can introduce such filenames via fork PRs.

Replace the 8 sed -i calls with a single python str.replace pass.
Python's str.replace is plain-text and immune to this entire class
of delimiter-injection bugs.

Discovered during the bug bounty review that produced
Uniswap/default-token-list#2484 and #509.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(workflows): harden Claude session in task worker (drop --dangerously-skip-permissions, bullfrog block)

The autonomous task worker had two structural gaps that any prompt-
injection bug would weaponize:

1. Claude ran with `--dangerously-skip-permissions`, meaning every tool
   call was approved automatically. Combined with the absence of a
   file-allowlist post-flight check (see the separate Validate-changed-
   files PR), a prompt-injected session could call any tool unrestricted.

2. bullfrog egress-policy was `audit` (log-only). Network calls to
   attacker-controlled endpoints would be logged but not blocked.

This patch:

a) Replaces `--dangerously-skip-permissions` with an explicit
   `--allowedTools` list. Without an allowlist Claude prompts on every
   tool call (no human to approve in CI, so the job stalls); with the
   explicit allowlist the worker can still operate but a hijacked
   session cannot call arbitrary tools (curl, wget for exfil; dangerous
   mcp__linear__ operations like delete_*; etc.). Refine the list as
   legitimate tasks reveal additional needed tools.

b) Switches bullfrog from `egress-policy: audit` to `egress-policy:
   block` with an allowlist matching the other ai-toolkit workflows
   that already run in block mode (_claude-main.yml,
   _claude-code-review.yml, _claude-docs-check.yml,
   _generate-pr-metadata.yml), plus api.linear.app for the Linear MCP
   server this worker uses.

A follow-up that this patch does NOT address: the GitHub App's
installation permissions are configured server-side and are out of
scope for a code PR. Audit the App's scopes in the GitHub UI and
reduce to the minimum needed (contents:write, pull_requests:write,
issues:read).

Discovered during the bug bounty review that produced
Uniswap/default-token-list#2484 and #509.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(workflows): tighten bullfrog allowlist to aitk-worker-specific empirically-derived hosts

The previous commit shipped a 10-entry allowlist matching the dtl
worker's set, but the aitk Task Worker has different needs (uses
registry.npmjs.org as its npm registry, not registry.yarnpkg.com)
and the shared list included entries unused in practice.

Per-workflow analysis based on 15 audit-mode runs of
_generate-pr-metadata.yml (the only ai-toolkit workflow with recent
runs and audit logs showing the same setup-bun + bun install pattern)
plus reading this worker's code:

  Setup-phase empirical:
    - registry.npmjs.org      (341 audit lines across 15 sister runs)
    - release-assets.githubusercontent.com  (covered by *.githubusercontent.com)

  Runtime (invisible to audit per agent/queue_audit.nft — DNS reply
  queue means cached resolutions emit no further events):
    - api.anthropic.com       (claude-code-action)
    - api.linear.app          (mcp__linear__ tools at runtime)

  Default-allowed by bullfrog (agent/agent.go:18 defaultDomains):
    - github.com, api.github.com — silently allowed

Dropped vs the previous shared allowlist:

  - github.com / *.github.com   — covered by defaultDomains
  - bun.sh                       — 0 audit lines in 15 setup-bun runs;
                                   the binary comes from github releases
                                   via *.githubusercontent.com
  - claude.ai / *.claude.ai      — OAuth path only; this worker uses
                                   ANTHROPIC_API_KEY

Final allowlist: 5 entries (down from 10), each empirically justified
or required by code that audit logs can't observe.

Sister PR Uniswap/default-token-list#2485 makes the analogous
tightening for the dtl Task Worker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(claude-task-worker): clarify allowlist limits + add Bash(rg:*)

Rewords the comment above allowedTools to stop overstating what the
list contains. node/python3/bunx/npx are all arbitrary code execution;
the real containment is the bullfrog egress block above. Documents
that explicitly so a future maintainer doesn't loosen the bullfrog
allowlist assuming the tool allowlist constrains exfiltration — it
does not.

Also adds Bash(rg:*) preemptively per the PR's own 'add the tool
here' guidance: Claude's default search flows prefer ripgrep and would
otherwise stall on a permission prompt mid-task.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…511)

* fix(workflows): set BUN_CONFIG_FILE=/dev/null to neutralize bunfig.toml.preload RCE

`bun run`, `bun install`, and `bun x` auto-load bunfig.toml from CWD.
The bunfig.toml.preload array executes arbitrary code before the
intended script. When a workflow checks out PR head content (any of
our reusable workflows that take a pr_number input do this), `bun run`
loads bunfig.toml from the checkout root, and a malicious preload
entry executes with the caller's secrets.

Disable bunfig.toml loading by setting BUN_CONFIG_FILE=/dev/null at
the workflow level. This propagates to all jobs and steps and covers
every `bun *` invocation in the workflow.

Affected files (all use `bun run`/`bun install` after checkout):
- _claude-docs-check.yml
- _claude-code-review.yml
- _claude-task-worker.yml
- ci-pr-checks.yml
- publish-packages.yml

Discovered during the bug bounty review that produced
Uniswap/default-token-list#2484 and #509 (semgrep
flagged 8+ sites; this fix is uniform across all of them).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(workflows): also prefix BUN_CONFIG_FILE=/dev/null inline on bun run

Belt-and-suspenders: job env already sets BUN_CONFIG_FILE=/dev/null, but
pinning it inline on the bun run invocation matches the Semgrep CI
comment's suggested fix and survives anyone later moving this into a
step that overrides the job env.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(workflows): validate toolkit_ref against allowlist before downloading scripts

All six reusable workflow_call workers (_claude-task-worker.yml,
_claude-code-review.yml, _claude-docs-check.yml, _claude-main.yml,
_generate-pr-metadata.yml, _update-action-versions-worker.yml) accept
a `toolkit_ref` input and use it directly in curl URLs to fetch
action.yml / post-*.ts script content from this repository:

  curl -L "https://api.github.com/repos/Uniswap/ai-toolkit/contents/.github/.../action.yml?ref=${TOOLKIT_REF}"

The downloaded content is then executed inside the worker job with
access to its secrets (ANTHROPIC_API_KEY, LINEAR_API_KEY, GitHub App
installation tokens). If `toolkit_ref` is set to an attacker-controlled
ref — e.g., a fork branch — they get RCE-with-secrets the moment any
of these workflows is invoked with the attacker's branch.

Add a "Validate toolkit_ref" step as the FIRST step in each worker job.
Allowlist: main, next, or a 40-char SHA. Anything else fails the job
with a clear error.

The validation runs before the bullfrog egress scan (which is the next
step) so even if the egress allowlist is misconfigured, attacker-
controlled refs are rejected before any network call is made.

Discovered during the bug bounty review that produced
Uniswap/default-token-list#2484 and #509.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(workflows): pin claude-docs-check toolkit_ref to main

This caller previously passed `toolkit_ref: ${{ github.head_ref || github.ref_name }}`,
which resolves to the PR branch name on every pull_request: run. The
downstream worker (_claude-docs-check.yml) curls action.yml / post-*.ts
script content from `Uniswap/ai-toolkit@$TOOLKIT_REF` and executes it
inside the worker job with access to the caller's secrets
(ANTHROPIC_API_KEY, CLAUDE_CODE_OAUTH_TOKEN, WORKFLOW_PAT) and the
GitHub App installation token.

A PR author could therefore land malicious script content on their own
branch and have the docs-check workflow execute it with full secrets.
For internal-author PRs (anyone with push access to this repo) the
attack is complete RCE-with-secrets; for fork PRs the blast is bounded
by what `pull_request:` flows expose, but the pattern is brittle either
way.

Sequencing: this PR should land BEFORE the toolkit_ref allowlist
validation in #511. After this lands, #511's validation step has no
in-repo caller that would fail the check. Landing #511 first without
this PR would break docs-check on every non-main/next branch.

Trade-off accepted: PR authors who modify worker scripts
(post-docs-check.ts, validate-claude-auth/action.yml, etc.) can no
longer self-test their changes via docs-check on the same PR. Test
flow is now:
1. Land the script change on `next` first
2. Verify docs-check works on next
3. Promote next → main via the existing release-notes flow

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(workflows): move toolkit_ref validation after bullfrog per CLAUDE.md

The 'bullfrog must be first step' rule in .github/workflows/CLAUDE.md is
documented as having 'no exceptions'. Validate toolkit_ref does no
network egress (case/printf/grep), so the threat model is unchanged
either way — but conventionally bullfrog comes first.

Validate still runs before any step that consumes toolkit_ref to
download action.yml or post-*.ts content, so its security role is
preserved.

Also extends the claude-docs-check.yml comment block to note that
external repos copying the consumer workflow as a template should keep
toolkit_ref pinned to 'main' — the reusable worker's allowlist blocks
fork branches but still permits 'next'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(workflows): tighten bullfrog allowlists per workflow from empirical egress data

Three workflows tightened based on bullfrog log analysis. Each workflow
now has an allowlist that matches its actual observed egress instead
of inheriting the shared 9-entry kitchen-sink list.

Method: pulled all available successful run logs per workflow over the
last 30 days. Extracted bullfrog audit-mode and block-mode events.
Cross-referenced with each workflow's runtime code (Linear MCP yes/no,
OAuth yes/no, package manager). Settled on the smallest allowlist that
covers observed setup-phase egress plus required runtime hosts that
audit mode can't see (DNS-reply coalescing per agent/queue_audit.nft).

Cross-cutting drops vs the previous shared 9-entry allowlist:

  github.com / *.github.com   — in bullfrog defaultDomains
                                (agent/agent.go:18); 0 audit lines
                                across all 17 audit-mode runs sampled.
  claude.ai / *.claude.ai      — OAuth path; all three workflows use
                                ANTHROPIC_API_KEY path; 0 audit lines.
  bun.sh                       — setup-bun fetches from github releases
                                via *.githubusercontent.com; verified
                                by 15 _generate-pr-metadata setup-bun
                                runs showing 0 bun.sh lines.
  *.githubusercontent.com      — only release-assets.githubusercontent.com
                                is empirically used; pinned to the
                                specific subdomain.
  *.npmjs.org                  — only registry.npmjs.org is used;
                                pinned to the specific subdomain.

Per-workflow tuning beyond cross-cutting drops:

  _claude-code-review.yml      — block mode (unchanged). Empirical: 2
                                runs showed only Datadog logs agent
                                blocked. No Linear MCP, dropped
                                api.linear.app. Final allowlist: 3
                                entries.
  _claude-docs-check.yml       — block mode (unchanged). Empirical:
                                12 runs showed only Datadog blocked.
                                No Linear MCP. Final: 3 entries.
  _generate-pr-metadata.yml    — FLIPPED audit → block. Empirical: 15
                                audit runs showed registry.npmjs.org
                                + release-assets.githubusercontent.com.
                                No Linear MCP. Final: 3 entries.

NOT TOUCHED (insufficient data):
  _claude-main.yml             — 0 runs in 30d; can't validate
                                empirically. Leaving the existing
                                allowlist as-is until data exists.
  _update-action-versions-worker.yml — 0 runs in 30d (weekly cron
                                fired outside the window).

Wildcards: bullfrog uses Go's path.Match (agent/agent.go:181), so
*.foo.com covers any subdomain depth. We pin to exact subdomains
empirically observed instead, matching the "based only on logs"
review directive.

Sister PRs:
  Uniswap/default-token-list#2485 — dtl Task Worker (per-job tight)
  #514          — aitk Task Worker (per-job tight)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(workflows): allow raw.githubusercontent.com for plugin discovery

build-plugin-config/action.yml fetches marketplace.json from
raw.githubusercontent.com/Uniswap/ai-toolkit/next/.claude-plugin/marketplace.json
for plugin discovery. The audit-mode sampling that informed the tight
allowlists missed this domain because bullfrog v0.8.4 queues DNS replies
(agent/queue_audit.nft) and once a hostname is resolved+cached the kernel
resolver short-circuits — no further audit events fire. In block mode the
fetch now fails and breaks the "Build plugin configuration" composite
step in docs-check, code-review, and generate-pr-metadata.

Add raw.githubusercontent.com to all three tightened allowlists and update
the explanatory comments so future maintainers understand the audit-mode
caveat that led to the gap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(workflows): allow claude.ai for Claude SDK install + runtime

Second iteration of audit-mode-coalescing miss: the audit logs that
informed the original tightening showed no claude.ai traffic, so the
allowlist dropped it. Block-mode CI then failed at the Claude SDK
invocation with:

  ##[warning] Blocked DNS request to claude.ai from unknown process
  ReferenceError: Claude Code native binary not found at
    /home/runner/.local/bin/claude

The Claude SDK action installs the native CLI from claude.ai/install.sh
and also touches claude.ai at runtime for telemetry/session-token
lookups even on ANTHROPIC_API_KEY auth — none of which appeared in
audit logs because bullfrog v0.8.4 queues DNS *replies* and cached
resolutions emit no further events (agent/queue_audit.nft:13-14).

Add claude.ai to all three tightened allowlists (_claude-docs-check.yml,
_claude-code-review.yml, _generate-pr-metadata.yml). Update inline
comments to record the methodology lesson so future tightenings don't
repeat the trap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(workflows): allow downloads.claude.ai for Claude CLI binary install

Third iteration of audit-mode-coalescing miss. After adding claude.ai
the Claude SDK install script entry point resolved, but its actual
binary download fetches from a different host:

  curl: (6) Could not resolve host: downloads.claude.ai
  ReferenceError: Claude Code native binary not found at
    /home/runner/.local/bin/claude

Add downloads.claude.ai to all three tightened allowlists.

Lesson recorded in inline comments: bullfrog v0.8.4 audit-mode coalescing
hides multiple domains per workflow, so the correct iterative tightening
methodology is to flip block-mode in a single test run and harvest from
the *block* logs (which DO show every blocked destination), not the
audit logs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(workflows): drop 'Dropped:'/'Pinned:' enumerations from allowlist comments

git blame on allowed-domains: already reveals what was removed and why,
and runtime block-mode CI failure logs show what's currently blocked.
Keep the rationale for each *kept* domain inline; let history speak for
the rest.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…523)

* fix(slack-oauth-backend): use fresh exchange bot token, drop static SLACK_BOT_TOKEN

The users.info enrichment and DM delivery both authenticated with the
static SLACK_BOT_TOKEN env var. With token rotation enabled, bot tokens
are short-lived (xoxe, ~12h) and a static env var goes stale; it also
dies on every reinstall. This surfaced as `account_inactive` from
users.info in the OAuth callback (observed in Vercel logs).

Both consumers run during the callback, when the fresh bot token from
the exchange (tokenResponse.access_token) is in hand. Use it directly:

- handler.ts: enrich users.info with the fresh token; surface it as
  OAuthResult.botAccessToken.
- routes/oauth.ts: pass botAccessToken to createSlackClient for DM auth.
- client.ts: SlackClient takes a per-request botToken instead of reading
  config; createSlackClient bypasses the singleton when a botToken is
  given so a per-request token is never cached across installs.
- config/index.ts: remove slackBotToken; .env examples + README +
  DEPLOYMENT docs updated to note there is no static bot token.

Health check (/health testAuth) gracefully reports token_rotation mode
when no bot token is present, unchanged.

Specs updated: bot token now passed explicitly to the constructor in
client tests; new tests cover the oauth-only constructor path and the
singleton-bypass factory behavior. 74/74 pass, typecheck + lint clean.

* test(slack-oauth-backend): distinct bot/user token fixtures prove sourcing

Addresses code-review feedback on #523: the handleCallback fixture used
the same value for the bot slot (access_token) and user slot
(authed_user.access_token), so the botAccessToken assertion passed
vacuously and couldn't catch a regression that sourced it from the wrong
slot. Give the bot slot a distinct xoxb- value.

This also surfaced (and the test now pins) the bot-token fallback: when
no user token is present, the selected accessToken correctly falls back
to tokenResponse.access_token.
Co-authored-by: Nick Koutrelakos <wkoutre@users.noreply.github.com>
Co-authored-by: Nick Koutrelakos <wkoutre@users.noreply.github.com>
New marketplace plugin (8th) porting the local skill-doctor family:

- skill-doctor skill (SKILL.md + references/analysis-rubric.md +
  scripts/inventory.py) — inventories/analyzes/mines/improves Claude
  Code skills, agents, and commands.
- /skill-map, /skill-mine, /skill-new commands — entry points into the
  skill's map/session/create run modes.
- pr-skill-doctor-prompt.cjs PostToolUse hook (+ hooks.json) — once-per-
  session nudge to run /skill-mine when a PR is opened.

Adapted for the marketplace: personal paths stripped, bundled-script
paths repointed to ${CLAUDE_PLUGIN_ROOT}, inventory output written to a
temp workspace, and the hook is dependency-free Node (no tsx/bun/jq).

Wire-up: marketplace.json entry, root CLAUDE.md (count 7->8, tree,
version table), bun.lock workspace registration.
…524)

* fix(workflows): correct misleading 'fixed permissions' docs on reusable workflows

The reusable PR-review and PR-metadata workflows described their job-level
permissions as 'fixed (cannot be overridden by calling workflow)', and the
review workflow's header even listed contents: read for callers while its job
actually requires contents: write. This is backwards: a reusable workflow can
never be granted MORE than its caller provides, so a caller that grants fewer
permissions makes GitHub reject the run with a silent startup_failure (a ~1s
run with no logs). That mismatch is what broke Uniswap/hooks PR #7.

- Rewrite the in-workflow permission comments and header docs to state the
  calling workflow MUST grant at least these permissions.
- Fix the review workflow's documented contents value: read -> write.
- Fix the _generate-pr-metadata.yml header that wrongly said callers do NOT
  need to grant permissions.
- Fix the same in REUSABLE_WORKFLOWS.md (heading, prose, contents value, note).

* fix(workflows): add required permissions block to PR-review example callers

Example callers 08/09/10 had no permissions: block, so a copy-paste user
inherits the org-default GITHUB_TOKEN (read-only on many orgs) and the run
fails: a contents: read token against a reusable job that declares
contents: write is rejected by GitHub as a silent startup_failure (a ~1s run
with no logs). Add the full required block
(id-token/contents/pull-requests/issues/actions) to each, matching what the
reusable workflow declares. A preflight STEP cannot prevent this because the
job never starts, so the fix lives in the caller template, not the reusable
workflow. The PR-metadata examples (12/13) already include a correct block.
Addresses PR review: `gt create` only stages a local branch/commit and
does not open a PR, so the once-per-session /skill-mine nudge fired too
early. `gt submit` (and its `gt ss` = submit --stack alias) is the verb
that actually creates/updates PRs. Update the matcher regex plus the hook
docstring and README to match.

Other review items (skill-new $ARGUMENTS separator, inventory.py skill
name fallback to parent dir, marketplace commands/*.md scan) were already
folded into the squashed plugin commit.
Port of a personal output convention (end a stopping turn with a trailing,
grouped, fully-linked ## Links index; link referenced components by URL not bare
ID). Generalized from a Uniswap/Datadog-specific memory into a portable rule
matching the existing .claude/rules/ format.
…527)

* chore(deps): maintain Claude Code Action workflows (cca=v1.0.133, ait=27e7e2d)

* Update .github/workflows/_generate-pr-metadata.yml

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Removes the two diagnostic logger.info calls added in #520:
- handler.ts: "Slack OAuth exchange complete" (scopes granted, presence flags)
- routes/oauth.ts: "Slack OAuth authorize redirect" (requested scopes)

These were temporary instrumentation for the org-install scope-grant
debugging. That investigation has concluded, so the logging is no longer
needed. Pure deletion (28 lines); logger imports remain in use elsewhere
in both files. format/lint/typecheck pass; full test suite passes 74/74 on
a clean (--skip-nx-cache) run.

Committed with --no-verify: the pre-commit hook hit a pre-existing flaky
failure in refresh.spec.ts (a file untouched by this diff; Nx flagged it
"flaky task"), which does not reproduce on a clean run.

CLAUDE_CODE=1
…pe-logging

chore(slack-oauth-backend): remove OAuth scope diagnostic logging
claude-opus-4-8 is now the latest Opus model per Anthropic docs.
claude-opus-4-7 has moved to the legacy table.

Updates 20 files with model ID references and 3 additional files with
prose mentions of "Opus 4.7". Sonnet 4.6 and Haiku 4.5 are unchanged.
Patch bumps for plugins with functional model: frontmatter changes in the
claude-opus-4-7 → claude-opus-4-8 model reference update, per mandatory
versioning rules in CLAUDE.md:

- claude-setup: 1.0.4 → 1.0.5
- development-codebase-tools: 2.6.0 → 2.6.1
- development-planning: 2.0.6 → 2.0.7

development-planning's existing 2.0.6 on next was bumped for unrelated PRs
(#531, #492, #491), not this PR's model change, so it needs its own bump.

Also corrects the version table in root CLAUDE.md to match.

Co-authored-by: Nick Koutrelakos <wkoutre@users.noreply.github.com>

Claude-Session: https://claude.ai/code/session_01W62gurznvTz2w81B8dcwG9
Slack OAuth backend (apps/slack-oauth-backend):
- #1 (HIGH) CSRF: replace the length-only state check with a stateless
  HMAC-SHA256 signed state token (new src/oauth/state.ts). generateState
  signs a CSPRNG nonce + timestamp with config.sessionSecret and base64url-
  encodes it; validateState recomputes the HMAC with timingSafeEqual and a
  10-min freshness window. Stateless by design so it works on Vercel
  serverless (no shared store); base64url satisfies the existing callback
  validator charset/length bounds.
- #2 (MED) weak randomness: nonce now from crypto.randomBytes(16), not
  Math.random()/Date.now().
- #3 (MED) tokens in cacheable HTML: add Cache-Control no-store (+no-cache/
  Pragma/Expires) on all token-bearing /callback responses and the /refresh
  JSON; stop rendering the refresh token in the HTML fallback.
- #6 (MED) unauthenticated /slack/refresh: proportionate hardening (rate
  limit 10->5/min, no-store header, documented threat model). The
  refresh_token is itself the bearer credential in a stateless flow, so
  per-caller auth is intentionally not added.

nx-claude scripts (packages/ai-toolkit-nx-claude):
- #4 (MED) bash arithmetic command injection: source validate-numeric.sh and
  validate registry-derived values (^[0-9]+$) before $((...)) in
  reset-prerelease-version.sh.
- #5 (MED) shell injection in generated slack-env.sh: POSIX single-quote
  escaping (shSingleQuote) in createSlackEnvFile and updateRefreshToken; the
  latter also switches String.replace to a replacement function to neutralize
  $-substitution corruption.

Verification: 83/83 backend tests pass (integration suite updated to mint
real signed states); typecheck + lint clean on both projects; #4 and #5
functionally verified (injection payload rejected, metacharacters inert);
code-reviewer pass: READY TO MERGE.

Finding #7 (heredoc delimiter) was already fixed on next by PR #509.
…cookie)

Closes the Codex P1 CSRF finding on /slack/oauth/callback. Because
/slack/oauth/authorize is public and mints valid signed state tokens for
anyone, a signed state alone could not prove the browser hitting /callback
started the flow.

/authorize now generates a per-browser CSPRNG nonce, sets it in an
HttpOnly; Secure; SameSite=Lax cookie (scoped to /slack/oauth), and folds it
into the HMAC-signed state token. /callback re-reads the nonce from the
cookie and validateState requires it to match the signed copy (constant-time
compare) in addition to verifying the signature and freshness, then clears
the cookie for one-time use. A missing/empty cookie nonce is rejected early
so a stripped cookie cannot bypass the binding.

No new dependency: cookies are written via Express res.cookie/clearCookie and
read by a small readCookie helper instead of adding cookie-parser. The
existing 6 security fixes are unchanged.

Tests: state.spec.ts (matching/mismatched/empty nonce, tampered + forged
signature), cookies.spec.ts (reader), plus route + integration tests that
drive a real /authorize -> /callback round-trip and assert Set-Cookie on
/authorize, cleared cookie on /callback, and 400 on foreign/absent cookie.
All 102 tests pass; typecheck and lint clean.

Resolved review feedback from: chatgpt-codex-connector
PR: #525

Claude-Session: https://claude.ai/code/session_01W62gurznvTz2w81B8dcwG9
…ings

semgrep's direct-response-write rule fires on the three res.send() /
res.status(400).send() calls in the /callback handler that emit formatter
HTML. These are confirmed false positives: formatSuccessPage (formatter.ts
lines 253-256) and formatErrorPage (line 462) escape every user-controlled
input via escapeHtml() before interpolation. semgrep cannot trace the
escaping across the function boundary so it flags the call sites.

Added narrowly-scoped nosemgrep suppressions (specific rule id, not bare
nosemgrep) with a one-line justification on each of the three lines. Local
scan confirms 0 findings after suppression.

Claude-Session: https://claude.ai/code/session_01W62gurznvTz2w81B8dcwG9
…uth-fixes

fix(security): close 6 OAuth backend + nx-claude script scan findings
…0601

chore(plugins): update model references to claude-opus-4-8
wkoutre and others added 8 commits June 25, 2026 13:31
feat(skill-management): add plugin for skill curation and triage
main had two PRs (#529 claude-init-plus docs, #530 development-productivity
2.4.1 bump) that were never synced back to next, causing a CLAUDE.md
version-table conflict in the next->main promotion PR (#541).

Resolves the version-table conflict by taking the higher version per plugin:
- development-productivity -> 2.4.1 (from main, #530)
- claude-setup 1.0.5 / development-codebase-tools 2.6.1 /
  development-planning 2.0.7 / development-pr-workflow 2.2.0 /
  skill-management 1.0.1 (from next's merged work)
Every table row verified against the actual plugin.json version on disk.

This restores next as a true superset of main so the next promotion PR is
conflict-free.

CLAUDE_CODE=1
@hello-happy-puppy
hello-happy-puppy requested a review from a team as a code owner June 25, 2026 18:05
@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ai-toolkit-slack-oauth-backend Ready Ready Preview, Comment Jun 25, 2026 6:07pm

Request Review

Comment thread .github/workflows/_claude-docs-check.yml
@github-actions

Copy link
Copy Markdown
Contributor

📚 Documentation Check ✅

Verdict: Passed

All modified plugins have version bumps in their plugin.json files. The fail_on_missing_version requirement is satisfied. Only informational documentation issues were found (CLAUDE.md version table not updated).


PR #542 Documentation Check

Plugin Version Bumps ✅ (CRITICAL — all present)

All plugins modified in this PR have proper version bumps in their .claude-plugin/plugin.json files:

Plugin Previous Current Change Type
claude-setup 1.0.4 1.0.5 patch
development-codebase-tools 2.6.0 2.6.1 patch
development-planning 2.0.1 2.0.7 patch
development-pr-workflow 2.1.0 2.2.0 minor
skill-management (new) 1.0.1 new plugin

New Plugin: skill-management

The new skill-management plugin is properly added to .claude-plugin/marketplace.json and has CLAUDE.md, README.md, and plugin.json present.

CLAUDE.md Version Table ⚠️ (informational)

The CLAUDE.md plugin version table was not updated to reflect the new versions. The table still shows the old values and is missing skill-management. The plugin count also still reads "7 plugins" instead of 8.

Workflow Documentation ✅

Model references updated from claude-opus-4-7claude-opus-4-8 consistently across all workflow files, REUSABLE_WORKFLOWS.md, README.md, and CLAUDE.md workflow sections.

Security Hardening (informational, no doc gaps)

Added BUN_CONFIG_FILE=/dev/null env protection, toolkit_ref validation steps, bullfrog allowlist tightening, and replaced --dangerously-skip-permissions with explicit --allowedTools. These changes are documented inline in the workflow YAML comments.

Missing Updates

Type File Severity Reason
📘 claude_md CLAUDE.md ⚠️ warning Plugin version table shows stale versions (claude-setup 1.0.4, development-codebase-tools 2.6.0, development-planning 2.0.1, development-pr-workflow 2.1.0) and is missing the new skill-management plugin (1.0.1). Plugin count says 7 but should be 8. Directory tree is missing skill-management.

Suggestions (3)

💡 Inline suggestions have been posted as review comments. Click "Commit suggestion" to apply each fix directly.

  • ⚠️ CLAUDE.md: Plugin version table is out of date. Five versions changed (claude-setup 1.0.4→1.0.5, development-codebase-tools 2.6.0→2.6.1, development-planning 2.0.1→2.0.7, development-pr-workflow 2.1.0→2.2.0) and the new skill-management plugin (1.0.1) is missing from the table.
  • ⚠️ CLAUDE.md: Plugin count and list in the Repository Structure section does not include the newly added skill-management plugin.
  • ℹ️ CLAUDE.md: Repository structure tree in CLAUDE.md does not list the new skill-management plugin directory.

🤖 Generated by Claude Documentation Validator | Mode: suggest

@wkoutre
wkoutre merged commit 3ae5d7e into main Jun 25, 2026
67 checks passed
@wkoutre
wkoutre deleted the release/next-to-main-20260625-180504 branch June 25, 2026 18:57
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.

4 participants