tamperward 2.23.16 → 2.23.18

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/README.md CHANGED
@@ -257,6 +257,7 @@ platforms.
257
257
  | Capability | Linux | macOS | Windows |
258
258
  | --- | --- | --- | --- |
259
259
  | `check` / policy evaluation | Supported | Supported | Supported |
260
+ | parsed output (`--json`, `--format github`, hook/sweep JSON) on a pipe | Complete before exit | Complete before exit (asynchronous pipe drained first) | Complete before exit (asynchronous pipe drained first) |
260
261
  | Claude hook / Stop adapter | Supported where Claude Code command hooks are available | Same | Same |
261
262
  | `watch` / observer telemetry | Supported; backend health is reported | Supported/degraded according to `fs.watch` health | Supported/degraded according to `fs.watch` health |
262
263
  | opt-in `hook-service` | Supported (per-user `0600` unix socket) | Supported (per-user `0600` unix socket) | **Unsupported; `start` refuses, hooks run in-process** |
package/dist/cli/index.js CHANGED
@@ -302,6 +302,29 @@ var init_hook_client = __esm({
302
302
  }
303
303
  });
304
304
 
305
+ // src/cli/exit.ts
306
+ function exitAfterFlush(code2) {
307
+ process.exitCode = code2;
308
+ let pending = 0;
309
+ const done = () => {
310
+ pending -= 1;
311
+ if (pending === 0) process.exit(code2);
312
+ };
313
+ for (const stream of [process.stdout, process.stderr]) {
314
+ pending += 1;
315
+ stream.on("error", noop);
316
+ stream.write("", done);
317
+ }
318
+ }
319
+ var noop;
320
+ var init_exit = __esm({
321
+ "src/cli/exit.ts"() {
322
+ "use strict";
323
+ noop = () => {
324
+ };
325
+ }
326
+ });
327
+
305
328
  // src/lazy-deps.ts
306
329
  import { createRequire } from "node:module";
307
330
  function loadPicomatch() {
@@ -6756,6 +6779,25 @@ function entryShape(h) {
6756
6779
  }
6757
6780
  return { problems, runs };
6758
6781
  }
6782
+ function judgeGateEntry(event, matcher, h) {
6783
+ const cmd = typeof h.command === "string" ? h.command : "";
6784
+ const a = analyseGateCommand(cmd, event);
6785
+ const shape = entryShape(h);
6786
+ const problems = [...a.problems, ...shape.problems];
6787
+ let runs = a.runs && shape.runs;
6788
+ let tools = null;
6789
+ if (event === "PreToolUse") {
6790
+ const sel = matcherSelection(matcher);
6791
+ tools = sel.tools;
6792
+ if (sel.problem) {
6793
+ problems.push(sel.problem);
6794
+ }
6795
+ } else if (matcher !== void 0 && !(typeof matcher === "string" && (matcher === "" || matcher === "*"))) {
6796
+ problems.push(`a \`matcher\` (${JSON.stringify(matcher)}) on the Stop entry`);
6797
+ runs = false;
6798
+ }
6799
+ return { runs, problems, pin: a.pin, tools };
6800
+ }
6759
6801
  function gateCandidates(s, event) {
6760
6802
  const out3 = [];
6761
6803
  for (const group of eventEntries(s, event) ?? []) {
@@ -6778,22 +6820,7 @@ function gateCandidates(s, event) {
6778
6820
  continue;
6779
6821
  }
6780
6822
  if (!MENTIONS_GATE.test(cmd)) continue;
6781
- const a = analyseGateCommand(cmd, event);
6782
- const shape = entryShape(h);
6783
- const problems = [...a.problems, ...shape.problems];
6784
- let runs = a.runs && shape.runs;
6785
- let tools = null;
6786
- if (event === "PreToolUse") {
6787
- const sel = matcherSelection(group.matcher);
6788
- tools = sel.tools;
6789
- if (sel.problem) {
6790
- problems.push(sel.problem);
6791
- }
6792
- } else if (group.matcher !== void 0 && !(typeof group.matcher === "string" && (group.matcher === "" || group.matcher === "*"))) {
6793
- problems.push(`a \`matcher\` (${JSON.stringify(group.matcher)}) on the Stop entry`);
6794
- runs = false;
6795
- }
6796
- out3.push({ key: JSON.stringify({ m: group.matcher, h }), runs, problems, pin: a.pin, tools });
6823
+ out3.push({ key: JSON.stringify({ m: group.matcher, h }), ...judgeGateEntry(event, group.matcher, h) });
6797
6824
  }
6798
6825
  }
