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,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
/**
|
|
4
|
+
* claude-crap :: PostToolUse hook — retrospective verifier.
|
|
5
|
+
*
|
|
6
|
+
* This hook runs immediately AFTER a Write / Edit / MultiEdit / NotebookEdit
|
|
7
|
+
* call has mutated a file on disk. Its job is to inspect the artifact the
|
|
8
|
+
* agent just produced and surface anything that the PreToolUse gatekeeper
|
|
9
|
+
* could not catch without the finished file:
|
|
10
|
+
*
|
|
11
|
+
* - Missing test harness for a production source file
|
|
12
|
+
* (Golden Rule enforcement from CLAUDE.md).
|
|
13
|
+
* - Crude "silenced warning" signatures (`eslint-disable`, `// @ts-ignore`,
|
|
14
|
+
* `# nosec`, `# type: ignore`). These often hide real issues.
|
|
15
|
+
* - TODO / FIXME / XXX markers in newly committed code.
|
|
16
|
+
*
|
|
17
|
+
* PostToolUse is **non-blocking** by design: every finding is emitted on
|
|
18
|
+
* stderr as a warning and the tool call is allowed to proceed. The agent
|
|
19
|
+
* is expected to read the warnings and remediate on its next turn, and
|
|
20
|
+
* the Stop quality gate will block the task close if any violation
|
|
21
|
+
* persists all the way to the end.
|
|
22
|
+
*
|
|
23
|
+
* The hook is intentionally cheap: only the artifact's path and raw
|
|
24
|
+
* bytes are examined. Deep SAST / CRAP / TDR analysis is deferred to the
|
|
25
|
+
* Stop hook which calls the MCP server.
|
|
26
|
+
*
|
|
27
|
+
* @module hooks/post-tool-use
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { promises as fs } from "node:fs";
|
|
31
|
+
import { resolve } from "node:path";
|
|
32
|
+
|
|
33
|
+
import { ExitCodes, readStdinJson, runHook, warnNonBlocking } from "./lib/hook-io.mjs";
|
|
34
|
+
import { findTestFile, isTestFile } from "./lib/test-harness.mjs";
|
|
35
|
+
|
|
36
|
+
const WORKSPACE_ROOT = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Source extensions we care about for the test-harness rule. Files
|
|
40
|
+
* outside this list (README, YAML, JSON, etc.) are skipped silently.
|
|
41
|
+
*/
|
|
42
|
+
const PRODUCTION_SOURCE_EXTENSIONS = new Set([
|
|
43
|
+
".ts",
|
|
44
|
+
".tsx",
|
|
45
|
+
".mts",
|
|
46
|
+
".cts",
|
|
47
|
+
".js",
|
|
48
|
+
".jsx",
|
|
49
|
+
".mjs",
|
|
50
|
+
".cjs",
|
|
51
|
+
".py",
|
|
52
|
+
".java",
|
|
53
|
+
".cs",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Inline suppression patterns. Each entry is a regex we look for in the
|
|
58
|
+
* just-written file. When matched, the hook emits a warning naming the
|
|
59
|
+
* suppression and asking the agent to remove it.
|
|
60
|
+
*/
|
|
61
|
+
const SUPPRESSION_PATTERNS = [
|
|
62
|
+
{ id: "SUPP-ESLINT-DISABLE", re: /\beslint-disable(?:-next-line)?\b/, tool: "ESLint" },
|
|
63
|
+
{ id: "SUPP-TS-IGNORE", re: /@ts-ignore/, tool: "TypeScript" },
|
|
64
|
+
{ id: "SUPP-TS-EXPECT-ERROR", re: /@ts-expect-error/, tool: "TypeScript" },
|
|
65
|
+
{ id: "SUPP-NOSEC", re: /#\s*nosec/, tool: "Bandit" },
|
|
66
|
+
{ id: "SUPP-TYPE-IGNORE", re: /#\s*type:\s*ignore/, tool: "mypy / pyright" },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const TODO_MARKER_REGEX = /\b(TODO|FIXME|XXX|HACK)\b/;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @typedef {Object} HookInput
|
|
73
|
+
* @property {string} [session_id]
|
|
74
|
+
* @property {string} [hook_event_name]
|
|
75
|
+
* @property {string} tool_name
|
|
76
|
+
* @property {Record<string, unknown>} tool_input
|
|
77
|
+
* @property {Record<string, unknown>} [tool_response]
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validate the minimum structural shape of a PostToolUse payload. Throws
|
|
82
|
+
* with a descriptive error when the payload is unrecognizable — the
|
|
83
|
+
* caller's fail-open harness will degrade gracefully.
|
|
84
|
+
*
|
|
85
|
+
* @param {unknown} payload
|
|
86
|
+
* @returns {HookInput}
|
|
87
|
+
*/
|
|
88
|
+
function validate(payload) {
|
|
89
|
+
if (!payload || typeof payload !== "object") throw new Error("payload is not an object");
|
|
90
|
+
const p = /** @type {Record<string, unknown>} */ (payload);
|
|
91
|
+
if (typeof p.tool_name !== "string") throw new Error("payload.tool_name missing");
|
|
92
|
+
if (!p.tool_input || typeof p.tool_input !== "object") throw new Error("payload.tool_input missing");
|
|
93
|
+
return /** @type {HookInput} */ (p);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Extract the target file path from the tool input, if any. Returns
|
|
98
|
+
* `null` when the tool does not operate on a single file (e.g. Bash).
|
|
99
|
+
*
|
|
100
|
+
* @param {HookInput} input
|
|
101
|
+
* @returns {string | null}
|
|
102
|
+
*/
|
|
103
|
+
function extractTargetFile(input) {
|
|
104
|
+
const fp = input.tool_input.file_path;
|
|
105
|
+
if (typeof fp === "string") return fp;
|
|
106
|
+
const np = input.tool_input.notebook_path;
|
|
107
|
+
if (typeof np === "string") return np;
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Is this extension one of the production source languages we guard?
|
|
113
|
+
*
|
|
114
|
+
* @param {string} filePath
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
*/
|
|
117
|
+
function isProductionSource(filePath) {
|
|
118
|
+
for (const ext of PRODUCTION_SOURCE_EXTENSIONS) {
|
|
119
|
+
if (filePath.endsWith(ext)) return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Rule: production source files must have an accompanying test file.
|
|
126
|
+
*
|
|
127
|
+
* Fires a `SONAR-TEST-MISSING` warning when none of the conventional
|
|
128
|
+
* test locations exists. Does not block — the Stop gate enforces the
|
|
129
|
+
* strict verdict.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} filePath Absolute path to the artifact just written.
|
|
132
|
+
* @param {string} toolName Name of the tool that wrote it (for logging).
|
|
133
|
+
*/
|
|
134
|
+
async function checkTestHarness(filePath, toolName) {
|
|
135
|
+
if (isTestFile(filePath)) return;
|
|
136
|
+
if (!isProductionSource(filePath)) return;
|
|
137
|
+
|
|
138
|
+
const resolution = await findTestFile(WORKSPACE_ROOT, filePath);
|
|
139
|
+
if (resolution.testFile) return;
|
|
140
|
+
|
|
141
|
+
warnNonBlocking({
|
|
142
|
+
title: "PostToolUse",
|
|
143
|
+
ruleId: "SONAR-TEST-MISSING",
|
|
144
|
+
tool: toolName,
|
|
145
|
+
reason:
|
|
146
|
+
`No test file was found for '${filePath}'. ` +
|
|
147
|
+
`The Golden Rule in CLAUDE.md requires a test harness to accompany every production source file. ` +
|
|
148
|
+
`Corrective action: before the Stop quality gate runs, create a test next to the file or under the ` +
|
|
149
|
+
`mirror tree at 'tests/' with a name such as '${resolution.candidates[0]}'.`,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Rule: inline suppression markers are forbidden.
|
|
155
|
+
*
|
|
156
|
+
* Reads the just-written file and scans for common linter / type-checker
|
|
157
|
+
* suppression annotations. When found, emits a warning naming the tool
|
|
158
|
+
* whose output was being silenced.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} filePath
|
|
161
|
+
* @param {string} toolName
|
|
162
|
+
*/
|
|
163
|
+
async function checkSuppressionMarkers(filePath, toolName) {
|
|
164
|
+
let content;
|
|
165
|
+
try {
|
|
166
|
+
content = await fs.readFile(filePath, "utf8");
|
|
167
|
+
} catch {
|
|
168
|
+
// File not readable (unusual: PostToolUse runs after a successful write).
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const suppression of SUPPRESSION_PATTERNS) {
|
|
173
|
+
if (!suppression.re.test(content)) continue;
|
|
174
|
+
warnNonBlocking({
|
|
175
|
+
title: "PostToolUse",
|
|
176
|
+
ruleId: `SONAR-${suppression.id}`,
|
|
177
|
+
tool: toolName,
|
|
178
|
+
reason:
|
|
179
|
+
`Found a suppression marker (${suppression.id}) that silences ${suppression.tool}. ` +
|
|
180
|
+
`CLAUDE.md forbids silencing findings — fix the underlying issue instead. ` +
|
|
181
|
+
`Corrective action: remove the suppression and address the warning it was hiding. If the warning ` +
|
|
182
|
+
`is truly a false positive, add an entry to the tool's configuration file with a clear rationale.`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Rule: freshly committed TODO/FIXME markers are tracked.
|
|
189
|
+
*
|
|
190
|
+
* Not every TODO is a defect, but TODOs that slip into committed code
|
|
191
|
+
* are a leading indicator of technical debt. We emit a single aggregated
|
|
192
|
+
* warning per file rather than one per line.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} filePath
|
|
195
|
+
* @param {string} toolName
|
|
196
|
+
*/
|
|
197
|
+
async function checkTodoMarkers(filePath, toolName) {
|
|
198
|
+
let content;
|
|
199
|
+
try {
|
|
200
|
+
content = await fs.readFile(filePath, "utf8");
|
|
201
|
+
} catch {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const lines = content.split(/\r?\n/);
|
|
205
|
+
/** @type {number[]} */
|
|
206
|
+
const hits = [];
|
|
207
|
+
lines.forEach((line, idx) => {
|
|
208
|
+
if (TODO_MARKER_REGEX.test(line)) hits.push(idx + 1);
|
|
209
|
+
});
|
|
210
|
+
if (hits.length === 0) return;
|
|
211
|
+
|
|
212
|
+
warnNonBlocking({
|
|
213
|
+
title: "PostToolUse",
|
|
214
|
+
ruleId: "SONAR-TODO-MARKER",
|
|
215
|
+
tool: toolName,
|
|
216
|
+
reason:
|
|
217
|
+
`Found ${hits.length} TODO/FIXME/HACK marker(s) in '${filePath}' at line(s) ${hits.slice(0, 5).join(", ")}` +
|
|
218
|
+
`${hits.length > 5 ? ", ..." : ""}. ` +
|
|
219
|
+
`These are tracked by the TDR engine as debt. Either resolve them now or open a linked ticket and ` +
|
|
220
|
+
`reference it in the comment so the Stop gate can audit the backlog.`,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function main() {
|
|
225
|
+
const payload = await readStdinJson();
|
|
226
|
+
const input = validate(payload);
|
|
227
|
+
const filePath = extractTargetFile(input);
|
|
228
|
+
if (!filePath) {
|
|
229
|
+
// Tool did not write a file (e.g. Bash). Nothing to verify.
|
|
230
|
+
process.exit(ExitCodes.ALLOW);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const absolute = resolve(WORKSPACE_ROOT, filePath);
|
|
234
|
+
await checkTestHarness(absolute, input.tool_name);
|
|
235
|
+
await checkSuppressionMarkers(absolute, input.tool_name);
|
|
236
|
+
await checkTodoMarkers(absolute, input.tool_name);
|
|
237
|
+
|
|
238
|
+
// PostToolUse never blocks — always allow. Warnings already wrote to stderr.
|
|
239
|
+
process.stdout.write(
|
|
240
|
+
JSON.stringify({ status: "verified", tool: input.tool_name, file: filePath }) + "\n",
|
|
241
|
+
);
|
|
242
|
+
process.exit(ExitCodes.ALLOW);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
runHook("PostToolUse", main);
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
/**
|
|
4
|
+
* claude-crap :: PreToolUse hook — prophylactic gatekeeper.
|
|
5
|
+
*
|
|
6
|
+
* Contract with Claude Code (see https://code.claude.com/docs/en/hooks):
|
|
7
|
+
*
|
|
8
|
+
* stdin : JSON payload `{ session_id, tool_name, tool_input, ... }`
|
|
9
|
+
* stdout : Free-form informational output. Never interpreted by Claude.
|
|
10
|
+
* stderr : Corrective message injected into the agent's context when
|
|
11
|
+
* the hook exits with code 2.
|
|
12
|
+
* exit 0 : Allow the tool call.
|
|
13
|
+
* exit 2 : ABORT the tool call. Claude Code forwards stderr to the LLM.
|
|
14
|
+
* exit N : Non-zero, non-2 exit. Treated by Claude Code as "allow"
|
|
15
|
+
* (fail-open). claude-crap uses this code only for LOW-RISK
|
|
16
|
+
* tools when the hook itself errors; HIGH-RISK tools always
|
|
17
|
+
* fall back to exit 2 (fail-closed) — see the allowlist below.
|
|
18
|
+
*
|
|
19
|
+
* Deterministic design principles:
|
|
20
|
+
*
|
|
21
|
+
* - Zero network I/O.
|
|
22
|
+
* - Zero filesystem I/O beyond reading stdin and writing stderr.
|
|
23
|
+
* - Target latency: under 200 ms in the common case.
|
|
24
|
+
* - All rules live in `./lib/gatekeeper-rules.mjs` so they can be tested
|
|
25
|
+
* in isolation without spinning up Claude Code.
|
|
26
|
+
*
|
|
27
|
+
* Fail-open vs fail-closed (F-A06-01):
|
|
28
|
+
*
|
|
29
|
+
* The CLAUDE.md contract states that "none of your proposals bypass
|
|
30
|
+
* those filters." A fully fail-open gatekeeper would break that
|
|
31
|
+
* contract the instant any rule throws an exception or any payload
|
|
32
|
+
* fails to parse. But a fully fail-closed gatekeeper would deadlock
|
|
33
|
+
* the user whenever Claude Code sends an unusual payload, which is
|
|
34
|
+
* also unacceptable.
|
|
35
|
+
*
|
|
36
|
+
* The compromise is an allowlist of HIGH-risk tool names: Write,
|
|
37
|
+
* Edit, MultiEdit, NotebookEdit, Bash. For those tools, ANY failure
|
|
38
|
+
* to evaluate the rules (parse error, rule throw, validator throw)
|
|
39
|
+
* exits 2 with a structured corrective message. For every other
|
|
40
|
+
* tool, the legacy fail-open behavior is preserved.
|
|
41
|
+
*
|
|
42
|
+
* When the stdin payload is unparseable we still try to recover the
|
|
43
|
+
* tool name via a best-effort regex so the fail-closed check can
|
|
44
|
+
* trigger even in the degraded path.
|
|
45
|
+
*
|
|
46
|
+
* What this hook does NOT do:
|
|
47
|
+
*
|
|
48
|
+
* Deep SAST, CRAP computation, tree-sitter AST parsing, coverage lookup
|
|
49
|
+
* and SARIF aggregation all live in PostToolUse (retrospective) or Stop
|
|
50
|
+
* (final quality gate). Those stages call the MCP server. The PreToolUse
|
|
51
|
+
* hook is intentionally a cheap synchronous speed bump.
|
|
52
|
+
*
|
|
53
|
+
* @module hooks/pre-tool-use
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import { runAllRules } from "./lib/gatekeeper-rules.mjs";
|
|
57
|
+
|
|
58
|
+
/** Allow the tool call to proceed. */
|
|
59
|
+
const EXIT_ALLOW = 0;
|
|
60
|
+
/** Block the tool call and inject `stderr` into the agent's context. */
|
|
61
|
+
const EXIT_BLOCK = 2;
|
|
62
|
+
/** Internal hook failure (fail-open for LOW-risk tools only). */
|
|
63
|
+
const EXIT_INTERNAL_ERROR = 1;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Tool names for which the gatekeeper MUST fail closed when it cannot
|
|
67
|
+
* evaluate the payload. These are exactly the tools that can mutate the
|
|
68
|
+
* workspace or execute a shell, so bypassing the gatekeeper for them
|
|
69
|
+
* would violate the CLAUDE.md Golden Rule.
|
|
70
|
+
*/
|
|
71
|
+
const HIGH_RISK_TOOLS = Object.freeze(
|
|
72
|
+
new Set(["Write", "Edit", "MultiEdit", "NotebookEdit", "Bash"]),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Best-effort regex used to recover `tool_name` from stdin that does not
|
|
77
|
+
* parse as JSON. The regex is intentionally lenient: it just looks for
|
|
78
|
+
* the first `"tool_name": "<something>"` substring anywhere in the raw
|
|
79
|
+
* input. When nothing matches we return `null` and the caller treats
|
|
80
|
+
* the tool as unknown (which means fail-open).
|
|
81
|
+
*/
|
|
82
|
+
const TOOL_NAME_EXTRACT_RE = /"tool_name"\s*:\s*"([^"]+)"/;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Read the full stdin stream as a raw UTF-8 string. Kept separate from
|
|
86
|
+
* JSON parsing so the caller can attempt a best-effort `tool_name`
|
|
87
|
+
* extraction on malformed input.
|
|
88
|
+
*
|
|
89
|
+
* @returns {Promise<string>} The stdin contents, trimmed of surrounding whitespace.
|
|
90
|
+
*/
|
|
91
|
+
async function readStdinRaw() {
|
|
92
|
+
/** @type {Buffer[]} */
|
|
93
|
+
const chunks = [];
|
|
94
|
+
for await (const chunk of process.stdin) {
|
|
95
|
+
chunks.push(/** @type {Buffer} */ (chunk));
|
|
96
|
+
}
|
|
97
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse a raw stdin string as JSON, rethrowing with a friendly message.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} raw Raw stdin contents.
|
|
104
|
+
* @returns {object} The parsed JSON payload.
|
|
105
|
+
* @throws When the string is empty or not valid JSON.
|
|
106
|
+
*/
|
|
107
|
+
function parseStdinJson(raw) {
|
|
108
|
+
if (!raw) {
|
|
109
|
+
throw new Error("stdin was empty — claude-crap PreToolUse expected a hook JSON payload");
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(raw);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
throw new Error(`stdin is not valid JSON: ${/** @type {Error} */ (err).message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Validate that the payload has the minimum shape of a Claude Code hook
|
|
120
|
+
* and narrow its type for downstream consumers. Throws when the payload
|
|
121
|
+
* is unrecognizable — the caller handles the fail-closed / fail-open
|
|
122
|
+
* decision.
|
|
123
|
+
*
|
|
124
|
+
* @param {unknown} payload Raw parsed JSON from stdin.
|
|
125
|
+
* @returns {import("./lib/gatekeeper-rules.mjs").HookInput}
|
|
126
|
+
* @throws When required fields are missing or the wrong type.
|
|
127
|
+
*/
|
|
128
|
+
function validateHookPayload(payload) {
|
|
129
|
+
if (!payload || typeof payload !== "object") {
|
|
130
|
+
throw new Error("payload is not an object");
|
|
131
|
+
}
|
|
132
|
+
const p = /** @type {Record<string, unknown>} */ (payload);
|
|
133
|
+
if (typeof p.tool_name !== "string") {
|
|
134
|
+
throw new Error("payload.tool_name is missing or not a string");
|
|
135
|
+
}
|
|
136
|
+
if (!p.tool_input || typeof p.tool_input !== "object") {
|
|
137
|
+
throw new Error("payload.tool_input is missing or not an object");
|
|
138
|
+
}
|
|
139
|
+
return /** @type {import("./lib/gatekeeper-rules.mjs").HookInput} */ (p);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Best-effort extractor for `tool_name` from unparseable stdin. Used as
|
|
144
|
+
* a last resort when the gatekeeper needs to decide between fail-open
|
|
145
|
+
* and fail-closed but the payload never made it through `JSON.parse`.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} raw Raw stdin contents.
|
|
148
|
+
* @returns {string | null} The extracted tool name, or `null`.
|
|
149
|
+
*/
|
|
150
|
+
function extractToolNameFromRaw(raw) {
|
|
151
|
+
if (!raw) return null;
|
|
152
|
+
const match = TOOL_NAME_EXTRACT_RE.exec(raw);
|
|
153
|
+
return match && typeof match[1] === "string" ? match[1] : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* `true` when the given tool name is in the high-risk allowlist and
|
|
158
|
+
* therefore must fail closed on any internal hook error.
|
|
159
|
+
*
|
|
160
|
+
* @param {string | null | undefined} toolName
|
|
161
|
+
* @returns {boolean}
|
|
162
|
+
*/
|
|
163
|
+
function isHighRiskTool(toolName) {
|
|
164
|
+
return typeof toolName === "string" && HIGH_RISK_TOOLS.has(toolName);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Render the fail-closed corrective message. Claude Code injects this
|
|
169
|
+
* text into the agent's context when the hook exits with code 2, so it
|
|
170
|
+
* must be imperative and actionable.
|
|
171
|
+
*
|
|
172
|
+
* @param {Object} opts
|
|
173
|
+
* @param {string} opts.toolName Recovered tool name (e.g. `"Write"`).
|
|
174
|
+
* @param {string} opts.phase Which phase failed — `"parse"` or `"evaluate"`.
|
|
175
|
+
* @param {string} opts.detail Short technical description of the failure.
|
|
176
|
+
* @returns {string} Multi-line box ready to write to stderr.
|
|
177
|
+
*/
|
|
178
|
+
function renderFailClosedMessage({ toolName, phase, detail }) {
|
|
179
|
+
return [
|
|
180
|
+
"╭─ claude-crap :: PreToolUse BLOCKED (fail-closed) ───────────────",
|
|
181
|
+
"│ rule : SONAR-GATEKEEPER-FAILCLOSED",
|
|
182
|
+
`│ tool : ${toolName}`,
|
|
183
|
+
`│ phase: ${phase}`,
|
|
184
|
+
"│",
|
|
185
|
+
"│ The gatekeeper could not evaluate this call and the tool is in",
|
|
186
|
+
"│ the high-risk allowlist (Write, Edit, MultiEdit, NotebookEdit,",
|
|
187
|
+
"│ Bash). Per CLAUDE.md, enforcement must not be bypassed by a hook",
|
|
188
|
+
"│ failure for tools that mutate files or run a shell.",
|
|
189
|
+
"│",
|
|
190
|
+
"│ Corrective action: fix the gatekeeper bug surfaced by the detail",
|
|
191
|
+
"│ line below, then retry. If you cannot fix it, ask the user to",
|
|
192
|
+
"│ invoke a non-mutating tool (Read, Glob, Grep) to inspect the",
|
|
193
|
+
"│ situation instead.",
|
|
194
|
+
"│",
|
|
195
|
+
`│ detail: ${detail}`,
|
|
196
|
+
"╰──────────────────────────────────────────────────────────────────",
|
|
197
|
+
].join("\n");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Handle a hook internal error uniformly. Decides between fail-closed
|
|
202
|
+
* (exit 2, for high-risk tools) and fail-open (exit 1, for everything
|
|
203
|
+
* else), writes the appropriate message to stderr, and exits. Never
|
|
204
|
+
* returns.
|
|
205
|
+
*
|
|
206
|
+
* @param {Object} opts
|
|
207
|
+
* @param {string | null} opts.toolName Best-effort recovered tool name.
|
|
208
|
+
* @param {string} opts.phase `"parse"` or `"evaluate"`.
|
|
209
|
+
* @param {string} opts.detail Short technical reason.
|
|
210
|
+
* @returns {never}
|
|
211
|
+
*/
|
|
212
|
+
function exitOnInternalError({ toolName, phase, detail }) {
|
|
213
|
+
if (isHighRiskTool(toolName)) {
|
|
214
|
+
process.stderr.write(
|
|
215
|
+
renderFailClosedMessage({
|
|
216
|
+
toolName: /** @type {string} */ (toolName),
|
|
217
|
+
phase,
|
|
218
|
+
detail,
|
|
219
|
+
}) + "\n",
|
|
220
|
+
);
|
|
221
|
+
process.exit(EXIT_BLOCK);
|
|
222
|
+
}
|
|
223
|
+
process.stderr.write(
|
|
224
|
+
`[claude-crap] PreToolUse: ${phase} failure (${detail}). ` +
|
|
225
|
+
`Tool '${toolName ?? "<unknown>"}' is not in the high-risk allowlist; ` +
|
|
226
|
+
`falling back to permissive mode (fail-open).\n`,
|
|
227
|
+
);
|
|
228
|
+
process.exit(EXIT_INTERNAL_ERROR);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Entrypoint. Reads the hook payload, runs every rule, and exits with
|
|
233
|
+
* the appropriate code. Any unexpected failure is routed through
|
|
234
|
+
* `exitOnInternalError`, which fails closed for high-risk tools.
|
|
235
|
+
*/
|
|
236
|
+
async function main() {
|
|
237
|
+
/** @type {string} */
|
|
238
|
+
let raw = "";
|
|
239
|
+
/** @type {import("./lib/gatekeeper-rules.mjs").HookInput | null} */
|
|
240
|
+
let input = null;
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
raw = await readStdinRaw();
|
|
244
|
+
const parsed = parseStdinJson(raw);
|
|
245
|
+
input = validateHookPayload(parsed);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
const recoveredTool = extractToolNameFromRaw(raw);
|
|
248
|
+
exitOnInternalError({
|
|
249
|
+
toolName: recoveredTool,
|
|
250
|
+
phase: "parse",
|
|
251
|
+
detail: /** @type {Error} */ (err).message,
|
|
252
|
+
});
|
|
253
|
+
return; // unreachable, but keeps the type checker happy
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const verdict = runAllRules(input);
|
|
258
|
+
if (verdict && verdict.blocked) {
|
|
259
|
+
// Structured message for the LLM. Claude Code injects stderr into
|
|
260
|
+
// the agent's context whenever a hook exits with code 2, so this
|
|
261
|
+
// text effectively becomes a prompt. Keep it imperative and actionable.
|
|
262
|
+
const message = [
|
|
263
|
+
"╭─ claude-crap :: PreToolUse BLOCKED ────────────────────────────",
|
|
264
|
+
`│ rule : ${verdict.ruleId}`,
|
|
265
|
+
`│ tool : ${input.tool_name}`,
|
|
266
|
+
"│",
|
|
267
|
+
`│ ${verdict.reason}`,
|
|
268
|
+
"╰──────────────────────────────────────────────────────────────────",
|
|
269
|
+
].join("\n");
|
|
270
|
+
process.stderr.write(`${message}\n`);
|
|
271
|
+
process.exit(EXIT_BLOCK);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Silent pass-through. We still emit a single JSON line on stdout so
|
|
276
|
+
// that the hooks transcript can be audited after the fact.
|
|
277
|
+
process.stdout.write(
|
|
278
|
+
JSON.stringify({ status: "allow", tool: input.tool_name, rules_evaluated: 4 }) + "\n",
|
|
279
|
+
);
|
|
280
|
+
process.exit(EXIT_ALLOW);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
exitOnInternalError({
|
|
283
|
+
toolName: input.tool_name,
|
|
284
|
+
phase: "evaluate",
|
|
285
|
+
detail: /** @type {Error} */ (err).message,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
main();
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
/**
|
|
4
|
+
* claude-crap :: SessionStart hook — baseline context seeder.
|
|
5
|
+
*
|
|
6
|
+
* Runs once when Claude Code starts a new interactive session with this
|
|
7
|
+
* plugin active. Its purpose is to fix the agent's opening mental model
|
|
8
|
+
* without paying for it in tokens later: the hook writes a short,
|
|
9
|
+
* structured briefing on stdout so Claude Code can inject it into the
|
|
10
|
+
* session's system context.
|
|
11
|
+
*
|
|
12
|
+
* The briefing contains:
|
|
13
|
+
*
|
|
14
|
+
* - A reminder of the Golden Rule and the hook contract.
|
|
15
|
+
* - The current configured thresholds (CRAP, TDR rating, LOC cost).
|
|
16
|
+
* - Baseline metrics pulled from the last consolidated SARIF report.
|
|
17
|
+
*
|
|
18
|
+
* Exit semantics:
|
|
19
|
+
*
|
|
20
|
+
* - Exit 0 → briefing written to stdout, Claude Code appends it to
|
|
21
|
+
* the session context.
|
|
22
|
+
* - Any other exit → fail-open; the session starts with no briefing.
|
|
23
|
+
*
|
|
24
|
+
* The hook never blocks, never reads the filesystem beyond the SARIF
|
|
25
|
+
* report, and never calls the MCP server — it must complete in under
|
|
26
|
+
* 10 seconds per the `hooks.json` timeout budget.
|
|
27
|
+
*
|
|
28
|
+
* @module hooks/session-start
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { ExitCodes, runHook } from "./lib/hook-io.mjs";
|
|
32
|
+
import { evaluateQualityGate, loadQualityGateConfig } from "./lib/quality-gate.mjs";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Render the opening briefing as Markdown. The text lands in the
|
|
36
|
+
* agent's system context, so it must be compact and imperative.
|
|
37
|
+
*
|
|
38
|
+
* @param {import("./lib/quality-gate.mjs").QualityGateConfig} config
|
|
39
|
+
* @param {import("./lib/quality-gate.mjs").GateVerdict} verdict
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
function renderBriefing(config, verdict) {
|
|
43
|
+
const { summary } = verdict;
|
|
44
|
+
return [
|
|
45
|
+
"## claude-crap session briefing",
|
|
46
|
+
"",
|
|
47
|
+
"This session is running under the claude-crap plugin. You are bound by:",
|
|
48
|
+
"",
|
|
49
|
+
"- **Golden Rule** — do not write functional code until a characterization test",
|
|
50
|
+
" pins the current behavior.",
|
|
51
|
+
"- **Hook contract** — PreToolUse can abort with exit 2, PostToolUse emits",
|
|
52
|
+
" warnings, Stop / SubagentStop enforce the quality gate.",
|
|
53
|
+
"- **Deterministic engines** — anchor decisions in the claude-crap MCP tools",
|
|
54
|
+
" (`compute_crap`, `compute_tdr`, `analyze_file_ast`, `ingest_sarif`,",
|
|
55
|
+
" `require_test_harness`) rather than in speculative reasoning.",
|
|
56
|
+
"",
|
|
57
|
+
"### Current policy",
|
|
58
|
+
"",
|
|
59
|
+
`- CRAP threshold: **${config.crapThreshold}** (block on any function above it)`,
|
|
60
|
+
`- Maintainability ceiling: **${config.tdrMaxRating}** (worse ratings halt the Stop gate)`,
|
|
61
|
+
`- Cost per LOC: **${config.minutesPerLoc}** minutes (used as the TDR denominator)`,
|
|
62
|
+
"",
|
|
63
|
+
"### Baseline workspace metrics",
|
|
64
|
+
"",
|
|
65
|
+
`- Workspace LOC: **${summary.physicalLoc}**`,
|
|
66
|
+
`- Total findings: **${summary.totalFindings}** ` +
|
|
67
|
+
`(error: ${summary.errorFindings}, warning: ${summary.warningFindings}, note: ${summary.noteFindings})`,
|
|
68
|
+
`- Remediation debt: **${summary.remediationMinutes} min**`,
|
|
69
|
+
`- TDR: **${summary.tdrPercent}%** → rating **${summary.tdrRating}**`,
|
|
70
|
+
summary.toolsSeen.length > 0
|
|
71
|
+
? `- Scanners already ingested: ${summary.toolsSeen.join(", ")}`
|
|
72
|
+
: "- Scanners already ingested: <none>",
|
|
73
|
+
"",
|
|
74
|
+
verdict.passed
|
|
75
|
+
? "The baseline currently passes the quality gate. Keep it that way."
|
|
76
|
+
: `⚠️ Baseline would FAIL the Stop gate (${verdict.failures.length} policy violation(s)). ` +
|
|
77
|
+
"Your first priority should be remediating existing findings before introducing new code.",
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function main() {
|
|
82
|
+
const config = loadQualityGateConfig();
|
|
83
|
+
let verdict;
|
|
84
|
+
try {
|
|
85
|
+
verdict = await evaluateQualityGate(config);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
// If we cannot produce a verdict (e.g. MCP server not built yet),
|
|
88
|
+
// fail open: write a stripped briefing so the session still starts.
|
|
89
|
+
process.stderr.write(
|
|
90
|
+
`[claude-crap] SessionStart: could not evaluate baseline: ${/** @type {Error} */ (err).message}\n`,
|
|
91
|
+
);
|
|
92
|
+
process.stdout.write(
|
|
93
|
+
[
|
|
94
|
+
"## claude-crap session briefing",
|
|
95
|
+
"",
|
|
96
|
+
"claude-crap is active but the baseline quality gate could not run.",
|
|
97
|
+
"Run `cd src/mcp-server && npm run build` to enable deterministic metrics.",
|
|
98
|
+
"The Golden Rule still applies: no functional code without a prior test.",
|
|
99
|
+
].join("\n") + "\n",
|
|
100
|
+
);
|
|
101
|
+
process.exit(ExitCodes.ALLOW);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
process.stdout.write(renderBriefing(config, verdict) + "\n");
|
|
106
|
+
process.exit(ExitCodes.ALLOW);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
runHook("SessionStart", main);
|