claude-crap 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/CHANGELOG.md +308 -0
  2. package/LICENSE +21 -0
  3. package/README.md +550 -0
  4. package/bin/claude-crap.mjs +141 -0
  5. package/dist/adapters/bandit.d.ts +48 -0
  6. package/dist/adapters/bandit.d.ts.map +1 -0
  7. package/dist/adapters/bandit.js +145 -0
  8. package/dist/adapters/bandit.js.map +1 -0
  9. package/dist/adapters/common.d.ts +73 -0
  10. package/dist/adapters/common.d.ts.map +1 -0
  11. package/dist/adapters/common.js +78 -0
  12. package/dist/adapters/common.js.map +1 -0
  13. package/dist/adapters/eslint.d.ts +52 -0
  14. package/dist/adapters/eslint.d.ts.map +1 -0
  15. package/dist/adapters/eslint.js +142 -0
  16. package/dist/adapters/eslint.js.map +1 -0
  17. package/dist/adapters/index.d.ts +47 -0
  18. package/dist/adapters/index.d.ts.map +1 -0
  19. package/dist/adapters/index.js +64 -0
  20. package/dist/adapters/index.js.map +1 -0
  21. package/dist/adapters/semgrep.d.ts +30 -0
  22. package/dist/adapters/semgrep.d.ts.map +1 -0
  23. package/dist/adapters/semgrep.js +130 -0
  24. package/dist/adapters/semgrep.js.map +1 -0
  25. package/dist/adapters/stryker.d.ts +55 -0
  26. package/dist/adapters/stryker.d.ts.map +1 -0
  27. package/dist/adapters/stryker.js +165 -0
  28. package/dist/adapters/stryker.js.map +1 -0
  29. package/dist/ast/cyclomatic.d.ts +48 -0
  30. package/dist/ast/cyclomatic.d.ts.map +1 -0
  31. package/dist/ast/cyclomatic.js +106 -0
  32. package/dist/ast/cyclomatic.js.map +1 -0
  33. package/dist/ast/index.d.ts +26 -0
  34. package/dist/ast/index.d.ts.map +1 -0
  35. package/dist/ast/index.js +23 -0
  36. package/dist/ast/index.js.map +1 -0
  37. package/dist/ast/language-config.d.ts +70 -0
  38. package/dist/ast/language-config.d.ts.map +1 -0
  39. package/dist/ast/language-config.js +192 -0
  40. package/dist/ast/language-config.js.map +1 -0
  41. package/dist/ast/tree-sitter-engine.d.ts +133 -0
  42. package/dist/ast/tree-sitter-engine.d.ts.map +1 -0
  43. package/dist/ast/tree-sitter-engine.js +270 -0
  44. package/dist/ast/tree-sitter-engine.js.map +1 -0
  45. package/dist/config.d.ts +57 -0
  46. package/dist/config.d.ts.map +1 -0
  47. package/dist/config.js +78 -0
  48. package/dist/config.js.map +1 -0
  49. package/dist/crap-config.d.ts +97 -0
  50. package/dist/crap-config.d.ts.map +1 -0
  51. package/dist/crap-config.js +144 -0
  52. package/dist/crap-config.js.map +1 -0
  53. package/dist/dashboard/server.d.ts +65 -0
  54. package/dist/dashboard/server.d.ts.map +1 -0
  55. package/dist/dashboard/server.js +147 -0
  56. package/dist/dashboard/server.js.map +1 -0
  57. package/dist/index.d.ts +32 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +574 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/metrics/crap.d.ts +71 -0
  62. package/dist/metrics/crap.d.ts.map +1 -0
  63. package/dist/metrics/crap.js +67 -0
  64. package/dist/metrics/crap.js.map +1 -0
  65. package/dist/metrics/index.d.ts +31 -0
  66. package/dist/metrics/index.d.ts.map +1 -0
  67. package/dist/metrics/index.js +27 -0
  68. package/dist/metrics/index.js.map +1 -0
  69. package/dist/metrics/score.d.ts +143 -0
  70. package/dist/metrics/score.d.ts.map +1 -0
  71. package/dist/metrics/score.js +224 -0
  72. package/dist/metrics/score.js.map +1 -0
  73. package/dist/metrics/tdr.d.ts +106 -0
  74. package/dist/metrics/tdr.d.ts.map +1 -0
  75. package/dist/metrics/tdr.js +117 -0
  76. package/dist/metrics/tdr.js.map +1 -0
  77. package/dist/metrics/workspace-walker.d.ts +43 -0
  78. package/dist/metrics/workspace-walker.d.ts.map +1 -0
  79. package/dist/metrics/workspace-walker.js +137 -0
  80. package/dist/metrics/workspace-walker.js.map +1 -0
  81. package/dist/sarif/index.d.ts +21 -0
  82. package/dist/sarif/index.d.ts.map +1 -0
  83. package/dist/sarif/index.js +19 -0
  84. package/dist/sarif/index.js.map +1 -0
  85. package/dist/sarif/sarif-builder.d.ts +128 -0
  86. package/dist/sarif/sarif-builder.d.ts.map +1 -0
  87. package/dist/sarif/sarif-builder.js +79 -0
  88. package/dist/sarif/sarif-builder.js.map +1 -0
  89. package/dist/sarif/sarif-store.d.ts +205 -0
  90. package/dist/sarif/sarif-store.d.ts.map +1 -0
  91. package/dist/sarif/sarif-store.js +246 -0
  92. package/dist/sarif/sarif-store.js.map +1 -0
  93. package/dist/sarif/sarif-validator.d.ts +45 -0
  94. package/dist/sarif/sarif-validator.d.ts.map +1 -0
  95. package/dist/sarif/sarif-validator.js +138 -0
  96. package/dist/sarif/sarif-validator.js.map +1 -0
  97. package/dist/schemas/tool-schemas.d.ts +216 -0
  98. package/dist/schemas/tool-schemas.d.ts.map +1 -0
  99. package/dist/schemas/tool-schemas.js +208 -0
  100. package/dist/schemas/tool-schemas.js.map +1 -0
  101. package/dist/sdk.d.ts +45 -0
  102. package/dist/sdk.d.ts.map +1 -0
  103. package/dist/sdk.js +44 -0
  104. package/dist/sdk.js.map +1 -0
  105. package/dist/tools/index.d.ts +24 -0
  106. package/dist/tools/index.d.ts.map +1 -0
  107. package/dist/tools/index.js +23 -0
  108. package/dist/tools/index.js.map +1 -0
  109. package/dist/tools/test-harness.d.ts +75 -0
  110. package/dist/tools/test-harness.d.ts.map +1 -0
  111. package/dist/tools/test-harness.js +137 -0
  112. package/dist/tools/test-harness.js.map +1 -0
  113. package/dist/workspace-guard.d.ts +53 -0
  114. package/dist/workspace-guard.d.ts.map +1 -0
  115. package/dist/workspace-guard.js +61 -0
  116. package/dist/workspace-guard.js.map +1 -0
  117. package/package.json +133 -0
  118. package/plugin/.claude-plugin/plugin.json +29 -0
  119. package/plugin/.mcp.json +18 -0
  120. package/plugin/CLAUDE.md +143 -0
  121. package/plugin/bundle/dashboard/public/index.html +368 -0
  122. package/plugin/bundle/dashboard/public/vendor/vue.global.prod.js +9 -0
  123. package/plugin/bundle/mcp-server.mjs +8718 -0
  124. package/plugin/bundle/mcp-server.mjs.map +7 -0
  125. package/plugin/bundle/tdr-engine.mjs +50 -0
  126. package/plugin/bundle/tdr-engine.mjs.map +7 -0
  127. package/plugin/hooks/hooks.json +62 -0
  128. package/plugin/hooks/lib/crap-config.mjs +152 -0
  129. package/plugin/hooks/lib/gatekeeper-rules.mjs +257 -0
  130. package/plugin/hooks/lib/hook-io.mjs +151 -0
  131. package/plugin/hooks/lib/quality-gate.mjs +329 -0
  132. package/plugin/hooks/lib/test-harness.mjs +152 -0
  133. package/plugin/hooks/post-tool-use.mjs +245 -0
  134. package/plugin/hooks/pre-tool-use.mjs +290 -0
  135. package/plugin/hooks/session-start.mjs +109 -0
  136. package/plugin/hooks/stop-quality-gate.mjs +226 -0
  137. package/plugin/package.json +18 -0
  138. package/plugin/skills/adopt/SKILL.md +74 -0
  139. package/plugin/skills/analyze/SKILL.md +77 -0
  140. package/plugin/skills/check-test/SKILL.md +50 -0
  141. package/plugin/skills/score/SKILL.md +31 -0
  142. package/scripts/bug-report.mjs +328 -0
  143. package/scripts/build-fast.mjs +130 -0
  144. package/scripts/bundle-plugin.mjs +74 -0
  145. package/scripts/doctor.mjs +320 -0
  146. package/scripts/install.mjs +192 -0
  147. package/scripts/lib/cli-ui.mjs +122 -0
  148. package/scripts/postinstall.mjs +127 -0
  149. package/scripts/run-tests.mjs +95 -0
  150. package/scripts/status.mjs +110 -0
  151. package/scripts/uninstall.mjs +72 -0
  152. package/src/adapters/bandit.ts +191 -0
  153. package/src/adapters/common.ts +133 -0
  154. package/src/adapters/eslint.ts +187 -0
  155. package/src/adapters/index.ts +78 -0
  156. package/src/adapters/semgrep.ts +150 -0
  157. package/src/adapters/stryker.ts +218 -0
  158. package/src/ast/cyclomatic.ts +131 -0
  159. package/src/ast/index.ts +33 -0
  160. package/src/ast/language-config.ts +231 -0
  161. package/src/ast/tree-sitter-engine.ts +385 -0
  162. package/src/config.ts +109 -0
  163. package/src/crap-config.ts +196 -0
  164. package/src/dashboard/public/index.html +368 -0
  165. package/src/dashboard/public/vendor/vue.global.prod.js +9 -0
  166. package/src/dashboard/server.ts +205 -0
  167. package/src/index.ts +696 -0
  168. package/src/metrics/crap.ts +101 -0
  169. package/src/metrics/index.ts +51 -0
  170. package/src/metrics/score.ts +329 -0
  171. package/src/metrics/tdr.ts +155 -0
  172. package/src/metrics/workspace-walker.ts +146 -0
  173. package/src/sarif/index.ts +31 -0
  174. package/src/sarif/sarif-builder.ts +139 -0
  175. package/src/sarif/sarif-store.ts +347 -0
  176. package/src/sarif/sarif-validator.ts +145 -0
  177. package/src/schemas/tool-schemas.ts +225 -0
  178. package/src/sdk.ts +110 -0
  179. package/src/tests/adapters/bandit.test.ts +111 -0
  180. package/src/tests/adapters/dispatch.test.ts +100 -0
  181. package/src/tests/adapters/eslint.test.ts +138 -0
  182. package/src/tests/adapters/semgrep.test.ts +125 -0
  183. package/src/tests/adapters/stryker.test.ts +103 -0
  184. package/src/tests/crap-config.test.ts +228 -0
  185. package/src/tests/crap.test.ts +59 -0
  186. package/src/tests/cyclomatic.test.ts +87 -0
  187. package/src/tests/dashboard-http.test.ts +108 -0
  188. package/src/tests/dashboard-integrity.test.ts +128 -0
  189. package/src/tests/integration/mcp-server.integration.test.ts +352 -0
  190. package/src/tests/pre-tool-use-hook.test.ts +178 -0
  191. package/src/tests/sarif-store.test.ts +241 -0
  192. package/src/tests/sarif-validator.test.ts +164 -0
  193. package/src/tests/score.test.ts +260 -0
  194. package/src/tests/skills-frontmatter.test.ts +172 -0
  195. package/src/tests/stop-quality-gate-strictness.test.ts +243 -0
  196. package/src/tests/tdr.test.ts +86 -0
  197. package/src/tests/test-harness.test.ts +153 -0
  198. package/src/tests/workspace-guard.test.ts +111 -0
  199. package/src/tools/index.ts +24 -0
  200. package/src/tools/test-harness.ts +158 -0
  201. package/src/workspace-guard.ts +64 -0
  202. package/tsconfig.json +27 -0