6799
6826
  return out3;
@@ -10745,7 +10772,7 @@ function runHookService(args) {
10745
10772
  }).catch((e) => {
10746
10773
  process.stderr.write(`tamperward hook-service: ${e instanceof Error ? e.message : String(e)}
10747
10774
  `);
10748
- process.exit(2);
10775
+ exitAfterFlush(2);
10749
10776
  });
10750
10777
  return -1;
10751
10778
  }
@@ -10769,14 +10796,15 @@ function runHookService(args) {
10769
10796
  if (!isRecord(res) || typeof res.served !== "number") {
10770
10797
  process.stdout.write(`tamperward hook-service: not running (pid ${state.pid} did not answer, or is another version); ${optIn}
10771
10798
  `);
10772
- process.exit(0);
10799
+ exitAfterFlush(0);
10800
+ return;
10773
10801
  }
10774
10802
  const cache3 = isRecord(res.cache) ? res.cache : {};
10775
10803
  process.stdout.write(
10776
10804
  `tamperward hook-service: running (pid ${state.pid}, tamperward@${state.version}, root ${state.root}, since ${state.started_at}); served: ${res.served}; snapshot cache hits/misses/entries: ${String(cache3.hits ?? 0)}/${String(cache3.misses ?? 0)}/${String(cache3.size ?? 0)}; ${optIn}
10777
10805
  `
10778
10806
  );
10779
- process.exit(0);
10807
+ exitAfterFlush(0);
10780
10808
  });
10781
10809
  return -1;
10782
10810
  }
