Compare commits

..

1 Commits

Author SHA1 Message Date
Bryan Thompson
b76c6c9d75 Add nvidia-skills plugin 2026-05-29 13:10:46 -05:00
9 changed files with 136 additions and 226 deletions

View File

@@ -19,7 +19,7 @@
"url": "https://github.com/42Crunch-AI/claude-plugins.git",
"path": "plugins/api-security-testing",
"ref": "v1.5.5",
"sha": "b404d99a3f0bc1f3e74a1638671e2e3319187e2c"
"sha": "5c8074d846b852c21da23bbf6effbfdabb18ba2d"
},
"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": "0a015c06894332091b79e055e0404fbc1a18c9fe"
"sha": "ecd1e2b2c493ba0627774f36a897bd44d47fef1d"
},
"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": "1584dd52f388482db78949456addfa29a4c9d9c3"
"sha": "5ddccc36737b8bdc3dcabb3d6f51daa350c3d16d"
},
"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": "9d46cc0a092c0a8c01a5bd06a4349985cc6c8f08"
"sha": "5d982e8a5f1e0b06545adac69ff0348141587725"
},
"homepage": "https://github.com/awslabs/agent-plugins"
},
@@ -193,7 +193,7 @@
"source": {
"source": "url",
"url": "https://github.com/astronomer/agents.git",
"sha": "7ce4a12d3cabb506294134c91a1b876d4b166a70"
"sha": "535a040ca9e27aaed6da13f0f959625fb3294820"
},
"homepage": "https://github.com/astronomer/agents"
},
@@ -203,7 +203,7 @@
"source": {
"source": "url",
"url": "https://github.com/atlanhq/agent-toolkit.git",
"sha": "b0efcc8e6adc64d052b634ac1103932390413fd9"
"sha": "790398c87378f128bdc74c31bb7ecfb8e4695f29"
},
"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": "bb9708ec7c4c7145bd64033dbece0bfaed0c2ad5"
"sha": "c4e905c49884747be65e7ed42ccfb118c67f57ac"
},
"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": "c38453f6a99bbfeaf73b5be81db987ec6af982da"
"sha": "c771dc1c77bfd5a67686afb464ccebd227c02b0f"
},
"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": "9d46cc0a092c0a8c01a5bd06a4349985cc6c8f08"
"sha": "5d982e8a5f1e0b06545adac69ff0348141587725"
},
"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": "9d46cc0a092c0a8c01a5bd06a4349985cc6c8f08"
"sha": "5d982e8a5f1e0b06545adac69ff0348141587725"
},
"homepage": "https://github.com/awslabs/agent-plugins"
},
@@ -346,7 +346,7 @@
"source": {
"source": "url",
"url": "https://github.com/microsoft/azure-skills.git",
"sha": "7cb89c221ecc9eccb71580aaff3695408cdeef2b"
"sha": "d02fd24f151f5133650eaa78e7da3cac2cedd72f"
},
"homepage": "https://github.com/microsoft/azure-skills"
},
@@ -412,7 +412,7 @@
"source": {
"source": "url",
"url": "https://github.com/brightdata/skills.git",
"sha": "da73549126e5834a9230ee5532d4917d43aedf11"
"sha": "071e9d4db77c8561e333799f25ea85f11f7b667d"
},
"homepage": "https://docs.brightdata.com"
},
@@ -442,7 +442,7 @@
"url": "https://github.com/carta/plugins.git",
"path": "plugins/carta-cap-table",
"ref": "main",
"sha": "e66d331cd8e669ee121c96ee35b0c91acd828970"
"sha": "5e6c9d1cfa3bff9b91138e7906c6eb088fd9a66a"
},
"homepage": "https://carta.com"
},
@@ -458,7 +458,7 @@
"url": "https://github.com/carta/plugins.git",
"path": "plugins/carta-crm",
"ref": "main",
"sha": "e66d331cd8e669ee121c96ee35b0c91acd828970"
"sha": "5e6c9d1cfa3bff9b91138e7906c6eb088fd9a66a"
},
"homepage": "https://carta.com"
},
@@ -474,7 +474,7 @@
"url": "https://github.com/carta/plugins.git",
"path": "plugins/carta-investors",
"ref": "main",
"sha": "e66d331cd8e669ee121c96ee35b0c91acd828970"
"sha": "5e6c9d1cfa3bff9b91138e7906c6eb088fd9a66a"
},
"homepage": "https://carta.com"
},
@@ -501,7 +501,7 @@
"source": {
"source": "url",
"url": "https://github.com/ChromeDevTools/chrome-devtools-mcp.git",
"sha": "2e039c09e1a273581d9b51081a0feb8a57791947"
"sha": "60be3e6bc157bd1121ea1d4b6ad59e37a73cac3e"
},
"homepage": "https://github.com/ChromeDevTools/chrome-devtools-mcp"
},
@@ -716,7 +716,7 @@
"source": {
"source": "url",
"url": "https://github.com/CodSpeedHQ/codspeed.git",
"sha": "407dd3c930b8dc5e5655a2d91a65d88f01829955"
"sha": "ecf3c2ebf959479126d631ad39d317738d559388"
},
"homepage": "https://codspeed.io"
},
@@ -753,7 +753,7 @@
"source": {
"source": "url",
"url": "https://github.com/get-convex/convex-backend-skill.git",
"sha": "ece93250d560f0ce32a24223dea92b33050b2a66"
"sha": "5e59870cda2a5892e18a7164d1a46fcf57b70bea"
},
"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": "fb25d60ecdbc0129071802dad210a65168ca55a9"
"sha": "99edea095f4e32ed008706b55257d0893fb93387"
},
"homepage": "https://github.com/CrowdStrike/foundry-skills"
},
@@ -830,7 +830,7 @@
"source": {
"source": "url",
"url": "https://github.com/dash0hq/dash0-agent-plugin.git",
"sha": "d1ad56f86f2a9ae74eccf1df2bb2985c963005b1"
"sha": "2909be7ebc2804af464e0d7f660ccc2b62d94623"
},
"homepage": "https://dash0.com/"
},
@@ -841,7 +841,7 @@
"source": {
"source": "url",
"url": "https://github.com/astronomer/agents.git",
"sha": "7ce4a12d3cabb506294134c91a1b876d4b166a70"
"sha": "535a040ca9e27aaed6da13f0f959625fb3294820"
},
"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": "86eb482b33d943aa4242ae6f06d627ec12064d46"
"sha": "7bc75b5e53d6eaae103132fd1a47de26239e4ae4"
},
"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": "7ce4a12d3cabb506294134c91a1b876d4b166a70"
"sha": "535a040ca9e27aaed6da13f0f959625fb3294820"
},
"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": "9d46cc0a092c0a8c01a5bd06a4349985cc6c8f08"
"sha": "5d982e8a5f1e0b06545adac69ff0348141587725"
},
"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": "4c3dfbd259bc2c6c815f7575d27ca26bc09d0d17"
"sha": "8124faae2154117382b1046aa74d8901a3ffe930"
},
"homepage": "https://datarobot.com"
},
@@ -946,7 +946,7 @@
"url": "https://github.com/awslabs/agent-plugins.git",
"path": "plugins/deploy-on-aws",
"ref": "main",
"sha": "9d46cc0a092c0a8c01a5bd06a4349985cc6c8f08"
"sha": "5d982e8a5f1e0b06545adac69ff0348141587725"
},
"homepage": "https://github.com/awslabs/agent-plugins"
},
@@ -1048,7 +1048,7 @@
"url": "https://github.com/expo/skills.git",
"path": "plugins/expo",
"ref": "main",
"sha": "fdd3df12151a208853fe540ffea9a67773446377"
"sha": "510373b50956ef4dc84c20bb4c9cce70b618aa06"
},
"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": "e71cec486062680f0c8f8823afcb3558ad81ce60"
"sha": "01d11b30ace699a27f9ea7decf6ce6c9857f71ff"
},
"homepage": "https://github.com/firecrawl/firecrawl-claude-plugin.git"
},
@@ -1217,7 +1217,7 @@
"source": {
"source": "url",
"url": "https://github.com/huggingface/skills.git",
"sha": "df627be1837523c91ac6df472e3dc543d3107bd9"
"sha": "7a493b09c81aae09a41bd2e1fa33dfc0f68acd75"
},
"homepage": "https://github.com/huggingface/skills.git"
},
@@ -1231,7 +1231,7 @@
"source": {
"source": "url",
"url": "https://github.com/hunter-io/claude-plugin.git",
"sha": "9b6146520c48f9dcc6092f106e5c1a5762ca3e7a"
"sha": "c67942395cde155e9ad4ed8e3a137926f9992fb8"
},
"homepage": "https://hunter.io"
},
@@ -1245,7 +1245,7 @@
"source": {
"source": "url",
"url": "https://github.com/heygen-com/hyperframes.git",
"sha": "bc3701f5905c5ba7c8cf03c3bbe3a49162d2b1f1"
"sha": "7ea4d1c1314bd60d5273efa92626bd1d0f9c621d"
},
"homepage": "https://hyperframes.heygen.com"
},
@@ -1410,7 +1410,7 @@
"url": "https://github.com/pydantic/skills.git",
"path": "plugins/logfire",
"ref": "main",
"sha": "eb17c0da94de81488825c0198475233dc1f06393"
"sha": "0c38c5bb5679f6cc41956bbbf811396a0d108ac9"
},
"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": "ba967158392bec9f0c199cd39196af64222f0ab0"
"sha": "f52c138924d8035b39e8fe02d41c6712fc41ceb4"
},
"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": "9736dfc757f5ed4f05da0480b202af09e93a27de"
"sha": "95ed06468957ddc9de609b25c390b30c3864eac8"
},
"homepage": "https://docs.nimbleway.com/integrations/agent-skills/plugin-installation"
},
@@ -1653,6 +1653,21 @@
},
"homepage": "https://github.com/makenotion/claude-code-notion-plugin"
},
{
"name": "nvidia-skills",
"description": "NVIDIA agent skills for accelerated-computing workflows — starting with cuOpt vehicle-routing optimization (VRP, TSP, PDP) via the cuOpt Python API.",
"author": {
"name": "NVIDIA"
},
"category": "development",
"source": {
"source": "git-subdir",
"url": "https://github.com/NVIDIA/skills.git",
"path": "plugins/nvidia-skills",
"ref": "main"
},
"homepage": "https://github.com/NVIDIA/skills"
},
{
"name": "oracle-ai-data-platform-workbench-spark-connectors",
"description": "Oracle AI Data Platform Workbench Spark connectors for Claude Code. 18 connector skills covering every data source workbench customers commonly need: Oracle Autonomous DB family (ALH/ADW/ATP) via wallet/IAM-DB-Token/API-key, ExaCS, Fusion ERP REST, Fusion BICC, EPM Cloud Planning, Essbase 21c, OCI Streaming (Kafka), OCI Object Storage, Apache Iceberg, plus external systems (PostgreSQL, MySQL/HeatWave, SQL Server, Snowflake, Azure ADLS Gen2, AWS S3, generic REST, custom JDBC, Excel). Live-validated on the workbench `tpcds` cluster (Spark 3.5.0): 17 PASS / 4 ship-as-is out of 21 test rows.",
@@ -1665,7 +1680,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": "6e59f24cd3e8870649e7f9b2e3e106502b43fd5f"
"sha": "f7ea9cae6fce69a4e3798dfc1d5216ac1d0dd7e8"
},
"homepage": "https://docs.oracle.com/en/cloud/paas/ai-data-platform/index.html"
},
@@ -1681,7 +1696,7 @@
"url": "https://github.com/growthxai/output.git",
"path": "coding_assistants/claude/plugins/outputai",
"ref": "main",
"sha": "0eeffece25b6f471c48b705a214471164b8c5946"
"sha": "93dd22ee568a97911a332b5aa0d9cebb2b6f7da1"
},
"homepage": "https://output.ai"
},
@@ -1846,7 +1861,7 @@
"url": "https://github.com/pydantic/skills.git",
"path": "plugins/ai",
"ref": "main",
"sha": "eb17c0da94de81488825c0198475233dc1f06393"
"sha": "0c38c5bb5679f6cc41956bbbf811396a0d108ac9"
},
"homepage": "https://github.com/pydantic/skills/tree/main/plugins/ai"
},
@@ -1884,7 +1899,7 @@
"source": {
"source": "url",
"url": "https://github.com/qdrant/skills.git",
"sha": "ea62a9857dabcc169597549da7681bd6d4cd13e9"
"sha": "1390c811e03922b822dc9e12b832ba4dc82e0bf0"
},
"homepage": "https://skills.qdrant.tech"
},
@@ -1895,7 +1910,7 @@
"source": {
"source": "url",
"url": "https://github.com/qodo-ai/qodo-skills.git",
"sha": "8aec13d6ac60feb9d9f84f36aa1753234de17dc8"
"sha": "b1eb0389480ee6de8df874f40a230ed2625ef0d3"
},
"homepage": "https://github.com/qodo-ai/qodo-skills.git"
},
@@ -1909,7 +1924,7 @@
"source": {
"source": "url",
"url": "https://github.com/TheQtCompanyRnD/agent-skills.git",
"sha": "a7189a7bc17e616b725e7ce4e46a4f5ebd50d94f"
"sha": "23772fa2264b3ff1037a96164b2c28d2b29a4c2f"
},
"homepage": "https://www.qt.io/"
},
@@ -1923,7 +1938,7 @@
"source": {
"source": "url",
"url": "https://github.com/quarkusio/quarkus-agent-mcp.git",
"sha": "32cad78bd9040efe31794cfc10f70caf2a724dd9"
"sha": "77fd36284a80b3ed1bde3d2fe48a0b2f99e4941e"
},
"homepage": "https://quarkus.io"
},
@@ -1975,7 +1990,7 @@
"url": "https://github.com/redis/agent-skills.git",
"path": "plugins/redis-development",
"ref": "main",
"sha": "5ca2e1a2d82a768221e8f71a02e3ca095a37d38e"
"sha": "18da4e42371f7eee0dcfafd8461effd41de351e9"
},
"homepage": "https://redis.io"
},
@@ -1985,7 +2000,7 @@
"source": {
"source": "url",
"url": "https://github.com/Digital-Process-Tools/claude-remember.git",
"sha": "c2c82ab5fd2f4f5c0cddc9c7d8a749655dec4cb9"
"sha": "c9b34417a8132f0416411a0ca51d009a256a3acc"
},
"homepage": "https://github.com/Digital-Process-Tools/claude-remember"
},
@@ -1999,7 +2014,7 @@
"source": {
"source": "url",
"url": "https://github.com/resend/resend-skills.git",
"sha": "376d1c3fb37cc7d22ab21cce836f4d6f323922de"
"sha": "78469829399beec62b8f815f109ebfcfa3b0680b"
},
"homepage": "https://resend.com"
},
@@ -2097,7 +2112,7 @@
"source": {
"source": "url",
"url": "https://github.com/sanity-io/agent-toolkit.git",
"sha": "d7545f5cc6f8fb39554083b52ad074a6d912db9f"
"sha": "236348e29b31e834ce71e4e2e3072184dd1c1e27"
},
"homepage": "https://www.sanity.io"
},
@@ -2131,7 +2146,7 @@
"url": "https://github.com/SAP/open-ux-tools.git",
"path": "packages/fiori-mcp-server",
"ref": "main",
"sha": "7432d23a7b5c3bd1c0a01cf76696bf0c417ecd1f"
"sha": "d2a6fce818f3c046c5bbb041507be4632f926602"
},
"homepage": "https://github.com/SAP/open-ux-tools/tree/main/packages/fiori-mcp-server"
},
@@ -2198,7 +2213,7 @@
"source": {
"source": "url",
"url": "https://github.com/getsentry/sentry-for-claude.git",
"sha": "d6123be331e2224b037e1ffefd27c806e7566dcf"
"sha": "ed0875684192bb8a050297a896657ff9db1ffdf5"
},
"homepage": "https://github.com/getsentry/sentry-for-claude/tree/main"
},
@@ -2214,7 +2229,7 @@
"url": "https://github.com/getsentry/cli.git",
"path": "plugins/sentry-cli",
"ref": "main",
"sha": "db90767935558db16c45036f89e68edaa1dde106"
"sha": "d9bcd70eaa467fb3ddf591bfbfb0686fd1e9c016"
},
"homepage": "https://sentry.io"
},
@@ -2279,7 +2294,7 @@
"source": {
"source": "url",
"url": "https://github.com/Shopify/Shopify-AI-Toolkit.git",
"sha": "859be93bfc858f183ff5eb40183e35a4d91d2950"
"sha": "c164cf45c4bc1d17bbc105168d99a4f744cfaac2"
},
"homepage": "https://shopify.dev"
},
@@ -2364,7 +2379,7 @@
"source": {
"source": "url",
"url": "https://github.com/spotify/ads-claude-plugin.git",
"sha": "73b8bd490e02d3ed0bb4c8e228a470c46f995154"
"sha": "7ed948b85337f6b31a82dfaa8f033b6843659fa3"
},
"homepage": "https://github.com/spotify/ads-claude-plugin"
},
@@ -2377,7 +2392,7 @@
"url": "https://github.com/stripe/ai.git",
"path": "providers/claude/plugin",
"ref": "main",
"sha": "99425a010474c6aab745a975d06764e323c2c4d4"
"sha": "a34795211da530a168f581122011bb5ceb2e4bd0"
},
"homepage": "https://github.com/stripe/ai/tree/main/providers/claude/plugin"
},
@@ -2400,7 +2415,7 @@
"source": {
"source": "url",
"url": "https://github.com/supabase-community/supabase-plugin.git",
"sha": "3217ac038647f6901a166f3264a32f01833f73ba"
"sha": "1b910c021aee8c9c054196f0e840b3a65e1a7c63"
},
"homepage": "https://github.com/supabase-community/supabase-plugin"
},
@@ -2445,7 +2460,7 @@
"source": {
"source": "url",
"url": "https://github.com/JetBrains/teamcity-cli.git",
"sha": "9436b94b228579ba952aba809357776c3db9ce1a"
"sha": "7f8419738b452108ff181365be30c1fab0a6905e"
},
"homepage": "https://www.jetbrains.com/teamcity/"
},
@@ -2538,7 +2553,7 @@
"url": "https://github.com/UI5/plugins-coding-agents.git",
"path": "plugins/ui5",
"ref": "main",
"sha": "7acd8328399a221e161ae5bb04a5675696f92920"
"sha": "78f657e6a5004b5cdd1b998aabea616023eeabbb"
},
"homepage": "https://github.com/UI5/plugins-coding-agents"
},
@@ -2556,7 +2571,7 @@
"url": "https://github.com/UI5/plugins-coding-agents.git",
"path": "plugins/ui5-typescript-conversion",
"ref": "main",
"sha": "7acd8328399a221e161ae5bb04a5675696f92920"
"sha": "78f657e6a5004b5cdd1b998aabea616023eeabbb"
},
"homepage": "https://github.com/UI5/plugins-coding-agents"
},
@@ -2595,7 +2610,7 @@
"source": {
"source": "url",
"url": "https://github.com/explorium-ai/vibeprospecting-plugin.git",
"sha": "c00b11db4efc3e7b7aaffc10d71db33c806d5607"
"sha": "ada4d569dbf70194fe18750ecbc5170e9a3f120a"
},
"homepage": "https://www.vibeprospecting.ai/product/claude-plugin"
},
@@ -2620,7 +2635,7 @@
"source": {
"source": "url",
"url": "https://github.com/wix/skills.git",
"sha": "c5b343f2dadba06da91ee6de07272161fb68d40d"
"sha": "5da7e749a466ef9ddcdb2822099b940b9a1bc151"
},
"homepage": "https://dev.wix.com/docs/wix-cli/guides/development/about-wix-skills"
},
@@ -2727,7 +2742,7 @@
"source": {
"source": "url",
"url": "https://github.com/zscaler/zscaler-mcp-server.git",
"sha": "be37fb604a07dc9c5a4c3e009312c4f11acaa6d3"
"sha": "8409e1661b7f7171bfbb9297e1ecfc61c28b6d92"
},
"homepage": "https://github.com/zscaler/zscaler-mcp-server"
}

