vigiles 15.0.2 → 15.0.3
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/dist/adapters/claude-code/layout.js +3 -0
- package/dist/adapters/claude-code/run-scripts.d.ts +21 -9
- package/dist/adapters/claude-code/run-scripts.js +33 -22
- package/dist/adapters/codex/driver.js +3 -1
- package/dist/adapters/codex/eval.js +5 -0
- package/dist/audit-score.js +32 -6
- package/dist/check-count.d.ts +62 -3
- package/dist/check-count.js +123 -5
- package/dist/check.d.ts +55 -0
- package/dist/check.js +91 -0
- package/dist/cli.js +456 -54
- package/dist/core/bash-effects.d.ts +41 -0
- package/dist/core/bash-effects.js +278 -0
- package/dist/core/foreign-runner.d.ts +185 -0
- package/dist/core/foreign-runner.js +228 -0
- package/dist/core/harness-driver.d.ts +18 -1
- package/dist/core/hook-program.d.ts +319 -20
- package/dist/core/hook-program.js +743 -49
- package/dist/core/layout.d.ts +13 -0
- package/dist/core/lethal-trifecta.d.ts +93 -0
- package/dist/core/lethal-trifecta.js +409 -65
- package/dist/core/markdown.d.ts +32 -0
- package/dist/core/markdown.js +36 -0
- package/dist/core/merge-conflict.d.ts +50 -0
- package/dist/core/merge-conflict.js +84 -0
- package/dist/core/test-file-ext.d.ts +80 -0
- package/dist/core/test-file-ext.js +90 -0
- package/dist/core/types.d.ts +11 -0
- package/dist/coverage-artifact.d.ts +383 -0
- package/dist/coverage-artifact.js +586 -0
- package/dist/coverage-evidence.d.ts +87 -79
- package/dist/coverage-evidence.js +212 -211
- package/dist/coverage-probe.d.ts +125 -0
- package/dist/coverage-probe.js +700 -0
- package/dist/doc-commands.d.ts +119 -0
- package/dist/doc-commands.js +158 -0
- package/dist/eval-lock.d.ts +9 -0
- package/dist/eval-lock.js +14 -2
- package/dist/eval.js +28 -0
- package/dist/fs-walk.d.ts +80 -0
- package/dist/fs-walk.js +148 -0
- package/dist/harness-assert.js +42 -3
- package/dist/harness-test.js +31 -2
- package/dist/judge.js +4 -0
- package/dist/leaderboard.d.ts +16 -1
- package/dist/leaderboard.js +19 -2
- package/dist/load-hook.js +6 -1
- package/dist/mock-model.d.ts +58 -4
- package/dist/mock-model.js +133 -5
- package/dist/observe.d.ts +1 -1
- package/dist/observe.js +44 -1
- package/dist/plugin-loader.js +52 -13
- package/dist/run-hook.d.ts +58 -0
- package/dist/run-hook.js +70 -0
- package/dist/run-script.js +75 -0
- package/dist/scaffold-test.d.ts +3 -0
- package/dist/scaffold-test.js +10 -5
- package/dist/scan-behavioral.js +4 -0
- package/dist/scan-core.d.ts +22 -0
- package/dist/scan-core.js +46 -14
- package/dist/scan-files.js +19 -2
- package/dist/scan.d.ts +22 -5
- package/dist/scan.js +67 -13
- package/dist/skill-contract.d.ts +46 -0
- package/dist/skill-contract.js +201 -0
- package/dist/skill-refs.d.ts +67 -0
- package/dist/skill-refs.js +117 -0
- package/dist/test-coverage-files.d.ts +7 -1
- package/dist/test-coverage-files.js +66 -55
- package/dist/test-coverage.d.ts +155 -19
- package/dist/test-coverage.js +430 -91
- package/dist/testing.d.ts +8 -2
- package/dist/testing.js +19 -1
- package/dist/trigger-containment.d.ts +85 -0
- package/dist/trigger-containment.js +125 -0
- package/dist/ts-runner-caps.d.ts +16 -0
- package/dist/ts-runner-caps.js +43 -0
- package/dist/unit.d.ts +2 -2
- package/dist/unit.js +2 -1
- package/hooks/eval-lock-nudge.sh +14 -6
- package/package.json +2 -2
- package/skills/test-harness/SKILL.md +68 -2
|
@@ -97,6 +97,14 @@ export interface CommandView {
|
|
|
97
97
|
* line happens to be side-effecting for an unrelated reason (`grep -c x
|
|
98
98
|
* notes/S.md 2>/dev/null`) matches both and gets blocked. To gate WRITES to a
|
|
99
99
|
* directory, use {@link writesTo}; conflating the two is the trap.
|
|
100
|
+
*
|
|
101
|
+
* OVER-INCLUSIVE ON PURPOSE, AND THE OPPOSITE WAY ROUND FROM {@link
|
|
102
|
+
* PathView.under}. This is a DENYLIST primitive — every caller spells
|
|
103
|
+
* `touches(secret) ? deny() : allow()` — so a miss is an ALLOW, not silence. An
|
|
104
|
+
* unprovable answer is therefore reported as a MATCH: a token that could name
|
|
105
|
+
* something under the prefix under some root or some `cd` matches, and the gate
|
|
106
|
+
* blocks. See {@link prefixVerdict} for the three answers and {@link
|
|
107
|
+
* matchesPrefix} for the two biases.
|
|
100
108
|
*/
|
|
101
109
|
touches(prefixes: readonly string[]): boolean;
|
|
102
110
|
/**
|
|
@@ -130,7 +138,36 @@ export interface CommandView {
|
|
|
130
138
|
*/
|
|
131
139
|
pipesToShell(): boolean;
|
|
132
140
|
}
|
|
133
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Trailing separators removed — EXCEPT the one that is the path itself.
|
|
143
|
+
*
|
|
144
|
+
* 🔴 THE CARVE-OUT IS THE RULE, NOT AN EDGE CASE, and skipping it fails
|
|
145
|
+
* SILENTLY. Strip every trailing separator and the POSIX root `"/"` becomes
|
|
146
|
+
* `""`, the Windows drive root `"C:/"` becomes `"C:"` — and `isAbsoluteRef`
|
|
147
|
+
* reads a bare drive letter as RELATIVE. Nothing throws: an allowlist prefix of
|
|
148
|
+
* `"C:/"` simply stops matching, so a confinement gate denies every path and a
|
|
149
|
+
* react hook goes quiet, both looking like a correct decision.
|
|
150
|
+
*
|
|
151
|
+
* This lives here, in one place, because the same normalisation is applied to
|
|
152
|
+
* two different things — the PREFIX being matched against, and the project ROOT
|
|
153
|
+
* a path is resolved against (`run-hook.ts` imports it for the latter). Round 28
|
|
154
|
+
* fixed the root and left the prefix; round 29 found the prefix. Two copies of a
|
|
155
|
+
* rule is how the second one survives the fix to the first.
|
|
156
|
+
*/
|
|
157
|
+
export declare function trimTrailingSeparators(path: string): string;
|
|
158
|
+
/**
|
|
159
|
+
* An AST-backed view of a Bash command.
|
|
160
|
+
*
|
|
161
|
+
* @param raw - the command line as the tool event carried it.
|
|
162
|
+
* @param root - the project root repo-relative prefixes resolve against, from
|
|
163
|
+
* the runtime ({@link projectRootOf}: `$CLAUDE_PROJECT_DIR`, else the event's
|
|
164
|
+
* own `cwd`, never `process.cwd()`). Omitting it does NOT disarm the denylist
|
|
165
|
+
* matchers — without a root an absolute token is `"undecidable"` rather than
|
|
166
|
+
* placeable, and {@link CommandView.touches} treats that as a match. It does
|
|
167
|
+
* cost precision: with a root, `/somewhere-else/notes/x` is provably not this
|
|
168
|
+
* repo's `notes`; without one it can only be over-blocked.
|
|
169
|
+
*/
|
|
170
|
+
export declare function commandView(raw: string, root?: string): CommandView;
|
|
134
171
|
/**
|
|
135
172
|
* The typed event a Bash gate decides over. Generic over the declared context
|
|
136
173
|
* `needs` (`N`) so `e.ctx` exposes ONLY the facts the hook declared — reading an
|
|
@@ -160,13 +197,24 @@ export declare const tool: (name: string) => {
|
|
|
160
197
|
tool: string;
|
|
161
198
|
};
|
|
162
199
|
export declare function defineHook<const N extends readonly NeedSpec[] = readonly []>(p: HookProgram<N>): HookProgram<N>;
|
|
163
|
-
/**
|
|
200
|
+
/**
|
|
201
|
+
* Build the typed event from a raw PreToolUse event, then decide.
|
|
202
|
+
*
|
|
203
|
+
* `root` is the project root repo-relative prefixes in the hook resolve against,
|
|
204
|
+
* threaded exactly as {@link decideFileGate} threads it: omitted, it falls back
|
|
205
|
+
* to the event's OWN `cwd` (a payload field, so this stays pure and reads no
|
|
206
|
+
* environment); the CLI passes {@link projectRootOf}, which prefers
|
|
207
|
+
* `$CLAUDE_PROJECT_DIR`. Until 2026-08-12 no root reached here at all, and the
|
|
208
|
+
* denylist matchers on {@link CommandView} were bypassable by spelling a path
|
|
209
|
+
* absolutely — see the block above {@link PrefixVerdict} for the measurement.
|
|
210
|
+
*/
|
|
164
211
|
export declare function decideProgram<N extends readonly NeedSpec[]>(program: HookProgram<N>, rawEvent: {
|
|
165
212
|
tool_name?: string;
|
|
166
213
|
tool_input?: {
|
|
167
214
|
command?: unknown;
|
|
168
215
|
};
|
|
169
|
-
|
|
216
|
+
cwd?: unknown;
|
|
217
|
+
}, ctx?: Record<string, string | boolean>, root?: string | undefined): Decision;
|
|
170
218
|
/** Map a Decision to the CC hook exit code — the protocol the author never writes. */
|
|
171
219
|
export declare function decisionExitCode(d: Decision): number;
|
|
172
220
|
/**
|
|
@@ -270,10 +318,105 @@ export declare function verifyHookStamp(source: string, stamp: SHA256Hash): bool
|
|
|
270
318
|
/** An AST-free view of a file path — the matching primitive for file tools. */
|
|
271
319
|
export interface PathView {
|
|
272
320
|
readonly raw: string;
|
|
273
|
-
/**
|
|
321
|
+
/**
|
|
322
|
+
* True iff the path sits under at least one allowed prefix (e.g. "src/**").
|
|
323
|
+
*
|
|
324
|
+
* A prefix is matched in ITS OWN spelling: a repo-relative prefix (`"src"`,
|
|
325
|
+
* `"migratsiya/papers/"`) is compared against the path made repo-relative, an
|
|
326
|
+
* absolute prefix (`"/etc"`) against the path made absolute. Which of those is
|
|
327
|
+
* available depends on the project root — see {@link pathView}.
|
|
328
|
+
*/
|
|
274
329
|
under(prefixes: readonly string[]): boolean;
|
|
330
|
+
/**
|
|
331
|
+
* The path as a repo-relative reference, or `undefined` when this view cannot
|
|
332
|
+
* produce one — an absolute path with no known root, or a path that resolves
|
|
333
|
+
* OUTSIDE the root (`/etc/passwd`, a sibling checkout). Exposed because a hook
|
|
334
|
+
* that wants to report or re-match the path needs the same answer `under`
|
|
335
|
+
* used, and `raw` alone does not carry it.
|
|
336
|
+
*/
|
|
337
|
+
readonly rel: string | undefined;
|
|
275
338
|
}
|
|
276
|
-
|
|
339
|
+
/**
|
|
340
|
+
* A view of a file path, matched against prefixes by {@link PathView.under}.
|
|
341
|
+
*
|
|
342
|
+
* 🔴 `root` IS NOT OPTIONAL IN PRACTICE — WITHOUT IT THIS IS DEAD FOR EVERY REAL
|
|
343
|
+
* EVENT. Claude Code's Edit/Write/MultiEdit tools always send an ABSOLUTE
|
|
344
|
+
* `file_path`, and every hook in the wild writes repo-relative prefixes
|
|
345
|
+
* (`under(["migratsiya/papers/"])`). Before the root existed, the two could
|
|
346
|
+
* never meet: the comparison ran `"/home/u/repo/migratsiya/papers/x.tex"`
|
|
347
|
+
* against `"migratsiya/papers"` and returned `false` for all input, forever.
|
|
348
|
+
* MEASURED 2026-08-12 on the live runtime with one compiled react hook and two
|
|
349
|
+
* spellings of the same file — the relative one printed its checklist, the
|
|
350
|
+
* absolute one exited 0 in silence. Three shipped hooks in a consumer repo were
|
|
351
|
+
* dead by it, all three compiled *specifically to replace hooks that were dead*,
|
|
352
|
+
* and none of the tests noticed because the harness helper built relative paths.
|
|
353
|
+
*
|
|
354
|
+
* ## Which way a miss errs, and why it is not the same answer as `touches`
|
|
355
|
+
*
|
|
356
|
+
* `under` is the ALLOWLIST/COVERAGE primitive: its callers spell
|
|
357
|
+
* `under(P) ? allow() : deny()` (confinement) and `under(P) ? notice() : nothing()`
|
|
358
|
+
* (a nudge). For both, an unprovable answer must be `false` — a confinement gate
|
|
359
|
+
* denies (fails closed) and a nudge stays quiet. So:
|
|
360
|
+
*
|
|
361
|
+
* - path resolves OUTSIDE the root → `false` for every relative prefix. Not a
|
|
362
|
+
* bias, the truth: a repo-relative prefix names nothing in another checkout.
|
|
363
|
+
* This is the {@link resolveRef} lesson — the suffix match it replaced handed
|
|
364
|
+
* `/home/other-project/package.json` a grant meant for this repo's.
|
|
365
|
+
* - NO root known and the path is absolute → `false` for every relative prefix.
|
|
366
|
+
* Here we genuinely cannot tell, and `false` is the choice: silence, never a
|
|
367
|
+
* false grant. It is also the pre-fix behaviour, so nothing regresses.
|
|
368
|
+
*
|
|
369
|
+
* ⚠️ THE ASYMMETRY INVERTS FOR A DENYLIST, AND `under` DOES NOT SERVE THAT CASE.
|
|
370
|
+
* A gate written `under(["secrets"]) ? deny() : allow()` reads an unprovable
|
|
371
|
+
* `false` as ALLOW — a false grant. The Bash-side counterpart {@link
|
|
372
|
+
* CommandView.touches} exists precisely for that direction and takes the
|
|
373
|
+
* opposite bias from the SAME rule ({@link prefixVerdict}), which is why the
|
|
374
|
+
* resolver {@link matchesPrefix} makes each caller name its own. `PathView` has
|
|
375
|
+
* no such counterpart; a file-side denylist inherits the allowlist bias.
|
|
376
|
+
* Reported, not papered over.
|
|
377
|
+
*
|
|
378
|
+
* @param raw - the `file_path` the tool event carried, any spelling.
|
|
379
|
+
* @param root - the project root relative prefixes resolve against. Comes from
|
|
380
|
+
* the runtime (`$CLAUDE_PROJECT_DIR`, else the event's own `cwd`), never from
|
|
381
|
+
* `process.cwd()` — under a git worktree the process's cwd is a DIFFERENT
|
|
382
|
+
* checkout from the one the harness resolved the hook out of, and that exact
|
|
383
|
+
* mismatch already wedged a repo once.
|
|
384
|
+
*/
|
|
385
|
+
export declare function pathView(raw: string, root?: string): PathView;
|
|
386
|
+
/**
|
|
387
|
+
* The project root a compiled hook's repo-relative prefixes resolve against.
|
|
388
|
+
*
|
|
389
|
+
* Two sources, in order, and `process.cwd()` is deliberately NOT one of them:
|
|
390
|
+
*
|
|
391
|
+
* 1. **`$CLAUDE_PROJECT_DIR`** — what the harness itself resolved the hook's own
|
|
392
|
+
* path against (`.claude/settings.json` spells the command
|
|
393
|
+
* `"$CLAUDE_PROJECT_DIR/.claude/hooks/x.hook.ts"`), so it is the same root the
|
|
394
|
+
* hook was loaded from by construction.
|
|
395
|
+
* 2. **the event's `cwd`** — Claude Code puts the session's working directory in
|
|
396
|
+
* every hook payload. A fallback, not a peer: it is where the session is, and
|
|
397
|
+
* the two coincide in the ordinary case.
|
|
398
|
+
*
|
|
399
|
+
* `process.cwd()` is excluded because under a git worktree it can be a different
|
|
400
|
+
* checkout than the one the harness is driving, and a root from the wrong
|
|
401
|
+
* checkout turns every repo-relative prefix into a non-match — the same silent
|
|
402
|
+
* death this whole function exists to end. When NEITHER source is present the
|
|
403
|
+
* answer is `undefined` and the miss errs toward silence (see {@link pathView}).
|
|
404
|
+
*/
|
|
405
|
+
export declare function projectRootOf(event: {
|
|
406
|
+
readonly cwd?: unknown;
|
|
407
|
+
}, env?: Readonly<Record<string, string | undefined>>): string | undefined;
|
|
408
|
+
/**
|
|
409
|
+
* The one case {@link pathView} cannot answer and must not answer silently: an
|
|
410
|
+
* ABSOLUTE `file_path` with no project root in sight. Every repo-relative prefix
|
|
411
|
+
* in the hook returns `false`, so a gate waves everything through and a nudge
|
|
412
|
+
* never fires — indistinguishable, from the outside, from a hook with nothing to
|
|
413
|
+
* say. Returns the warning line, or `undefined` when there is nothing wrong.
|
|
414
|
+
*
|
|
415
|
+
* Narrow on purpose: a RELATIVE `file_path` is decidable without a root, so it
|
|
416
|
+
* warns about nothing. That matters because a react hook's `notice` also goes to
|
|
417
|
+
* stderr, and a warning on every event would read as the hook firing.
|
|
418
|
+
*/
|
|
419
|
+
export declare function undecidablePathWarning(filePath: unknown, root: string | undefined): string | undefined;
|
|
277
420
|
/** The event a file-tool gate decides over (Edit/Write/Read carry `file_path`). */
|
|
278
421
|
export interface FileToolEvent<N extends readonly NeedSpec[] = readonly ProviderName[]> {
|
|
279
422
|
readonly event: string;
|
|
@@ -298,13 +441,22 @@ export declare const tools: (...names: string[]) => {
|
|
|
298
441
|
tools: string[];
|
|
299
442
|
};
|
|
300
443
|
export declare function defineFileGate<const N extends readonly NeedSpec[] = readonly []>(p: Omit<FileGateHook<N>, "role">): FileGateHook<N>;
|
|
301
|
-
/**
|
|
444
|
+
/**
|
|
445
|
+
* Run a file-tool gate against a raw PreToolUse event (reads `file_path`).
|
|
446
|
+
*
|
|
447
|
+
* `root` is the project root repo-relative prefixes resolve against; omitted, it
|
|
448
|
+
* falls back to the event's OWN `cwd` (a payload field — this stays pure and
|
|
449
|
+
* reads no environment). The CLI passes {@link projectRootOf} instead, which
|
|
450
|
+
* prefers `$CLAUDE_PROJECT_DIR`. Without either, an absolute `file_path` matches
|
|
451
|
+
* no relative prefix — see {@link pathView} for why that direction was chosen.
|
|
452
|
+
*/
|
|
302
453
|
export declare function decideFileGate<N extends readonly NeedSpec[]>(hook: FileGateHook<N>, raw: {
|
|
303
454
|
tool_name?: string;
|
|
304
455
|
tool_input?: {
|
|
305
456
|
file_path?: unknown;
|
|
306
457
|
};
|
|
307
|
-
|
|
458
|
+
cwd?: unknown;
|
|
459
|
+
}, ctx?: Record<string, string | boolean>, root?: string | undefined): Decision;
|
|
308
460
|
/** The event a UserPromptSubmit gate decides over — it sees the prompt TEXT. */
|
|
309
461
|
export interface PromptEvent<N extends readonly NeedSpec[] = readonly ProviderName[]> {
|
|
310
462
|
readonly event: string;
|
|
@@ -443,14 +595,20 @@ export interface ReactHook {
|
|
|
443
595
|
readonly react: (e: ReactEvent) => Reaction;
|
|
444
596
|
}
|
|
445
597
|
export declare const defineReact: (p: Omit<ReactHook, "role">) => ReactHook;
|
|
446
|
-
/**
|
|
598
|
+
/**
|
|
599
|
+
* Run a react hook against a raw PostToolUse event → the (classified) Reaction.
|
|
600
|
+
*
|
|
601
|
+
* `root` behaves exactly as in {@link decideFileGate}: the event's own `cwd` by
|
|
602
|
+
* default, the CLI's {@link projectRootOf} when the runtime supplies one.
|
|
603
|
+
*/
|
|
447
604
|
export declare function runReact(hook: ReactHook, raw: {
|
|
448
605
|
tool_name?: string;
|
|
449
606
|
tool_input?: {
|
|
450
607
|
file_path?: unknown;
|
|
451
608
|
};
|
|
452
609
|
tool_response?: unknown;
|
|
453
|
-
|
|
610
|
+
cwd?: unknown;
|
|
611
|
+
}, root?: string | undefined): Reaction;
|
|
454
612
|
/** The raw event fields the decode functions read (the union across roles). */
|
|
455
613
|
export interface RawHookEvent {
|
|
456
614
|
readonly tool_name?: string;
|
|
@@ -466,6 +624,12 @@ export interface RawHookEvent {
|
|
|
466
624
|
readonly prompt?: string;
|
|
467
625
|
/** Stop / SubagentStop (stop-gate) — the prior-block loop guard. */
|
|
468
626
|
readonly stop_hook_active?: boolean;
|
|
627
|
+
/**
|
|
628
|
+
* The session's working directory, which Claude Code puts in EVERY hook
|
|
629
|
+
* payload. The fallback project root repo-relative path prefixes resolve
|
|
630
|
+
* against when the runtime does not pass one — see {@link projectRootOf}.
|
|
631
|
+
*/
|
|
632
|
+
readonly cwd?: string;
|
|
469
633
|
}
|
|
470
634
|
/** The normalized outcome of running a hook program — discriminated by role. */
|
|
471
635
|
export type HookProgramOutcome = {
|
|
@@ -478,26 +642,161 @@ export type HookProgramOutcome = {
|
|
|
478
642
|
readonly kind: "reaction";
|
|
479
643
|
readonly reaction: Reaction;
|
|
480
644
|
};
|
|
645
|
+
/** Record where a hook program was loaded from. Called by `loadHook` ONLY. */
|
|
646
|
+
export declare function rememberHookSource(hook: AnyHook, file: string): void;
|
|
647
|
+
/** The file a hook was loaded from, or `undefined` for one built in-process. */
|
|
648
|
+
export declare function hookSource(hook: AnyHook): string | undefined;
|
|
481
649
|
/**
|
|
482
650
|
* Evaluate a compiled hook against a raw event, in-process, dispatching by role:
|
|
483
651
|
* a gate → its `Decision`, an inject → the injected context text, a react → its
|
|
484
652
|
* (effect-classified) `Reaction`. Pure — no subprocess, no model. The ergonomic
|
|
485
653
|
* base for testing a compiled hook (see `assertHookDenies` / `assertHookAllows`).
|
|
486
654
|
*/
|
|
487
|
-
export declare function runHookProgram(hook: AnyHook, event: RawHookEvent, ctx?: Record<string, string | boolean
|
|
655
|
+
export declare function runHookProgram(hook: AnyHook, event: RawHookEvent, ctx?: Record<string, string | boolean>, root?: string | undefined): HookProgramOutcome;
|
|
656
|
+
/**
|
|
657
|
+
* The repository a wedged hook belongs to, as the runtime sees it. Supplied by
|
|
658
|
+
* the caller because core takes no `node:path` and reads no disk — and because
|
|
659
|
+
* the hook's own path does NOT determine it (see {@link isLoadPathRepairEvent}).
|
|
660
|
+
*/
|
|
661
|
+
export interface HookRepoPaths {
|
|
662
|
+
/**
|
|
663
|
+
* Absolute root that relative references resolve against. Must be the SAME
|
|
664
|
+
* root the runtime reads the hook and its stamp with, or the write accepted
|
|
665
|
+
* here is not the file the runtime reads.
|
|
666
|
+
*/
|
|
667
|
+
readonly root: string;
|
|
668
|
+
/**
|
|
669
|
+
* The actual files whose breakage can wedge THIS hook's module resolution,
|
|
670
|
+
* already resolved by the caller: the `package.json` at every ancestor of the
|
|
671
|
+
* hook, plus the repo's `.vigilesrc.json`. Node walks that chain, so a
|
|
672
|
+
* monorepo package's own `package.json` is on it and a sibling checkout's is
|
|
673
|
+
* not.
|
|
674
|
+
*/
|
|
675
|
+
readonly loadPathFiles: readonly string[];
|
|
676
|
+
}
|
|
488
677
|
/**
|
|
489
|
-
* True when this event IS the author repairing the hook —
|
|
490
|
-
*
|
|
678
|
+
* True when this event IS the author repairing the hook — a FILE WRITE, and only
|
|
679
|
+
* a file write.
|
|
680
|
+
*
|
|
681
|
+
* 🔴 THIS USED TO ADMIT A BASH COMMAND, AND THAT WAS THE WHOLE PROBLEM. The
|
|
682
|
+
* escape accepted `vigiles compile …`, and recognising a trusted ACTION from an
|
|
683
|
+
* untrusted STRING produced five findings in a row, each fix correct and each
|
|
684
|
+
* followed by another: `.some()` over the leaves → the operand (`compile
|
|
685
|
+
* /tmp/payload.spec.ts`, which `loadSpec` imports) → the executable path
|
|
686
|
+
* (`/tmp/vigiles compile`) → the working directory (`cd /tmp/evil && vigiles
|
|
687
|
+
* compile`). The degrees of freedom in a command string are unbounded — argv,
|
|
688
|
+
* cwd, PATH, `node_modules` resolution, a hostile `.vigilesrc.json`, a shell
|
|
689
|
+
* function — so no amount of parsing closes the class. Two more constraints
|
|
690
|
+
* predict two more findings.
|
|
691
|
+
*
|
|
692
|
+
* ## Why the command was not needed at all — measured, not argued
|
|
693
|
+
*
|
|
694
|
+
* MEASURED 2026-08-12 against the real runtime, on a stale-stamp fixture:
|
|
695
|
+
* writing `{}` into the stamp sidecar (or deleting it) makes
|
|
696
|
+
* `verifyStampOrRefuse` return early, so the hook LOADS AND ENFORCES again —
|
|
697
|
+
* `ls` passed and `git push --force` was still denied with the hook's own reason.
|
|
698
|
+
* The repo is unwedged by a file write, and the gate is back on duty; the author
|
|
699
|
+
* then runs `vigiles compile` through the NORMAL gate, needing no escape at all.
|
|
700
|
+
*
|
|
701
|
+
* MEASURED the same day on the load-wedge fixture: with `package.json` holding
|
|
702
|
+
* conflict markers, `vigiles compile <hook>` exits 1 —
|
|
703
|
+
* `Cannot load hook …: Invalid package config` — because compile has to load the
|
|
704
|
+
* hook through the same broken resolver the runtime just failed on. The command
|
|
705
|
+
* the escape existed to permit CANNOT repair that wedge. It was pure liability.
|
|
706
|
+
*
|
|
707
|
+
* So the Bash escape kept only a git whitelist — and a later round measured that
|
|
708
|
+
* those run `.git/hooks/*` too and deleted them as well. NO command is an escape
|
|
709
|
+
* now; the repair is what it always should have been: a write
|
|
710
|
+
* ({@link isLoadPathRepairEvent}).
|
|
711
|
+
*
|
|
712
|
+
* ## What is accepted here
|
|
713
|
+
*
|
|
714
|
+
* A write whose target is the hook's own source, or its stamp sidecar
|
|
715
|
+
* (`.vigiles/hooks/<basename>.json`). Both are repo-owned paths derived from the
|
|
716
|
+
* file the runtime was invoked with, never from the event.
|
|
717
|
+
*
|
|
718
|
+
* 🔴 SCOPE IS NOT THE SAME QUESTION AS EXECUTION. A previous round defended a
|
|
719
|
+
* suffix comparison here on the ground that a write executes nothing — true, and
|
|
720
|
+
* beside the point: `/home/another-project/.claude/hooks/guard.hook.mjs` ends
|
|
721
|
+
* the same way as this repo's hook, so a wedge in one checkout granted a write
|
|
722
|
+
* into another. Both sides now resolve against `repoRoot` and compare whole
|
|
723
|
+
* ({@link resolveRef}); the absolute-vs-relative tolerance that suffix matching
|
|
724
|
+
* was reached for is what resolution gives you properly.
|
|
725
|
+
*
|
|
726
|
+
* This grants nothing new: a Bash gate never gated file tools in the first place
|
|
727
|
+
* (the 2026-08-10 incident was fixed by hand-editing JSON while every Bash
|
|
728
|
+
* command was refused), and a file gate already let the hook's own source be
|
|
729
|
+
* rewritten, which is strictly more powerful than clearing its stamp.
|
|
730
|
+
*/
|
|
731
|
+
export declare function isStampRepairEvent(event: RawHookEvent, hookFile: string, repoRoot: string): boolean;
|
|
732
|
+
/**
|
|
733
|
+
* True when this event is a load-path REPAIR — a file write, and only a file
|
|
734
|
+
* write, to one of the files whose breakage takes the runtime down.
|
|
735
|
+
*
|
|
736
|
+
* 🔴 THE GIT ESCAPE USED TO LIVE HERE AND IT EXECUTED REPO CODE. `git merge
|
|
737
|
+
* --abort` · `git rebase --abort` · `git checkout -- <path>` were admitted on the
|
|
738
|
+
* reasoning that they "only move the tree to states git already holds; none
|
|
739
|
+
* executes a line of repo code". MEASURED against git 2.43.0 with hooks installed
|
|
740
|
+
* in `.git/hooks/`, and the reasoning is false for ALL THREE:
|
|
741
|
+
*
|
|
742
|
+
* git checkout -- f.txt → post-checkout (args `<sha> <sha> 0`)
|
|
743
|
+
* git merge --abort → reference-transaction (prepared, committed)
|
|
744
|
+
* git rebase --abort → reference-transaction (×4) + post-checkout
|
|
745
|
+
*
|
|
746
|
+
* `reference-transaction` fires on ANY ref update, and every one of these updates
|
|
747
|
+
* a ref. `.git/hooks/*` is writable by exactly the actor this door assumes —
|
|
748
|
+
* whoever could wedge the repo in the first place — so the whitelist was an
|
|
749
|
+
* arbitrary-execution path standing open precisely while the gate enforced
|
|
750
|
+
* nothing. Same shape as the `vigiles compile` escape one layer down: a command
|
|
751
|
+
* believed inert because of what it MEANS rather than what it DOES.
|
|
752
|
+
*
|
|
753
|
+
* ## Why the replacement is a write, not a safer command
|
|
754
|
+
*
|
|
755
|
+
* `git -c core.hooksPath=/nonexistent …` does suppress all three (measured: no
|
|
756
|
+
* hook ran for any of them). It was rejected anyway. It puts free-form structure
|
|
757
|
+
* back into the accepted string — the exact thing five findings on the compile
|
|
758
|
+
* door came from — and it buys nothing, because MEASURED on the load-wedge
|
|
759
|
+
* fixture a plain file write already restores the gate to ENFORCING:
|
|
760
|
+
*
|
|
761
|
+
* wedged : `ls` refused
|
|
762
|
+
* after Edit : `ls` allowed, `git push --force` DENIED by the hook's own reason
|
|
763
|
+
*
|
|
764
|
+
* You do not need to finish recovering, only to stop being wedged. Once the hook
|
|
765
|
+
* loads, the gate decides normally and `git merge --abort` is an ordinary allowed
|
|
766
|
+
* command — through the gate, not around it.
|
|
767
|
+
*
|
|
768
|
+
* ## What is accepted
|
|
769
|
+
*
|
|
770
|
+
* A write to the hook's own source, its stamp sidecar, or one of
|
|
771
|
+
* {@link HARNESS_CONFIG_FILES}. That set is COMPLETE for a compiled hook rather
|
|
772
|
+
* than a guess: `checkHookImports` rejects every import but `vigiles/hook`, so the
|
|
773
|
+
* load path is the hook file plus the config that resolves that specifier —
|
|
774
|
+
* nothing else can break it.
|
|
775
|
+
*
|
|
776
|
+
* A file write executes nothing, which is the property no command on this door
|
|
777
|
+
* ever had.
|
|
778
|
+
*
|
|
779
|
+
* ## …IN THIS REPOSITORY, which is a separate question
|
|
491
780
|
*
|
|
492
|
-
*
|
|
493
|
-
*
|
|
494
|
-
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
781
|
+
* "A write executes nothing" answers the EXECUTION objection and says nothing
|
|
782
|
+
* about SCOPE. Under the suffix comparison this used to do, a write to
|
|
783
|
+
* `/home/another-project/package.json` was accepted while THIS repo was wedged —
|
|
784
|
+
* a file that cannot repair the failure, in a checkout the wedged session has no
|
|
785
|
+
* business touching. Every candidate is now resolved against
|
|
786
|
+
* {@link HookRepoPaths.root} and compared whole against the paths the runtime
|
|
787
|
+
* itself derived ({@link HookRepoPaths.loadPathFiles}, plus this root's
|
|
788
|
+
* {@link HARNESS_CONFIG_FILES} so the floor case can never be argued away).
|
|
497
789
|
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
790
|
+
* ⚠️ THE HOOK'S OWN PATH DOES NOT GIVE THE ROOT, so the caller passes it. A hook
|
|
791
|
+
* lives at `.claude/hooks/…`, `.vigiles/hooks/…`, a plugin subdirectory or the
|
|
792
|
+
* repo root itself; there is no fixed depth to walk up, and finding a `.git`
|
|
793
|
+
* marker would mean reading disk, which core does not do. Nor would a
|
|
794
|
+
* git-derived root be RIGHT: `verifyStampOrRefuse` reads the hook and its stamp
|
|
795
|
+
* sidecar via `process.cwd()`, so any other root would accept writes to files
|
|
796
|
+
* the runtime does not read — repairs that do not repair. The root is therefore
|
|
797
|
+
* the one the rest of the runtime already uses, and it is passed in rather than
|
|
798
|
+
* invented here.
|
|
500
799
|
*/
|
|
501
|
-
export declare function
|
|
800
|
+
export declare function isLoadPathRepairEvent(event: RawHookEvent, hookFile: string, repo: HookRepoPaths): boolean;
|
|
502
801
|
export {};
|
|
503
802
|
//# sourceMappingURL=hook-program.d.ts.map
|