claude-dev-env 8.4.2 → 8.5.0
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/commands/fix-followups.md +66 -0
- package/hooks/followup_ledger.py +143 -0
- package/hooks/hooks_constants/followup_ledger_constants.py +27 -0
- package/hooks/test_followup_ledger.py +83 -0
- package/package.json +1 -1
- package/rules/flag-non-breaking-findings.md +72 -0
- package/scripts/dev_env_scripts_constants/followup_constants.py +72 -0
- package/scripts/followup_cli.py +273 -0
- package/scripts/test_followup_cli.py +156 -0
- package/scripts/test_validate_instruction_pairs.py +144 -1
- package/scripts/validate_instruction_pairs.py +277 -47
- package/scripts/windows/pstack/pstack-command-shim.ps1 +9 -2
- package/scripts/audit-hooks-cli.mjs +0 -59
- package/scripts/audit-hooks.mjs +0 -452
- package/scripts/tests/test-audit-hooks-cli.mjs +0 -150
- package/scripts/tests/test-audit-hooks.mjs +0 -497
|
@@ -1,497 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import test from "node:test";
|
|
7
|
-
import { fileURLToPath } from "node:url";
|
|
8
|
-
|
|
9
|
-
import { auditHooks } from "../audit-hooks.mjs";
|
|
10
|
-
|
|
11
|
-
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../..");
|
|
12
|
-
|
|
13
|
-
function writeJson(filePath, document) {
|
|
14
|
-
writeFileSync(filePath, JSON.stringify(document), "utf8");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function createAuditFixture({ canonicalHooks = {}, claudeHooks = { hooks: {} }, codexHooks = { hooks: {} } } = {}) {
|
|
18
|
-
const fixtureRoot = mkdtempSync(path.join(tmpdir(), "hook-audit-"));
|
|
19
|
-
const canonicalHooksRoot = path.join(fixtureRoot, "packages", "claude-dev-env", "hooks");
|
|
20
|
-
const homeDirectory = path.join(fixtureRoot, "home");
|
|
21
|
-
mkdirSync(canonicalHooksRoot, { recursive: true });
|
|
22
|
-
mkdirSync(path.join(homeDirectory, ".claude", "hooks"), { recursive: true });
|
|
23
|
-
mkdirSync(path.join(homeDirectory, ".codex", "hooks"), { recursive: true });
|
|
24
|
-
writeJson(path.join(canonicalHooksRoot, "hooks.json"), { hooks: canonicalHooks });
|
|
25
|
-
writeJson(path.join(homeDirectory, ".claude", "settings.json"), claudeHooks);
|
|
26
|
-
writeJson(path.join(homeDirectory, ".codex", "hooks.json"), codexHooks);
|
|
27
|
-
return { fixtureRoot, homeDirectory, canonicalHooksRoot };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function writeHookFile(hooksRoot, relativePath) {
|
|
31
|
-
const hookPath = path.join(hooksRoot, relativePath);
|
|
32
|
-
mkdirSync(path.dirname(hookPath), { recursive: true });
|
|
33
|
-
writeFileSync(hookPath, "", "utf8");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function runGit(repositoryRoot, argumentsList, environment) {
|
|
37
|
-
const command = spawnSync("git", argumentsList, {
|
|
38
|
-
cwd: repositoryRoot,
|
|
39
|
-
encoding: "utf8",
|
|
40
|
-
env: environment,
|
|
41
|
-
shell: false,
|
|
42
|
-
});
|
|
43
|
-
assert.equal(command.status, 0, command.stderr);
|
|
44
|
-
return command;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
test("reports the canonical execution graph", () => {
|
|
48
|
-
const fixture = createAuditFixture({
|
|
49
|
-
canonicalHooks: {
|
|
50
|
-
PreToolUse: [{
|
|
51
|
-
matcher: "Write",
|
|
52
|
-
hooks: [{ type: "command", command: "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/write_existing_file_blocker.py" }],
|
|
53
|
-
}],
|
|
54
|
-
},
|
|
55
|
-
});
|
|
56
|
-
try {
|
|
57
|
-
const report = auditHooks({ repositoryRoot: fixture.fixtureRoot });
|
|
58
|
-
assert.equal(report.summary.directCount, 1);
|
|
59
|
-
assert.equal(report.summary.dispatcherCount, 0);
|
|
60
|
-
assert.equal(report.summary.effectiveCount, 0);
|
|
61
|
-
assert.equal(report.summary.logicalAssociationCount, 1);
|
|
62
|
-
} finally {
|
|
63
|
-
rmSync(fixture.fixtureRoot, { force: true, recursive: true });
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test("does not expand the retired stop dispatcher", () => {
|
|
68
|
-
const fixture = createAuditFixture({
|
|
69
|
-
canonicalHooks: {
|
|
70
|
-
Stop: [{
|
|
71
|
-
hooks: [{ type: "command", command: "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/stop_dispatcher.py" }],
|
|
72
|
-
}],
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
const constantsPath = path.join(
|
|
76
|
-
fixture.canonicalHooksRoot,
|
|
77
|
-
"hooks_constants",
|
|
78
|
-
"stop_dispatcher_constants.py",
|
|
79
|
-
);
|
|
80
|
-
writeHookFile(fixture.canonicalHooksRoot, "blocking/stop_dispatcher.py");
|
|
81
|
-
mkdirSync(path.dirname(constantsPath), { recursive: true });
|
|
82
|
-
writeFileSync(constantsPath, "ALL_STOP_HOSTED_HOOK_PATHS = ()\n", "utf8");
|
|
83
|
-
try {
|
|
84
|
-
const report = auditHooks({ repositoryRoot: fixture.fixtureRoot });
|
|
85
|
-
assert.equal(report.summary.dispatcherCount, 0);
|
|
86
|
-
assert.equal(report.hostedRegistrations.length, 0);
|
|
87
|
-
assert.equal(report.directRegistrations[0].target, "script:blocking/stop_dispatcher.py");
|
|
88
|
-
} finally {
|
|
89
|
-
rmSync(fixture.fixtureRoot, { force: true, recursive: true });
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("renders deterministic private JSON", () => {
|
|
94
|
-
const report = auditHooks({ repositoryRoot });
|
|
95
|
-
const first = JSON.stringify(report);
|
|
96
|
-
const second = JSON.stringify(report);
|
|
97
|
-
assert.equal(first, second);
|
|
98
|
-
assert.equal(first.includes(repositoryRoot), false);
|
|
99
|
-
assert.equal(first.includes("python3 ${CLAUDE_PLUGIN_ROOT}"), false);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test("does not compare the live graph with retired baseline counts", () => {
|
|
103
|
-
const report = auditHooks({ repositoryRoot });
|
|
104
|
-
assert.equal(report.summary.directCount, report.directRegistrations.length);
|
|
105
|
-
assert.equal(report.summary.effectiveCount, report.hostedRegistrations.length);
|
|
106
|
-
assert.equal(report.summary.logicalAssociationCount, report.logicalRegistrations.length);
|
|
107
|
-
assert.ok(report.summary.directCount > 0);
|
|
108
|
-
assert.ok(report.summary.effectiveCount > 0);
|
|
109
|
-
assert.ok(report.summary.logicalAssociationCount > report.summary.directCount);
|
|
110
|
-
assert.equal(report.findings.some(({ code }) => code === "DIRECT_COUNT_DRIFT"), false);
|
|
111
|
-
assert.equal(report.findings.some(({ code }) => code === "HOSTED_COUNT_DRIFT"), false);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("does not execute registered commands", () => {
|
|
115
|
-
const fixture = createAuditFixture({
|
|
116
|
-
canonicalHooks: {
|
|
117
|
-
PreToolUse: [{
|
|
118
|
-
hooks: [{
|
|
119
|
-
command: "node -e \"require('node:fs').writeFileSync('audit-executed', 'yes')\"",
|
|
120
|
-
type: "command",
|
|
121
|
-
}],
|
|
122
|
-
}],
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
try {
|
|
126
|
-
auditHooks({ repositoryRoot: fixture.fixtureRoot });
|
|
127
|
-
assert.equal(existsSync(path.join(fixture.fixtureRoot, "audit-executed")), false);
|
|
128
|
-
} finally {
|
|
129
|
-
rmSync(fixture.fixtureRoot, { force: true, recursive: true });
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("reports every unclassified target", () => {
|
|
134
|
-
const temporaryRoot = mkdtempSync(path.join(tmpdir(), "hook-audit-"));
|
|
135
|
-
const catalogPath = path.join(temporaryRoot, "lifecycle.json");
|
|
136
|
-
writeFileSync(catalogPath, '{"schema_version":1,"hooks":{}}', "utf8");
|
|
137
|
-
try {
|
|
138
|
-
const report = auditHooks({ catalogPath, repositoryRoot });
|
|
139
|
-
assert.equal(report.summary.unclassifiedCount, report.summary.uniqueTargetCount);
|
|
140
|
-
assert.equal(report.findings.some(({ code }) => code === "UNCLASSIFIED_HOOK"), true);
|
|
141
|
-
} finally {
|
|
142
|
-
rmSync(temporaryRoot, { force: true, recursive: true });
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("reports canonical native Git hooks from the installer registry", () => {
|
|
147
|
-
const report = auditHooks({ repositoryRoot });
|
|
148
|
-
assert.deepEqual(
|
|
149
|
-
report.knownGitRegistrations.map(({ target }) => target),
|
|
150
|
-
["git:pre-commit", "git:pre-push", "git:post-commit"],
|
|
151
|
-
);
|
|
152
|
-
assert.equal(report.summary.knownGitHookCount, 3);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("keeps each hosted matcher as narrow as its roster", () => {
|
|
156
|
-
const fixture = createAuditFixture({
|
|
157
|
-
canonicalHooks: {
|
|
158
|
-
PreToolUse: [{
|
|
159
|
-
matcher: "Bash|Write",
|
|
160
|
-
hooks: [{ type: "command", command: "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pre_tool_use_dispatcher.py" }],
|
|
161
|
-
}],
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
const constantsRoot = path.join(fixture.canonicalHooksRoot, "hooks_constants");
|
|
165
|
-
writeHookFile(fixture.canonicalHooksRoot, "blocking/pre_tool_use_dispatcher.py");
|
|
166
|
-
mkdirSync(constantsRoot, { recursive: true });
|
|
167
|
-
writeFileSync(
|
|
168
|
-
path.join(constantsRoot, "pre_tool_use_dispatcher_constants.py"),
|
|
169
|
-
[
|
|
170
|
-
'BASH_TOOL_NAME = "Bash"',
|
|
171
|
-
"ALL_BASH_TOOL_NAMES = frozenset({BASH_TOOL_NAME})",
|
|
172
|
-
"ALL_HOSTED_HOOK_ENTRIES = (",
|
|
173
|
-
'HostedHookEntry(script_relative_path="blocking/shell_substitution_blocker.py", applicable_tool_names=ALL_BASH_TOOL_NAMES),',
|
|
174
|
-
")",
|
|
175
|
-
].join("\n"),
|
|
176
|
-
"utf8",
|
|
177
|
-
);
|
|
178
|
-
try {
|
|
179
|
-
const report = auditHooks({ repositoryRoot: fixture.fixtureRoot });
|
|
180
|
-
const shellGate = report.hostedRegistrations.find(({ target }) =>
|
|
181
|
-
target.endsWith("shell_substitution_blocker.py"),
|
|
182
|
-
);
|
|
183
|
-
assert.equal(shellGate.matcher, "Bash");
|
|
184
|
-
} finally {
|
|
185
|
-
rmSync(fixture.fixtureRoot, { force: true, recursive: true });
|
|
186
|
-
}
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test("omits untrusted lifecycle prose from output", () => {
|
|
190
|
-
const temporaryRoot = mkdtempSync(path.join(tmpdir(), "hook-audit-"));
|
|
191
|
-
const catalogPath = path.join(temporaryRoot, "lifecycle.json");
|
|
192
|
-
const privateText = "PRIVATE_TOKEN python3 C:/Users/example/private.py";
|
|
193
|
-
const catalog = {
|
|
194
|
-
hooks: {
|
|
195
|
-
"script:blocking/code_rules_enforcer.py": {
|
|
196
|
-
lifecycle: "move_to_linter",
|
|
197
|
-
reason: privateText,
|
|
198
|
-
replacement: privateText,
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
writeFileSync(catalogPath, JSON.stringify(catalog), "utf8");
|
|
203
|
-
try {
|
|
204
|
-
const rendered = JSON.stringify(auditHooks({ catalogPath, repositoryRoot }));
|
|
205
|
-
assert.equal(rendered.includes(privateText), false);
|
|
206
|
-
} finally {
|
|
207
|
-
rmSync(temporaryRoot, { force: true, recursive: true });
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
test("classifies installed-only targets", () => {
|
|
212
|
-
const fixture = createAuditFixture();
|
|
213
|
-
const temporaryRoot = fixture.fixtureRoot;
|
|
214
|
-
const claudeRoot = path.join(fixture.homeDirectory, ".claude");
|
|
215
|
-
const codexRoot = path.join(fixture.homeDirectory, ".codex");
|
|
216
|
-
const installedOnly = {
|
|
217
|
-
hooks: {
|
|
218
|
-
SessionStart: [{ hooks: [{ command: "node cleanup.mjs", type: "command" }], matcher: "" }],
|
|
219
|
-
},
|
|
220
|
-
};
|
|
221
|
-
writeJson(path.join(claudeRoot, "settings.json"), installedOnly);
|
|
222
|
-
writeJson(path.join(codexRoot, "hooks.json"), { hooks: {} });
|
|
223
|
-
const catalogPath = path.join(temporaryRoot, "lifecycle.json");
|
|
224
|
-
writeFileSync(catalogPath, '{"hooks":{}}', "utf8");
|
|
225
|
-
try {
|
|
226
|
-
const report = auditHooks({
|
|
227
|
-
catalogPath,
|
|
228
|
-
homeDirectory: fixture.homeDirectory,
|
|
229
|
-
includeInstalled: true,
|
|
230
|
-
requireNonblocking: true,
|
|
231
|
-
repositoryRoot: temporaryRoot,
|
|
232
|
-
});
|
|
233
|
-
const missingTargets = new Set(
|
|
234
|
-
report.findings.filter(({ code }) => code === "UNCLASSIFIED_HOOK").map(({ target }) => target),
|
|
235
|
-
);
|
|
236
|
-
assert.equal(missingTargets.has("external:cleanup.mjs"), true);
|
|
237
|
-
} finally {
|
|
238
|
-
rmSync(temporaryRoot, { force: true, recursive: true });
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test("reads dispatcher children from the installed hook tree", () => {
|
|
243
|
-
const fixture = createAuditFixture();
|
|
244
|
-
const temporaryRoot = fixture.fixtureRoot;
|
|
245
|
-
const claudeRoot = path.join(fixture.homeDirectory, ".claude");
|
|
246
|
-
const installedHooks = path.join(claudeRoot, "hooks");
|
|
247
|
-
const constantsRoot = path.join(installedHooks, "hooks_constants");
|
|
248
|
-
const blockingRoot = path.join(installedHooks, "blocking");
|
|
249
|
-
mkdirSync(constantsRoot, { recursive: true });
|
|
250
|
-
mkdirSync(blockingRoot, { recursive: true });
|
|
251
|
-
const config = {
|
|
252
|
-
hooks: {
|
|
253
|
-
PreToolUse: [{
|
|
254
|
-
hooks: [{
|
|
255
|
-
command: "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pre_tool_use_dispatcher.py",
|
|
256
|
-
type: "command",
|
|
257
|
-
}],
|
|
258
|
-
matcher: "Write",
|
|
259
|
-
}],
|
|
260
|
-
},
|
|
261
|
-
};
|
|
262
|
-
const roster = [
|
|
263
|
-
'WRITE_TOOL_NAME = "Write"',
|
|
264
|
-
"ALL_WRITE_TOOL_NAMES = frozenset({WRITE_TOOL_NAME})",
|
|
265
|
-
"ALL_HOSTED_HOOK_ENTRIES = (",
|
|
266
|
-
'HostedHookEntry(script_relative_path="blocking/installed_only.py", applicable_tool_names=ALL_WRITE_TOOL_NAMES),',
|
|
267
|
-
")",
|
|
268
|
-
].join("\n");
|
|
269
|
-
writeJson(path.join(claudeRoot, "settings.json"), config);
|
|
270
|
-
writeFileSync(path.join(constantsRoot, "pre_tool_use_dispatcher_constants.py"), roster, "utf8");
|
|
271
|
-
writeFileSync(path.join(blockingRoot, "pre_tool_use_dispatcher.py"), "", "utf8");
|
|
272
|
-
writeJson(path.join(fixture.homeDirectory, ".codex", "hooks.json"), { hooks: {} });
|
|
273
|
-
try {
|
|
274
|
-
const report = auditHooks({
|
|
275
|
-
homeDirectory: fixture.homeDirectory,
|
|
276
|
-
includeInstalled: true,
|
|
277
|
-
repositoryRoot: temporaryRoot,
|
|
278
|
-
});
|
|
279
|
-
assert.equal(
|
|
280
|
-
report.installedLogicalRegistrations.some(({ target }) => target.endsWith("installed_only.py")),
|
|
281
|
-
true,
|
|
282
|
-
);
|
|
283
|
-
assert.equal(
|
|
284
|
-
report.findings.some(({ code, target }) =>
|
|
285
|
-
code === "MISSING_HOOK_TARGET" && target === "script:blocking/installed_only.py",
|
|
286
|
-
),
|
|
287
|
-
true,
|
|
288
|
-
);
|
|
289
|
-
} finally {
|
|
290
|
-
rmSync(temporaryRoot, { force: true, recursive: true });
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
test("reports missing installed dispatcher targets", () => {
|
|
295
|
-
const fixture = createAuditFixture({
|
|
296
|
-
claudeHooks: {
|
|
297
|
-
hooks: {
|
|
298
|
-
PreToolUse: [{
|
|
299
|
-
matcher: "Write",
|
|
300
|
-
hooks: [{
|
|
301
|
-
type: "command",
|
|
302
|
-
command: "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pre_tool_use_dispatcher.py",
|
|
303
|
-
}],
|
|
304
|
-
}],
|
|
305
|
-
},
|
|
306
|
-
},
|
|
307
|
-
});
|
|
308
|
-
const constantsPath = path.join(
|
|
309
|
-
fixture.homeDirectory,
|
|
310
|
-
".claude",
|
|
311
|
-
"hooks",
|
|
312
|
-
"hooks_constants",
|
|
313
|
-
"pre_tool_use_dispatcher_constants.py",
|
|
314
|
-
);
|
|
315
|
-
mkdirSync(path.dirname(constantsPath), { recursive: true });
|
|
316
|
-
writeFileSync(constantsPath, "ALL_HOSTED_HOOK_ENTRIES = ()\n", "utf8");
|
|
317
|
-
try {
|
|
318
|
-
const report = auditHooks({
|
|
319
|
-
homeDirectory: fixture.homeDirectory,
|
|
320
|
-
includeInstalled: true,
|
|
321
|
-
repositoryRoot: fixture.fixtureRoot,
|
|
322
|
-
});
|
|
323
|
-
assert.equal(
|
|
324
|
-
report.findings.some(({ code, scope, target }) =>
|
|
325
|
-
code === "MISSING_HOOK_TARGET" &&
|
|
326
|
-
scope === "claude" &&
|
|
327
|
-
target === "script:blocking/pre_tool_use_dispatcher.py",
|
|
328
|
-
),
|
|
329
|
-
true,
|
|
330
|
-
);
|
|
331
|
-
} finally {
|
|
332
|
-
rmSync(fixture.fixtureRoot, { force: true, recursive: true });
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
test("rejects an installed-only registration without nonblocking lifecycle", () => {
|
|
337
|
-
const fixture = createAuditFixture({
|
|
338
|
-
claudeHooks: {
|
|
339
|
-
hooks: {
|
|
340
|
-
SessionStart: [{
|
|
341
|
-
hooks: [{ command: "node cleanup.mjs", type: "command" }],
|
|
342
|
-
}],
|
|
343
|
-
},
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
const catalogPath = path.join(fixture.fixtureRoot, "lifecycle.json");
|
|
347
|
-
writeJson(catalogPath, {
|
|
348
|
-
hooks: {
|
|
349
|
-
"external:cleanup.mjs": {
|
|
350
|
-
lifecycle: "delete",
|
|
351
|
-
reason: "The installed entry is retired.",
|
|
352
|
-
replacement: null,
|
|
353
|
-
},
|
|
354
|
-
},
|
|
355
|
-
});
|
|
356
|
-
try {
|
|
357
|
-
const report = auditHooks({
|
|
358
|
-
catalogPath,
|
|
359
|
-
homeDirectory: fixture.homeDirectory,
|
|
360
|
-
includeInstalled: true,
|
|
361
|
-
requireNonblocking: true,
|
|
362
|
-
repositoryRoot: fixture.fixtureRoot,
|
|
363
|
-
});
|
|
364
|
-
assert.equal(report.registrationEligibility.status, "ineligible");
|
|
365
|
-
assert.equal(
|
|
366
|
-
report.findings.some(({ code, target }) =>
|
|
367
|
-
code === "NONBLOCKING_LIFECYCLE_MISMATCH" && target === "external:cleanup.mjs",
|
|
368
|
-
),
|
|
369
|
-
true,
|
|
370
|
-
);
|
|
371
|
-
} finally {
|
|
372
|
-
rmSync(fixture.fixtureRoot, { force: true, recursive: true });
|
|
373
|
-
}
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
test("checks canonical managed parity for Codex registrations", () => {
|
|
377
|
-
const fixture = createAuditFixture({
|
|
378
|
-
canonicalHooks: {
|
|
379
|
-
PreToolUse: [{
|
|
380
|
-
matcher: "Write",
|
|
381
|
-
hooks: [{ type: "command", command: "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/write_existing_file_blocker.py" }],
|
|
382
|
-
}],
|
|
383
|
-
},
|
|
384
|
-
});
|
|
385
|
-
try {
|
|
386
|
-
const report = auditHooks({
|
|
387
|
-
homeDirectory: fixture.homeDirectory,
|
|
388
|
-
includeInstalled: true,
|
|
389
|
-
repositoryRoot: fixture.fixtureRoot,
|
|
390
|
-
});
|
|
391
|
-
assert.equal(
|
|
392
|
-
report.findings.some(({ code, scope, target }) =>
|
|
393
|
-
code === "MISSING_MANAGED_REGISTRATION" &&
|
|
394
|
-
scope === "codex" &&
|
|
395
|
-
target === "script:blocking/write_existing_file_blocker.py",
|
|
396
|
-
),
|
|
397
|
-
true,
|
|
398
|
-
);
|
|
399
|
-
} finally {
|
|
400
|
-
rmSync(fixture.fixtureRoot, { force: true, recursive: true });
|
|
401
|
-
}
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
test("compares managed registrations by event, matcher, and target", () => {
|
|
405
|
-
const command = "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/write_existing_file_blocker.py";
|
|
406
|
-
const fixture = createAuditFixture({
|
|
407
|
-
canonicalHooks: {
|
|
408
|
-
PreToolUse: [{ matcher: "Write", hooks: [{ type: "command", command }] }],
|
|
409
|
-
},
|
|
410
|
-
claudeHooks: {
|
|
411
|
-
hooks: {
|
|
412
|
-
PostToolUse: [{ matcher: "Read", hooks: [{ type: "command", command }] }],
|
|
413
|
-
},
|
|
414
|
-
},
|
|
415
|
-
codexHooks: {
|
|
416
|
-
hooks: {
|
|
417
|
-
PreToolUse: [{ matcher: "Write", hooks: [{ type: "command", command }] }],
|
|
418
|
-
},
|
|
419
|
-
},
|
|
420
|
-
});
|
|
421
|
-
writeHookFile(path.join(fixture.homeDirectory, ".claude", "hooks"), "blocking/write_existing_file_blocker.py");
|
|
422
|
-
writeHookFile(path.join(fixture.homeDirectory, ".codex", "hooks"), "blocking/write_existing_file_blocker.py");
|
|
423
|
-
try {
|
|
424
|
-
const report = auditHooks({
|
|
425
|
-
homeDirectory: fixture.homeDirectory,
|
|
426
|
-
includeInstalled: true,
|
|
427
|
-
repositoryRoot: fixture.fixtureRoot,
|
|
428
|
-
});
|
|
429
|
-
assert.equal(
|
|
430
|
-
report.findings.some(({ code, scope, target }) =>
|
|
431
|
-
code === "MISSING_MANAGED_REGISTRATION" &&
|
|
432
|
-
scope === "claude" &&
|
|
433
|
-
target === "script:blocking/write_existing_file_blocker.py",
|
|
434
|
-
),
|
|
435
|
-
true,
|
|
436
|
-
);
|
|
437
|
-
assert.equal(
|
|
438
|
-
report.findings.some(({ code, scope }) =>
|
|
439
|
-
code === "MISSING_MANAGED_REGISTRATION" && scope === "codex",
|
|
440
|
-
),
|
|
441
|
-
false,
|
|
442
|
-
);
|
|
443
|
-
} finally {
|
|
444
|
-
rmSync(fixture.fixtureRoot, { force: true, recursive: true });
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
test("finds default Git hooks from a worktree", () => {
|
|
449
|
-
const sourceRoot = mkdtempSync(path.join(tmpdir(), "hook-audit-git-"));
|
|
450
|
-
const worktreeRoot = path.join(sourceRoot, "worktree");
|
|
451
|
-
const globalConfigPath = path.join(sourceRoot, "global-gitconfig");
|
|
452
|
-
writeFileSync(globalConfigPath, "", "utf8");
|
|
453
|
-
const environment = { ...process.env, GIT_CONFIG_GLOBAL: globalConfigPath, GIT_CONFIG_NOSYSTEM: "1" };
|
|
454
|
-
const previousGlobalConfig = process.env.GIT_CONFIG_GLOBAL;
|
|
455
|
-
const previousNoSystem = process.env.GIT_CONFIG_NOSYSTEM;
|
|
456
|
-
process.env.GIT_CONFIG_GLOBAL = globalConfigPath;
|
|
457
|
-
process.env.GIT_CONFIG_NOSYSTEM = "1";
|
|
458
|
-
try {
|
|
459
|
-
runGit(sourceRoot, ["init", "--quiet"], environment);
|
|
460
|
-
runGit(sourceRoot, ["config", "user.email", "hook-audit@example.invalid"], environment);
|
|
461
|
-
runGit(sourceRoot, ["config", "user.name", "Hook Audit"], environment);
|
|
462
|
-
runGit(sourceRoot, ["commit", "--quiet", "--allow-empty", "-m", "fixture"], environment);
|
|
463
|
-
runGit(sourceRoot, ["worktree", "add", "--quiet", worktreeRoot, "HEAD"], environment);
|
|
464
|
-
writeHookFile(path.join(sourceRoot, ".git", "hooks"), "pre-commit");
|
|
465
|
-
writeHookFile(path.join(sourceRoot, ".git", "hooks"), "post-commit");
|
|
466
|
-
const canonicalHooksRoot = path.join(worktreeRoot, "packages", "claude-dev-env", "hooks");
|
|
467
|
-
const homeDirectory = path.join(sourceRoot, "home");
|
|
468
|
-
mkdirSync(canonicalHooksRoot, { recursive: true });
|
|
469
|
-
mkdirSync(path.join(homeDirectory, ".claude", "hooks"), { recursive: true });
|
|
470
|
-
mkdirSync(path.join(homeDirectory, ".codex", "hooks"), { recursive: true });
|
|
471
|
-
writeJson(path.join(canonicalHooksRoot, "hooks.json"), { hooks: {} });
|
|
472
|
-
writeJson(path.join(homeDirectory, ".claude", "settings.json"), { hooks: {} });
|
|
473
|
-
writeJson(path.join(homeDirectory, ".codex", "hooks.json"), { hooks: {} });
|
|
474
|
-
const report = auditHooks({
|
|
475
|
-
homeDirectory,
|
|
476
|
-
includeInstalled: true,
|
|
477
|
-
requireNonblocking: true,
|
|
478
|
-
repositoryRoot: worktreeRoot,
|
|
479
|
-
});
|
|
480
|
-
assert.deepEqual(report.gitRegistrations.map(({ target }) => target), [
|
|
481
|
-
"git:pre-commit",
|
|
482
|
-
"git:post-commit",
|
|
483
|
-
]);
|
|
484
|
-
assert.equal(
|
|
485
|
-
report.findings.some(({ code, target }) =>
|
|
486
|
-
code === "NATIVE_GIT_BLOCKING_HOOK" && target === "git:pre-commit",
|
|
487
|
-
),
|
|
488
|
-
true,
|
|
489
|
-
);
|
|
490
|
-
} finally {
|
|
491
|
-
if (previousGlobalConfig === undefined) delete process.env.GIT_CONFIG_GLOBAL;
|
|
492
|
-
else process.env.GIT_CONFIG_GLOBAL = previousGlobalConfig;
|
|
493
|
-
if (previousNoSystem === undefined) delete process.env.GIT_CONFIG_NOSYSTEM;
|
|
494
|
-
else process.env.GIT_CONFIG_NOSYSTEM = previousNoSystem;
|
|
495
|
-
rmSync(sourceRoot, { force: true, recursive: true });
|
|
496
|
-
}
|
|
497
|
-
});
|