vigiles 14.13.0 → 14.13.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/dist/cli.js +41 -4
- package/dist/core/hook-program.d.ts +14 -0
- package/dist/core/hook-program.js +56 -0
- package/package.json +1 -1
- package/skills/debug-my-harness/SKILL.md +1 -0
- package/skills/edit-spec/SKILL.md +1 -0
- package/skills/strengthen/SKILL.md +1 -0
- package/skills/test-harness/SKILL.md +1 -0
package/dist/cli.js
CHANGED
|
@@ -4876,12 +4876,32 @@ function emitGate(decision, on, mode, file) {
|
|
|
4876
4876
|
return; // emit nothing, exit 0
|
|
4877
4877
|
}
|
|
4878
4878
|
}
|
|
4879
|
+
/**
|
|
4880
|
+
* Print the loud stderr banner that accompanies a REPAIR-only pass-through, and
|
|
4881
|
+
* return true — the caller allows exactly this one tool call. Shared by the two
|
|
4882
|
+
* refusal paths (stale stamp, unloadable program) so the wording can't drift.
|
|
4883
|
+
*/
|
|
4884
|
+
function announceRepairEscape(file, why) {
|
|
4885
|
+
console.error(`vigiles: hook ${file} ${why}.\n` +
|
|
4886
|
+
`vigiles: ALLOWING this one call because it is the repair action ` +
|
|
4887
|
+
`(\`vigiles compile ${file}\`, or an edit to the hook itself) — without ` +
|
|
4888
|
+
`this the gate blocks the only command that can fix it.\n` +
|
|
4889
|
+
`vigiles: every OTHER tool call stays BLOCKED until the hook is recompiled.`);
|
|
4890
|
+
return true;
|
|
4891
|
+
}
|
|
4879
4892
|
/**
|
|
4880
4893
|
* Fail closed if a stamp sidecar exists and the on-disk source no longer
|
|
4881
4894
|
* matches it — a hand-edit that smuggles in a capability breaks the stamp.
|
|
4882
4895
|
* No sidecar → run uncompiled (e.g. a test fixture or a not-yet-compiled hook).
|
|
4896
|
+
*
|
|
4897
|
+
* ONE exception, and it is loud: the author's own REPAIR action
|
|
4898
|
+
* ({@link isStampRepairEvent}) is let through, or a repo WEDGES. A stale stamp on
|
|
4899
|
+
* a PreToolUse Bash gate blocks every Bash command — including `vigiles compile`,
|
|
4900
|
+
* the only command that regenerates the stamp — so a normal edit-compile cycle
|
|
4901
|
+
* could paint you into a corner whose only escape was hand-editing
|
|
4902
|
+
* `.claude/settings.json` to unwire the gate. Observed 2026-08-03.
|
|
4883
4903
|
*/
|
|
4884
|
-
function verifyStampOrRefuse(file) {
|
|
4904
|
+
function verifyStampOrRefuse(file, event) {
|
|
4885
4905
|
const stampPath = hookStampPath(file);
|
|
4886
4906
|
if (!(0, node_fs_1.existsSync)(stampPath))
|
|
4887
4907
|
return;
|
|
@@ -4889,7 +4909,12 @@ function verifyStampOrRefuse(file) {
|
|
|
4889
4909
|
const { stamp } = JSON.parse((0, node_fs_1.readFileSync)(stampPath, "utf-8"));
|
|
4890
4910
|
const source = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(process.cwd(), file), "utf-8");
|
|
4891
4911
|
if (stamp && !(0, hook_program_js_1.verifyHookStamp)(source, stamp)) {
|
|
4892
|
-
|
|
4912
|
+
if ((0, hook_program_js_1.isStampRepairEvent)(event, file)) {
|
|
4913
|
+
announceRepairEscape(file, "does not match its compiled stamp");
|
|
4914
|
+
return;
|
|
4915
|
+
}
|
|
4916
|
+
console.error(`vigiles: hook ${file} does not match its compiled stamp (tampered). ` +
|
|
4917
|
+
`If YOU edited it, run \`vigiles compile ${file}\` to regenerate the stamp.`);
|
|
4893
4918
|
process.exit(2);
|
|
4894
4919
|
}
|
|
4895
4920
|
}
|
|
@@ -4902,7 +4927,10 @@ function verifyStampOrRefuse(file) {
|
|
|
4902
4927
|
* points at. Reads the live event on stdin, loads the typed program, verifies
|
|
4903
4928
|
* its stamp, and dispatches by role: a gate exits 2 + reason on `deny`; an
|
|
4904
4929
|
* inject prints `additionalContext`; a react runs its effect-classified
|
|
4905
|
-
* command. A hook that won't load
|
|
4930
|
+
* command. A hook that won't load — or whose stamp is stale — fails CLOSED
|
|
4931
|
+
* (exit 2), never silent-allow, with ONE loudly-announced exception: the repair
|
|
4932
|
+
* action itself ({@link isStampRepairEvent}), or the repo wedges with no way to
|
|
4933
|
+
* recompile.
|
|
4906
4934
|
*/
|
|
4907
4935
|
async function runHookProgramCommand(file) {
|
|
4908
4936
|
if (!file) {
|
|
@@ -4929,11 +4957,20 @@ async function runHookProgramCommand(file) {
|
|
|
4929
4957
|
program = await loadHookProgram(file);
|
|
4930
4958
|
}
|
|
4931
4959
|
catch {
|
|
4960
|
+
// Same bootstrap escape as the stale stamp below: an edit that leaves the
|
|
4961
|
+
// hook unloadable (a typo mid-edit) otherwise blocks the recompile that
|
|
4962
|
+
// would fix it. A hook that can't load enforces nothing either way, so
|
|
4963
|
+
// refusing the repair only wedges the repo. Everything else still fails
|
|
4964
|
+
// CLOSED (exit 2), never silent-allow.
|
|
4965
|
+
if ((0, hook_program_js_1.isStampRepairEvent)(event, file)) {
|
|
4966
|
+
announceRepairEscape(file, "cannot be loaded");
|
|
4967
|
+
return;
|
|
4968
|
+
}
|
|
4932
4969
|
console.error(`vigiles: cannot load hook program ${file}`);
|
|
4933
4970
|
process.exit(2);
|
|
4934
4971
|
return;
|
|
4935
4972
|
}
|
|
4936
|
-
verifyStampOrRefuse(file);
|
|
4973
|
+
verifyStampOrRefuse(file, event);
|
|
4937
4974
|
switch ((0, hook_program_js_1.dispatchKind)(program)) {
|
|
4938
4975
|
case "inject": {
|
|
4939
4976
|
const out = (0, hook_program_js_1.runInject)(program, { source: event.source });
|
|
@@ -485,5 +485,19 @@ export type HookProgramOutcome = {
|
|
|
485
485
|
* base for testing a compiled hook (see `assertHookDenies` / `assertHookAllows`).
|
|
486
486
|
*/
|
|
487
487
|
export declare function runHookProgram(hook: AnyHook, event: RawHookEvent, ctx?: Record<string, string | boolean>): HookProgramOutcome;
|
|
488
|
+
/**
|
|
489
|
+
* True when this event IS the author repairing the hook — the only thing a
|
|
490
|
+
* stale-stamp refusal must let through, or the repo wedges (see the note above):
|
|
491
|
+
*
|
|
492
|
+
* - a Bash command that invokes `vigiles compile` (however it's launched —
|
|
493
|
+
* `npx vigiles compile`, `pnpm exec vigiles compile`, `./node_modules/.bin/vigiles compile`),
|
|
494
|
+
* which is what regenerates the stamp; or
|
|
495
|
+
* - an edit/write whose target IS this hook's own source file, so a FILE gate
|
|
496
|
+
* over the repo can still be fixed by editing the hook again.
|
|
497
|
+
*
|
|
498
|
+
* AST-backed (`leafCommandsNormalized`), so it sees the invocation through a
|
|
499
|
+
* compound command or a wrapper, exactly like every other matcher here.
|
|
500
|
+
*/
|
|
501
|
+
export declare function isStampRepairEvent(event: RawHookEvent, hookFile: string): boolean;
|
|
488
502
|
export {};
|
|
489
503
|
//# sourceMappingURL=hook-program.d.ts.map
|
|
@@ -25,6 +25,7 @@ exports.runInject = runInject;
|
|
|
25
25
|
exports.responseView = responseView;
|
|
26
26
|
exports.runReact = runReact;
|
|
27
27
|
exports.runHookProgram = runHookProgram;
|
|
28
|
+
exports.isStampRepairEvent = isStampRepairEvent;
|
|
28
29
|
/**
|
|
29
30
|
* Compiled hooks — a hook as a CONSTRAINED TYPED PROGRAM, not arbitrary shell.
|
|
30
31
|
*
|
|
@@ -543,4 +544,59 @@ function runHookProgram(hook, event, ctx = {}) {
|
|
|
543
544
|
return (0, hash_js_1.assertNever)(kind);
|
|
544
545
|
}
|
|
545
546
|
}
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
// Stale-stamp REPAIR detection — the bootstrap deadlock's escape hatch.
|
|
549
|
+
//
|
|
550
|
+
// `verifyStampOrRefuse` makes a compiled hook refuse to run once its source no
|
|
551
|
+
// longer matches its stamp. That is correct for tampering, but it wedges the
|
|
552
|
+
// AUTHOR: if the hook is a PreToolUse Bash gate wired into the same repo, editing
|
|
553
|
+
// its source makes it block EVERY Bash command — including `vigiles compile`,
|
|
554
|
+
// the only command that regenerates the stamp. Observed 2026-08-03; the only
|
|
555
|
+
// escape was hand-editing `.claude/settings.json` to unwire the gate, compile,
|
|
556
|
+
// and rewire.
|
|
557
|
+
//
|
|
558
|
+
// Fail-closed is kept for everything else. The ONE exception is the repair
|
|
559
|
+
// action itself, and it is announced loudly on stderr. This does not weaken the
|
|
560
|
+
// stamp's threat model in a way that matters: while the stamp is stale the hook
|
|
561
|
+
// enforces NOTHING (it refuses every call), and an attacker who can rewrite a
|
|
562
|
+
// hook's source can equally rewrite `.claude/settings.json` — the escape they'd
|
|
563
|
+
// otherwise use. What the stamp buys is that a smuggled capability can never run
|
|
564
|
+
// SILENTLY, and that is unchanged.
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
/** Compare two path references without node:path (core stays dependency-free). */
|
|
567
|
+
function samePathRef(a, b) {
|
|
568
|
+
const norm = (p) => p.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
569
|
+
const [x, y] = [norm(a), norm(b)];
|
|
570
|
+
return x === y || x.endsWith("/" + y) || y.endsWith("/" + x);
|
|
571
|
+
}
|
|
572
|
+
/** The basename of a path token, for matching `npx vigiles` / `./bin/vigiles`. */
|
|
573
|
+
function basenameOf(token) {
|
|
574
|
+
const parts = token.replace(/\\/g, "/").split("/");
|
|
575
|
+
return parts[parts.length - 1] ?? token;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* True when this event IS the author repairing the hook — the only thing a
|
|
579
|
+
* stale-stamp refusal must let through, or the repo wedges (see the note above):
|
|
580
|
+
*
|
|
581
|
+
* - a Bash command that invokes `vigiles compile` (however it's launched —
|
|
582
|
+
* `npx vigiles compile`, `pnpm exec vigiles compile`, `./node_modules/.bin/vigiles compile`),
|
|
583
|
+
* which is what regenerates the stamp; or
|
|
584
|
+
* - an edit/write whose target IS this hook's own source file, so a FILE gate
|
|
585
|
+
* over the repo can still be fixed by editing the hook again.
|
|
586
|
+
*
|
|
587
|
+
* AST-backed (`leafCommandsNormalized`), so it sees the invocation through a
|
|
588
|
+
* compound command or a wrapper, exactly like every other matcher here.
|
|
589
|
+
*/
|
|
590
|
+
function isStampRepairEvent(event, hookFile) {
|
|
591
|
+
const filePath = event.tool_input?.file_path;
|
|
592
|
+
if (typeof filePath === "string" && samePathRef(filePath, hookFile))
|
|
593
|
+
return true;
|
|
594
|
+
const command = event.tool_input?.command;
|
|
595
|
+
if (typeof command !== "string")
|
|
596
|
+
return false;
|
|
597
|
+
return (0, bash_effects_js_1.leafCommandsNormalized)(command).some((leaf) => {
|
|
598
|
+
const i = leaf.argv.findIndex((a) => basenameOf(a) === "vigiles");
|
|
599
|
+
return i !== -1 && leaf.argv.slice(i + 1).includes("compile");
|
|
600
|
+
});
|
|
601
|
+
}
|
|
546
602
|
//# sourceMappingURL=hook-program.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vigiles",
|
|
3
|
-
"version": "14.13.
|
|
3
|
+
"version": "14.13.2",
|
|
4
4
|
"description": "Lint & test the harness your AI agent runs on — verify the references in your CLAUDE.md / AGENTS.md and test that your hooks and skills actually work.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: debug-my-harness
|
|
3
|
+
allowed-tools: Read, Glob, Grep
|
|
3
4
|
description: Diagnose why an agent harness misbehaved by reading the local flight-recorder ledger (.vigiles/runs.jsonl) — which skills fired or got hijacked, which hooks blocked or wrongly allowed, which subagent tool-contract violations happened, and how a skill's trigger rate moved. Use when asked why a skill stopped firing, why a hook didn't block, why the wrong skill ran, or to debug/investigate what the harness actually did. NOT for writing new rules (use strengthen) or editing the spec (use edit-spec).
|
|
4
5
|
---
|
|
5
6
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: edit-spec
|
|
3
|
+
allowed-tools: Read, Edit, Write, Glob, Grep, Bash
|
|
3
4
|
description: Edit a vigiles .spec.ts to change a compiled instruction file (CLAUDE.md / AGENTS.md) — add, modify, or remove a rule, section, command, or key file. Use whenever you need to change a CLAUDE.md/AGENTS.md that carries a vigiles hash (edit the spec, never the artifact), including adding a new enforce()/check()/guidance() rule.
|
|
4
5
|
argument-hint: <what to change — e.g., "add a rule about error handling" or "update the testing section">
|
|
5
6
|
---
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: strengthen
|
|
3
|
+
allowed-tools: Read, Edit, Write, Glob, Grep, Bash
|
|
3
4
|
description: Upgrade a vigiles spec's guidance() rules to enforce() — scan the guidance rules in a CLAUDE.md/AGENTS.md spec and find existing linter rules (ESLint, Ruff, Clippy, Pylint, RuboCop, Stylelint) that back them. Use when asked to strengthen, harden, or make vigiles rules enforceable; NOT for general linting or fixing lint errors.
|
|
4
5
|
---
|
|
5
6
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: test-harness
|
|
3
|
+
allowed-tools: Read, Edit, Write, Glob, Grep, Bash
|
|
3
4
|
description: Install vigiles and test a Claude Code harness — hooks, skills, settings, CLAUDE.md — by picking the right tier (unit / deterministic / eval) and writing a test that passes. Use when the user wants to check that a hook fires or blocks, that a skill triggers, that injected context lands, or that a harness change moves what the agent does.
|
|
4
5
|
---
|
|
5
6
|
|