@@ -0,0 +1,50 @@
1
+ // Generated by scripts/bundle-plugin.mjs — DO NOT EDIT
2
+
3
+ // src/metrics/tdr.ts
4
+ var RATING_ORDER = ["A", "B", "C", "D", "E"];
5
+ function ratingToRank(rating) {
6
+ return RATING_ORDER.indexOf(rating);
7
+ }
8
+ function ratingIsWorseThan(actual, limit) {
9
+ return ratingToRank(actual) > ratingToRank(limit);
10
+ }
11
+ function classifyTdr(percent) {
12
+ if (!Number.isFinite(percent) || percent < 0) {
13
+ throw new Error(`[tdr] percent is invalid: ${percent}`);
14
+ }
15
+ if (percent <= 5) return "A";
16
+ if (percent <= 10) return "B";
17
+ if (percent <= 20) return "C";
18
+ if (percent <= 50) return "D";
19
+ return "E";
20
+ }
21
+ function computeTdr(input) {
22
+ if (input.totalLinesOfCode <= 0) {
23
+ throw new Error(`[tdr] totalLinesOfCode must be > 0, got ${input.totalLinesOfCode}`);
24
+ }
25
+ if (input.minutesPerLoc <= 0) {
26
+ throw new Error(`[tdr] minutesPerLoc must be > 0, got ${input.minutesPerLoc}`);
27
+ }
28
+ if (input.remediationMinutes < 0) {
29
+ throw new Error(`[tdr] remediationMinutes must be \u2265 0, got ${input.remediationMinutes}`);
30
+ }
31
+ const developmentCostMinutes = input.minutesPerLoc * input.totalLinesOfCode;
32
+ const ratio = input.remediationMinutes / developmentCostMinutes;
33
+ const percent = ratio * 100;
34
+ const rating = classifyTdr(percent);
35
+ return {
36
+ ratio: Number(ratio.toFixed(6)),
37
+ percent: Number(percent.toFixed(4)),
38
+ rating,
39
+ remediationMinutes: input.remediationMinutes,
40
+ totalLinesOfCode: input.totalLinesOfCode,
41
+ developmentCostMinutes
42
+ };
43
+ }
44
+ export {
45
+ classifyTdr,
46
+ computeTdr,
47
+ ratingIsWorseThan,
48
+ ratingToRank
49
+ };
50
+ //# sourceMappingURL=tdr-engine.mjs.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/metrics/tdr.ts"],
4
+ "sourcesContent": ["/**\n * Technical Debt Ratio (TDR) \u2014 deterministic computation and rating.\n *\n * The Technical Debt Ratio expresses how expensive it would be to remediate\n * all known issues in a scope, relative to how much it would have cost to\n * write the code in the first place. Formally (see docs/quality-gate.md):\n *\n * TDR = remediationCost / (costPerLine \u00D7 totalLinesOfCode)\n *\n * Where the remediation cost is the sum (in minutes) of every linter /\n * scanner / mutator finding's individual estimated effort, and the per-line\n * cost is assumed to be a constant `minutesPerLoc` (industry default: 30\n * minutes per line of code, including design, writing and review).\n *\n * The resulting ratio is converted to a percentage and mapped to a letter\n * grade A..E. The thresholds are strict and non-negotiable:\n *\n * | Rating | TDR % | Meaning |\n * |--------|--------------|-------------------------------------------|\n * | A | 0..5% | Excellent \u2014 remediation cost is noise |\n * | B | >5..10% | Low risk |\n * | C | >10..20% | Moderate, watch closely |\n * | D | >20..50% | Critical, remediation plan required |\n * | E | >50% | Unmaintainable \u2014 halt feature work |\n *\n * Rating E always halts the workflow at the Stop quality gate, regardless\n * of the configured `TDR_MAX_RATING` tolerance.\n *\n * @module metrics/tdr\n */\n\nimport type { MaintainabilityRating } from \"../config.js\";\n\n/**\n * Inputs required to compute a Technical Debt Ratio over any scope\n * (project, module, or file).\n */\nexport interface TdrInput {\n /** Sum of all finding remediation estimates, in minutes. Must be \u2265 0. */\n readonly remediationMinutes: number;\n /** Total lines of code in the scope. Must be > 0 (division denominator). */\n readonly totalLinesOfCode: number;\n /** Assumed development cost per LOC, in minutes. Must be > 0. */\n readonly minutesPerLoc: number;\n}\n\n/**\n * Result of a TDR computation with both the raw ratio and the letter grade.\n */\nexport interface TdrResult {\n /** Raw ratio (remediation / development), rounded to 6 decimals. */\n readonly ratio: number;\n /** Same ratio expressed as a percentage, rounded to 4 decimals. */\n readonly percent: number;\n /** Letter grade derived from `percent` via {@link classifyTdr}. */\n readonly rating: MaintainabilityRating;\n /** Remediation input, echoed for traceability. */\n readonly remediationMinutes: number;\n /** LOC input, echoed for traceability. */\n readonly totalLinesOfCode: number;\n /** Computed `minutesPerLoc \u00D7 totalLinesOfCode`, useful for the dashboard. */\n readonly developmentCostMinutes: number;\n}\n\n/** Canonical ordering used by {@link ratingToRank}. */\nconst RATING_ORDER: ReadonlyArray<MaintainabilityRating> = [\"A\", \"B\", \"C\", \"D\", \"E\"];\n\n/**\n * Convert a letter rating to its numeric rank (A=0, E=4). Useful when\n * comparing two ratings without relying on lexical order.\n *\n * @param rating The rating letter.\n * @returns Its rank in `[0, 4]`.\n */\nexport function ratingToRank(rating: MaintainabilityRating): number {\n return RATING_ORDER.indexOf(rating);\n}\n\n/**\n * Return `true` when `actual` is strictly worse than `limit`, false otherwise.\n * Used by the Stop quality gate to decide whether to block task completion.\n *\n * @param actual Rating currently achieved by the project.\n * @param limit Maximum tolerated rating (worst allowed).\n * @returns `true` if `actual` should trigger a block.\n *\n * @example\n * ratingIsWorseThan(\"D\", \"C\") // \u2192 true\n * ratingIsWorseThan(\"B\", \"C\") // \u2192 false\n * ratingIsWorseThan(\"C\", \"C\") // \u2192 false (equal, not worse)\n */\nexport function ratingIsWorseThan(\n actual: MaintainabilityRating,\n limit: MaintainabilityRating,\n): boolean {\n return ratingToRank(actual) > ratingToRank(limit);\n}\n\n/**\n * Map a TDR percentage to its letter rating. The boundaries are inclusive\n * on the upper end (5% is still an A, 10% is still a B, etc.).\n *\n * @param percent TDR expressed as a percentage. Must be \u2265 0.\n * @returns Letter rating A..E.\n * @throws When `percent` is negative or not finite.\n */\nexport function classifyTdr(percent: number): MaintainabilityRating {\n if (!Number.isFinite(percent) || percent < 0) {\n throw new Error(`[tdr] percent is invalid: ${percent}`);\n }\n if (percent <= 5) return \"A\";\n if (percent <= 10) return \"B\";\n if (percent <= 20) return \"C\";\n if (percent <= 50) return \"D\";\n return \"E\";\n}\n\n/**\n * Compute the Technical Debt Ratio for a scope and return the full result.\n * This function is pure and deterministic.\n *\n * @param input Remediation minutes, total LOC and the cost-per-line assumption.\n * @returns A {@link TdrResult} ready to be serialized to SARIF properties.\n * @throws When any numeric input is out of range.\n *\n * @example\n * // 240 minutes of remediation across 500 LOC at 30 min/LOC\n * computeTdr({ remediationMinutes: 240, totalLinesOfCode: 500, minutesPerLoc: 30 })\n * // \u2192 { ratio: 0.016, percent: 1.6, rating: \"A\", ... }\n */\nexport function computeTdr(input: TdrInput): TdrResult {\n if (input.totalLinesOfCode <= 0) {\n throw new Error(`[tdr] totalLinesOfCode must be > 0, got ${input.totalLinesOfCode}`);\n }\n if (input.minutesPerLoc <= 0) {\n throw new Error(`[tdr] minutesPerLoc must be > 0, got ${input.minutesPerLoc}`);\n }\n if (input.remediationMinutes < 0) {\n throw new Error(`[tdr] remediationMinutes must be \u2265 0, got ${input.remediationMinutes}`);\n }\n\n const developmentCostMinutes = input.minutesPerLoc * input.totalLinesOfCode;\n const ratio = input.remediationMinutes / developmentCostMinutes;\n const percent = ratio * 100;\n const rating = classifyTdr(percent);\n\n return {\n ratio: Number(ratio.toFixed(6)),\n percent: Number(percent.toFixed(4)),\n rating,\n remediationMinutes: input.remediationMinutes,\n totalLinesOfCode: input.totalLinesOfCode,\n developmentCostMinutes,\n };\n}\n"],
5
+ "mappings": ";;;AAiEA,IAAM,eAAqD,CAAC,KAAK,KAAK,KAAK,KAAK,GAAG;AAS5E,SAAS,aAAa,QAAuC;AAClE,SAAO,aAAa,QAAQ,MAAM;AACpC;AAeO,SAAS,kBACd,QACA,OACS;AACT,SAAO,aAAa,MAAM,IAAI,aAAa,KAAK;AAClD;AAUO,SAAS,YAAY,SAAwC;AAClE,MAAI,CAAC,OAAO,SAAS,OAAO,KAAK,UAAU,GAAG;AAC5C,UAAM,IAAI,MAAM,6BAA6B,OAAO,EAAE;AAAA,EACxD;AACA,MAAI,WAAW,EAAG,QAAO;AACzB,MAAI,WAAW,GAAI,QAAO;AAC1B,MAAI,WAAW,GAAI,QAAO;AAC1B,MAAI,WAAW,GAAI,QAAO;AAC1B,SAAO;AACT;AAeO,SAAS,WAAW,OAA4B;AACrD,MAAI,MAAM,oBAAoB,GAAG;AAC/B,UAAM,IAAI,MAAM,2CAA2C,MAAM,gBAAgB,EAAE;AAAA,EACrF;AACA,MAAI,MAAM,iBAAiB,GAAG;AAC5B,UAAM,IAAI,MAAM,wCAAwC,MAAM,aAAa,EAAE;AAAA,EAC/E;AACA,MAAI,MAAM,qBAAqB,GAAG;AAChC,UAAM,IAAI,MAAM,kDAA6C,MAAM,kBAAkB,EAAE;AAAA,EACzF;AAEA,QAAM,yBAAyB,MAAM,gBAAgB,MAAM;AAC3D,QAAM,QAAQ,MAAM,qBAAqB;AACzC,QAAM,UAAU,QAAQ;AACxB,QAAM,SAAS,YAAY,OAAO;AAElC,SAAO;AAAA,IACL,OAAO,OAAO,MAAM,QAAQ,CAAC,CAAC;AAAA,IAC9B,SAAS,OAAO,QAAQ,QAAQ,CAAC,CAAC;AAAA,IAClC;AAAA,IACA,oBAAoB,MAAM;AAAA,IAC1B,kBAAkB,MAAM;AAAA,IACxB;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "$schema": "https://code.claude.com/schemas/hooks.json",
3
+ "hooks": {
4
+ "PreToolUse": [
5
+ {
6
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit|Bash",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pre-tool-use.mjs",
11
+ "timeout": 15
12
+ }
13
+ ]
14
+ }
15
+ ],
16
+ "PostToolUse": [
17
+ {
18
+ "matcher": "Write|Edit|MultiEdit|NotebookEdit",
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/post-tool-use.mjs",
23
+ "timeout": 30
24
+ }
25
+ ]
26
+ }
27
+ ],
28
+ "Stop": [
29
+ {
30
+ "hooks": [
31
+ {
32
+ "type": "command",
33
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/stop-quality-gate.mjs",
34
+ "timeout": 120
35
+ }
36
+ ]
37
+ }
38
+ ],
39
+ "SubagentStop": [
40
+ {
41
+ "hooks": [
42
+ {
43
+ "type": "command",
44
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/stop-quality-gate.mjs",
45
+ "timeout": 120
46
+ }
47
+ ]
48
+ }
49
+ ],
50
+ "SessionStart": [
51
+ {
52
+ "hooks": [
53
+ {
54
+ "type": "command",
55
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs",
56
+ "timeout": 10
57
+ }
58
+ ]
59
+ }
60
+ ]
61
+ }
62
+ }
@@ -0,0 +1,152 @@
1
+ // @ts-check
2
+ /**
3
+ * Workspace-level sonar configuration loader (JS twin).
4
+ *
5
+ * This is the hook-side copy of `src/crap-config.ts`. Hooks live
6
+ * outside the TypeScript `rootDir` and cannot import the compiled
7
+ * `dist/` engine at the top of the module graph (the hook needs to
8
+ * work even when `dist/` is stale or missing), so we keep a
9
+ * zero-dependency JS twin that implements the same algorithm.
10
+ *
11
+ * See `src/crap-config.ts` for the full rationale. The two copies
12
+ * are validated against the same behavior table in
13
+ * `src/tests/crap-config.test.ts` and in
14
+ * `src/tests/stop-quality-gate-strictness.test.ts`.
15
+ *
16
+ * Resolution order (most specific wins):
17
+ *
18
+ * 1. `CLAUDE_CRAP_STRICTNESS` environment variable
19
+ * 2. `.claude-crap.json` at the workspace root
20
+ * 3. Hardcoded default `"strict"`
21
+ *
22
+ * @module hooks/lib/crap-config
23
+ */
24
+
25
+ import { readFileSync } from "node:fs";
26
+ import { join } from "node:path";
27
+
28
+ /**
29
+ * @typedef {"strict" | "warn" | "advisory"} Strictness
30
+ */
31
+
32
+ /**
33
+ * Exhaustive list of valid strictness values. Keep in sync with the
34
+ * TypeScript twin at `src/crap-config.ts`.
35
+ */
36
+ export const STRICTNESS_VALUES = Object.freeze(["strict", "warn", "advisory"]);
37
+
38
+ /**
39
+ * Default strictness when neither the env var nor the file provides
40
+ * a value. `"strict"` preserves the hard-failing Stop gate as the
41
+ * out-of-the-box experience.
42
+ *
43
+ * @type {Strictness}
44
+ */
45
+ export const DEFAULT_STRICTNESS = "strict";
46
+
47
+ /**
48
+ * Error thrown by {@link loadCrapConfig} when the configuration is
49
+ * rejected. Hook callers catch this and fall back to the default so
50
+ * a busted config file never deadlocks the user.
51
+ */
52
+ export class CrapConfigError extends Error {
53
+ /** @param {string} message */
54
+ constructor(message) {
55
+ super(message);
56
+ this.name = "CrapConfigError";
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Resolve the effective sonar configuration for a given workspace.
62
+ * Pure function except for the one synchronous file read on
63
+ * `<workspaceRoot>/.claude-crap.json` and the single env lookup.
64
+ *
65
+ * @param {{ workspaceRoot: string }} options
66
+ * @returns {{ strictness: Strictness, strictnessSource: "env" | "file" | "default" }}
67
+ * @throws {CrapConfigError} on any invalid input.
68
+ */
69
+ export function loadCrapConfig(options) {
70
+ const envRaw = process.env["CLAUDE_CRAP_STRICTNESS"];
71
+ if (typeof envRaw === "string" && envRaw.trim() !== "") {
72
+ const normalized = envRaw.trim().toLowerCase();
73
+ if (!isStrictness(normalized)) {
74
+ throw new CrapConfigError(
75
+ `[crap-config] CLAUDE_CRAP_STRICTNESS="${envRaw}" is not a valid strictness. ` +
76
+ `Expected one of: ${STRICTNESS_VALUES.join(", ")}.`,
77
+ );
78
+ }
79
+ return { strictness: normalized, strictnessSource: "env" };
80
+ }
81
+
82
+ const fromFile = readFromFile(options.workspaceRoot);
83
+ if (fromFile) return { strictness: fromFile, strictnessSource: "file" };
84
+
85
+ return { strictness: DEFAULT_STRICTNESS, strictnessSource: "default" };
86
+ }
87
+
88
+ /**
89
+ * Attempt to read and validate `.claude-crap.json` at the workspace
90
+ * root. Returns `null` when the file is missing (the common case for
91
+ * fresh installs). Throws {@link CrapConfigError} on malformed JSON,
92
+ * a non-object root, a wrong-type `strictness` field, or an unknown
93
+ * enum value — so the caller cannot silently drop into the default
94
+ * on a typo.
95
+ *
96
+ * @param {string} workspaceRoot
97
+ * @returns {Strictness | null}
98
+ */
99
+ function readFromFile(workspaceRoot) {
100
+ const filePath = join(workspaceRoot, ".claude-crap.json");
101
+ let raw;
102
+ try {
103
+ raw = readFileSync(filePath, "utf8");
104
+ } catch (err) {
105
+ const error = /** @type {NodeJS.ErrnoException} */ (err);
106
+ if (error.code === "ENOENT") return null;
107
+ throw new CrapConfigError(
108
+ `[crap-config] Failed to read ${filePath}: ${error.message}`,
109
+ );
110
+ }
111
+
112
+ let parsed;
113
+ try {
114
+ parsed = JSON.parse(raw);
115
+ } catch (err) {
116
+ throw new CrapConfigError(
117
+ `[crap-config] ${filePath} is not valid JSON: ${/** @type {Error} */ (err).message}`,
118
+ );
119
+ }
120
+
121
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
122
+ throw new CrapConfigError(
123
+ `[crap-config] ${filePath} must be a JSON object at the top level`,
124
+ );
125
+ }
126
+ if (!("strictness" in parsed)) return null;
127
+
128
+ const value = parsed.strictness;
129
+ if (typeof value !== "string") {
130
+ throw new CrapConfigError(
131
+ `[crap-config] ${filePath}: 'strictness' must be a string, got ${typeof value}`,
132
+ );
133
+ }
134
+ const normalized = value.trim().toLowerCase();
135
+ if (!isStrictness(normalized)) {
136
+ throw new CrapConfigError(
137
+ `[crap-config] ${filePath}: 'strictness' is "${value}"; ` +
138
+ `expected one of ${STRICTNESS_VALUES.join(", ")}.`,
139
+ );
140
+ }
141
+ return /** @type {Strictness} */ (normalized);
142
+ }
143
+
144
+ /**
145
+ * Runtime type guard for the Strictness union.
146
+ *
147
+ * @param {string} value
148
+ * @returns {value is Strictness}
149
+ */
150
+ function isStrictness(value) {
151
+ return STRICTNESS_VALUES.includes(value);
152
+ }
@@ -0,0 +1,257 @@
1
+ // @ts-check
2
+ /**
3
+ * Deterministic prophylactic rules for the claude-crap PreToolUse gatekeeper.
4
+ *
5
+ * Each rule is a pure function (input → verdict). Rules never perform I/O:
6
+ * rules that would need a deep analysis instead trigger an MCP tool call
7
+ * from a later hook (PostToolUse or Stop). The PreToolUse hook itself must
8
+ * respond within Claude Code's 15-second timeout window — anything that
9
+ * could block for longer than a few hundred milliseconds belongs elsewhere.
10
+ *
11
+ * Each rule returns a verdict of the shape:
12
+ *
13
+ * { blocked: boolean, ruleId: string, reason: string }
14
+ *
15
+ * When `blocked === true`, the hook will exit with code 2 and write the
16
+ * `reason` text to stderr. Claude Code forwards stderr from a blocking
17
+ * hook straight into the agent's context window, so the reason text is
18
+ * effectively a prompt to the LLM — it must be imperative and corrective.
19
+ *
20
+ * All reason strings are in English because they are injected into the
21
+ * agent's context and the plugin is distributed publicly.
22
+ *
23
+ * @module hooks/lib/gatekeeper-rules
24
+ */
25
+
26
+ /**
27
+ * Minimal shape of the JSON payload Claude Code sends on stdin to a hook.
28
+ * See https://code.claude.com/docs/en/hooks for the full spec.
29
+ *
30
+ * @typedef {Object} HookInput
31
+ * @property {string} [session_id] - Current session identifier.
32
+ * @property {string} [transcript_path] - Path to the conversation transcript.
33
+ * @property {string} [hook_event_name] - "PreToolUse" for this hook.
34
+ * @property {string} tool_name - Name of the tool about to be invoked.
35
+ * @property {Record<string, unknown>} tool_input - Raw arguments proposed by the LLM.
36
+ */
37
+
38
+ /**
39
+ * Verdict returned by each rule. Rules that do not trigger return `null`
40
+ * instead of a verdict — the runner uses that signal to short-circuit.
41
+ *
42
+ * @typedef {Object} Verdict
43
+ * @property {boolean} blocked - `true` if the hook should abort the tool call.
44
+ * @property {string} ruleId - Stable identifier for this rule (SONAR-XXX-NNN).
45
+ * @property {string} reason - Imperative, corrective message for the LLM.
46
+ */
47
+
48
+ /**
49
+ * Default blocklist regex for sensitive paths. This is replaced at runtime
50
+ * by whatever the user configured via `CLAUDE_PLUGIN_OPTION_BLOCKED_PATH_PATTERNS`.
51
+ */
52
+ const DEFAULT_BLOCKED_PATH_REGEX =
53
+ /(^|\/)(\.git|\.env|\.env\..*|node_modules|\.venv|secrets?|credentials?|id_rsa|\.ssh)(\/|$)/i;
54
+
55
+ /**
56
+ * Heuristic signatures of secrets that should never be committed to source.
57
+ * This list is intentionally conservative — the gatekeeper is a speed bump,
58
+ * not a replacement for a real secret scanner. Deeper detection runs in
59
+ * PostToolUse via the MCP `ingest_sarif` tool.
60
+ */
61
+ const HARDCODED_SECRET_PATTERNS = [
62
+ { id: "SEC-AWS", re: /AKIA[0-9A-Z]{16}/ },
63
+ { id: "SEC-PRIVKEY", re: /-----BEGIN (RSA |OPENSSH |EC |DSA |PGP )?PRIVATE KEY-----/ },
64
+ { id: "SEC-SLACKTOKEN", re: /xox[baprs]-[0-9A-Za-z-]{10,}/ },
65
+ { id: "SEC-GHTOKEN", re: /gh[pousr]_[A-Za-z0-9]{36,}/ },
66
+ { id: "SEC-JWT", re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
67
+ ];
68
+
69
+ /**
70
+ * Destructive shell patterns. These commands can silently destroy work,
71
+ * overwrite published git history, or execute remote code without review.
72
+ * The gatekeeper refuses them outright — if the user really needs to run
73
+ * one of these, they should do it from their own terminal.
74
+ */
75
+ const DESTRUCTIVE_BASH_PATTERNS = [
76
+ { id: "BASH-RMROOT", re: /\brm\s+(-[frR]+\s+|--force\s+|--recursive\s+).*(\s|^)\/($|\s)/ },
77
+ { id: "BASH-RMHOME", re: /\brm\s+-[frR]+\s+.*\$HOME\b/ },
78
+ { id: "BASH-DD", re: /\bdd\s+.*of=\/dev\/(sd|nvme|disk)/ },
79
+ { id: "BASH-GITFORCE", re: /\bgit\s+push\s+.*--force(?!-with-lease)/ },
80
+ { id: "BASH-GITRESET", re: /\bgit\s+reset\s+--hard\s+origin/ },
81
+ { id: "BASH-CURLSUDO", re: /\bcurl\s+[^|]*\|\s*(sudo\s+)?(bash|sh|zsh|fish)/ },
82
+ ];
83
+
84
+ /**
85
+ * Compile the user-configured blocked-path regex. Falls back to the default
86
+ * pattern if the configured value is missing or malformed — we never let a
87
+ * broken regex disable the gatekeeper entirely.
88
+ *
89
+ * @param {string | undefined} value Raw regex source from the environment.
90
+ * @returns {RegExp} A compiled, case-insensitive regex.
91
+ */
92
+ function compileBlockedPathRegex(value) {
93
+ if (!value) return DEFAULT_BLOCKED_PATH_REGEX;
94
+ try {
95
+ return new RegExp(value, "i");
96
+ } catch {
97
+ return DEFAULT_BLOCKED_PATH_REGEX;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Rule 1 — blocked destination path.
103
+ *
104
+ * Fires when the LLM tries to Write, Edit or NotebookEdit any file that
105
+ * matches the blocklist regex. This is the first line of defense against
106
+ * accidental edits to `.env`, `.git`, `node_modules`, SSH keys, etc.
107
+ *
108
+ * @param {HookInput} input Parsed hook payload.
109
+ * @returns {Verdict | null} A blocking verdict, or `null` to pass.
110
+ */
111
+ export function checkBlockedPath(input) {
112
+ const filePath =
113
+ typeof input.tool_input.file_path === "string"
114
+ ? input.tool_input.file_path
115
+ : typeof input.tool_input.notebook_path === "string"
116
+ ? input.tool_input.notebook_path
117
+ : undefined;
118
+ if (!filePath) return null;
119
+
120
+ const regex = compileBlockedPathRegex(process.env.CLAUDE_PLUGIN_OPTION_BLOCKED_PATH_PATTERNS);
121
+ if (regex.test(filePath)) {
122
+ return {
123
+ blocked: true,
124
+ ruleId: "SONAR-PATH-001",
125
+ reason:
126
+ `Path '${filePath}' matches BLOCKED_PATH_PATTERNS. ` +
127
+ `claude-crap refuses to write or edit sensitive paths such as secrets, .git, node_modules, or .env files. ` +
128
+ `Corrective action: pick a file outside those directories. If this change is legitimate, ` +
129
+ `ask the user to relax CLAUDE_PLUGIN_OPTION_BLOCKED_PATH_PATTERNS before retrying.`,
130
+ };
131
+ }
132
+ return null;
133
+ }
134
+
135
+ /**
136
+ * Rule 2 — hardcoded secrets in proposed content.
137
+ *
138
+ * Scans `content` (Write), `new_string` (Edit) and every element of
139
+ * `edits[]` (MultiEdit) for well-known secret signatures. Does NOT run
140
+ * full entropy analysis — that is the job of a secret scanner plugged in
141
+ * via PostToolUse / `ingest_sarif`.
142
+ *
143
+ * @param {HookInput} input Parsed hook payload.
144
+ * @returns {Verdict | null} A blocking verdict, or `null` to pass.
145
+ */
146
+ export function checkHardcodedSecrets(input) {
147
+ const candidates = [];
148
+ if (typeof input.tool_input.content === "string") {
149
+ candidates.push(input.tool_input.content);
150
+ }
151
+ if (typeof input.tool_input.new_string === "string") {
152
+ candidates.push(input.tool_input.new_string);
153
+ }
154
+ if (Array.isArray(input.tool_input.edits)) {
155
+ for (const edit of input.tool_input.edits) {
156
+ if (edit && typeof edit === "object" && typeof (/** @type {any} */ (edit).new_string) === "string") {
157
+ candidates.push(/** @type {any} */ (edit).new_string);
158
+ }
159
+ }
160
+ }
161
+ if (candidates.length === 0) return null;
162
+
163
+ for (const text of candidates) {
164
+ for (const pat of HARDCODED_SECRET_PATTERNS) {
165
+ if (pat.re.test(text)) {
166
+ return {
167
+ blocked: true,
168
+ ruleId: `SONAR-SEC-${pat.id}`,
169
+ reason:
170
+ `A likely hardcoded secret (${pat.id}) was detected in the proposed content. ` +
171
+ `Per the Golden Rule in CLAUDE.md, credentials must never be embedded in source code. ` +
172
+ `Corrective action: move the value to an environment variable or a managed secret; ` +
173
+ `do not commit tokens, private keys, or JWTs to the source tree under any circumstance.`,
174
+ };
175
+ }
176
+ }
177
+ }
178
+ return null;
179
+ }
180
+
181
+ /**
182
+ * Rule 3 — destructive Bash commands.
183
+ *
184
+ * Only fires when `tool_name === "Bash"`. Refuses commands that can
185
+ * recursively delete the workspace, overwrite published git history,
186
+ * write raw bytes to a block device, or pipe a remote script into a shell.
187
+ *
188
+ * @param {HookInput} input Parsed hook payload.
189
+ * @returns {Verdict | null} A blocking verdict, or `null` to pass.
190
+ */
191
+ export function checkDestructiveBash(input) {
192
+ if (input.tool_name !== "Bash") return null;
193
+ const command =
194
+ typeof input.tool_input.command === "string" ? input.tool_input.command : undefined;
195
+ if (!command) return null;
196
+
197
+ for (const pat of DESTRUCTIVE_BASH_PATTERNS) {
198
+ if (pat.re.test(command)) {
199
+ return {
200
+ blocked: true,
201
+ ruleId: `SONAR-BASH-${pat.id}`,
202
+ reason:
203
+ `The proposed Bash command matched the destructive pattern ${pat.id}: '${command}'. ` +
204
+ `claude-crap blocks operations that can wipe the project tree, rewrite published git history, ` +
205
+ `or execute remote code without review. ` +
206
+ `Corrective action: if this operation is truly intended, ask the user to confirm and run it ` +
207
+ `manually from their own terminal instead of through the agent.`,
208
+ };
209
+ }
210
+ }
211
+ return null;
212
+ }
213
+
214
+ /**
215
+ * Rule 4 — test harness presence (no-op in PreToolUse by design).
216
+ *
217
+ * The CLAUDE.md Golden Rule forbids writing functional code before a
218
+ * test safety net exists. Enforcing that strictly requires reading
219
+ * the workspace to check for an accompanying test file, which is too
220
+ * slow for the 15 s PreToolUse budget. The full check therefore runs
221
+ * in PostToolUse via the MCP `require_test_harness` tool — this rule
222
+ * stays in the pipeline purely as the registered slot for rule ID
223
+ * `SONAR-TEST-001`, so the rule count the hook reports on stdout
224
+ * stays stable and downstream consumers can correlate the slot with
225
+ * its PostToolUse counterpart.
226
+ *
227
+ * @param {HookInput} _input Parsed hook payload (unused; always returns null).
228
+ * @returns {Verdict | null} Always `null`; enforcement happens in PostToolUse.
229
+ */
230
+ export function checkTestHarnessPresence(_input) {
231
+ return null;
232
+ }
233
+
234
+ /**
235
+ * Run every rule in order, cheapest first, and return the first blocking
236
+ * verdict found. Returns `null` when the proposed action passes every rule.
237
+ *
238
+ * Ordering matters: path checks are nearly free, destructive-bash checks
239
+ * run a handful of regexes, and secret checks iterate a longer pattern
240
+ * list. Keeping cheap rules first minimizes the common-case latency.
241
+ *
242
+ * @param {HookInput} input Parsed hook payload.
243
+ * @returns {Verdict | null} First blocking verdict, or `null` to pass.
244
+ */
245
+ export function runAllRules(input) {
246
+ const rules = [
247
+ checkBlockedPath,
248
+ checkDestructiveBash,
249
+ checkHardcodedSecrets,
250
+ checkTestHarnessPresence,
251
+ ];
252
+ for (const rule of rules) {
253
+ const verdict = rule(input);
254
+ if (verdict && verdict.blocked) return verdict;
255
+ }
256
+ return null;
257
+ }