@@ -10789,6 +10817,7 @@ var init_hook_service = __esm({
10789
10817
  init_ptree_cache();
10790
10818
  init_repo_context();
10791
10819
  init_hook();
10820
+ init_exit();
10792
10821
  init_hook_client();
10793
10822
  MAX_REQUEST_BYTES = 64 * 1024 * 1024;
10794
10823
  STOP_WAIT_MS = 1e4;
@@ -11164,6 +11193,17 @@ function planGitignore(cwd) {
11164
11193
  apply: () => atomicReplaceFile(path, next, existingMode(path, 420))
11165
11194
  };
11166
11195
  }
11196
+ function gateVerdicts(event, groups, needle) {
11197
+ const out3 = [];
11198
+ for (const group of groups) {
11199
+ for (const h of group.hooks ?? []) {
11200
+ if (!needle.test(String(h.command ?? ""))) continue;
11201
+ const { runs, problems } = judgeGateEntry(event, group.matcher, h);
11202
+ out3.push({ event, group, h, runs, problems });
11203
+ }
11204
+ }
11205
+ return out3;
11206
+ }
11167
11207
  function planClaudeHooks(cwd) {
11168
11208
  const rel = ".claude/settings.json";
11169
11209
  const path = join14(cwd, rel);
@@ -11197,10 +11237,24 @@ function planClaudeHooks(cwd) {
11197
11237
  const needPre = preEntries.length === 0;
11198
11238
  const stopEntries = entriesWith(hooks.Stop, SWEEP_RE);
11199
11239
  const needStop = stopEntries.length === 0;
11240
+ const neutralised = [
11241
+ ...gateVerdicts("PreToolUse", preEntries, HOOK_RE),
11242
+ ...gateVerdicts("Stop", stopEntries, SWEEP_RE)
11243
+ ].filter((v) => !v.runs);
11244
+ const foreign = neutralised.filter((v) => ours(String(v.h.command ?? "")) === null);
11245
+ if (foreign.length > 0) {
11246
+ return {
11247
+ item: "agent",
11248
+ path: rel,
11249
+ status: "error",
11250
+ detail: `${foreign.map((v) => `the ${v.event} gate entry is neutralised (${v.problems.join("; ")})`).join("; ")} \u2014 the runtime would not run the gate through it; remove the entry, then re-run init (refusing to rewrite a hand-written entry)`
11251
+ };
11252
+ }
11200
11253
  const stale = preEntries.filter((m) => missingTools(m.matcher).length > 0);
11201
- const repin = [...preEntries, ...stopEntries].flatMap((m) => m.hooks ?? []).filter((h) => stalePin(String(h.command ?? "")));
11254
+ const restoring = new Set(neutralised.map((v) => v.h));
11255
+ const repin = [...preEntries, ...stopEntries].flatMap((m) => m.hooks ?? []).filter((h) => !restoring.has(h) && stalePin(String(h.command ?? "")));
11202
11256
  const needDisableFalse = settings.disableAllHooks !== false;
11203
- if (!needPre && !needStop && stale.length === 0 && repin.length === 0 && !needDisableFalse) {
11257
+ if (!needPre && !needStop && stale.length === 0 && repin.length === 0 && neutralised.length === 0 && !needDisableFalse) {
11204
11258
  return { item: "agent", path: rel, status: "ok", detail: "PreToolUse + Stop hooks already wired" };
11205
11259
  }
11206
11260
  const repairing = stale.length > 0 ? [...new Set(stale.flatMap((m) => missingTools(m.matcher)))] : [];
@@ -11214,9 +11268,16 @@ function planClaudeHooks(cwd) {
11214
11268
  needStop && "wire Stop sweep",
11215
11269
  repairing.length > 0 && `widen the PreToolUse matcher to cover ${repairing.join(", ")}`,
11216
11270
  repin.length > 0 && `re-pin the hook commands to tamperward@${TW_VERSION} (was ${pins.join(", ")})`,
11271
+ ...neutralised.map((v) => `restore the ${v.event} gate entry to the shape init writes \u2014 it is neutralised (${v.problems.join("; ")})`),
11217
11272
  needDisableFalse && (settings.disableAllHooks === void 0 ? "declare disableAllHooks: false so the user settings file cannot switch the hooks off" : `set disableAllHooks: false (was ${JSON.stringify(settings.disableAllHooks)})`)
11218
11273
  ].filter(Boolean).join(" + "),
11219
11274
  apply: () => {
11275
+ for (const v of neutralised) {
11276
+ for (const k of Object.keys(v.h)) delete v.h[k];
11277
+ v.h.type = "command";
11278
+ v.h.command = v.event === "PreToolUse" ? HOOK_CMD : SWEEP_CMD;
11279
+ if (v.event === "Stop") delete v.group.matcher;
11280
+ }
11220
11281
  for (const h of repin) {
11221
11282
  const kind = ours(String(h.command ?? ""))?.[2];
11222
11283
  h.command = kind === "hook claude" ? HOOK_CMD : kind === "sweep claude" ? SWEEP_CMD : h.command;
@@ -11316,7 +11377,7 @@ ${block.join("\n")}
11316
11377
  }
11317
11378
  const lines = existing.split("\n");
11318
11379
  const dead = unconditionalExitAt(lines);
11319
- const at = lines.findIndex((l) => PRECOMMIT_RE.test(l));
11380
+ const at = lines.findIndex((l) => !/^\s*#/.test(l) && PRECOMMIT_RE.test(l));
11320
11381
  const write = (out3) => atomicReplaceFile(path, out3.join("\n"), hookMode());
11321
11382
  if (at !== -1 && (dead === -1 || at < dead)) {
11322
11383
  if (!stalePin(lines[at])) {
@@ -11559,6 +11620,7 @@ var init_init = __esm({
11559
11620
  init_verifier_limits();
11560
11621
  init_wiring();
11561
11622
  init_narrow();
11623
+ init_hook_tampering();
11562
11624
  init_repo_context();
11563
11625
  init_safe_write();
11564
11626
  errText3 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
@@ -17779,6 +17841,7 @@ var init_main = __esm({
17779
17841
 
17780
17842
  // src/cli/index.ts
17781
17843
  init_hook_client();
17844
+ init_exit();
17782
17845
  import { readFileSync as readFileSync24 } from "node:fs";
17783
17846
  function loadMain() {
17784
17847
  return Promise.resolve().then(() => (init_main(), main_exports));
@@ -17810,4 +17873,4 @@ async function launch(argv) {
17810
17873
  return main2.guardedMain(argv);
17811
17874
  }
17812
17875
  var code = await launch(process.argv.slice(2));
17813
- if (code >= 0) process.exit(code);
17876
+ if (code >= 0) exitAfterFlush(code);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.23.16",
3
+ "version": "2.23.18",
4
4
  "description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "hexrift",