View File

@@ -10,42 +10,15 @@ 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
# (plugin-specific override) or CLAUDE_CONFIG_DIR (CC-wide config dir, #1868).
# 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"
)
DEBUG_LOG_FILE = os.environ.get("SECURITY_GUIDANCE_DEBUG_LOG") or os.path.join(
state_dir(), "log.txt"
_DEFAULT_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

View File

@@ -23,12 +23,6 @@ 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
@@ -96,7 +90,10 @@ def main() -> tuple[int, str, str]:
if _sdk_on_syspath():
return NOOP_SYSTEM, "", ""
state_dir = Path(_resolve_state_dir())
state_dir = Path(
os.environ.get("SECURITY_WARNINGS_STATE_DIR")
or os.path.expanduser("~/.claude/security")
)
venv = state_dir / "agent-sdk-venv"
# Windows venvs put the interpreter at Scripts\python.exe; POSIX uses bin/python.
if sys.platform == "win32":
@@ -242,7 +239,10 @@ def _maybe_emit_user_notice(outcome: int, pv: int) -> str | None:
if outcome != HOOK_PY_INCOMPATIBLE:
return None
try:
state_dir = Path(_resolve_state_dir())
state_dir = Path(
os.environ.get("SECURITY_WARNINGS_STATE_DIR")
or os.path.expanduser("~/.claude/security")
)
marker = state_dir / f".agentic_unavailable_notice_v{pv or 0}"
if marker.exists():
return None

View File

@@ -32,17 +32,12 @@ GIT_CMD = [
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, timeout=5
cwd=cwd, capture_output=True, text=True, timeout=5
)
if result.returncode == 0 and result.stdout.strip():
return result.stdout.decode("utf-8", errors="replace").strip()
return result.stdout.strip()
return None
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
return None
@@ -57,17 +52,13 @@ 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, timeout=5
cwd=cwd, capture_output=True, text=True, timeout=5
)
if result.returncode != 0:
return None
git_dir = result.stdout.decode("utf-8", errors="replace").strip()
git_dir = result.stdout.strip()
if not os.path.isabs(git_dir):
git_dir = os.path.join(cwd, git_dir)
index_path = os.path.join(git_dir, "index")
@@ -137,13 +128,9 @@ 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, timeout=10,
cwd=cwd, capture_output=True, text=True, timeout=10,
env=env,
)
yield env
@@ -157,17 +144,11 @@ 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, timeout=5,
cwd=cwd, capture_output=True, text=True, timeout=5,
)
if r.returncode != 0:
return None
path = r.stdout.decode("utf-8", errors="replace").strip()
return path if path else None
return r.stdout.strip() if r.returncode == 0 and r.stdout.strip() else None
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
return None
@@ -183,15 +164,13 @@ 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, timeout=5,
cwd=repo_root, capture_output=True, text=True, timeout=5,
)
if r.returncode != 0:
return None
d = r.stdout.decode("utf-8", errors="replace").strip()
d = r.stdout.strip()
return d if os.path.isabs(d) else os.path.join(repo_root, d)
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
return None
@@ -200,15 +179,13 @@ 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, timeout=10,
cwd=repo_root, capture_output=True, text=True, timeout=10,
)
if r.returncode != 0:
return []
return [s for s in r.stdout.decode("utf-8", errors="replace").strip().split("\n") if s]
return [s for s in r.stdout.strip().split("\n") if s]
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
return []
@@ -243,11 +220,9 @@ 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, timeout=5,
cwd=repo_root, capture_output=True, text=True, timeout=5,
)
if r.returncode == 0 and r.stdout.strip():
return ref
@@ -435,12 +410,9 @@ 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, timeout=5,
cwd=cwd, capture_output=True, text=True, timeout=5,
)
return result.returncode == 0
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):

