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 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
- console.error(`vigiles: hook ${file} does not match its compiled stamp (tampered).`);
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 fails CLOSED (exit 2), never silent-allow.
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.0",
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