@hasna/hooks 0.9.5 → 0.9.6

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/index.js CHANGED
@@ -4762,12 +4762,12 @@ __export(exports_store, {
4762
4762
  checkScriptHash: () => checkScriptHash,
4763
4763
  LockFileError: () => LockFileError
4764
4764
  });
4765
- import { createHash } from "crypto";
4766
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
4765
+ import { createHash as createHash2 } from "crypto";
4766
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync } from "fs";
4767
4767
  import { readFile } from "fs/promises";
4768
4768
  import { randomBytes } from "crypto";
4769
4769
  function sha256Of(input) {
4770
- return createHash("sha256").update(input).digest("hex");
4770
+ return createHash2("sha256").update(input).digest("hex");
4771
4771
  }
4772
4772
  async function sha256File(path) {
4773
4773
  const buf = await readFile(path);
@@ -4828,11 +4828,11 @@ function assertLockShape(parsed, path) {
4828
4828
  }
4829
4829
  function readLock() {
4830
4830
  const path = getLockPath();
4831
- if (!existsSync6(path))
4831
+ if (!existsSync7(path))
4832
4832
  return { hooks: {} };
4833
4833
  let raw;
4834
4834
  try {
4835
- raw = readFileSync3(path, "utf-8");
4835
+ raw = readFileSync4(path, "utf-8");
4836
4836
  } catch {
4837
4837
  throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
4838
4838
  }
@@ -4935,7 +4935,7 @@ async function verifyScriptHash(name, scriptPath) {
4935
4935
  return checkScriptHash(name, await sha256File(scriptPath));
4936
4936
  }
4937
4937
  function retrustHook(name, scriptPath, version, source) {
4938
- const actual = sha256Of(readFileSync3(scriptPath));
4938
+ const actual = sha256Of(readFileSync4(scriptPath));
4939
4939
  const db = storeDb();
4940
4940
  if (db) {
4941
4941
  upsertHookRecord(db, {
@@ -4974,12 +4974,12 @@ __export(exports_profiles, {
4974
4974
  deleteProfile: () => deleteProfile,
4975
4975
  createProfile: () => createProfile
4976
4976
  });
4977
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
4978
- import { join as join8 } from "path";
4977
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
4978
+ import { join as join9 } from "path";
4979
4979
  function getProfilesDir() {
4980
- const newDir = join8(getEffectiveDataRoot(), "profiles");
4981
- const oldDir = join8(getHomeDir(), ".hooks", "profiles");
4982
- if (!existsSync9(newDir) && existsSync9(oldDir)) {
4980
+ const newDir = join9(getEffectiveDataRoot(), "profiles");
4981
+ const oldDir = join9(getHomeDir(), ".hooks", "profiles");
4982
+ if (!existsSync10(newDir) && existsSync10(oldDir)) {
4983
4983
  mkdirSync6(newDir, { recursive: true });
4984
4984
  cpSync2(oldDir, newDir, { recursive: true });
4985
4985
  }
@@ -4987,12 +4987,12 @@ function getProfilesDir() {
4987
4987
  }
4988
4988
  function ensureProfilesDir() {
4989
4989
  const dir = getProfilesDir();
4990
- if (!existsSync9(dir)) {
4990
+ if (!existsSync10(dir)) {
4991
4991
  mkdirSync6(dir, { recursive: true });
4992
4992
  }
4993
4993
  }
4994
4994
  function profilePath(id) {
4995
- return join8(getProfilesDir(), `${id}.json`);
4995
+ return join9(getProfilesDir(), `${id}.json`);
4996
4996
  }
4997
4997
  function shortUuid() {
4998
4998
  return crypto.randomUUID().slice(0, 8);
@@ -5018,23 +5018,23 @@ function createProfile(input) {
5018
5018
  function getProfile(id) {
5019
5019
  const path = profilePath(id);
5020
5020
  try {
5021
- if (!existsSync9(path))
5021
+ if (!existsSync10(path))
5022
5022
  return null;
5023
- return JSON.parse(readFileSync6(path, "utf-8"));
5023
+ return JSON.parse(readFileSync7(path, "utf-8"));
5024
5024
  } catch {
5025
5025
  return null;
5026
5026
  }
5027
5027
  }
5028
5028
  function listProfiles() {
5029
5029
  const dir = getProfilesDir();
5030
- if (!existsSync9(dir))
5030
+ if (!existsSync10(dir))
5031
5031
  return [];
5032
5032
  try {
5033
5033
  const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
5034
5034
  const profiles = [];
5035
5035
  for (const file of files) {
5036
5036
  try {
5037
- const content = readFileSync6(join8(dir, file), "utf-8");
5037
+ const content = readFileSync7(join9(dir, file), "utf-8");
5038
5038
  profiles.push(JSON.parse(content));
5039
5039
  } catch {}
5040
5040
  }
@@ -5057,7 +5057,7 @@ function updateProfile(id, data) {
5057
5057
  }
5058
5058
  function deleteProfile(id) {
5059
5059
  const path = profilePath(id);
5060
- if (!existsSync9(path))
5060
+ if (!existsSync10(path))
5061
5061
  return false;
5062
5062
  try {
5063
5063
  rmSync(path);
@@ -5087,7 +5087,7 @@ function importProfiles(profiles) {
5087
5087
  continue;
5088
5088
  }
5089
5089
  const path = profilePath(profile.agent_id);
5090
- if (existsSync9(path)) {
5090
+ if (existsSync10(path)) {
5091
5091
  skipped++;
5092
5092
  continue;
5093
5093
  }
@@ -5104,15 +5104,15 @@ var init_profiles = __esm(() => {
5104
5104
  // ../contracts/dist/client/transport.js
5105
5105
  import { isIP } from "net";
5106
5106
  import { spawnSync } from "child_process";
5107
- import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync22 } from "fs";
5107
+ import { closeSync as closeSync22, fstatSync as fstatSync2, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
5108
5108
  import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
5109
5109
  import { hostname as osHostname } from "os";
5110
- import { isAbsolute as isAbsolute2, join as join22 } from "path";
5110
+ import { isAbsolute as isAbsolute3, join as join22 } from "path";
5111
5111
  import { createRequire } from "module";
5112
5112
  import { fileURLToPath as fileURLToPath22 } from "url";
5113
- import { closeSync as closeSync3, fstatSync, lstatSync as lstatSync2, openSync as openSync3, readFileSync as readFileSync7, realpathSync as realpathSync2, statSync } from "fs";
5113
+ import { closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3, readFileSync as readFileSync8, realpathSync as realpathSync3, statSync } from "fs";
5114
5114
  import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
5115
- import { basename as basename2, dirname as dirname5, join as join9, relative, sep as sep2 } from "path";
5115
+ import { basename as basename2, dirname as dirname6, join as join10, relative, sep as sep2 } from "path";
5116
5116
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
5117
5117
  function envToken(name) {
5118
5118
  return name.toUpperCase().replace(/-/g, "_");
@@ -5186,14 +5186,14 @@ function o(e2, r2, t2) {
5186
5186
  }
5187
5187
  }
5188
5188
  function entryPoint(directory) {
5189
- const root = realpathSync2(directory);
5190
- const fd = openSync3(join9(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
5189
+ const root = realpathSync3(directory);
5190
+ const fd = openSync3(join10(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
5191
5191
  let pkg;
5192
5192
  try {
5193
- const stat = fstatSync(fd);
5193
+ const stat = fstatSync3(fd);
5194
5194
  if (!stat.isFile() || stat.size > MAX_PACKAGE_BYTES)
5195
5195
  throw new Error("Invalid Secrets package metadata");
5196
- pkg = JSON.parse(readFileSync7(fd, "utf8"));
5196
+ pkg = JSON.parse(readFileSync8(fd, "utf8"));
5197
5197
  } finally {
5198
5198
  closeSync3(fd);
5199
5199
  }
@@ -5207,7 +5207,7 @@ function entryPoint(directory) {
5207
5207
  if (/[\\%?#\0]/.test(target) || parts.some((part) => !part || part === "." || part === ".." || part === "node_modules")) {
5208
5208
  throw new Error("Invalid Secrets import export");
5209
5209
  }
5210
- const file = realpathSync2(join9(root, ...parts));
5210
+ const file = realpathSync3(join10(root, ...parts));
5211
5211
  const within = relative(root, file);
5212
5212
  if (!within || within === ".." || within.startsWith(`..${sep2}`) || !statSync(file).isFile()) {
5213
5213
  throw new Error("Secrets import export escapes its package or is not a file");
@@ -5215,13 +5215,13 @@ function entryPoint(directory) {
5215
5215
  return pathToFileURL(file).href;
5216
5216
  }
5217
5217
  function resolveInstalledSecrets(parentUrl) {
5218
- let directory = dirname5(fileURLToPath3(parentUrl));
5218
+ let directory = dirname6(fileURLToPath3(parentUrl));
5219
5219
  for (;; ) {
5220
5220
  if (basename2(directory) !== "node_modules") {
5221
- const candidate = join9(directory, "node_modules", PACKAGE);
5221
+ const candidate = join10(directory, "node_modules", PACKAGE);
5222
5222
  let absent = false;
5223
5223
  try {
5224
- lstatSync2(candidate);
5224
+ lstatSync3(candidate);
5225
5225
  } catch (error) {
5226
5226
  if (error.code !== "ENOENT")
5227
5227
  throw error;
@@ -5230,7 +5230,7 @@ function resolveInstalledSecrets(parentUrl) {
5230
5230
  if (!absent)
5231
5231
  return entryPoint(candidate);
5232
5232
  }
5233
- const parent = dirname5(directory);
5233
+ const parent = dirname6(directory);
5234
5234
  if (parent === directory)
5235
5235
  throw new Error("Secrets SDK is not installed for this consumer");
5236
5236
  directory = parent;
@@ -5242,7 +5242,7 @@ function homeDir(env) {
5242
5242
  }
5243
5243
  function absoluteOverride(env, key) {
5244
5244
  const value = env[key]?.trim();
5245
- return value && isAbsolute2(value) ? value : null;
5245
+ return value && isAbsolute3(value) ? value : null;
5246
5246
  }
5247
5247
  function hasnaHomeDir(env) {
5248
5248
  const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
@@ -5288,9 +5288,9 @@ function parseEnvFile(text) {
5288
5288
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
5289
5289
  continue;
5290
5290
  let value = withoutExport.slice(equals + 1).trim();
5291
- const quote = value[0];
5292
- if (quote === '"' || quote === "'") {
5293
- if (value.length < 2 || !value.endsWith(quote)) {
5291
+ const quote2 = value[0];
5292
+ if (quote2 === '"' || quote2 === "'") {
5293
+ if (value.length < 2 || !value.endsWith(quote2)) {
5294
5294
  unusable.add(key);
5295
5295
  continue;
5296
5296
  }
@@ -5319,7 +5319,7 @@ function readAppConfigFile(path) {
5319
5319
  };
5320
5320
  let fd = -1;
5321
5321
  try {
5322
- fd = openSync2(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
5322
+ fd = openSync22(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
5323
5323
  } catch (error) {
5324
5324
  const code = error.code;
5325
5325
  if (code === "ENOENT" || code === "ENOTDIR")
@@ -5348,7 +5348,7 @@ function readAppConfigFile(path) {
5348
5348
  return parseEnvFile(bytes.toString("utf8"));
5349
5349
  } finally {
5350
5350
  if (fd !== -1)
5351
- closeSync2(fd);
5351
+ closeSync22(fd);
5352
5352
  }
5353
5353
  }
5354
5354
  function readCredentialFile(path, apiKeyKeys, pointerKey) {
@@ -6202,12 +6202,12 @@ var init_db_writer = __esm(() => {
6202
6202
  });
6203
6203
 
6204
6204
  // src/lib/hook-env.ts
6205
- import { dirname as dirname7, isAbsolute as isAbsolute3 } from "path";
6206
- import { existsSync as existsSync11, realpathSync as realpathSync3, statSync as statSync2 } from "fs";
6205
+ import { dirname as dirname8, isAbsolute as isAbsolute4 } from "path";
6206
+ import { existsSync as existsSync12, realpathSync as realpathSync4, statSync as statSync2 } from "fs";
6207
6207
  import { tmpdir as tmpdir2 } from "os";
6208
6208
  function isDeniedEnvName(name) {
6209
6209
  const upper = name.toUpperCase();
6210
- if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
6210
+ if (DENY_PREFIXES.some((prefix2) => upper.startsWith(prefix2)))
6211
6211
  return true;
6212
6212
  if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
6213
6213
  return true;
@@ -6216,13 +6216,13 @@ function isDeniedEnvName(name) {
6216
6216
  function isInterpreterInjectionEnvName(name) {
6217
6217
  if (INTERPRETER_INJECTION.has(name))
6218
6218
  return true;
6219
- return INTERPRETER_INJECTION_PREFIXES.some((prefix) => name.startsWith(prefix));
6219
+ return INTERPRETER_INJECTION_PREFIXES.some((prefix2) => name.startsWith(prefix2));
6220
6220
  }
6221
6221
  function isBashFunctionEnvName(name) {
6222
6222
  return name.startsWith("BASH_FUNC_");
6223
6223
  }
6224
6224
  function isUnsafePathEntry(entry, home) {
6225
- if (entry === "" || !isAbsolute3(entry))
6225
+ if (entry === "" || !isAbsolute4(entry))
6226
6226
  return true;
6227
6227
  if (home && (entry === home || entry.startsWith(home + "/")))
6228
6228
  return true;
@@ -6232,7 +6232,7 @@ function isUnsafePathEntry(entry, home) {
6232
6232
  return true;
6233
6233
  const canonical = (path) => {
6234
6234
  try {
6235
- return realpathSync3(path);
6235
+ return realpathSync4(path);
6236
6236
  } catch {
6237
6237
  return path;
6238
6238
  }
@@ -6251,9 +6251,9 @@ function isUnsafePathEntry(entry, home) {
6251
6251
  }
6252
6252
  function sanitizeHookPath(pathValue, home) {
6253
6253
  const baseline = PATH_BASELINE.slice();
6254
- if (process.platform === "darwin" && existsSync11("/opt/homebrew/bin"))
6254
+ if (process.platform === "darwin" && existsSync12("/opt/homebrew/bin"))
6255
6255
  baseline.push("/opt/homebrew/bin");
6256
- const bunDir = dirname7(process.execPath);
6256
+ const bunDir = dirname8(process.execPath);
6257
6257
  const seen = new Set;
6258
6258
  const out = [];
6259
6259
  for (const dir of baseline) {
@@ -6378,7 +6378,7 @@ __export(exports_run, {
6378
6378
  });
6379
6379
  import { randomBytes as randomBytes2 } from "crypto";
6380
6380
  import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
6381
- import { basename as basename3, dirname as dirname8, isAbsolute as isAbsolute4, join as join12 } from "path";
6381
+ import { basename as basename3, dirname as dirname9, isAbsolute as isAbsolute5, join as join13 } from "path";
6382
6382
  function extensionOf(scriptPath) {
6383
6383
  const base = basename3(scriptPath);
6384
6384
  const dot = base.lastIndexOf(".");
@@ -6447,9 +6447,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
6447
6447
  return chunks.join("");
6448
6448
  }
6449
6449
  async function executeVerifiedScript(options) {
6450
- const scriptDir = dirname8(options.scriptPath);
6450
+ const scriptDir = dirname9(options.scriptPath);
6451
6451
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
6452
- const tempPath = join12(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
6452
+ const tempPath = join13(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
6453
6453
  let proc = null;
6454
6454
  let timedOut = false;
6455
6455
  const timeoutTimer = options.timeout ? setTimeout(() => {
@@ -6458,7 +6458,7 @@ async function executeVerifiedScript(options) {
6458
6458
  }, options.timeout) : null;
6459
6459
  try {
6460
6460
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
6461
- proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
6461
+ proc = Bun.spawn([...interpreter.command, isAbsolute5(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
6462
6462
  stdin: new Response(options.stdin),
6463
6463
  stdout: "pipe",
6464
6464
  stderr: "pipe",
@@ -7034,6 +7034,7 @@ var HOOKS = [
7034
7034
  },
7035
7035
  {
7036
7036
  name: "workspace-repos-guard",
7037
+ timeoutSeconds: 10,
7037
7038
  displayName: "Workspace Repos Guard",
7038
7039
  description: "Blocks writes and deletes that break the protected repo-checkout structure (~/.hasna/repos/clones canonical, plus legacy ~/workspace/repos during the migration window): writes to a protected root, top-level entry creation, non-org folders, and any delete under a protected root",
7039
7040
  version: "0.1.0",
@@ -7052,7 +7053,7 @@ var HOOKS = [
7052
7053
  matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
7053
7054
  tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
7054
7055
  rewritesInput: true,
7055
- timeoutSeconds: 5
7056
+ timeoutSeconds: 10
7056
7057
  },
7057
7058
  {
7058
7059
  name: "checktests",
@@ -7541,9 +7542,9 @@ function getHook(name) {
7541
7542
  return HOOKS.find((h) => h.name === name);
7542
7543
  }
7543
7544
  // src/lib/installer.ts
7544
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
7545
- import { join as join7, dirname as dirname4 } from "path";
7546
- import { homedir as homedir3 } from "os";
7545
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
7546
+ import { join as join8, dirname as dirname5 } from "path";
7547
+ import { homedir as homedir4 } from "os";
7547
7548
  import { fileURLToPath as fileURLToPath2 } from "url";
7548
7549
 
7549
7550
  // src/lib/resolve.ts
@@ -7634,6 +7635,137 @@ function resolveHook(name) {
7634
7635
  // src/lib/installer.ts
7635
7636
  init_manifest();
7636
7637
 
7638
+ // src/lib/native-safety.ts
7639
+ import { createHash } from "crypto";
7640
+ import { closeSync, constants, existsSync as existsSync6, fstatSync, lstatSync, openSync, readFileSync as readFileSync3, realpathSync as realpathSync2 } from "fs";
7641
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join7 } from "path";
7642
+ import { homedir as homedir3 } from "os";
7643
+ function isNativeSafetyName(name) {
7644
+ return name === "trash-guard" || name === "workspace-repos-guard";
7645
+ }
7646
+ var supervisorSource = String.raw`async function supervise(encoded) {
7647
+ const fs = require("node:fs");
7648
+ const path = require("node:path");
7649
+ const crypto = require("node:crypto");
7650
+ const cp = require("node:child_process");
7651
+ const fail = () => { process.stderr.write("Native safety guard refused: verification or execution failed.\n"); process.exit(2); };
7652
+ try {
7653
+ const b = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
7654
+ const check = (file, hash, maximum) => {
7655
+ if (!path.isAbsolute(file) || fs.realpathSync(file) !== file) throw Error();
7656
+ for (let parent = path.dirname(file);;) {
7657
+ const s = fs.lstatSync(parent);
7658
+ if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022)) throw Error();
7659
+ if (parent === path.dirname(parent)) break;
7660
+ parent = path.dirname(parent);
7661
+ }
7662
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
7663
+ try {
7664
+ const s = fs.fstatSync(fd);
7665
+ if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022) || s.size > maximum) throw Error();
7666
+ const bytes = fs.readFileSync(fd);
7667
+ if (crypto.createHash("sha256").update(bytes).digest("hex") !== hash) throw Error();
7668
+ return bytes;
7669
+ } finally { fs.closeSync(fd); }
7670
+ };
7671
+ check(b.runtime, b.runtimeSHA256, 200_000_000);
7672
+ const bytes = check(b.worker, b.workerSHA256, 256_000);
7673
+ const deadline = setTimeout(fail, 1500);
7674
+ const chunks = []; let size = 0;
7675
+ for await (const chunk of process.stdin) {
7676
+ size += chunk.length;
7677
+ if (size > 1_048_576) throw Error();
7678
+ chunks.push(chunk);
7679
+ }
7680
+ clearTimeout(deadline);
7681
+ const input = Buffer.concat(chunks);
7682
+ const child = cp.spawnSync(b.runtime, ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", "--eval", bytes.toString("utf8"), b.name], {
7683
+ input, env: { HOME: b.home, PATH: b.path }, cwd: "/", timeout: 2500, killSignal: "SIGKILL", maxBuffer: 1_048_576,
7684
+ });
7685
+ if (child.error || child.status !== 0 || child.signal || child.stderr.length) throw Error();
7686
+ const result = JSON.parse(child.stdout.toString("utf8"));
7687
+ if (!result || Array.isArray(result) || Object.keys(result).length !== 1) throw Error();
7688
+ if (result.verdict === null) return;
7689
+ const output = result.verdict?.hookSpecificOutput;
7690
+ if (!output || Object.keys(result.verdict).length !== 1 || output.hookEventName !== "PreToolUse") throw Error();
7691
+ if (output.permissionDecision === "deny") {
7692
+ if (Object.keys(output).sort().join() !== "hookEventName,permissionDecision,permissionDecisionReason" || typeof output.permissionDecisionReason !== "string" || !output.permissionDecisionReason.trim()) throw Error();
7693
+ } else if (output.permissionDecision === "allow") {
7694
+ if (b.name !== "trash-guard" || Object.keys(output).sort().join() !== "hookEventName,permissionDecision,updatedInput" || !output.updatedInput || typeof output.updatedInput.command !== "string" || !output.updatedInput.command.trim()) throw Error();
7695
+ } else throw Error();
7696
+ process.stdout.write(JSON.stringify(result.verdict));
7697
+ } catch { fail(); }
7698
+ }`;
7699
+ var quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'";
7700
+ var program = `(${supervisorSource})(process.argv[1]);`;
7701
+ var shell = `cd / || exit 2
7702
+ exec 3<&0 || exit 2
7703
+ /usr/bin/env -i HOME="$3" PATH="$4" "$2" --config=/dev/null --no-env-file --no-macros --no-install --eval ${quote(program)} "$1" <&3 3<&- 2>/dev/null &
7704
+ runner=$!
7705
+ exec 3<&-
7706
+ (trap 'kill "$sleeper" 2>/dev/null; exit 0' TERM; /bin/sleep 6 & sleeper=$!; wait "$sleeper"; kill -KILL "$runner" 2>/dev/null) &
7707
+ watchdog=$!
7708
+ wait "$runner"; result=$?
7709
+ kill "$watchdog" 2>/dev/null; wait "$watchdog" 2>/dev/null
7710
+ if [ "$result" -ne 0 ]; then printf '%s\\n' 'Native safety guard refused: verification or execution failed.' >&2; exit 2; fi`;
7711
+ var prefix = `/usr/bin/env -i /bin/sh -c ${quote(shell)} hasna-native-safety-v1 `;
7712
+ function validateBinding(b) {
7713
+ if (!b || !isNativeSafetyName(b.name) || ![b.runtime, b.worker, b.home].every((p) => typeof p === "string" && isAbsolute2(p) && !/[\0\r\n]/.test(p)) || ![b.runtimeSHA256, b.workerSHA256].every((h) => /^[0-9a-f]{64}$/.test(h)) || typeof b.path !== "string" || b.path.split(":").some((p) => !isAbsolute2(p) || /[\0\r\n]/.test(p)))
7714
+ throw new Error("Invalid native safety binding");
7715
+ }
7716
+ function buildNativeSafetyCommand(binding) {
7717
+ validateBinding(binding);
7718
+ const encoded = Buffer.from(JSON.stringify(binding)).toString("base64url");
7719
+ return prefix + [encoded, binding.runtime, binding.home, binding.path].map(quote).join(" ");
7720
+ }
7721
+ function nativeSafetyRegistration(command) {
7722
+ if (!command.startsWith(prefix))
7723
+ return;
7724
+ const encoded = command.slice(prefix.length).match(/^'([A-Za-z0-9_-]+)' /)?.[1];
7725
+ if (!encoded || encoded.length > 16384)
7726
+ return;
7727
+ try {
7728
+ const binding = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
7729
+ return buildNativeSafetyCommand(binding) === command ? binding.name : undefined;
7730
+ } catch {
7731
+ return;
7732
+ }
7733
+ }
7734
+ function pinEligibleFile(file, maximum, executable = false) {
7735
+ if (!isAbsolute2(file) || realpathSync2(file) !== file)
7736
+ throw new Error("Native safety requires canonical files");
7737
+ for (let parent = dirname3(file);; ) {
7738
+ const s = lstatSync(parent);
7739
+ if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || s.mode & 18)
7740
+ throw new Error("Native safety requires trusted, non-writable file ancestry");
7741
+ if (parent === dirname3(parent))
7742
+ break;
7743
+ parent = dirname3(parent);
7744
+ }
7745
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
7746
+ try {
7747
+ const s = fstatSync(fd);
7748
+ if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || s.mode & 18 || s.size > maximum || executable && !(s.mode & 64))
7749
+ throw new Error("Native safety requires an eligible installed file");
7750
+ const bytes = readFileSync3(fd);
7751
+ if (bytes.length > maximum)
7752
+ throw new Error("Native safety file exceeds its bound");
7753
+ return createHash("sha256").update(bytes).digest("hex");
7754
+ } finally {
7755
+ closeSync(fd);
7756
+ }
7757
+ }
7758
+ function installedNativeSafetyCommand(name) {
7759
+ const home = realpathSync2(homedir3());
7760
+ const runtime = realpathSync2(process.execPath);
7761
+ const candidates = [join7(import.meta.dir, "../../bin/native-safety-entry.js"), join7(import.meta.dir, "../bin/native-safety-entry.js"), join7(import.meta.dir, "native-safety-entry.js")];
7762
+ const worker = candidates.find(existsSync6);
7763
+ if (!worker)
7764
+ throw new Error("Bundled native safety runner is missing; build or reinstall @hasna/hooks before installing its guards.");
7765
+ const canonicalWorker = realpathSync2(worker);
7766
+ return buildNativeSafetyCommand({ name, runtime, runtimeSHA256: pinEligibleFile(runtime, 200000000, true), worker: canonicalWorker, workerSHA256: pinEligibleFile(canonicalWorker, 256000), home, path: `${dirname3(runtime)}:${join7(home, ".bun/bin")}:/usr/local/bin:/usr/bin:/bin` });
7767
+ }
7768
+
7637
7769
  // src/lib/mementos-options.ts
7638
7770
  function validateMementosRegistration(name, value) {
7639
7771
  if (value === undefined)
@@ -7695,6 +7827,9 @@ var REGISTRATION_COMMAND_RE = /^(?:hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?|
7695
7827
  function registrationName(command) {
7696
7828
  if (typeof command !== "string")
7697
7829
  return;
7830
+ const native = nativeSafetyRegistration(command);
7831
+ if (native)
7832
+ return native;
7698
7833
  if (/^hooks run mementos-context(?:\s|$)/.test(command.trim())) {
7699
7834
  try {
7700
7835
  retainedMementosRegistration([command.trim()]);
@@ -7714,16 +7849,16 @@ function matchersOverlap(a, b) {
7714
7849
  init_store();
7715
7850
 
7716
7851
  // src/lib/codex-settings.ts
7717
- import { closeSync, existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
7718
- import { dirname as dirname3 } from "path";
7852
+ import { closeSync as closeSync2, existsSync as existsSync8, lstatSync as lstatSync2, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
7853
+ import { dirname as dirname4 } from "path";
7719
7854
  import { randomUUID } from "crypto";
7720
7855
  var snapshots = new WeakMap;
7721
7856
  function current(path) {
7722
7857
  try {
7723
- const stat = lstatSync(path);
7858
+ const stat = lstatSync2(path);
7724
7859
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
7725
7860
  throw new Error("invalid_file");
7726
- return readFileSync4(path, "utf8");
7861
+ return readFileSync5(path, "utf8");
7727
7862
  } catch (error) {
7728
7863
  if (error.code === "ENOENT")
7729
7864
  return null;
@@ -7755,11 +7890,11 @@ function writeCodexSettings(path, settings) {
7755
7890
  const snapshot = snapshots.get(settings);
7756
7891
  if (!snapshot || snapshot.path !== path)
7757
7892
  throw new Error("Codex settings require a complete current read before writing.");
7758
- mkdirSync4(dirname3(path), { recursive: true });
7893
+ mkdirSync4(dirname4(path), { recursive: true });
7759
7894
  const lock = `${path}.hooks-write.lock`;
7760
7895
  let fd;
7761
7896
  try {
7762
- fd = openSync(lock, "wx", 384);
7897
+ fd = openSync2(lock, "wx", 384);
7763
7898
  } catch {
7764
7899
  throw new Error("Codex settings are being edited; retry after the active writer finishes.");
7765
7900
  }
@@ -7775,16 +7910,16 @@ function writeCodexSettings(path, settings) {
7775
7910
  renameSync2(temp, path);
7776
7911
  snapshots.set(settings, { path, raw: next });
7777
7912
  } finally {
7778
- if (existsSync7(temp))
7913
+ if (existsSync8(temp))
7779
7914
  unlinkSync(temp);
7780
- closeSync(fd);
7915
+ closeSync2(fd);
7781
7916
  unlinkSync(lock);
7782
7917
  }
7783
7918
  }
7784
7919
 
7785
7920
  // src/lib/installer.ts
7786
- var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
7787
- var HOOKS_DIR = existsSync8(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
7921
+ var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
7922
+ var HOOKS_DIR = existsSync9(join8(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join8(__dirname2, "..", "..", "hooks") : join8(__dirname2, "..", "hooks");
7788
7923
  function normalizeHookName(name) {
7789
7924
  return name.startsWith("hook-") ? name : `hook-${name}`;
7790
7925
  }
@@ -7796,8 +7931,7 @@ function removeHookEntriesByName(entries, hookName) {
7796
7931
  if (!Array.isArray(entry.hooks))
7797
7932
  return [entry];
7798
7933
  const kept = entry.hooks.filter((h) => {
7799
- const match = h.command?.match(/^hooks run ([\w-]+)/);
7800
- return !(match && match[1] === hookName);
7934
+ return registrationName(h.command) !== hookName;
7801
7935
  });
7802
7936
  return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
7803
7937
  });
@@ -7873,29 +8007,29 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
7873
8007
  return globalOverride;
7874
8008
  const dir = getTargetSettingsDir(target);
7875
8009
  if (target === "codex")
7876
- return join7(scope === "project" ? process.cwd() : homedir3(), dir, "hooks.json");
8010
+ return join8(scope === "project" ? process.cwd() : homedir4(), dir, "hooks.json");
7877
8011
  if (scope === "project") {
7878
- return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
8012
+ return target === "codewith" ? join8(process.cwd(), dir, "config.toml") : join8(process.cwd(), dir, "settings.json");
7879
8013
  }
7880
- return target === "codewith" ? join7(homedir3(), dir, "config.toml") : join7(homedir3(), dir, "settings.json");
8014
+ return target === "codewith" ? join8(homedir4(), dir, "config.toml") : join8(homedir4(), dir, "settings.json");
7881
8015
  }
7882
8016
  function getHookPath(name) {
7883
8017
  const shortName = shortHookName(name);
7884
8018
  const resolved = resolveHookDir(shortName);
7885
8019
  if (resolved)
7886
8020
  return resolved;
7887
- return join7(HOOKS_DIR, normalizeHookName(shortName));
8021
+ return join8(HOOKS_DIR, normalizeHookName(shortName));
7888
8022
  }
7889
8023
  function hookExists(name) {
7890
- return existsSync8(getHookPath(name));
8024
+ return existsSync9(getHookPath(name));
7891
8025
  }
7892
8026
  function readSettings(scope = "global", target = "claude") {
7893
8027
  const path = getSettingsPath(scope, target);
7894
8028
  if (target === "codex")
7895
8029
  return readCodexSettings(path);
7896
8030
  try {
7897
- if (existsSync8(path)) {
7898
- return JSON.parse(readFileSync5(path, "utf-8"));
8031
+ if (existsSync9(path)) {
8032
+ return JSON.parse(readFileSync6(path, "utf-8"));
7899
8033
  }
7900
8034
  } catch (error) {
7901
8035
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -7906,8 +8040,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
7906
8040
  const path = getSettingsPath(scope, target);
7907
8041
  if (target === "codex")
7908
8042
  return writeCodexSettings(path, settings);
7909
- const dir = dirname4(path);
7910
- if (!existsSync8(dir)) {
8043
+ const dir = dirname5(path);
8044
+ if (!existsSync9(dir)) {
7911
8045
  mkdirSync5(dir, { recursive: true });
7912
8046
  }
7913
8047
  writeFileSync4(path, JSON.stringify(settings, null, 2) + `
@@ -7935,6 +8069,9 @@ function codewithTimeout(name) {
7935
8069
  switch (name) {
7936
8070
  case "mementos-context":
7937
8071
  return 12;
8072
+ case "trash-guard":
8073
+ case "workspace-repos-guard":
8074
+ return 10;
7938
8075
  case "knowledge-context":
7939
8076
  return 6;
7940
8077
  case "session-start":
@@ -7944,7 +8081,6 @@ function codewithTimeout(name) {
7944
8081
  case "prompt-guard":
7945
8082
  return 3;
7946
8083
  case "worktree-guard":
7947
- case "trash-guard":
7948
8084
  case "stop-sync":
7949
8085
  return 5;
7950
8086
  default:
@@ -7973,11 +8109,13 @@ function codewithStatusMessage(name) {
7973
8109
  }
7974
8110
  function buildCodewithTomlFragment(name, profile, mementos) {
7975
8111
  const shortName = shortHookName(name);
7976
- const meta = resolveHookMeta(shortName);
8112
+ if (isNativeSafetyName(shortName) && profile)
8113
+ throw new Error("Bundled native safety guards do not select registry profiles");
8114
+ const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
7977
8115
  if (!meta)
7978
8116
  throw new Error(`Hook '${shortName}' not found`);
7979
8117
  validateMementosRegistration(shortName, mementos);
7980
- const command = (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
8118
+ const command = isNativeSafetyName(shortName) ? installedNativeSafetyCommand(shortName) : (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
7981
8119
  const matcher = codewithMatcher(meta.matcher);
7982
8120
  const fragments = [];
7983
8121
  for (const event of getHookEvents(meta)) {
@@ -8002,17 +8140,17 @@ function buildCodewithTomlFragment(name, profile, mementos) {
8002
8140
  function readCodewithConfig(scope, configPath) {
8003
8141
  const path = getSettingsPath(scope, "codewith", configPath);
8004
8142
  try {
8005
- return existsSync8(path) ? readFileSync5(path, "utf-8") : "";
8143
+ return existsSync9(path) ? readFileSync6(path, "utf-8") : "";
8006
8144
  } catch {
8007
8145
  return "";
8008
8146
  }
8009
8147
  }
8010
8148
  function appendCodewithFragment(fragment, scope, configPath) {
8011
8149
  const path = getSettingsPath(scope, "codewith", configPath);
8012
- const dir = dirname4(path);
8013
- if (!existsSync8(dir))
8150
+ const dir = dirname5(path);
8151
+ if (!existsSync9(dir))
8014
8152
  mkdirSync5(dir, { recursive: true });
8015
- const existing = existsSync8(path) ? readFileSync5(path, "utf-8") : "";
8153
+ const existing = existsSync9(path) ? readFileSync6(path, "utf-8") : "";
8016
8154
  const sep2 = existing.trim() ? `
8017
8155
 
8018
8156
  ` : "";
@@ -8020,7 +8158,7 @@ function appendCodewithFragment(fragment, scope, configPath) {
8020
8158
  return path;
8021
8159
  }
8022
8160
  function detectConflict(name, scope, target) {
8023
- const meta = resolveHookMeta(name);
8161
+ const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
8024
8162
  if (!meta || !meta.matcher)
8025
8163
  return;
8026
8164
  const events = new Set(getHookEvents(meta));
@@ -8040,7 +8178,7 @@ function detectConflict(name, scope, target) {
8040
8178
  return;
8041
8179
  }
8042
8180
  function detectRewriteConflict(name, scope, target, lookup = getHook) {
8043
- const meta = resolveHookMeta(name);
8181
+ const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
8044
8182
  if (!meta?.matcher || !meta.rewritesInput)
8045
8183
  return;
8046
8184
  if (!getHookEvents(meta).includes("PreToolUse"))
@@ -8061,7 +8199,10 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
8061
8199
  }
8062
8200
  function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
8063
8201
  const shortName = shortHookName(name);
8064
- if (!hookExists(shortName)) {
8202
+ if (isNativeSafetyName(shortName) && (profile || target === "gemini" || target === "codewith" && codewithMode === "write")) {
8203
+ return { hook: shortName, success: false, error: "Bundled native safety guards support Claude/Codex and managed Codewith fragments, without registry profiles.", target };
8204
+ }
8205
+ if (!isNativeSafetyName(shortName) && !hookExists(shortName)) {
8065
8206
  return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
8066
8207
  }
8067
8208
  if (target === "codewith") {
@@ -8153,7 +8294,7 @@ function installHook(name, options = {}) {
8153
8294
  }
8154
8295
  if (target === "all") {
8155
8296
  const shortName = shortHookName(name);
8156
- const meta = resolveHookMeta(shortName);
8297
+ const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
8157
8298
  if (meta) {
8158
8299
  const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
8159
8300
  if (unsupportedTargets.length > 0) {
@@ -8186,7 +8327,7 @@ function installHook(name, options = {}) {
8186
8327
  return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
8187
8328
  }
8188
8329
  function registerHook(name, scope = "global", target = "claude", profile, mementos) {
8189
- const meta = resolveHookMeta(name);
8330
+ const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
8190
8331
  if (!meta)
8191
8332
  return;
8192
8333
  const eventKeys = getHookEvents(meta).map((event) => {
@@ -8203,8 +8344,17 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
8203
8344
  const settings = readSettings(scope, target);
8204
8345
  if (!settings.hooks)
8205
8346
  settings.hooks = {};
8347
+ if (isNativeSafetyName(name)) {
8348
+ for (const groups of Object.values(settings.hooks))
8349
+ for (const group of groups)
8350
+ for (const hook of group.hooks ?? []) {
8351
+ if (typeof hook.command === "string" && /(?:^|\s)hasna-native-safety-v\d+(?:\s|$)/.test(hook.command) && !registrationName(hook.command)) {
8352
+ throw new Error("Unknown managed safety definition; reconcile its exact version before replacing it.");
8353
+ }
8354
+ }
8355
+ }
8206
8356
  const retained = name === "mementos-context" && mementos === undefined ? retainedMementosRegistration(Object.values(settings.hooks).flatMap((groups) => Array.isArray(groups) ? groups.flatMap((group) => Array.isArray(group.hooks) ? group.hooks.map((hook) => hook.command).filter((command) => typeof command === "string") : []) : [])) : undefined;
8207
- const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
8357
+ const hookCommand = isNativeSafetyName(name) ? installedNativeSafetyCommand(name) : (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
8208
8358
  if (target === "codex") {
8209
8359
  const positions = [];
8210
8360
  for (const [event, groups] of Object.entries(settings.hooks)) {
@@ -8212,9 +8362,11 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
8212
8362
  for (let index = 0;index < entry.hooks.length; index++) {
8213
8363
  const command = entry.hooks[index]?.command;
8214
8364
  const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
8215
- if (match && (match[1] ?? match[2]) === name) {
8365
+ if (match && (match[1] ?? match[2]) === name || registrationName(command) === name) {
8216
8366
  if (registrationName(command) !== name)
8217
8367
  throw new Error("Unrecognized Codex registration; reconcile it before updating this hook.");
8368
+ if (isNativeSafetyName(name) && entry.hooks[index].async !== undefined && entry.hooks[index].async !== false)
8369
+ throw new Error("Asynchronous safety hooks cannot block; reconcile this registration before updating it.");
8218
8370
  positions.push({ event, entry, index });
8219
8371
  }
8220
8372
  }
@@ -8278,12 +8430,20 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
8278
8430
  if (target === "codewith") {
8279
8431
  const config = readCodewithConfig(scope);
8280
8432
  const registered2 = [];
8281
- const re = /^\s*command\s*=\s*["']([^"'\n]+)["']\s*$/gm;
8282
- let match;
8283
- while (match = re.exec(config)) {
8284
- const name = registrationName(match[1]);
8285
- if (name)
8286
- registered2.push(name);
8433
+ try {
8434
+ const parsed = Bun.TOML.parse(config);
8435
+ for (const groups of Object.values(parsed.hooks ?? {})) {
8436
+ if (!Array.isArray(groups))
8437
+ continue;
8438
+ for (const group of groups)
8439
+ for (const hook of group.hooks ?? []) {
8440
+ const name = registrationName(hook.command);
8441
+ if (name)
8442
+ registered2.push(name);
8443
+ }
8444
+ }
8445
+ } catch {
8446
+ return [];
8287
8447
  }
8288
8448
  return [...new Set(registered2)];
8289
8449
  }
@@ -8384,10 +8544,10 @@ function codewithHasHookEntryInText(configText, name) {
8384
8544
  }
8385
8545
  function codewithHasHookEntry(name, scope = "global") {
8386
8546
  const path = getSettingsPath(scope, "codewith");
8387
- if (!existsSync8(path))
8547
+ if (!existsSync9(path))
8388
8548
  return false;
8389
8549
  try {
8390
- return codewithHasHookEntryInText(readFileSync5(path, "utf-8"), name);
8550
+ return codewithHasHookEntryInText(readFileSync6(path, "utf-8"), name);
8391
8551
  } catch {
8392
8552
  return false;
8393
8553
  }
@@ -8396,12 +8556,12 @@ function removeStoreDirFailClosed(dir) {
8396
8556
  const entries = readdirSync3(dir, { withFileTypes: true });
8397
8557
  for (const entry of entries) {
8398
8558
  if (entry.isDirectory()) {
8399
- removeStoreDirFailClosed(join7(dir, entry.name));
8559
+ removeStoreDirFailClosed(join8(dir, entry.name));
8400
8560
  }
8401
8561
  }
8402
8562
  for (const entry of entries) {
8403
8563
  if (!entry.isDirectory()) {
8404
- unlinkSync2(join7(dir, entry.name));
8564
+ unlinkSync2(join8(dir, entry.name));
8405
8565
  }
8406
8566
  }
8407
8567
  rmdirSync(dir);
@@ -8432,8 +8592,8 @@ function uninstallHook(name, scope = "global", target = "claude") {
8432
8592
  const registrationsRemaining = [];
8433
8593
  if (target === "codewith" || target === "all") {
8434
8594
  const codewithPath = getSettingsPath(scope, "codewith");
8435
- if (existsSync8(codewithPath)) {
8436
- const before = readFileSync5(codewithPath, "utf-8");
8595
+ if (existsSync9(codewithPath)) {
8596
+ const before = readFileSync6(codewithPath, "utf-8");
8437
8597
  const after = removeCodewithHookEntry(before, shortName);
8438
8598
  if (after.removed) {
8439
8599
  writeFileSync4(codewithPath, after.text, "utf-8");
@@ -8475,7 +8635,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
8475
8635
  // src/index.ts
8476
8636
  init_profiles();
8477
8637
  init_config();
8478
- import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
8638
+ import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
8479
8639
 
8480
8640
  // src/lib/transport.ts
8481
8641
  init_transport();
@@ -8615,15 +8775,15 @@ init_manifest();
8615
8775
  // src/lib/custom-install.ts
8616
8776
  init_config();
8617
8777
  init_manifest();
8618
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync2 } from "fs";
8619
- import { join as join10 } from "path";
8778
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync9, rmSync as rmSync2 } from "fs";
8779
+ import { join as join11 } from "path";
8620
8780
  import { tmpdir } from "os";
8621
8781
  function manifestAt(dir) {
8622
- const path = join10(dir, "manifest.json");
8623
- if (!existsSync10(path))
8782
+ const path = join11(dir, "manifest.json");
8783
+ if (!existsSync11(path))
8624
8784
  return;
8625
8785
  try {
8626
- return parseManifest(readFileSync8(path, "utf-8"));
8786
+ return parseManifest(readFileSync9(path, "utf-8"));
8627
8787
  } catch {
8628
8788
  return;
8629
8789
  }
@@ -8642,8 +8802,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
8642
8802
  return { name, version: manifest.version, kind, source, dir, scriptPath };
8643
8803
  }
8644
8804
  async function cloneToTemp(gitUrl) {
8645
- const tmp = mkdtempSync(join10(tmpdir(), "hooks-install-"));
8646
- const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join10(tmp, "repo")], {
8805
+ const tmp = mkdtempSync(join11(tmpdir(), "hooks-install-"));
8806
+ const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join11(tmp, "repo")], {
8647
8807
  stdout: "pipe",
8648
8808
  stderr: "pipe"
8649
8809
  });
@@ -8656,7 +8816,7 @@ async function cloneToTemp(gitUrl) {
8656
8816
  rmSync2(tmp, { recursive: true, force: true });
8657
8817
  throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
8658
8818
  }
8659
- return join10(tmp, "repo");
8819
+ return join11(tmp, "repo");
8660
8820
  }
8661
8821
  function isGitUrl(value) {
8662
8822
  return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
@@ -8666,7 +8826,7 @@ function isCustomSource(value) {
8666
8826
  return true;
8667
8827
  if (value.startsWith("http://") || value.startsWith("https://"))
8668
8828
  return true;
8669
- return existsSync10(value);
8829
+ return existsSync11(value);
8670
8830
  }
8671
8831
  async function installCustomSource(source) {
8672
8832
  if (isGitUrl(source)) {
@@ -8680,11 +8840,11 @@ async function installCustomSource(source) {
8680
8840
  if (source.startsWith("http://") || source.startsWith("https://")) {
8681
8841
  return installManifestUrl(source);
8682
8842
  }
8683
- if (!existsSync10(source)) {
8843
+ if (!existsSync11(source)) {
8684
8844
  throw new Error(`No such local path: ${source}`);
8685
8845
  }
8686
8846
  if (source.endsWith("manifest.json")) {
8687
- const manifest = parseManifest(readFileSync8(source, "utf-8"));
8847
+ const manifest = parseManifest(readFileSync9(source, "utf-8"));
8688
8848
  return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
8689
8849
  }
8690
8850
  return copyManifestDir(source, source, "local");
@@ -8714,16 +8874,16 @@ async function installManifestUrl(url) {
8714
8874
  return { name, version: manifest.version, kind: "url", source: url, dir, scriptPath };
8715
8875
  }
8716
8876
  // src/lib/sync.ts
8717
- import { createHash as createHash2 } from "crypto";
8718
- import { readFileSync as readFileSync9 } from "fs";
8877
+ import { createHash as createHash3 } from "crypto";
8878
+ import { readFileSync as readFileSync10 } from "fs";
8719
8879
  init_db();
8720
8880
  init_store();
8721
8881
  init_manifest();
8722
8882
  function sha256OfText(text) {
8723
- return createHash2("sha256").update(text).digest("hex");
8883
+ return createHash3("sha256").update(text).digest("hex");
8724
8884
  }
8725
8885
  function sha256OfPath(path) {
8726
- return createHash2("sha256").update(readFileSync9(path)).digest("hex");
8886
+ return createHash3("sha256").update(readFileSync10(path)).digest("hex");
8727
8887
  }
8728
8888
  function collectBundledCatalog() {
8729
8889
  const out = [];
@@ -9024,8 +9184,8 @@ async function installPinnedFromBundled(name, version) {
9024
9184
  // src/serve.ts
9025
9185
  init_manifest();
9026
9186
  init_store();
9027
- import { readFileSync as readFileSync10 } from "fs";
9028
- import { dirname as dirname6, join as join11 } from "path";
9187
+ import { readFileSync as readFileSync11 } from "fs";
9188
+ import { dirname as dirname7, join as join12 } from "path";
9029
9189
  import { fileURLToPath as fileURLToPath4 } from "url";
9030
9190
 
9031
9191
  // src/lib/secure-compare.ts
@@ -9042,155 +9202,356 @@ function secureEqual(a, b) {
9042
9202
  }
9043
9203
 
9044
9204
  // src/openapi.ts
9045
- var openApiDocument = {
9046
- openapi: "3.1.0",
9047
- info: {
9048
- title: "Hooks Registry API",
9049
- version: "0.1.0",
9050
- description: "Local registry HTTP API for @hasna/hooks: catalog, lock, and artifact reads are open; publishing (PUT /api/v1/hooks) requires the API key (authorization: Bearer or x-api-key)."
9051
- },
9052
- servers: [{ url: "/" }],
9053
- paths: {
9054
- "/health": {
9055
- get: {
9056
- summary: "Liveness probe",
9057
- operationId: "getHealth",
9058
- responses: {
9059
- "200": {
9060
- description: "Service is alive",
9061
- content: {
9062
- "application/json": {
9063
- schema: { type: "object", additionalProperties: true }
9205
+ function buildOpenApiDocument(version) {
9206
+ return {
9207
+ openapi: "3.1.0",
9208
+ info: {
9209
+ title: "Hooks Registry API",
9210
+ version,
9211
+ description: "Local registry HTTP API for @hasna/hooks: catalog, lock, and artifact reads are open; publishing (PUT /api/v1/hooks) requires the API key (authorization: Bearer or x-api-key)."
9212
+ },
9213
+ servers: [{ url: "/" }],
9214
+ paths: {
9215
+ "/health": {
9216
+ get: {
9217
+ summary: "Liveness probe",
9218
+ operationId: "getHealth",
9219
+ responses: {
9220
+ "200": {
9221
+ description: "Service is alive",
9222
+ content: {
9223
+ "application/json": {
9224
+ schema: { $ref: "#/components/schemas/HooksHealth" }
9225
+ }
9064
9226
  }
9065
9227
  }
9066
9228
  }
9067
9229
  }
9068
- }
9069
- },
9070
- "/ready": {
9071
- get: {
9072
- summary: "Readiness probe \u2014 the hooks.lock store is readable",
9073
- operationId: "getReady",
9074
- responses: {
9075
- "200": { description: "Lock store is readable" },
9076
- "503": { description: "Lock store is not readable" }
9230
+ },
9231
+ "/ready": {
9232
+ get: {
9233
+ summary: "Readiness probe \u2014 the hooks.lock store is readable",
9234
+ operationId: "getReady",
9235
+ responses: {
9236
+ "200": {
9237
+ description: "Lock store is readable",
9238
+ content: {
9239
+ "application/json": {
9240
+ schema: { $ref: "#/components/schemas/HooksHealth" }
9241
+ }
9242
+ }
9243
+ },
9244
+ "503": {
9245
+ description: "Lock store is not readable",
9246
+ content: {
9247
+ "application/json": {
9248
+ schema: { $ref: "#/components/schemas/HooksHealth" }
9249
+ }
9250
+ }
9251
+ }
9252
+ }
9077
9253
  }
9078
- }
9079
- },
9080
- "/version": {
9081
- get: {
9082
- summary: "Package version",
9083
- operationId: "getVersion",
9084
- responses: {
9085
- "200": {
9086
- description: "The installed @hasna/hooks version",
9087
- content: {
9088
- "application/json": {
9089
- schema: { type: "object", properties: { version: { type: "string" } } }
9254
+ },
9255
+ "/version": {
9256
+ get: {
9257
+ summary: "Package version",
9258
+ operationId: "getVersion",
9259
+ responses: {
9260
+ "200": {
9261
+ description: "The installed @hasna/hooks version",
9262
+ content: {
9263
+ "application/json": {
9264
+ schema: { $ref: "#/components/schemas/HooksVersion" }
9265
+ }
9090
9266
  }
9091
9267
  }
9092
9268
  }
9093
9269
  }
9094
- }
9095
- },
9096
- "/openapi.json": {
9097
- get: {
9098
- summary: "This OpenAPI document",
9099
- operationId: "getOpenApi",
9100
- responses: { "200": { description: "OpenAPI 3.1 document" } }
9101
- }
9102
- },
9103
- "/api/v1/catalog": {
9104
- get: {
9105
- summary: "List the full hook catalog with versions and sha256",
9106
- operationId: "getCatalog",
9107
- responses: {
9108
- "200": {
9109
- description: "Catalog entries",
9110
- content: {
9111
- "application/json": {
9112
- schema: {
9113
- type: "object",
9114
- properties: {
9115
- hooks: {
9116
- type: "array",
9117
- items: {
9118
- type: "object",
9119
- properties: {
9120
- name: { type: "string" },
9121
- version: { type: "string" },
9122
- sha256: { type: "string" },
9123
- events: { type: "array", items: { type: "string" } }
9124
- }
9125
- }
9126
- }
9127
- }
9270
+ },
9271
+ "/openapi.json": {
9272
+ get: {
9273
+ summary: "This OpenAPI document",
9274
+ operationId: "getOpenApi",
9275
+ responses: {
9276
+ "200": {
9277
+ description: "OpenAPI 3.1 document",
9278
+ content: {
9279
+ "application/json": {
9280
+ schema: { type: "object", additionalProperties: true }
9128
9281
  }
9129
9282
  }
9130
9283
  }
9131
9284
  }
9132
9285
  }
9133
- }
9134
- },
9135
- "/api/v1/lock": {
9136
- get: {
9137
- summary: "Read the published hooks.lock with per-hook versions",
9138
- operationId: "getLock",
9139
- responses: { "200": { description: "The lock file" } }
9140
- }
9141
- },
9142
- "/api/v1/hooks/{name}/{version}": {
9143
- get: {
9144
- summary: "Fetch one hook's manifest and script",
9145
- operationId: "getHookArtifact",
9146
- parameters: [
9147
- { name: "name", in: "path", required: true, schema: { type: "string" } },
9148
- { name: "version", in: "path", required: true, schema: { type: "string" } }
9149
- ],
9150
- responses: {
9151
- "200": {
9152
- description: "Hook artifact; x-hook-sha256 header carries the script hash"
9286
+ },
9287
+ "/api/v1/catalog": {
9288
+ get: {
9289
+ summary: "List the full hook catalog with versions and sha256",
9290
+ operationId: "getCatalog",
9291
+ responses: {
9292
+ "200": {
9293
+ description: "Catalog entries",
9294
+ content: {
9295
+ "application/json": {
9296
+ schema: { $ref: "#/components/schemas/HooksCatalog" }
9297
+ }
9298
+ }
9299
+ }
9300
+ }
9301
+ }
9302
+ },
9303
+ "/api/v1/lock": {
9304
+ get: {
9305
+ summary: "Read the published hooks.lock with per-hook versions",
9306
+ operationId: "getLock",
9307
+ responses: {
9308
+ "200": {
9309
+ description: "The lock file",
9310
+ content: {
9311
+ "application/json": {
9312
+ schema: { $ref: "#/components/schemas/HooksLock" }
9313
+ }
9314
+ }
9315
+ }
9316
+ }
9317
+ }
9318
+ },
9319
+ "/api/v1/hooks/{name}/{version}": {
9320
+ get: {
9321
+ summary: "Fetch one hook's manifest and script",
9322
+ operationId: "getHookArtifact",
9323
+ parameters: [
9324
+ { name: "name", in: "path", required: true, schema: { type: "string" } },
9325
+ { name: "version", in: "path", required: true, schema: { type: "string" } }
9326
+ ],
9327
+ responses: {
9328
+ "200": {
9329
+ description: "Hook artifact; x-hook-sha256 header carries the script hash",
9330
+ headers: {
9331
+ "x-hook-sha256": {
9332
+ description: "sha256 of the served script, the same value the lock pins",
9333
+ schema: { type: "string" }
9334
+ }
9335
+ },
9336
+ content: {
9337
+ "application/json": {
9338
+ schema: { $ref: "#/components/schemas/HooksArtifact" }
9339
+ }
9340
+ }
9341
+ },
9342
+ "400": {
9343
+ description: "Invalid URL encoding or semver version",
9344
+ content: {
9345
+ "application/json": {
9346
+ schema: { $ref: "#/components/schemas/HooksError" }
9347
+ }
9348
+ }
9349
+ },
9350
+ "404": {
9351
+ description: "Hook not found locally",
9352
+ content: {
9353
+ "application/json": {
9354
+ schema: { $ref: "#/components/schemas/HooksError" }
9355
+ }
9356
+ }
9357
+ }
9358
+ }
9359
+ }
9360
+ },
9361
+ "/api/v1/hooks": {
9362
+ put: {
9363
+ summary: "Publish/retrust a hook (requires the API key)",
9364
+ operationId: "putHook",
9365
+ requestBody: {
9366
+ required: true,
9367
+ content: {
9368
+ "application/json": {
9369
+ schema: { $ref: "#/components/schemas/HooksPublishRequest" }
9370
+ }
9371
+ }
9372
+ },
9373
+ responses: {
9374
+ "200": {
9375
+ description: "Hook retrusted and pin updated",
9376
+ content: {
9377
+ "application/json": {
9378
+ schema: { $ref: "#/components/schemas/HooksPublishResponse" }
9379
+ }
9380
+ }
9381
+ },
9382
+ "400": {
9383
+ description: "Invalid JSON body",
9384
+ content: {
9385
+ "application/json": {
9386
+ schema: { $ref: "#/components/schemas/HooksError" }
9387
+ }
9388
+ }
9389
+ },
9390
+ "401": {
9391
+ description: "Missing or invalid API key",
9392
+ content: {
9393
+ "application/json": {
9394
+ schema: { $ref: "#/components/schemas/HooksError" }
9395
+ }
9396
+ }
9397
+ },
9398
+ "404": {
9399
+ description: "Hook not found in local store",
9400
+ content: {
9401
+ "application/json": {
9402
+ schema: { $ref: "#/components/schemas/HooksError" }
9403
+ }
9404
+ }
9405
+ },
9406
+ "409": {
9407
+ description: "Version mismatch with the local store",
9408
+ content: {
9409
+ "application/json": {
9410
+ schema: { $ref: "#/components/schemas/HooksError" }
9411
+ }
9412
+ }
9413
+ }
9153
9414
  },
9154
- "400": { description: "Invalid URL encoding or semver version" },
9155
- "404": { description: "Hook not found locally" }
9415
+ security: [{ apiKey: [] }]
9156
9416
  }
9157
9417
  }
9158
9418
  },
9159
- "/api/v1/hooks": {
9160
- put: {
9161
- summary: "Publish/retrust a hook (requires the API key)",
9162
- operationId: "putHook",
9163
- requestBody: {
9164
- required: true,
9165
- content: {
9166
- "application/json": {
9167
- schema: {
9168
- type: "object",
9169
- properties: { name: { type: "string" }, version: { type: "string" } }
9170
- }
9419
+ components: {
9420
+ securitySchemes: {
9421
+ apiKey: {
9422
+ type: "http",
9423
+ scheme: "bearer",
9424
+ description: "The registry API key \u2014 resolved by the client through the @hasna/contracts chain (HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials); the server compares the inbound bearer/x-api-key value."
9425
+ }
9426
+ },
9427
+ schemas: {
9428
+ HooksHealth: {
9429
+ type: "object",
9430
+ additionalProperties: false,
9431
+ required: ["status", "name"],
9432
+ properties: {
9433
+ status: { type: "string" },
9434
+ name: { type: "string" }
9435
+ }
9436
+ },
9437
+ HooksVersion: {
9438
+ type: "object",
9439
+ additionalProperties: false,
9440
+ required: ["version"],
9441
+ properties: { version: { type: "string" } }
9442
+ },
9443
+ HooksCatalogEntry: {
9444
+ type: "object",
9445
+ additionalProperties: false,
9446
+ required: ["name", "version", "sha256", "events", "description", "source", "versions"],
9447
+ properties: {
9448
+ name: { type: "string" },
9449
+ version: { type: "string" },
9450
+ sha256: { type: "string" },
9451
+ events: { type: "array", items: { type: "string" } },
9452
+ description: { type: "string" },
9453
+ source: {
9454
+ type: "string",
9455
+ description: "bundled | custom | custom-overrides-bundled"
9456
+ },
9457
+ versions: { type: "array", items: { type: "string" } }
9458
+ }
9459
+ },
9460
+ HooksCatalog: {
9461
+ type: "object",
9462
+ additionalProperties: false,
9463
+ required: ["hooks"],
9464
+ properties: {
9465
+ hooks: { type: "array", items: { $ref: "#/components/schemas/HooksCatalogEntry" } }
9466
+ }
9467
+ },
9468
+ HooksLockEntry: {
9469
+ type: "object",
9470
+ additionalProperties: false,
9471
+ required: ["version", "sha256", "source"],
9472
+ properties: {
9473
+ version: { type: "string" },
9474
+ sha256: { type: "string" },
9475
+ source: { type: "string" },
9476
+ pinned: {
9477
+ type: "boolean",
9478
+ description: "True for an explicit install/update pin, absent for sync-maintained pins."
9479
+ },
9480
+ versions: { type: "array", items: { type: "string" } }
9481
+ }
9482
+ },
9483
+ HooksLock: {
9484
+ type: "object",
9485
+ additionalProperties: false,
9486
+ required: ["hooks"],
9487
+ properties: {
9488
+ hooks: {
9489
+ type: "object",
9490
+ additionalProperties: { $ref: "#/components/schemas/HooksLockEntry" }
9171
9491
  }
9172
9492
  }
9173
9493
  },
9174
- responses: {
9175
- "200": { description: "Hook retrusted and pin updated" },
9176
- "401": { description: "Missing or invalid API key" },
9177
- "404": { description: "Hook not found in local store" },
9178
- "409": { description: "Version mismatch with the local store" }
9494
+ HooksManifest: {
9495
+ type: "object",
9496
+ additionalProperties: false,
9497
+ required: ["name", "version", "events", "script"],
9498
+ properties: {
9499
+ name: { type: "string" },
9500
+ version: { type: "string" },
9501
+ description: { type: "string" },
9502
+ events: { type: "array", items: { type: "string" } },
9503
+ script: { type: "string" },
9504
+ script_kind: { type: "string", enum: ["inline", "file"] },
9505
+ args: { type: "array", items: { type: "string" } },
9506
+ timeout_ms: { type: "number" }
9507
+ }
9179
9508
  },
9180
- security: [{ apiKey: [] }]
9181
- }
9182
- }
9183
- },
9184
- components: {
9185
- securitySchemes: {
9186
- apiKey: {
9187
- type: "http",
9188
- scheme: "bearer",
9189
- description: "The registry API key \u2014 resolved by the client through the @hasna/contracts chain (HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials); the server compares the inbound bearer/x-api-key value."
9509
+ HooksArtifact: {
9510
+ type: "object",
9511
+ additionalProperties: false,
9512
+ required: ["manifest", "script"],
9513
+ properties: {
9514
+ manifest: { $ref: "#/components/schemas/HooksManifest" },
9515
+ script: { type: "string" }
9516
+ }
9517
+ },
9518
+ HooksError: {
9519
+ type: "object",
9520
+ additionalProperties: false,
9521
+ required: ["error"],
9522
+ properties: { error: { type: "string" } }
9523
+ },
9524
+ HooksPublishRequest: {
9525
+ type: "object",
9526
+ additionalProperties: false,
9527
+ required: ["name"],
9528
+ properties: {
9529
+ name: { type: "string" },
9530
+ version: { type: "string" }
9531
+ }
9532
+ },
9533
+ HooksPublishResponse: {
9534
+ type: "object",
9535
+ additionalProperties: false,
9536
+ required: ["ok", "hook"],
9537
+ properties: {
9538
+ ok: { type: "boolean" },
9539
+ hook: {
9540
+ type: "object",
9541
+ additionalProperties: false,
9542
+ required: ["name", "version", "sha256"],
9543
+ properties: {
9544
+ name: { type: "string" },
9545
+ version: { type: "string" },
9546
+ sha256: { type: "string" }
9547
+ }
9548
+ }
9549
+ }
9550
+ }
9190
9551
  }
9191
9552
  }
9192
- }
9193
- };
9553
+ };
9554
+ }
9194
9555
 
9195
9556
  // src/serve.ts
9196
9557
  init_semver();
@@ -9199,8 +9560,8 @@ var SERVE_HOST = "127.0.0.1";
9199
9560
  var SERVE_SERVICE_NAME = "hooks-registry";
9200
9561
  function packageVersion() {
9201
9562
  try {
9202
- const pkgPath = join11(dirname6(fileURLToPath4(import.meta.url)), "..", "package.json");
9203
- return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
9563
+ const pkgPath = join12(dirname7(fileURLToPath4(import.meta.url)), "..", "package.json");
9564
+ return JSON.parse(readFileSync11(pkgPath, "utf-8")).version || "0.0.0";
9204
9565
  } catch {
9205
9566
  return "0.0.0";
9206
9567
  }
@@ -9289,7 +9650,7 @@ function handleServeRequest(req, apiKey) {
9289
9650
  return Promise.resolve(json({ version: packageVersion() }));
9290
9651
  }
9291
9652
  if (url.pathname === "/openapi.json" && req.method === "GET") {
9292
- return Promise.resolve(json(openApiDocument));
9653
+ return Promise.resolve(json(buildOpenApiDocument(packageVersion())));
9293
9654
  }
9294
9655
  if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
9295
9656
  return buildCatalog().then((catalog) => json({ hooks: catalog }));
@@ -9466,11 +9827,11 @@ async function runHook(name, input, options = {}) {
9466
9827
  throw new Error(`Hook '${name}' not found`);
9467
9828
  const hookScript = getHookPath(name) + "/src/hook.ts";
9468
9829
  const resolvedScript = custom2?.scriptPath;
9469
- const script = resolvedScript ?? (existsSync12(hookScript) ? hookScript : undefined);
9830
+ const script = resolvedScript ?? (existsSync13(hookScript) ? hookScript : undefined);
9470
9831
  if (!script)
9471
9832
  throw new Error(`Hook script not found: ${name}`);
9472
9833
  const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
9473
- const content = readFileSync11(script);
9834
+ const content = readFileSync12(script);
9474
9835
  const check = checkScriptHash2(name, sha256Of2(content));
9475
9836
  if (!check.ok) {
9476
9837
  throw new Error(`Hook '${name}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${name}' to trust the new content.`);