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.
- package/CHANGELOG.md +308 -0
- package/LICENSE +21 -0
- package/README.md +550 -0
- package/bin/claude-crap.mjs +141 -0
- package/dist/adapters/bandit.d.ts +48 -0
- package/dist/adapters/bandit.d.ts.map +1 -0
- package/dist/adapters/bandit.js +145 -0
- package/dist/adapters/bandit.js.map +1 -0
- package/dist/adapters/common.d.ts +73 -0
- package/dist/adapters/common.d.ts.map +1 -0
- package/dist/adapters/common.js +78 -0
- package/dist/adapters/common.js.map +1 -0
- package/dist/adapters/eslint.d.ts +52 -0
- package/dist/adapters/eslint.d.ts.map +1 -0
- package/dist/adapters/eslint.js +142 -0
- package/dist/adapters/eslint.js.map +1 -0
- package/dist/adapters/index.d.ts +47 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +64 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/semgrep.d.ts +30 -0
- package/dist/adapters/semgrep.d.ts.map +1 -0
- package/dist/adapters/semgrep.js +130 -0
- package/dist/adapters/semgrep.js.map +1 -0
- package/dist/adapters/stryker.d.ts +55 -0
- package/dist/adapters/stryker.d.ts.map +1 -0
- package/dist/adapters/stryker.js +165 -0
- package/dist/adapters/stryker.js.map +1 -0
- package/dist/ast/cyclomatic.d.ts +48 -0
- package/dist/ast/cyclomatic.d.ts.map +1 -0
- package/dist/ast/cyclomatic.js +106 -0
- package/dist/ast/cyclomatic.js.map +1 -0
- package/dist/ast/index.d.ts +26 -0
- package/dist/ast/index.d.ts.map +1 -0
- package/dist/ast/index.js +23 -0
- package/dist/ast/index.js.map +1 -0
- package/dist/ast/language-config.d.ts +70 -0
- package/dist/ast/language-config.d.ts.map +1 -0
- package/dist/ast/language-config.js +192 -0
- package/dist/ast/language-config.js.map +1 -0
- package/dist/ast/tree-sitter-engine.d.ts +133 -0
- package/dist/ast/tree-sitter-engine.d.ts.map +1 -0
- package/dist/ast/tree-sitter-engine.js +270 -0
- package/dist/ast/tree-sitter-engine.js.map +1 -0
- package/dist/config.d.ts +57 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +78 -0
- package/dist/config.js.map +1 -0
- package/dist/crap-config.d.ts +97 -0
- package/dist/crap-config.d.ts.map +1 -0
- package/dist/crap-config.js +144 -0
- package/dist/crap-config.js.map +1 -0
- package/dist/dashboard/server.d.ts +65 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +147 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +574 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/crap.d.ts +71 -0
- package/dist/metrics/crap.d.ts.map +1 -0
- package/dist/metrics/crap.js +67 -0
- package/dist/metrics/crap.js.map +1 -0
- package/dist/metrics/index.d.ts +31 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +27 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/score.d.ts +143 -0
- package/dist/metrics/score.d.ts.map +1 -0
- package/dist/metrics/score.js +224 -0
- package/dist/metrics/score.js.map +1 -0
- package/dist/metrics/tdr.d.ts +106 -0
- package/dist/metrics/tdr.d.ts.map +1 -0
- package/dist/metrics/tdr.js +117 -0
- package/dist/metrics/tdr.js.map +1 -0
- package/dist/metrics/workspace-walker.d.ts +43 -0
- package/dist/metrics/workspace-walker.d.ts.map +1 -0
- package/dist/metrics/workspace-walker.js +137 -0
- package/dist/metrics/workspace-walker.js.map +1 -0
- package/dist/sarif/index.d.ts +21 -0
- package/dist/sarif/index.d.ts.map +1 -0
- package/dist/sarif/index.js +19 -0
- package/dist/sarif/index.js.map +1 -0
- package/dist/sarif/sarif-builder.d.ts +128 -0
- package/dist/sarif/sarif-builder.d.ts.map +1 -0
- package/dist/sarif/sarif-builder.js +79 -0
- package/dist/sarif/sarif-builder.js.map +1 -0
- package/dist/sarif/sarif-store.d.ts +205 -0
- package/dist/sarif/sarif-store.d.ts.map +1 -0
- package/dist/sarif/sarif-store.js +246 -0
- package/dist/sarif/sarif-store.js.map +1 -0
- package/dist/sarif/sarif-validator.d.ts +45 -0
- package/dist/sarif/sarif-validator.d.ts.map +1 -0
- package/dist/sarif/sarif-validator.js +138 -0
- package/dist/sarif/sarif-validator.js.map +1 -0
- package/dist/schemas/tool-schemas.d.ts +216 -0
- package/dist/schemas/tool-schemas.d.ts.map +1 -0
- package/dist/schemas/tool-schemas.js +208 -0
- package/dist/schemas/tool-schemas.js.map +1 -0
- package/dist/sdk.d.ts +45 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +44 -0
- package/dist/sdk.js.map +1 -0
- package/dist/tools/index.d.ts +24 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/test-harness.d.ts +75 -0
- package/dist/tools/test-harness.d.ts.map +1 -0
- package/dist/tools/test-harness.js +137 -0
- package/dist/tools/test-harness.js.map +1 -0
- package/dist/workspace-guard.d.ts +53 -0
- package/dist/workspace-guard.d.ts.map +1 -0
- package/dist/workspace-guard.js +61 -0
- package/dist/workspace-guard.js.map +1 -0
- package/package.json +133 -0
- package/plugin/.claude-plugin/plugin.json +29 -0
- package/plugin/.mcp.json +18 -0
- package/plugin/CLAUDE.md +143 -0
- package/plugin/bundle/dashboard/public/index.html +368 -0
- package/plugin/bundle/dashboard/public/vendor/vue.global.prod.js +9 -0
- package/plugin/bundle/mcp-server.mjs +8718 -0
- package/plugin/bundle/mcp-server.mjs.map +7 -0
- package/plugin/bundle/tdr-engine.mjs +50 -0
- package/plugin/bundle/tdr-engine.mjs.map +7 -0
- package/plugin/hooks/hooks.json +62 -0
- package/plugin/hooks/lib/crap-config.mjs +152 -0
- package/plugin/hooks/lib/gatekeeper-rules.mjs +257 -0
- package/plugin/hooks/lib/hook-io.mjs +151 -0
- package/plugin/hooks/lib/quality-gate.mjs +329 -0
- package/plugin/hooks/lib/test-harness.mjs +152 -0
- package/plugin/hooks/post-tool-use.mjs +245 -0
- package/plugin/hooks/pre-tool-use.mjs +290 -0
- package/plugin/hooks/session-start.mjs +109 -0
- package/plugin/hooks/stop-quality-gate.mjs +226 -0
- package/plugin/package.json +18 -0
- package/plugin/skills/adopt/SKILL.md +74 -0
- package/plugin/skills/analyze/SKILL.md +77 -0
- package/plugin/skills/check-test/SKILL.md +50 -0
- package/plugin/skills/score/SKILL.md +31 -0
- package/scripts/bug-report.mjs +328 -0
- package/scripts/build-fast.mjs +130 -0
- package/scripts/bundle-plugin.mjs +74 -0
- package/scripts/doctor.mjs +320 -0
- package/scripts/install.mjs +192 -0
- package/scripts/lib/cli-ui.mjs +122 -0
- package/scripts/postinstall.mjs +127 -0
- package/scripts/run-tests.mjs +95 -0
- package/scripts/status.mjs +110 -0
- package/scripts/uninstall.mjs +72 -0
- package/src/adapters/bandit.ts +191 -0
- package/src/adapters/common.ts +133 -0
- package/src/adapters/eslint.ts +187 -0
- package/src/adapters/index.ts +78 -0
- package/src/adapters/semgrep.ts +150 -0
- package/src/adapters/stryker.ts +218 -0
- package/src/ast/cyclomatic.ts +131 -0
- package/src/ast/index.ts +33 -0
- package/src/ast/language-config.ts +231 -0
- package/src/ast/tree-sitter-engine.ts +385 -0
- package/src/config.ts +109 -0
- package/src/crap-config.ts +196 -0
- package/src/dashboard/public/index.html +368 -0
- package/src/dashboard/public/vendor/vue.global.prod.js +9 -0
- package/src/dashboard/server.ts +205 -0
- package/src/index.ts +696 -0
- package/src/metrics/crap.ts +101 -0
- package/src/metrics/index.ts +51 -0
- package/src/metrics/score.ts +329 -0
- package/src/metrics/tdr.ts +155 -0
- package/src/metrics/workspace-walker.ts +146 -0
- package/src/sarif/index.ts +31 -0
- package/src/sarif/sarif-builder.ts +139 -0
- package/src/sarif/sarif-store.ts +347 -0
- package/src/sarif/sarif-validator.ts +145 -0
- package/src/schemas/tool-schemas.ts +225 -0
- package/src/sdk.ts +110 -0
- package/src/tests/adapters/bandit.test.ts +111 -0
- package/src/tests/adapters/dispatch.test.ts +100 -0
- package/src/tests/adapters/eslint.test.ts +138 -0
- package/src/tests/adapters/semgrep.test.ts +125 -0
- package/src/tests/adapters/stryker.test.ts +103 -0
- package/src/tests/crap-config.test.ts +228 -0
- package/src/tests/crap.test.ts +59 -0
- package/src/tests/cyclomatic.test.ts +87 -0
- package/src/tests/dashboard-http.test.ts +108 -0
- package/src/tests/dashboard-integrity.test.ts +128 -0
- package/src/tests/integration/mcp-server.integration.test.ts +352 -0
- package/src/tests/pre-tool-use-hook.test.ts +178 -0
- package/src/tests/sarif-store.test.ts +241 -0
- package/src/tests/sarif-validator.test.ts +164 -0
- package/src/tests/score.test.ts +260 -0
- package/src/tests/skills-frontmatter.test.ts +172 -0
- package/src/tests/stop-quality-gate-strictness.test.ts +243 -0
- package/src/tests/tdr.test.ts +86 -0
- package/src/tests/test-harness.test.ts +153 -0
- package/src/tests/workspace-guard.test.ts +111 -0
- package/src/tools/index.ts +24 -0
- package/src/tools/test-harness.ts +158 -0
- package/src/workspace-guard.ts +64 -0
- 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
|
+
}
|