View File

@@ -37,7 +37,7 @@
{
"type": "command",
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/sg-python.sh\" \"${CLAUDE_PLUGIN_ROOT}/hooks/security_reminder_hook.py\"",
"if": "Bash(git commit:*)",
"if": "Bash(git commit:*)|Bash(gt create:*)|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"
@@ -45,31 +45,7 @@
{
"type": "command",
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/sg-python.sh\" \"${CLAUDE_PLUGIN_ROOT}/hooks/security_reminder_hook.py\"",
"if": "Bash(git push:*)",
"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:*)",
"if": "Bash(git push:*)|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"

View File

@@ -27,7 +27,7 @@ from typing import Optional, Tuple, Dict, Any, List
import extensibility
import review_api
from _base import debug_log, _record_usage, _PV, PROVENANCE_TAG, state_dir as _resolve_state_dir # noqa: F401
from _base import debug_log, _record_usage, _PV, PROVENANCE_TAG # noqa: F401
from session_state import with_locked_state
@@ -355,7 +355,10 @@ 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 = _resolve_state_dir()
_state_dir = os.environ.get(
"SECURITY_WARNINGS_STATE_DIR",
os.path.expanduser("~/.claude/security"),
)
_inject_agent_sdk_venv_into_syspath(_state_dir)
try:
import asyncio as _asyncio # noqa: F811
@@ -1142,7 +1145,10 @@ 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 = _resolve_state_dir()
_state_dir = os.environ.get(
"SECURITY_WARNINGS_STATE_DIR",
os.path.expanduser("~/.claude/security"),
)
_venv_tried = _inject_agent_sdk_venv_into_syspath(_state_dir)
try:
import asyncio as _asyncio # noqa: F811

View File

@@ -82,7 +82,6 @@ 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
@@ -549,11 +548,7 @@ def handle_user_prompt_submit(input_data):
elif sha:
debug_log(f"Captured git baseline: {sha[:12]}")
else:
# 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)")
debug_log("Failed to capture git baseline (not a git repo?)")
sys.exit(0)
@@ -645,15 +640,7 @@ _COMMIT_SHA_RE = re.compile(r'^\[[^\]]*?\b([0-9a-f]{7,40})\]', re.MULTILINE)
# 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'
)
_GIT_COMMIT_RE = re.compile(r'\b(?:git\s+commit|gt\s+(?:create|modify))(?:\s|$)')
# 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
@@ -860,30 +847,23 @@ 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, timeout=5,
cwd=repo_root, capture_output=True, text=True, timeout=5,
)
sha = r.stdout.decode("utf-8", errors="replace").strip()
if r.returncode == 0 and sha:
return sha
if r.returncode == 0 and r.stdout.strip():
return r.stdout.strip()
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, timeout=5,
cwd=repo_root, capture_output=True, text=True, timeout=5,
)
sha = r.stdout.decode("utf-8", errors="replace").strip()
if r.returncode == 0 and sha:
return sha
if r.returncode == 0 and r.stdout.strip():
return r.stdout.strip()
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
pass
return None
@@ -1335,13 +1315,12 @@ 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, timeout=5,
cwd=repo_root, capture_output=True, text=True, timeout=5,
)
if r.returncode == 0:
full_shas.append(r.stdout.decode("utf-8", errors="replace").strip())
full_shas.append(r.stdout.strip())
_append_reviewed_shas(repo_root, full_shas, vulns_found=len(vulns or []))
except Exception:
pass
@@ -1543,10 +1522,9 @@ 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, timeout=5)
head = r.stdout.decode("utf-8", errors="replace").strip() if r.returncode == 0 else None
capture_output=True, text=True, timeout=5)
head = r.stdout.strip() if r.returncode == 0 else None
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
pass
push_section = _push_section(bash_output or "")
@@ -1576,15 +1554,14 @@ 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, timeout=5)
cwd=repo_root, capture_output=True, text=True, timeout=5)
r_prev = subprocess.run(
[*GIT_CMD, "rev-parse", "--verify", "-q", "@{u}@{1}"],
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 ""
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 ""
quiet_success = bool(cur and prev_u and cur == head and prev_u != cur)
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
pass
@@ -1598,12 +1575,11 @@ 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, timeout=5,
cwd=repo_root, capture_output=True, text=True, timeout=5,
)
local_sha = r.stdout.decode("utf-8", errors="replace").strip() if r.returncode == 0 else ""
local_sha = r.stdout.strip() if r.returncode == 0 else ""
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
local_sha = ""
if local_sha and local_sha != head:
@@ -2080,7 +2056,10 @@ def handle_stop_hook(input_data):
})
sys.exit(0)
_SDK_BOOTSTRAP_THROTTLE = os.path.join(_resolve_state_dir(), ".sdk_bootstrap_spawned")
_SDK_BOOTSTRAP_THROTTLE = os.path.join(
os.environ.get("SECURITY_WARNINGS_STATE_DIR")
or os.path.expanduser("~/.claude/security"),
".sdk_bootstrap_spawned")
def _maybe_bootstrap_agent_sdk_async():
"""Fire-and-forget SDK bootstrap, for remote-pod environments.

View File

@@ -19,7 +19,7 @@ import os
import re
from datetime import datetime
from _base import debug_log, state_dir as _state_dir
from _base import debug_log
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 = _state_dir()
state_dir = os.environ.get("SECURITY_WARNINGS_STATE_DIR", os.path.expanduser("~/.claude/security"))
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 = _state_dir()
state_dir = os.environ.get("SECURITY_WARNINGS_STATE_DIR", os.path.expanduser("~/.claude/security"))
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 = _state_dir()
state_dir = os.environ.get("SECURITY_WARNINGS_STATE_DIR", os.path.expanduser("~/.claude/security"))
if not os.path.exists(state_dir):
return

View File

@@ -22,17 +22,6 @@
# "${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