Compare commits

...

331 Commits

Author SHA1 Message Date
Morgan Lunt
ecc5139c30 code-modernization: legacy toolchain is advisory, not a transform blocker
Legacy code often cannot build locally by nature — CICS/IMS programs
have no local translator and the real runtime may be a mainframe the
user doesn't have. Stopping transform on a failed legacy smoke compile
would block exactly those systems.

- transform Step 0a: the target toolchain remains required (its tests
  cannot run without it); a failed or impossible legacy compile no
  longer stops the run — the equivalence strategy switches to recorded
  traces / golden-master fixtures, and that downgrade is stated in the
  plan and in TRANSFORMATION_NOTES.md so reviewers know the strength of
  the proof
- preflight: a red legacy toolchain now yields Ready-with-gaps for
  transform/reimagine instead of Not-ready
2026-06-09 08:48:05 -07:00
Morgan Lunt
a7e1f99070 code-modernization: limit marketplace.json change to the one description line
The previous commit round-tripped the catalog through a JSON serializer,
which escaped non-ASCII characters in every other plugin's description.
Restore the file from main and change only the code-modernization entry.
2026-06-09 08:48:04 -07:00
Morgan Lunt
0c40a74425 code-modernization: add topology viewer screenshot to README
Rendered from AWS CardDemo (the public mainframe sample app) by running
/modernize-map end to end and capturing the resulting TOPOLOGY.html.
2026-06-09 08:48:04 -07:00
Morgan Lunt
bec8d7c93a code-modernization: vendor d3, viewer robustness, status command, pipeline fixes
Viewer (assets/topology-viewer.html):
- inline a minified d3 subset (hierarchy/pack, zoom, selection,
  interpolateZoom, ease; ISC license) instead of loading from a CDN —
  the page is now fully self-contained and works on air-gapped networks
- handle duplicate node ids (unique-suffix; edges bind to the first
  occurrence) and store parent references directly, fixing
  level-of-detail and selection corruption with messy generated data
- share one reveal rule between drawing, edge culling, and hit-testing
  so edges no longer draw into collapsed containers
- pre-bucket edges by kind and keep a per-node adjacency map; the
  hover/selection pass no longer scans every edge each frame
- cancel in-flight fly-to animations when a new one starts; clamp
  fly-to zoom to the zoom extent; derive max zoom from the smallest
  leaf so deep estates stay reachable
- render dead-end candidates (new deadEnds field) with a dashed
  outline and a sidebar badge
- clicking a node during a flow walkthrough exits the walkthrough;
  search results clear on selection and Escape; surrogate-safe label
  truncation; clearer stats line; explicit empty-topology message

Commands:
- new /modernize-status: read-only progress report — artifact inventory
  with timestamps, staleness flags, secrets-hygiene checks, next step
- map: deadEnds in the topology schema; datastore names must be logical
  identifiers with credentials stripped from URLs/DSNs
- brief: read topology.json + .mmd files (not the interactive HTML);
  staleness check against inputs; effort unit aligned to person-months
- transform: secret-safe characterization-test prompt; diff -y fallback
  when delta is missing; credential-safe diff selection
- reimagine: target vision is everything after the first argument (was
  silently truncated to one word); masking rules in spec/scaffold/
  handoff prompts
- brief/transform/reimagine: human-approval gates phrased as explicit
  stop-and-wait instead of 'enter plan mode'
- preflight: delta in the tool table; brief added to the verdict list
- README: preflight/status in the workflow; legacy/ deny list also
  covers Write; plugin + marketplace descriptions updated
2026-06-09 08:48:04 -07:00
Morgan Lunt
8745968186 code-modernization: harden topology viewer and template injection
Fixes from an adversarial review of the new viewer:

- pin d3 to 7.9.0 and load it via dynamic import with an explicit
  error panel when the CDN is unreachable (previously a blocked CDN
  produced a silent dark page — a real concern for restricted networks)
- coerce ids/names/loc at intake: a single missing name or non-numeric
  loc previously threw inside the render loop or propagated NaN through
  the pack layout, blanking the canvas with no error
- normalize flows/steps/edges defensively (null entries, missing steps,
  numeric ids vs string lookups)
- mirror the level-of-detail reveal rule in the hit test so clicks
  can't select nodes that aren't drawn
- scope the Escape shortcut so clearing the search box doesn't reset
  the viewport; set zoom clickDistance(4) so trackpad jitter doesn't
  swallow selection clicks
- round canvas backing-store size (fractional devicePixelRatio caused
  a reallocation every frame on 125%/150% display scaling)
- modernize-map: use braced ${CLAUDE_PLUGIN_ROOT} so substitution
  actually happens, assert the injection marker exists in the template,
  and correct the documented failure mode
2026-06-09 08:48:04 -07:00
Morgan Lunt
1c4a5cfded code-modernization: interactive topology map, preflight command, persona flows
modernize-map previously rendered the call graph and data lineage as
static Mermaid diagrams, which become unreadable once a node has ~10+
edges — exactly the shape of real legacy systems. It now builds an
interactive viewer from a shipped template (assets/topology-viewer.html):
a zoomable circle-pack of domains/modules sized by LOC, rendered to
canvas with level-of-detail reveal, dependency edges with per-kind
toggles, search with fly-to, a per-node detail sidebar, and a flow
walkthrough mode. Small domain-level .mmd exports remain for docs.

- topology.json now has a documented schema (hierarchy + edges + entry
  points + observations + flows) consumed by the viewer
- map traces 2-4 business flows anchored to personas (claimant,
  operator, auditor), each step in plain business language mapped to
  the modules that implement it; the viewer plays them as numbered
  paths
- brief gains a Business Walkthroughs section connecting each persona
  flow to the phase that replaces it
- new modernize-preflight command: detects the stack, checks analysis
  tooling, smoke-compiles a real source file with the legacy toolchain,
  inventories missing copybooks/descriptors/binary-only artifacts, and
  writes a per-command readiness verdict
- transform now verifies legacy + target toolchains before its plan
  gate instead of failing at test time
- README: commands updated, optional-tooling section reframed as 'what
  to give Claude'
