mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-06-10 02:03:34 +00:00
Compare commits
18 Commits
fix-2082-d
...
bump/remem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ff99e42f5 | ||
|
|
0d82eac145 | ||
|
|
3d368d2972 | ||
|
|
84011d43b1 | ||
|
|
2a822c0787 | ||
|
|
a40c9f1e83 | ||
|
|
c7a3e2ffa0 | ||
|
|
1ecf3d1bac | ||
|
|
c40770ae5a | ||
|
|
7a0a7f486e | ||
|
|
42487ee6fd | ||
|
|
bc07f7a1fd | ||
|
|
9e150cfd48 | ||
|
|
8435428dfc | ||
|
|
0d22ba3501 | ||
|
|
37ffc76005 | ||
|
|
982070e51f | ||
|
|
5212308979 |
@@ -19,7 +19,7 @@
|
||||
"url": "https://github.com/42Crunch-AI/claude-plugins.git",
|
||||
"path": "plugins/api-security-testing",
|
||||
"ref": "v1.5.5",
|
||||
"sha": "5c8074d846b852c21da23bbf6effbfdabb18ba2d"
|
||||
"sha": "b404d99a3f0bc1f3e74a1638671e2e3319187e2c"
|
||||
},
|
||||
"homepage": "https://42crunch.com"
|
||||
},
|
||||
@@ -35,7 +35,7 @@
|
||||
"url": "https://github.com/adobe/skills.git",
|
||||
"path": "plugins/creative-cloud/adobe-for-creativity",
|
||||
"ref": "main",
|
||||
"sha": "ecd1e2b2c493ba0627774f36a897bd44d47fef1d"
|
||||
"sha": "0a015c06894332091b79e055e0404fbc1a18c9fe"
|
||||
},
|
||||
"homepage": "https://github.com/adobe/skills/tree/main/plugins/creative-cloud/adobe-for-creativity"
|
||||
},
|
||||
@@ -57,7 +57,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/SalesforceAIResearch/agentforce-adlc.git",
|
||||
"sha": "5ddccc36737b8bdc3dcabb3d6f51daa350c3d16d"
|
||||
"sha": "1584dd52f388482db78949456addfa29a4c9d9c3"
|
||||
},
|
||||
"homepage": "https://github.com/SalesforceAIResearch/agentforce-adlc"
|
||||
},
|
||||
@@ -120,7 +120,7 @@
|
||||
"url": "https://github.com/awslabs/agent-plugins.git",
|
||||
"path": "plugins/amazon-location-service",
|
||||
"ref": "main",
|
||||
"sha": "5d982e8a5f1e0b06545adac69ff0348141587725"
|
||||
"sha": "9d46cc0a092c0a8c01a5bd06a4349985cc6c8f08"
|
||||
},
|
||||
"homepage": "https://github.com/awslabs/agent-plugins"
|
||||
},
|
||||
@@ -193,7 +193,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/astronomer/agents.git",
|
||||
"sha": "535a040ca9e27aaed6da13f0f959625fb3294820"
|
||||
"sha": "7ce4a12d3cabb506294134c91a1b876d4b166a70"
|
||||
},
|
||||
"homepage": "https://github.com/astronomer/agents"
|
||||
},
|
||||
@@ -203,7 +203,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/atlanhq/agent-toolkit.git",
|
||||
"sha": "790398c87378f128bdc74c31bb7ecfb8e4695f29"
|
||||
"sha": "b0efcc8e6adc64d052b634ac1103932390413fd9"
|
||||
},
|
||||
"homepage": "https://docs.atlan.com/"
|
||||
},
|
||||
@@ -226,7 +226,7 @@
|
||||
"source": "url",
|
||||
"url": "https://github.com/BrainBlend-AI/atomic-agents.git",
|
||||
"path": "claude-plugin/atomic-agents",
|
||||
"sha": "c4e905c49884747be65e7ed42ccfb118c67f57ac"
|
||||
"sha": "bb9708ec7c4c7145bd64033dbece0bfaed0c2ad5"
|
||||
},
|
||||
"homepage": "https://github.com/BrainBlend-AI/atomic-agents",
|
||||
"tags": [
|
||||
@@ -245,7 +245,7 @@
|
||||
"url": "https://github.com/auth0/agent-skills.git",
|
||||
"path": "plugins/auth0",
|
||||
"ref": "main",
|
||||
"sha": "c771dc1c77bfd5a67686afb464ccebd227c02b0f"
|
||||
"sha": "c38453f6a99bbfeaf73b5be81db987ec6af982da"
|
||||
},
|
||||
"homepage": "https://auth0.com/docs/quickstart/agent-skills"
|
||||
},
|
||||
@@ -274,7 +274,7 @@
|
||||
"url": "https://github.com/awslabs/agent-plugins.git",
|
||||
"path": "plugins/aws-amplify",
|
||||
"ref": "main",
|
||||
"sha": "5d982e8a5f1e0b06545adac69ff0348141587725"
|
||||
"sha": "9d46cc0a092c0a8c01a5bd06a4349985cc6c8f08"
|
||||
},
|
||||
"homepage": "https://github.com/awslabs/agent-plugins"
|
||||
},
|
||||
@@ -335,7 +335,7 @@
|
||||
"url": "https://github.com/awslabs/agent-plugins.git",
|
||||
"path": "plugins/aws-serverless",
|
||||
"ref": "main",
|
||||
"sha": "5d982e8a5f1e0b06545adac69ff0348141587725"
|
||||
"sha": "9d46cc0a092c0a8c01a5bd06a4349985cc6c8f08"
|
||||
},
|
||||
"homepage": "https://github.com/awslabs/agent-plugins"
|
||||
},
|
||||
@@ -346,7 +346,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/microsoft/azure-skills.git",
|
||||
"sha": "d02fd24f151f5133650eaa78e7da3cac2cedd72f"
|
||||
"sha": "7cb89c221ecc9eccb71580aaff3695408cdeef2b"
|
||||
},
|
||||
"homepage": "https://github.com/microsoft/azure-skills"
|
||||
},
|
||||
@@ -412,7 +412,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/brightdata/skills.git",
|
||||
"sha": "071e9d4db77c8561e333799f25ea85f11f7b667d"
|
||||
"sha": "da73549126e5834a9230ee5532d4917d43aedf11"
|
||||
},
|
||||
"homepage": "https://docs.brightdata.com"
|
||||
},
|
||||
@@ -442,7 +442,7 @@
|
||||
"url": "https://github.com/carta/plugins.git",
|
||||
"path": "plugins/carta-cap-table",
|
||||
"ref": "main",
|
||||
"sha": "5e6c9d1cfa3bff9b91138e7906c6eb088fd9a66a"
|
||||
"sha": "e66d331cd8e669ee121c96ee35b0c91acd828970"
|
||||
},
|
||||
"homepage": "https://carta.com"
|
||||
},
|
||||
@@ -458,7 +458,7 @@
|
||||
"url": "https://github.com/carta/plugins.git",
|
||||
"path": "plugins/carta-crm",
|
||||
"ref": "main",
|
||||
"sha": "5e6c9d1cfa3bff9b91138e7906c6eb088fd9a66a"
|
||||
"sha": "e66d331cd8e669ee121c96ee35b0c91acd828970"
|
||||
},
|
||||
"homepage": "https://carta.com"
|
||||
},
|
||||
@@ -474,7 +474,7 @@
|
||||
"url": "https://github.com/carta/plugins.git",
|
||||
"path": "plugins/carta-investors",
|
||||
"ref": "main",
|
||||
"sha": "5e6c9d1cfa3bff9b91138e7906c6eb088fd9a66a"
|
||||
"sha": "e66d331cd8e669ee121c96ee35b0c91acd828970"
|
||||
},
|
||||
"homepage": "https://carta.com"
|
||||
},
|
||||
@@ -501,7 +501,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/ChromeDevTools/chrome-devtools-mcp.git",
|
||||
"sha": "60be3e6bc157bd1121ea1d4b6ad59e37a73cac3e"
|
||||
"sha": "2e039c09e1a273581d9b51081a0feb8a57791947"
|
||||
},
|
||||
"homepage": "https://github.com/ChromeDevTools/chrome-devtools-mcp"
|
||||
},
|
||||
@@ -716,7 +716,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/CodSpeedHQ/codspeed.git",
|
||||
"sha": "ecf3c2ebf959479126d631ad39d317738d559388"
|
||||
"sha": "407dd3c930b8dc5e5655a2d91a65d88f01829955"
|
||||
},
|
||||
"homepage": "https://codspeed.io"
|
||||
},
|
||||
@@ -753,7 +753,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/get-convex/convex-backend-skill.git",
|
||||
"sha": "5e59870cda2a5892e18a7164d1a46fcf57b70bea"
|
||||
"sha": "ece93250d560f0ce32a24223dea92b33050b2a66"
|
||||
},
|
||||
"homepage": "https://github.com/get-convex/convex-backend-skill",
|
||||
"keywords": [
|
||||
@@ -784,7 +784,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/CrowdStrike/foundry-skills.git",
|
||||
"sha": "99edea095f4e32ed008706b55257d0893fb93387"
|
||||
"sha": "fb25d60ecdbc0129071802dad210a65168ca55a9"
|
||||
},
|
||||
"homepage": "https://github.com/CrowdStrike/foundry-skills"
|
||||
},
|
||||
@@ -830,7 +830,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/dash0hq/dash0-agent-plugin.git",
|
||||
"sha": "2909be7ebc2804af464e0d7f660ccc2b62d94623"
|
||||
"sha": "d1ad56f86f2a9ae74eccf1df2bb2985c963005b1"
|
||||
},
|
||||
"homepage": "https://dash0.com/"
|
||||
},
|
||||
@@ -841,7 +841,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/astronomer/agents.git",
|
||||
"sha": "535a040ca9e27aaed6da13f0f959625fb3294820"
|
||||
"sha": "7ce4a12d3cabb506294134c91a1b876d4b166a70"
|
||||
},
|
||||
"homepage": "https://github.com/astronomer/agents"
|
||||
},
|
||||
@@ -855,7 +855,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/gemini-cli-extensions/data-agent-kit-starter-pack.git",
|
||||
"sha": "7bc75b5e53d6eaae103132fd1a47de26239e4ae4"
|
||||
"sha": "86eb482b33d943aa4242ae6f06d627ec12064d46"
|
||||
},
|
||||
"homepage": "https://github.com/gemini-cli-extensions/data-agent-kit-starter-pack"
|
||||
},
|
||||
@@ -865,7 +865,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/astronomer/agents.git",
|
||||
"sha": "535a040ca9e27aaed6da13f0f959625fb3294820"
|
||||
"sha": "7ce4a12d3cabb506294134c91a1b876d4b166a70"
|
||||
},
|
||||
"homepage": "https://github.com/astronomer/agents"
|
||||
},
|
||||
@@ -878,7 +878,7 @@
|
||||
"url": "https://github.com/awslabs/agent-plugins.git",
|
||||
"path": "plugins/databases-on-aws",
|
||||
"ref": "main",
|
||||
"sha": "5d982e8a5f1e0b06545adac69ff0348141587725"
|
||||
"sha": "9d46cc0a092c0a8c01a5bd06a4349985cc6c8f08"
|
||||
},
|
||||
"homepage": "https://github.com/awslabs/agent-plugins"
|
||||
},
|
||||
@@ -920,7 +920,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/datarobot-oss/datarobot-agent-skills.git",
|
||||
"sha": "8124faae2154117382b1046aa74d8901a3ffe930"
|
||||
"sha": "4c3dfbd259bc2c6c815f7575d27ca26bc09d0d17"
|
||||
},
|
||||
"homepage": "https://datarobot.com"
|
||||
},
|
||||
@@ -946,7 +946,7 @@
|
||||
"url": "https://github.com/awslabs/agent-plugins.git",
|
||||
"path": "plugins/deploy-on-aws",
|
||||
"ref": "main",
|
||||
"sha": "5d982e8a5f1e0b06545adac69ff0348141587725"
|
||||
"sha": "9d46cc0a092c0a8c01a5bd06a4349985cc6c8f08"
|
||||
},
|
||||
"homepage": "https://github.com/awslabs/agent-plugins"
|
||||
},
|
||||
@@ -1048,7 +1048,7 @@
|
||||
"url": "https://github.com/expo/skills.git",
|
||||
"path": "plugins/expo",
|
||||
"ref": "main",
|
||||
"sha": "510373b50956ef4dc84c20bb4c9cce70b618aa06"
|
||||
"sha": "fdd3df12151a208853fe540ffea9a67773446377"
|
||||
},
|
||||
"homepage": "https://github.com/expo/skills/blob/main/plugins/expo/README.md"
|
||||
},
|
||||
@@ -1114,7 +1114,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/firecrawl/firecrawl-claude-plugin.git",
|
||||
"sha": "01d11b30ace699a27f9ea7decf6ce6c9857f71ff"
|
||||
"sha": "e71cec486062680f0c8f8823afcb3558ad81ce60"
|
||||
},
|
||||
"homepage": "https://github.com/firecrawl/firecrawl-claude-plugin.git"
|
||||
},
|
||||
@@ -1217,7 +1217,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/huggingface/skills.git",
|
||||
"sha": "7a493b09c81aae09a41bd2e1fa33dfc0f68acd75"
|
||||
"sha": "df627be1837523c91ac6df472e3dc543d3107bd9"
|
||||
},
|
||||
"homepage": "https://github.com/huggingface/skills.git"
|
||||
},
|
||||
@@ -1231,7 +1231,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/hunter-io/claude-plugin.git",
|
||||
"sha": "c67942395cde155e9ad4ed8e3a137926f9992fb8"
|
||||
"sha": "9b6146520c48f9dcc6092f106e5c1a5762ca3e7a"
|
||||
},
|
||||
"homepage": "https://hunter.io"
|
||||
},
|
||||
@@ -1245,7 +1245,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/heygen-com/hyperframes.git",
|
||||
"sha": "7ea4d1c1314bd60d5273efa92626bd1d0f9c621d"
|
||||
"sha": "bc3701f5905c5ba7c8cf03c3bbe3a49162d2b1f1"
|
||||
},
|
||||
"homepage": "https://hyperframes.heygen.com"
|
||||
},
|
||||
@@ -1410,7 +1410,7 @@
|
||||
"url": "https://github.com/pydantic/skills.git",
|
||||
"path": "plugins/logfire",
|
||||
"ref": "main",
|
||||
"sha": "0c38c5bb5679f6cc41956bbbf811396a0d108ac9"
|
||||
"sha": "eb17c0da94de81488825c0198475233dc1f06393"
|
||||
},
|
||||
"homepage": "https://github.com/pydantic/skills/tree/main/plugins/logfire"
|
||||
},
|
||||
@@ -1523,7 +1523,7 @@
|
||||
"url": "https://github.com/mercadopago/mercadopago-claude-marketplace.git",
|
||||
"path": "plugins/mercadopago",
|
||||
"ref": "main",
|
||||
"sha": "f52c138924d8035b39e8fe02d41c6712fc41ceb4"
|
||||
"sha": "ba967158392bec9f0c199cd39196af64222f0ab0"
|
||||
},
|
||||
"homepage": "https://github.com/mercadopago/mercadopago-claude-marketplace/tree/main/plugins/mercadopago"
|
||||
},
|
||||
@@ -1638,7 +1638,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/Nimbleway/agent-skills.git",
|
||||
"sha": "95ed06468957ddc9de609b25c390b30c3864eac8"
|
||||
"sha": "9736dfc757f5ed4f05da0480b202af09e93a27de"
|
||||
},
|
||||
"homepage": "https://docs.nimbleway.com/integrations/agent-skills/plugin-installation"
|
||||
},
|
||||
@@ -1665,7 +1665,7 @@
|
||||
"url": "https://github.com/oracle-samples/oracle-aidp-samples.git",
|
||||
"path": "ai/claude-code-plugins/oracle-ai-data-platform-workbench-spark-connectors",
|
||||
"ref": "main",
|
||||
"sha": "f7ea9cae6fce69a4e3798dfc1d5216ac1d0dd7e8"
|
||||
"sha": "6e59f24cd3e8870649e7f9b2e3e106502b43fd5f"
|
||||
},
|
||||
"homepage": "https://docs.oracle.com/en/cloud/paas/ai-data-platform/index.html"
|
||||
},
|
||||
@@ -1681,7 +1681,7 @@
|
||||
"url": "https://github.com/growthxai/output.git",
|
||||
"path": "coding_assistants/claude/plugins/outputai",
|
||||
"ref": "main",
|
||||
"sha": "93dd22ee568a97911a332b5aa0d9cebb2b6f7da1"
|
||||
"sha": "0eeffece25b6f471c48b705a214471164b8c5946"
|
||||
},
|
||||
"homepage": "https://output.ai"
|
||||
},
|
||||
@@ -1846,7 +1846,7 @@
|
||||
"url": "https://github.com/pydantic/skills.git",
|
||||
"path": "plugins/ai",
|
||||
"ref": "main",
|
||||
"sha": "0c38c5bb5679f6cc41956bbbf811396a0d108ac9"
|
||||
"sha": "eb17c0da94de81488825c0198475233dc1f06393"
|
||||
},
|
||||
"homepage": "https://github.com/pydantic/skills/tree/main/plugins/ai"
|
||||
},
|
||||
@@ -1884,7 +1884,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/qdrant/skills.git",
|
||||
"sha": "1390c811e03922b822dc9e12b832ba4dc82e0bf0"
|
||||
"sha": "ea62a9857dabcc169597549da7681bd6d4cd13e9"
|
||||
},
|
||||
"homepage": "https://skills.qdrant.tech"
|
||||
},
|
||||
@@ -1895,7 +1895,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/qodo-ai/qodo-skills.git",
|
||||
"sha": "b1eb0389480ee6de8df874f40a230ed2625ef0d3"
|
||||
"sha": "8aec13d6ac60feb9d9f84f36aa1753234de17dc8"
|
||||
},
|
||||
"homepage": "https://github.com/qodo-ai/qodo-skills.git"
|
||||
},
|
||||
@@ -1909,7 +1909,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/TheQtCompanyRnD/agent-skills.git",
|
||||
"sha": "23772fa2264b3ff1037a96164b2c28d2b29a4c2f"
|
||||
"sha": "a7189a7bc17e616b725e7ce4e46a4f5ebd50d94f"
|
||||
},
|
||||
"homepage": "https://www.qt.io/"
|
||||
},
|
||||
@@ -1923,7 +1923,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/quarkusio/quarkus-agent-mcp.git",
|
||||
"sha": "77fd36284a80b3ed1bde3d2fe48a0b2f99e4941e"
|
||||
"sha": "32cad78bd9040efe31794cfc10f70caf2a724dd9"
|
||||
},
|
||||
"homepage": "https://quarkus.io"
|
||||
},
|
||||
@@ -1975,7 +1975,7 @@
|
||||
"url": "https://github.com/redis/agent-skills.git",
|
||||
"path": "plugins/redis-development",
|
||||
"ref": "main",
|
||||
"sha": "18da4e42371f7eee0dcfafd8461effd41de351e9"
|
||||
"sha": "5ca2e1a2d82a768221e8f71a02e3ca095a37d38e"
|
||||
},
|
||||
"homepage": "https://redis.io"
|
||||
},
|
||||
@@ -1985,7 +1985,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/Digital-Process-Tools/claude-remember.git",
|
||||
"sha": "c9b34417a8132f0416411a0ca51d009a256a3acc"
|
||||
"sha": "a4ff96f38622f7c4920dc349d59cc980663336f4"
|
||||
},
|
||||
"homepage": "https://github.com/Digital-Process-Tools/claude-remember"
|
||||
},
|
||||
@@ -1999,7 +1999,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/resend/resend-skills.git",
|
||||
"sha": "78469829399beec62b8f815f109ebfcfa3b0680b"
|
||||
"sha": "376d1c3fb37cc7d22ab21cce836f4d6f323922de"
|
||||
},
|
||||
"homepage": "https://resend.com"
|
||||
},
|
||||
@@ -2097,7 +2097,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/sanity-io/agent-toolkit.git",
|
||||
"sha": "236348e29b31e834ce71e4e2e3072184dd1c1e27"
|
||||
"sha": "d7545f5cc6f8fb39554083b52ad074a6d912db9f"
|
||||
},
|
||||
"homepage": "https://www.sanity.io"
|
||||
},
|
||||
@@ -2131,7 +2131,7 @@
|
||||
"url": "https://github.com/SAP/open-ux-tools.git",
|
||||
"path": "packages/fiori-mcp-server",
|
||||
"ref": "main",
|
||||
"sha": "d2a6fce818f3c046c5bbb041507be4632f926602"
|
||||
"sha": "7432d23a7b5c3bd1c0a01cf76696bf0c417ecd1f"
|
||||
},
|
||||
"homepage": "https://github.com/SAP/open-ux-tools/tree/main/packages/fiori-mcp-server"
|
||||
},
|
||||
@@ -2198,7 +2198,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/getsentry/sentry-for-claude.git",
|
||||
"sha": "ed0875684192bb8a050297a896657ff9db1ffdf5"
|
||||
"sha": "d6123be331e2224b037e1ffefd27c806e7566dcf"
|
||||
},
|
||||
"homepage": "https://github.com/getsentry/sentry-for-claude/tree/main"
|
||||
},
|
||||
@@ -2214,7 +2214,7 @@
|
||||
"url": "https://github.com/getsentry/cli.git",
|
||||
"path": "plugins/sentry-cli",
|
||||
"ref": "main",
|
||||
"sha": "d9bcd70eaa467fb3ddf591bfbfb0686fd1e9c016"
|
||||
"sha": "db90767935558db16c45036f89e68edaa1dde106"
|
||||
},
|
||||
"homepage": "https://sentry.io"
|
||||
},
|
||||
@@ -2279,7 +2279,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/Shopify/Shopify-AI-Toolkit.git",
|
||||
"sha": "c164cf45c4bc1d17bbc105168d99a4f744cfaac2"
|
||||
"sha": "859be93bfc858f183ff5eb40183e35a4d91d2950"
|
||||
},
|
||||
"homepage": "https://shopify.dev"
|
||||
},
|
||||
@@ -2364,7 +2364,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/spotify/ads-claude-plugin.git",
|
||||
"sha": "7ed948b85337f6b31a82dfaa8f033b6843659fa3"
|
||||
"sha": "73b8bd490e02d3ed0bb4c8e228a470c46f995154"
|
||||
},
|
||||
"homepage": "https://github.com/spotify/ads-claude-plugin"
|
||||
},
|
||||
@@ -2377,7 +2377,7 @@
|
||||
"url": "https://github.com/stripe/ai.git",
|
||||
"path": "providers/claude/plugin",
|
||||
"ref": "main",
|
||||
"sha": "a34795211da530a168f581122011bb5ceb2e4bd0"
|
||||
"sha": "99425a010474c6aab745a975d06764e323c2c4d4"
|
||||
},
|
||||
"homepage": "https://github.com/stripe/ai/tree/main/providers/claude/plugin"
|
||||
},
|
||||
@@ -2400,7 +2400,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/supabase-community/supabase-plugin.git",
|
||||
"sha": "1b910c021aee8c9c054196f0e840b3a65e1a7c63"
|
||||
"sha": "3217ac038647f6901a166f3264a32f01833f73ba"
|
||||
},
|
||||
"homepage": "https://github.com/supabase-community/supabase-plugin"
|
||||
},
|
||||
@@ -2445,7 +2445,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/JetBrains/teamcity-cli.git",
|
||||
"sha": "7f8419738b452108ff181365be30c1fab0a6905e"
|
||||
"sha": "9436b94b228579ba952aba809357776c3db9ce1a"
|
||||
},
|
||||
"homepage": "https://www.jetbrains.com/teamcity/"
|
||||
},
|
||||
@@ -2538,7 +2538,7 @@
|
||||
"url": "https://github.com/UI5/plugins-coding-agents.git",
|
||||
"path": "plugins/ui5",
|
||||
"ref": "main",
|
||||
"sha": "78f657e6a5004b5cdd1b998aabea616023eeabbb"
|
||||
"sha": "7acd8328399a221e161ae5bb04a5675696f92920"
|
||||
},
|
||||
"homepage": "https://github.com/UI5/plugins-coding-agents"
|
||||
},
|
||||
@@ -2556,7 +2556,7 @@
|
||||
"url": "https://github.com/UI5/plugins-coding-agents.git",
|
||||
"path": "plugins/ui5-typescript-conversion",
|
||||
"ref": "main",
|
||||
"sha": "78f657e6a5004b5cdd1b998aabea616023eeabbb"
|
||||
"sha": "7acd8328399a221e161ae5bb04a5675696f92920"
|
||||
},
|
||||
"homepage": "https://github.com/UI5/plugins-coding-agents"
|
||||
},
|
||||
@@ -2595,7 +2595,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/explorium-ai/vibeprospecting-plugin.git",
|
||||
"sha": "ada4d569dbf70194fe18750ecbc5170e9a3f120a"
|
||||
"sha": "c00b11db4efc3e7b7aaffc10d71db33c806d5607"
|
||||
},
|
||||
"homepage": "https://www.vibeprospecting.ai/product/claude-plugin"
|
||||
},
|
||||
@@ -2620,7 +2620,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/wix/skills.git",
|
||||
"sha": "5da7e749a466ef9ddcdb2822099b940b9a1bc151"
|
||||
"sha": "c5b343f2dadba06da91ee6de07272161fb68d40d"
|
||||
},
|
||||
"homepage": "https://dev.wix.com/docs/wix-cli/guides/development/about-wix-skills"
|
||||
},
|
||||
@@ -2727,7 +2727,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/zscaler/zscaler-mcp-server.git",
|
||||
"sha": "8409e1661b7f7171bfbb9297e1ecfc61c28b6d92"
|
||||
"sha": "be37fb604a07dc9c5a4c3e009312c4f11acaa6d3"
|
||||
},
|
||||
"homepage": "https://github.com/zscaler/zscaler-mcp-server"
|
||||
}
|
||||
|
||||
81
.github/workflows/bump-plugin-shas.yml
vendored
81
.github/workflows/bump-plugin-shas.yml
vendored
@@ -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
|
||||
|
||||
8
.github/workflows/validate-plugins.yml
vendored
8
.github/workflows/validate-plugins.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -355,9 +355,9 @@ def _list_untracked(cwd):
|
||||
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"],
|
||||
[*GIT_CMD, "ls-files", "--others", "--exclude-standard", "-z"],
|
||||
cwd=repo, capture_output=True, timeout=15,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
|
||||
@@ -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
|
||||
@@ -90,10 +96,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":
|
||||
@@ -239,10 +242,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
|
||||
|
||||
@@ -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,15 +233,12 @@ def _git_diff_range(repo_root, base, head="HEAD"):
|
||||
them reviewed — otherwise unreviewed commits get permanently silenced.
|
||||
"""
|
||||
try:
|
||||
# core.quotePath=false makes git emit raw UTF-8 in `diff --git a/... b/...`
|
||||
# headers instead of C-quoting non-ASCII path bytes (`"a/\303\201vila/..."`
|
||||
# vs `a/Ávila/...`). The downstream `re.match(r'^a/(.+?) b/(.+)$', ...)`
|
||||
# in parse_diff_into_files / extract_file_paths_from_diff matches the
|
||||
# raw form only — quoted headers slip past and the entire file is
|
||||
# silently dropped from review. See #2082 (sibling of #2056 / #2075).
|
||||
# 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, "-c", "core.quotePath=false",
|
||||
"diff", "-p", "--no-color", "--no-ext-diff", base, head],
|
||||
[*GIT_CMD, "diff", "-p", "--no-color", "--no-ext-diff", base, head],
|
||||
cwd=repo_root, capture_output=True, timeout=30,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
@@ -220,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
|
||||
@@ -330,8 +363,9 @@ def _git_name_only(cwd, base, include_untracked=False):
|
||||
# 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],
|
||||
[*GIT_CMD, "diff", "--name-only", "-z", base],
|
||||
cwd=cwd, capture_output=True, timeout=30,
|
||||
env=env,
|
||||
)
|
||||
@@ -368,9 +402,9 @@ def _git_status_porcelain(cwd):
|
||||
# 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"],
|
||||
[*GIT_CMD, "status", "--porcelain=v1", "-uall", "-z"],
|
||||
cwd=cwd, capture_output=True, timeout=30,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
@@ -410,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):
|
||||
@@ -443,11 +480,8 @@ def get_git_diff(cwd, baseline_sha, full_context=False, paths=None, untracked_pa
|
||||
# change exists to fix.
|
||||
return ""
|
||||
|
||||
# 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. See #2082 (sibling of #2056 / #2075).
|
||||
cmd = [*GIT_CMD, "-c", "core.quotePath=false",
|
||||
"diff", "--no-color", "--no-ext-diff", baseline_sha] + (["--unified=99999"] if full_context else []) + pathspec
|
||||
# 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:
|
||||
# env is None when no index could be found (bare repo / not a
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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, _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
|
||||
@@ -482,10 +479,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 +501,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",
|
||||
@@ -1145,10 +1156,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
|
||||
|
||||
@@ -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,27 @@ 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
|
||||
that CC surfaces via the modern hook-output protocol
|
||||
(hookSpecificOutput.additionalContext) instead of the legacy stderr +
|
||||
exit(2) pair. The caller passes the finding-explanation text it would
|
||||
have written to stderr; the JSON channel carries it cleanly so CC's UI
|
||||
shows the reason properly instead of "Permission denied with no reason".
|
||||
See anthropics/claude-plugins-official#1375 and #1783. Empty/None
|
||||
means no hookSpecificOutput field is emitted (preserves backward compat
|
||||
for legacy emit-sites that only want metrics).
|
||||
|
||||
`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): which event
|
||||
the hookSpecificOutput attaches to. Defaults to "PostToolUse" since the
|
||||
commit-review and push-sweep handlers are the most common callers;
|
||||
handle_stop_hook explicitly passes "Stop".
|
||||
"""
|
||||
head = {}
|
||||
if _PV and "pv" not in metrics:
|
||||
@@ -223,6 +251,17 @@ def emit_metrics(metrics, rewake_summary=None):
|
||||
out = {"metrics": metrics}
|
||||
if rewake_summary:
|
||||
out["rewakeSummary"] = rewake_summary
|
||||
if additional_context:
|
||||
# Wrap in hookSpecificOutput per CC's modern hook-output contract.
|
||||
# Drops the legacy `sys.stderr.write(...) + sys.exit(2)` shape that
|
||||
# left CC's UI showing "denied with no reason" (#1783) and triggered
|
||||
# "json output validation failed" on older CC versions (#1375).
|
||||
out["hookSpecificOutput"] = {
|
||||
"hookEventName": hook_event_name,
|
||||
"additionalContext": additional_context,
|
||||
}
|
||||
if system_message:
|
||||
out["systemMessage"] = system_message
|
||||
print(json.dumps(out), flush=True)
|
||||
|
||||
# =====================================================================
|
||||
@@ -510,7 +549,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 +637,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 +688,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 +860,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
|
||||
@@ -1121,18 +1197,18 @@ def handle_commit_review_posttooluse(input_data):
|
||||
# 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, "-c", "core.quotePath=false",
|
||||
"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:
|
||||
result = subprocess.run(
|
||||
[*GIT_CMD, "-c", "core.quotePath=false",
|
||||
"show", "-p", "--no-color", "--no-ext-diff", sha, "--"],
|
||||
[*GIT_CMD, "show", "-p", "--no-color", "--no-ext-diff", sha, "--"],
|
||||
cwd=repo_root, capture_output=True, timeout=15
|
||||
)
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
|
||||
@@ -1259,12 +1335,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
|
||||
@@ -1366,18 +1443,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):
|
||||
@@ -1458,9 +1543,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 "")
|
||||
@@ -1490,14 +1576,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
|
||||
@@ -1511,11 +1598,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:
|
||||
@@ -1634,17 +1722,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
|
||||
@@ -1697,9 +1791,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):
|
||||
@@ -1932,6 +2031,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),
|
||||
@@ -1945,10 +2049,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:
|
||||
@@ -1976,10 +2080,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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user