tamperward 2.23.16 → 2.23.17

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +56 -19
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -6756,6 +6756,25 @@ function entryShape(h) {
6756
6756
  }
6757
6757
  return { problems, runs };
6758
6758
  }
6759
+ function judgeGateEntry(event, matcher, h) {
6760
+ const cmd = typeof h.command === "string" ? h.command : "";
6761
+ const a = analyseGateCommand(cmd, event);
6762
+ const shape = entryShape(h);
6763
+ const problems = [...a.problems, ...shape.problems];
6764
+ let runs = a.runs && shape.runs;
6765
+ let tools = null;
6766
+ if (event === "PreToolUse") {
6767
+ const sel = matcherSelection(matcher);
6768
+ tools = sel.tools;
6769
+ if (sel.problem) {
6770
+ problems.push(sel.problem);
6771
+ }
6772
+ } else if (matcher !== void 0 && !(typeof matcher === "string" && (matcher === "" || matcher === "*"))) {
6773
+ problems.push(`a \`matcher\` (${JSON.stringify(matcher)}) on the Stop entry`);
6774
+ runs = false;
6775
+ }
6776
+ return { runs, problems, pin: a.pin, tools };
6777
+ }
6759
6778
  function gateCandidates(s, event) {
6760
6779
  const out3 = [];
6761
6780
  for (const group of eventEntries(s, event) ?? []) {
@@ -6778,22 +6797,7 @@ function gateCandidates(s, event) {
6778
6797
  continue;
6779
6798
  }
6780
6799
  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 });
6800
+ out3.push({ key: JSON.stringify({ m: group.matcher, h }), ...judgeGateEntry(event, group.matcher, h) });
6797
6801
  }
6798
6802
  }
6799
6803
  return out3;
@@ -11164,6 +11168,17 @@ function planGitignore(cwd) {
11164
11168
  apply: () => atomicReplaceFile(path, next, existingMode(path, 420))
11165
11169
  };
11166
11170
  }
11171
+ function gateVerdicts(event, groups, needle) {
11172
+ const out3 = [];
11173
+ for (const group of groups) {
11174
+ for (const h of group.hooks ?? []) {
11175
+ if (!needle.test(String(h.command ?? ""))) continue;
11176
+ const { runs, problems } = judgeGateEntry(event, group.matcher, h);
11177
+ out3.push({ event, group, h, runs, problems });
11178
+ }
11179
+ }
11180
+ return out3;
11181
+ }
11167
11182
  function planClaudeHooks(cwd) {
11168
11183
  const rel = ".claude/settings.json";
11169
11184
  const path = join14(cwd, rel);
@@ -11197,10 +11212,24 @@ function planClaudeHooks(cwd) {
11197
11212
  const needPre = preEntries.length === 0;
11198
11213
  const stopEntries = entriesWith(hooks.Stop, SWEEP_RE);
11199
11214
  const needStop = stopEntries.length === 0;
11215
+ const neutralised = [
11216
+ ...gateVerdicts("PreToolUse", preEntries, HOOK_RE),
11217
+ ...gateVerdicts("Stop", stopEntries, SWEEP_RE)
11218
+ ].filter((v) => !v.runs);
11219
+ const foreign = neutralised.filter((v) => ours(String(v.h.command ?? "")) === null);
11220
+ if (foreign.length > 0) {
11221
+ return {
11222
+ item: "agent",
11223
+ path: rel,
11224
+ status: "error",
11225
+ 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)`
11226
+ };
11227
+ }
11200
11228
  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 ?? "")));
11229
+ const restoring = new Set(neutralised.map((v) => v.h));
11230
+ const repin = [...preEntries, ...stopEntries].flatMap((m) => m.hooks ?? []).filter((h) => !restoring.has(h) && stalePin(String(h.command ?? "")));
11202
11231
  const needDisableFalse = settings.disableAllHooks !== false;
11203
- if (!needPre && !needStop && stale.length === 0 && repin.length === 0 && !needDisableFalse) {
11232
+ if (!needPre && !needStop && stale.length === 0 && repin.length === 0 && neutralised.length === 0 && !needDisableFalse) {
11204
11233
  return { item: "agent", path: rel, status: "ok", detail: "PreToolUse + Stop hooks already wired" };
11205
11234
  }
11206
11235
  const repairing = stale.length > 0 ? [...new Set(stale.flatMap((m) => missingTools(m.matcher)))] : [];
@@ -11214,9 +11243,16 @@ function planClaudeHooks(cwd) {
11214
11243
  needStop && "wire Stop sweep",
11215
11244
  repairing.length > 0 && `widen the PreToolUse matcher to cover ${repairing.join(", ")}`,
11216
11245
  repin.length > 0 && `re-pin the hook commands to tamperward@${TW_VERSION} (was ${pins.join(", ")})`,
11246
+ ...neutralised.map((v) => `restore the ${v.event} gate entry to the shape init writes \u2014 it is neutralised (${v.problems.join("; ")})`),
11217
11247
  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
11248
  ].filter(Boolean).join(" + "),
11219
11249
  apply: () => {
11250
+ for (const v of neutralised) {
11251
+ for (const k of Object.keys(v.h)) delete v.h[k];
11252
+ v.h.type = "command";
11253
+ v.h.command = v.event === "PreToolUse" ? HOOK_CMD : SWEEP_CMD;
11254
+ if (v.event === "Stop") delete v.group.matcher;
11255
+ }
11220
11256
  for (const h of repin) {
11221
11257
  const kind = ours(String(h.command ?? ""))?.[2];
11222
11258
  h.command = kind === "hook claude" ? HOOK_CMD : kind === "sweep claude" ? SWEEP_CMD : h.command;
@@ -11316,7 +11352,7 @@ ${block.join("\n")}
11316
11352
  }
11317
11353
  const lines = existing.split("\n");
11318
11354
  const dead = unconditionalExitAt(lines);
11319
- const at = lines.findIndex((l) => PRECOMMIT_RE.test(l));
11355
+ const at = lines.findIndex((l) => !/^\s*#/.test(l) && PRECOMMIT_RE.test(l));
11320
11356
  const write = (out3) => atomicReplaceFile(path, out3.join("\n"), hookMode());
11321
11357
  if (at !== -1 && (dead === -1 || at < dead)) {
11322
11358
  if (!stalePin(lines[at])) {
@@ -11559,6 +11595,7 @@ var init_init = __esm({
11559
11595
  init_verifier_limits();
11560
11596
  init_wiring();
11561
11597
  init_narrow();
11598
+ init_hook_tampering();
11562
11599
  init_repo_context();
11563
11600
  init_safe_write();
11564
11601
  errText3 = (e) => (e instanceof Error ? e.message : String(e)).replace(/\s+/g, " ").trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.23.16",
3
+ "version": "2.23.17",
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",