mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-06-20 16:47:21 +00:00
Compare commits
23 Commits
fix-2056-w
...
bump/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f2543cbe7 | ||
|
|
3d368d2972 | ||
|
|
84011d43b1 | ||
|
|
2a822c0787 | ||
|
|
a40c9f1e83 | ||
|
|
c7a3e2ffa0 | ||
|
|
1ecf3d1bac | ||
|
|
c40770ae5a | ||
|
|
7a0a7f486e | ||
|
|
42487ee6fd | ||
|
|
bc07f7a1fd | ||
|
|
9e150cfd48 | ||
|
|
38b298d5b2 | ||
|
|
8435428dfc | ||
|
|
0d22ba3501 | ||
|
|
37ffc76005 | ||
|
|
982070e51f | ||
|
|
68a700837c | ||
|
|
5212308979 | ||
|
|
3d349d40b9 | ||
|
|
12a5376e20 | ||
|
|
04127de5d1 | ||
|
|
a67587c816 |
@@ -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": "8d74ee6b6fdce4a1c46b98b5a66706c9393fc369"
|
||||
},
|
||||
"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": "f16aaf2a4ec7d59963c4fdf91e7358bd485e992e"
|
||||
},
|
||||
"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": "57d6099f499fe21572bdf943396630bd3d968550"
|
||||
},
|
||||
"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": "a4fdd4d0605239aa7be75a57371f4e7e71efc40c"
|
||||
},
|
||||
"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": "f16aaf2a4ec7d59963c4fdf91e7358bd485e992e"
|
||||
},
|
||||
"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": "f16aaf2a4ec7d59963c4fdf91e7358bd485e992e"
|
||||
},
|
||||
"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": "d3440b8a4f138585a512ecd4e0c54ede13ab1cc2"
|
||||
},
|
||||
"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": "f512df80dd54fd0a607ece2f18a9d226ca705019"
|
||||
},
|
||||
"homepage": "https://carta.com"
|
||||
},
|
||||
@@ -458,7 +458,7 @@
|
||||
"url": "https://github.com/carta/plugins.git",
|
||||
"path": "plugins/carta-crm",
|
||||
"ref": "main",
|
||||
"sha": "5e6c9d1cfa3bff9b91138e7906c6eb088fd9a66a"
|
||||
"sha": "f512df80dd54fd0a607ece2f18a9d226ca705019"
|
||||
},
|
||||
"homepage": "https://carta.com"
|
||||
},
|
||||
@@ -474,7 +474,7 @@
|
||||
"url": "https://github.com/carta/plugins.git",
|
||||
"path": "plugins/carta-investors",
|
||||
"ref": "main",
|
||||
"sha": "5e6c9d1cfa3bff9b91138e7906c6eb088fd9a66a"
|
||||
"sha": "f512df80dd54fd0a607ece2f18a9d226ca705019"
|
||||
},
|
||||
"homepage": "https://carta.com"
|
||||
},
|
||||
@@ -490,7 +490,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/cap-js/mcp-server.git",
|
||||
"sha": "92dc99f5ba0c56957ed5d390484693a69ebd1206"
|
||||
"sha": "9658cea90c782a6ab007ac16278c90fa4feca0ed"
|
||||
},
|
||||
"homepage": "https://cap.cloud.sap/"
|
||||
},
|
||||
@@ -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": "f79d57d207f039e44a31a976564715f7731e71b6"
|
||||
},
|
||||
"homepage": "https://codspeed.io"
|
||||
},
|
||||
@@ -753,7 +753,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/get-convex/convex-backend-skill.git",
|
||||
"sha": "5e59870cda2a5892e18a7164d1a46fcf57b70bea"
|
||||
"sha": "002f9c834cdb834ddef1e4867d87cb6e80f0acba"
|
||||
},
|
||||
"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": "2908d2f470f14f58d378b26c2af58825fa8d0149"
|
||||
},
|
||||
"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": "f16aaf2a4ec7d59963c4fdf91e7358bd485e992e"
|
||||
},
|
||||
"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": "f16aaf2a4ec7d59963c4fdf91e7358bd485e992e"
|
||||
},
|
||||
"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": "81178096fa7ae860285923948b8ba13d03c7fa0c"
|
||||
},
|
||||
"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": "49abf82b2ef2ff2bcc6ca072aac0fe8627390a1d"
|
||||
},
|
||||
"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": "4eb5fbbcb75e0c4c4f2c0d8aa02756165fdde629"
|
||||
},
|
||||
"homepage": "https://hunter.io"
|
||||
},
|
||||
@@ -1245,7 +1245,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/heygen-com/hyperframes.git",
|
||||
"sha": "7ea4d1c1314bd60d5273efa92626bd1d0f9c621d"
|
||||
"sha": "f8abff2e1c0958b61419b4d2ad91ceca7a7ea110"
|
||||
},
|
||||
"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": "d20075e440af2a209bc3b011a336276336e6d10c"
|
||||
},
|
||||
"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": "5a87ebc13343ce6ebabac0bcc443c1da0bcf2459"
|
||||
},
|
||||
"homepage": "https://output.ai"
|
||||
},
|
||||
@@ -1729,7 +1729,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/gopigment/ai-plugins.git",
|
||||
"sha": "4bf16c80558416b9d69fa6531af8588fb2fcbe27"
|
||||
"sha": "abf36e64750d1323a4cc5fe79161597668231224"
|
||||
},
|
||||
"homepage": "https://www.pigment.com"
|
||||
},
|
||||
@@ -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"
|
||||
},
|
||||
@@ -2113,7 +2113,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/cap-js/mcp-server.git",
|
||||
"sha": "92dc99f5ba0c56957ed5d390484693a69ebd1206"
|
||||
"sha": "9658cea90c782a6ab007ac16278c90fa4feca0ed"
|
||||
},
|
||||
"homepage": "https://cap.cloud.sap/"
|
||||
},
|
||||
@@ -2131,7 +2131,7 @@
|
||||
"url": "https://github.com/SAP/open-ux-tools.git",
|
||||
"path": "packages/fiori-mcp-server",
|
||||
"ref": "main",
|
||||
"sha": "d2a6fce818f3c046c5bbb041507be4632f926602"
|
||||
"sha": "c3e0940ef29ac520b4cc5bd6fd71fecede7b3342"
|
||||
},
|
||||
"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": "849303a8411c242d250885ffe714235a3bc2f5fe"
|
||||
},
|
||||
"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": "213c156a47fb2952287938f91e94b5073402530d"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
@@ -2411,7 +2411,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/obra/superpowers.git",
|
||||
"sha": "f2cbfbefebbfef77321e4c9abc9e949826bea9d7"
|
||||
"sha": "6fd4507659784c351abbd2bc264c7162cfd386dc"
|
||||
},
|
||||
"homepage": "https://github.com/obra/superpowers.git"
|
||||
},
|
||||
@@ -2445,7 +2445,7 @@
|
||||
"source": {
|
||||
"source": "url",
|
||||
"url": "https://github.com/JetBrains/teamcity-cli.git",
|
||||
"sha": "7f8419738b452108ff181365be30c1fab0a6905e"
|
||||
"sha": "533c8cb20928be912188fd8ae40fcba24cc720ca"
|
||||
},
|
||||
"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": "357f8d5b7cd81455e6a57cc64dba83985131d78f"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -32,6 +38,8 @@ BUILD_FAILED = 3 # venv create or pip install raised/timed out
|
||||
# llm.py also matches Windows venv layout (Lib/site-packages). Don't reuse the
|
||||
# value — telemetry rows from older plugin builds still emit 4.
|
||||
SKIP_SENTINEL = 5 # another SessionStart is currently building
|
||||
HOOK_PY_INCOMPATIBLE = 6 # hook interpreter is <3.10 — SDK syntax can't load
|
||||
# here no matter how the venv was built. See #2071.
|
||||
|
||||
|
||||
def _sdk_on_syspath() -> bool:
|
||||
@@ -62,13 +70,33 @@ def main() -> tuple[int, str, str]:
|
||||
err_phase / err_kind are non-empty only on BUILD_FAILED — they let
|
||||
telemetry split bootstrap failures by root cause.
|
||||
"""
|
||||
# Honesty check (fixes the misleading NOOP_VENV in #2071): the SDK
|
||||
# requires Python >=3.10 and uses 3.10+ syntax (match statements,
|
||||
# PEP 604 unions). On a 3.9 hook interpreter we CANNOT import it no
|
||||
# matter how the venv was built — llm.py runs in this same interpreter
|
||||
# and the syntax-level import will SyntaxError. macOS ships 3.9.6 as
|
||||
# the default `python3` and `/usr/bin` precedes Homebrew in PATH, so
|
||||
# this case is the default state for a large share of macOS users.
|
||||
#
|
||||
# sg-python.sh now prefers python3.10+ binaries so most users won't
|
||||
# reach this branch; the fallback to 3.9 is preserved for the
|
||||
# pattern-warning hooks that don't need the SDK. Reporting
|
||||
# HOOK_PY_INCOMPATIBLE here:
|
||||
# (a) avoids 30-60s of wasted pip install,
|
||||
# (b) avoids the lie where the venv_py probe says NOOP_VENV but the
|
||||
# consumer import fails, and
|
||||
# (c) gives telemetry a clean bucket to size the affected fleet.
|
||||
if sys.version_info < (3, 10):
|
||||
return (
|
||||
HOOK_PY_INCOMPATIBLE,
|
||||
"hook_py",
|
||||
f"py_{sys.version_info[0]}.{sys.version_info[1]}",
|
||||
)
|
||||
|
||||
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":
|
||||
@@ -195,6 +223,53 @@ def main() -> tuple[int, str, str]:
|
||||
sentinel.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _maybe_emit_user_notice(outcome: int, pv: int) -> str | None:
|
||||
"""Return a one-time user-visible notice when the agentic reviewer is
|
||||
in a persistent broken state on this machine, or None if we've already
|
||||
shown the notice for this plugin version (or shouldn't show one).
|
||||
|
||||
The marker file is plugin-version-keyed: a future plugin update can
|
||||
re-notify if behavior changes (e.g. we ship out-of-process SDK in v3
|
||||
and want to tell affected users it's fixed). Failures to write the
|
||||
marker degrade to "skip the notice this session" so we don't spam
|
||||
every SessionStart on a read-only home dir.
|
||||
|
||||
Currently only HOOK_PY_INCOMPATIBLE qualifies. BUILD_FAILED is
|
||||
intentionally excluded — it covers transient causes (network failure,
|
||||
pip registry hiccup, in-flight rebuild) where the next session may
|
||||
succeed and a permanent notice would mislead.
|
||||
"""
|
||||
if outcome != HOOK_PY_INCOMPATIBLE:
|
||||
return None
|
||||
try:
|
||||
state_dir = Path(_resolve_state_dir())
|
||||
marker = state_dir / f".agentic_unavailable_notice_v{pv or 0}"
|
||||
if marker.exists():
|
||||
return None
|
||||
state_dir.mkdir(parents=True, exist_ok=True)
|
||||
# Write timestamp + Python version so the marker is self-documenting
|
||||
# if a user goes looking. O_EXCL would be racier with no real win
|
||||
# (two concurrent SessionStarts both showing the notice once is fine).
|
||||
marker.write_text(
|
||||
f"{time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())} "
|
||||
f"py={sys.version_info[0]}.{sys.version_info[1]}\n"
|
||||
)
|
||||
except OSError:
|
||||
return None
|
||||
return (
|
||||
f"⚠ security-guidance plugin: the cross-file commit reviewer "
|
||||
f"(layer 3 of 3 — catches IDOR, auth-bypass, cross-file SSRF) "
|
||||
f"is unavailable in this environment. It requires Python ≥3.10, "
|
||||
f"but the hook is running on "
|
||||
f"{sys.version_info[0]}.{sys.version_info[1]}.\n\n"
|
||||
f"Pattern checks and the single-shot LLM diff review are still "
|
||||
f"active. To enable the deeper reviewer, install Python 3.10+ "
|
||||
f"(e.g. `brew install python` on macOS) and restart Claude Code.\n\n"
|
||||
f"This notice is shown once per plugin version. "
|
||||
f"See: github.com/anthropics/claude-plugins-official/issues/2071"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Tell the harness this is async — venv create + pip install can take
|
||||
# 30-60s on a cold cache, well past the default sync hook timeout.
|
||||
@@ -231,4 +306,18 @@ if __name__ == "__main__":
|
||||
pv = _plugin_version_int()
|
||||
if pv:
|
||||
metrics["pv"] = pv
|
||||
print(json.dumps({"metrics": metrics}), flush=True)
|
||||
response: dict[str, object] = {"metrics": metrics}
|
||||
# One-time user-visible notice when the agentic reviewer is dead on
|
||||
# arrival. Uses hookSpecificOutput.additionalContext (SessionStart's
|
||||
# supported channel for surfacing text to both the model and the user)
|
||||
# plus systemMessage as a belt-and-suspenders. Marker-file-gated so
|
||||
# this fires exactly once per plugin version per install — see
|
||||
# _maybe_emit_user_notice.
|
||||
notice = _maybe_emit_user_notice(outcome, pv)
|
||||
if notice:
|
||||
response["hookSpecificOutput"] = {
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": notice,
|
||||
}
|
||||
response["systemMessage"] = notice
|
||||
print(json.dumps(response), flush=True)
|
||||
|
||||
@@ -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
|
||||
@@ -323,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,
|
||||
)
|
||||
@@ -361,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:
|
||||
@@ -403,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):
|
||||
@@ -436,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
|
||||
@@ -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
|
||||
@@ -1118,11 +1194,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 +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
|
||||
@@ -1361,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):
|
||||
@@ -1453,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 "")
|
||||
@@ -1485,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
|
||||
@@ -1506,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:
|
||||
@@ -1629,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
|
||||
@@ -1692,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):
|
||||
@@ -1927,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),
|
||||
@@ -1940,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:
|
||||
@@ -1971,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
|
||||
@@ -47,21 +58,65 @@ fi
|
||||
|
||||
probe() {
|
||||
# $1..N: the interpreter command (may be multi-word like `py -3`)
|
||||
# Probe writes the major version to stdout and exits 0 iff it's >=3.
|
||||
"$@" -c 'import sys; print(sys.version_info[0])' 2>/dev/null
|
||||
# Writes "<major>.<minor>" to stdout and exits 0 iff at least Python 3.
|
||||
"$@" -c 'import sys; print(f"{sys.version_info[0]}.{sys.version_info[1]}")' 2>/dev/null
|
||||
}
|
||||
|
||||
# True iff arg is a "M.m" version string >= 3.10. claude_agent_sdk requires
|
||||
# Python >= 3.10; below that, pip install fails ("No matching distribution")
|
||||
# and the LLM-powered review (Stop / commit / push) silently no-ops while
|
||||
# pattern checks (PostToolUse regex) keep working. macOS ships 3.9.6 as the
|
||||
# default `python3` on current versions, so this guard matters in practice.
|
||||
# See anthropics/claude-plugins-official#2071.
|
||||
is_sdk_compatible() {
|
||||
case "$1" in
|
||||
3.1[0-9]|3.[2-9][0-9]|[4-9].*|[1-9][0-9].*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Pass 1 — try minor-versioned binaries in descending order. These are only
|
||||
# present if the user explicitly installed them (Homebrew / python.org / pyenv),
|
||||
# so picking one here always upgrades over the system `python3`. Highest
|
||||
# available wins; the user doesn't have to PATH-prefer it.
|
||||
for cmd in "python3.13" "python3.12" "python3.11" "python3.10"; do
|
||||
v=$(probe "$cmd") || continue
|
||||
if is_sdk_compatible "$v"; then
|
||||
exec "$cmd" "$@"
|
||||
fi
|
||||
done
|
||||
|
||||
# Pass 2 — bare interpreters, but only if SDK-compatible. Covers Linux distros
|
||||
# that ship 3.10+ as the default `python3`, and Windows where `python` /
|
||||
# `py -3` resolves to the user's python.org install.
|
||||
for cmd in "python3" "python" "py -3"; do
|
||||
# Word-split intentionally so `py -3` works
|
||||
# shellcheck disable=SC2086
|
||||
v=$(probe $cmd) || continue
|
||||
if [ "$v" = "3" ]; then
|
||||
if is_sdk_compatible "$v"; then
|
||||
# shellcheck disable=SC2086
|
||||
exec $cmd "$@"
|
||||
fi
|
||||
done
|
||||
|
||||
# Pass 3 — fallback to any Python 3, even <3.10. Pattern-based checks
|
||||
# (PostToolUse regex on Edit/Write) only need 3.6+ and are useful on their
|
||||
# own; the SDK-dependent paths will detect the version mismatch and degrade
|
||||
# inside the Python code. Without this fallback, the entire plugin would
|
||||
# stop working on default macOS, which is a regression vs today.
|
||||
for cmd in "python3" "python" "py -3"; do
|
||||
# shellcheck disable=SC2086
|
||||
v=$(probe $cmd) || continue
|
||||
# Accept anything that successfully reported a "M.m" string.
|
||||
case "$v" in
|
||||
[0-9]*.[0-9]*)
|
||||
# shellcheck disable=SC2086
|
||||
exec $cmd "$@"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "security-guidance: no working Python 3 interpreter found." >&2
|
||||
echo " tried: python3, python, py -3" >&2
|
||||
echo " tried: python3.13, python3.12, python3.11, python3.10, python3, python, py -3" >&2
|
||||
echo " on Windows, install Python from https://python.org (NOT the Microsoft Store)" >&2
|
||||
echo " on macOS, install Python 3.10+ via Homebrew (\`brew install python\`)" >&2
|
||||
exit 1
|
||||
|
||||
Reference in New Issue
Block a user