2026-06-09 08:48:04 -07:00
github-actions[bot]
bbbff6ab54 bump(qdrant-skills): 11df00a7 → 82337ccd (#2458)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:38:37 -05:00
github-actions[bot]
6105eea1c6 bump(sentry-cli): 329f5c5d → 9e9fe0fb (#2462)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:38:13 -05:00
github-actions[bot]
7a574ede07 bump(codspeed): 9793aaf9 → c6112f16 (#2451)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:37:48 -05:00
github-actions[bot]
50507ce03c bump(expo): 145a923c → c3886024 (#2452)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:37:24 -05:00
github-actions[bot]
9dbb38fff1 bump(oracle-ai-data-platform-workbench-spark-connectors): 04cc355f → 00cedef3 (#2456)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:36:45 -05:00
github-actions[bot]
0f2b68bec6 bump(sap-fiori-mcp-server): f6e9ae1f → fbfe8c32 (#2461)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:36:19 -05:00
github-actions[bot]
e408e54811 bump(aws-data-analytics): df13dea6 → 55b9acfe (#2445)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:35:53 -05:00
github-actions[bot]
7a5c13e654 bump(carta-cap-table): 0227331a → 9eb31290 (#2447)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:35:26 -05:00
github-actions[bot]
a0bf1eed49 bump(chrome-devtools-mcp): 7afd0167 → 702d3734 (#2450)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:34:57 -05:00
github-actions[bot]
e0825af81b bump(brightdata-plugin): 3e6d0838 → bd5bd76b (#2446)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:34:27 -05:00
github-actions[bot]
318f7c9674 bump(carta-crm): 0227331a → 9eb31290 (#2448)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:33:56 -05:00
github-actions[bot]
60706e7bc3 bump(carta-investors): 0227331a → 9eb31290 (#2449)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:33:27 -05:00
github-actions[bot]
f00d6d8005 bump(nvidia-skills): e29b3c65 → 0482ebce (#2455)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:32:56 -05:00
github-actions[bot]
48bbef8757 bump(hyperframes): 1fd1b316 → 25420bf4 (#2453)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:32:48 -05:00
github-actions[bot]
4d5ccd2968 bump(jfrog): 8324c7fc → 117febaa (#2454)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:32:16 -05:00
github-actions[bot]
aa6e8702bc bump(posthog): 9105eb4d → db4a8663 (#2457)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:31:43 -05:00
github-actions[bot]
36046dac80 bump(rc): b34f9beb → 473fd504 (#2459)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:31:12 -05:00
github-actions[bot]
19b7347f82 bump(aws-core): df13dea6 → 55b9acfe (#2444)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:30:47 -05:00
github-actions[bot]
d321de478d bump(revenuecat): b34f9beb → 473fd504 (#2460)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:30:38 -05:00
github-actions[bot]
4a501b5766 bump(aws-agents): df13dea6 → 55b9acfe (#2443)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:30:32 -05:00
github-actions[bot]
795d2ba506 bump(valtown): a3e88468 → 02631f99 (#2464)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:30:11 -05:00
github-actions[bot]
297568419b bump(wix): 8ed898ab → 188ed338 (#2465)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 13:30:01 -05:00
github-actions[bot]
ed3ff7abb3 bump(clickhouse-best-practices): 3a1ee115 → 544384f4 (#2436)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 08:15:22 -05:00
github-actions[bot]
6046414b87 bump(huggingface-skills): 504191c5 → d7223848 (#2438)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 08:15:10 -05:00
github-actions[bot]
a348b32801 bump(clickhouse): 1f30864b → ecbd4762 (#2435)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 08:14:53 -05:00
github-actions[bot]
c033853393 bump(dash0): 1e64ae2d → 5ff7aa5b (#2437)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 08:14:29 -05:00
github-actions[bot]
2b08b5b3ca bump(42crunch-api-security-testing): c2951754 → db2fb7e5 (#2433)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 08:13:49 -05:00
github-actions[bot]
0d0478adb9 bump(sap-fiori-mcp-server): 070cb3c2 → f6e9ae1f (#2440)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 08:13:25 -05:00
github-actions[bot]
e007f1b979 bump(chrome-devtools-mcp): f90f863d → 7afd0167 (#2434)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 08:12:57 -05:00
github-actions[bot]
3aa99bd279 bump(hyperframes): fc01717c → 1fd1b316 (#2439)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 08:12:48 -05:00
github-actions[bot]
f5962b8e64 bump(wix): 038e0c55 → 8ed898ab (#2441)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-08 08:12:14 -05:00
github-actions[bot]
1b46aa6d4a bump(hyperframes): 48fcf4ad → fc01717c (#2427)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-07 20:02:17 -05:00
github-actions[bot]
7cdfbb4cb2 bump(dataverse): ab906c96 → 2d50cf65 (#2426)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-07 20:01:41 -05:00
github-actions[bot]
13bb250718 bump(brightdata-plugin): 68651246 → 3e6d0838 (#2422)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-07 13:03:03 -05:00
github-actions[bot]
8f2f03a654 bump(hyperframes): 29d6f1ea → 48fcf4ad (#2423)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-07 13:02:41 -05:00
github-actions[bot]
e27e302c39 bump(wix): 3f8f2a68 → 038e0c55 (#2424)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-07 13:02:21 -05:00
github-actions[bot]
50d9d8775d bump(exa): ad888a18 → f0838825 (#2415)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-07 08:41:51 -05:00
github-actions[bot]
f13b8b9fb2 bump(railway): daa67716 → 1df604eb (#2417)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-07 08:41:29 -05:00
github-actions[bot]
84c2cdf3ad bump(hyperframes): dd956744 → 29d6f1ea (#2416)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-07 08:41:17 -05:00
github-actions[bot]
d136461a6c bump(wix): 29a68ecd → 3f8f2a68 (#2418)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-07 08:41:07 -05:00
github-actions[bot]
1fb8ee7628 bump(hyperframes): 731bc78f → dd956744 (#2413)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 19:02:51 -05:00
github-actions[bot]
093e91b259 bump(hyperframes): 16416734 → 731bc78f (#2412)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 15:30:43 -05:00
github-actions[bot]
fc4b52db22 bump(snowflake-cortex-code): dbd0d635 → 6a22eb1f (#2409)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 13:03:28 -05:00
github-actions[bot]
d447c246ca bump(wix): 9dca098f → 29a68ecd (#2410)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 13:03:07 -05:00
github-actions[bot]
6cf7e6331e bump(snowflake-cortex-code): 54760f12 → dbd0d635 (#2397)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 10:15:08 -05:00
github-actions[bot]
139e43d457 bump(ui5-typescript-conversion): 767ac53c → 9b3d7d80 (#2401)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 10:14:47 -05:00
github-actions[bot]
67f20b04f0 bump(carta-crm): 54602de6 → 0227331a (#2393)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 10:14:26 -05:00
github-actions[bot]
18ccde8c37 bump(carta-investors): 54602de6 → 0227331a (#2394)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 10:14:04 -05:00
github-actions[bot]
0ea0ddc5cc bump(nvidia-skills): bb0436fa → e29b3c65 (#2396)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 10:13:42 -05:00
github-actions[bot]
bc00d658e7 bump(teamcity-cli): 3cc3013c → 67e21f0b (#2398)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 10:13:19 -05:00
github-actions[bot]
5ca5306896 bump(togetherai-skills): 9772f2a2 → fb94cc14 (#2399)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 10:12:56 -05:00
github-actions[bot]
76d3c7ee37 bump(vibe-prospecting): 7ed0c4e2 → aa5903f5 (#2403)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 10:12:32 -05:00
github-actions[bot]
88afc50343 bump(hyperframes): bacfb175 → 16416734 (#2395)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 10:12:06 -05:00
github-actions[bot]
ed4e896219 bump(ui5): 767ac53c → 9b3d7d80 (#2400)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 10:11:41 -05:00
github-actions[bot]
ec38e7a881 bump(valtown): e01069e1 → a3e88468 (#2402)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 10:11:15 -05:00
github-actions[bot]
f1e2e3f1e4 bump(wix): f99715fc → 9dca098f (#2404)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 10:10:48 -05:00
github-actions[bot]
8b26e9f1c1 bump(carta-cap-table): 54602de6 → 0227331a (#2392)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 10:10:04 -05:00
github-actions[bot]
7562e052d5 bump(zapier): f34a7854 → 770167c5 (#2405)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 10:09:52 -05:00
github-actions[bot]
4b538d5d40 bump(zscaler): be37fb60 → f84ce4f0 (#2406)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-06 10:09:23 -05:00
github-actions[bot]
3d5017bc1d bump(sap-fiori-mcp-server): b326a9a5 → 070cb3c2 (#2384)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:41:21 -05:00
github-actions[bot]
d56b86d462 bump(carta-cap-table): 26056825 → 54602de6 (#2366)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:41:12 -05:00
github-actions[bot]
4fe9dd9b4b bump(aws-startup-advisor): ad7eadc8 → 1dd90935 (#2365)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:41:03 -05:00
github-actions[bot]
7dc4ed688f bump(chrome-devtools-mcp): 89718901 → f90f863d (#2369)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:40:53 -05:00
github-actions[bot]
65a01fafdc bump(datarobot-agent-skills): debe471c → b5a8f7a4 (#2372)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:40:44 -05:00
github-actions[bot]
dcc5a1a8b3 bump(migration-to-aws): ad7eadc8 → 1dd90935 (#2378)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:40:35 -05:00
github-actions[bot]
0278237073 bump(save-to-spotify): 35527660 → cd4ea681 (#2385)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:40:25 -05:00
github-actions[bot]
659f9f42ad bump(desktop-commander): f53f916f → 7a9b2ff0 (#2373)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:21:14 -05:00
github-actions[bot]
659d30b1f6 bump(huggingface-skills): 14cea99d → 504191c5 (#2374)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:21:05 -05:00
github-actions[bot]
c7ec509ba6 bump(qdrant-skills): 21c64d2e → 11df00a7 (#2381)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:20:40 -05:00
github-actions[bot]
317a14e25e bump(sentry-cli): 5b78ddaf → 329f5c5d (#2387)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:20:14 -05:00
github-actions[bot]
22bb99c414 bump(hunter): 3f8f3f5f → 494b0bd6 (#2375)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:19:48 -05:00
github-actions[bot]
7929ae4a4b bump(hyperframes): 4b51cc64 → bacfb175 (#2376)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:19:21 -05:00
github-actions[bot]
c422aeed0c bump(looker): ca53ea9d → e912c034 (#2377)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:18:54 -05:00
github-actions[bot]
a68871c83b bump(outputai): 5d7e612a → 2cc4685e (#2379)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:18:27 -05:00
github-actions[bot]
08a05826ce bump(posthog): d9d80933 → 9105eb4d (#2380)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:17:59 -05:00
github-actions[bot]
44fa671ac4 bump(railway): 831130cd → daa67716 (#2382)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:17:30 -05:00
github-actions[bot]
303bc792b4 bump(resend): 0f598ef5 → 0888546d (#2383)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:17:01 -05:00
github-actions[bot]
13882305d8 bump(42crunch-api-security-testing): 27815ced → c2951754 (#2362)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:16:16 -05:00
github-actions[bot]
3fdc816735 bump(shopify-ai-toolkit): 859be93b → a8e87a7c (#2388)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:16:08 -05:00
github-actions[bot]
f3a89dac7c bump(snowflake-cortex-code): c3f72002 → 54760f12 (#2389)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:15:36 -05:00
github-actions[bot]
330882bb53 bump(agentforce-adlc): 1db738be → fad761fc (#2363)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:15:05 -05:00
github-actions[bot]
6fa207be17 bump(carta-crm): 26056825 → 54602de6 (#2367)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:14:32 -05:00
github-actions[bot]
d50355a54d bump(carta-investors): 26056825 → 54602de6 (#2368)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:14:00 -05:00
github-actions[bot]
6658696d8f bump(auth0): beda869d → bdf0dc23 (#2364)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:13:53 -05:00
github-actions[bot]
71216eb7e0 bump(cloudflare): 60147cbb → c5b7b06b (#2370)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:13:19 -05:00
github-actions[bot]
b1e201e422 bump(codspeed): bfff1506 → 9793aaf9 (#2371)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:12:44 -05:00
github-actions[bot]
8bb44ef9b8 bump(sentry): 849303a8 → 030b01fb (#2386)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:12:35 -05:00
github-actions[bot]
9c0179fb60 bump(stripe): e27ee009 → b8f6adcb (#2390)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:11:58 -05:00
github-actions[bot]
f2c56408f8 bump(supabase): 3217ac03 → 2ed49769 (#2391)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 19:11:21 -05:00
Bryan Thompson
4230aea142 Add knowledge-catalog plugin (#2321) 2026-06-05 12:15:15 -07:00
Bryan Thompson
a60315ae16 Add alloydb-omni plugin (#2325) 2026-06-05 12:15:12 -07:00
Bryan Thompson
d46ebdd1eb Add cloud-sql-sqlserver plugin (#2322) 2026-06-05 12:14:55 -07:00
Bryan Thompson
30a7536d0f Add dataproc plugin (#2324) 2026-06-05 12:14:41 -07:00
Bryan Thompson
f0fc619c64 Add bigquery-data-analytics plugin (#2317) 2026-06-05 12:14:38 -07:00
Bryan Thompson
c770602299 Add cloud-sql-mysql plugin (#2323) 2026-06-05 12:14:18 -07:00
Bryan Thompson
89bc907203 Add oracledb plugin (#2319) 2026-06-05 12:14:13 -07:00
Bryan Thompson
948295e907 Add firestore-native plugin (#2320) 2026-06-05 12:14:10 -07:00
Bryan Thompson
9883cde440 Add looker plugin (#2318) 2026-06-05 12:13:55 -07:00
Bryan Thompson
96d59f53eb Add spanner plugin (#2316) 2026-06-05 12:13:52 -07:00
github-actions[bot]
e4fc76d71f bump(qdrant-skills): cace39df → 21c64d2e (#2354)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 11:16:56 -05:00
github-actions[bot]
ef7eea8a27 bump(carta-crm): ea02da68 → 26056825 (#2333)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 11:16:52 -05:00
github-actions[bot]
607793a939 bump(netlify-skills): cffaf74f → 5f777ba6 (#2348)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 11:16:36 -05:00
github-actions[bot]
2b019efea0 bump(nvidia-skills): e695a839 → bb0436fa (#2349)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 11:16:11 -05:00
github-actions[bot]
983f2789dc bump(carta-investors): ea02da68 → 26056825 (#2334)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 11:16:07 -05:00
github-actions[bot]
1f910e18c0 bump(azure): 58fd9094 → 02a614f6 (#2331)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 11:16:04 -05:00
github-actions[bot]
908c255afb bump(apollo-skills): 9ccf1347 → 60508910 (#2328)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 11:15:58 -05:00
github-actions[bot]
435820146b bump(codspeed): f79d57d2 → bfff1506 (#2336)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:58:09 -05:00
github-actions[bot]
c1f2ebd30c bump(expo): fdd3df12 → 145a923c (#2341)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:57:43 -05:00
github-actions[bot]
bfd6cc4453 bump(fullstory): 384555c3 → b20614e2 (#2343)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:57:16 -05:00
github-actions[bot]
90a522ac76 bump(hunter): 69c4e59e → 3f8f3f5f (#2344)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:56:49 -05:00
github-actions[bot]
e01bc27de9 bump(hyperframes): 8228932e → 4b51cc64 (#2345)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:56:21 -05:00
github-actions[bot]
0cc28d3f85 bump(outputai): d3c9b1f4 → 5d7e612a (#2351)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:55:52 -05:00
github-actions[bot]
2a2965bf7d bump(pigment): abf36e64 → f7bb2190 (#2352)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:55:23 -05:00
github-actions[bot]
2ee946660a bump(posthog): a4873114 → d9d80933 (#2353)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:54:54 -05:00
github-actions[bot]
2b0af2c713 bump(aws-startup-advisor): 30808e64 → ad7eadc8 (#2330)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:54:10 -05:00
github-actions[bot]
285c6086c8 bump(carta-cap-table): ea02da68 → 26056825 (#2332)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:53:39 -05:00
github-actions[bot]
64d2239ee3 bump(clickhouse-best-practices): 46ef08cc → 3a1ee115 (#2335)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:53:08 -05:00
github-actions[bot]
c5a851d162 bump(crowdstrike-falcon-foundry): b3f4ecb4 → c542c932 (#2337)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:52:36 -05:00
github-actions[bot]
00e70ff764 bump(datarobot-agent-skills): 90a33c0c → debe471c (#2339)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:52:04 -05:00
github-actions[bot]
f76697f228 bump(migration-to-aws): 30808e64 → ad7eadc8 (#2347)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:51:32 -05:00
github-actions[bot]
7e7fd1e19f bump(desktop-commander): cf857bf0 → f53f916f (#2340)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:51:24 -05:00
github-actions[bot]
1084105a18 bump(forge-skills): 2014fae5 → 02103cca (#2342)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:50:50 -05:00
github-actions[bot]
636410d7b4 bump(mcp-apps): a9907802 → ca1d2989 (#2346)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:50:15 -05:00
github-actions[bot]
fa09cccba0 bump(quarkus-agent): 01847d5d → e711107a (#2355)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:50:07 -05:00
github-actions[bot]
b41c121fef bump(auth0): 9d93554c → beda869d (#2329)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:49:31 -05:00
github-actions[bot]
58d3bf4a70 bump(oracle-ai-data-platform-workbench-spark-connectors): dcd5a5a1 → 04cc355f (#2350)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:49:06 -05:00
github-actions[bot]
e5483762ee bump(dash0): 8801a219 → 1e64ae2d (#2338)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:48:48 -05:00
github-actions[bot]
94d4566c99 bump(42crunch-api-security-testing): 1db60984 → 27815ced (#2327)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:48:22 -05:00
github-actions[bot]
2346b18566 bump(sanity): 7e049737 → 66f0ec5d (#2356)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-05 08:47:59 -05:00
Charmaine Lee
bd7cf41fc8 Merge pull request #2311 from anthropics/readd-valtown
Re-add valtown plugin entry
2026-06-04 15:13:14 -04:00
Bryan Thompson
52e0a89d75 Re-add valtown plugin entry 2026-06-04 13:56:55 -05:00
Bryan Thompson
e4afd8274a Remove valtown plugin entry (#2310) 2026-06-04 14:48:58 -04:00
Bryan Thompson
056ed579e1 Rename vals plugin to valtown (#2309) 2026-06-04 13:59:59 -04:00
Charmaine Lee
9b3f5842eb Merge pull request #2306 from anthropics/add-vals
Add vals plugin
2026-06-04 12:48:26 -04:00
github-actions[bot]
0a028a197f bump(revenuecat): 81262a33 → b34f9beb (#2299)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 11:17:39 -05:00
Bryan Thompson
971bf7f77b Add vals plugin 2026-06-04 09:41:51 -05:00
Bryan Thompson
84a0cba999 Update aws-startup-advisor listing description (#2305)
Refreshes the marketplace listing description for the AWS Startup Advisor
plugin at the maintainer's request.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 10:39:44 -04:00
github-actions[bot]
4ef055161a bump(migration-to-aws): 440c6a26 → 30808e64 (#2295)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:43:34 -05:00
github-actions[bot]
4b4a60ed46 bump(ui5-typescript-conversion): d6f37836 → 767ac53c (#2302)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:43:12 -05:00
github-actions[bot]
f525ab814a bump(crowdstrike-falcon-foundry): eb1e31d6 → b3f4ecb4 (#2292)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:42:50 -05:00
github-actions[bot]
f7407b0091 bump(agentforce-adlc): 55220ca3 → 1db738be (#2287)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:42:26 -05:00
github-actions[bot]
730d487f4d bump(carta-crm): 318b0c97 → ea02da68 (#2290)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:42:02 -05:00
github-actions[bot]
844a06dd94 bump(carta-investors): 318b0c97 → ea02da68 (#2291)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:41:38 -05:00
github-actions[bot]
83963ed0f7 bump(huggingface-skills): 49abf82b → 14cea99d (#2293)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:41:12 -05:00
github-actions[bot]
45b7e6ec36 bump(quarkus-agent): 629622a4 → 01847d5d (#2296)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:40:44 -05:00
github-actions[bot]
05c4276085 bump(hyperframes): 1a617d30 → 8228932e (#2294)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:40:16 -05:00
github-actions[bot]
fae0d05910 bump(railway): 7718b390 → 831130cd (#2297)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:39:47 -05:00
github-actions[bot]
26bccc54c1 bump(rc): 81262a33 → b34f9beb (#2298)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:39:17 -05:00
github-actions[bot]
ed56c0d518 bump(stripe): 38cc559c → e27ee009 (#2300)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:38:47 -05:00
github-actions[bot]
9ea29a2377 bump(ui5): d6f37836 → 767ac53c (#2301)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:38:13 -05:00
github-actions[bot]
1450e35366 bump(aws-startup-advisor): 440c6a26 → 30808e64 (#2288)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:37:25 -05:00
github-actions[bot]
c1f89408c2 bump(carta-cap-table): 318b0c97 → ea02da68 (#2289)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:36:50 -05:00
github-actions[bot]
790bfbdd95 bump(wix): 4a6e9dac → f99715fc (#2303)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-04 08:36:31 -05:00
github-actions[bot]
1c23816966 bump(sap-fiori-mcp-server): a5c52e7e → b326a9a5 (#2271)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:59:15 -05:00
github-actions[bot]
b5751f418c bump(carta-cap-table): 619dbde9 → 318b0c97 (#2268)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:58:07 -05:00
github-actions[bot]
ddc3437f95 bump(ui5-typescript-conversion): 7acd8328 → d6f37836 (#2279)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:57:16 -05:00
github-actions[bot]
4ebb5a75a5 bump(carta-crm): 619dbde9 → 318b0c97 (#2269)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:56:25 -05:00
github-actions[bot]
0c2a34f802 bump(carta-investors): 619dbde9 → 318b0c97 (#2270)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:55:34 -05:00
github-actions[bot]
a25353363c bump(sentry-cli): 5abcacdf → 5b78ddaf (#2272)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:54:42 -05:00
github-actions[bot]
e316c19f6f bump(servicenow-sdk): 06adf37c → 35ef6130 (#2273)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:53:51 -05:00
github-actions[bot]
6a3ae4e747 bump(teamcity-cli): 533c8cb2 → 3cc3013c (#2276)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:52:59 -05:00
github-actions[bot]
7b0c2ddd17 bump(togetherai-skills): f957e292 → 9772f2a2 (#2277)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:52:08 -05:00
github-actions[bot]
9bccbd173a bump(slack): 7b945895 → 38fb9592 (#2274)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:51:17 -05:00
github-actions[bot]
2c5a70bb49 bump(sonarqube): c64e09af → 712b9328 (#2275)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:50:25 -05:00
github-actions[bot]
25abef4427 bump(ui5): 7acd8328 → d6f37836 (#2278)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:49:34 -05:00
github-actions[bot]
1bdaa2dcea bump(wix): 2da8231f → 4a6e9dac (#2280)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:48:41 -05:00
github-actions[bot]
e5a0936496 bump(zoominfo): 678c0d1b → cfdebda5 (#2281)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:47:49 -05:00
github-actions[bot]
630feb5365 bump(data-agent-kit-starter-pack): 7b17cb51 → fb908645 (#2252)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:27:35 -05:00
github-actions[bot]
7a3f4bdaab bump(amazon-location-service): 187edde6 → fc54dfa2 (#2239)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:26:43 -05:00
github-actions[bot]
37a0394791 bump(carta-cap-table): eb00237d → 619dbde9 (#2246)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:25:52 -05:00
github-actions[bot]
b48886695f bump(chrome-devtools-mcp): 30d59a78 → 89718901 (#2249)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:24:44 -05:00
github-actions[bot]
823b7264c9 bump(crowdstrike-falcon-foundry): 5b661388 → eb1e31d6 (#2250)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:23:53 -05:00
github-actions[bot]
6cb60f9d47 bump(databases-on-aws): 187edde6 → fc54dfa2 (#2253)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:22:45 -05:00
github-actions[bot]
fed4a6b362 bump(datarobot-agent-skills): bf57624a → 90a33c0c (#2255)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:21:53 -05:00
github-actions[bot]
e23c03def5 bump(deploy-on-aws): 187edde6 → fc54dfa2 (#2256)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:21:02 -05:00
github-actions[bot]
3f0d0cca4e bump(sap-cds-mcp): 9658cea9 → b7891319 (#2267)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:20:10 -05:00
github-actions[bot]
f0800afe0c bump(apollo-skills): e1d07720 → 9ccf1347 (#2240)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:19:19 -05:00
github-actions[bot]
a121f46335 bump(aws-amplify): 187edde6 → fc54dfa2 (#2243)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:18:12 -05:00
github-actions[bot]
e203a236d0 bump(aws-serverless): 187edde6 → fc54dfa2 (#2244)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:17:20 -05:00
github-actions[bot]
dfa29c64bf bump(carta-crm): eb00237d → 619dbde9 (#2247)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:16:28 -05:00
github-actions[bot]
c0a1e1f807 bump(carta-investors): f512df80 → 619dbde9 (#2248)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:15:36 -05:00
github-actions[bot]
e9f40d88f2 bump(desktop-commander): ce4669cc → cf857bf0 (#2257)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:14:42 -05:00
github-actions[bot]
d997216b9f bump(sagemaker-ai): 187edde6 → fc54dfa2 (#2265)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:13:33 -05:00
github-actions[bot]
c480548d4a bump(airwallex): 95d59ac7 → a903ab76 (#2238)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:12:39 -05:00
github-actions[bot]
5608da5867 bump(atlan): cda594f4 → 789507c0 (#2241)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:11:30 -05:00
github-actions[bot]
1653d264d0 bump(auth0): 27b0aab6 → 9d93554c (#2242)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:10:37 -05:00
github-actions[bot]
1be78e5ccc bump(azure): b2b5d8b1 → 58fd9094 (#2245)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:09:45 -05:00
github-actions[bot]
abdccf388f bump(dash0): 03001303 → 8801a219 (#2251)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:08:52 -05:00
github-actions[bot]
eb3c22451b bump(datadog): eeb2f746 → 96c28a8c (#2254)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:07:58 -05:00
github-actions[bot]
f3d705ed97 bump(fiftyone): 6c002d68 → d34365bd (#2258)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:07:06 -05:00
github-actions[bot]
f3ee528f2b bump(hyperframes): f5d81cb5 → 1a617d30 (#2259)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:05:56 -05:00
github-actions[bot]
a6bf8efa48 bump(mapbox): fc705cd9 → 75ac667c (#2260)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:04:47 -05:00
github-actions[bot]
328152fe82 bump(mongodb): bcd65180 → 9ea7387c (#2261)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:03:54 -05:00
github-actions[bot]
7efac88777 bump(outputai): 34badf9f → d3c9b1f4 (#2262)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:03:00 -05:00
github-actions[bot]
3c5b8d2f9e bump(posthog): 3ab2bf76 → a4873114 (#2263)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:01:52 -05:00
github-actions[bot]
8b7512de3a bump(resend): 376d1c3f → 0f598ef5 (#2264)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 18:00:47 -05:00
github-actions[bot]
1a20df0d5f bump(sanity): d7545f5c → 7e049737 (#2266)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 17:59:55 -05:00
Bryan Thompson
6f9037182d Add vanta listing for VantaInc/vanta-mcp-plugin (#2236)
Adds an additional marketplace entry named `vanta` pointing to the same
source as the existing `vanta-mcp-plugin` entry (VantaInc/vanta-mcp-plugin
at the pinned SHA `345d86b5`). No code change — same upstream the existing
entry already uses; this exposes the plugin under the developer's canonical
plugin name (`vanta`, per the repo's plugin.json) alongside the existing slug.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:19:00 -07:00
Bryan Thompson
42eb33dcd1 Add airwallex plugin (#2194) 2026-06-03 12:51:17 -07:00
github-actions[bot]
ab91f484de bump(sap-fiori-mcp-server): 7432d23a → a5c52e7e (#2147)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 14:18:37 -05:00
github-actions[bot]
4979da050c bump(carta-investors): e66d331c → f512df80 (#2128)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 19:05:51 +00:00
Bryan Thompson
66c63c89d2 Add lusha plugin (#2230) 2026-06-03 11:07:35 -07:00
github-actions[bot]
1d93263223 bump(sap-cds-mcp): 92dc99f5 → 9658cea9 (#2146)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 09:21:44 -05:00
github-actions[bot]
d81de2d9ab bump(remember): c2c82ab5 → a4ff96f3 (#2145)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 09:21:18 -05:00
github-actions[bot]
72d0792fe9 bump(auth0): c38453f6 → 27b0aab6 (#2122)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 09:20:54 -05:00
github-actions[bot]
9f597b4ef2 bump(carta-crm): 49cfc652 → eb00237d (#2207)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 09:19:36 -05:00
github-actions[bot]
4f16798996 bump(oracle-ai-data-platform-workbench-spark-connectors): d20075e4 → dcd5a5a1 (#2222)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:26:20 -05:00
github-actions[bot]
1e3d218e11 bump(data-agent-kit-starter-pack): 86eb482b → 7b17cb51 (#2213)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:25:04 -05:00
github-actions[bot]
b4a11cf644 bump(42crunch-api-security-testing): b404d99a → 1db60984 (#2198)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:23:42 -05:00
github-actions[bot]
406fd96219 bump(aws-data-analytics): ba1cc8ca → df13dea6 (#2203)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:22:20 -05:00
github-actions[bot]
6a38aaef26 bump(aws-startup-advisor): 23a4f5ea → 440c6a26 (#2204)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:21:02 -05:00
github-actions[bot]
814ef4af89 bump(carta-cap-table): 49cfc652 → eb00237d (#2206)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:19:45 -05:00
github-actions[bot]
a070a4dba3 bump(chrome-devtools-mcp): 692b28ad → 30d59a78 (#2209)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:18:29 -05:00
github-actions[bot]
cc9a6c8269 bump(crowdstrike-falcon-foundry): 2908d2f4 → 5b661388 (#2211)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:17:13 -05:00
github-actions[bot]
8fb820f67c bump(datarobot-agent-skills): 1bef2843 → bf57624a (#2214)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:15:59 -05:00
github-actions[bot]
9b2e502281 bump(agentforce-adlc): 1584dd52 → 55220ca3 (#2199)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:14:40 -05:00
github-actions[bot]
6e6310b80a bump(atomic-agents): 57d6099f → 32439940 (#2200)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:13:42 -05:00
github-actions[bot]
fdb10ece9e bump(aws-agents): ba1cc8ca → df13dea6 (#2201)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:12:26 -05:00
github-actions[bot]
bc4d00b957 bump(aws-core): ba1cc8ca → df13dea6 (#2202)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:11:27 -05:00
github-actions[bot]
f93710e514 bump(cds-mcp): 9658cea9 → b7891319 (#2208)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:09:38 -05:00
github-actions[bot]
8862801139 bump(mcp-apps): 7d4434e9 → a9907802 (#2219)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:08:25 -05:00
github-actions[bot]
6dd6f9c21e bump(nvidia-skills): 91dbdf23 → e695a839 (#2221)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:07:27 -05:00
github-actions[bot]
a432b3965a bump(pydantic-ai): 56dadc9c → e412b6d8 (#2225)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:06:28 -05:00
github-actions[bot]
d6ccaa6ee2 bump(qdrant-skills): ea62a985 → cace39df (#2226)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:05:14 -05:00
github-actions[bot]
b8ccdcc586 bump(quarkus-agent): 065b4595 → 629622a4 (#2227)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:03:56 -05:00
github-actions[bot]
8f42f64807 bump(azure): d3440b8a → b2b5d8b1 (#2205)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:02:33 -05:00
github-actions[bot]
9ea894cf81 bump(clickhouse): bfd22b9c → 1f30864b (#2210)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:01:19 -05:00
github-actions[bot]
db0594acbe bump(dash0): 0ec3db6b → 03001303 (#2212)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 08:00:05 -05:00
github-actions[bot]
764574c529 bump(firecrawl): 81178096 → 6768fb78 (#2215)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 07:59:09 -05:00
github-actions[bot]
baa919e1e0 bump(hunter): 4eb5fbbc → 69c4e59e (#2216)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 07:58:15 -05:00
github-actions[bot]
14485f083b bump(hyperframes): 3c7e2f36 → f5d81cb5 (#2217)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 07:57:03 -05:00
github-actions[bot]
f41adcdec4 bump(logfire): 56dadc9c → e412b6d8 (#2218)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 07:55:49 -05:00
github-actions[bot]
defa28f86c bump(miro): da5405f8 → 9d7c3dc0 (#2220)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 07:54:52 -05:00
github-actions[bot]
64d6c18552 bump(outputai): 5a87ebc1 → 34badf9f (#2223)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 07:53:38 -05:00
github-actions[bot]
614fb60707 bump(posthog): 1b743cdb → 3ab2bf76 (#2224)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-03 07:52:42 -05:00
Bryan Thompson
c917011ca6 Add migration-to-aws plugin (#2195) 2026-06-02 23:48:35 +01:00
Mohamed Hegazy
d4d90844a0 Merge pull request #2193 from anthropics/fix-2159-stop-hook-schema
security-guidance: emit schema-valid Stop-hook output (#2159) — 2.0.2 → 2.0.3
2026-06-02 15:17:34 -07:00
Mohamed Hegazy
70c28b9c2f security-guidance: emit schema-valid Stop-hook output (#2159) — 2.0.2 → 2.0.3
Fixes #2159. The Stop hook emits feedback via
`hookSpecificOutput: {hookEventName: "Stop", additionalContext}`, but
`Stop`/`SubagentStop` are NOT members of CC's `hookSpecificOutput`
discriminated union (coreSchemas.ts — valid members are PreToolUse,
PostToolUse, UserPromptSubmit, SessionStart, etc.). So the emitted shape
violates CC's documented hook-output schema.

Impact is CC-version-dependent — important nuance, established empirically:

  - Reporter (array0224-cloud) on CLI 2.1.150 / 2.1.152: CC rejects the
    Stop feedback; the block/reason never reaches the model, so the
    auto-rewake/fix loop is lost. (Detection still runs + logs.)

  - On CLI 2.1.160 (current) the asyncRewake completion path is lenient:
    its gate is `isSyncHookJSONOutput` (hooks.ts) which is just
    `!(json.async === true)` — NOT a strict schema parse. So the invalid
    hookSpecificOutput is tolerated: metrics + rewakeSummary are still
    consumed and the model still receives the findings. I could NOT
    reproduce the rejection on 2.1.160, and BQ confirms Stop-path
    vulns_found metrics are recorded normally (~21k with-vuln fires / 3d),
    i.e. NOT dropped. (An earlier draft of this message claimed metrics
    were dropped — that was wrong; corrected after checking telemetry +
    repro'ing the old plugin on 2.1.160.)

So this is defensive schema-correctness: the plugin should emit output
that conforms to CC's documented union regardless of how strictly a given
CC version validates it. The reporter's environment validates strictly;
relying on the current version's leniency is fragile.

Fix (CC's documented asyncRewake "clean pattern" — hooks.ts: "error text
on stderr, JSON on stdout"):
  - For Stop/SubagentStop, emit_metrics writes guidance to stderr (the
    asyncRewake body channel CC delivers via `stderr || stdout`) and sets
    top-level `decision: "block"` + `reason` (valid SyncHookJSONOutput
    fields; also the documented sync Stop-hook contract for the `-p`
    fallback). It does NOT emit a Stop hookSpecificOutput.
  - PostToolUse (commit-review, push-sweep) is unchanged — valid union
    member, keeps the modern hookSpecificOutput protocol.

Verified locally on macOS Python 3.13:

  - py_compile clean.
  - 11 new tests (test_2159_stop_hook_schema.py) pin the contract: Stop
    output carries no hookSpecificOutput, uses top-level decision/reason,
    writes guidance to stderr; the emitted hookEventName (when present) is
    a valid union member. 2 existing tests that asserted the buggy
    Stop->hookSpecificOutput shape were corrected. Full suite 464/464
    pass + 2 skipped.

  - END-TO-END in /tmux on CLI 2.1.160:
    * FIXED plugin (2.0.3): staged pickle.loads + os.system, benign edit
      pulls the file into review_set; Stop LLM review found 2 critical
      vulns; CC delivered a clean rewake ("Background security review
      found issues" + both findings). All hooks (UPS, PostToolUse[Edit]
      pattern, PostToolUse[Bash] commit-review, Stop) fired clean; zero
      schema rejections / errors / http_err in the debug log.
    * OLD plugin (2.0.2) on the SAME 2.1.160: also delivered Stop feedback
      (confirming the no-repro-on-latest finding above) — which proves the
      fix carries NO regression risk on current CC while making the output
      robust for the stricter versions where it actually breaks.

Version bumped 2.0.2 -> 2.0.3 per the per-PR-bump policy (#2114: a bump is
the only way the fix reaches the existing fleet — relevant for users still
on the CC versions where this breaks).

Closes #2159.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-02 11:30:11 -07:00
github-actions[bot]
63457674dd bump(oracle-ai-data-platform-workbench-spark-connectors): 6e59f24c → d20075e4 (#2141)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 10:06:15 -05:00
github-actions[bot]
7d10ac5f67 bump(crowdstrike-falcon-foundry): fb25d60e → 2908d2f4 (#2133)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 10:03:48 -05:00
github-actions[bot]
9bd1e49f2e bump(adobe-for-creativity): 8d74ee6b → e23271f6 (#2161)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:57:00 -05:00
github-actions[bot]
c7abc99aa1 bump(amazon-location-service): f16aaf2a → 187edde6 (#2163)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:55:47 -05:00
github-actions[bot]
3a0bba1d81 bump(carta-cap-table): f512df80 → 49cfc652 (#2169)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:54:33 -05:00
github-actions[bot]
c326beefc4 bump(chrome-devtools-mcp): 2e039c09 → 692b28ad (#2171)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:53:19 -05:00
github-actions[bot]
cc8231f0e8 bump(datarobot-agent-skills): 4c3dfbd2 → 1bef2843 (#2172)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:52:06 -05:00
github-actions[bot]
768c45f7c7 bump(cds-mcp): 92dc99f5 → 9658cea9 (#2129)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:50:52 -05:00
github-actions[bot]
890215ba95 bump(huggingface-skills): df627be1 → 49abf82b (#2138)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:47:11 -05:00
github-actions[bot]
77c7b71496 bump(quarkus-agent): 32cad78b → 065b4595 (#2144)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:44:02 -05:00
github-actions[bot]
8e3e55e512 bump(aws-amplify): f16aaf2a → 187edde6 (#2165)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:40:50 -05:00
github-actions[bot]
a8c0b70061 bump(aws-serverless): f16aaf2a → 187edde6 (#2166)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:39:37 -05:00
github-actions[bot]
4167d03e26 bump(bigdata-com): c77a09ca → 67c30be9 (#2167)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:38:22 -05:00
github-actions[bot]
8900b21abc bump(brightdata-plugin): da735491 → 68651246 (#2168)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:37:06 -05:00
github-actions[bot]
0e4789818a bump(carta-crm): f512df80 → 49cfc652 (#2170)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:35:52 -05:00
github-actions[bot]
89a3934ffa bump(desktop-commander): 9c44119a → ce4669cc (#2173)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:34:38 -05:00
github-actions[bot]
4053de6b37 bump(duende-skills): 2c803785 → 72e39de9 (#2175)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:33:23 -05:00
github-actions[bot]
907ad63743 bump(mcp-apps): 9a37ad71 → 7d4434e9 (#2177)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:32:08 -05:00
github-actions[bot]
75c86cb7ad bump(nvidia-skills): 62b685a2 → 91dbdf23 (#2179)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:30:54 -05:00
github-actions[bot]
b9447ff795 bump(pydantic-ai): eb17c0da → 56dadc9c (#2181)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:29:39 -05:00
github-actions[bot]
9a8303c6d9 bump(sentry-cli): db907679 → 5abcacdf (#2182)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:28:22 -05:00
github-actions[bot]
9557d751dc bump(teamcity-cli): 9436b94b → 533c8cb2 (#2185)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:27:08 -05:00
github-actions[bot]
eac1df92db bump(togetherai-skills): a1277729 → f957e292 (#2186)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:25:52 -05:00
github-actions[bot]
4263502749 bump(vibe-prospecting): c00b11db → 7ed0c4e2 (#2187)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:24:36 -05:00
github-actions[bot]
c0c282b87b bump(azure): 7cb89c22 → d3440b8a (#2125)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:23:22 -05:00
github-actions[bot]
abeea5843f bump(clickhouse): 36889764 → bfd22b9c (#2130)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:21:33 -05:00
github-actions[bot]
a5d43627ef bump(codspeed): 407dd3c9 → f79d57d2 (#2131)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:18:56 -05:00
github-actions[bot]
6a8591db5f bump(convex): ece93250 → 002f9c83 (#2132)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:16:18 -05:00
github-actions[bot]
31fd7f0923 bump(dash0): d1ad56f8 → 0ec3db6b (#2134)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:13:58 -05:00
github-actions[bot]
9cb21aab75 bump(firecrawl): e71cec48 → 81178096 (#2137)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:11:53 -05:00
github-actions[bot]
7328989616 bump(hunter): 9b614652 → 4eb5fbbc (#2139)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:09:48 -05:00
github-actions[bot]
8a7f6912b2 bump(hyperframes): bc3701f5 → 3c7e2f36 (#2140)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:07:26 -05:00
github-actions[bot]
a6ce4ca3d5 bump(outputai): 0eeffece → 5a87ebc1 (#2142)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:05:21 -05:00
github-actions[bot]
785a75e88b bump(pigment): 4bf16c80 → abf36e64 (#2143)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:03:34 -05:00
github-actions[bot]
697a046997 bump(sentry): d6123be3 → 849303a8 (#2148)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 09:00:06 -05:00
github-actions[bot]
8d45b83d6c bump(alloydb): 4a756532 → bbf4eb36 (#2162)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 08:58:31 -05:00
github-actions[bot]
87e08885b5 bump(atlan): b0efcc8e → cda594f4 (#2164)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 08:57:16 -05:00
github-actions[bot]
2de71f5542 bump(dominodatalab): 47c6e0a7 → 56c3fc39 (#2174)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 08:56:01 -05:00
github-actions[bot]
798cb06aa3 bump(logfire): eb17c0da → 56dadc9c (#2176)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 08:54:47 -05:00
github-actions[bot]
2a22053549 bump(postiz): 238aede6 → 41c5a9db (#2180)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 08:53:31 -05:00
github-actions[bot]
754f7f2f54 bump(stripe): 99425a01 → 38cc559c (#2183)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 08:52:16 -05:00
github-actions[bot]
05107962e7 bump(superpowers): f2cbfbef → 6fd45076 (#2184)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 08:51:01 -05:00
github-actions[bot]
56a8f8df52 bump(wix): c5b343f2 → 2da8231f (#2188)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-02 08:49:04 -05:00
Mohamed Hegazy
bf7e852731 Merge pull request #2154 from anthropics/venv-failure-deepdive
security-guidance: 5 venv-specific err_kind categories + stderr_signature bucket (2.0.1 → 2.0.2)
2026-06-01 17:29:36 -07:00
Bryan Thompson
3866e34b15 Add sagemaker-ai plugin (re-list after YAML fix) (#2158)
sagemaker-ai was dropped from the marketplace in #1762 (validate-plugins
adoption) due to a manifest/YAML error. AWS has since fixed it; the plugin
validates clean at awslabs/agent-plugins@187edde (claude plugin validate passes).

Re-listed as a git-subdir source SHA-pinned to current monorepo HEAD,
matching its sibling AWS entries (deploy-on-aws, databases-on-aws).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 17:25:46 -07:00
Bryan Thompson
3d490adc34 bump(databases-on-aws, deploy-on-aws): f16aaf2a → 187edde (#2157)
Both plugins in awslabs/agent-plugins had their subpaths edited in commit
187edde (after the morning bump cron pinned them to f16aaf2a), so they fell
behind again on merge. Manual catch-up bump to current monorepo HEAD.

- databases-on-aws: 4 files changed under plugins/databases-on-aws/ (v1.1.0)
- deploy-on-aws:    7 files changed under plugins/deploy-on-aws/ (v1.2.0)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 17:25:16 -07:00
github-actions[bot]
08d1b59559 bump(amazon-location-service): 9d46cc0a → f16aaf2a (#2120)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-01 19:08:14 -05:00
github-actions[bot]
49880c89fe bump(databases-on-aws): 9d46cc0a → f16aaf2a (#2135)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-01 18:42:30 -05:00
github-actions[bot]
7951b76e19 bump(deploy-on-aws): 9d46cc0a → f16aaf2a (#2136)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-01 18:41:19 -05:00
Bryan Thompson
9cc1748a65 Add aws-startup-advisor plugin (#2156) 2026-06-02 00:38:25 +01:00
Mohamed Hegazy
009392eee4 security-guidance: 5 venv-specific err_kind categories + stderr_signature bucket (2.0.1 → 2.0.2)
PR #2112's telemetry visibility surfaced an immediate finding from
the first 3h of v2.0.1 data: **2,406 phase=2 / err=99 sessions** —
"venv stage / uncategorized" — dominating BUILD_FAILED. The original
err_kind detection patterns were all pip-flavored (pip_no_match,
dns_fail, ssl_verify, etc.) and didn't catch venv-creation failure
modes, so they all collapsed to the catch-all _uncategorized (99)
bucket.

This PR fills the gap on two axes.

## 1. Five new venv-specific err_kind categories (codes 11-15)

Each gated on `err_phase == "venv"` so the same substring doesn't
mis-fire in pip-phase failures:

  - 11 `venv_ensurepip_fail` — Debian/Ubuntu without python3-venv
    installed; stderr matches "ensurepip is not available" or
    "ensurepip ... returned non-zero". Predicted to be the biggest
    chunk based on Linux distro market share.
  - 12 `venv_path_too_long` — Windows MAX_PATH (260) or POSIX
    ENAMETOOLONG. Triggered when state_dir + venv layout exceeds
    the path limit (deep Lib/site-packages/<pkg>/<...> paths).
  - 13 `venv_no_module` — `python3 -m venv` itself missing
    ("No module named 'venv'"). Rare but distinctive.
  - 14 `venv_already_exists` — Errno 17 / "file exists" — sentinel
    race past O_EXCL or stale dir survived `--clear`.
  - 15 `venv_setup_failed` — generic "virtual environment was not
    created successfully" catch-all for venv setup failures that
    don't match a more specific category.

All 5 occupy reserved slots in SDK_BOOTSTRAP_ERR_CODES per the
APPEND-ONLY contract from PR #2112.

## 2. `sdk_bootstrap_stderr_sig` integer hash

For "other:<tail>" err_kinds (which encode to _uncategorized = 99),
emit a bounded integer hash (0-999) of the first ~30 chars of the
stderr tail. This restores cardinality to the _uncategorized bucket
in BQ aggregation without unbounded keyspace — same stderr message
always maps to the same bucket, so a real failure mode replicating
across thousands of machines clusters cleanly. Bounded at 1000
buckets: well below any "high cardinality" alarm but wide enough to
distinguish ~30 distinct dominant patterns (birthday-paradox
collision probability ~50% at ~37 distinct inputs).

The field auto-omits (`if sig:` gate) when err_kind is categorized
— no key-budget cost on the common-case categorized failures.

## Version bump 2.0.1 → 2.0.2

PR #2114 confirmed the version-bump mechanism is the only way to
propagate code changes to the existing fleet — without a bump, CC's
plugin updater short-circuits on string-equality of installation
version vs marketplace version. Following the policy we established:
**bump patch on every functional PR**.

By 17:31:42Z on 2026-06-01 (1m22s after #2114 merged), v2.0.1 was
already appearing in BQ. v2.0.2 should follow the same propagation
curve — ~30% adoption within 3 hours, full convergence within a few
days.

## Verified locally

  - py_compile clean.
  - 15 new tests in test_venv_failure_deepdive.py (added to internal
    test suite at sg-staging/tests/, not in this PR):

      * 5 parametrized: each new err_kind maps to its expected code (11-15).
      * 1 APPEND-ONLY regression: existing codes 1-10 + 99 unchanged.
      * 6 stderr_sig: non-other inputs → 0; None/empty → 0; deterministic
        same-input → same-output; bounded to 0-999; distinct inputs →
        distinct hashes (5/5 with P(collision) ≈ 1%); leading-chars focus
        (path-varying stderr with shared 30-char prefix collide as designed).
      * 1 static-shape catcher: every new `err_kind = "venv_..."` branch
        in main() is guarded by `err_phase == "venv"`. Catches the
        regression where someone adds a venv pattern without the phase
        gate and starts mis-categorizing pip-phase failures.
      * 1 map-coverage: all err_kind strings assigned anywhere in
        ensure_agent_sdk.main() are present in SDK_BOOTSTRAP_ERR_CODES
        (catches new categories added in code but forgotten in the map).
      * 1 emit-shape: the metric block uses `_encode_stderr_sig`, the
        `sdk_bootstrap_stderr_sig` key is written conditionally on `if
        sig:`. Catches the regression where someone removes the
        helper or makes the emit unconditional (would pad every
        categorized BUILD_FAILED row with a zero-valued field).

  - Full suite: 452/452 pass + 2 skipped (live API tests, opt-in).

## What this unblocks in BQ

```sql
-- For the 2,406 sessions/3h that were phase=2/err=99 on v2.0.1,
-- v2.0.2+ will split them across the new categories. Query:
SELECT
  CAST(JSON_VALUE(additional_metadata, "$.sdk_bootstrap_err") AS INT64) AS err,
  CAST(JSON_VALUE(additional_metadata, "$.sdk_bootstrap_stderr_sig") AS INT64) AS sig,
  COUNT(*) AS sessions
FROM `proj-product-data-nhme.raw_events.claude_code_internal_event`
WHERE _PARTITIONTIME >= ...
  AND CAST(JSON_VALUE(additional_metadata, "$.sdk_bootstrap") AS INT64) = 3
  AND CAST(JSON_VALUE(additional_metadata, "$.sdk_bootstrap_phase") AS INT64) = 2  -- venv
GROUP BY err, sig
ORDER BY sessions DESC
```

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 16:05:49 -07:00
Mohamed Hegazy
9f6eae5114 Merge pull request #2155 from anthropics/fix-nvidia-skills-sha
fix(nvidia-skills): add missing source.sha (validator invariant I5; unblocks all PRs touching marketplace.json)
2026-06-01 15:57:55 -07:00
github-actions[bot]
1fe78a3f60 bump(carta-crm): e66d331c → f512df80 (#2127)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-01 17:49:39 -05:00
Mohamed Hegazy
267c4e6f06 fix(nvidia-skills): add missing source.sha (validator invariant I5)
The nvidia-skills entry was added in PR #2088 with:

  "source": {
    "source": "git-subdir",
    "url": "https://github.com/NVIDIA/skills.git",
    "path": "plugins/nvidia-skills",
    "ref": "main"
  }

It's missing the required `sha` field. The marketplace validator
enforces invariant I5 ("source.sha is missing or not a 40-char hex
SHA") on every git-subdir source — without it, the action fails:

  ##[error]invariant I5: nvidia-skills: source.sha is missing or not
  a 40-char hex SHA

This has been silently failing the "Validate Plugins" CI on every
PR that touches marketplace.json since #2088 merged on 2026-05-03.
Confirmed by checking the last 5 completed validate runs on main —
all 5 , including PR #2114 (security-guidance bump that you merged
earlier today). The validator failure was getting swallowed because
all the other PR-level checks (Check MCP URLs, Scan Plugins, Validate
Plugin Licenses) were passing, and humans were `gh pr merge --admin`-ing
through it.

Fix: add the sha field pinned to the current upstream HEAD of
github.com/NVIDIA/skills.git on the `main` branch.

  Resolved via: git ls-remote https://github.com/NVIDIA/skills.git refs/heads/main
  SHA:          62b685a20ac45285cafd1e22782abbed33172c17

This mirrors the shape of other git-subdir entries with both `ref`
and `sha` (e.g. 42crunch-api-security-testing pins ref="v1.5.5",
sha="b404d99a...", adobe-for-creativity pins ref="main", sha="8d74ee6b...").

Unblocks every in-flight PR that touches marketplace.json — including
PR #2154 (security-guidance venv-deepdive) which is currently
red-blocked on this.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 15:24:29 -07:00
github-actions[bot]
12b3721b22 bump(carta-cap-table): e66d331c → f512df80 (#2126)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-01 15:28:15 -05:00
github-actions[bot]
e11db042eb bump(aws-serverless): 9d46cc0a → f16aaf2a (#2124)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-01 15:28:09 -05:00
github-actions[bot]
b92bc59595 bump(aws-amplify): 9d46cc0a → f16aaf2a (#2123)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-01 15:25:31 -05:00
Mohamed Hegazy
fcdcd079e3 Merge pull request #2112 from anthropics/telemetry-failure-signals
security-guidance: emit HTTP error codes + fix sdk_bootstrap phase/err encoding (telemetry visibility)
2026-06-01 10:31:29 -07:00
Mohamed Hegazy
5adb5a2d26 Merge pull request #2114 from anthropics/bump-2-0-1-propagate-fixes
security-guidance: bump 2.0.0 → 2.0.1 to propagate 8 weeks-of-fixes to existing users
2026-06-01 10:30:17 -07:00
github-actions[bot]
a63dc11763 bump(atomic-agents): bb9708ec → 57d6099f (#2121)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-01 12:12:10 -05:00
github-actions[bot]
025f4d4477 bump(adobe-for-creativity): 0a015c06 → 8d74ee6b (#2119)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-06-01 11:12:45 -05:00
Bryan Thompson
e586a0fc00 Add nvidia-skills plugin (#2088) 2026-06-01 09:09:21 -07:00
Bryan Thompson
0d82eac145 bump: switch to per-entry PR mode (one PR per stale plugin) (#2051)
* bump: switch to per-entry PR mode (one PR per stale plugin)

Replaces the single batched bump PR with one PR per stale plugin so a
single failing plugin no longer blocks the rest. Pins to a feature
branch of the bump-plugin-shas action that adds 'pr-mode: per-entry';
re-pin to the merge commit on the action's main when that lands.

- pr-mode: per-entry → one PR per plugin on bump/<slug>
- max_bumps default lowered 130 → 30 (per-entry scans cost more)
- scan dispatch fanned out over pr-urls JSON (one per per-entry branch)
- header comments updated for per-entry semantics

* bump: re-pin to merged composite action SHA on -community main

The pr-mode: per-entry input now lives on main of the bump-plugin-shas
action (merged at e2019b2a). Update the pin and drop the now-stale
header comment that tracked the feature branch.

* bump: dispatch all three required checks per per-entry PR

Bump PRs are opened with GITHUB_TOKEN, which doesn't fire on:pull_request
(recursion guard). The per-entry cutover already dispatched scan-plugins.yml
per branch to satisfy the `scan` required check, but `check` (Check MCP URLs)
and `validate` (Validate Plugins) are also required on main and likewise
never fired — leaving every bump PR BLOCKED on missing checks (observed on
the batched #2079, which only cleared after a human-authored push re-fired
the pull_request workflows).

Fix: dispatch all three workflows per per-entry bump branch. Each runs its
job unconditionally on workflow_dispatch, so the check run lands on the
branch HEAD (= PR head) and satisfies the required check.

- validate-plugins.yml: add workflow_dispatch trigger (check-mcp-urls.yml
  already had one). gh workflow run requires the trigger on the default
  branch; this lands together with the per-entry bump so main stays
  consistent.
- bump-plugin-shas.yml: loop the dispatch over
  {scan-plugins,check-mcp-urls,validate-plugins}; tolerate a single
  transient dispatch failure (warn, don't abort) so one hiccup can't
  strand the rest of the batch.

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

* bump: fail the per-entry check-dispatch step when a dispatch fails

The dispatch step logged each failed gh workflow run as a warning and exited 0, so a transient API error or rate limit could leave a per-entry bump PR missing a required check while the bump run still showed green. The composite action skips slugs with an open PR, so the stranded PR was never retried.

Attempt every dispatch (one failure must not strand the other branches), record failures via a temp file (the while loop runs in a pipe subshell), then emit an error and exit non-zero if any dispatch failed, so the bump run goes red and the affected PR can be re-dispatched.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 06:38:22 +01:00
Mohamed Hegazy
17b532f92e security-guidance: bump 2.0.0 → 2.0.1 to propagate 8 weeks-of-fixes to the existing fleet
The 8 PRs we shipped since 2026-05-26 (#2076, #2077, #2078, #2086,
#2091, #2100, #2101, #2105) all changed plugin code without bumping
the version. CC's plugin updater uses string equality for the
freshness check (pluginOperations.ts:1835):

    const isUpToDate =
      installation.version === newVersion ||
      installation.installPath === versionedPath ||
      installation.installPath === zipPath
    if (isUpToDate) return { alreadyUpToDate: true }

Users who installed v2.0.0 anywhere between 2026-05-26 and 2026-05-31
have `installation.version === "2.0.0"` in their installed_plugins.json.
The marketplace also advertises "2.0.0" (until this commit), so
isUpToDate returns true and the plugin cache directory is never
refreshed — they keep running whatever 2.0.0 code was current on the
day they installed. The marketplace git pull happens; the per-user
cache install does NOT.

Empirical evidence: in BQ today (5/31) on Windows v2.0.0 fires,
**73% emit sdk_bootstrap outcome 4 (SKIP_WIN32)** — a code path
retired in PR #2055's Windows-enable fix. Those users are running a
plugin tree that pre-dates the fix, even though their telemetry
shows pv=20000.

The fix is a one-line version bump. Once the marketplace advertises
2.0.1, every CC autoupdate cycle sees installation.version (2.0.0)
!= newVersion (2.0.1), installs the new version, and the user's next
session loads the fixed code.

This PR:

1. plugins/security-guidance/.claude-plugin/plugin.json: 2.0.0 → 2.0.1
2. .claude-plugin/marketplace.json security-guidance entry: 2.0.0 → 2.0.1

What 2.0.1 carries (versus 2.0.0 as published 5/26):

  - #2076 — Graphite gt commit/push detection
  - #2077 — hookSpecificOutput.additionalContext on async-rewake exit-2
  - #2078 — CLAUDE_CONFIG_DIR support
  - #2086 — core.quotePath=false on diff feeders (Arabic/Hebrew/CJK paths)
  - #2091 — fix Bash(...|...) if-clause regression from #2076
  - #2100 — drop text=True from subprocess.run, bake PYTHONUTF8=1 (Windows non-cp1252 path crash)
  - #2101 — core.quotePath=false on GIT_CMD globally
  - #2105 — output_format → output_config.format API migration (#2098)

Verified locally:

  - plugin.json + marketplace.json both valid JSON.
  - _read_plugin_version_int() returns 20001 (was 20000).
  - Existing test suite passes — 408 tests, no regressions caused by
    the version bump itself. (29 unrelated failures are from
    test_telemetry_failure_signals.py which expects PR #2112's
    not-yet-merged code.)

Going forward: bumping `patch` on every functional PR closes this
gap entirely. Without that policy, every fix only reaches NEW
installs, never the existing fleet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 12:10:16 -07:00
Mohamed Hegazy
475038edfc security-guidance: emit HTTP error codes + fix sdk_bootstrap phase/err encoding
Fills two failure-visibility gaps in plugin telemetry.

## Gap 1: HTTP errors from _call_claude invisible

Before: a 4xx/5xx response from the LLM API caused `_call_claude` to
return None and produce ZERO fingerprint in tengu_hook_plugin_metrics.
A failed call looked identical to "no review needed". The recent
deprecation-400 outage (PR #2105, output_format → output_config.format,
#2098) was invisible in aggregate dashboards until a user manually
reported errors from their debug log. Cohort-specific or partial
outages would never show up in BQ.

Fix: add `http_err_last` (most recent status) and `http_err_count`
to the existing `_USAGE` accumulator in `_base.py`. `_usage_metrics()`
snapshots them whenever count > 0 (skip-path no-pollute contract
preserved when count == 0). All `_call_claude` error sites now call
the new `_record_http_error()` helper alongside the existing
`_last_call_claude_http_error` module-state assignment.

Now any future API failure category is queryable in BQ in real time:

  SELECT
    DATE(server_timestamp, "America/Los_Angeles") AS d,
    CAST(JSON_VALUE(additional_metadata, "$.http_err_last") AS INT64) AS code,
    COUNT(*) AS n
  FROM ... WHERE event_name = "tengu_hook_plugin_metrics"
    AND JSON_VALUE(additional_metadata, "$.pluginId") LIKE "%security-guidance%"
    AND JSON_VALUE(additional_metadata, "$.http_err_count") IS NOT NULL
  GROUP BY d, code ORDER BY d, n DESC

## Gap 2: sdk_bootstrap_phase / sdk_bootstrap_err always NULL in BQ

Before: ensure_agent_sdk.py emitted these as strings (e.g. "pip",
"dns_fail"). CC's plugin-metrics pipeline silently drops
plugin-emitted string values — only bool|finite-number plugin metrics
reach BigQuery. (CC-core fields like `subscription_type` are exempt
because they're injected downstream of plugin validation.)

Confirmed empirically: ~185K BUILD_FAILED rows in BQ over the past 2
days had `sdk_bootstrap_phase` = NULL and `sdk_bootstrap_err` = NULL
despite the Python code emitting them. ~28K BUILD_FAILED sessions/day
had no diagnostic split — flying blind on whether the failures are
pip-no-match vs dns-fail vs ssl-verify vs proxy-auth etc.

Fix: encode phase + err_kind as stable integers via
SDK_BOOTSTRAP_PHASE_CODES and SDK_BOOTSTRAP_ERR_CODES. Phase: 1=pre,
2=venv, 3=pip, 4=main. Err: 10 known categories (1-10), 11-98
reserved, 99 = uncategorized catch-all (covers "exc:<X>", "other:<X>",
and unmapped strings). APPEND-ONLY for telemetry stability.

Also corrects the misleading "CC accepts string metric values"
comment in ensure_agent_sdk.py that led to the bug originally.

Verified locally on macOS Python 3.13:

  - py_compile clean.
  - 32 new tests in test_telemetry_failure_signals.py (added to
    internal test suite at sg-staging/tests/, not in this PR):

      * 4 HTTP-error tracking unit tests: _record_http_error increments
        count + tracks most-recent; handles None/invalid; -1 for
        network/timeout.
      * 4 _usage_metrics emission tests: empty when no activity;
        successful call has no http_err fields; failure-only has http_err
        and no api_calls; mixed has both.
      * 1 contract test: every emitted value is bool|finite-number
        (catches future regression of the string-dropping bug class).
      * 13 sdk_bootstrap encoding tests (parametrized over the 10 known
        err_kind categories + 5 catch-all shapes): each maps to the
        right integer; unknown phase = 0; unknown err = 99.
      * 1 static-shape regression catcher: every `err_kind = "..."`
        string in ensure_agent_sdk.main() must be in
        SDK_BOOTSTRAP_ERR_CODES (otherwise new err_kinds silently
        collapse to 99).
      * 2 emit-shape regression catchers: the assignments in main() go
        through _encode_phase / _encode_err_kind helpers (no raw
        strings); no literal string values for sdk_bootstrap_phase/err.
      * 1 comment-accuracy: the misleading "CC accepts string metric
        values" comment is gone.

  - Full suite: 437/437 pass + 2 skipped (live API tests, opt-in).

NOT verified end-to-end against BQ — would require shipping +
observing in production for 24h to confirm the http_err and
sdk_bootstrap_phase/err fields actually appear in
tengu_hook_plugin_metrics rows. The unit tests pin the contract; if
the wire shape is broken, BQ will show NULL for the new fields and we
revisit (with the same diagnostic the BUILD_FAILED bug gave us).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-31 08:34:35 -07:00
Mohamed Hegazy
3d368d2972 Merge pull request #2105 from anthropics/fix-2098-output-config-format
security-guidance: migrate _call_claude from deprecated output_format to output_config.format (#2098)
2026-05-30 22:32:02 -07:00
Mohamed Hegazy
84011d43b1 security-guidance: migrate from deprecated output_format to output_config.format (#2098)
Fixes #2098. The Anthropic Messages API moved structured-output
schema specification from a top-level `output_format` field to a
nested `output_config.format` field, per
https://platform.claude.com/docs/en/build-with-claude/structured-outputs.

Per docs the old form "will continue working for a transition period"
— and indeed for api-key + non-streaming auth it still returns HTTP
200 (verified via live API). But OAuth Bearer users with CLI 2.1.158
hit `invalid_request_error: output_format: This field is deprecated.
Use 'output_config.format' instead.` consistently — reporter saw 462
errors in one day. The trigger appears to be auth mode + possibly
stream:true (their controlled curl bypass used Bearer + stream=true);
api-key + non-streaming was my initial repro attempt and didn't fire.

The bug only affected `_call_claude` (the legacy direct-urllib path).
The agentic `_agentic_review` path goes through claude_agent_sdk →
subprocesses to the `claude` CLI binary, which already uses the new
`output_config.format` shape correctly (per src/utils/sideQuery.ts:263
in claude-cli-internal). So this PR only needs to fix the plugin's
direct HTTP path.

This commit:

1. llm.py: rewrite the payload literal in `_call_claude` to use
   `output_config: { format: { type: 'json_schema', schema: ... } }`
   instead of top-level `output_format`.

2. llm.py: in the adaptive-thinking branch, MERGE `effort: "high"`
   into the existing `output_config` dict instead of reassigning.
   Reassignment would silently clobber the format schema set in (1).
   The pre-existing code did `payload["output_config"] = {"effort":
   "high"}` which was correct WHEN output_format was top-level (and
   output_config wasn't otherwise used). With the migration the
   existing dict carries the schema, so we extend it not replace it.

Verified locally on macOS Python 3.13:

  - py_compile clean.
  - Existing 401 tests still pass — 0 regression.
  - 6 new tests in test_2098_output_config_format.py (added to
    internal test suite at sg-staging/tests/, not in this PR):

      * 2 static-shape: the `_call_claude` source no longer contains
        top-level `"output_format":` AND uses `output_config`. The
        adaptive-thinking branch does NOT reassign output_config (and
        DOES set output_config['effort']). Catches the regression
        class where a future refactor reintroduces either bug.
      * 2 payload-shape unit (mocked urllib): both thinking_budget=0
        and thinking_budget>0+adaptive code paths produce a payload
        with the correct `output_config.format` shape AND no
        `output_format` top-level. The adaptive path verifies both
        `format` and `effort` coexist in output_config (i.e., the
        merge fix works).
      * 2 live-API gating (skip-on-no-key): the new shape returns
        HTTP 200 against api.anthropic.com; the old shape's current
        status is recorded for canary purposes (still 200 for
        api-key today, but reporter shows it's 400 for OAuth).

  - Full suite: 405/405 pass + 2 skipped (live API tests, opt-in).
  - The reporter's exact deprecation 400 message reproduces if you
    swap auth to OAuth Bearer + stream:true (could not test locally
    without extracting the keychain OAuth token, which was out of
    scope). The fix shape is API-contract-level so it doesn't depend
    on which auth mode triggers the 400.

NOT verified end-to-end via OAuth-authenticated plugin invocation on
my machine (auto-mode classifier correctly declined to extract the
keychain token). Reporter's 462 production errors + the docs
migration notice + the live-API HTTP 200 on the new form are
sufficient evidence to ship.

Closes #2098.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 20:11:41 -07:00
Mohamed Hegazy
2a822c0787 Merge pull request #2101 from anthropics/fix-2099-quotepath-global
security-guidance: move core.quotePath=false to GIT_CMD globally (#2099 followup)
2026-05-30 20:07:06 -07:00
Mohamed Hegazy
a40c9f1e83 security-guidance: move core.quotePath=false to GIT_CMD globally (#2099 followup)
Followup to PR #2086 (which added the flag to 4 specific git call
sites) and PR #2100 (text=True purge for #2099).

The Windows reporter for #2099 noticed more git invocations still
lacked the flag — rev-parse path queries (--show-toplevel, --git-dir,
--git-common-dir), reflog %gs subjects, and `git show <sha>:<path>`
all output paths but the per-site PR #2086 approach missed them. The
result: an Arabic-named directory shows up via _git_diff_range but
rev-parse-emitted paths get C-quoted, breaking downstream
os.path.isabs() checks.

Fix: add `-c core.quotePath=false` to GIT_CMD itself as the 4th
config-set. Every subprocess.run using the *GIT_CMD splat picks it
up automatically — diff feeders, rev-parse path queries, reflog log,
ls-files, status, git show. No more per-site flag duplication.

This commit:

1. gitutil.py: add -c core.quotePath=false to GIT_CMD.
2. Remove the now-redundant per-site flags at the 7 call sites that
   previously had inline -c core.quotePath=false (cleanup, since the
   global setting subsumes them):
   gitutil.py: _git_diff_range, _git_name_only, _git_status_porcelain,
               get_git_diff (4 sites)
   diffstate.py: _list_untracked git ls-files (1 site)
   security_reminder_hook.py: commit-review git diff + git show (2 sites)

Verified locally on latest main (post PR #2100 merge) with macOS
Python 3.13:

  - py_compile clean on all 3 modified files.
  - Bare main BEFORE my fix: 400/401 pass — 1 failure proves the gap
    (test_git_cmd_contains_quotepath_false catches the missing flag).
  - Main + my fix: 401/401 pass.
  - 23 new tests in test_quotepath_global.py (added to internal test
    suite at sg-staging/tests/, not in this PR):

      * 1 GIT_CMD-level: GIT_CMD list contains core.quotePath=false as
        a (-c, value) pair. Single source of truth — single place a
        future PR will be caught if the flag gets dropped.
      * 10 static-shape (one per hooks/*.py): every subprocess.run
        uses the *GIT_CMD splat (no bare git invocation that would
        bypass the global flag).
      * 12 end-to-end (parametrized over Arabic, Hebrew, CJK directory
        names): real git repo, _git_diff_range emits unquoted diff,
        extract_file_paths_from_diff and parse_diff_into_files keep
        the non-ASCII path in their output, _git_toplevel returns the
        non-ASCII path intact.

  - 1 staleness fix in test_diff_parser_non_ascii.py
    (test_no_bare_git_diff_or_show_without_flag): updated to accept
    EITHER inline core.quotePath=false OR *GIT_CMD splat (which
    globally provides it).

NOT verified end-to-end on Windows with a non-ASCII repo root path.
The new global-flag test pins the contract permanently, and the
parametrized macOS tests confirm parser behavior on ASCII-control
paths in non-ASCII directories. The Windows-specific rev-parse
quoting behavior follows from the same git contract our macOS test
environment exercises (POSIX git always emits raw UTF-8 regardless
of quotePath; on Windows the flag is what makes output raw).

Closes the #2099 followup specifically about _git_diff_range /
rev-parse --show-toplevel / git log %gs paths slipping past.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 12:19:38 -07:00
Mohamed Hegazy
c7a3e2ffa0 Merge pull request #2100 from anthropics/fix-2099-text-true-pythonutf8
security-guidance: purge text=True from subprocess.run + bake PYTHONUTF8=1 (#2099)
2026-05-30 12:14:03 -07:00
Mohamed Hegazy
1ecf3d1bac security-guidance: purge text=True from subprocess.run + bake PYTHONUTF8=1 (#2099)
URGENT WINDOWS FIX. Sibling of #2056 / PR #2075 but covering 14 more
sites that PR #2075 missed.

The bug class: on Windows with cp1252 default encoding (typical
en-US locale), `subprocess.run(..., text=True)` decodes child stdout
AND stderr via `locale.getpreferredencoding()`. When git emits a
UTF-8 byte that's undefined in cp1252 (e.g. `0x81` from ف, present
in any path/filename/branch ref/commit message containing
Arabic/Hebrew/CJK), Python's internal `_readerthread` raises
UnicodeDecodeError. The thread crash is silent in Python 3.13+ (only
printed to stderr), but `subprocess.run` returns `stdout=None` and
the caller AttributeErrors on `.strip()`. The user sees a misleading
"WinError 267" or similar catch-all message instead of the real
decode failure.

PR #2075 fixed 6 specific helpers in `diffstate.py` / `gitutil.py`.
This commit covers the 14 survivors. Plus a defense-in-depth belt:
`PYTHONUTF8=1` exported by sg-python.sh.

This commit:

1. sg-python.sh: `export PYTHONUTF8=1` (PEP 540). No-op on
   macOS/Linux (already UTF-8). On Windows, makes Python's
   `locale.getpreferredencoding()` return UTF-8 instead of cp1252 —
   so even if a future regression slips in text=True, the decode
   succeeds. Must be set BEFORE Python starts; changing it from
   inside the interpreter has no effect.

2. gitutil.py: convert 8 subprocess.run sites from
   `capture_output=True, text=True` to `capture_output=True` +
   manual `r.stdout.decode("utf-8", errors="replace")`:
     - _git_rev_parse_head           (stdout = SHA, stderr risk)
     - _find_git_index               (stdout = PATH, primary bug site)
     - _temp_index git add           (returncode only, stderr risk)
     - _git_toplevel                 (stdout = PATH, primary bug site)
     - _git_dir                      (stdout = PATH, primary bug site)
     - _git_rev_list_range           (stdout = SHAs, stderr risk)
     - _detect_main_branch           (stdout = ref, stderr risk)
     - merge-base --is-ancestor      (returncode only, stderr risk)

3. security_reminder_hook.py: convert 6 subprocess.run sites
   (rev-parse @{u}/@{u}@{1}/local_ref, merge-base, HEAD lookup,
   reflog SHA resolution) — same pattern.

4. security_reminder_hook.py: fix the misleading log line in
   handle_user_prompt_submit. Was:
     debug_log("Failed to capture git baseline (not a git repo?)")
   Now includes the cwd in the message so the next reporter doesn't
   waste an hour grepping for the real WinError, per reporter's
   secondary finding.

Verified locally on macOS Python 3.13:

  - py_compile clean on all modified files.
  - bash -n sg-python.sh clean.
  - sg-python.sh actually propagates PYTHONUTF8=1 to child Python
    (verified via probe — sys.flags.utf8_mode=1).
  - Existing 353 tests still pass — 0 regression.
  - 25 new tests in test_2099_subprocess_text_true.py:
      * 10 static-shape catchers (one per hooks/*.py file). Any
        future PR that reintroduces text=True OR encoding= in
        subprocess.run fails this check at PR time. Single source
        of truth for the regression class.
      * 2 sg-python.sh verifiers (literal export + actual
        propagation to child Python).
      * 5 macOS end-to-end against a real git repo containing
        non-cp1252 content (`ف.py` filename): _git_toplevel,
        _git_dir, _find_git_index, _git_rev_parse_head,
        _git_rev_list_range all return clean values without
        AttributeError / UnicodeDecodeError.
      * 7 round-trip bytes-decode pattern verifiers (parametrized
        over Arabic ف, Hebrew א, Japanese 案, raw 0x81, multiple
        cp1252-undefined bytes, real-world git diff headers).
      * 1 sanity check that cp1252 strict DOES raise on 0x81
        (proves the test environment can catch the bug class).

  - Full suite: 378/378 pass in 5.56s.

  - End-to-end tmux smoke test driving real claude 2.1.145 CLI:
    Made a git commit via Bash tool call. All 4 hooks fired through
    the fixed plugin path:
      11:28:16.730  Hook called with args: …/plugin/hooks/security_reminder_hook.py
      11:28:16.734  Processing: hook_event=UserPromptSubmit
      11:28:16.825  Captured git baseline: 445f7f213256
      11:28:19.923  Hook called with args: …
      11:28:19.923  Processing: hook_event=PostToolUse, tool=Bash
      11:28:19.971  Commit review: detected git commit in command
      11:28:20.020  Commit review: 1/1 sha(s) resolved, 1 files
      11:28:26.415  Hook called with args: …
      11:28:26.416  Processing: hook_event=Stop
      11:28:26.550  Stop hook: empty review set

    Confirms: PYTHONUTF8=1 export doesn't break anything; converted
    helpers (_git_rev_parse_head, _git_toplevel, _git_dir,
    _find_git_index) run end-to-end without issue on the happy path.

NOT verified end-to-end on Windows with actual non-cp1252 content
in path/filename/stderr. The static-shape catcher pins the
regression class permanently. Reporter's PYTHONUTF8=1 workaround
empirically proves the encoding-mode fix works for the affected
scenario; this commit just bakes it in.

Closes #2099.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 11:29:07 -07:00
Mohamed Hegazy
c40770ae5a Merge pull request #2078 from anthropics/fix-1868-claude-config-dir
security-guidance: respect CLAUDE_CONFIG_DIR for plugin state files (#1868)
2026-05-29 16:14:35 -07:00
github-actions[bot]
7a0a7f486e Bump 58 plugin SHA pin(s) to upstream HEAD (#2079)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-05-29 21:18:49 +00:00
Mohamed Hegazy
42487ee6fd Merge pull request #2091 from anthropics/fix-2089-if-clause-regression
URGENT: security-guidance: fix #2089 regression — split |-joined if clauses
2026-05-29 13:39:45 -07:00
Mohamed Hegazy
bc07f7a1fd security-guidance: 5 precise if entries fixing #2089 regression + gt support
URGENT REGRESSION FIX. PR #2076 (Graphite gt workflow) gated the
PostToolUse commit/push hooks with:

    "if": "Bash(git commit:*)|Bash(gt create:*)|Bash(gt modify:*)"
    "if": "Bash(git push:*)|Bash(gt submit:*)"

mirroring the regex-OR idiom that `matcher` uses
("Edit|Write|MultiEdit|NotebookEdit"). But `if` is NOT a regex —
it's a SINGLE permission-rule string. The CC harness's dispatch
filter parses the entire `if` value as one rule of shape
`ToolName(rule_content)` via:

    let firstParen = H.indexOf("(");
    let lastParen  = H.lastIndexOf(")");      // searches from END
    if (lastParen !== H.length - 1) return { toolName: H };
    let toolName    = H.slice(0, firstParen);
    let ruleContent = H.slice(firstParen + 1, lastParen);

Applied to the broken commit clause:
    toolName    = "Bash"
    ruleContent = "git commit:*)|Bash(gt create:*)|Bash(gt modify:*"

The garbled `ruleContent` never matches any real command, so the
hook never fires — for ANY workflow, not just gt. The plugin's
deepest review layer was dead in production for all users on builds
shipping PR #2076.

Fix shape: split into separate hook entries, each with its own
well-formed single-rule `if` clause. The Python hook self-routes
commit vs push via the bash-command regexes and dedups concurrent
spawns via `_claim_bash_hook_once`, so multiple entries firing the
same script is safe.

This commit:

1. hooks.json: 5 precise entries (one per command shape) instead of
   the broken |-joined 2-entry form. Restores the original commit/
   push behavior bit-for-bit (`Bash(git commit:*)` + `Bash(git push:*)`
   are unchanged from pre-#2076), and adds 3 separate entries for
   the Graphite commands (`Bash(gt create:*)`, `Bash(gt modify:*)`,
   `Bash(gt submit:*)`). No git behavior change.

   The earlier draft used the broader `Bash(git *)` + `Bash(gt *)`
   per the reporter's suggestion, but that has a real cost: every
   `git status` / `git log` / `git diff` would spawn the Python
   hook only to early-exit via the regex matcher. Precise per-command
   entries avoid the spawn overhead and match the pre-#2076 cost
   profile exactly.

2. security_reminder_hook.py: widen `_GIT_COMMIT_RE` to tolerate
   `git -C <path>` and `git -c k=v` global options between `git`
   and `commit` (mirrors `_GIT_PUSH_RE`'s long-standing tolerance).
   Without this, `git -C /repo commit` is silently dropped by the
   handler — reporter flagged this as the secondary finding.

Verified locally on macOS Python 3.13:

  - hooks.json valid JSON, 5 `if` clauses each parses to a single
    `{toolName: "Bash", ruleContent: "<command>:*"}` pair.
  - py_compile security_reminder_hook.py clean.
  - 9-case regex sanity: all 4 commit forms match (bare, -C path,
    -c k=v, gt create/modify); 3 non-commit forms reject (status,
    gt submit, gt log). Pre-fix would reject -C path form.
  - 7 new tests in test_2089_if_clause_validity.py + 2 updated tests
    in test_gt_graphite_workflow.py:
      * 12 sanity tests for a Python parser mirroring harness's BA(H)
        — pinned so a future refactor can't silently start accepting
        the broken form.
      * 2 hooks.json validity: every `if` clause parses as a single
        valid rule; at least one if-gated hook exists.
      * 1 post-fix structure: separate entries cover git AND gt.
      * 2 updated gt-coverage: SOME clause covers git, SOME clause
        covers gt (no longer requires both in the same |-joined
        clause, which was the broken shape).

    TDD-verified the test catches the bug: temporarily restored
    main's broken |-joined hooks.json, ran the new test, saw
    `test_every_if_clause_is_single_valid_rule` fail with a clear
    error explaining #2089's cause. Restored fix, test passes.

  - Full suite: 336/353 pass (17 unrelated failures from open PRs
    #2078 / #2086 not in this branch).

NOT verified end-to-end with a real CC instance triggering the hooks
on a git or gt commit. The static-shape tests catch the regression
class and the regex sanity tests pin the `git -C` tolerance, but
the asyncRewake feedback loop needs runtime verification.

Closes #2089. Restores the closes for #2048 that PR #2076 attempted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 13:22:20 -07:00
Mohamed Hegazy
9e150cfd48 Merge pull request #2086 from anthropics/fix-2082-diff-parser-non-ascii
security-guidance: pass core.quotePath=false to diff feeders (#2082)
2026-05-29 08:11:25 -07:00
Mohamed Hegazy
38b298d5b2 security-guidance: pass core.quotePath=false to diff feeders (#2082)
Fixes anthropics/claude-plugins-official#2082 — diff feeders use git's
default quotePath setting, which C-quotes any path with a non-ASCII
byte. The downstream parsers in gitutil.parse_diff_into_files /
gitutil.extract_file_paths_from_diff match the diff header with
`re.match(r'^a/(.+?) b/(.+)$', ...)`, which only sees the raw
`a/path b/path` form. The C-quoted `"a/\303\201vila/..."` form
slips past the regex, the `continue` fires, and the file is silently
dropped from review.

Effect: a vulnerable file like `Ávila/payment.py` with
`os.system('curl ' + user_input)` never reaches the LLM reviewer.
False negative in exactly the direction the plugin exists to catch.

Sibling of #2056 / #2075: those fixed the UTF-8 decode of the
subprocess output (text=True crashed the reader thread on Windows
cp1252). This one fixes the diff-feeder commands themselves — the
name-only helpers (_git_name_only, _git_status_porcelain) already
pass core.quotePath=false for this exact reason; the diff-text
feeders were the holdouts.

Fix: add `-c core.quotePath=false` to 4 git invocations:

  - gitutil._git_diff_range            (push-sweep feed)
  - gitutil.get_git_diff                (Stop-hook feed)
  - security_reminder_hook commit-review `git diff` (amend delta)
  - security_reminder_hook commit-review `git show`  (post-amend)

With the flag, git emits raw UTF-8 in the diff header
(`a/Ávila/payment.py`), the regex matches, and both files (the
non-ASCII vulnerable one + any ASCII control file) flow through to
review correctly.

Verified locally on macOS Python 3.13:

  - py_compile clean on both files.
  - Existing 45 smoke + extensibility tests still pass.
  - 8 new tests in test_diff_parser_non_ascii.py (added to internal
    test suite at sg-staging/tests/, not in this PR):

      * 2 static-shape: gitutil._git_diff_range and get_git_diff both
        contain `core.quotePath=false` in their source.
      * 2 commit-review static: every subprocess.run in
        handle_commit_review_posttooluse that mentions `"diff"` or
        `"show"` also passes the flag. Catches the regression
        class where a new diff/show call site is added without
        plumbing the flag through.
      * 4 end-to-end with a real git repo containing a
        `Ávila/payment.py` baseline-and-edit:
          - WITHOUT flag: header is C-quoted, both parsers drop the
            non-ASCII file (demonstrates the bug).
          - WITH flag: header is raw UTF-8, both parsers see the file.
          - parse_diff_into_files (the other parse path) also keeps
            the file with the flag.
          - get_git_diff end-to-end produces unquoted output whose
            file list includes the non-ASCII path.

  - 53/53 pass total (45 existing + 8 new) in 3.41s.

NOT verified end-to-end with a real CC commit-review fire on a
non-ASCII path. The static-shape tests catch the regression and the
end-to-end git-repo tests pin parser behavior, but the actual
LLM-review-with-vuln-found path requires runtime verification against
an Anthropic-API-credentialed CC session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 07:56:22 -07:00
Mohamed Hegazy
8435428dfc Merge pull request #2077 from anthropics/fix-1358-1375-1783-hook-output-protocol
security-guidance: emit findings via hookSpecificOutput.additionalContext (#1358 #1375 #1783)
2026-05-29 00:20:48 -07:00
Mohamed Hegazy
0d22ba3501 security-guidance: respect CLAUDE_CONFIG_DIR for plugin state files (#1868)
Fixes #1868 — when CLAUDE_CONFIG_DIR is set to a non-default location
(e.g. ~/.config/claude for XDG compliance, or a multi-tenant install
path), the plugin still wrote state files to the hardcoded ~/.claude/
path, leaving stale state and breaking CLAUDE_CONFIG_DIR's purpose.

Resolution precedence (highest first):
  1. SECURITY_WARNINGS_STATE_DIR  — plugin-specific override (existing)
  2. CLAUDE_CONFIG_DIR/security    — CC's config-dir env (new — #1868)
  3. ~/.claude/security            — default fallback (unchanged)

Empty-string env vars (e.g. CLAUDE_CONFIG_DIR= in a misconfigured
shell) are treated as not-set so the empty path doesn't collide with
os.path.join and silently write to /security at the filesystem root.

Implementation: a single state_dir() helper in _base.py is the source
of truth for resolution. All five modules that previously had inline
SECURITY_WARNINGS_STATE_DIR / ~/.claude/security resolutions
(_base.py, session_state.py, ensure_agent_sdk.py, llm.py, and one
site in security_reminder_hook.py) now call state_dir() instead.
Re-implementing the precedence inline risks drift — one module gets
a future fix, others don't.

The helper is called per-invocation rather than cached at import time
so test monkeypatches of the env vars take effect, and so a long-
running test or future shared-process scenario can change the env
between calls and have the next call observe the new value. The
per-call cost is negligible compared to the subprocess-spawn cost
the hooks pay every fire in production.

Three hardcoded ~/.claude/security strings remain but are NOT
functional resolutions:
  - _base.py:39: the fallback BRANCH inside state_dir() itself
  - ensure_agent_sdk.py:6, :11: docstring text describing default
                                location for users

Verified locally on macOS Python 3.13:

  - py_compile clean on all 5 modified files.
  - Existing 45 smoke + extensibility tests still pass.
  - 14 new tests in test_claude_config_dir.py (added to internal test
    suite at sg-staging/tests/, not in this PR):

      * 7 resolution-semantics: default fallback, CLAUDE_CONFIG_DIR
        override, SECURITY_WARNINGS_STATE_DIR beats both, tilde
        expansion, empty-string handling (CLAUDE_CONFIG_DIR= must
        fall back, NOT join to /security).
      * 4 static-shape: each of session_state / ensure_agent_sdk /
        llm / security_reminder_hook either imports state_dir from
        _base OR has zero resolution patterns. Catches the
        regression where someone adds a new state-file writer and
        re-implements resolution inline, missing the
        CLAUDE_CONFIG_DIR branch.
      * 3 end-to-end: with CLAUDE_CONFIG_DIR set, get_state_file /
        get_lock_file return paths under <CLAUDE_CONFIG_DIR>/security/;
        save_state round-trip writes a file to the redirected path
        and re-reads the same contents.

  - 59/59 pass total (45 existing + 14 new) in 2.54s.

NOT verified end-to-end with a real CC instance setting
CLAUDE_CONFIG_DIR. The shape tests catch the regression class
(hardcoded ~/.claude/), and the end-to-end test pins the behavior
that user state files actually land at the redirected path.

Closes #1868.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 23:57:10 -07:00
Mohamed Hegazy
37ffc76005 security-guidance: emit findings via hookSpecificOutput.additionalContext (#1358 #1375 #1783)
Fixes #1358, #1375, and #1783 — three related complaints about the
hook output protocol used at the three asyncRewake exit-2 sites
(handle_commit_review_posttooluse, handle_push_sweep_posttooluse,
handle_stop_hook).

The old shape at each site was:

  emit_metrics({...})                              # JSON to stdout (metrics)
  sys.stderr.write(banner + guidance + suffix)     # plain text to stderr
  sys.exit(2)                                      # asyncRewake trigger

That triggered three reported problems:

  #1375: CC's hook system parsing stdout for a SyncHookJSONOutput sees
         only the bare metrics dict — no findings reason — and on older
         CC versions surfaces a 'json output validation failed' error
         because stderr's plain text isn't valid JSON.
  #1783: CC's UI shows 'Permission to use Edit has been denied' with no
         permissionDecisionReason — the stderr text is invisible to that
         UI surface; CC only renders fields it can find in the JSON.
  #1358: Reporters experienced the exit(2) as 'gating' behavior rather
         than 'warning' behavior. The pattern-warning path in main()
         was migrated to exit(0) + hookSpecificOutput.additionalContext
         long ago; these three asyncRewake sites were never updated.

Fix: extend emit_metrics() to accept additional_context, system_message,
and hook_event_name kwargs, and emit them in the same SyncHookJSONOutput
line as the metrics. CC's parser stops scanning stdout after the first
{-prefixed line, so the findings must ride in that same line — calling
emit_metrics twice or adding a second print(json.dumps(...)) would
silently drop the second emission.

At each of the three call sites: route the guidance text that used to
go to stderr through additional_context instead. The stderr.write is
dropped — additionalContext carries the same text to the model via the
JSON channel, and the legacy stderr surface is what triggered #1375's
JSON validation error on older CC clients.

exit(2) is preserved at all three sites. That's the documented mechanism
for triggering the asyncRewake 'force fix' feedback loop (per the
inline comment at the stop-hook site); switching to exit(0) without
verifying CC's protocol-version support risks dropping the rewake
entirely and silently losing all the findings the hook just computed.

For push-sweep specifically: emit_metrics had to move from an
unconditional pre-emission (line ~1680) to two conditional sites (one
in the no-vulns branch with exit(0), one in the with-vulns branch with
exit(2)) because the with-vulns branch needs to attach additional_context
and CC reads only the first JSON line — a second emit would be ignored.
Behavior is preserved: every push-sweep fire emits exactly one metrics
line, just at a slightly later point in the function body.

Verified locally on macOS Python 3.13:

  - py_compile clean.
  - Existing 45 smoke + extensibility tests still pass.
  - 21 new tests in test_hook_output_protocol.py (added to internal
    test suite at sg-staging/tests/, not in this PR):

      * 6 backward-compat: emit_metrics with metrics only, with
        rewake_summary, etc. — verifies the legacy callers still
        produce the same output shape.
      * 5 additional_context shape: lands in hookSpecificOutput,
        round-trips the value, default hook_event_name is sensible,
        empty/None doesn't pollute the JSON with an empty hSO block.
      * 3 system_message shape: lands in systemMessage, empty/None
        suppressed, round-trips.
      * 1 combined: metrics + rewake_summary + additional_context +
        system_message + hook_event_name all merge into one JSON line.
      * 6 round-trip safety: emoji, quotes, backslashes, newlines,
        Unicode (山田太郎 + 🎉), tabs, null bytes — all survive the
        json.dumps cycle.
      * 6 static-shape: each of the three asyncRewake handlers
        (commit_review, push_sweep, stop_hook) is checked to confirm
        it passes additional_context to emit_metrics and no longer
        writes the PROVENANCE_BANNER guidance to stderr. Catches the
        regression class where a new exit(2) site forgets to plumb
        guidance through the JSON channel.

  - 66/66 pass total (45 existing + 21 new) in 2.57s.

NOT verified end-to-end with a real CC instance triggering all three
hooks. The static-shape tests + the JSON round-trip tests should catch
any regression in the emit_metrics output, but the actual interaction
with CC's asyncRewake / rewakeMessage flow (especially: does
hookSpecificOutput.additionalContext successfully appear in the
rewakeMessage that CC sends to the model?) needs runtime verification
against a CC version that supports the modern protocol.

The reporter for #1375 specifically called out that CC's older
versions surfaced 'json output validation failed' on the old stderr-
only output; this fix changes the stdout shape to valid JSON with the
findings included, which should resolve that error class.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 23:53:04 -07:00
Mohamed Hegazy
982070e51f Merge pull request #2076 from anthropics/fix-2048-graphite-gt-workflow
security-guidance: detect Graphite (gt) commands as commit/push events (#2048)
2026-05-28 23:44:58 -07:00
Mohamed Hegazy
68a700837c Merge pull request #2075 from anthropics/fix-2056-windows-unicode-decode
security-guidance: lenient UTF-8 decode in 6 git-subprocess helpers (#2056)
2026-05-28 23:36:36 -07:00
Mohamed Hegazy
5212308979 security-guidance: detect Graphite (gt) commands as commit/push events (#2048)
Fixes anthropics/claude-plugins-official#2048 — teams using Graphite
for stacked PRs (`gt create` / `gt modify` / `gt submit`) never get
the commit/push agentic review because the hook matcher only catches
literal `git commit` / `git push` Bash calls. gt shells out to git
as a subprocess, but the hook fires on Claude's top-level tool call,
which is `gt create` — not the `git commit` invocation inside the
gt subprocess that Claude Code never observes.

Per-edit pattern checks and end-of-turn Stop review still fire (those
don't depend on detecting the commit command), so the silent-coverage-
gap is bounded to the deepest review layer for Graphite users. Still:
that's exactly the layer designed to catch IDOR / auth-bypass /
cross-file SSRF, so the gap matters.

Semantic mapping (per the reporter):

  gt create  -> commit            (like `git commit`)
  gt modify  -> commit + amend    (like `git commit --amend`)
  gt submit  -> push              (like `git push`)

Changes:

1. hooks/hooks.json: extend two PostToolUse `if` matchers.

   "Bash(git commit:*)"
     -> "Bash(git commit:*)|Bash(gt create:*)|Bash(gt modify:*)"
   "Bash(git push:*)"
     -> "Bash(git push:*)|Bash(gt submit:*)"

   Without this, the hook subprocess never spawns for gt invocations
   and the Python regex changes below are dead code.

2. hooks/security_reminder_hook.py: extend three regexes that classify
   the bash command line.

   _GIT_COMMIT_RE: now also matches `gt create` and `gt modify`.
     Used at 4 sites (handler gate, multi-commit count, prompt
     detection, event classification). Compound commands like
     `gt create -am a && gt submit` now correctly trigger both the
     commit and push paths.

   _GIT_AMEND_RE: now also matches `gt modify` (semantically an
     amend). The amend code path uses reflog to find the pre-amend
     SHA and diff against THAT instead of HEAD~1 — same code path
     now applies to `gt modify`.

   _GIT_PUSH_RE: now also matches `gt submit`. Tolerates the same
     `git -C path` / `git -c k=v` global options as before for the
     git form; gt has its own flag layer that doesn't conflict.

Verified locally on macOS Python 3.13:

  - JSON valid (hooks.json roundtrips).
  - Existing 45 smoke + extensibility tests still pass.
  - 76 new tests in test_gt_graphite_workflow.py (added to internal
    test suite this PR doesn't ship — kept in sg-staging tests/ until
    we have a story for shipping plugin tests publicly):

      * 16 parametrized commit-match: native git commit variants +
        all gt create / gt modify variants from the reporter's repro.
      * 11 parametrized commit-reject: gt submit, gt log, gtoolkit
        (word-boundary), agt create, etc.
      * 9 parametrized amend-match: git commit --amend variants +
        gt modify variants + chained git+gt.
      * 7 parametrized amend-reject: regular git commit, gt create,
        gt submit, echo'd substring noise.
      * 11 parametrized push-match: git push variants + gt submit
        variants + chained.
      * 12 parametrized push-reject: git commit, gt log, gt fetch,
        gt down, gt restack, gh pr create, agt submit.
      * 3 compound-command class tests: git+gt mixtures trigger both
        paths; gt modify chained with gt submit triggers
        amend + push.
      * 3 commit-invocation-count tests: gt commands contribute to
        the multi-commit-detection findall count.
      * 2 hooks.json static config tests: read the JSON, verify the
        commit and push `if` clauses include the gt cases. Catches
        the easy regression where someone updates the Python regex
        but forgets to widen the matcher.

  - 121/121 pass total (45 existing + 76 new) in 2.50s.

NOT verified end-to-end with a real `gt` install. Reporter has the
deterministic Graphite workflow and offered to retest. The regex +
matcher widening is a clean superset — current git-only matching still
works (verified by the 45-test smoke suite that uses `git commit` /
`git push` exclusively), and the new gt cases are pure additions.

Not in this PR:

  - `gt prev` / `gt next` / `gt up` / `gt down` etc. — pure
    navigation, no commit / push side effect.
  - `gt restack` — could in principle rewrite commits (so the
    plugin's reviewed-shas cache becomes stale), but it doesn't
    create reviewable new content. Out of scope.
  - `gh pr create` — already explicitly NOT a separate matcher per
    the existing comment in _GIT_PUSH_RE (gh invokes git push as a
    child process; the bash hook only sees the top-level
    `gh pr create`). Same architectural issue as gt but with a
    different cost/benefit per the existing comment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 23:33:14 -07:00
Mohamed Hegazy
3d349d40b9 Merge pull request #2074 from anthropics/fix-xss-rules-non-js-false-positives
security-guidance: gate XSS pattern rules to JS-family files
2026-05-28 23:18:17 -07:00
Mohamed Hegazy
6a63e35e75 security-guidance: lenient UTF-8 decode in 6 git-subprocess helpers (#2056)
Fixes anthropics/claude-plugins-official#2056 — on Windows, when the
worktree contains an untracked file whose name has a character undefined
in cp1252 (accented capitals like Á Í Ï Ð Ý, most CJK, emoji), the
UserPromptSubmit hook crashes:

  Exception in thread Thread-5 (_readerthread):
    UnicodeDecodeError: 'charmap' codec can't decode byte 0x81
  Traceback (most recent call last):
    File diffstate.py, line 338, in _list_untracked
      for p in r.stdout.split('\\0'):
  AttributeError: 'NoneType' object has no attribute 'split'

Non-blocking (UPS failures still let the prompt through) but the
baseline-untracked snapshot is silently lost, so the Stop-hook review
mis-handles pre-existing untracked files.

Root cause (reporter's diagnosis, verified):

1. core.quotePath=false makes git emit raw UTF-8 for non-ASCII filenames.
2. subprocess.run(..., text=True) decodes via
   locale.getpreferredencoding(False) in strict mode — on Windows that
   is cp1252, in which 0x81 / 0x8D / 0x8F / 0x90 / 0x9D are undefined.
   Those bytes appear in the UTF-8 encodings of Á (C3 81), Í (C3 8D),
   Ï (C3 8F), Ð (C3 90), Ý (C3 9D), and a large fraction of CJK / emoji
   codepoints.
3. The decode runs in the subprocess reader thread. The thread raises
   UnicodeDecodeError, threading prints 'Exception in thread Thread-N',
   subprocess.run returns with stdout=None. The handler then does
   None.split('\\0') -> AttributeError, which is NOT in the narrow
   except (TimeoutExpired, FileNotFoundError, OSError) tuple, so it
   escapes the helper, propagates out of UserPromptSubmit's
   ThreadPoolExecutor.result(), and exits the hook non-zero.

This is internally inconsistent: gitutil._git_diff_range,
security_reminder_hook._reflog_amend_lookup (line ~540), and the commit
diff loop (line ~1115) already do bytes + decode utf-8/replace, with
comments explicitly noting that text=True would crash. The fix below
extends that established pattern to the helpers that were holdouts.

Affected helpers (6 total):

  - diffstate._list_untracked            <- reporter, hot path, CRITICAL
  - diffstate.capture_git_baseline       <- reporter, latent
  - diffstate.get_baseline_file_content  <- audit, file content read, HIGH
  - gitutil._git_name_only                <- reporter, latent
  - gitutil._git_status_porcelain         <- reporter, latent
  - gitutil._git_reflog_recent_commits    <- audit, embeds %gs commit msg, HIGH

For each one:

  - Drop text=True from subprocess.run.
  - Decode r.stdout / r.stderr as .decode('utf-8', errors='replace').
  - Add ValueError to the except tuple as defense against any future
    strict-decode regression (UnicodeDecodeError is a ValueError
    subclass; including it explicitly degrades the helper to its
    empty/None return instead of escaping out of the hook).

Verified locally on macOS Python 3.13:

  - py_compile clean on both files.
  - 45 existing smoke + extensibility tests still pass.
  - 21 new internal tests (not in this PR — added to the team's local
    test suite at staging/tests/test_unicode_decode.py):
      * 18 static-shape parametrized: each of the 6 fixed helpers has
        no text=True in its subprocess calls, contains errors='replace',
        and lists ValueError in its except.
      * Deterministic end-to-end: create real git repo + Ávila_report.txt
        untracked, call _list_untracked, verify it returns
        {'Ávila_report.txt': <mtime>} without crashing.
      * Deterministic end-to-end: same for capture_git_baseline (verifies
        the latent stderr-warning case stays valid).
      * Deterministic end-to-end: get_baseline_file_content on a file
        whose content has 山田太郎 + 🎉; verify the bytes round-trip
        through the decode.
  - 66/66 tests pass total (45 existing + 21 new).

NOT verified end-to-end on Windows — would need actual cp1252 strict
decode to fire. Reporter has the deterministic repro and will
re-verify on their Win11 / Python 3.14.x setup before merge.

Not in this PR (defense-in-depth, lower risk):

  - 3 git rev-parse calls returning path output (gitutil._find_git_index,
    _git_toplevel, _git_dir) could fail on Windows if cwd is in a
    non-ASCII install directory. Same fix shape but unreported and
    much lower probability — worth a separate follow-up if anyone
    actually hits it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 23:15:16 -07:00
Mohamed Hegazy
12a5376e20 security-guidance: gate XSS pattern rules to JS-family files
Closes #410, #2037, #2045, #1640, #1280, #1329, #1341, #255,
anthropics/claude-code#46720 (partial closes on overlap with other rules).

The plugin's substring-only XSS / browser-DOM rules
(new_function_injection, react_dangerously_set_html, document_write_xss,
innerHTML_xss, outerHTML_xss, insertAdjacentHTML_xss) fired on any file
containing the trigger substring — including:

  * Markdown documentation explaining XSS sinks
  * Blog posts / READMEs that name browser APIs
  * Python tutorials referencing dangerouslySetInnerHTML
  * Plugin skill files with example HTML strings
  * .yaml / .json configs that happen to contain the literal string
  * .gitignore / Dockerfile / Makefile

These constructs have no meaning outside JS/TS source. Add a
path_filter: lambda p: p.endswith(_JS_EXTS) to each so they fire only
on .js, .jsx, .ts, .tsx, .mjs, .cjs, .mts, .cts, .vue, .svelte.

Cross-checked against the existing _JS_EXTS-gated rules
(regex_exec_substring, child_process_exec, exec_substring) — same
pattern, same constant, same intent. Uses the module-level _JS_EXTS
tuple so future extension changes propagate to all 6 rules atomically.

Verified locally on macOS Python 3.13:
  - py_compile clean.
  - 45-test existing smoke + extensibility suite still passes.
  - 151 new parametrized tests in test_xss_gate.py (added to internal
    test suite this PR doesn't ship): each gated rule x every
    JS-family extension accepts, x every non-JS path (.md / .py /
    .yaml / .json / .txt / .html / Dockerfile / Makefile / .gitignore
    / .sh / .go / .rs / .rb) rejects. 196 tests pass total.

Doesn't address everything in the false-positive cluster — issues that
require Python-rule gating (#1114 .env.schema exec), tighter substring
scoping (#660 pickle in usernames), or hook-protocol changes (#1358
exit-2 vs warning, #1375 plain-text-vs-JSON output) need separate PRs.
This PR covers the JS-substring subset cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 23:07:53 -07:00
Mohamed Hegazy
04127de5d1 Merge pull request #2073 from anthropics/fix-2071-macos-python-39
security-guidance: enable LLM review on default macOS Python 3.9 (#2071)
2026-05-28 22:59:23 -07:00
24 changed files with 1997 additions and 338 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -2,25 +2,24 @@ name: Bump Plugin SHAs
# Nightly sweep: for each external entry whose upstream HEAD has moved past
# its pinned SHA, validate at the new SHA with `claude plugin validate`
# inline, then open one PR with all passing bumps. Each run force-resets the
# bump/plugin-shas branch, so a previous night's unmerged PR is replaced (and
# its review state discarded) — review and merge same-day to avoid churn.
# inline, then open one PR per bumped plugin on branch `bump/<slug>`.
# Failing entries stay isolated in their own PR; passing bumps merge
# independently.
#
# Bot-free — uses the default GITHUB_TOKEN. PRs opened with GITHUB_TOKEN don't
# trigger on:pull_request workflows, so the policy scan (`Scan Plugins`, a
# required status check on main) would never run and the bump PR could never
# merge. workflow_dispatch is exempt from that recursion guard, so we dispatch
# the scan ourselves on the bump branch after the PR is opened. The check run
# lands on the branch HEAD — the same SHA as the PR head — and satisfies the
# required check.
# trigger on:pull_request workflows, so the required status checks on main
# (`scan` from Scan Plugins, `check` from Check MCP URLs, `validate` from
# Validate Plugins) would never run and the bump PR could never merge.
# workflow_dispatch is exempt from that recursion guard, so we dispatch all
# three ourselves against each per-entry bump branch after its PR is opened.
# Each check run lands on the branch HEAD — the same SHA as the PR head — and
# satisfies the corresponding required check. (Each of those workflows runs
# its job unconditionally on workflow_dispatch, so a dispatch always reports.)
#
# max-bumps is set above the external-entry count so a single run can clear
# any backlog. The cost-control mechanisms are downstream:
# - scan-plugins.yml caches verdicts by (plugin, sha) so an unchanged SHA
# is never re-scanned across nightly force-resets.
# - revert-failed-bumps.yml drops policy-failing entries from the bump PR
# so one bad upstream can't block the rest.
# See those files for details.
# max-bumps caps the per-night work for cost control. Per-entry scans are
# more expensive than a single batched scan, so the cap is conservative.
# The composite action skips entries that already have an open bump PR, so
# re-dispatches don't pile up duplicate work.
on:
schedule:
@@ -30,12 +29,12 @@ on:
max_bumps:
description: Cap on plugins bumped this run
required: false
default: '130'
default: '30'
permissions:
contents: write
pull-requests: write
actions: write # gh workflow run scan-plugins.yml on the bump branch
actions: write # gh workflow run {scan-plugins,check-mcp-urls,validate-plugins}.yml per bump branch
concurrency:
group: bump-plugin-shas
@@ -43,8 +42,8 @@ concurrency:
jobs:
bump:
runs-on: ubuntu-latest
# Per-bump cost is ~2s (ls-remote + shallow clone + validate); 130 entries
# is ~5 min. The 60 min ceiling absorbs slow upstreams without letting a
# Per-bump cost is ~2s (ls-remote + shallow clone + validate); 30 entries
# is ~1-2 min. The 60 min ceiling absorbs slow upstreams without letting a
# pathological run consume the default 360 min budget.
timeout-minutes: 60
steps:
@@ -52,18 +51,44 @@ jobs:
# createCommitOnBranch-based bump so commits are signed by GitHub and
# satisfy the org-level required_signatures ruleset on main.
- uses: anthropics/claude-plugins-community/.github/actions/bump-plugin-shas@c41c6911de0afffd2bc5cd8b21fb1e06444ee13b
- uses: anthropics/claude-plugins-community/.github/actions/bump-plugin-shas@e2019b2a01f11aa1484c53540b1cfab5eebbc299
id: bump
with:
marketplace-path: .claude-plugin/marketplace.json
max-bumps: ${{ inputs.max_bumps || '130' }}
max-bumps: ${{ inputs.max_bumps || '30' }}
pr-mode: per-entry
claude-cli-version: latest
# `bump/plugin-shas` is the action's default `pr-branch`. The scan diffs
# the branch against origin/main (the action's base-ref fallback when
# there's no pull_request event) and scans only the bumped entries.
- name: Dispatch policy scan on bump branch
if: steps.bump.outputs.pr-url != ''
# Per-entry fan-out: dispatch the three required checks against each bump
# branch. `pr-urls` is a JSON array of {name, old_sha, new_sha, branch,
# pr_url} entries emitted by the composite action when pr-mode is
# per-entry. All three (scan / check / validate) are required on main and
# none fire on the GITHUB_TOKEN-opened PR, so each must be dispatched.
# A single failed dispatch (transient API error / rate limit) must not
# strand the remaining branches, so we attempt every dispatch, then fail
# the step if any failed: a missing required check would otherwise leave
# its bump PR silently blocked behind a green run, and the composite
# action skips slugs with an open PR so it would never be retried.
- name: Dispatch required checks per per-entry PR
if: steps.bump.outputs.pr-urls != '' && steps.bump.outputs.pr-urls != '[]'
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run scan-plugins.yml --ref bump/plugin-shas
PR_URLS: ${{ steps.bump.outputs.pr-urls }}
run: |
set -euo pipefail
dispatch_failures="$(mktemp)"
jq -c '.[]' <<<"$PR_URLS" | while read -r entry; do
branch=$(jq -r '.branch' <<<"$entry")
name=$(jq -r '.name' <<<"$entry")
for wf in scan-plugins check-mcp-urls validate-plugins; do
echo "Dispatching ${wf}.yml against $branch ($name)"
if ! gh workflow run "${wf}.yml" --ref "$branch"; then
echo "::error::Failed to dispatch ${wf}.yml against $branch ($name) — required check will be missing; re-dispatch with: gh workflow run ${wf}.yml --ref $branch"
echo "${wf} ${branch}" >> "$dispatch_failures"
fi
done
done
if [ -s "$dispatch_failures" ]; then
echo "::error::$(wc -l < "$dispatch_failures" | tr -d ' ') required-check dispatch(es) failed; the affected bump PR(s) are blocked until re-dispatched (see annotations above)."
exit 1
fi

View File

@@ -12,6 +12,14 @@ on:
branches: [main]
paths:
- '.claude-plugin/**'
# `validate` is a required status check on main. Bump PRs are opened with
# GITHUB_TOKEN, which doesn't fire on:pull_request (recursion guard), so the
# path-filtered trigger above never reports on them and the PR would be
# blocked forever. The bump workflow dispatches this against each per-entry
# bump branch instead; the check run lands on the branch HEAD (= PR head)
# and satisfies the required check. The validate job runs unconditionally,
# so a dispatch always reports.
workflow_dispatch:
permissions:
contents: read

View File

@@ -1,6 +1,6 @@
{
"name": "code-modernization",
"description": "Modernize legacy codebases (COBOL, legacy Java/C++, monolith web apps) with a structured assess map extract-rules brief reimagine/transform harden workflow and specialist review agents",
"description": "Modernize legacy codebases (COBOL, legacy Java/C++, monolith web apps) with a structured preflight / assess / map / extract-rules / brief / reimagine / transform / harden workflow, an interactive topology viewer, and specialist review agents",
"author": {
"name": "Anthropic",
"email": "support@anthropic.com"

View File

@@ -7,7 +7,7 @@ A structured workflow and set of specialist agents for modernizing legacy codeba
Legacy modernization fails most often not because the target technology is wrong, but because teams skip steps: they transform code before understanding it, reimagine architecture before extracting business rules, or ship without a harness that would catch behavior drift. This plugin enforces a sequence:
```
assess → map → extract-rules → brief → reimagine | transform → harden
preflight → assess → map → extract-rules → brief → reimagine | transform → harden
```
The discovery commands (`assess`, `map`, `extract-rules`) build artifacts under `analysis/<system>/`. The `brief` command synthesizes them into an approval gate. The build commands (`reimagine`, `transform`) write new code under `modernized/`. The `harden` command audits the legacy system and produces a reviewable remediation patch. Each step has a dedicated slash command, and specialist agents (legacy analyst, business rules extractor, architecture critic, security auditor, test engineer) are invoked from within those commands — or directly — to keep the work honest.
@@ -20,25 +20,36 @@ Commands take a `<system-dir>` argument and assume the system being modernized l
mkdir -p legacy && ln -s /path/to/your/legacy/codebase legacy/billing
```
## Optional tooling
## What to give Claude
`/modernize-assess` works best with [`scc`](https://github.com/boyter/scc) (LOC + complexity + COCOMO) or [`cloc`](https://github.com/AlDanial/cloc), and falls back to `find`/`wc` if neither is installed. Portfolio mode also benefits from [`lizard`](https://github.com/terryyin/lizard) (cyclomatic complexity). The commands degrade gracefully without them, but the metrics will be coarser.
The commands degrade gracefully, but each of these makes the output meaningfully better — run `/modernize-preflight <system-dir>` to check all of them at once and get a readiness report:
- **Analysis tools**: [`scc`](https://github.com/boyter/scc) (LOC + complexity + COCOMO) or [`cloc`](https://github.com/AlDanial/cloc); [`lizard`](https://github.com/terryyin/lizard) for portfolio mode. Without them, metrics fall back to `find`/`wc` and get coarser.
- **A working build toolchain** for the legacy stack (e.g. GnuCOBOL for COBOL) — required before `/modernize-transform` can prove behavioral equivalence, and verified by preflight with a real smoke compile against your code.
- **The whole system in the tree**: deployment descriptors (JCL, CICS definitions, route configs), copybooks/includes, and DDL/schemas. Entry-point detection and data lineage in `/modernize-map` are guesswork without them.
- **Production telemetry** (optional): an observability MCP server or batch job logs enable the runtime overlay in `/modernize-assess` and timing annotations on critical paths.
## Commands
The commands are designed to be run in order, but each produces a standalone artifact so you can stop, review, and resume.
### `/modernize-preflight <system-dir> [target-stack]`
Environment readiness check, meant to run first: detects the legacy stack, checks analysis tooling, **smoke-compiles a real source file** with the legacy toolchain (the errors this surfaces — missing copybooks, wrong dialect flags — are the ones that otherwise appear mid-transform), inventories missing includes / deployment descriptors / binary-only artifacts, and probes for telemetry. Produces `analysis/<system>/PREFLIGHT.md` with a per-command Ready / Ready-with-gaps / Not-ready verdict.
### `/modernize-assess <system-dir>` — or — `/modernize-assess --portfolio <parent-dir>`
Inventory the legacy codebase: languages, line counts, complexity, build system, integrations, technical debt, security posture, documentation gaps, and a COCOMO-derived effort estimate. Produces `analysis/<system>/ASSESSMENT.md` and `analysis/<system>/ARCHITECTURE.mmd`. Spawns `legacy-analyst` (×2) and `security-auditor` in parallel for deep reads. With `--portfolio`, sweeps every subdirectory of a parent directory and writes a sequencing heat-map to `analysis/portfolio.html`.
### `/modernize-map <system-dir>`
Build a dependency and topology map of the **legacy** system: program/module call graph, data lineage (programs ↔ data stores), entry points, dead-end candidates, and one traced critical-path business flow. Writes a re-runnable extraction script and produces `analysis/<system>/topology.json` (machine-readable), `analysis/<system>/TOPOLOGY.html` (rendered Mermaid + architect observations), and standalone `call-graph.mmd`, `data-lineage.mmd`, and `critical-path.mmd`.
![Interactive topology map of AWS CardDemo — domains as containers, modules sized by lines of code, dependency edges colored by kind, entry points ringed](assets/topology-viewer-screenshot.jpg)
Build a dependency and topology map of the **legacy** system: program/module call graph, data lineage (programs ↔ data stores), entry points, dead-end candidates, and 24 traced business flows each anchored to a persona (the claimant, the operator, the auditor — not the maintainer). Writes a re-runnable extraction script and produces `analysis/<system>/topology.json` plus `analysis/<system>/TOPOLOGY.html` — an **interactive zoomable map** (circle-pack of domains/modules sized by LOC, dependency edges with per-kind toggles, search, click-for-details sidebar, and a walkthrough mode that plays each persona flow as a numbered path with a plain-language narrative). Built from a template shipped with the plugin, so it works on systems far too dense for a static diagram. Small domain-level `call-graph.mmd`, `data-lineage.mmd`, and `critical-path.mmd` are still exported for docs and PRs.
### `/modernize-extract-rules <system-dir> [module-pattern]`
Mine the business rules embedded in the legacy code — calculations, validations, eligibility, state transitions, policies — into Given/When/Then "Rule Cards" with `file:line` citations and confidence ratings. Spawns three `business-rules-extractor` agents in parallel (calculations, validations, lifecycle). Produces `analysis/<system>/BUSINESS_RULES.md` and `analysis/<system>/DATA_OBJECTS.md`.
### `/modernize-brief <system-dir> [target-stack]`
Synthesize the discovery artifacts into a phased **Modernization Brief** — the single document a steering committee approves and engineering executes: target architecture, strangler-fig phase plan with entry/exit criteria, behavior contract, validation strategy, open questions, and an approval block. Reads `ASSESSMENT.md`, `TOPOLOGY.html`, and `BUSINESS_RULES.md` and **stops if any are missing** — run the discovery commands first. Produces `analysis/<system>/MODERNIZATION_BRIEF.md` and enters plan mode as a human-in-the-loop gate.
Synthesize the discovery artifacts into a phased **Modernization Brief** — the single document a steering committee approves and engineering executes: target architecture, strangler-fig phase plan with entry/exit criteria, persona-based business walkthroughs (the section non-technical approvers actually read), behavior contract, validation strategy, open questions, and an approval block. Reads `ASSESSMENT.md`, `TOPOLOGY.html`, and `BUSINESS_RULES.md` and **stops if any are missing** — run the discovery commands first. Produces `analysis/<system>/MODERNIZATION_BRIEF.md` and enters plan mode as a human-in-the-loop gate.
### `/modernize-reimagine <system-dir> <target-vision>`
Greenfield rebuild from extracted intent rather than a structural port. Mines a spec (`analysis/<system>/AI_NATIVE_SPEC.md`), designs a target architecture and has it adversarially reviewed (`analysis/<system>/REIMAGINED_ARCHITECTURE.md`), then **scaffolds services with executable acceptance tests** under `modernized/<system>-reimagined/` and writes a `CLAUDE.md` knowledge handoff for the new system. Two human-in-the-loop checkpoints. Spawns `business-rules-extractor`, `legacy-analyst` (×2), `architecture-critic`, and general-purpose scaffolding agents.
@@ -46,6 +57,9 @@ Greenfield rebuild from extracted intent rather than a structural port. Mines a
### `/modernize-transform <system-dir> <module> <target-stack>`
Surgical, single-module strangler-fig rewrite. Plans first (HITL gate), then writes characterization tests via `test-engineer`, then an idiomatic target implementation under `modernized/<system>/<module>/`, proves equivalence by running the tests, and produces `TRANSFORMATION_NOTES.md` mapping legacy → modern with deliberate deviations called out. Reviewed by `architecture-critic`.
### `/modernize-status <system-dir>`
Read-only progress report: artifact inventory with timestamps per workflow stage, staleness flags (e.g. a brief older than the assessment it was built from), secrets-hygiene checks (quarantine file gitignored and never committed), and the single most useful next command. Run it anytime you come back to a modernization after a break.
### `/modernize-harden <system-dir>`
Security hardening pass on the **legacy** system: OWASP/CWE scan, dependency CVEs, secrets, injection. Spawns `security-auditor`. Produces `analysis/<system>/SECURITY_FINDINGS.md` ranked Critical / High / Medium / Low and a reviewed `analysis/<system>/security_remediation.patch` with minimal fixes for the Critical/High findings. The patch is reviewed by a second `security-auditor` pass before you see it. **Never edits `legacy/`** — you review and apply the patch yourself when ready, then re-run to verify. Useful as a pre-modernization step when the legacy system will keep running in production during the migration.
@@ -81,17 +95,21 @@ This plugin ships commands and agents, but modernization projects benefit from a
"Edit(modernized/**)"
],
"deny": [
"Edit(legacy/**)"
"Edit(legacy/**)",
"Write(legacy/**)"
]
}
}
```
Adjust `legacy/` and `modernized/` to match your actual layout. The key invariants: `Edit` under `legacy/` is denied, and writes are scoped to `analysis/` (for documents) and `modernized/` (for the new code). Every command in this plugin respects this — `/modernize-harden` writes a patch to `analysis/` rather than editing `legacy/` in place.
Adjust `legacy/` and `modernized/` to match your actual layout. The key invariants: `Edit`/`Write` under `legacy/` are denied, and writes are scoped to `analysis/` (for documents) and `modernized/` (for the new code). Note this guards the file tools — shell commands that mutate files (`sed -i`, `git apply`) still go through the normal Bash permission prompt, so review those prompts with the same invariant in mind. Every command in this plugin respects this — `/modernize-harden` writes a patch to `analysis/` rather than editing `legacy/` in place.
## Typical Workflow
```bash
# 0. Check the environment is ready (tools, toolchain, source completeness)
/modernize-preflight billing
# 1. Inventory the legacy system (or sweep a portfolio of them)
/modernize-assess billing
@@ -112,6 +130,9 @@ Adjust `legacy/` and `modernized/` to match your actual layout. The key invarian
# 6. Security-harden the legacy system that's still in production
/modernize-harden billing
# Anytime: where am I, what's stale, what's next
/modernize-status billing
```
## License

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

File diff suppressed because one or more lines are too long

View File

@@ -8,10 +8,19 @@ single document a steering committee approves and engineering executes.
Target stack: `$2` (if blank, recommend one based on the assessment findings).
Read `analysis/$1/ASSESSMENT.md`, `analysis/$1/TOPOLOGY.html` (and the `.mmd`
files alongside it), and `analysis/$1/BUSINESS_RULES.md` first. If any are
missing, say so and stop — they come from `/modernize-assess`, `/modernize-map`,
and `/modernize-extract-rules` respectively. Run those first.
Read `analysis/$1/ASSESSMENT.md`, `analysis/$1/topology.json` (plus the
`.mmd` files alongside it — do NOT read `TOPOLOGY.html`, it's an
interactive viewer with the data minified inside), and
`analysis/$1/BUSINESS_RULES.md` first. If any are missing, say so and
stop — they come from `/modernize-assess`, `/modernize-map`, and
`/modernize-extract-rules` respectively. Run those first.
**Staleness check:** compare modification times. If any input is newer
than an existing `MODERNIZATION_BRIEF.md`, the brief is being justifiably
regenerated; but if an existing brief is newer than all inputs and the
user re-ran this command anyway, ask what changed. Either way, note the
input timestamps in the brief's header so reviewers can see what it was
built from.
## The Brief
@@ -31,28 +40,38 @@ fewest-dependencies first. For each phase:
- Scope (which legacy modules, which target services)
- Entry criteria (what must be true to start)
- Exit criteria (what tests/metrics prove it's done)
- Estimated effort (person-weeks, derived from COCOMO + complexity data)
- Estimated effort (person-months, same unit as the assessment's COCOMO
figure — convert deliberately if you present weeks)
- Risk level + top 2 risks + mitigation
Render the phases as a Mermaid `gantt` chart.
### 4. Behavior Contract
### 4. Business Walkthroughs
For each persona flow in `analysis/$1/topology.json` (`flows` — produced
by `/modernize-map`), a short narrative table: persona, what happens in
business language, which legacy modules implement it today, and which
phase from §3 replaces each. This is the section non-technical approvers
actually read — it connects "Phase 2" to "what happens when a customer
files a claim". If topology.json has no flows, derive 23 walkthroughs
from the entry points and say they need SME confirmation.
### 5. Behavior Contract
List the **P0 rules** from BUSINESS_RULES.md (the ones tagged `Priority: P0`
money, regulatory, data integrity) that MUST be proven equivalent before any
phase ships. These become the regression suite. Flag any P0 rule with
Confidence < High as a blocker requiring SME confirmation before its phase
starts.
### 5. Validation Strategy
### 6. Validation Strategy
State which combination applies: characterization tests, contract tests,
parallel-run / dual-execution diff, property-based tests, manual UAT.
Justify per phase.
### 6. Open Questions
### 7. Open Questions
Anything requiring human/SME decision before Phase 1 starts. Each as a
checkbox the approver must tick.
### 7. Approval Block
### 8. Approval Block
```
Approved by: ________________ Date: __________
Approval covers: Phase 1 only | Full plan
@@ -60,6 +79,7 @@ Approval covers: Phase 1 only | Full plan
## Present
Enter **plan mode** and present a summary of the brief. Do NOT proceed to any
transformation until the user explicitly approves. This gate is the
human-in-the-loop control point.
Present a summary of the brief and **stop — write nothing further until
the user explicitly approves** (use plan mode if the session supports
it). This gate is the human-in-the-loop control point; "no objection" is
not approval.

View File

@@ -55,50 +55,124 @@ re-run and audited. Have it write a machine-readable
`analysis/$1/topology.json` and print a human summary. Run it; show the
summary (cap at ~200 lines for very large estates).
## Render
`topology.json` must follow this schema — it feeds the interactive viewer:
From the extracted data, generate **three Mermaid diagrams** and write them
to `analysis/$1/TOPOLOGY.html` as a self-contained page that renders in any
browser.
The HTML page must use: dark `#1e1e1e` background, `#d4d4d4` text,
`#cc785c` for `<h2>`/accents, `system-ui` font, all CSS **inline** (no
external stylesheets). Load Mermaid from a CDN in `<head>`:
```html
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
mermaid.initialize({ startOnLoad: true, theme: 'dark' });
</script>
```json
{
"system": "<display name>",
"root": {
"id": "sys", "name": "<system>", "kind": "system",
"children": [
{ "id": "dom:<domain>", "name": "<Domain>", "kind": "domain",
"children": [
{ "id": "<MODULE>", "name": "<MODULE>", "kind": "module",
"language": "cobol", "loc": 1234, "file": "src/MODULE.cbl" }
] },
{ "id": "dom:data", "name": "Data stores", "kind": "domain",
"children": [
{ "id": "ds:<NAME>", "name": "<NAME>", "kind": "datastore" }
] }
]
},
"edges": [
{ "source": "<id>", "target": "<id>", "kind": "call" }
],
"entryPoints": ["<id>", "..."],
"deadEnds": ["<id>", "..."],
"observations": ["<architect observation>", "..."],
"flows": [
{ "name": "<business flow>", "persona": "<who experiences it>",
"description": "<one sentence, plain language>",
"steps": [
{ "label": "<business-language step>", "nodes": ["<id>", "<id>"] }
] }
]
}
```
Each diagram goes in a `<pre class="mermaid">...</pre>` block. Do **not**
wrap diagrams in markdown ` ``` ` fences inside the HTML.
- Group leaf modules under `domain` containers (use the domains from
`/modernize-assess` if available). Leaf kinds: `module`, `datastore`,
`job`, `screen`. `loc` drives circle size — include it for modules.
- Edge kinds: `call` (direct), `dispatch` (dynamic/router), `read`,
`write`. Every edge endpoint must be a leaf id that exists in the tree.
- `deadEnds`: the dead-end candidates from the extraction, rendered with
a dashed outline in the viewer. Apply the suppression rules above —
anything that could be the target of an unresolved dynamic call does
NOT belong here; record that uncertainty in `observations` instead.
- **Datastore ids and names must be logical identifiers** — DD name,
dataset name, table/schema name, at most host:port. If the resolved
config value is a URL or DSN, strip userinfo and credential query
params before it goes anywhere in topology.json: the file gets
committed and the viewer displays names verbatim. Never copy raw
config values into `observations`.
- `observations`: 37 architect observations — tight coupling clusters,
single points of failure, service-extraction candidates, data stores
with too many writers, dispatch targets the extraction could not
resolve.
- `flows` is the **persona walkthrough** section — see below.
1. **`graph TD` — Module call graph.** Cluster by domain (use `subgraph`).
Highlight entry points in a distinct style. Cap at ~40 nodes — if larger,
show domain-level with one expanded domain.
## Persona flows
2. **`graph LR` — Data lineage.** Programs → data stores.
Mark read vs write edges.
Trace **24 end-to-end business flows**, each anchored to a persona —
the people who experience the system, not the people who maintain it
(e.g. for a benefits system: the claimant, the caseworker, the auditor;
for billing: the customer, the billing operator). For each flow:
3. **`flowchart TD` — Critical path.** Trace ONE end-to-end business flow
(e.g., "monthly billing run" or "process payment") through every program
and data store it touches, in execution order. If production telemetry is
available (see `/modernize-assess` Step 4), annotate each step with its
p50/p99 wall-clock.
- `name` + one-sentence `description` in plain business language —
something a steering committee member relates to ("a claimant files a
weekly claim"), not a data-flow label ("CLM batch ingest").
- `steps`: 38 steps, each with a business-language `label` and the
`nodes` (programs + data stores) that implement that step, in
execution order.
Also export the three diagrams as standalone `.mmd` files for re-use:
`analysis/$1/call-graph.mmd`, `analysis/$1/data-lineage.mmd`,
`analysis/$1/critical-path.mmd`.
This is the bridge between the technical map and non-technical
stakeholders: the same diagram answers "which program does X" for
engineers and "what happens when someone files a claim" for everyone else.
## Annotate
## Render
Below each `<pre class="mermaid">` block in TOPOLOGY.html, add a `<ul>`
with 3-5 **architect observations**: tight coupling clusters, single
points of failure, candidates for service extraction, data stores
touched by too many writers.
`analysis/$1/TOPOLOGY.html` is an **interactive map**: a zoomable
circle-pack of the whole system (domains as containers, modules sized by
LOC) with dependency edges, search, per-node detail sidebar, edge-kind
toggles, and a flow-walkthrough mode that plays each persona flow as a
numbered path. Build it from the template that ships with this plugin —
do not hand-write the viewer:
```bash
python3 - "${CLAUDE_PLUGIN_ROOT}/assets/topology-viewer.html" analysis/$1 <<'EOF'
import json, sys
tpl_path, out_dir = sys.argv[1], sys.argv[2]
tpl = open(tpl_path).read()
marker = "/*__TOPOLOGY_DATA__*/ null"
assert marker in tpl, f"injection marker not found in {tpl_path}"
data = json.dumps(json.load(open(f"{out_dir}/topology.json")))
open(f"{out_dir}/TOPOLOGY.html", "w").write(
tpl.replace(marker, "/*__TOPOLOGY_DATA__*/ " + data))
print(f"wrote {out_dir}/TOPOLOGY.html")
EOF
```
The viewer is fully self-contained (the d3 subset it needs is inlined in
the template) — it works offline and on air-gapped networks. If the
`python3` invocation fails to find the template,
`${CLAUDE_PLUGIN_ROOT}` was not substituted — report that rather than
hand-writing a viewer.
Mermaid stays for **small, exportable** diagrams. Generate standalone
`.mmd` files for reuse in docs and PRs — but keep each under ~40 edges;
collapse to domain level if the full graph is bigger (dense Mermaid
becomes unreadable, which is exactly what the interactive map is for):
- `analysis/$1/call-graph.mmd` — domain-level `graph TD`, entry points
highlighted
- `analysis/$1/data-lineage.mmd``graph LR`, programs → data stores,
read vs write marked
- `analysis/$1/critical-path.mmd``flowchart TD` of the primary flow
from `flows`, annotated with p50/p99 wall-clock if telemetry is
available (see `/modernize-assess` Step 4)
## Present
Tell the user to open `analysis/$1/TOPOLOGY.html` in a browser.
Tell the user to open `analysis/$1/TOPOLOGY.html` in a browser, and to
try: search for a module, click it to see its connections, and pick a
persona flow from the walkthrough dropdown.

View File

@@ -0,0 +1,98 @@
---
description: Environment readiness check — analysis tools, build toolchain, source completeness, telemetry access
argument-hint: <system-dir> [target-stack]
---
Check whether this environment is ready to analyze — and eventually
transform — `legacy/$1`, and tell the user exactly what to fix before the
other commands run into it. Modernization sessions fail late and
confusingly when this isn't done: assessment metrics silently degrade
without analysis tools, characterization tests can't run without a build
toolchain, and dependency maps come out wrong when half the source isn't
in the tree.
Run every check even when an early one fails — the point is one complete
readiness report, not the first error.
## Check 1 — Detect the stack
Fingerprint `legacy/$1` from file extensions and manifests: languages,
build system, deployment/config descriptors. This drives which checks
below apply. Report what was detected and the rough file split.
## Check 2 — Analysis tooling
For each, check availability (`command -v`) and report version, what it's
used for, and what degrades without it:
| Tool | Used by | Without it |
|---|---|---|
| `scc` (or `cloc`) | assess | LOC/complexity fall back to `find`+`wc`; COCOMO estimate gets coarser |
| `lizard` | assess --portfolio | complexity estimated from decision-keyword counts |
| `glow` | all | markdown artifacts render as plain text |
| `delta` | transform | side-by-side diffs fall back to `diff -y` |
Include the platform's install one-liner for anything missing
(`brew install scc`, `apt install cloc`, `pip install lizard`, …).
## Check 3 — Build toolchain (smoke test, not just presence)
Identify the compiler/interpreter for the detected legacy stack — e.g.
GnuCOBOL (`cobc`) for COBOL, JDK + Maven/Gradle for Java, `cc`/`make` for
C, `dotnet` for .NET. Then **prove it works on this codebase**: pick one
representative source file and run a syntax-only compile
(`cobc -fsyntax-only`, `javac`, `gcc -fsyntax-only`, …).
A failed smoke test is the most valuable output of this command — report
the actual error and diagnose it: missing copybook/include path, missing
dialect flag (`-std=ibm` etc.), fixed vs free format, missing dependency
jar. These are the errors that otherwise surface mid-`/modernize-transform`
with much less context.
If the user passed a `[target-stack]`, do the same for it: runtime,
package manager, test framework (`mvn -v`, `npm -v`, `pytest --version`, …).
## Check 4 — Source completeness
The dependency map is only as good as what's in the tree. Check for the
detected stack's equivalents of:
- **Referenced-but-missing includes** — copybooks (`COPY X` with no
`X.cpy`), headers, imports that resolve nowhere. Count and list the top
missing names.
- **Deployment/config descriptors** — JCL for batch COBOL, CICS CSD
definitions, `web.xml`/route configs, cron/scheduler definitions.
Without these, entry-point detection and the code↔storage join in
`/modernize-map` are guesswork.
- **Data definitions** — DDL, schemas, copybook record layouts, ORM
mappings.
- **Binary-only artifacts** — load modules, jars, DLLs with no matching
source. These become unmappable black boxes; flag them now.
## Check 5 — Optional context
- **Production telemetry** — is an observability/APM MCP server connected,
or are batch job logs / runtime exports available? (Enables the runtime
overlay in `/modernize-assess` Step 4 and timing annotations in
`/modernize-map`.)
- **Version control history** — is `legacy/$1` under git with meaningful
history? (Change-frequency data sharpens risk ranking.)
## Report
Write `analysis/$1/PREFLIGHT.md`: a status table — one row per check,
status ✅ / ⚠️ / ❌, what was found, and the fix for anything not green —
followed by a **Ready / Ready-with-gaps / Not ready** verdict per command:
- `assess` + `map` + `extract-rules` — need Checks 12 green-ish and
Check 4's missing-include count low
- `brief` — needs only the three discovery artifacts; no tooling
- `transform` + `reimagine` — additionally need Check 3 green for the
**target** stack. A red legacy toolchain downgrades these to
Ready-with-gaps, not Not-ready: equivalence testing falls back to
recorded traces / golden-master fixtures instead of dual execution
(common and expected for CICS/IMS code that has no local runtime)
- `harden` — needs Check 2 plus any stack-specific SAST tooling found
Print the table in the session too, and end with the single most
important fix if anything is red.

View File

@@ -3,7 +3,11 @@ description: Multi-agent greenfield rebuild — extract specs from legacy, desig
argument-hint: <system-dir> <target-vision>
---
**Reimagine** `legacy/$1` as: $2
The first token of `$ARGUMENTS` is the system dir (`$1`); **everything
after it is the target vision** — it is usually multiple words, so do not
truncate it to one token. Below, `<vision>` means that full remainder.
**Reimagine** `legacy/$1` as: <vision>
This is not a port — it's a rebuild from extracted intent. The legacy system
becomes the *specification source*, not the structural template. This command
@@ -19,7 +23,8 @@ Spawn concurrently and show the user that all three are running:
2. **legacy-analyst** — "Catalog every external interface of legacy/$1:
inbound (screens, APIs, batch triggers, queues) and outbound (reports,
files, downstream calls, DB writes). For each: name, direction, payload
shape, frequency/SLA if discernible."
shape, frequency/SLA if discernible. Mask any credential embedded in
endpoints or payload examples per your secret-handling rules."
3. **legacy-analyst** — "Identify the core domain entities in legacy/$1 and
their relationships. Return as an entity list + Mermaid erDiagram."
@@ -32,6 +37,9 @@ Collect results. Write `analysis/$1/AI_NATIVE_SPEC.md` containing:
- **Non-functional requirements** inferred from legacy (batch windows, volumes)
- **Behavior Contract** (the Given/When/Then rules — these are the acceptance tests)
Credential values are masked everywhere in the spec; connection details
appear as env-var placeholders (`${DATABASE_URL}`), never literals.
## Phase B — HITL checkpoint #1
Present the spec summary. Ask the user **one focused question**: "Which of
@@ -40,20 +48,21 @@ should deliberately drop?" Wait for the answer. Record it in the spec.
## Phase C — Architecture (single agent, then critique)
Design the target architecture for "$2":
Design the target architecture for "<vision>":
- Mermaid C4 Container diagram
- Service boundaries with rationale (which rules/entities live where)
- Technology choices with one-line justification each
- Data migration approach from legacy stores
Then spawn **architecture-critic**: "Review this proposed architecture for
$2 against the spec in analysis/$1/AI_NATIVE_SPEC.md. Identify over-engineering,
<vision> against the spec in analysis/$1/AI_NATIVE_SPEC.md. Identify over-engineering,
missed requirements, scaling risks, and simpler alternatives." Incorporate
the critique. Write the result to `analysis/$1/REIMAGINED_ARCHITECTURE.md`.
## Phase D — HITL checkpoint #2
Enter plan mode. Present the architecture. Wait for approval.
Present the architecture and **stop — scaffold nothing until the user
explicitly approves** (use plan mode if the session supports it).
## Phase E — Parallel scaffolding
@@ -65,7 +74,9 @@ in parallel**:
and AI_NATIVE_SPEC.md. Create: project skeleton, domain model, API stubs
matching the interface contracts, and **executable acceptance tests** for every
behavior-contract rule assigned to this service (mark unimplemented ones as
expected-failure/skip with the rule ID). Write to modernized/$1-reimagined/<service-name>/."
expected-failure/skip with the rule ID). No credential literal from legacy
code becomes a test fixture or config default — use fake same-shape values
and env-var placeholders. Write to modernized/$1-reimagined/<service-name>/."
Show the agents' progress. When all complete, run the acceptance test suites
and report: total tests, passing (scaffolded behavior), pending (rule IDs
@@ -77,7 +88,9 @@ Write `modernized/$1-reimagined/CLAUDE.md` — the persistent context file for
the new system, containing: architecture summary, service responsibilities,
where the spec lives, how to run tests, and the legacy→modern traceability
map. This file IS the knowledge graph that future agents and engineers will
load.
load — and it gets committed: connection details and credentials appear
only as env-var names with a pointer to where they're provisioned, never
as values.
Report: services scaffolded, acceptance tests defined, % behaviors with a
home, location of all artifacts.

View File

@@ -0,0 +1,54 @@
---
description: Where am I in the modernization workflow — artifact inventory, staleness, secrets hygiene, next step
argument-hint: <system-dir>
---
Report where the modernization of `$1` stands, in one screen. This is a
read-only command — inspect, never modify.
## 1 — Artifact inventory
Check `analysis/$1/` and `modernized/$1*/` and build a table — one row per
workflow stage, with the artifact's presence and modification time:
| Stage | Artifacts |
|---|---|
| preflight | `PREFLIGHT.md` |
| assess | `ASSESSMENT.md`, `ARCHITECTURE.mmd` |
| map | `topology.json`, `TOPOLOGY.html`, `*.mmd`, `extract_topology.*` |
| extract-rules | `BUSINESS_RULES.md`, `DATA_OBJECTS.md` |
| brief | `MODERNIZATION_BRIEF.md` (note whether the approval block is signed) |
| harden | `SECURITY_FINDINGS.md`, `security_remediation.patch` |
| transform / reimagine | each `modernized/$1*/<module>/` dir — note test presence and whether `TRANSFORMATION_NOTES.md` exists |
## 2 — Staleness
Flag any artifact older than an upstream artifact it derives from:
- `MODERNIZATION_BRIEF.md` older than `ASSESSMENT.md`, `topology.json`,
or `BUSINESS_RULES.md` → the brief no longer reflects discovery;
recommend re-running `/modernize-brief`.
- `TOPOLOGY.html` older than `topology.json` → re-run the injection step
from `/modernize-map`.
- Any `TRANSFORMATION_NOTES.md` older than `BUSINESS_RULES.md` → the
module may not implement the latest rule set; list which.
## 3 — Secrets hygiene
- Does `analysis/.gitignore` exist and cover `SECRETS.local.md` /
`*.local.patch`? (`git check-ignore` when in a git repo.)
- If `SECRETS.local.md` exists: confirm it is NOT tracked
(`git ls-files --error-unmatch`, expect failure) and has never been
committed (`git log --all --oneline -- <path>`, expect empty). If
either check fails, say so prominently and recommend rotation plus
history scrubbing.
## 4 — Verdict
End with three lines:
- **Where you are** — the furthest completed stage and roughly how much
of the system it covers (e.g. "mapped 100%, 2 of 14 modules
transformed").
- **What's stale** — or "nothing".
- **Next command** — the single most useful next step, with a one-line
reason.

View File

@@ -9,10 +9,37 @@ equivalence.
This is a surgical, single-module transformation — one vertical slice of the
strangler fig. Output goes to `modernized/$1/$2/`.
## Step 0 — Plan (HITL gate)
## Step 0aToolchain check (fail fast on target, adapt on legacy)
Verify the build environment **before** planning, not when the tests
first run:
- **Target stack ($3) — required.** Runtime, package manager, and test
framework all respond (`java -version` + `mvn -v`, `node -v` + `npm -v`,
`python3 -V` + `pytest --version`, …). If any are missing, stop and
report what to install — the new code and its tests cannot run without
them, so a plan gate now would just defer the failure an hour. Suggest
`/modernize-preflight $1 $3` for the full readiness report.
- **Legacy stack — advisory, never a blocker.** Try a syntax-only compile
of the module being transformed (e.g. `cobc -fsyntax-only`). Legacy
code often *cannot* build locally by nature, not by misconfiguration —
CICS/IMS programs have no local translator, and the real runtime may be
a mainframe you don't have. A failed or impossible legacy compile does
**not** stop the transform; it changes the equivalence strategy:
- dual-execution proof is off the table — characterization tests
assert against **recorded traces / golden-master fixtures** (real
production outputs, captured reports/screens, SME-confirmed
examples) instead of live legacy runs
- say so explicitly in the Step 0b plan and later in
TRANSFORMATION_NOTES.md ("equivalence is trace-based; legacy was not
executable in this environment"), so reviewers know the strength of
the proof they're approving
## Step 0b — Plan (HITL gate)
Read the source module and any business rules in `analysis/$1/BUSINESS_RULES.md`
that reference it. Then **enter plan mode** and present:
that reference it. Then present the plan and **stop — write no code until
the user explicitly approves** (use plan mode if the session supports it):
- Which source files are in scope
- The target module structure (packages/classes/files you'll create)
- Which business rules / behaviors this module implements
@@ -30,7 +57,9 @@ identify every observable behavior, and encode each as a test case with
concrete input → expected output pairs derived from the legacy logic.
Target framework: <appropriate for $3>. Write to
`modernized/$1/$2/src/test/`. These tests define 'done' — the new code
must pass all of them."
must pass all of them. Follow your secret-handling rules: no credential
literal from legacy code becomes a fixture; substitute fake same-shape
values and read anything genuinely live from environment variables."
Show the user the test file. Get a 👍 before proceeding.
@@ -68,6 +97,10 @@ Then show a visual diff of one representative behavior, legacy vs modern:
```bash
delta --side-by-side <(sed -n '<lines>p' legacy/$1/<file>) modernized/$1/$2/src/main/<file>
```
(Fall back to `diff -y --width=160` if `delta` isn't installed.) Never
pick a credential-bearing line range for this diff, and mask any
credential-like literal quoted in TRANSFORMATION_NOTES.md — the notes
live in `modernized/` and get committed.
## Step 5 — Architecture review

View File

@@ -1,6 +1,6 @@
{
"name": "security-guidance",
"version": "2.0.0",
"version": "2.0.3",
"description": "Security review for Claude-generated code. Pattern-based warnings on edits, LLM-powered diff review on Stop, and an agentic commit reviewer that catches injection, XSS, SSRF, hardcoded secrets, and 25+ other vulnerability classes.",
"author": {
"name": "David Dworken",

View File

@@ -10,15 +10,42 @@ import os
import threading
from datetime import datetime
def state_dir():
"""Return the absolute path of the plugin's state directory.
Resolution precedence (highest first):
1. SECURITY_WARNINGS_STATE_DIR — plugin-specific override (existing)
2. CLAUDE_CONFIG_DIR/security — CC's config-dir env var (#1868)
3. ~/.claude/security — default fallback
Empty-string env vars are treated as not-set so a misconfigured shell
(`CLAUDE_CONFIG_DIR=` with no value) doesn't silently write to
/security at the filesystem root.
Returns a fully-expanded absolute path (no literal `~`) so subprocess
callers can pass it through to code that doesn't re-expand tildes.
Called per-invocation rather than cached at import time so test
monkeypatches of the env vars take effect — the plugin's hooks each
run as fresh subprocesses in production, so the per-call cost is
negligible compared to subprocess spawn.
"""
explicit = os.environ.get("SECURITY_WARNINGS_STATE_DIR")
if explicit:
return os.path.expanduser(explicit)
cc_config = os.environ.get("CLAUDE_CONFIG_DIR")
if cc_config:
return os.path.expanduser(os.path.join(cc_config, "security"))
return os.path.expanduser("~/.claude/security")
# Debug log file. Lives under the plugin state dir (default ~/.claude/security/)
# rather than /tmp because /tmp is world-writable on multi-user hosts (TOCTOU /
# symlink-attack surface, cross-user log leakage). Overridable per-process via
# SECURITY_GUIDANCE_DEBUG_LOG, or per-state-dir via SECURITY_WARNINGS_STATE_DIR.
_DEFAULT_STATE_DIR = os.path.expanduser(
os.environ.get("SECURITY_WARNINGS_STATE_DIR") or "~/.claude/security"
)
# SECURITY_GUIDANCE_DEBUG_LOG, or per-state-dir via SECURITY_WARNINGS_STATE_DIR
# (plugin-specific override) or CLAUDE_CONFIG_DIR (CC-wide config dir, #1868).
DEBUG_LOG_FILE = os.environ.get("SECURITY_GUIDANCE_DEBUG_LOG") or os.path.join(
_DEFAULT_STATE_DIR, "log.txt"
state_dir(), "log.txt"
)
# Cap the debug log so parallel-worker fleets don't fill disk. When the active
# file exceeds this it's atomically rotated to <file>.1 (overwriting any prior
@@ -89,7 +116,18 @@ _PV = _read_plugin_version_int()
# Emitted via _usage_metrics() into the existing emit_metrics() channel so
# hook metrics rows carry per-invocation token/cost totals
# alongside the existing skip_reason / vulns_found fields.
_USAGE = {"in": 0, "out": 0, "cr": 0, "cw": 0, "cost": 0.0, "n": 0}
_USAGE = {
"in": 0, "out": 0, "cr": 0, "cw": 0, "cost": 0.0, "n": 0,
# HTTP error visibility (#2098 visibility gap — see emit comment in
# _usage_metrics). Without this, API failures from `_call_claude` left
# zero fingerprint in telemetry: the call returns None, the caller's
# emit_metrics carries no api_calls field, and the failure is
# indistinguishable from "no review needed". The deprecation outage
# that broke every commit-review LLM call was invisible until users
# reported it manually.
"http_err_last": 0, # most recent HTTP error code this invocation
"http_err_count": 0, # total HTTP errors (4xx + 5xx + network)
}
_USAGE_LOCK = threading.Lock()
# $/Mtok (input, output). Used only for the raw-HTTP path; the SDK path
@@ -139,19 +177,55 @@ def _record_usage(usage, model, cost_usd=None):
_USAGE["n"] += 1
def _record_http_error(status):
"""Record an HTTP error from an LLM API call. `status` is the HTTP
status code (integer 400599) or -1 for network/timeout errors. Stored
in `_USAGE["http_err_last"]` (most recent) and counted in
`_USAGE["http_err_count"]`. Snapshot via `_usage_metrics()` so every
subsequent `emit_metrics` includes the failure fingerprint.
Background: without this, the most recent example was the #2098
deprecation 400. Every hook fire's LLM call returned HTTP 400; the
plugin caught it and returned None; the emit_metrics carried no
api_calls field; aggregate dashboards looked normal. The failure
only became visible when a user manually reported errors out of
their debug log. With this field, a category-of-failure spike (4xx,
5xx, or -1 network) is queryable from BQ in real time.
"""
try:
s = int(status)
except (TypeError, ValueError):
return
with _USAGE_LOCK:
_USAGE["http_err_last"] = s
_USAGE["http_err_count"] += 1
def _usage_metrics():
"""Snapshot the accumulator as metric keys. Returns {} when no API calls
were made so skip-path emits don't burn key budget. cost_usd rounded to
1e-6 to keep the float finite/short for the zod schema."""
with _USAGE_LOCK:
if _USAGE["n"] == 0:
return {}
return {
"tok_in": _USAGE["in"],
"tok_out": _USAGE["out"],
"tok_cache_r": _USAGE["cr"],
"tok_cache_w": _USAGE["cw"],
"cost_usd": round(_USAGE["cost"], 6),
"api_calls": _USAGE["n"],
}
AND no HTTP errors were made so skip-path emits don't burn key budget.
cost_usd rounded to 1e-6 to keep the float finite/short for the zod
schema.
HTTP errors (`http_err_last`, `http_err_count`) emitted ONLY when
`http_err_count > 0` so successful calls don't pad every metrics row
with two zero fields.
"""
with _USAGE_LOCK:
if _USAGE["n"] == 0 and _USAGE["http_err_count"] == 0:
return {}
out = {}
if _USAGE["n"] > 0:
out.update({
"tok_in": _USAGE["in"],
"tok_out": _USAGE["out"],
"tok_cache_r": _USAGE["cr"],
"tok_cache_w": _USAGE["cw"],
"cost_usd": round(_USAGE["cost"], 6),
"api_calls": _USAGE["n"],
})
if _USAGE["http_err_count"] > 0:
out["http_err_last"] = _USAGE["http_err_last"]
out["http_err_count"] = _USAGE["http_err_count"]
return out

View File

@@ -138,7 +138,17 @@ def restore_unreviewed_stop_state(session_id, paths, baseline_sha):
def get_baseline_file_content(session_id, file_path, cwd):
"""Get the content of a file at the baseline SHA. Returns None if unavailable."""
"""Get the content of a file at the baseline SHA. Returns None if unavailable.
Decode the file content as UTF-8 with errors="replace" rather than using
text=True: source files in user repos can be latin-1 / cp1252 / shift-jis
/ etc., and on Windows text=True would decode via locale.getpreferredencoding()
in strict mode and raise UnicodeDecodeError in the subprocess reader
thread — leaving result.stdout=None and propagating AttributeError when
the caller tries to use it. Same class as the existing migrations at
security_reminder_hook.py:540 (reflog subjects) and :1115 (commit
diffs); this helper was missed in that pass. See
anthropics/claude-plugins-official#2056."""
baseline_sha = load_baseline_sha(session_id)
if not baseline_sha:
return None
@@ -151,12 +161,12 @@ def get_baseline_file_content(session_id, file_path, cwd):
return None
result = subprocess.run(
[*GIT_CMD, "show", f"{baseline_sha}:{rel_path}"],
cwd=cwd, capture_output=True, text=True, timeout=5
cwd=cwd, capture_output=True, timeout=5
)
if result.returncode == 0:
return result.stdout
return (result.stdout or b"").decode("utf-8", errors="replace")
return None
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
except (subprocess.TimeoutExpired, FileNotFoundError, OSError, ValueError):
return None
@@ -173,11 +183,16 @@ def capture_git_baseline(cwd):
and `compute_v2_review_set` subtracts that set so pre-existing untracked
files are not reviewed as Claude-authored.
"""
# stdout is a SHA so text=True is safe on stdout, but a non-ASCII
# filename in `git stash create`'s STDERR warning (e.g. a worktree
# with `Ávila_report.txt` triggers a quotePath/locale warning) would
# trip the stderr reader thread on Windows cp1252. Decode both streams
# leniently for symmetry with _list_untracked. See #2056.
try:
# Check if HEAD exists (i.e., repo has at least one commit)
head_check = subprocess.run(
[*GIT_CMD, "rev-parse", "HEAD"],
cwd=cwd, capture_output=True, text=True, timeout=5
cwd=cwd, capture_output=True, timeout=5
)
if head_check.returncode != 0:
# No commits yet — skip review rather than creating commits in the user's repo
@@ -186,20 +201,20 @@ def capture_git_baseline(cwd):
result = subprocess.run(
[*GIT_CMD, "stash", "create"],
cwd=cwd, capture_output=True, text=True, timeout=15
cwd=cwd, capture_output=True, timeout=15
)
sha = result.stdout.strip()
sha = (result.stdout or b"").decode("utf-8", errors="replace").strip()
if sha:
return sha
# Working tree is clean — stash create returns empty. Use HEAD.
result = subprocess.run(
[*GIT_CMD, "rev-parse", "HEAD"],
cwd=cwd, capture_output=True, text=True, timeout=5
cwd=cwd, capture_output=True, timeout=5
)
sha = result.stdout.strip()
sha = (result.stdout or b"").decode("utf-8", errors="replace").strip()
return sha if sha else None
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
except (subprocess.TimeoutExpired, FileNotFoundError, OSError, ValueError) as e:
debug_log(f"Failed to capture git baseline: {e}")
return None
@@ -323,19 +338,35 @@ def _list_untracked(cwd):
mtime is captured so an in-place edit during the turn is still reviewed.
Uses ls-files (not status) for the UPS path: the index diff isn't needed,
and ls-files --others only walks the worktree against .gitignore."""
and ls-files --others only walks the worktree against .gitignore.
Decodes stdout/stderr as UTF-8 with errors="replace" instead of using
text=True. With core.quotePath=false git emits raw UTF-8 bytes for
non-ASCII filenames; text=True decodes via locale.getpreferredencoding()
in strict mode — on Windows that's cp1252 with several undefined bytes
(0x81/0x8D/0x8F/0x90/0x9D), all of which appear in UTF-8 encodings of
common accented capitals (Á Í Ï Ð Ý) and most CJK/emoji codepoints.
A non-ASCII filename in the worktree crashed the subprocess reader
thread, left r.stdout=None, and propagated AttributeError out of the
helper — silently losing the baseline snapshot every UserPromptSubmit.
See anthropics/claude-plugins-official#2056. The sibling helpers in
gitutil.py already follow the lenient pattern; this function and
capture_git_baseline / _git_name_only / _git_status_porcelain were
the holdouts."""
try:
repo = _git_toplevel(cwd) or cwd
# core.quotePath=false comes from GIT_CMD globally (see gitutil.py).
r = subprocess.run(
[*GIT_CMD, "-c", "core.quotePath=false", "ls-files",
"--others", "--exclude-standard", "-z"],
cwd=repo, capture_output=True, text=True, timeout=15,
[*GIT_CMD, "ls-files", "--others", "--exclude-standard", "-z"],
cwd=repo, capture_output=True, timeout=15,
)
if r.returncode != 0:
debug_log(f"_list_untracked rc={r.returncode}: {r.stderr[:200]}")
stderr_str = (r.stderr or b"").decode("utf-8", errors="replace")
debug_log(f"_list_untracked rc={r.returncode}: {stderr_str[:200]}")
return {}
stdout = (r.stdout or b"").decode("utf-8", errors="replace")
out = {}
for p in r.stdout.split("\0"):
for p in stdout.split("\0"):
if not p:
continue
try:
@@ -346,7 +377,9 @@ def _list_untracked(cwd):
debug_log(f"_list_untracked: capped at {UNTRACKED_BASELINE_CAP}")
break
return out
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
except (subprocess.TimeoutExpired, FileNotFoundError, OSError, ValueError) as e:
# ValueError guards against any future strict-decode regression
# so the helper degrades to {} instead of crashing the hook.
debug_log(f"_list_untracked error: {e}")
return {}

View File

@@ -23,6 +23,12 @@ import sys
import time
from pathlib import Path
# Shared state-dir resolver: SECURITY_WARNINGS_STATE_DIR → CLAUDE_CONFIG_DIR/security
# → ~/.claude/security. See _base.state_dir for resolution precedence. Re-aliased
# here to match the existing local name (state_dir was already a local var in
# main() and _maybe_emit_user_notice).
from _base import state_dir as _resolve_state_dir
# Outcome codes for the sdk_bootstrap metric. Values are stable for telemetry.
NOOP_SYSTEM = 0 # claude_agent_sdk already importable in system python
NOOP_VENV = 1 # venv already built and SDK imports from it
@@ -36,6 +42,122 @@ HOOK_PY_INCOMPATIBLE = 6 # hook interpreter is <3.10 — SDK syntax can't load
# here no matter how the venv was built. See #2071.
# Phase + err-kind integer encoding for sdk_bootstrap_phase / sdk_bootstrap_err.
#
# Earlier versions emitted these as STRINGS (e.g. "pip", "dns_fail"). CC's
# plugin-metrics pipeline silently drops plugin-emitted string values —
# only `bool|finite-number` plugin metrics reach BigQuery. (CC-core
# metrics like `subscription_type` are exempt because they're injected
# downstream of plugin validation.) Confirmed empirically: 185K
# BUILD_FAILED rows in BQ had `sdk_bootstrap_phase`/`sdk_bootstrap_err`
# = NULL despite the Python code emitting them. This left ~28K
# BUILD_FAILED sessions/day with no diagnostic split — flying blind on
# the real failure modes (pip-no-match vs dns-fail vs ssl-verify etc.).
#
# Fix: encode as small integers per the maps below. Values are
# APPEND-ONLY for telemetry stability. Reserve 99 as the "unknown /
# uncategorized" bucket so an unmapped err_kind (e.g., a new exception
# type) still emits a non-zero signal.
SDK_BOOTSTRAP_PHASE_CODES = {
"pre": 1, # pre-venv (state_dir.mkdir, sentinel open)
"venv": 2, # python -m venv --clear
"pip": 3, # pip install
"main": 4, # uncaught exception above main()
}
SDK_BOOTSTRAP_ERR_CODES = {
"pip_no_match": 1,
"dns_fail": 2,
"conn_refused": 3,
"ssl_verify": 4,
"perm_denied": 5,
"no_pip": 6,
"disk_full": 7,
"proxy_auth": 8,
"stderr_timeout": 9, # pip stderr containing "timeout"/"timed out"
"subprocess_timeout": 10, # subprocess.TimeoutExpired (>120s)
# Venv-stage specific categories added after PR #2112 telemetry surfaced
# 2,406 phase=2/err=99 sessions in the first 3h of v2.0.1 — venv phase
# failing in ways the original pip-flavored patterns didn't catch. These
# all split out of what was previously collapsing to _uncategorized.
"venv_ensurepip_fail": 11, # Debian/Ubuntu missing python3-venv;
# stderr mentions ensurepip non-zero exit
# or "ensurepip is not available"
"venv_path_too_long": 12, # Windows MAX_PATH (260) or POSIX
# ENAMETOOLONG — venv writes deep paths
# under state_dir/agent-sdk-venv/Lib/...
"venv_no_module": 13, # `python3 -m venv` itself missing — "No
# module named 'venv'" / "No module named venv"
"venv_already_exists": 14, # Errno 17 / "file exists" — sentinel race
# past O_EXCL or stale dir survived --clear
"venv_setup_failed": 15, # Generic "virtual environment was not
# created successfully" — catches the long
# tail of venv setup failures that don't
# match a more specific category above
# 1698 reserved for future categories; APPEND-ONLY.
# 99 catches everything else (including "exc:<TypeName>" and "other:<tail>"
# — the original string is debug-loggable but the integer is what makes
# it to telemetry). For the "other:" tail, `sdk_bootstrap_stderr_sig`
# carries a bounded integer hash so we can still distinguish patterns
# in BQ aggregation.
"_uncategorized": 99,
}
def _encode_phase(s):
"""Map err_phase string to its telemetry integer code, or 0 if unset.
Empty/None → 0 lets `if encoded:` cleanly skip emission. Per
SDK_BOOTSTRAP_PHASE_CODES, valid codes are 1-4."""
return SDK_BOOTSTRAP_PHASE_CODES.get((s or "").strip(), 0)
def _encode_err_kind(s):
"""Map err_kind string to its telemetry integer code, or 0 if unset.
Direct hits use the static map; "exc:<X>" and "other:<tail>" both
collapse to _uncategorized (99) — the raw string survives in debug
logs, only the integer reaches BQ."""
s = (s or "").strip()
if not s:
return 0
if s in SDK_BOOTSTRAP_ERR_CODES:
return SDK_BOOTSTRAP_ERR_CODES[s]
# Prefix matches for the catch-all categories
if s.startswith("exc:") or s.startswith("other:") or s == "other":
return SDK_BOOTSTRAP_ERR_CODES["_uncategorized"]
# Unknown string — still emit as uncategorized rather than dropping
return SDK_BOOTSTRAP_ERR_CODES["_uncategorized"]
def _encode_stderr_sig(err_kind):
"""Bounded integer hash of the stderr tail captured in "other:<tail>"
err_kinds. Lets us distinguish patterns INSIDE the _uncategorized
(code 99) bucket without unbounded cardinality.
Returns 0 for non-"other:" err_kinds (so the field auto-omits from
emit_metrics on categorized failures — see the emit block in main()).
Strategy: take the tail's first ~30 chars (post-lowercase, post-trim),
SHA-1, fold the first 2 bytes to 0999. Different stderr messages
cluster into different buckets; same stderr always maps to the same
bucket. Cardinality is bounded at 1000, well below any "high
cardinality" alarm — and a real failure mode typically produces
near-identical stderr across thousands of machines, so 1000 buckets
is comfortably wide.
Why first ~30 chars: stderr like "ERROR: Command failed: <full
path>" varies the tail wildly (paths) but the categorization signal
is in the leading words. Dropping the suffix focuses the hash on
the discriminative part.
"""
if not err_kind or not err_kind.startswith("other:"):
return 0
import hashlib
tail = err_kind[len("other:"):].strip().lower()[:30]
if not tail:
return 0
h = hashlib.sha1(tail.encode("utf-8", errors="replace")).digest()
return int.from_bytes(h[:2], "big") % 1000
def _sdk_on_syspath() -> bool:
# find_spec is ~10ms; actually importing the SDK pulls in
# transitive deps and costs ~800ms — too heavy for a
@@ -90,10 +212,7 @@ def main() -> tuple[int, str, str]:
if _sdk_on_syspath():
return NOOP_SYSTEM, "", ""
state_dir = Path(
os.environ.get("SECURITY_WARNINGS_STATE_DIR")
or os.path.expanduser("~/.claude/security")
)
state_dir = Path(_resolve_state_dir())
venv = state_dir / "agent-sdk-venv"
# Windows venvs put the interpreter at Scripts\python.exe; POSIX uses bin/python.
if sys.platform == "win32":
@@ -177,7 +296,34 @@ def main() -> tuple[int, str, str]:
else:
stderr_str = str(stderr_b)
s = stderr_str.lower()
if "no matching distribution" in s or "could not find a version" in s:
# Venv-specific patterns checked FIRST — they overlap with some pip
# patterns (e.g. "no module named ensurepip" could match no_pip OR
# venv_ensurepip_fail; the venv-stage interpretation is the right
# one when err_phase=="venv"). Order is venv-most-specific →
# pip-historical → generic.
if err_phase == "venv" and (
"ensurepip is not available" in s
or ("ensurepip" in s and "returned non-zero" in s)
or "the virtual environment was not created" in s and "ensurepip" in s
):
err_kind = "venv_ensurepip_fail"
elif err_phase == "venv" and (
"[errno 36]" in s
or "file name too long" in s
or "path too long" in s
):
err_kind = "venv_path_too_long"
elif err_phase == "venv" and (
"no module named venv" in s
or "no module named 'venv'" in s
):
err_kind = "venv_no_module"
elif err_phase == "venv" and (
"[errno 17]" in s
or ("file exists" in s and "venv" in s)
):
err_kind = "venv_already_exists"
elif "no matching distribution" in s or "could not find a version" in s:
err_kind = "pip_no_match"
elif "name or service not known" in s or "name resolution" in s \
or "nodename nor servname" in s or "temporary failure in name" in s:
@@ -196,6 +342,15 @@ def main() -> tuple[int, str, str]:
err_kind = "proxy_auth"
elif "timeout" in s or "timed out" in s:
err_kind = "stderr_timeout"
elif err_phase == "venv" and (
"virtual environment was not created" in s
or "error: command" in s and "venv" in s
):
# Generic venv-setup catch-all — matched AFTER the more specific
# venv patterns above so we don't shadow them, but BEFORE the
# other: fallback so generic venv setup failures get their own
# bucket instead of polluting the long-tail signature space.
err_kind = "venv_setup_failed"
else:
# First 60 chars of the last non-empty stderr line — bounded to
# stay inside CC's metric value-length budget. Real failure modes
@@ -239,10 +394,7 @@ def _maybe_emit_user_notice(outcome: int, pv: int) -> str | None:
if outcome != HOOK_PY_INCOMPATIBLE:
return None
try:
state_dir = Path(
os.environ.get("SECURITY_WARNINGS_STATE_DIR")
or os.path.expanduser("~/.claude/security")
)
state_dir = Path(_resolve_state_dir())
marker = state_dir / f".agentic_unavailable_notice_v{pv or 0}"
if marker.exists():
return None
@@ -288,21 +440,33 @@ if __name__ == "__main__":
# and takes the FIRST non-{"async":...} JSON line as the hook response;
# its `metrics` key is forwarded to the hook metrics event on the
# next attachments pass. Must be a single line — the registry splits on
# \n and json-parses each independently. Values must be bool|number OR
# short strings (CC accepts string metric values if they're not
# null). Stay inside the 10-key emit cap.
# \n and json-parses each independently.
#
# IMPORTANT — values must be bool|finite-number. The validation comment
# has historically said "or short strings" but that was wrong: CC's
# plugin-metrics pipeline silently drops plugin-emitted string values.
# Stay inside the 10-key emit cap.
metrics: dict[str, object] = {
"sdk_bootstrap": outcome,
"sdk_bootstrap_ms": round((time.perf_counter() - t0) * 1000),
}
if err_kind:
# Truncate defensively; categorized values are <40 chars but the
# `other:<tail>` mode could be longer. err_phase may be empty for
# pre-venv failures (state_dir.mkdir perm-denied, sentinel O_EXCL
# raising a non-FileExistsError OSError) — emit as "pre" so the
# err_kind isn't silently dropped.
metrics["sdk_bootstrap_phase"] = (err_phase or "pre")[:16]
metrics["sdk_bootstrap_err"] = err_kind[:96]
# Encode phase + err_kind as integer codes (see
# SDK_BOOTSTRAP_PHASE_CODES / SDK_BOOTSTRAP_ERR_CODES). Earlier
# versions emitted these as strings and CC dropped them — restoring
# the diagnostic split that 28K BUILD_FAILED/day need to triage by
# root cause. err_phase defaults to "pre" when empty (pre-venv
# failure path, e.g. state_dir.mkdir perm-denied).
metrics["sdk_bootstrap_phase"] = _encode_phase(err_phase or "pre")
metrics["sdk_bootstrap_err"] = _encode_err_kind(err_kind)
# For "other:<tail>" (encoded err==99), emit a bounded integer
# hash of the stderr tail so BQ can distinguish patterns inside
# the _uncategorized bucket without unbounded cardinality. Zero
# when err_kind is categorized — the schema reader treats 0 as
# "no signal", matching the absence convention.
sig = _encode_stderr_sig(err_kind)
if sig:
metrics["sdk_bootstrap_stderr_sig"] = sig
pv = _plugin_version_int()
if pv:
metrics["pv"] = pv

View File

@@ -26,18 +26,34 @@ GIT_CMD = [
"git",
"-c", "core.fsmonitor=false",
"-c", "core.hooksPath=/dev/null",
# core.quotePath=false: emit raw UTF-8 in path-emitting commands instead
# of C-quoting non-ASCII bytes (default `"\\303\\201vila/..."` vs
# `Ávila/...`). Downstream parsers — both ours (parse_diff_into_files,
# extract_file_paths_from_diff) and Python stdlib (os.path.isabs,
# os.path.join) — expect raw paths and silently drop / mishandle the
# quoted form. Adding the flag globally to GIT_CMD covers every
# subprocess.run site that uses the splat — diff feeders, rev-parse
# path queries (--show-toplevel, --git-dir, --git-common-dir),
# reflog %gs subjects, ls-files, status, etc. — without per-site
# flag duplication. See #2082, #2099.
"-c", "core.quotePath=false",
]
def _git_rev_parse_head(cwd):
"""Return the current HEAD SHA, or None if not a git repo / no commits."""
try:
# See #2099: text=True on Windows cp1252 crashes the reader thread on
# any UTF-8 byte undefined in cp1252 (e.g. via a git error message
# referencing a non-ASCII filename in stderr). stdout is a SHA so it
# IS safe; stderr is not. capture_output=True with bytes-by-default
# never decodes, so the reader thread can't crash.
result = subprocess.run(
[*GIT_CMD, "rev-parse", "HEAD"],
cwd=cwd, capture_output=True, text=True, timeout=5
cwd=cwd, capture_output=True, timeout=5
)
if result.returncode == 0 and result.stdout.strip():
return result.stdout.strip()
return result.stdout.decode("utf-8", errors="replace").strip()
return None
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
return None
@@ -52,13 +68,17 @@ def _find_git_index(cwd):
Returns the absolute path to the index file, or None.
"""
try:
# See #2099: stdout here is a PATH which can contain non-ASCII bytes
# (e.g. C:\אבטחה\repo\.git). text=True decodes via cp1252 strict on
# Windows → crashes the reader thread → returns stdout=None →
# caller does .strip() on None → AttributeError. Decode manually.
result = subprocess.run(
[*GIT_CMD, "rev-parse", "--git-dir"],
cwd=cwd, capture_output=True, text=True, timeout=5
cwd=cwd, capture_output=True, timeout=5
)
if result.returncode != 0:
return None
git_dir = result.stdout.strip()
git_dir = result.stdout.decode("utf-8", errors="replace").strip()
if not os.path.isabs(git_dir):
git_dir = os.path.join(cwd, git_dir)
index_path = os.path.join(git_dir, "index")
@@ -128,9 +148,13 @@ def _temp_index(cwd, untracked_paths=None):
else:
add_args = None
if add_args:
# No stdout used here (only returncode matters), but text=True
# still spawns reader threads that decode stderr — git error
# messages can reference non-ASCII filenames and crash on
# cp1252. See #2099. Drop text=True so bytes stay raw.
subprocess.run(
[*GIT_CMD, "add", "--intent-to-add"] + add_args,
cwd=cwd, capture_output=True, text=True, timeout=10,
cwd=cwd, capture_output=True, timeout=10,
env=env,
)
yield env
@@ -144,11 +168,17 @@ def _temp_index(cwd, untracked_paths=None):
def _git_toplevel(cwd):
"""Absolute repo root for `cwd`, or None if not in a work tree."""
try:
# See #2099: stdout is a PATH — `C:\אבטחה\repo` returned as UTF-8
# bytes by git. text=True would decode via cp1252 strict on Windows
# → reader-thread crash. Decode manually with errors="replace".
r = subprocess.run(
[*GIT_CMD, "rev-parse", "--show-toplevel"],
cwd=cwd, capture_output=True, text=True, timeout=5,
cwd=cwd, capture_output=True, timeout=5,
)
return r.stdout.strip() if r.returncode == 0 and r.stdout.strip() else None
if r.returncode != 0:
return None
path = r.stdout.decode("utf-8", errors="replace").strip()
return path if path else None
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
return None
@@ -164,13 +194,15 @@ def _git_dir(repo_root):
callers can degrade (push-sweep state is best-effort).
"""
try:
# See #2099: stdout is a PATH (shared gitdir), may be non-ASCII.
# Decode bytes manually to avoid cp1252 reader-thread crash.
r = subprocess.run(
[*GIT_CMD, "rev-parse", "--git-common-dir"],
cwd=repo_root, capture_output=True, text=True, timeout=5,
cwd=repo_root, capture_output=True, timeout=5,
)
if r.returncode != 0:
return None
d = r.stdout.strip()
d = r.stdout.decode("utf-8", errors="replace").strip()
return d if os.path.isabs(d) else os.path.join(repo_root, d)
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
return None
@@ -179,13 +211,15 @@ def _git_dir(repo_root):
def _git_rev_list_range(repo_root, base, head="HEAD"):
"""Shas in `base..head`, oldest→newest. Empty list on error."""
try:
# See #2099: stdout is ASCII SHAs, but stderr can carry git error
# messages referencing non-ASCII filenames — keep bytes raw.
r = subprocess.run(
[*GIT_CMD, "rev-list", "--reverse", f"{base}..{head}"],
cwd=repo_root, capture_output=True, text=True, timeout=10,
cwd=repo_root, capture_output=True, timeout=10,
)
if r.returncode != 0:
return []
return [s for s in r.stdout.strip().split("\n") if s]
return [s for s in r.stdout.decode("utf-8", errors="replace").strip().split("\n") if s]
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
return []
@@ -199,6 +233,10 @@ def _git_diff_range(repo_root, base, head="HEAD"):
them reviewed — otherwise unreviewed commits get permanently silenced.
"""
try:
# GIT_CMD globally passes core.quotePath=false (see definition) so
# non-ASCII paths in `diff --git a/... b/...` headers come through as
# raw UTF-8, not C-quoted. Required by the downstream
# parse_diff_into_files / extract_file_paths_from_diff regex.
r = subprocess.run(
[*GIT_CMD, "diff", "-p", "--no-color", "--no-ext-diff", base, head],
cwd=repo_root, capture_output=True, timeout=30,
@@ -213,9 +251,11 @@ def _git_diff_range(repo_root, base, head="HEAD"):
def _detect_main_branch(repo_root):
for ref in ("origin/HEAD", "origin/main", "origin/master", "main", "master"):
try:
# See #2099: stdout is a SHA but stderr can carry non-ASCII git
# warnings — keep bytes raw to avoid cp1252 reader-thread crash.
r = subprocess.run(
[*GIT_CMD, "rev-parse", "--verify", "-q", ref],
cwd=repo_root, capture_output=True, text=True, timeout=5,
cwd=repo_root, capture_output=True, timeout=5,
)
if r.returncode == 0 and r.stdout.strip():
return ref
@@ -259,19 +299,29 @@ def _git_reflog_recent_commits(repo_root, max_age_s=120, max_n=5):
# %gs (the reflog subject) is `commit: <commit-msg first line>` and can
# contain `|`; put it LAST so split("|", 2) leaves it intact. %H is
# hex and %ct is integer, so the first two fields are delimiter-safe.
#
# Bytes + decode utf-8/replace: %gs embeds commit-message subjects
# which git stores as raw bytes — commits can be authored in
# latin-1 / cp1252 / shift-jis etc., and text=True would raise
# UnicodeDecodeError in the subprocess reader thread on Windows
# cp1252 (subprocess.run returns r.stdout=None, then
# r.stdout.splitlines() AttributeErrors). Mirrors the existing
# migration at security_reminder_hook.py:540 — same pattern was
# missed here. See anthropics/claude-plugins-official#2056.
r = subprocess.run(
[*GIT_CMD, "log", "-g", "-n", str(max_n),
"--format=%H|%ct|%gs", "HEAD"],
cwd=repo_root, capture_output=True, text=True, timeout=5,
cwd=repo_root, capture_output=True, timeout=5,
)
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
except (subprocess.TimeoutExpired, FileNotFoundError, OSError, ValueError):
return [], 0
if r.returncode != 0:
return [], 0
stdout = (r.stdout or b"").decode("utf-8", errors="replace")
import time as _time
now = int(_time.time())
fresh, stale = [], 0
for idx, line in enumerate(r.stdout.splitlines()):
for idx, line in enumerate(stdout.splitlines()):
parts = line.split("|", 2)
if len(parts) != 3:
continue
@@ -306,23 +356,32 @@ def _git_name_only(cwd, base, include_untracked=False):
must distinguish None (error → don't trust as a filter) from set()
(genuinely nothing changed). `-c core.quotePath=false -z` keeps non-ASCII
and space-containing paths intact."""
# Decode stdout/stderr as UTF-8 with errors="replace" instead of using
# text=True. core.quotePath=false makes git emit raw UTF-8 for non-ASCII
# paths, and text=True on Windows decodes via cp1252 strict — a non-ASCII
# changed path would crash the subprocess reader thread, leave
# result.stdout=None, and propagate AttributeError out of the helper.
# Same fix shape as diffstate._list_untracked. See #2056.
def _run(env):
# core.quotePath=false comes from GIT_CMD globally (see definition).
result = subprocess.run(
[*GIT_CMD, "-c", "core.quotePath=false", "diff", "--name-only", "-z", base],
cwd=cwd, capture_output=True, text=True, timeout=30,
[*GIT_CMD, "diff", "--name-only", "-z", base],
cwd=cwd, capture_output=True, timeout=30,
env=env,
)
if result.returncode != 0:
debug_log(f"_git_name_only({base!r}) rc={result.returncode}: {result.stderr[:200]}")
stderr_str = (result.stderr or b"").decode("utf-8", errors="replace")
debug_log(f"_git_name_only({base!r}) rc={result.returncode}: {stderr_str[:200]}")
return None
return {p for p in result.stdout.split("\0") if p}
stdout = (result.stdout or b"").decode("utf-8", errors="replace")
return {p for p in stdout.split("\0") if p}
try:
if not include_untracked:
return _run(None)
with _temp_index(cwd) as env:
return _run(env)
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
except (subprocess.TimeoutExpired, FileNotFoundError, OSError, ValueError) as e:
debug_log(f"_git_name_only({base!r}) error: {e}")
return None
@@ -339,17 +398,22 @@ def _git_status_porcelain(cwd):
collapses to `dir/`). Required so the untracked set subtracts cleanly
against the UPS-time `_list_untracked` snapshot, which uses ls-files and
therefore always lists individual files."""
# Lenient decode: same UTF-8 + errors="replace" pattern as the
# sibling helpers — a non-ASCII path in the worktree would otherwise
# crash the cp1252 reader thread on Windows. See #2056.
try:
# core.quotePath=false comes from GIT_CMD globally (see definition).
r = subprocess.run(
[*GIT_CMD, "-c", "core.quotePath=false", "status",
"--porcelain=v1", "-uall", "-z"],
cwd=cwd, capture_output=True, text=True, timeout=30,
[*GIT_CMD, "status", "--porcelain=v1", "-uall", "-z"],
cwd=cwd, capture_output=True, timeout=30,
)
if r.returncode != 0:
debug_log(f"_git_status_porcelain rc={r.returncode}: {r.stderr[:200]}")
stderr_str = (r.stderr or b"").decode("utf-8", errors="replace")
debug_log(f"_git_status_porcelain rc={r.returncode}: {stderr_str[:200]}")
return None, None
tracked, untracked = set(), set()
entries = r.stdout.split("\0")
stdout = (r.stdout or b"").decode("utf-8", errors="replace")
entries = stdout.split("\0")
i = 0
while i < len(entries):
e = entries[i]
@@ -368,7 +432,9 @@ def _git_status_porcelain(cwd):
i += 1
i += 1
return tracked, untracked
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
except (subprocess.TimeoutExpired, FileNotFoundError, OSError, ValueError) as e:
# ValueError guards against any future strict-decode regression
# so the helper degrades to (None, None) instead of crashing.
debug_log(f"_git_status_porcelain error: {e}")
return None, None
@@ -378,9 +444,12 @@ def _is_ancestor(cwd, maybe_ancestor, descendant):
"""True if `maybe_ancestor` is reachable from `descendant` (i.e. HEAD
moved forward via commit/merge, not sideways via checkout)."""
try:
# See #2099: only returncode matters, but text=True spawns reader
# threads that decode stderr — git error messages can carry non-ASCII
# filenames. Drop text=True to keep bytes raw, avoid cp1252 crash.
result = subprocess.run(
[*GIT_CMD, "merge-base", "--is-ancestor", maybe_ancestor, descendant],
cwd=cwd, capture_output=True, text=True, timeout=5,
cwd=cwd, capture_output=True, timeout=5,
)
return result.returncode == 0
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
@@ -411,6 +480,7 @@ def get_git_diff(cwd, baseline_sha, full_context=False, paths=None, untracked_pa
# change exists to fix.
return ""
# core.quotePath=false comes from GIT_CMD globally (see definition).
cmd = [*GIT_CMD, "diff", "--no-color", "--no-ext-diff", baseline_sha] + (["--unified=99999"] if full_context else []) + pathspec
try:
with _temp_index(cwd, untracked_paths) as env:

View File

@@ -49,6 +49,30 @@
"asyncRewake": true,
"rewakeMessage": "Background security review of pushed commits not yet reviewed — address or acknowledge the findings below, then continue with the user's original request or continue waiting for their reply:",
"rewakeSummary": "Push security review found issues"
},
{
"type": "command",
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/sg-python.sh\" \"${CLAUDE_PLUGIN_ROOT}/hooks/security_reminder_hook.py\"",
"if": "Bash(gt create:*)",
"asyncRewake": true,
"rewakeMessage": "Background security review of commit — address or acknowledge the findings below, then continue with the user's original request or continue waiting for their reply:",
"rewakeSummary": "Commit security review found issues"
},
{
"type": "command",
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/sg-python.sh\" \"${CLAUDE_PLUGIN_ROOT}/hooks/security_reminder_hook.py\"",
"if": "Bash(gt modify:*)",
"asyncRewake": true,
"rewakeMessage": "Background security review of commit — address or acknowledge the findings below, then continue with the user's original request or continue waiting for their reply:",
"rewakeSummary": "Commit security review found issues"
},
{
"type": "command",
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/sg-python.sh\" \"${CLAUDE_PLUGIN_ROOT}/hooks/security_reminder_hook.py\"",
"if": "Bash(gt submit:*)",
"asyncRewake": true,
"rewakeMessage": "Background security review of pushed commits not yet reviewed — address or acknowledge the findings below, then continue with the user's original request or continue waiting for their reply:",
"rewakeSummary": "Push security review found issues"
}
],
"matcher": "Bash"

View File

@@ -27,7 +27,7 @@ from typing import Optional, Tuple, Dict, Any, List
import extensibility
import review_api
from _base import debug_log, _record_usage, _PV, PROVENANCE_TAG # noqa: F401
from _base import debug_log, _record_usage, _record_http_error, _PV, PROVENANCE_TAG, state_dir as _resolve_state_dir # noqa: F401
from session_state import with_locked_state
@@ -355,10 +355,7 @@ def _call_claude_via_sdk(prompt, output_schema, *, max_tokens=16000, model=None)
# Try the venv ensure_agent_sdk.py builds. Same fallback logic as
# agentic_review() — duplicated here so the 3P path doesn't require
# the agentic path to have run first.
_state_dir = os.environ.get(
"SECURITY_WARNINGS_STATE_DIR",
os.path.expanduser("~/.claude/security"),
)
_state_dir = _resolve_state_dir()
_inject_agent_sdk_venv_into_syspath(_state_dir)
try:
import asyncio as _asyncio # noqa: F811
@@ -371,6 +368,7 @@ def _call_claude_via_sdk(prompt, output_schema, *, max_tokens=16000, model=None)
except Exception as e:
debug_log(f"3P sdk-single-turn: SDK unavailable ({e})")
_last_call_claude_http_error = -1
_record_http_error(-1)
return None
cli_path = os.environ.get("SG_AGENTIC_CLI_PATH") or None
@@ -428,6 +426,7 @@ def _call_claude_via_sdk(prompt, output_schema, *, max_tokens=16000, model=None)
except _asyncio.TimeoutError:
debug_log("3P sdk-single-turn: timeout after 60s")
_last_call_claude_http_error = -1
_record_http_error(-1)
return None
except Exception as e:
debug_log(f"3P sdk-single-turn: query failed ({e})")
@@ -436,6 +435,7 @@ def _call_claude_via_sdk(prompt, output_schema, *, max_tokens=16000, model=None)
for _l in _captured_stderr[:20]:
debug_log(f" | {_l.rstrip()}")
_last_call_claude_http_error = -1
_record_http_error(-1)
return None
@@ -482,10 +482,21 @@ def _call_claude(prompt, output_schema, thinking_budget=10000, max_tokens=16000,
"max_tokens": max_tokens,
"system": CLAUDE_CODE_SYSTEM_PROMPT,
"messages": [{"role": "user", "content": prompt}],
"output_format": {
"type": "json_schema",
"schema": output_schema
}
# API moved the structured-output schema from top-level `output_format`
# to `output_config.format` per
# https://platform.claude.com/docs/en/build-with-claude/structured-outputs.
# The old form "continues to work for a transition period" for some
# auth modes (API key + non-streaming), but is rejected with
# `invalid_request_error: output_format: This field is deprecated.
# Use 'output_config.format' instead.` for others (OAuth Bearer +
# newer CLI versions hit it consistently — reporter saw 462 errors
# in one day). See #2098.
"output_config": {
"format": {
"type": "json_schema",
"schema": output_schema,
},
},
}
if thinking_budget > 0:
# Models trained on adaptive thinking (4.6+) reject the budget_tokens
@@ -493,7 +504,10 @@ def _call_claude(prompt, output_schema, thinking_budget=10000, max_tokens=16000,
# models (4.5 and earlier, all 3.x) reject adaptive. Pick by model.
if _model_supports_adaptive_thinking(payload["model"]):
payload["thinking"] = {"type": "adaptive"}
payload["output_config"] = {"effort": "high"}
# Merge `effort` into the existing output_config dict (which
# now carries the `format` schema) rather than reassigning —
# otherwise the schema is silently overwritten. See #2098.
payload["output_config"]["effort"] = "high"
else:
payload["thinking"] = {
"type": "enabled",
@@ -531,6 +545,7 @@ def _call_claude(prompt, output_schema, thinking_budget=10000, max_tokens=16000,
error_body = e.read().decode("utf-8") if e.fp else ""
debug_log(f"API error: {e.code} - {error_body[:200]}")
_last_call_claude_http_error = e.code
_record_http_error(e.code)
return None
except (urllib.error.URLError, TimeoutError) as e:
if attempt < 2:
@@ -540,6 +555,7 @@ def _call_claude(prompt, output_schema, thinking_budget=10000, max_tokens=16000,
else:
debug_log(f"Request failed after retries: {e}")
_last_call_claude_http_error = -1
_record_http_error(-1)
return None
if not response_data:
@@ -548,6 +564,7 @@ def _call_claude(prompt, output_schema, thinking_budget=10000, max_tokens=16000,
# call uses the token; record the 401 so callers don't see error=None.
if _last_call_claude_http_error is None:
_last_call_claude_http_error = 401
_record_http_error(401)
return None
# Find the text block (skip thinking blocks)
@@ -1145,10 +1162,7 @@ def agentic_review(
# ~/.claude/security/ with the SDK installed; try that as a fallback
# before giving up. The system import is attempted first so users
# who DO have it never touch the venv.
_state_dir = os.environ.get(
"SECURITY_WARNINGS_STATE_DIR",
os.path.expanduser("~/.claude/security"),
)
_state_dir = _resolve_state_dir()
_venv_tried = _inject_agent_sdk_venv_into_syspath(_state_dir)
try:
import asyncio as _asyncio # noqa: F811

View File

@@ -94,6 +94,9 @@ Only use exec() if you absolutely need shell features and the input is guarantee
},
{
"ruleName": "new_function_injection",
# JS-only construct: gate to JS/TS files so docs/.md and other prose
# mentioning "new Function" don't trip the warning.
"path_filter": lambda p: p.endswith(_JS_EXTS),
"substrings": ["new Function"],
"reminder": "\u26a0\ufe0f Security Warning: Using new Function() with string interpolation is a CODE INJECTION vulnerability. If any variable is concatenated or interpolated into the function body string, an attacker controlling that variable can execute arbitrary code. Use safe alternatives: for property access use obj[key] or array.reduce((o, k) => o[k], root); for computation use a safe expression parser. NEVER interpolate untrusted strings into new Function() bodies.",
},
@@ -107,16 +110,24 @@ Only use exec() if you absolutely need shell features and the input is guarantee
},
{
"ruleName": "react_dangerously_set_html",
# JS/TS-only (React); gate so .md docs / .py / .go files don't trip.
"path_filter": lambda p: p.endswith(_JS_EXTS),
"substrings": ["dangerouslySetInnerHTML"],
"reminder": "⚠️ Security Warning: dangerouslySetInnerHTML can lead to XSS vulnerabilities if used with untrusted content. Ensure all content is properly sanitized using an HTML sanitizer library like DOMPurify, or use safe alternatives.",
},
{
"ruleName": "document_write_xss",
# Browser DOM API: only meaningful in JS/TS source.
"path_filter": lambda p: p.endswith(_JS_EXTS),
"substrings": ["document.write"],
"reminder": "⚠️ Security Warning: document.write() can be exploited for XSS attacks and has performance issues. Use DOM manipulation methods like createElement() and appendChild() instead.",
},
{
"ruleName": "innerHTML_xss",
# Browser DOM API: only meaningful in JS/TS source. Closes FPs like
# docs/example HTML, playground/self-contained skills that hardcode
# innerHTML strings with zero user input (#410).
"path_filter": lambda p: p.endswith(_JS_EXTS),
"substrings": [".innerHTML =", ".innerHTML="],
"reminder": "⚠️ Security Warning: Setting innerHTML with untrusted content can lead to XSS vulnerabilities. Use textContent for plain text or safe DOM methods for HTML content. If you need HTML support, consider using an HTML sanitizer library such as DOMPurify.",
},
@@ -217,11 +228,15 @@ Additionally, validate user inputs:
},
{
"ruleName": "outerHTML_xss",
# Browser DOM API: only meaningful in JS/TS source.
"path_filter": lambda p: p.endswith(_JS_EXTS),
"substrings": [".outerHTML =", ".outerHTML="],
"reminder": "⚠️ Security Warning: Use textContent or sanitize with DOMPurify. outerHTML assignment is an XSS sink equivalent to innerHTML.",
},
{
"ruleName": "insertAdjacentHTML_xss",
# Browser DOM API: only meaningful in JS/TS source.
"path_filter": lambda p: p.endswith(_JS_EXTS),
"substrings": [".insertAdjacentHTML("],
"reminder": "⚠️ Security Warning: Use insertAdjacentText() or sanitize with DOMPurify. insertAdjacentHTML is an XSS sink.",
},

View File

@@ -82,6 +82,7 @@ from _base import ( # noqa: E402,F401
PROVENANCE_TAG, PROVENANCE_BANNER,
_read_plugin_version_int, _PV, _USAGE, _USAGE_LOCK,
_PRICE_PER_MTOK, _PRICE_DEFAULT, _record_usage, _usage_metrics,
state_dir as _resolve_state_dir,
)
import extensibility # noqa: E402
from patterns import ( # noqa: E402,F401
@@ -190,7 +191,13 @@ CONTINUATION_SUFFIX = (
"response."
)
def emit_metrics(metrics, rewake_summary=None):
def emit_metrics(
metrics,
rewake_summary=None,
additional_context=None,
system_message=None,
hook_event_name="PostToolUse",
):
"""
Write a SyncHookJSONOutput line to stdout for Claude Code to pick up.
For asyncRewake (Stop) hooks, CC scans stdout for the first {-prefixed line
@@ -213,6 +220,45 @@ def emit_metrics(metrics, rewake_summary=None):
rewakeSummary in hooks.json, shown to the user in the terminal as the
task-notification one-liner. Must be in the same JSON line as the metrics
because CC stops scanning stdout after the first {-prefixed line.
`additional_context` (asyncRewake findings): model-visible guidance text.
Delivery channel depends on `hook_event_name` because CC's hook-output
contract is NOT symmetric across events:
- PostToolUse (commit-review, push-sweep): surfaced via the modern
hookSpecificOutput.additionalContext protocol. `PostToolUse` is a
member of CC's hookSpecificOutput discriminated union
(coreSchemas.ts), so the JSON validates and metrics/rewakeSummary
are consumed. See #1375 / #1783 for why this replaced the legacy
stderr + exit(2) shape for PostToolUse.
- Stop / SubagentStop: there is NO `Stop` member in that union, so
emitting hookSpecificOutput{hookEventName:"Stop"} makes the whole
line fail isSyncHookJSONOutput validation — which on the asyncRewake
path silently drops metrics AND rewakeSummary, and (because the
legacy stderr write was removed) leaks the raw JSON to the model as
the rewake body. CC's asyncRewake delivery actually reads
`stderr || stdout` for the model-visible body and only scans stdout
JSON for metrics+rewakeSummary — it never reads additionalContext
on this path. So for Stop we use the documented clean pattern:
guidance on stderr, valid JSON (metrics + rewakeSummary +
top-level decision/reason) on stdout. The top-level decision:"block"
+ reason also covers the sync-fallback path (single-shot `claude -p`,
where asyncRewake degrades to a sync Stop hook that reads
decision/reason). See #2159.
Empty/None additional_context emits neither channel (back-compat for
metrics-only callers).
`system_message` (optional, asyncRewake only): user-visible TUI message,
distinct from rewakeSummary which is the task-notification one-liner.
Use sparingly — the rewakeMessage in hooks.json is the primary user
surface; systemMessage adds a per-fire override when the static
rewakeMessage isn't specific enough for the finding being shown.
`hook_event_name` (used only when additional_context is set): selects the
delivery channel above. Defaults to "PostToolUse" (commit-review and
push-sweep are the most common callers); handle_stop_hook passes "Stop".
"""
head = {}
if _PV and "pv" not in metrics:
@@ -223,6 +269,26 @@ def emit_metrics(metrics, rewake_summary=None):
out = {"metrics": metrics}
if rewake_summary:
out["rewakeSummary"] = rewake_summary
if additional_context:
if hook_event_name in ("Stop", "SubagentStop"):
# Stop is NOT in CC's hookSpecificOutput union — emitting it there
# fails schema validation and drops metrics+rewakeSummary (#2159).
# Clean pattern: guidance on stderr (the asyncRewake body channel,
# delivered via `stderr || stdout`), top-level decision/reason for
# the sync-fallback path. stdout JSON stays valid so metrics +
# rewakeSummary survive.
sys.stderr.write(additional_context)
sys.stderr.flush()
out["decision"] = "block"
out["reason"] = additional_context
else:
# PostToolUse et al. — valid union member; modern protocol.
out["hookSpecificOutput"] = {
"hookEventName": hook_event_name,
"additionalContext": additional_context,
}
if system_message:
out["systemMessage"] = system_message
print(json.dumps(out), flush=True)
# =====================================================================
@@ -510,7 +576,11 @@ def handle_user_prompt_submit(input_data):
elif sha:
debug_log(f"Captured git baseline: {sha[:12]}")
else:
debug_log("Failed to capture git baseline (not a git repo?)")
# Show cwd so the next reporter can immediately see when this isn't
# actually "not a git repo" but a path-encoding / permissions / git
# invocation failure. See #2099.
debug_log(f"Failed to capture git baseline (cwd={cwd!r}) — not a git repo, "
f"or git invocation failed (check log entries above)")
sys.exit(0)
@@ -594,8 +664,29 @@ _COMMIT_SHA_RE = re.compile(r'^\[[^\]]*?\b([0-9a-f]{7,40})\]', re.MULTILINE)
# detection — it does NOT tolerate `git -c k=v commit` global options, which
# keeps this hook aligned with CC's commit attribution on what counts as a
# commit.
_GIT_COMMIT_RE = re.compile(r'\bgit\s+commit(?:\s|$)')
_GIT_AMEND_RE = re.compile(r'\s--amend\b')
#
# Also matches `gt create` and `gt modify` — Graphite's stacked-PR wrapper
# around git. `gt create` produces a new commit (mapped to git commit
# semantics); `gt modify` amends the current commit (mapped to git commit
# --amend, also flagged by _GIT_AMEND_RE below). The hooks.json matcher
# widening for `gt create:*` / `gt modify:*` / `gt submit:*` ships in the
# same change set — without that widening this regex change is dead code
# because the hook subprocess never spawns for gt invocations. See #2048.
_GIT_COMMIT_RE = re.compile(
# `git -C <path>` and `git -c key=val` global options are allowed between
# `git` and `commit` (mirrors the long-standing tolerance in
# _GIT_PUSH_RE). Without this, `git -C /repo commit` is silently dropped
# by the handler — see #2089's secondary finding. The gt branch has no
# global-option layer to worry about.
r'\bgit(?:\s+-[Cc]\s+\S+|\s+--\S+=\S+)*\s+commit\b'
r'|\bgt\s+(?:create|modify)\b'
)
# Match either the `--amend` flag (with the leading whitespace boundary
# preserved from the original) OR `gt modify` which is semantically an
# amend. The handler treats matches as "find the pre-amend SHA via reflog
# and diff against THAT, not against the post-amend HEAD's parent" — same
# code path for both git --amend and gt modify.
_GIT_AMEND_RE = re.compile(r'(?:\s--amend\b|\bgt\s+modify\b)')
# Rolling-window cap on LLM commit-review calls. See atomic_check_rate_limit
# docstring for the rationale that motivated the switch from a lifetime cap.
@@ -624,8 +715,13 @@ COMMIT_REVIEW_RATE_WINDOW_S = int(
# entry would buy minimal extra coverage (sessions that push only via gh) at
# the cost of an extra python spawn on every `... && gh pr create` compound
# (the common case). Those sessions are caught on their next standalone `git push`.
# Matches `git push` (with optional `-c k=v` / `-C path` global options
# CC's hooks.json matcher doesn't tolerate) OR `gt submit` — Graphite's
# stacked-PR push command. gt submit forwards to `git push` internally,
# but the bash hook fires on Claude's top-level command so we need to
# recognize gt submit at the matcher level. See #2048.
_GIT_PUSH_RE = re.compile(
r'\bgit(?:\s+-[cC]\s+\S+|\s+--\S+=\S+)*\s+push\b'
r'(?:\bgit(?:\s+-[cC]\s+\S+|\s+--\S+=\S+)*\s+push\b|\bgt\s+submit\b)'
)
# `git push` stdout: "abc1234..def5678 branch -> branch" (or `+abc..def` on
@@ -791,23 +887,30 @@ def _detect_prev_upstream(repo_root, bash_output):
# @{u}@{1} — only meaningful if an upstream is configured.
for ref in ("@{u}@{1}", "@{push}@{1}"):
try:
# See #2099: stdout is a SHA but stderr can carry non-ASCII git
# warnings — keep bytes raw to avoid cp1252 reader-thread crash.
r = subprocess.run(
[*GIT_CMD, "rev-parse", "--verify", "-q", ref],
cwd=repo_root, capture_output=True, text=True, timeout=5,
cwd=repo_root, capture_output=True, timeout=5,
)
if r.returncode == 0 and r.stdout.strip():
return r.stdout.strip()
sha = r.stdout.decode("utf-8", errors="replace").strip()
if r.returncode == 0 and sha:
return sha
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
pass
main = _detect_main_branch(repo_root)
if main:
try:
# See #2099: drop text=True; decode bytes manually so a
# cp1252-undefined byte in git's stderr doesn't crash the
# reader thread.
r = subprocess.run(
[*GIT_CMD, "merge-base", "HEAD", main],
cwd=repo_root, capture_output=True, text=True, timeout=5,
cwd=repo_root, capture_output=True, timeout=5,
)
if r.returncode == 0 and r.stdout.strip():
return r.stdout.strip()
sha = r.stdout.decode("utf-8", errors="replace").strip()
if r.returncode == 0 and sha:
return sha
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
pass
return None
@@ -1118,11 +1221,16 @@ def handle_commit_review_posttooluse(input_data):
resolved = 0
for sha in shas:
try:
# core.quotePath=false: emit raw UTF-8 in `diff --git a/... b/...`
# headers so non-ASCII paths aren't C-quoted past the downstream
# parse_diff_into_files regex (sibling of #2056 / #2075). See #2082.
# core.quotePath=false comes from GIT_CMD globally (see gitutil.py).
if pre_amend_sha:
# Delta review: pre-amend → post-amend. `git diff` (not show)
# so the output is a pure unified diff with no commit header.
result = subprocess.run(
[*GIT_CMD, "diff", "--no-color", "--no-ext-diff", pre_amend_sha, sha, "--"],
[*GIT_CMD, "diff", "--no-color", "--no-ext-diff",
pre_amend_sha, sha, "--"],
cwd=repo_root, capture_output=True, timeout=15
)
else:
@@ -1254,12 +1362,13 @@ def handle_commit_review_posttooluse(input_data):
try:
full_shas = []
for s in shas:
# See #2099: drop text=True; decode manually for cp1252 safety.
r = subprocess.run(
[*GIT_CMD, "rev-parse", "--verify", "-q", s],
cwd=repo_root, capture_output=True, text=True, timeout=5,
cwd=repo_root, capture_output=True, timeout=5,
)
if r.returncode == 0:
full_shas.append(r.stdout.strip())
full_shas.append(r.stdout.decode("utf-8", errors="replace").strip())
_append_reviewed_shas(repo_root, full_shas, vulns_found=len(vulns or []))
except Exception:
pass
@@ -1361,18 +1470,26 @@ def handle_commit_review_posttooluse(input_data):
if s in sev:
sev[s] += 1
# Rebuild guidance from new_vulns only — concrete_guidance from the LLM
# still lists deduped entries. Pass via additional_context so CC surfaces
# the reason via hookSpecificOutput.additionalContext instead of empty
# stdout (#1783) / stderr-only "json output validation failed" (#1375).
_commit_guidance = (PROVENANCE_BANNER + "\n\n"
+ _format_vulns_guidance(new_vulns)
+ CONTINUATION_SUFFIX + "\n")
emit_metrics({
"vulns_found": len(new_vulns), **_base, **_agentic_m,
"critical_count": sev["critical"], "high_count": sev["high"],
"files_reviewed": len(diff_files), "review_ms": review_ms,
**({"deduped": n_deduped} if n_deduped else {}),
}, rewake_summary=_format_vulns_summary(new_vulns, prefix="Commit security review found"))
}, rewake_summary=_format_vulns_summary(new_vulns, prefix="Commit security review found"),
additional_context=_commit_guidance,
hook_event_name="PostToolUse")
# Rebuild guidance from new_vulns only — concrete_guidance from the LLM
# still lists deduped entries.
sys.stderr.write(PROVENANCE_BANNER + "\n\n"
+ _format_vulns_guidance(new_vulns)
+ CONTINUATION_SUFFIX + "\n")
# exit(2) is preserved per the asyncRewake protocol — it's what CC
# uses as the "force fix" signal that triggers the rewakeMessage flow.
# The stderr.write was removed; additional_context above now carries
# the same text via the modern JSON channel. See #1358/#1375/#1783.
sys.exit(2)
def handle_push_sweep_posttooluse(input_data):
@@ -1453,9 +1570,10 @@ def handle_push_sweep_posttooluse(input_data):
# both.
head = None
try:
# See #2099: drop text=True; decode manually for cp1252 safety.
r = subprocess.run([*GIT_CMD, "rev-parse", "HEAD"], cwd=repo_root,
capture_output=True, text=True, timeout=5)
head = r.stdout.strip() if r.returncode == 0 else None
capture_output=True, timeout=5)
head = r.stdout.decode("utf-8", errors="replace").strip() if r.returncode == 0 else None
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
pass
push_section = _push_section(bash_output or "")
@@ -1485,14 +1603,15 @@ def handle_push_sweep_posttooluse(input_data):
quiet_success = False
if not (bash_output or "").strip() and not interrupted:
try:
# See #2099: drop text=True; decode manually for cp1252 safety.
r_cur = subprocess.run(
[*GIT_CMD, "rev-parse", "--verify", "-q", "@{u}"],
cwd=repo_root, capture_output=True, text=True, timeout=5)
cwd=repo_root, capture_output=True, timeout=5)
r_prev = subprocess.run(
[*GIT_CMD, "rev-parse", "--verify", "-q", "@{u}@{1}"],
cwd=repo_root, capture_output=True, text=True, timeout=5)
cur = r_cur.stdout.strip() if r_cur.returncode == 0 else ""
prev_u = r_prev.stdout.strip() if r_prev.returncode == 0 else ""
cwd=repo_root, capture_output=True, timeout=5)
cur = r_cur.stdout.decode("utf-8", errors="replace").strip() if r_cur.returncode == 0 else ""
prev_u = r_prev.stdout.decode("utf-8", errors="replace").strip() if r_prev.returncode == 0 else ""
quiet_success = bool(cur and prev_u and cur == head and prev_u != cur)
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
pass
@@ -1506,11 +1625,12 @@ def handle_push_sweep_posttooluse(input_data):
# reviewed-shas state.
for local_ref in new_branch_matches:
try:
# See #2099: drop text=True; decode manually for cp1252 safety.
r = subprocess.run(
[*GIT_CMD, "rev-parse", "--verify", "-q", local_ref],
cwd=repo_root, capture_output=True, text=True, timeout=5,
cwd=repo_root, capture_output=True, timeout=5,
)
local_sha = r.stdout.strip() if r.returncode == 0 else ""
local_sha = r.stdout.decode("utf-8", errors="replace").strip() if r.returncode == 0 else ""
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
local_sha = ""
if local_sha and local_sha != head:
@@ -1629,17 +1749,23 @@ def handle_push_sweep_posttooluse(input_data):
# Metrics — keep within the 10-key cap; agentic sub-metrics are dropped
# here in favour of the push-sweep funnel keys (telemetry can join on session_id
# to the per-commit fires for agentic detail). rewake_summary must ride
# this line (CC reads only the first {-prefixed stdout line); it's a
# no-op when new_vulns is empty since we exit 0 below.
emit_metrics({
# this line (CC reads only the first {-prefixed stdout line); the emit
# is deferred to the two exit points below so the with-vulns path can
# also pass additional_context in the same JSON line (#1375/#1783) —
# the by-design "CC keeps only the first JSON line" constraint means
# we can't emit twice. Builds the shared metrics dict here; vulns path
# adds additional_context, no-vulns path emits as-is.
_push_metrics = {
**_base, "pushed": len(push_range), "unreviewed": len(tail),
"prefix_advanced": prefix_advanced, "vulns_found": len(new_vulns),
"files_reviewed": len(diff_files), "review_ms": review_ms,
**({"deduped": n_deduped} if n_deduped else {}),
}, rewake_summary=_format_vulns_summary(new_vulns, prefix="Push security review found"))
}
_push_rewake_summary = _format_vulns_summary(new_vulns, prefix="Push security review found")
if not new_vulns:
debug_log("Push sweep: no new findings")
emit_metrics(_push_metrics, rewake_summary=_push_rewake_summary)
sys.exit(0)
# First-push of a big branch can surface many findings at once across
@@ -1692,9 +1818,14 @@ def handle_push_sweep_posttooluse(input_data):
guidance = _format_vulns_guidance(reported) or ""
else:
guidance = concrete_guidance or _format_vulns_guidance(reported) or ""
sys.stderr.write(
PROVENANCE_BANNER + "\n\n" + guidance + CONTINUATION_SUFFIX + "\n"
)
# Emit metrics + additional_context together — single JSON line is the
# contract CC's hook parser expects. exit(2) preserved as the asyncRewake
# "force fix" trigger (see comment near handle_commit_review_posttooluse).
# See #1358 / #1375 / #1783.
emit_metrics(_push_metrics, rewake_summary=_push_rewake_summary,
additional_context=(PROVENANCE_BANNER + "\n\n"
+ guidance + CONTINUATION_SUFFIX + "\n"),
hook_event_name="PostToolUse")
sys.exit(2)
def handle_stop_hook(input_data):
@@ -1927,6 +2058,11 @@ def handle_stop_hook(input_data):
# untracked_baseline_n is the signal for whether the UPS-time
# untracked-snapshot capture actually ran.
sweep_trimmed = {k: v for k, v in sweep.items() if k != "warn_unresolved_mask"}
# Pass guidance via additional_context so CC surfaces the findings via
# hookSpecificOutput.additionalContext instead of stderr-only (which
# was the cause of "json output validation failed" / empty-reason UI in
# #1375 / #1783). exit(2) preserved as the asyncRewake "force fix"
# signal — that's the documented mechanism. See #1358 / #1375 / #1783.
emit_metrics({
"vulns_found": len(vulns),
"untracked_baseline_n": len(untracked_at_baseline),
@@ -1940,10 +2076,10 @@ def handle_stop_hook(input_data):
**({"diff_truncated": llm._last_review_truncated_bytes}
if llm._last_review_truncated_bytes else {}),
**sweep_trimmed,
}, rewake_summary=_format_vulns_summary(vulns))
# Exit code 2 with stderr forces Claude to continue and fix
sys.stderr.write(PROVENANCE_BANNER + "\n\n" + concrete_guidance + CONTINUATION_SUFFIX + "\n")
}, rewake_summary=_format_vulns_summary(vulns),
additional_context=(PROVENANCE_BANNER + "\n\n"
+ concrete_guidance + CONTINUATION_SUFFIX + "\n"),
hook_event_name="Stop")
sys.exit(2)
if llm._last_call_claude_http_error is not None:
@@ -1971,10 +2107,7 @@ def handle_stop_hook(input_data):
})
sys.exit(0)
_SDK_BOOTSTRAP_THROTTLE = os.path.join(
os.environ.get("SECURITY_WARNINGS_STATE_DIR")
or os.path.expanduser("~/.claude/security"),
".sdk_bootstrap_spawned")
_SDK_BOOTSTRAP_THROTTLE = os.path.join(_resolve_state_dir(), ".sdk_bootstrap_spawned")
def _maybe_bootstrap_agent_sdk_async():
"""Fire-and-forget SDK bootstrap, for remote-pod environments.

View File

@@ -19,7 +19,7 @@ import os
import re
from datetime import datetime
from _base import debug_log
from _base import debug_log, state_dir as _state_dir
def _state_key(session_id):
@@ -36,20 +36,20 @@ def _state_key(session_id):
def get_state_file(session_id):
"""Get session-specific state file path."""
state_dir = os.environ.get("SECURITY_WARNINGS_STATE_DIR", os.path.expanduser("~/.claude/security"))
state_dir = _state_dir()
return os.path.join(state_dir, f"security_warnings_state_{_state_key(session_id)}.json")
def get_lock_file(session_id):
"""Get session-specific lock file path."""
state_dir = os.environ.get("SECURITY_WARNINGS_STATE_DIR", os.path.expanduser("~/.claude/security"))
state_dir = _state_dir()
return os.path.join(state_dir, f"security_warnings_state_{_state_key(session_id)}.lock")
def cleanup_old_state_files():
"""Remove state files and lock files older than 30 days."""
try:
state_dir = os.environ.get("SECURITY_WARNINGS_STATE_DIR", os.path.expanduser("~/.claude/security"))
state_dir = _state_dir()
if not os.path.exists(state_dir):
return

View File

@@ -22,6 +22,17 @@
# "${CLAUDE_PLUGIN_ROOT}/hooks/security_reminder_hook.py"
set -e
# Force UTF-8 for ALL Python filesystem + IO operations (PEP 540).
# Without this, Windows Python defaults `locale.getpreferredencoding()` to
# cp1252 — which makes `text=True` in subprocess.run / open() / json.load
# crash the internal reader thread on any byte that's undefined in cp1252
# (e.g. the 0x81 byte from ف, present in any path/filename with
# Arabic/Hebrew/CJK characters). See #2056, #2099.
#
# No-op on macOS/Linux (already UTF-8). Must be set BEFORE Python starts —
# changing it from inside the interpreter has no effect.
export PYTHONUTF8=1
# Git Bash / MSYS on Windows hands script paths to this shim in POSIX form
# (`/c/Users/...`). When we exec a Windows `python.exe` (which we do on
# Windows since `python3` is the Microsoft Store stub), python interprets the