@hasna/hooks 0.9.4 → 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/bin/index.js CHANGED
@@ -2075,6 +2075,7 @@ var init_registry = __esm(() => {
2075
2075
  },
2076
2076
  {
2077
2077
  name: "workspace-repos-guard",
2078
+ timeoutSeconds: 10,
2078
2079
  displayName: "Workspace Repos Guard",
2079
2080
  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",
2080
2081
  version: "0.1.0",
@@ -2093,7 +2094,7 @@ var init_registry = __esm(() => {
2093
2094
  matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
2094
2095
  tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
2095
2096
  rewritesInput: true,
2096
- timeoutSeconds: 5
2097
+ timeoutSeconds: 10
2097
2098
  },
2098
2099
  {
2099
2100
  name: "checktests",
@@ -4500,6 +4501,139 @@ var init_resolve = __esm(() => {
4500
4501
  init_manifest();
4501
4502
  });
4502
4503
 
4504
+ // src/lib/native-safety.ts
4505
+ import { createHash } from "crypto";
4506
+ import { closeSync, constants, existsSync as existsSync7, fstatSync, lstatSync, openSync, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
4507
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join8 } from "path";
4508
+ import { homedir as homedir3 } from "os";
4509
+ function isNativeSafetyName(name) {
4510
+ return name === "trash-guard" || name === "workspace-repos-guard";
4511
+ }
4512
+ function validateBinding(b) {
4513
+ 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)))
4514
+ throw new Error("Invalid native safety binding");
4515
+ }
4516
+ function buildNativeSafetyCommand(binding) {
4517
+ validateBinding(binding);
4518
+ const encoded = Buffer.from(JSON.stringify(binding)).toString("base64url");
4519
+ return prefix + [encoded, binding.runtime, binding.home, binding.path].map(quote).join(" ");
4520
+ }
4521
+ function nativeSafetyRegistration(command) {
4522
+ if (!command.startsWith(prefix))
4523
+ return;
4524
+ const encoded = command.slice(prefix.length).match(/^'([A-Za-z0-9_-]+)' /)?.[1];
4525
+ if (!encoded || encoded.length > 16384)
4526
+ return;
4527
+ try {
4528
+ const binding = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
4529
+ return buildNativeSafetyCommand(binding) === command ? binding.name : undefined;
4530
+ } catch {
4531
+ return;
4532
+ }
4533
+ }
4534
+ function pinEligibleFile(file, maximum, executable = false) {
4535
+ if (!isAbsolute2(file) || realpathSync2(file) !== file)
4536
+ throw new Error("Native safety requires canonical files");
4537
+ for (let parent = dirname3(file);; ) {
4538
+ const s = lstatSync(parent);
4539
+ if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || s.mode & 18)
4540
+ throw new Error("Native safety requires trusted, non-writable file ancestry");
4541
+ if (parent === dirname3(parent))
4542
+ break;
4543
+ parent = dirname3(parent);
4544
+ }
4545
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
4546
+ try {
4547
+ const s = fstatSync(fd);
4548
+ if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || s.mode & 18 || s.size > maximum || executable && !(s.mode & 64))
4549
+ throw new Error("Native safety requires an eligible installed file");
4550
+ const bytes = readFileSync2(fd);
4551
+ if (bytes.length > maximum)
4552
+ throw new Error("Native safety file exceeds its bound");
4553
+ return createHash("sha256").update(bytes).digest("hex");
4554
+ } finally {
4555
+ closeSync(fd);
4556
+ }
4557
+ }
4558
+ function installedNativeSafetyCommand(name) {
4559
+ const home = realpathSync2(homedir3());
4560
+ const runtime = realpathSync2(process.execPath);
4561
+ const candidates = [join8(import.meta.dir, "../../bin/native-safety-entry.js"), join8(import.meta.dir, "../bin/native-safety-entry.js"), join8(import.meta.dir, "native-safety-entry.js")];
4562
+ const worker = candidates.find(existsSync7);
4563
+ if (!worker)
4564
+ throw new Error("Bundled native safety runner is missing; build or reinstall @hasna/hooks before installing its guards.");
4565
+ const canonicalWorker = realpathSync2(worker);
4566
+ return buildNativeSafetyCommand({ name, runtime, runtimeSHA256: pinEligibleFile(runtime, 200000000, true), worker: canonicalWorker, workerSHA256: pinEligibleFile(canonicalWorker, 256000), home, path: `${dirname3(runtime)}:${join8(home, ".bun/bin")}:/usr/local/bin:/usr/bin:/bin` });
4567
+ }
4568
+ var supervisorSource, quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'", program2, shell, prefix;
4569
+ var init_native_safety = __esm(() => {
4570
+ supervisorSource = String.raw`async function supervise(encoded) {
4571
+ const fs = require("node:fs");
4572
+ const path = require("node:path");
4573
+ const crypto = require("node:crypto");
4574
+ const cp = require("node:child_process");
4575
+ const fail = () => { process.stderr.write("Native safety guard refused: verification or execution failed.\n"); process.exit(2); };
4576
+ try {
4577
+ const b = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
4578
+ const check = (file, hash, maximum) => {
4579
+ if (!path.isAbsolute(file) || fs.realpathSync(file) !== file) throw Error();
4580
+ for (let parent = path.dirname(file);;) {
4581
+ const s = fs.lstatSync(parent);
4582
+ if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022)) throw Error();
4583
+ if (parent === path.dirname(parent)) break;
4584
+ parent = path.dirname(parent);
4585
+ }
4586
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
4587
+ try {
4588
+ const s = fs.fstatSync(fd);
4589
+ if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022) || s.size > maximum) throw Error();
4590
+ const bytes = fs.readFileSync(fd);
4591
+ if (crypto.createHash("sha256").update(bytes).digest("hex") !== hash) throw Error();
4592
+ return bytes;
4593
+ } finally { fs.closeSync(fd); }
4594
+ };
4595
+ check(b.runtime, b.runtimeSHA256, 200_000_000);
4596
+ const bytes = check(b.worker, b.workerSHA256, 256_000);
4597
+ const deadline = setTimeout(fail, 1500);
4598
+ const chunks = []; let size = 0;
4599
+ for await (const chunk of process.stdin) {
4600
+ size += chunk.length;
4601
+ if (size > 1_048_576) throw Error();
4602
+ chunks.push(chunk);
4603
+ }
4604
+ clearTimeout(deadline);
4605
+ const input = Buffer.concat(chunks);
4606
+ const child = cp.spawnSync(b.runtime, ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", "--eval", bytes.toString("utf8"), b.name], {
4607
+ input, env: { HOME: b.home, PATH: b.path }, cwd: "/", timeout: 2500, killSignal: "SIGKILL", maxBuffer: 1_048_576,
4608
+ });
4609
+ if (child.error || child.status !== 0 || child.signal || child.stderr.length) throw Error();
4610
+ const result = JSON.parse(child.stdout.toString("utf8"));
4611
+ if (!result || Array.isArray(result) || Object.keys(result).length !== 1) throw Error();
4612
+ if (result.verdict === null) return;
4613
+ const output = result.verdict?.hookSpecificOutput;
4614
+ if (!output || Object.keys(result.verdict).length !== 1 || output.hookEventName !== "PreToolUse") throw Error();
4615
+ if (output.permissionDecision === "deny") {
4616
+ if (Object.keys(output).sort().join() !== "hookEventName,permissionDecision,permissionDecisionReason" || typeof output.permissionDecisionReason !== "string" || !output.permissionDecisionReason.trim()) throw Error();
4617
+ } else if (output.permissionDecision === "allow") {
4618
+ 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();
4619
+ } else throw Error();
4620
+ process.stdout.write(JSON.stringify(result.verdict));
4621
+ } catch { fail(); }
4622
+ }`;
4623
+ program2 = `(${supervisorSource})(process.argv[1]);`;
4624
+ shell = `cd / || exit 2
4625
+ exec 3<&0 || exit 2
4626
+ /usr/bin/env -i HOME="$3" PATH="$4" "$2" --config=/dev/null --no-env-file --no-macros --no-install --eval ${quote(program2)} "$1" <&3 3<&- 2>/dev/null &
4627
+ runner=$!
4628
+ exec 3<&-
4629
+ (trap 'kill "$sleeper" 2>/dev/null; exit 0' TERM; /bin/sleep 6 & sleeper=$!; wait "$sleeper"; kill -KILL "$runner" 2>/dev/null) &
4630
+ watchdog=$!
4631
+ wait "$runner"; result=$?
4632
+ kill "$watchdog" 2>/dev/null; wait "$watchdog" 2>/dev/null
4633
+ if [ "$result" -ne 0 ]; then printf '%s\\n' 'Native safety guard refused: verification or execution failed.' >&2; exit 2; fi`;
4634
+ prefix = `/usr/bin/env -i /bin/sh -c ${quote(shell)} hasna-native-safety-v1 `;
4635
+ });
4636
+
4503
4637
  // src/lib/mementos-options.ts
4504
4638
  function validateMementosRegistration(name, value) {
4505
4639
  if (value === undefined)
@@ -4570,6 +4704,9 @@ function mementosRunEnvironment(env, value) {
4570
4704
  function registrationName(command) {
4571
4705
  if (typeof command !== "string")
4572
4706
  return;
4707
+ const native = nativeSafetyRegistration(command);
4708
+ if (native)
4709
+ return native;
4573
4710
  if (/^hooks run mementos-context(?:\s|$)/.test(command.trim())) {
4574
4711
  try {
4575
4712
  retainedMementosRegistration([command.trim()]);
@@ -4662,8 +4799,7 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
4662
4799
  if (!Array.isArray(hooks))
4663
4800
  return false;
4664
4801
  return hooks.some((entry) => entry?.hooks?.some((h) => {
4665
- const match = h?.command?.match(/^hooks run ([\w-]+)/);
4666
- if (!match || match[1] !== name)
4802
+ if (registrationName(h?.command) !== name)
4667
4803
  return false;
4668
4804
  if (!matcher)
4669
4805
  return true;
@@ -4675,6 +4811,7 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
4675
4811
  }
4676
4812
  var REGISTRATION_COMMAND_RE;
4677
4813
  var init_registration = __esm(() => {
4814
+ init_native_safety();
4678
4815
  init_resolve();
4679
4816
  REGISTRATION_COMMAND_RE = /^(?:hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?|hook-([\w-]+))$/;
4680
4817
  });
@@ -4873,9 +5010,9 @@ var init_migrations = __esm(() => {
4873
5010
  });
4874
5011
 
4875
5012
  // src/db/legacy-import.ts
4876
- import { existsSync as existsSync7, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
4877
- import { join as join8 } from "path";
4878
- import { homedir as homedir3 } from "os";
5013
+ import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
5014
+ import { join as join9 } from "path";
5015
+ import { homedir as homedir4 } from "os";
4879
5016
  function ensureMetaTable(db) {
4880
5017
  db.exec(`
4881
5018
  CREATE TABLE IF NOT EXISTS _meta (
@@ -4898,7 +5035,7 @@ function nanoid() {
4898
5035
  function importJsonlFile(db, filePath) {
4899
5036
  let count = 0;
4900
5037
  try {
4901
- const lines = readFileSync2(filePath, "utf-8").split(`
5038
+ const lines = readFileSync3(filePath, "utf-8").split(`
4902
5039
  `).filter(Boolean);
4903
5040
  for (const line of lines) {
4904
5041
  try {
@@ -4924,7 +5061,7 @@ function importJsonlFile(db, filePath) {
4924
5061
  function importErrorsLog(db, filePath) {
4925
5062
  let count = 0;
4926
5063
  try {
4927
- const lines = readFileSync2(filePath, "utf-8").split(`
5064
+ const lines = readFileSync3(filePath, "utf-8").split(`
4928
5065
  `).filter(Boolean);
4929
5066
  const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
4930
5067
  for (const line of lines) {
@@ -4949,25 +5086,25 @@ function importErrorsLog(db, filePath) {
4949
5086
  } catch {}
4950
5087
  return count;
4951
5088
  }
4952
- function runLegacyImport(db, homeDir = homedir3()) {
5089
+ function runLegacyImport(db, homeDir = homedir4()) {
4953
5090
  try {
4954
5091
  if (isAlreadyDone(db))
4955
5092
  return;
4956
5093
  let total = 0;
4957
- const claudeProjectsDir = join8(homeDir, ".claude", "projects");
4958
- if (existsSync7(claudeProjectsDir)) {
5094
+ const claudeProjectsDir = join9(homeDir, ".claude", "projects");
5095
+ if (existsSync8(claudeProjectsDir)) {
4959
5096
  try {
4960
5097
  const projectDirs = readdirSync2(claudeProjectsDir);
4961
5098
  for (const dir of projectDirs) {
4962
- const projectDir = join8(claudeProjectsDir, dir);
5099
+ const projectDir = join9(claudeProjectsDir, dir);
4963
5100
  try {
4964
5101
  const files = readdirSync2(projectDir);
4965
5102
  for (const file of files) {
4966
5103
  if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
4967
- total += importJsonlFile(db, join8(projectDir, file));
5104
+ total += importJsonlFile(db, join9(projectDir, file));
4968
5105
  }
4969
5106
  if (file === "errors.log") {
4970
- total += importErrorsLog(db, join8(projectDir, file));
5107
+ total += importErrorsLog(db, join9(projectDir, file));
4971
5108
  }
4972
5109
  }
4973
5110
  } catch {}
@@ -5014,8 +5151,8 @@ __export(exports_db, {
5014
5151
  allowLocalStore: () => allowLocalStore
5015
5152
  });
5016
5153
  import { Database } from "bun:sqlite";
5017
- import { existsSync as existsSync8, mkdirSync as mkdirSync2, cpSync } from "fs";
5018
- import { join as join9 } from "path";
5154
+ import { existsSync as existsSync9, mkdirSync as mkdirSync2, cpSync } from "fs";
5155
+ import { join as join10 } from "path";
5019
5156
  function refuseLocalStore(message) {
5020
5157
  localStoreRefusal = message;
5021
5158
  }
@@ -5030,8 +5167,8 @@ function localStoreRefusalMessage() {
5030
5167
  }
5031
5168
  function resolveDataDir() {
5032
5169
  const effective = getEffectiveDataRoot();
5033
- const oldDir = join9(getHomeDir(), ".hooks");
5034
- if (!existsSync8(effective) && existsSync8(oldDir)) {
5170
+ const oldDir = join10(getHomeDir(), ".hooks");
5171
+ if (!existsSync9(effective) && existsSync9(oldDir)) {
5035
5172
  mkdirSync2(effective, { recursive: true });
5036
5173
  cpSync(oldDir, effective, { recursive: true });
5037
5174
  }
@@ -5042,11 +5179,11 @@ function getDbPath() {
5042
5179
  if (explicitDb)
5043
5180
  return explicitDb;
5044
5181
  const dataDir3 = resolveDataDir();
5045
- return join9(dataDir3, "hooks.db");
5182
+ return join10(dataDir3, "hooks.db");
5046
5183
  }
5047
5184
  function ensureDir(dbPath) {
5048
5185
  const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
5049
- if (dir && !existsSync8(dir)) {
5186
+ if (dir && !existsSync9(dir)) {
5050
5187
  mkdirSync2(dir, { recursive: true });
5051
5188
  }
5052
5189
  }
@@ -5056,7 +5193,7 @@ function getDb() {
5056
5193
  if (instance)
5057
5194
  return instance;
5058
5195
  const dbPath = getDbPath();
5059
- const isNew = dbPath === ":memory:" || !existsSync8(dbPath);
5196
+ const isNew = dbPath === ":memory:" || !existsSync9(dbPath);
5060
5197
  ensureDir(dbPath);
5061
5198
  instance = new Database(dbPath);
5062
5199
  instance.exec("PRAGMA busy_timeout=5000");
@@ -5118,12 +5255,12 @@ __export(exports_store, {
5118
5255
  checkScriptHash: () => checkScriptHash,
5119
5256
  LockFileError: () => LockFileError
5120
5257
  });
5121
- import { createHash } from "crypto";
5122
- import { existsSync as existsSync9, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
5258
+ import { createHash as createHash2 } from "crypto";
5259
+ import { existsSync as existsSync10, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync } from "fs";
5123
5260
  import { readFile as readFile2 } from "fs/promises";
5124
5261
  import { randomBytes } from "crypto";
5125
5262
  function sha256Of(input) {
5126
- return createHash("sha256").update(input).digest("hex");
5263
+ return createHash2("sha256").update(input).digest("hex");
5127
5264
  }
5128
5265
  async function sha256File(path) {
5129
5266
  const buf = await readFile2(path);
@@ -5184,11 +5321,11 @@ function assertLockShape(parsed, path) {
5184
5321
  }
5185
5322
  function readLock() {
5186
5323
  const path = getLockPath();
5187
- if (!existsSync9(path))
5324
+ if (!existsSync10(path))
5188
5325
  return { hooks: {} };
5189
5326
  let raw;
5190
5327
  try {
5191
- raw = readFileSync3(path, "utf-8");
5328
+ raw = readFileSync4(path, "utf-8");
5192
5329
  } catch {
5193
5330
  throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
5194
5331
  }
@@ -5291,7 +5428,7 @@ async function verifyScriptHash(name, scriptPath) {
5291
5428
  return checkScriptHash(name, await sha256File(scriptPath));
5292
5429
  }
5293
5430
  function retrustHook(name, scriptPath, version, source) {
5294
- const actual = sha256Of(readFileSync3(scriptPath));
5431
+ const actual = sha256Of(readFileSync4(scriptPath));
5295
5432
  const db = storeDb();
5296
5433
  if (db) {
5297
5434
  upsertHookRecord(db, {
@@ -5318,15 +5455,15 @@ var init_store = __esm(() => {
5318
5455
  });
5319
5456
 
5320
5457
  // src/lib/codex-settings.ts
5321
- import { closeSync, existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
5322
- import { dirname as dirname3 } from "path";
5458
+ import { closeSync as closeSync2, existsSync as existsSync11, lstatSync as lstatSync2, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
5459
+ import { dirname as dirname4 } from "path";
5323
5460
  import { randomUUID as randomUUID3 } from "crypto";
5324
5461
  function current(path) {
5325
5462
  try {
5326
- const stat = lstatSync(path);
5463
+ const stat = lstatSync2(path);
5327
5464
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
5328
5465
  throw new Error("invalid_file");
5329
- return readFileSync4(path, "utf8");
5466
+ return readFileSync5(path, "utf8");
5330
5467
  } catch (error) {
5331
5468
  if (error.code === "ENOENT")
5332
5469
  return null;
@@ -5358,11 +5495,11 @@ function writeCodexSettings(path, settings) {
5358
5495
  const snapshot = snapshots.get(settings);
5359
5496
  if (!snapshot || snapshot.path !== path)
5360
5497
  throw new Error("Codex settings require a complete current read before writing.");
5361
- mkdirSync4(dirname3(path), { recursive: true });
5498
+ mkdirSync4(dirname4(path), { recursive: true });
5362
5499
  const lock = `${path}.hooks-write.lock`;
5363
5500
  let fd;
5364
5501
  try {
5365
- fd = openSync(lock, "wx", 384);
5502
+ fd = openSync2(lock, "wx", 384);
5366
5503
  } catch {
5367
5504
  throw new Error("Codex settings are being edited; retry after the active writer finishes.");
5368
5505
  }
@@ -5378,9 +5515,9 @@ function writeCodexSettings(path, settings) {
5378
5515
  renameSync2(temp, path);
5379
5516
  snapshots.set(settings, { path, raw: next });
5380
5517
  } finally {
5381
- if (existsSync10(temp))
5518
+ if (existsSync11(temp))
5382
5519
  unlinkSync(temp);
5383
- closeSync(fd);
5520
+ closeSync2(fd);
5384
5521
  unlinkSync(lock);
5385
5522
  }
5386
5523
  }
@@ -5409,9 +5546,9 @@ __export(exports_installer, {
5409
5546
  detectRewriteConflict: () => detectRewriteConflict,
5410
5547
  buildCodewithTomlFragment: () => buildCodewithTomlFragment
5411
5548
  });
5412
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
5413
- import { join as join10, dirname as dirname4 } from "path";
5414
- import { homedir as homedir4 } from "os";
5549
+ import { existsSync as existsSync12, mkdirSync as mkdirSync5, readFileSync as readFileSync6, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
5550
+ import { join as join11, dirname as dirname5 } from "path";
5551
+ import { homedir as homedir5 } from "os";
5415
5552
  import { fileURLToPath as fileURLToPath2 } from "url";
5416
5553
  function normalizeHookName(name) {
5417
5554
  return name.startsWith("hook-") ? name : `hook-${name}`;
@@ -5424,8 +5561,7 @@ function removeHookEntriesByName(entries, hookName) {
5424
5561
  if (!Array.isArray(entry.hooks))
5425
5562
  return [entry];
5426
5563
  const kept = entry.hooks.filter((h) => {
5427
- const match = h.command?.match(/^hooks run ([\w-]+)/);
5428
- return !(match && match[1] === hookName);
5564
+ return registrationName(h.command) !== hookName;
5429
5565
  });
5430
5566
  return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
5431
5567
  });
@@ -5459,29 +5595,29 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
5459
5595
  return globalOverride;
5460
5596
  const dir = getTargetSettingsDir(target);
5461
5597
  if (target === "codex")
5462
- return join10(scope === "project" ? process.cwd() : homedir4(), dir, "hooks.json");
5598
+ return join11(scope === "project" ? process.cwd() : homedir5(), dir, "hooks.json");
5463
5599
  if (scope === "project") {
5464
- return target === "codewith" ? join10(process.cwd(), dir, "config.toml") : join10(process.cwd(), dir, "settings.json");
5600
+ return target === "codewith" ? join11(process.cwd(), dir, "config.toml") : join11(process.cwd(), dir, "settings.json");
5465
5601
  }
5466
- return target === "codewith" ? join10(homedir4(), dir, "config.toml") : join10(homedir4(), dir, "settings.json");
5602
+ return target === "codewith" ? join11(homedir5(), dir, "config.toml") : join11(homedir5(), dir, "settings.json");
5467
5603
  }
5468
5604
  function getHookPath(name) {
5469
5605
  const shortName = shortHookName(name);
5470
5606
  const resolved = resolveHookDir(shortName);
5471
5607
  if (resolved)
5472
5608
  return resolved;
5473
- return join10(HOOKS_DIR, normalizeHookName(shortName));
5609
+ return join11(HOOKS_DIR, normalizeHookName(shortName));
5474
5610
  }
5475
5611
  function hookExists(name) {
5476
- return existsSync11(getHookPath(name));
5612
+ return existsSync12(getHookPath(name));
5477
5613
  }
5478
5614
  function readSettings(scope = "global", target = "claude") {
5479
5615
  const path = getSettingsPath(scope, target);
5480
5616
  if (target === "codex")
5481
5617
  return readCodexSettings(path);
5482
5618
  try {
5483
- if (existsSync11(path)) {
5484
- return JSON.parse(readFileSync5(path, "utf-8"));
5619
+ if (existsSync12(path)) {
5620
+ return JSON.parse(readFileSync6(path, "utf-8"));
5485
5621
  }
5486
5622
  } catch (error) {
5487
5623
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -5492,8 +5628,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
5492
5628
  const path = getSettingsPath(scope, target);
5493
5629
  if (target === "codex")
5494
5630
  return writeCodexSettings(path, settings);
5495
- const dir = dirname4(path);
5496
- if (!existsSync11(dir)) {
5631
+ const dir = dirname5(path);
5632
+ if (!existsSync12(dir)) {
5497
5633
  mkdirSync5(dir, { recursive: true });
5498
5634
  }
5499
5635
  writeFileSync4(path, JSON.stringify(settings, null, 2) + `
@@ -5521,6 +5657,9 @@ function codewithTimeout(name) {
5521
5657
  switch (name) {
5522
5658
  case "mementos-context":
5523
5659
  return 12;
5660
+ case "trash-guard":
5661
+ case "workspace-repos-guard":
5662
+ return 10;
5524
5663
  case "knowledge-context":
5525
5664
  return 6;
5526
5665
  case "session-start":
@@ -5530,7 +5669,6 @@ function codewithTimeout(name) {
5530
5669
  case "prompt-guard":
5531
5670
  return 3;
5532
5671
  case "worktree-guard":
5533
- case "trash-guard":
5534
5672
  case "stop-sync":
5535
5673
  return 5;
5536
5674
  default:
@@ -5559,11 +5697,13 @@ function codewithStatusMessage(name) {
5559
5697
  }
5560
5698
  function buildCodewithTomlFragment(name, profile, mementos) {
5561
5699
  const shortName = shortHookName(name);
5562
- const meta = resolveHookMeta(shortName);
5700
+ if (isNativeSafetyName(shortName) && profile)
5701
+ throw new Error("Bundled native safety guards do not select registry profiles");
5702
+ const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
5563
5703
  if (!meta)
5564
5704
  throw new Error(`Hook '${shortName}' not found`);
5565
5705
  validateMementosRegistration(shortName, mementos);
5566
- const command = (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
5706
+ const command = isNativeSafetyName(shortName) ? installedNativeSafetyCommand(shortName) : (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
5567
5707
  const matcher = codewithMatcher(meta.matcher);
5568
5708
  const fragments = [];
5569
5709
  for (const event of getHookEvents(meta)) {
@@ -5588,17 +5728,17 @@ function buildCodewithTomlFragment(name, profile, mementos) {
5588
5728
  function readCodewithConfig(scope, configPath) {
5589
5729
  const path = getSettingsPath(scope, "codewith", configPath);
5590
5730
  try {
5591
- return existsSync11(path) ? readFileSync5(path, "utf-8") : "";
5731
+ return existsSync12(path) ? readFileSync6(path, "utf-8") : "";
5592
5732
  } catch {
5593
5733
  return "";
5594
5734
  }
5595
5735
  }
5596
5736
  function appendCodewithFragment(fragment, scope, configPath) {
5597
5737
  const path = getSettingsPath(scope, "codewith", configPath);
5598
- const dir = dirname4(path);
5599
- if (!existsSync11(dir))
5738
+ const dir = dirname5(path);
5739
+ if (!existsSync12(dir))
5600
5740
  mkdirSync5(dir, { recursive: true });
5601
- const existing = existsSync11(path) ? readFileSync5(path, "utf-8") : "";
5741
+ const existing = existsSync12(path) ? readFileSync6(path, "utf-8") : "";
5602
5742
  const sep2 = existing.trim() ? `
5603
5743
 
5604
5744
  ` : "";
@@ -5606,7 +5746,7 @@ function appendCodewithFragment(fragment, scope, configPath) {
5606
5746
  return path;
5607
5747
  }
5608
5748
  function detectConflict(name, scope, target) {
5609
- const meta = resolveHookMeta(name);
5749
+ const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
5610
5750
  if (!meta || !meta.matcher)
5611
5751
  return;
5612
5752
  const events = new Set(getHookEvents(meta));
@@ -5626,7 +5766,7 @@ function detectConflict(name, scope, target) {
5626
5766
  return;
5627
5767
  }
5628
5768
  function detectRewriteConflict(name, scope, target, lookup = getHook) {
5629
- const meta = resolveHookMeta(name);
5769
+ const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
5630
5770
  if (!meta?.matcher || !meta.rewritesInput)
5631
5771
  return;
5632
5772
  if (!getHookEvents(meta).includes("PreToolUse"))
@@ -5647,7 +5787,10 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
5647
5787
  }
5648
5788
  function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
5649
5789
  const shortName = shortHookName(name);
5650
- if (!hookExists(shortName)) {
5790
+ if (isNativeSafetyName(shortName) && (profile || target === "gemini" || target === "codewith" && codewithMode === "write")) {
5791
+ return { hook: shortName, success: false, error: "Bundled native safety guards support Claude/Codex and managed Codewith fragments, without registry profiles.", target };
5792
+ }
5793
+ if (!isNativeSafetyName(shortName) && !hookExists(shortName)) {
5651
5794
  return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
5652
5795
  }
5653
5796
  if (target === "codewith") {
@@ -5739,7 +5882,7 @@ function installHook(name, options = {}) {
5739
5882
  }
5740
5883
  if (target === "all") {
5741
5884
  const shortName = shortHookName(name);
5742
- const meta = resolveHookMeta(shortName);
5885
+ const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
5743
5886
  if (meta) {
5744
5887
  const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
5745
5888
  if (unsupportedTargets.length > 0) {
@@ -5772,7 +5915,7 @@ function installHook(name, options = {}) {
5772
5915
  return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
5773
5916
  }
5774
5917
  function registerHook(name, scope = "global", target = "claude", profile, mementos) {
5775
- const meta = resolveHookMeta(name);
5918
+ const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
5776
5919
  if (!meta)
5777
5920
  return;
5778
5921
  const eventKeys = getHookEvents(meta).map((event) => {
@@ -5789,8 +5932,17 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
5789
5932
  const settings = readSettings(scope, target);
5790
5933
  if (!settings.hooks)
5791
5934
  settings.hooks = {};
5935
+ if (isNativeSafetyName(name)) {
5936
+ for (const groups of Object.values(settings.hooks))
5937
+ for (const group of groups)
5938
+ for (const hook of group.hooks ?? []) {
5939
+ if (typeof hook.command === "string" && /(?:^|\s)hasna-native-safety-v\d+(?:\s|$)/.test(hook.command) && !registrationName(hook.command)) {
5940
+ throw new Error("Unknown managed safety definition; reconcile its exact version before replacing it.");
5941
+ }
5942
+ }
5943
+ }
5792
5944
  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;
5793
- const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
5945
+ const hookCommand = isNativeSafetyName(name) ? installedNativeSafetyCommand(name) : (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
5794
5946
  if (target === "codex") {
5795
5947
  const positions = [];
5796
5948
  for (const [event, groups] of Object.entries(settings.hooks)) {
@@ -5798,9 +5950,11 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
5798
5950
  for (let index = 0;index < entry.hooks.length; index++) {
5799
5951
  const command = entry.hooks[index]?.command;
5800
5952
  const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
5801
- if (match && (match[1] ?? match[2]) === name) {
5953
+ if (match && (match[1] ?? match[2]) === name || registrationName(command) === name) {
5802
5954
  if (registrationName(command) !== name)
5803
5955
  throw new Error("Unrecognized Codex registration; reconcile it before updating this hook.");
5956
+ if (isNativeSafetyName(name) && entry.hooks[index].async !== undefined && entry.hooks[index].async !== false)
5957
+ throw new Error("Asynchronous safety hooks cannot block; reconcile this registration before updating it.");
5804
5958
  positions.push({ event, entry, index });
5805
5959
  }
5806
5960
  }
@@ -5864,12 +6018,20 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
5864
6018
  if (target === "codewith") {
5865
6019
  const config = readCodewithConfig(scope);
5866
6020
  const registered2 = [];
5867
- const re = /^\s*command\s*=\s*["']([^"'\n]+)["']\s*$/gm;
5868
- let match;
5869
- while (match = re.exec(config)) {
5870
- const name = registrationName(match[1]);
5871
- if (name)
5872
- registered2.push(name);
6021
+ try {
6022
+ const parsed = Bun.TOML.parse(config);
6023
+ for (const groups of Object.values(parsed.hooks ?? {})) {
6024
+ if (!Array.isArray(groups))
6025
+ continue;
6026
+ for (const group of groups)
6027
+ for (const hook of group.hooks ?? []) {
6028
+ const name = registrationName(hook.command);
6029
+ if (name)
6030
+ registered2.push(name);
6031
+ }
6032
+ }
6033
+ } catch {
6034
+ return [];
5873
6035
  }
5874
6036
  return [...new Set(registered2)];
5875
6037
  }
@@ -5914,9 +6076,9 @@ function removeHookForTarget(name, scope, target) {
5914
6076
  }
5915
6077
  function readSettingsAt(path) {
5916
6078
  try {
5917
- if (!existsSync11(path))
6079
+ if (!existsSync12(path))
5918
6080
  return;
5919
- const raw = readFileSync5(path, "utf-8");
6081
+ const raw = readFileSync6(path, "utf-8");
5920
6082
  return { raw, settings: JSON.parse(raw) };
5921
6083
  } catch {
5922
6084
  return;
@@ -5950,7 +6112,7 @@ function pruneStaleRegistrations(scope = "global") {
5950
6112
  staleCommandsByEvent.set(finding.event, commands);
5951
6113
  }
5952
6114
  const settings = target === "codex" ? readCodexSettings(path) : parsed.settings;
5953
- if (target === "codex" && readFileSync5(path, "utf8") !== parsed.raw)
6115
+ if (target === "codex" && readFileSync6(path, "utf8") !== parsed.raw)
5954
6116
  throw new Error("Codex settings changed before pruning; retry.");
5955
6117
  for (const [eventKey, commands] of staleCommandsByEvent) {
5956
6118
  const entries = settings.hooks?.[eventKey];
@@ -6038,10 +6200,10 @@ function codewithHasHookEntryInText(configText, name) {
6038
6200
  }
6039
6201
  function codewithHasHookEntry(name, scope = "global") {
6040
6202
  const path = getSettingsPath(scope, "codewith");
6041
- if (!existsSync11(path))
6203
+ if (!existsSync12(path))
6042
6204
  return false;
6043
6205
  try {
6044
- return codewithHasHookEntryInText(readFileSync5(path, "utf-8"), name);
6206
+ return codewithHasHookEntryInText(readFileSync6(path, "utf-8"), name);
6045
6207
  } catch {
6046
6208
  return false;
6047
6209
  }
@@ -6050,12 +6212,12 @@ function removeStoreDirFailClosed(dir) {
6050
6212
  const entries = readdirSync3(dir, { withFileTypes: true });
6051
6213
  for (const entry of entries) {
6052
6214
  if (entry.isDirectory()) {
6053
- removeStoreDirFailClosed(join10(dir, entry.name));
6215
+ removeStoreDirFailClosed(join11(dir, entry.name));
6054
6216
  }
6055
6217
  }
6056
6218
  for (const entry of entries) {
6057
6219
  if (!entry.isDirectory()) {
6058
- unlinkSync2(join10(dir, entry.name));
6220
+ unlinkSync2(join11(dir, entry.name));
6059
6221
  }
6060
6222
  }
6061
6223
  rmdirSync(dir);
@@ -6086,8 +6248,8 @@ function uninstallHook(name, scope = "global", target = "claude") {
6086
6248
  const registrationsRemaining = [];
6087
6249
  if (target === "codewith" || target === "all") {
6088
6250
  const codewithPath = getSettingsPath(scope, "codewith");
6089
- if (existsSync11(codewithPath)) {
6090
- const before = readFileSync5(codewithPath, "utf-8");
6251
+ if (existsSync12(codewithPath)) {
6252
+ const before = readFileSync6(codewithPath, "utf-8");
6091
6253
  const after = removeCodewithHookEntry(before, shortName);
6092
6254
  if (after.removed) {
6093
6255
  writeFileSync4(codewithPath, after.text, "utf-8");
@@ -6134,8 +6296,9 @@ var init_installer = __esm(() => {
6134
6296
  init_registration();
6135
6297
  init_store();
6136
6298
  init_codex_settings();
6137
- __dirname2 = dirname4(fileURLToPath2(import.meta.url));
6138
- HOOKS_DIR = existsSync11(join10(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join10(__dirname2, "..", "..", "hooks") : join10(__dirname2, "..", "hooks");
6299
+ init_native_safety();
6300
+ __dirname2 = dirname5(fileURLToPath2(import.meta.url));
6301
+ HOOKS_DIR = existsSync12(join11(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join11(__dirname2, "..", "..", "hooks") : join11(__dirname2, "..", "hooks");
6139
6302
  EVENT_MAP = {
6140
6303
  claude: {
6141
6304
  PreToolUse: "PreToolUse",
@@ -6248,12 +6411,12 @@ var init_redact = __esm(() => {
6248
6411
  });
6249
6412
 
6250
6413
  // src/lib/profiles.ts
6251
- import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
6252
- import { join as join11 } from "path";
6414
+ import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
6415
+ import { join as join12 } from "path";
6253
6416
  function getProfilesDir() {
6254
- const newDir = join11(getEffectiveDataRoot(), "profiles");
6255
- const oldDir = join11(getHomeDir(), ".hooks", "profiles");
6256
- if (!existsSync12(newDir) && existsSync12(oldDir)) {
6417
+ const newDir = join12(getEffectiveDataRoot(), "profiles");
6418
+ const oldDir = join12(getHomeDir(), ".hooks", "profiles");
6419
+ if (!existsSync13(newDir) && existsSync13(oldDir)) {
6257
6420
  mkdirSync6(newDir, { recursive: true });
6258
6421
  cpSync2(oldDir, newDir, { recursive: true });
6259
6422
  }
@@ -6261,12 +6424,12 @@ function getProfilesDir() {
6261
6424
  }
6262
6425
  function ensureProfilesDir() {
6263
6426
  const dir = getProfilesDir();
6264
- if (!existsSync12(dir)) {
6427
+ if (!existsSync13(dir)) {
6265
6428
  mkdirSync6(dir, { recursive: true });
6266
6429
  }
6267
6430
  }
6268
6431
  function profilePath(id) {
6269
- return join11(getProfilesDir(), `${id}.json`);
6432
+ return join12(getProfilesDir(), `${id}.json`);
6270
6433
  }
6271
6434
  function shortUuid() {
6272
6435
  return crypto.randomUUID().slice(0, 8);
@@ -6292,23 +6455,23 @@ function createProfile(input) {
6292
6455
  function getProfile(id) {
6293
6456
  const path = profilePath(id);
6294
6457
  try {
6295
- if (!existsSync12(path))
6458
+ if (!existsSync13(path))
6296
6459
  return null;
6297
- return JSON.parse(readFileSync6(path, "utf-8"));
6460
+ return JSON.parse(readFileSync7(path, "utf-8"));
6298
6461
  } catch {
6299
6462
  return null;
6300
6463
  }
6301
6464
  }
6302
6465
  function listProfiles() {
6303
6466
  const dir = getProfilesDir();
6304
- if (!existsSync12(dir))
6467
+ if (!existsSync13(dir))
6305
6468
  return [];
6306
6469
  try {
6307
6470
  const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
6308
6471
  const profiles = [];
6309
6472
  for (const file of files) {
6310
6473
  try {
6311
- const content = readFileSync6(join11(dir, file), "utf-8");
6474
+ const content = readFileSync7(join12(dir, file), "utf-8");
6312
6475
  profiles.push(JSON.parse(content));
6313
6476
  } catch {}
6314
6477
  }
@@ -6338,7 +6501,7 @@ function importProfiles(profiles) {
6338
6501
  continue;
6339
6502
  }
6340
6503
  const path = profilePath(profile.agent_id);
6341
- if (existsSync12(path)) {
6504
+ if (existsSync13(path)) {
6342
6505
  skipped++;
6343
6506
  continue;
6344
6507
  }
@@ -6352,14 +6515,19 @@ var init_profiles = __esm(() => {
6352
6515
  init_app_home();
6353
6516
  });
6354
6517
 
6355
- // ../../node_modules/.bun/@hasna+contracts@1.0.2+e0db24ae03127387/node_modules/@hasna/contracts/dist/client/transport.js
6518
+ // ../contracts/dist/client/transport.js
6356
6519
  import { isIP as isIP2 } from "net";
6357
6520
  import { spawnSync } from "child_process";
6358
- import { closeSync as closeSync2, fstatSync, openSync as openSync2, readFileSync as readFileSync7 } from "fs";
6359
- import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
6360
- import { createRequire as createRequire2 } from "module";
6521
+ import { closeSync as closeSync22, fstatSync as fstatSync2, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
6522
+ import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
6361
6523
  import { hostname as osHostname } from "os";
6362
- import { isAbsolute as isAbsolute2, join as join12 } from "path";
6524
+ import { isAbsolute as isAbsolute3, join as join22 } from "path";
6525
+ import { createRequire as createRequire2 } from "module";
6526
+ import { fileURLToPath as fileURLToPath22 } from "url";
6527
+ import { closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3, readFileSync as readFileSync8, realpathSync as realpathSync3, statSync } from "fs";
6528
+ import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
6529
+ import { basename as basename2, dirname as dirname6, join as join13, relative, sep as sep2 } from "path";
6530
+ import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
6363
6531
  function envToken(name) {
6364
6532
  return name.toUpperCase().replace(/-/g, "_");
6365
6533
  }
@@ -6376,27 +6544,133 @@ function credentialOverrideEnvKey(name) {
6376
6544
  function credentialPointerEnvKey(name) {
6377
6545
  return `HASNA_${envToken(name)}_API_KEY_REF`;
6378
6546
  }
6547
+ function e(e2, n, r) {
6548
+ throw new Error(r ? `No known conditions for "${n}" specifier in "${e2}" package` : `Missing "${n}" specifier in "${e2}" package`);
6549
+ }
6550
+ function n(n2, i, o, f) {
6551
+ let s, u, l = r(n2, o), c = function(e2) {
6552
+ let n3 = new Set(["default", ...e2.conditions || []]);
6553
+ return e2.unsafe || n3.add(e2.require ? "require" : "import"), e2.unsafe || n3.add(e2.browser ? "browser" : "node"), n3;
6554
+ }(f || {}), a = i[l];
6555
+ if (a === undefined) {
6556
+ let e2, n3, r, t;
6557
+ for (t in i)
6558
+ n3 && t.length < n3.length || (t[t.length - 1] === "/" && l.startsWith(t) ? (u = l.substring(t.length), n3 = t) : t.length > 1 && (r = t.indexOf("*", 1), ~r && (e2 = RegExp("^" + t.substring(0, r) + "(.*)" + t.substring(1 + r) + "$").exec(l), e2 && e2[1] && (u = e2[1], n3 = t))));
6559
+ a = i[n3];
6560
+ }
6561
+ return a || e(n2, l), s = t(a, c), s || e(n2, l, 1), u && function(e2, n3) {
6562
+ let r, t = 0, i2 = e2.length, o2 = /[*]/g, f2 = /[/]$/;
6563
+ for (;t < i2; t++)
6564
+ e2[t] = o2.test(r = e2[t]) ? r.replace(o2, n3) : f2.test(r) ? r + n3 : r;
6565
+ }(s, u), s;
6566
+ }
6567
+ function r(e2, n2, r2) {
6568
+ if (e2 === n2 || n2 === ".")
6569
+ return ".";
6570
+ let t = e2 + "/", i = t.length, o = n2.slice(0, i) === t, f = o ? n2.slice(i) : n2;
6571
+ return f[0] === "#" ? f : o || !r2 ? f.slice(0, 2) === "./" ? f : "./" + f : f;
6572
+ }
6573
+ function t(e2, n2, r2) {
6574
+ if (e2) {
6575
+ if (typeof e2 == "string")
6576
+ return r2 && r2.add(e2), [e2];
6577
+ let i, o;
6578
+ if (Array.isArray(e2)) {
6579
+ for (o = r2 || new Set, i = 0;i < e2.length; i++)
6580
+ t(e2[i], n2, o);
6581
+ if (!r2 && o.size)
6582
+ return [...o];
6583
+ } else
6584
+ for (i in e2)
6585
+ if (n2.has(i))
6586
+ return t(e2[i], n2, r2);
6587
+ }
6588
+ }
6589
+ function o(e2, r2, t2) {
6590
+ let i, o2 = e2.exports;
6591
+ if (o2) {
6592
+ if (typeof o2 == "string")
6593
+ o2 = { ".": o2 };
6594
+ else
6595
+ for (i in o2) {
6596
+ i[0] !== "." && (o2 = { ".": o2 });
6597
+ break;
6598
+ }
6599
+ return n(e2.name, o2, r2 || ".", t2);
6600
+ }
6601
+ }
6602
+ function entryPoint(directory) {
6603
+ const root = realpathSync3(directory);
6604
+ const fd = openSync3(join13(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
6605
+ let pkg;
6606
+ try {
6607
+ const stat = fstatSync3(fd);
6608
+ if (!stat.isFile() || stat.size > MAX_PACKAGE_BYTES2)
6609
+ throw new Error("Invalid Secrets package metadata");
6610
+ pkg = JSON.parse(readFileSync8(fd, "utf8"));
6611
+ } finally {
6612
+ closeSync3(fd);
6613
+ }
6614
+ if (!pkg || typeof pkg !== "object" || !("name" in pkg) || pkg.name !== PACKAGE) {
6615
+ throw new Error("Invalid Secrets package identity");
6616
+ }
6617
+ const target = o(pkg, ".")?.[0];
6618
+ if (typeof target !== "string" || !target.startsWith("./"))
6619
+ throw new Error("Missing Secrets import export");
6620
+ const parts = target.slice(2).split("/");
6621
+ if (/[\\%?#\0]/.test(target) || parts.some((part) => !part || part === "." || part === ".." || part === "node_modules")) {
6622
+ throw new Error("Invalid Secrets import export");
6623
+ }
6624
+ const file = realpathSync3(join13(root, ...parts));
6625
+ const within = relative(root, file);
6626
+ if (!within || within === ".." || within.startsWith(`..${sep2}`) || !statSync(file).isFile()) {
6627
+ throw new Error("Secrets import export escapes its package or is not a file");
6628
+ }
6629
+ return pathToFileURL(file).href;
6630
+ }
6631
+ function resolveInstalledSecrets(parentUrl) {
6632
+ let directory = dirname6(fileURLToPath3(parentUrl));
6633
+ for (;; ) {
6634
+ if (basename2(directory) !== "node_modules") {
6635
+ const candidate = join13(directory, "node_modules", PACKAGE);
6636
+ let absent = false;
6637
+ try {
6638
+ lstatSync3(candidate);
6639
+ } catch (error) {
6640
+ if (error.code !== "ENOENT")
6641
+ throw error;
6642
+ absent = true;
6643
+ }
6644
+ if (!absent)
6645
+ return entryPoint(candidate);
6646
+ }
6647
+ const parent = dirname6(directory);
6648
+ if (parent === directory)
6649
+ throw new Error("Secrets SDK is not installed for this consumer");
6650
+ directory = parent;
6651
+ }
6652
+ }
6379
6653
  function homeDir(env) {
6380
6654
  const home = env.HOME?.trim();
6381
6655
  return home ? home : null;
6382
6656
  }
6383
6657
  function absoluteOverride(env, key) {
6384
6658
  const value = env[key]?.trim();
6385
- return value && isAbsolute2(value) ? value : null;
6659
+ return value && isAbsolute3(value) ? value : null;
6386
6660
  }
6387
6661
  function hasnaHomeDir(env) {
6388
6662
  const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
6389
6663
  if (override)
6390
6664
  return override;
6391
6665
  const home = homeDir(env);
6392
- return home ? join12(home, HASNA_HOME_DIR) : null;
6666
+ return home ? join22(home, HASNA_HOME_DIR) : null;
6393
6667
  }
6394
6668
  function appConfigDir(name, env) {
6395
6669
  const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
6396
6670
  if (configRoot)
6397
- return join12(configRoot, name);
6671
+ return join22(configRoot, name);
6398
6672
  const root = hasnaHomeDir(env);
6399
- return root ? join12(root, name, CONFIG_SUBDIR) : null;
6673
+ return root ? join22(root, name, CONFIG_SUBDIR) : null;
6400
6674
  }
6401
6675
  function credentialDiskSourceList(name, env, profile = null) {
6402
6676
  if (!SAFE_APP_SLUG.test(name))
@@ -6405,7 +6679,7 @@ function credentialDiskSourceList(name, env, profile = null) {
6405
6679
  if (!directory)
6406
6680
  return [];
6407
6681
  const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
6408
- return [{ path: join12(directory, file), tier: "disk" }];
6682
+ return [{ path: join22(directory, file), tier: "disk" }];
6409
6683
  }
6410
6684
  function credentialDiskSources(name, env) {
6411
6685
  return credentialDiskSourceList(name, env, null).map((s) => s.path);
@@ -6428,9 +6702,9 @@ function parseEnvFile(text) {
6428
6702
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
6429
6703
  continue;
6430
6704
  let value = withoutExport.slice(equals + 1).trim();
6431
- const quote = value[0];
6432
- if (quote === '"' || quote === "'") {
6433
- if (value.length < 2 || !value.endsWith(quote)) {
6705
+ const quote2 = value[0];
6706
+ if (quote2 === '"' || quote2 === "'") {
6707
+ if (value.length < 2 || !value.endsWith(quote2)) {
6434
6708
  unusable.add(key);
6435
6709
  continue;
6436
6710
  }
@@ -6459,7 +6733,7 @@ function readAppConfigFile(path) {
6459
6733
  };
6460
6734
  let fd = -1;
6461
6735
  try {
6462
- fd = openSync2(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
6736
+ fd = openSync22(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
6463
6737
  } catch (error) {
6464
6738
  const code = error.code;
6465
6739
  if (code === "ENOENT" || code === "ENOTDIR")
@@ -6469,7 +6743,7 @@ function readAppConfigFile(path) {
6469
6743
  unsafe(`the path could not be opened (${code ?? "unknown error"})`);
6470
6744
  }
6471
6745
  try {
6472
- const before = fstatSync(fd);
6746
+ const before = fstatSync2(fd);
6473
6747
  if (!before.isFile())
6474
6748
  unsafe("the path is not a regular file");
6475
6749
  if (!configFileModeAllowed(before.mode)) {
@@ -6480,31 +6754,39 @@ function readAppConfigFile(path) {
6480
6754
  unsafe("the file is not owned by the current user");
6481
6755
  if (before.size > MAX_CREDENTIAL_FILE_BYTES2)
6482
6756
  unsafe("the file exceeds the size limit");
6483
- const bytes = readFileSync7(fd);
6484
- const after = fstatSync(fd);
6757
+ const bytes = readFileSync22(fd);
6758
+ const after = fstatSync2(fd);
6485
6759
  if (!configFileReadsCoherent(before, after)) {
6486
6760
  unsafe("the file changed while being read");
6487
6761
  }
6488
6762
  return parseEnvFile(bytes.toString("utf8"));
6489
6763
  } finally {
6490
6764
  if (fd !== -1)
6491
- closeSync2(fd);
6765
+ closeSync22(fd);
6492
6766
  }
6493
6767
  }
6494
- function readCredentialFile(path, apiKeyKeys) {
6768
+ function readCredentialFile(path, apiKeyKeys, pointerKey) {
6495
6769
  const parsed = readAppConfigFile(path);
6496
6770
  if (!parsed)
6497
6771
  return null;
6498
- for (const key of apiKeyKeys) {
6772
+ for (const key of [...apiKeyKeys, pointerKey]) {
6499
6773
  if (parsed.unusable.has(key)) {
6500
6774
  throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
6501
6775
  }
6502
6776
  }
6503
6777
  const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
6778
+ const pointer = parsed.values.get(pointerKey)?.trim();
6779
+ if (pointer !== undefined) {
6780
+ if (!VAULT_POINTER_SHAPE.test(pointer))
6781
+ throw new CredentialFileUnsafeError(path, `${pointerKey} must name a vault item`);
6782
+ if (values.length)
6783
+ throw new CredentialFileUnsafeError(path, "a credential file cannot select both a literal key and a vault reference");
6784
+ return { apiKey: "", pointerVaultKey: pointer };
6785
+ }
6504
6786
  if (new Set(values).size > 1) {
6505
6787
  throw new CredentialFileUnsafeError(path, "credential aliases disagree");
6506
6788
  }
6507
- return values[0] ?? null;
6789
+ return values[0] === undefined ? null : { apiKey: values[0] };
6508
6790
  }
6509
6791
  function appConfigDiskValue(name, env, keys) {
6510
6792
  const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
@@ -6576,6 +6858,18 @@ function sealCredential(fields) {
6576
6858
  });
6577
6859
  return Object.freeze(sealed);
6578
6860
  }
6861
+ function explicitCredential(appName, apiKey) {
6862
+ const source = "explicit apiKey option";
6863
+ assertUsableCredential(appName, source, apiKey);
6864
+ return sealCredential({
6865
+ apiKey,
6866
+ tier: "argument",
6867
+ source,
6868
+ deliberate: true,
6869
+ diskCandidates: [],
6870
+ warning: null
6871
+ });
6872
+ }
6579
6873
  function firstEnvValue(env, keys) {
6580
6874
  for (const key of keys) {
6581
6875
  if (!Object.prototype.hasOwnProperty.call(env, key))
@@ -6761,12 +7055,13 @@ function resolveCredential(name, env, options = {}) {
6761
7055
  }
6762
7056
  const paths = profileDiskSources(name, env, profile);
6763
7057
  for (const path of paths) {
6764
- const value = readCredentialFile(path, apiKeyKeys);
7058
+ const value = readCredentialFile(path, apiKeyKeys, pointerKeyName);
6765
7059
  if (value) {
6766
- assertUsableCredential(name, path, value);
7060
+ if (!value.pointerVaultKey)
7061
+ assertUsableCredential(name, path, value.apiKey);
6767
7062
  return sealCredential({
6768
- apiKey: value,
6769
- tier: "profile",
7063
+ ...value,
7064
+ tier: value.pointerVaultKey ? "pointer" : "profile",
6770
7065
  source: path,
6771
7066
  deliberate: true,
6772
7067
  diskCandidates: paths,
@@ -6774,7 +7069,7 @@ function resolveCredential(name, env, options = {}) {
6774
7069
  });
6775
7070
  }
6776
7071
  }
6777
- throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
7072
+ throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} or ${pointerKeyName} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
6778
7073
  }
6779
7074
  const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
6780
7075
  const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
@@ -6799,17 +7094,27 @@ function resolveCredential(name, env, options = {}) {
6799
7094
  });
6800
7095
  }
6801
7096
  const diskSourceList = credentialDiskSourceList(name, env, null);
6802
- const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
7097
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys, pointerKeyName) })).filter((hit) => hit.value !== null);
6803
7098
  if (diskHits.length > 0) {
6804
7099
  const winner = diskHits[0];
6805
- assertUsableCredential(name, winner.src.path, winner.value);
7100
+ if (winner.value.pointerVaultKey) {
7101
+ return sealCredential({
7102
+ ...winner.value,
7103
+ tier: "pointer",
7104
+ source: winner.src.path,
7105
+ deliberate: false,
7106
+ diskCandidates: diskPaths,
7107
+ warning: null
7108
+ });
7109
+ }
7110
+ assertUsableCredential(name, winner.src.path, winner.value.apiKey);
6806
7111
  const divergentSources = [
6807
- ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
6808
- ...envHit && envHit.value !== winner.value ? [envHit.key] : []
7112
+ ...diskHits.slice(1).filter((hit) => hit.value.apiKey !== winner.value.apiKey || hit.value.pointerVaultKey !== winner.value.pointerVaultKey).map((hit) => hit.src.path),
7113
+ ...envHit && envHit.value !== winner.value.apiKey ? [envHit.key] : []
6809
7114
  ];
6810
7115
  const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
6811
7116
  return sealCredential({
6812
- apiKey: winner.value,
7117
+ apiKey: winner.value.apiKey,
6813
7118
  tier: winner.src.tier,
6814
7119
  source: winner.src.path,
6815
7120
  deliberate: false,
@@ -6830,6 +7135,53 @@ function resolveCredential(name, env, options = {}) {
6830
7135
  }
6831
7136
  return null;
6832
7137
  }
7138
+ async function completePointerCredential(name, pointerResolution, env = process.env) {
7139
+ const secretsEnv = snapshotClientEnvironment("secrets", env);
7140
+ const vaultKey = pointerResolution.pointerVaultKey;
7141
+ const pointerEnvKey = pointerResolution.source;
7142
+ if (!vaultKey) {
7143
+ throw new CredentialResolutionError(name, `Pointer resolution from ${pointerEnvKey} carries no vault item key; this is a defect in the resolver.`, [pointerEnvKey]);
7144
+ }
7145
+ if (name === "secrets") {
7146
+ throw new CredentialResolutionError(name, "The Secrets bootstrap credential cannot reference the same hosted vault; configure an independent bootstrap provider.", [pointerEnvKey]);
7147
+ }
7148
+ let secretsSdk;
7149
+ try {
7150
+ const sdkUrl = resolveInstalledSecrets(import.meta.url);
7151
+ secretsSdk = requireSecretsSdk2(fileURLToPath22(sdkUrl));
7152
+ } catch {
7153
+ throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets SDK (@hasna/secrets) is not installed ` + `in this process. A vault pointer is TERMINAL: install @hasna/secrets to resolve it, or unset ${pointerEnvKey}.`, [pointerEnvKey]);
7154
+ }
7155
+ let client;
7156
+ try {
7157
+ const bootstrap = resolveCredential("secrets", secretsEnv);
7158
+ if (!bootstrap || bootstrap.tier === "pointer") {
7159
+ throw new CredentialResolutionError("secrets", "The Secrets vault requires an independent, non-reference bootstrap credential.", bootstrap ? [bootstrap.source] : []);
7160
+ }
7161
+ client = secretsSdk.createSecretsClientFromEnv(secretsEnv);
7162
+ } catch {
7163
+ throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the secrets client could not be configured from this ` + `environment (the secrets service URL and key env are missing or invalid). A vault pointer is TERMINAL and ` + `never falls through to a literal or disk credential.`, [pointerEnvKey]);
7164
+ }
7165
+ let secret;
7166
+ try {
7167
+ secret = await client.getSecret({ key: vaultKey });
7168
+ } catch {
7169
+ throw new CredentialResolutionError(name, `${pointerEnvKey} names vault item '${vaultKey}', but the vault could not be reached or the item is ` + `unavailable. A vault pointer is TERMINAL and never falls through to a literal or disk credential.`, [pointerEnvKey]);
7170
+ }
7171
+ const value = secret.value;
7172
+ if (!value) {
7173
+ throw new CredentialResolutionError(name, `${pointerEnvKey} resolved vault item '${vaultKey}', but it holds no value. A vault pointer is TERMINAL.`, [pointerEnvKey]);
7174
+ }
7175
+ assertUsableCredential(name, `${pointerEnvKey} -> vault:${vaultKey}`, value);
7176
+ return sealCredential({
7177
+ apiKey: value,
7178
+ tier: "pointer",
7179
+ source: `${pointerEnvKey} -> vault:${vaultKey}`,
7180
+ deliberate: pointerResolution.deliberate,
7181
+ diskCandidates: pointerResolution.diskCandidates,
7182
+ warning: null
7183
+ });
7184
+ }
6833
7185
  function defaultFleetGatewayBaseUrl(name) {
6834
7186
  return `${DEFAULT_FLEET_GATEWAY_ORIGIN}/${validateAppSlug(name)}`;
6835
7187
  }
@@ -7032,8 +7384,9 @@ function credentialDiskSourcesForMessage(name, env) {
7032
7384
  const paths = credentialDiskSources(name, env);
7033
7385
  return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
7034
7386
  }
7035
- var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk2, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2;
7387
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", PACKAGE = "@hasna/secrets", MAX_PACKAGE_BYTES2, CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2, requireSecretsSdk2, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2;
7036
7388
  var init_transport = __esm(() => {
7389
+ MAX_PACKAGE_BYTES2 = 1024 * 1024;
7037
7390
  CredentialResolutionError = class CredentialResolutionError extends Error {
7038
7391
  appName;
7039
7392
  attempted;
@@ -7061,7 +7414,6 @@ var init_transport = __esm(() => {
7061
7414
  INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
7062
7415
  CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
7063
7416
  AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
7064
- SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
7065
7417
  requireSecretsSdk2 = createRequire2(import.meta.url);
7066
7418
  ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
7067
7419
  DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -7150,7 +7502,9 @@ var exports_transport = {};
7150
7502
  __export(exports_transport, {
7151
7503
  rethrowHooksAuthorityFailure: () => rethrowHooksAuthorityFailure,
7152
7504
  resolveHooksTransport: () => resolveHooksTransport,
7505
+ resolveHooksServePublishKeyAsync: () => resolveHooksServePublishKeyAsync,
7153
7506
  resolveHooksServePublishKey: () => resolveHooksServePublishKey,
7507
+ resolveHooksRequestAuthority: () => resolveHooksRequestAuthority,
7154
7508
  hooksRegistryOrigin: () => hooksRegistryOrigin,
7155
7509
  announceHooksLocalMode: () => announceHooksLocalMode,
7156
7510
  __resetHooksLocalNotice: () => __resetHooksLocalNotice
@@ -7189,6 +7543,40 @@ function rethrowHooksAuthorityFailure(error) {
7189
7543
  }
7190
7544
  throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
7191
7545
  }
7546
+ function sameCredential(left, right) {
7547
+ return left === null || right === null ? left === right : left.apiKey === right.apiKey && left.pointerVaultKey === right.pointerVaultKey && left.source === right.source && left.tier === right.tier;
7548
+ }
7549
+ function changedBinding() {
7550
+ throw new Error("REMOTE_API_CREDENTIAL_INVALID: The configured Hooks authority or credential changed while preparing a request; no request was sent.");
7551
+ }
7552
+ async function resolveHooksRequestAuthority(authority) {
7553
+ if (authority.origin !== hooksRegistryOrigin(authority.v1BaseUrl))
7554
+ changedBinding();
7555
+ const binding = authorityBindings.get(authority);
7556
+ if (!binding)
7557
+ return { ...authority, apiKey: explicitCredential("hooks", authority.apiKey).apiKey };
7558
+ const snapshot = () => {
7559
+ const next = resolveHooksTransport(binding.env, binding.options);
7560
+ if (next.mode !== "remote" || !next.authority || next.authority.v1BaseUrl !== authority.v1BaseUrl)
7561
+ changedBinding();
7562
+ return { authority: next.authority, binding: authorityBindings.get(next.authority) };
7563
+ };
7564
+ const first = snapshot();
7565
+ const reviewed = snapshot();
7566
+ if (!sameCredential(first.binding.credential, reviewed.binding.credential))
7567
+ changedBinding();
7568
+ let credential = reviewed.binding.credential;
7569
+ try {
7570
+ if (credential.tier === "pointer")
7571
+ credential = await completePointerCredential("hooks", credential, binding.env);
7572
+ } catch (error) {
7573
+ rethrowHooksAuthorityFailure(error);
7574
+ }
7575
+ const final = snapshot();
7576
+ if (!sameCredential(reviewed.binding.credential, final.binding.credential))
7577
+ changedBinding();
7578
+ return { ...reviewed.authority, apiKey: credential.apiKey };
7579
+ }
7192
7580
  function resolveHooksTransport(env = process.env, options = {}) {
7193
7581
  if (selectsHooksLocalStore(env)) {
7194
7582
  announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
@@ -7202,7 +7590,7 @@ function resolveHooksTransport(env = process.env, options = {}) {
7202
7590
  try {
7203
7591
  credential = resolveCredential("hooks", resolverEnv, credentialOptions);
7204
7592
  resolution = resolveClientTransport("hooks", resolverEnv, {
7205
- credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
7593
+ credentials: credential && credential.tier !== "pointer" ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
7206
7594
  });
7207
7595
  } catch (error) {
7208
7596
  rethrowHooksAuthorityFailure(error);
@@ -7210,31 +7598,50 @@ function resolveHooksTransport(env = process.env, options = {}) {
7210
7598
  if (!credential) {
7211
7599
  throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
7212
7600
  }
7601
+ const authority = {
7602
+ origin: hooksRegistryOrigin(resolution.baseUrl),
7603
+ apiKey: credential.apiKey,
7604
+ apiUrlSource: resolution.apiUrlSource,
7605
+ apiKeySource: credential.source,
7606
+ apiKeyTier: credential.tier,
7607
+ v1BaseUrl: resolution.baseUrl,
7608
+ warning: resolution.warning
7609
+ };
7610
+ authorityBindings.set(authority, { env, options, credential });
7213
7611
  return {
7214
7612
  mode: "remote",
7215
7613
  source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
7216
- authority: {
7217
- origin: hooksRegistryOrigin(resolution.baseUrl),
7218
- apiKey: credential.apiKey,
7219
- apiUrlSource: resolution.apiUrlSource,
7220
- apiKeySource: credential.source,
7221
- apiKeyTier: credential.tier,
7222
- v1BaseUrl: resolution.baseUrl,
7223
- warning: resolution.warning
7224
- }
7614
+ authority
7225
7615
  };
7226
7616
  }
7227
7617
  function resolveHooksServePublishKey(env = process.env, options = {}) {
7228
7618
  const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
7229
7619
  const tier1 = options.credentials?.apiKey;
7230
7620
  const credentialOptions = tier1 !== undefined ? { ...credentials, apiKey: tier1 } : credentials;
7231
- return resolveCredential("hooks", resolverEnv, credentialOptions)?.apiKey;
7621
+ const credential = resolveCredential("hooks", resolverEnv, credentialOptions);
7622
+ if (credential?.tier === "pointer")
7623
+ throw new Error("Vault references require resolveHooksServePublishKeyAsync().");
7624
+ return credential?.apiKey;
7232
7625
  }
7233
- var localNoticePrinted = false;
7626
+ async function resolveHooksServePublishKeyAsync(env = process.env, options = {}) {
7627
+ const { env: resolverEnv, credentials } = hooksResolverInputs(env, options.credentials);
7628
+ const read = () => resolveCredential("hooks", resolverEnv, credentials);
7629
+ const selected = read();
7630
+ if (!selected || selected.tier !== "pointer")
7631
+ return selected?.apiKey;
7632
+ if (!sameCredential(selected, read()))
7633
+ changedBinding();
7634
+ const completed = await completePointerCredential("hooks", selected, env);
7635
+ if (!sameCredential(selected, read()))
7636
+ changedBinding();
7637
+ return completed.apiKey;
7638
+ }
7639
+ var localNoticePrinted = false, authorityBindings;
7234
7640
  var init_transport2 = __esm(() => {
7235
7641
  init_transport();
7236
7642
  init_config();
7237
7643
  init_local_opt_in();
7644
+ authorityBindings = new WeakMap;
7238
7645
  });
7239
7646
 
7240
7647
  // src/lib/db-writer.ts
@@ -7356,12 +7763,12 @@ var init_db_writer = __esm(() => {
7356
7763
  });
7357
7764
 
7358
7765
  // src/lib/hook-env.ts
7359
- import { dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
7360
- import { existsSync as existsSync13, realpathSync as realpathSync2, statSync } from "fs";
7766
+ import { dirname as dirname7, isAbsolute as isAbsolute4 } from "path";
7767
+ import { existsSync as existsSync14, realpathSync as realpathSync4, statSync as statSync2 } from "fs";
7361
7768
  import { tmpdir } from "os";
7362
7769
  function isDeniedEnvName(name) {
7363
7770
  const upper = name.toUpperCase();
7364
- if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
7771
+ if (DENY_PREFIXES.some((prefix2) => upper.startsWith(prefix2)))
7365
7772
  return true;
7366
7773
  if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
7367
7774
  return true;
@@ -7370,13 +7777,13 @@ function isDeniedEnvName(name) {
7370
7777
  function isInterpreterInjectionEnvName(name) {
7371
7778
  if (INTERPRETER_INJECTION.has(name))
7372
7779
  return true;
7373
- return INTERPRETER_INJECTION_PREFIXES.some((prefix) => name.startsWith(prefix));
7780
+ return INTERPRETER_INJECTION_PREFIXES.some((prefix2) => name.startsWith(prefix2));
7374
7781
  }
7375
7782
  function isBashFunctionEnvName(name) {
7376
7783
  return name.startsWith("BASH_FUNC_");
7377
7784
  }
7378
7785
  function isUnsafePathEntry(entry, home) {
7379
- if (entry === "" || !isAbsolute3(entry))
7786
+ if (entry === "" || !isAbsolute4(entry))
7380
7787
  return true;
7381
7788
  if (home && (entry === home || entry.startsWith(home + "/")))
7382
7789
  return true;
@@ -7386,7 +7793,7 @@ function isUnsafePathEntry(entry, home) {
7386
7793
  return true;
7387
7794
  const canonical = (path) => {
7388
7795
  try {
7389
- return realpathSync2(path);
7796
+ return realpathSync4(path);
7390
7797
  } catch {
7391
7798
  return path;
7392
7799
  }
@@ -7398,16 +7805,16 @@ function isUnsafePathEntry(entry, home) {
7398
7805
  return true;
7399
7806
  }
7400
7807
  try {
7401
- if ((statSync(entry).mode & 2) !== 0)
7808
+ if ((statSync2(entry).mode & 2) !== 0)
7402
7809
  return true;
7403
7810
  } catch {}
7404
7811
  return false;
7405
7812
  }
7406
7813
  function sanitizeHookPath(pathValue, home) {
7407
7814
  const baseline = PATH_BASELINE.slice();
7408
- if (process.platform === "darwin" && existsSync13("/opt/homebrew/bin"))
7815
+ if (process.platform === "darwin" && existsSync14("/opt/homebrew/bin"))
7409
7816
  baseline.push("/opt/homebrew/bin");
7410
- const bunDir = dirname5(process.execPath);
7817
+ const bunDir = dirname7(process.execPath);
7411
7818
  const seen = new Set;
7412
7819
  const out = [];
7413
7820
  for (const dir of baseline) {
@@ -7532,9 +7939,9 @@ __export(exports_run, {
7532
7939
  });
7533
7940
  import { randomBytes as randomBytes2 } from "crypto";
7534
7941
  import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
7535
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join13 } from "path";
7942
+ import { basename as basename3, dirname as dirname8, isAbsolute as isAbsolute5, join as join14 } from "path";
7536
7943
  function extensionOf(scriptPath) {
7537
- const base = basename2(scriptPath);
7944
+ const base = basename3(scriptPath);
7538
7945
  const dot = base.lastIndexOf(".");
7539
7946
  return dot > 0 ? base.slice(dot + 1) : "ts";
7540
7947
  }
@@ -7546,9 +7953,9 @@ function shebangInterpreter(firstLine) {
7546
7953
  return null;
7547
7954
  const parts = rest.split(/\s+/);
7548
7955
  if (parts[0] === "env" || parts[0] === "/usr/bin/env") {
7549
- return parts[1] && parts[1] !== "-S" ? basename2(parts[1]) : parts[2] ? basename2(parts[2]) : null;
7956
+ return parts[1] && parts[1] !== "-S" ? basename3(parts[1]) : parts[2] ? basename3(parts[2]) : null;
7550
7957
  }
7551
- return basename2(parts[0]);
7958
+ return basename3(parts[0]);
7552
7959
  }
7553
7960
  function interpreterFor(name, scriptPath, content) {
7554
7961
  const ext = extensionOf(scriptPath);
@@ -7582,8 +7989,8 @@ async function readPipeWithDeadline(stream, deadlineMs) {
7582
7989
  result = await Promise.race([
7583
7990
  reader.read(),
7584
7991
  new Promise((_, reject) => {
7585
- const t = setTimeout(() => reject(new Error("pipe-drain-deadline")), remaining);
7586
- t.unref?.();
7992
+ const t2 = setTimeout(() => reject(new Error("pipe-drain-deadline")), remaining);
7993
+ t2.unref?.();
7587
7994
  })
7588
7995
  ]);
7589
7996
  } catch {
@@ -7601,9 +8008,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
7601
8008
  return chunks.join("");
7602
8009
  }
7603
8010
  async function executeVerifiedScript(options) {
7604
- const scriptDir = dirname6(options.scriptPath);
8011
+ const scriptDir = dirname8(options.scriptPath);
7605
8012
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
7606
- const tempPath = join13(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
8013
+ const tempPath = join14(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
7607
8014
  let proc = null;
7608
8015
  let timedOut = false;
7609
8016
  const timeoutTimer = options.timeout ? setTimeout(() => {
@@ -7612,7 +8019,7 @@ async function executeVerifiedScript(options) {
7612
8019
  }, options.timeout) : null;
7613
8020
  try {
7614
8021
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
7615
- proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
8022
+ proc = Bun.spawn([...interpreter.command, isAbsolute5(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
7616
8023
  stdin: new Response(options.stdin),
7617
8024
  stdout: "pipe",
7618
8025
  stderr: "pipe",
@@ -7694,15 +8101,15 @@ __export(exports_custom_install, {
7694
8101
  installCustomSource: () => installCustomSource,
7695
8102
  ensureCustomDir: () => ensureCustomDir
7696
8103
  });
7697
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync3 } from "fs";
7698
- import { join as join14 } from "path";
8104
+ import { existsSync as existsSync15, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync9, rmSync as rmSync3 } from "fs";
8105
+ import { join as join15 } from "path";
7699
8106
  import { tmpdir as tmpdir2 } from "os";
7700
8107
  function manifestAt(dir) {
7701
- const path = join14(dir, "manifest.json");
7702
- if (!existsSync14(path))
8108
+ const path = join15(dir, "manifest.json");
8109
+ if (!existsSync15(path))
7703
8110
  return;
7704
8111
  try {
7705
- return parseManifest(readFileSync8(path, "utf-8"));
8112
+ return parseManifest(readFileSync9(path, "utf-8"));
7706
8113
  } catch {
7707
8114
  return;
7708
8115
  }
@@ -7721,8 +8128,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
7721
8128
  return { name, version: manifest.version, kind, source, dir, scriptPath };
7722
8129
  }
7723
8130
  async function cloneToTemp(gitUrl) {
7724
- const tmp = mkdtempSync(join14(tmpdir2(), "hooks-install-"));
7725
- const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join14(tmp, "repo")], {
8131
+ const tmp = mkdtempSync(join15(tmpdir2(), "hooks-install-"));
8132
+ const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join15(tmp, "repo")], {
7726
8133
  stdout: "pipe",
7727
8134
  stderr: "pipe"
7728
8135
  });
@@ -7735,7 +8142,7 @@ async function cloneToTemp(gitUrl) {
7735
8142
  rmSync3(tmp, { recursive: true, force: true });
7736
8143
  throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
7737
8144
  }
7738
- return join14(tmp, "repo");
8145
+ return join15(tmp, "repo");
7739
8146
  }
7740
8147
  function isGitUrl(value) {
7741
8148
  return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
@@ -7745,7 +8152,7 @@ function isCustomSource(value) {
7745
8152
  return true;
7746
8153
  if (value.startsWith("http://") || value.startsWith("https://"))
7747
8154
  return true;
7748
- return existsSync14(value);
8155
+ return existsSync15(value);
7749
8156
  }
7750
8157
  async function installCustomSource(source) {
7751
8158
  if (isGitUrl(source)) {
@@ -7759,11 +8166,11 @@ async function installCustomSource(source) {
7759
8166
  if (source.startsWith("http://") || source.startsWith("https://")) {
7760
8167
  return installManifestUrl(source);
7761
8168
  }
7762
- if (!existsSync14(source)) {
8169
+ if (!existsSync15(source)) {
7763
8170
  throw new Error(`No such local path: ${source}`);
7764
8171
  }
7765
8172
  if (source.endsWith("manifest.json")) {
7766
- const manifest = parseManifest(readFileSync8(source, "utf-8"));
8173
+ const manifest = parseManifest(readFileSync9(source, "utf-8"));
7767
8174
  return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
7768
8175
  }
7769
8176
  return copyManifestDir(source, source, "local");
@@ -7812,13 +8219,13 @@ __export(exports_sync, {
7812
8219
  fetchPinnedHook: () => fetchPinnedHook,
7813
8220
  commitSyncArtifacts: () => commitSyncArtifacts
7814
8221
  });
7815
- import { createHash as createHash2 } from "crypto";
7816
- import { readFileSync as readFileSync9 } from "fs";
8222
+ import { createHash as createHash3 } from "crypto";
8223
+ import { readFileSync as readFileSync10 } from "fs";
7817
8224
  function sha256OfText(text) {
7818
- return createHash2("sha256").update(text).digest("hex");
8225
+ return createHash3("sha256").update(text).digest("hex");
7819
8226
  }
7820
8227
  function sha256OfPath(path) {
7821
- return createHash2("sha256").update(readFileSync9(path)).digest("hex");
8228
+ return createHash3("sha256").update(readFileSync10(path)).digest("hex");
7822
8229
  }
7823
8230
  function collectBundledCatalog() {
7824
8231
  const out = [];
@@ -7836,7 +8243,14 @@ function collectBundledCatalog() {
7836
8243
  }
7837
8244
  async function fetchJson(base, path, apiKey, opts) {
7838
8245
  const headers = { accept: "application/json" };
7839
- if (apiKey)
8246
+ if (typeof apiKey === "object") {
8247
+ if (apiKey.origin !== base)
8248
+ throw new Error("Hooks registry authority does not match the request target");
8249
+ const resolved = await resolveHooksRequestAuthority(apiKey);
8250
+ if (resolved.origin !== base)
8251
+ throw new Error("Hooks registry authority changed before dispatch");
8252
+ headers["x-api-key"] = resolved.apiKey;
8253
+ } else if (apiKey)
7840
8254
  headers["x-api-key"] = apiKey;
7841
8255
  const res = await fetch(`${base}${path}`, {
7842
8256
  headers,
@@ -7904,8 +8318,8 @@ async function planSync(options = {}) {
7904
8318
  credentials: options.credentials
7905
8319
  });
7906
8320
  if (transport.mode === "remote" && transport.authority) {
7907
- const { origin, apiKey } = transport.authority;
7908
- const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
8321
+ const { origin } = transport.authority;
8322
+ const { catalog, remoteLock } = await fetchRemoteState(origin, transport.authority);
7909
8323
  return { apiUrl: origin, dryRun: options.dryRun ?? false, diff: computeDiff(catalog, remoteLock) };
7910
8324
  }
7911
8325
  const diff = computeDiff(collectBundledCatalog(), null);
@@ -7933,10 +8347,10 @@ async function syncHooks(options = {}) {
7933
8347
  }
7934
8348
  return plan;
7935
8349
  }
7936
- const { origin, apiKey } = transport.authority;
7937
- const { catalog, remoteLock } = await fetchRemoteState(origin, apiKey);
8350
+ const { origin } = transport.authority;
8351
+ const { catalog, remoteLock } = await fetchRemoteState(origin, transport.authority);
7938
8352
  const diff = computeDiff(catalog, remoteLock);
7939
- const staged = await stageSyncArtifacts(origin, apiKey, [...diff.added, ...diff.updated], remoteLock);
8353
+ const staged = await stageSyncArtifacts(origin, transport.authority, [...diff.added, ...diff.updated], remoteLock);
7940
8354
  await commitSyncArtifacts(staged, origin, remoteLock);
7941
8355
  return { apiUrl: origin, dryRun: false, diff };
7942
8356
  }
@@ -8597,21 +9011,20 @@ function secureEqual(a, b) {
8597
9011
  const ab = enc.encode(a);
8598
9012
  const bb = enc.encode(b);
8599
9013
  let diff = ab.length ^ bb.length;
8600
- const n = Math.max(ab.length, bb.length);
8601
- for (let i = 0;i < n; i++) {
9014
+ const n2 = Math.max(ab.length, bb.length);
9015
+ for (let i = 0;i < n2; i++) {
8602
9016
  diff |= (ab[i] ?? 0) ^ (bb[i] ?? 0);
8603
9017
  }
8604
9018
  return diff === 0;
8605
9019
  }
8606
9020
 
8607
9021
  // src/openapi.ts
8608
- var openApiDocument;
8609
- var init_openapi = __esm(() => {
8610
- openApiDocument = {
9022
+ function buildOpenApiDocument(version) {
9023
+ return {
8611
9024
  openapi: "3.1.0",
8612
9025
  info: {
8613
9026
  title: "Hooks Registry API",
8614
- version: "0.1.0",
9027
+ version,
8615
9028
  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)."
8616
9029
  },
8617
9030
  servers: [{ url: "/" }],
@@ -8625,7 +9038,7 @@ var init_openapi = __esm(() => {
8625
9038
  description: "Service is alive",
8626
9039
  content: {
8627
9040
  "application/json": {
8628
- schema: { type: "object", additionalProperties: true }
9041
+ schema: { $ref: "#/components/schemas/HooksHealth" }
8629
9042
  }
8630
9043
  }
8631
9044
  }
@@ -8637,8 +9050,22 @@ var init_openapi = __esm(() => {
8637
9050
  summary: "Readiness probe \u2014 the hooks.lock store is readable",
8638
9051
  operationId: "getReady",
8639
9052
  responses: {
8640
- "200": { description: "Lock store is readable" },
8641
- "503": { description: "Lock store is not readable" }
9053
+ "200": {
9054
+ description: "Lock store is readable",
9055
+ content: {
9056
+ "application/json": {
9057
+ schema: { $ref: "#/components/schemas/HooksHealth" }
9058
+ }
9059
+ }
9060
+ },
9061
+ "503": {
9062
+ description: "Lock store is not readable",
9063
+ content: {
9064
+ "application/json": {
9065
+ schema: { $ref: "#/components/schemas/HooksHealth" }
9066
+ }
9067
+ }
9068
+ }
8642
9069
  }
8643
9070
  }
8644
9071
  },
@@ -8651,7 +9078,7 @@ var init_openapi = __esm(() => {
8651
9078
  description: "The installed @hasna/hooks version",
8652
9079
  content: {
8653
9080
  "application/json": {
8654
- schema: { type: "object", properties: { version: { type: "string" } } }
9081
+ schema: { $ref: "#/components/schemas/HooksVersion" }
8655
9082
  }
8656
9083
  }
8657
9084
  }
@@ -8662,7 +9089,16 @@ var init_openapi = __esm(() => {
8662
9089
  get: {
8663
9090
  summary: "This OpenAPI document",
8664
9091
  operationId: "getOpenApi",
8665
- responses: { "200": { description: "OpenAPI 3.1 document" } }
9092
+ responses: {
9093
+ "200": {
9094
+ description: "OpenAPI 3.1 document",
9095
+ content: {
9096
+ "application/json": {
9097
+ schema: { type: "object", additionalProperties: true }
9098
+ }
9099
+ }
9100
+ }
9101
+ }
8666
9102
  }
8667
9103
  },
8668
9104
  "/api/v1/catalog": {
@@ -8674,23 +9110,7 @@ var init_openapi = __esm(() => {
8674
9110
  description: "Catalog entries",
8675
9111
  content: {
8676
9112
  "application/json": {
8677
- schema: {
8678
- type: "object",
8679
- properties: {
8680
- hooks: {
8681
- type: "array",
8682
- items: {
8683
- type: "object",
8684
- properties: {
8685
- name: { type: "string" },
8686
- version: { type: "string" },
8687
- sha256: { type: "string" },
8688
- events: { type: "array", items: { type: "string" } }
8689
- }
8690
- }
8691
- }
8692
- }
8693
- }
9113
+ schema: { $ref: "#/components/schemas/HooksCatalog" }
8694
9114
  }
8695
9115
  }
8696
9116
  }
@@ -8701,7 +9121,16 @@ var init_openapi = __esm(() => {
8701
9121
  get: {
8702
9122
  summary: "Read the published hooks.lock with per-hook versions",
8703
9123
  operationId: "getLock",
8704
- responses: { "200": { description: "The lock file" } }
9124
+ responses: {
9125
+ "200": {
9126
+ description: "The lock file",
9127
+ content: {
9128
+ "application/json": {
9129
+ schema: { $ref: "#/components/schemas/HooksLock" }
9130
+ }
9131
+ }
9132
+ }
9133
+ }
8705
9134
  }
8706
9135
  },
8707
9136
  "/api/v1/hooks/{name}/{version}": {
@@ -8714,10 +9143,35 @@ var init_openapi = __esm(() => {
8714
9143
  ],
8715
9144
  responses: {
8716
9145
  "200": {
8717
- description: "Hook artifact; x-hook-sha256 header carries the script hash"
9146
+ description: "Hook artifact; x-hook-sha256 header carries the script hash",
9147
+ headers: {
9148
+ "x-hook-sha256": {
9149
+ description: "sha256 of the served script, the same value the lock pins",
9150
+ schema: { type: "string" }
9151
+ }
9152
+ },
9153
+ content: {
9154
+ "application/json": {
9155
+ schema: { $ref: "#/components/schemas/HooksArtifact" }
9156
+ }
9157
+ }
9158
+ },
9159
+ "400": {
9160
+ description: "Invalid URL encoding or semver version",
9161
+ content: {
9162
+ "application/json": {
9163
+ schema: { $ref: "#/components/schemas/HooksError" }
9164
+ }
9165
+ }
8718
9166
  },
8719
- "400": { description: "Invalid URL encoding or semver version" },
8720
- "404": { description: "Hook not found locally" }
9167
+ "404": {
9168
+ description: "Hook not found locally",
9169
+ content: {
9170
+ "application/json": {
9171
+ schema: { $ref: "#/components/schemas/HooksError" }
9172
+ }
9173
+ }
9174
+ }
8721
9175
  }
8722
9176
  }
8723
9177
  },
@@ -8729,18 +9183,51 @@ var init_openapi = __esm(() => {
8729
9183
  required: true,
8730
9184
  content: {
8731
9185
  "application/json": {
8732
- schema: {
8733
- type: "object",
8734
- properties: { name: { type: "string" }, version: { type: "string" } }
8735
- }
9186
+ schema: { $ref: "#/components/schemas/HooksPublishRequest" }
8736
9187
  }
8737
9188
  }
8738
9189
  },
8739
9190
  responses: {
8740
- "200": { description: "Hook retrusted and pin updated" },
8741
- "401": { description: "Missing or invalid API key" },
8742
- "404": { description: "Hook not found in local store" },
8743
- "409": { description: "Version mismatch with the local store" }
9191
+ "200": {
9192
+ description: "Hook retrusted and pin updated",
9193
+ content: {
9194
+ "application/json": {
9195
+ schema: { $ref: "#/components/schemas/HooksPublishResponse" }
9196
+ }
9197
+ }
9198
+ },
9199
+ "400": {
9200
+ description: "Invalid JSON body",
9201
+ content: {
9202
+ "application/json": {
9203
+ schema: { $ref: "#/components/schemas/HooksError" }
9204
+ }
9205
+ }
9206
+ },
9207
+ "401": {
9208
+ description: "Missing or invalid API key",
9209
+ content: {
9210
+ "application/json": {
9211
+ schema: { $ref: "#/components/schemas/HooksError" }
9212
+ }
9213
+ }
9214
+ },
9215
+ "404": {
9216
+ description: "Hook not found in local store",
9217
+ content: {
9218
+ "application/json": {
9219
+ schema: { $ref: "#/components/schemas/HooksError" }
9220
+ }
9221
+ }
9222
+ },
9223
+ "409": {
9224
+ description: "Version mismatch with the local store",
9225
+ content: {
9226
+ "application/json": {
9227
+ schema: { $ref: "#/components/schemas/HooksError" }
9228
+ }
9229
+ }
9230
+ }
8744
9231
  },
8745
9232
  security: [{ apiKey: [] }]
8746
9233
  }
@@ -8753,10 +9240,135 @@ var init_openapi = __esm(() => {
8753
9240
  scheme: "bearer",
8754
9241
  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."
8755
9242
  }
9243
+ },
9244
+ schemas: {
9245
+ HooksHealth: {
9246
+ type: "object",
9247
+ additionalProperties: false,
9248
+ required: ["status", "name"],
9249
+ properties: {
9250
+ status: { type: "string" },
9251
+ name: { type: "string" }
9252
+ }
9253
+ },
9254
+ HooksVersion: {
9255
+ type: "object",
9256
+ additionalProperties: false,
9257
+ required: ["version"],
9258
+ properties: { version: { type: "string" } }
9259
+ },
9260
+ HooksCatalogEntry: {
9261
+ type: "object",
9262
+ additionalProperties: false,
9263
+ required: ["name", "version", "sha256", "events", "description", "source", "versions"],
9264
+ properties: {
9265
+ name: { type: "string" },
9266
+ version: { type: "string" },
9267
+ sha256: { type: "string" },
9268
+ events: { type: "array", items: { type: "string" } },
9269
+ description: { type: "string" },
9270
+ source: {
9271
+ type: "string",
9272
+ description: "bundled | custom | custom-overrides-bundled"
9273
+ },
9274
+ versions: { type: "array", items: { type: "string" } }
9275
+ }
9276
+ },
9277
+ HooksCatalog: {
9278
+ type: "object",
9279
+ additionalProperties: false,
9280
+ required: ["hooks"],
9281
+ properties: {
9282
+ hooks: { type: "array", items: { $ref: "#/components/schemas/HooksCatalogEntry" } }
9283
+ }
9284
+ },
9285
+ HooksLockEntry: {
9286
+ type: "object",
9287
+ additionalProperties: false,
9288
+ required: ["version", "sha256", "source"],
9289
+ properties: {
9290
+ version: { type: "string" },
9291
+ sha256: { type: "string" },
9292
+ source: { type: "string" },
9293
+ pinned: {
9294
+ type: "boolean",
9295
+ description: "True for an explicit install/update pin, absent for sync-maintained pins."
9296
+ },
9297
+ versions: { type: "array", items: { type: "string" } }
9298
+ }
9299
+ },
9300
+ HooksLock: {
9301
+ type: "object",
9302
+ additionalProperties: false,
9303
+ required: ["hooks"],
9304
+ properties: {
9305
+ hooks: {
9306
+ type: "object",
9307
+ additionalProperties: { $ref: "#/components/schemas/HooksLockEntry" }
9308
+ }
9309
+ }
9310
+ },
9311
+ HooksManifest: {
9312
+ type: "object",
9313
+ additionalProperties: false,
9314
+ required: ["name", "version", "events", "script"],
9315
+ properties: {
9316
+ name: { type: "string" },
9317
+ version: { type: "string" },
9318
+ description: { type: "string" },
9319
+ events: { type: "array", items: { type: "string" } },
9320
+ script: { type: "string" },
9321
+ script_kind: { type: "string", enum: ["inline", "file"] },
9322
+ args: { type: "array", items: { type: "string" } },
9323
+ timeout_ms: { type: "number" }
9324
+ }
9325
+ },
9326
+ HooksArtifact: {
9327
+ type: "object",
9328
+ additionalProperties: false,
9329
+ required: ["manifest", "script"],
9330
+ properties: {
9331
+ manifest: { $ref: "#/components/schemas/HooksManifest" },
9332
+ script: { type: "string" }
9333
+ }
9334
+ },
9335
+ HooksError: {
9336
+ type: "object",
9337
+ additionalProperties: false,
9338
+ required: ["error"],
9339
+ properties: { error: { type: "string" } }
9340
+ },
9341
+ HooksPublishRequest: {
9342
+ type: "object",
9343
+ additionalProperties: false,
9344
+ required: ["name"],
9345
+ properties: {
9346
+ name: { type: "string" },
9347
+ version: { type: "string" }
9348
+ }
9349
+ },
9350
+ HooksPublishResponse: {
9351
+ type: "object",
9352
+ additionalProperties: false,
9353
+ required: ["ok", "hook"],
9354
+ properties: {
9355
+ ok: { type: "boolean" },
9356
+ hook: {
9357
+ type: "object",
9358
+ additionalProperties: false,
9359
+ required: ["name", "version", "sha256"],
9360
+ properties: {
9361
+ name: { type: "string" },
9362
+ version: { type: "string" },
9363
+ sha256: { type: "string" }
9364
+ }
9365
+ }
9366
+ }
9367
+ }
8756
9368
  }
8757
9369
  }
8758
9370
  };
8759
- });
9371
+ }
8760
9372
 
8761
9373
  // src/serve.ts
8762
9374
  var exports_serve = {};
@@ -8768,13 +9380,13 @@ __export(exports_serve, {
8768
9380
  SERVE_HOST: () => SERVE_HOST,
8769
9381
  DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
8770
9382
  });
8771
- import { readFileSync as readFileSync10 } from "fs";
8772
- import { dirname as dirname7, join as join15 } from "path";
8773
- import { fileURLToPath as fileURLToPath3 } from "url";
9383
+ import { readFileSync as readFileSync11 } from "fs";
9384
+ import { dirname as dirname9, join as join16 } from "path";
9385
+ import { fileURLToPath as fileURLToPath4 } from "url";
8774
9386
  function packageVersion() {
8775
9387
  try {
8776
- const pkgPath = join15(dirname7(fileURLToPath3(import.meta.url)), "..", "package.json");
8777
- return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
9388
+ const pkgPath = join16(dirname9(fileURLToPath4(import.meta.url)), "..", "package.json");
9389
+ return JSON.parse(readFileSync11(pkgPath, "utf-8")).version || "0.0.0";
8778
9390
  } catch {
8779
9391
  return "0.0.0";
8780
9392
  }
@@ -8863,7 +9475,7 @@ function handleServeRequest(req, apiKey) {
8863
9475
  return Promise.resolve(json({ version: packageVersion() }));
8864
9476
  }
8865
9477
  if (url.pathname === "/openapi.json" && req.method === "GET") {
8866
- return Promise.resolve(json(openApiDocument));
9478
+ return Promise.resolve(json(buildOpenApiDocument(packageVersion())));
8867
9479
  }
8868
9480
  if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
8869
9481
  return buildCatalog().then((catalog) => json({ hooks: catalog }));
@@ -8935,7 +9547,7 @@ function startServeServer(options) {
8935
9547
  hostname: host,
8936
9548
  port,
8937
9549
  async fetch(req) {
8938
- return handleServeRequest(req, resolveHooksServePublishKey());
9550
+ return handleServeRequest(req, await resolveHooksServePublishKeyAsync());
8939
9551
  }
8940
9552
  });
8941
9553
  console.error(`hooks registry listening on http://${host}:${server.port} (publish requires an API key)`);
@@ -8948,7 +9560,6 @@ var init_serve = __esm(() => {
8948
9560
  init_store();
8949
9561
  init_resolve();
8950
9562
  init_transport2();
8951
- init_openapi();
8952
9563
  init_semver();
8953
9564
  if (false) {}
8954
9565
  });
@@ -9075,17 +9686,17 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
9075
9686
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9076
9687
  import { z as z2 } from "zod";
9077
9688
  import { createServer } from "http";
9078
- import { existsSync as existsSync15, readFileSync as readFileSync11 } from "fs";
9079
- import { join as join16, dirname as dirname8 } from "path";
9080
- import { fileURLToPath as fileURLToPath4 } from "url";
9689
+ import { existsSync as existsSync16, readFileSync as readFileSync12 } from "fs";
9690
+ import { join as join17, dirname as dirname10 } from "path";
9691
+ import { fileURLToPath as fileURLToPath5 } from "url";
9081
9692
  async function runVerifiedHook(name, stdin, timeoutMs) {
9082
9693
  const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
9083
9694
  const resolved = resolveHook2(name);
9084
9695
  const hookScript = resolveScriptPath2(name);
9085
- if (!resolved || !hookScript || !existsSync15(hookScript)) {
9696
+ if (!resolved || !hookScript || !existsSync16(hookScript)) {
9086
9697
  throw new Error(`Hook '${name}' not found`);
9087
9698
  }
9088
- const content = readFileSync11(hookScript);
9699
+ const content = readFileSync12(hookScript);
9089
9700
  const sha = sha256Of(content);
9090
9701
  const check = checkScriptHash(name, sha);
9091
9702
  if (!check.ok) {
@@ -9151,8 +9762,8 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
9151
9762
  return ran;
9152
9763
  }
9153
9764
  function formatInstallResults(results, extra) {
9154
- const installed = results.filter((r) => r.success).map((r) => r.hook);
9155
- const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
9765
+ const installed = results.filter((r2) => r2.success).map((r2) => r2.hook);
9766
+ const failed = results.filter((r2) => !r2.success).map((r2) => ({ hook: r2.hook, error: r2.error }));
9156
9767
  return {
9157
9768
  content: [{
9158
9769
  type: "text",
@@ -9290,7 +9901,7 @@ function createHooksServer() {
9290
9901
  const settingsPath = getSettingsPath(scope);
9291
9902
  const issues = [];
9292
9903
  const healthy = [];
9293
- const settingsExist = existsSync15(settingsPath);
9904
+ const settingsExist = existsSync16(settingsPath);
9294
9905
  if (!settingsExist) {
9295
9906
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
9296
9907
  }
@@ -9300,7 +9911,7 @@ function createHooksServer() {
9300
9911
  const meta = custom ? resolveHookMeta(name) : getHook(name);
9301
9912
  let hookHealthy = true;
9302
9913
  if (custom) {
9303
- if (!existsSync15(custom.scriptPath)) {
9914
+ if (!existsSync16(custom.scriptPath)) {
9304
9915
  issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
9305
9916
  hookHealthy = false;
9306
9917
  }
@@ -9310,14 +9921,14 @@ function createHooksServer() {
9310
9921
  continue;
9311
9922
  }
9312
9923
  const hookDir = getHookPath(name);
9313
- if (!existsSync15(join16(hookDir, "src", "hook.ts"))) {
9924
+ if (!existsSync16(join17(hookDir, "src", "hook.ts"))) {
9314
9925
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
9315
9926
  hookHealthy = false;
9316
9927
  }
9317
9928
  }
9318
9929
  if (meta && settingsExist) {
9319
9930
  try {
9320
- const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
9931
+ const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
9321
9932
  if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
9322
9933
  const eventName = meta.event.split(":")[0];
9323
9934
  issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
@@ -9354,10 +9965,10 @@ function createHooksServer() {
9354
9965
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
9355
9966
  }
9356
9967
  const hookPath = getHookPath(name);
9357
- const readmePath = join16(hookPath, "README.md");
9968
+ const readmePath = join17(hookPath, "README.md");
9358
9969
  let readme = "";
9359
- if (existsSync15(readmePath)) {
9360
- readme = readFileSync11(readmePath, "utf-8");
9970
+ if (existsSync16(readmePath)) {
9971
+ readme = readFileSync12(readmePath, "utf-8");
9361
9972
  }
9362
9973
  if (verbose) {
9363
9974
  return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
@@ -9515,8 +10126,8 @@ function createHooksServer() {
9515
10126
  }
9516
10127
  return installHook(name, { scope, overwrite: true });
9517
10128
  });
9518
- const updated = results.filter((r) => r.success).map((r) => r.hook);
9519
- const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
10129
+ const updated = results.filter((r2) => r2.success).map((r2) => r2.hook);
10130
+ const failed = results.filter((r2) => !r2.success).map((r2) => ({ hook: r2.hook, error: r2.error }));
9520
10131
  return { content: [{ type: "text", text: JSON.stringify({ updated, failed, total: results.length }) }] };
9521
10132
  });
9522
10133
  defineTool("hooks_context", "Get compact agent context in one call: installed hooks, active profile summary, settings path, and doctor status.", {
@@ -9540,7 +10151,7 @@ function createHooksServer() {
9540
10151
  const ctx = {
9541
10152
  scope,
9542
10153
  settings_path: settingsPath,
9543
- settings_exists: existsSync15(settingsPath),
10154
+ settings_exists: existsSync16(settingsPath),
9544
10155
  registered_hooks: hooks,
9545
10156
  hook_count: hooks.length,
9546
10157
  healthy,
@@ -9603,7 +10214,7 @@ function createHooksServer() {
9603
10214
  return { name, decision: "block", error: err instanceof Error ? err.message : String(err) };
9604
10215
  }
9605
10216
  }));
9606
- const blocked = results.find((r) => r.decision === "block");
10217
+ const blocked = results.find((r2) => r2.decision === "block");
9607
10218
  return {
9608
10219
  content: [{
9609
10220
  type: "text",
@@ -9637,8 +10248,8 @@ function createHooksServer() {
9637
10248
  const target = agent_type === "gemini" ? "gemini" : "claude";
9638
10249
  const toInstall = hooks && hooks.length > 0 ? hooks : ["gitguard", "checkpoint", "checktests", "protectfiles"];
9639
10250
  const results = toInstall.map((h) => installHook(h, { scope, overwrite: false, profile: profile.agent_id, target }));
9640
- const installed = results.filter((r) => r.success).map((r) => r.hook);
9641
- const failed = results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }));
10251
+ const installed = results.filter((r2) => r2.success).map((r2) => r2.hook);
10252
+ const failed = results.filter((r2) => !r2.success).map((r2) => ({ hook: r2.hook, error: r2.error }));
9642
10253
  return {
9643
10254
  content: [{
9644
10255
  type: "text",
@@ -9686,8 +10297,8 @@ function createHooksServer() {
9686
10297
  const settingsPath = getSettingsPath(scope);
9687
10298
  let settings = {};
9688
10299
  try {
9689
- if (existsSync15(settingsPath))
9690
- settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
10300
+ if (existsSync16(settingsPath))
10301
+ settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
9691
10302
  } catch {}
9692
10303
  if (!settings.hooks)
9693
10304
  settings.hooks = {};
@@ -9696,8 +10307,8 @@ function createHooksServer() {
9696
10307
  disabled.push(name);
9697
10308
  settings.hooks.__disabled = disabled;
9698
10309
  const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
9699
- const { dirname: dirname9 } = await import("path");
9700
- mkdirSync8(dirname9(settingsPath), { recursive: true });
10310
+ const { dirname: dirname11 } = await import("path");
10311
+ mkdirSync8(dirname11(settingsPath), { recursive: true });
9701
10312
  writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
9702
10313
  `);
9703
10314
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
@@ -9709,11 +10320,11 @@ function createHooksServer() {
9709
10320
  const settingsPath = getSettingsPath(scope);
9710
10321
  let settings = {};
9711
10322
  try {
9712
- if (existsSync15(settingsPath))
9713
- settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
10323
+ if (existsSync16(settingsPath))
10324
+ settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
9714
10325
  } catch {}
9715
10326
  if (settings.hooks?.__disabled) {
9716
- settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
10327
+ settings.hooks.__disabled = settings.hooks.__disabled.filter((n2) => n2 !== name);
9717
10328
  if (settings.hooks.__disabled.length === 0)
9718
10329
  delete settings.hooks.__disabled;
9719
10330
  const { writeFileSync: writeFileSync7 } = await import("fs");
@@ -9770,9 +10381,9 @@ function createHooksServer() {
9770
10381
  const m = s.match(/^(\d+)(s|m|h|d)$/);
9771
10382
  if (!m)
9772
10383
  return null;
9773
- const n = parseInt(m[1]);
10384
+ const n2 = parseInt(m[1]);
9774
10385
  const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
9775
- return new Date(Date.now() - n * ms).toISOString();
10386
+ return new Date(Date.now() - n2 * ms).toISOString();
9776
10387
  }
9777
10388
  let sql = "SELECT * FROM hook_events WHERE 1=1";
9778
10389
  const params = [];
@@ -9810,10 +10421,10 @@ function createHooksServer() {
9810
10421
  defineTool("hooks_log_tail", "Show recent hook events from SQLite. Compact summaries by default.", {
9811
10422
  n: z2.number().default(20).describe("Number of most recent events to return"),
9812
10423
  compact: z2.boolean().default(true).describe("Return compact event summaries by default. Set false for full rows.")
9813
- }, async ({ n, compact }) => {
10424
+ }, async ({ n: n2, compact }) => {
9814
10425
  const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
9815
10426
  const db = getDb2();
9816
- const maxRows = boundedLimit(n, 20, compact ? 100 : 500);
10427
+ const maxRows = boundedLimit(n2, 20, compact ? 100 : 500);
9817
10428
  const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(maxRows);
9818
10429
  const projected = projectLogRows(rows);
9819
10430
  return {
@@ -9840,9 +10451,9 @@ function createHooksServer() {
9840
10451
  const m = s.match(/^(\d+)(s|m|h|d)$/);
9841
10452
  if (!m)
9842
10453
  return s;
9843
- const n = parseInt(m[1]);
10454
+ const n2 = parseInt(m[1]);
9844
10455
  const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
9845
- return new Date(Date.now() - n * ms).toISOString();
10456
+ return new Date(Date.now() - n2 * ms).toISOString();
9846
10457
  }
9847
10458
  const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
9848
10459
  const rows = db.query("SELECT * FROM hook_events WHERE error IS NOT NULL AND timestamp >= ? ORDER BY timestamp DESC LIMIT ?").all(ts, maxRows);
@@ -9868,20 +10479,20 @@ function createHooksServer() {
9868
10479
  const m = s.match(/^(\d+)(s|m|h|d)$/);
9869
10480
  if (!m)
9870
10481
  return s;
9871
- const n = parseInt(m[1]);
10482
+ const n2 = parseInt(m[1]);
9872
10483
  const ms = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[m[2]];
9873
- return new Date(Date.now() - n * ms).toISOString();
10484
+ return new Date(Date.now() - n2 * ms).toISOString();
9874
10485
  }
9875
10486
  const ts = since.match(/^\d{4}/) ? since : parseDuration(since);
9876
10487
  const totals = db.query("SELECT hook_name, COUNT(*) as total, SUM(CASE WHEN error IS NOT NULL THEN 1 ELSE 0 END) as errors FROM hook_events WHERE timestamp >= ? GROUP BY hook_name ORDER BY total DESC").all(ts);
9877
- const summary = totals.map((r) => ({
9878
- hook_name: r.hook_name,
9879
- total: r.total,
9880
- errors: r.errors,
9881
- error_rate: r.total > 0 ? (r.errors / r.total * 100).toFixed(1) + "%" : "0%"
10488
+ const summary = totals.map((r2) => ({
10489
+ hook_name: r2.hook_name,
10490
+ total: r2.total,
10491
+ errors: r2.errors,
10492
+ error_rate: r2.total > 0 ? (r2.errors / r2.total * 100).toFixed(1) + "%" : "0%"
9882
10493
  }));
9883
- const grandTotal = totals.reduce((s, r) => s + r.total, 0);
9884
- const grandErrors = totals.reduce((s, r) => s + r.errors, 0);
10494
+ const grandTotal = totals.reduce((s, r2) => s + r2.total, 0);
10495
+ const grandErrors = totals.reduce((s, r2) => s + r2.errors, 0);
9885
10496
  return {
9886
10497
  content: [{
9887
10498
  type: "text",
@@ -9912,8 +10523,8 @@ function createHooksServer() {
9912
10523
  pkg.version
9913
10524
  ]);
9914
10525
  return { content: [{ type: "text", text: "Feedback saved. Thank you!" }] };
9915
- } catch (e) {
9916
- return { content: [{ type: "text", text: String(e) }], isError: true };
10526
+ } catch (e2) {
10527
+ return { content: [{ type: "text", text: String(e2) }], isError: true };
9917
10528
  }
9918
10529
  });
9919
10530
  defineTool("register_agent", "Register an agent session. Returns agent_id. Auto-triggers a heartbeat.", {
@@ -10080,13 +10691,13 @@ var init_server = __esm(() => {
10080
10691
  init_redact();
10081
10692
  init_authority();
10082
10693
  init_storage();
10083
- __dirname3 = dirname8(fileURLToPath4(import.meta.url));
10694
+ __dirname3 = dirname10(fileURLToPath5(import.meta.url));
10084
10695
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
10085
10696
  try {
10086
10697
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
10087
- const p = join16(__dirname3, rel);
10088
- if (existsSync15(p)) {
10089
- pkg = JSON.parse(readFileSync11(p, "utf-8"));
10698
+ const p = join17(__dirname3, rel);
10699
+ if (existsSync16(p)) {
10700
+ pkg = JSON.parse(readFileSync12(p, "utf-8"));
10090
10701
  break;
10091
10702
  }
10092
10703
  }
@@ -11828,9 +12439,9 @@ var {
11828
12439
 
11829
12440
  // src/cli/index.tsx
11830
12441
  import chalk2 from "chalk";
11831
- import { existsSync as existsSync16, readFileSync as readFileSync12, rmSync as rmSync4 } from "fs";
11832
- import { join as join17, dirname as dirname9 } from "path";
11833
- import { fileURLToPath as fileURLToPath5 } from "url";
12442
+ import { existsSync as existsSync17, readFileSync as readFileSync13, rmSync as rmSync4 } from "fs";
12443
+ import { join as join18, dirname as dirname11 } from "path";
12444
+ import { fileURLToPath as fileURLToPath6 } from "url";
11834
12445
 
11835
12446
  // src/cli/components/App.tsx
11836
12447
  import { useState as useState7 } from "react";
@@ -13002,6 +13613,7 @@ function App({ initialHooks, overwrite = false }) {
13002
13613
  }
13003
13614
 
13004
13615
  // src/cli/index.tsx
13616
+ init_native_safety();
13005
13617
  init_registry();
13006
13618
  init_installer();
13007
13619
  init_registration();
@@ -13017,10 +13629,10 @@ init_local_opt_in();
13017
13629
  init_transport2();
13018
13630
  init_db();
13019
13631
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
13020
- var __dirname4 = dirname9(fileURLToPath5(import.meta.url));
13021
- var pkgPath = existsSync16(join17(__dirname4, "..", "package.json")) ? join17(__dirname4, "..", "package.json") : join17(__dirname4, "..", "..", "package.json");
13022
- var pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
13023
- var program2 = new Command;
13632
+ var __dirname4 = dirname11(fileURLToPath6(import.meta.url));
13633
+ var pkgPath = existsSync17(join18(__dirname4, "..", "package.json")) ? join18(__dirname4, "..", "package.json") : join18(__dirname4, "..", "..", "package.json");
13634
+ var pkg2 = JSON.parse(readFileSync13(pkgPath, "utf-8"));
13635
+ var program3 = new Command;
13024
13636
  function resolveScope(options) {
13025
13637
  if (options.project)
13026
13638
  return "project";
@@ -13099,7 +13711,7 @@ async function openLocalStoreOrExit(json2) {
13099
13711
  function readToken(tokenFile) {
13100
13712
  if (tokenFile) {
13101
13713
  try {
13102
- return readFileSync12(tokenFile, "utf-8").trim();
13714
+ return readFileSync13(tokenFile, "utf-8").trim();
13103
13715
  } catch {
13104
13716
  return;
13105
13717
  }
@@ -13144,22 +13756,22 @@ function printDisclosureHint(hidden, detailCommand, options = {}) {
13144
13756
  }
13145
13757
  }
13146
13758
  function editDistance(a, b) {
13147
- const m = a.length, n = b.length;
13148
- const dp = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n).fill(0)]);
13149
- for (let j = 0;j <= n; j++)
13759
+ const m = a.length, n2 = b.length;
13760
+ const dp = Array.from({ length: m + 1 }, (_, i) => [i, ...Array(n2).fill(0)]);
13761
+ for (let j = 0;j <= n2; j++)
13150
13762
  dp[0][j] = j;
13151
13763
  for (let i = 1;i <= m; i++) {
13152
- for (let j = 1;j <= n; j++) {
13764
+ for (let j = 1;j <= n2; j++) {
13153
13765
  dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
13154
13766
  }
13155
13767
  }
13156
- return dp[m][n];
13768
+ return dp[m][n2];
13157
13769
  }
13158
13770
  function suggestHooks(name, max = 3) {
13159
- return HOOKS.map((h) => ({ name: h.name, dist: editDistance(name.toLowerCase(), h.name.toLowerCase()) })).filter(({ dist }) => dist <= 4).sort((a, b) => a.dist - b.dist).slice(0, max).map(({ name: n }) => n);
13771
+ return HOOKS.map((h) => ({ name: h.name, dist: editDistance(name.toLowerCase(), h.name.toLowerCase()) })).filter(({ dist }) => dist <= 4).sort((a, b) => a.dist - b.dist).slice(0, max).map(({ name: n2 }) => n2);
13160
13772
  }
13161
- program2.name("hooks").description("Install hooks for AI coding agents").version(pkg2.version);
13162
- program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
13773
+ program3.name("hooks").description("Install hooks for AI coding agents").version(pkg2.version);
13774
+ program3.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
13163
13775
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
13164
13776
  console.error(chalk2.red("The interactive hook browser requires a TTY terminal.") + `
13165
13777
  ` + chalk2.dim("Use `hooks search <query>`, `hooks list`, or `hooks categories` for non-interactive use."));
@@ -13167,7 +13779,7 @@ program2.command("interactive", { isDefault: true }).alias("i").description("Int
13167
13779
  }
13168
13780
  render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
13169
13781
  });
13170
- program2.command("init").description("Register a new agent profile with a unique ID").option("-a, --agent <type>", "Agent type: claude, gemini, custom", "claude").option("-n, --name <name>", "Optional display name for the agent").option("-j, --json", "Output as JSON", false).option("--cloudflare", "Configure a remote registry (Cloudflare worker)", false).option("--api-url <url>", "Remote registry API URL (with --cloudflare)").option("--api-key <ref>", "Vault key NAME for the registry API key (never the value; with --cloudflare)").action(async (options) => {
13782
+ program3.command("init").description("Register a new agent profile with a unique ID").option("-a, --agent <type>", "Agent type: claude, gemini, custom", "claude").option("-n, --name <name>", "Optional display name for the agent").option("-j, --json", "Output as JSON", false).option("--cloudflare", "Configure a remote registry (Cloudflare worker)", false).option("--api-url <url>", "Remote registry API URL (with --cloudflare)").option("--api-key <ref>", "Vault key NAME for the registry API key (never the value; with --cloudflare)").action(async (options) => {
13171
13783
  if (options.cloudflare) {
13172
13784
  if (!options.apiUrl) {
13173
13785
  const message = "--cloudflare requires --api-url <url>";
@@ -13237,13 +13849,13 @@ program2.command("init").description("Register a new agent profile with a unique
13237
13849
  if (profile.name) {
13238
13850
  console.log(` ${chalk2.dim("Name:")} ${profile.name}`);
13239
13851
  }
13240
- console.log(` ${chalk2.dim("Profile:")} ${join17(getProfilesDir(), `${profile.agent_id}.json`)}`);
13852
+ console.log(` ${chalk2.dim("Profile:")} ${join18(getProfilesDir(), `${profile.agent_id}.json`)}`);
13241
13853
  console.log();
13242
13854
  console.log(chalk2.dim(" Install hooks with this profile:"));
13243
13855
  console.log(` hooks install gitguard --profile ${profile.agent_id}`);
13244
13856
  console.log();
13245
13857
  });
13246
- program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>", "Agent profile ID").option("--enable-mementos", "Enable mementos-context for this invocation (provider assistance remains separately opt-in)").option("--disable-mementos", "Disable mementos-context for this invocation").option("--openrouter-secret-ref <reference>", "Secrets reference for the enabled Mementos hook; never a key value").description("Execute a hook (called by AI coding agents)").action(async (hook, options) => {
13858
+ program3.command("run").argument("<hook>", "Hook to run").option("--profile <id>", "Agent profile ID").option("--enable-mementos", "Enable mementos-context for this invocation (provider assistance remains separately opt-in)").option("--disable-mementos", "Disable mementos-context for this invocation").option("--openrouter-secret-ref <reference>", "Secrets reference for the enabled Mementos hook; never a key value").description("Execute a hook (called by AI coding agents)").action(async (hook, options) => {
13247
13859
  let mementos;
13248
13860
  try {
13249
13861
  mementos = mementosRegistrationFromFlags(hook, options);
@@ -13261,11 +13873,11 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
13261
13873
  process.exit(1);
13262
13874
  }
13263
13875
  const hookScript = resolveScriptPath2(hook);
13264
- if (!hookScript || !existsSync16(hookScript)) {
13876
+ if (!hookScript || !existsSync17(hookScript)) {
13265
13877
  console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
13266
13878
  process.exit(1);
13267
13879
  }
13268
- const content = readFileSync12(hookScript);
13880
+ const content = readFileSync13(hookScript);
13269
13881
  const sha = sha256Of2(content);
13270
13882
  const check = checkScriptHash2(hook, sha);
13271
13883
  if (!check.ok) {
@@ -13371,7 +13983,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
13371
13983
  process.stderr.write(stderr);
13372
13984
  process.exit(exitCode);
13373
13985
  });
13374
- program2.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").option("--enable-mementos", "Persistently enable mementos-context (provider assistance remains separately opt-in)").option("--disable-mementos", "Persistently disable mementos-context").option("--openrouter-secret-ref <reference>", "Secrets reference for the enabled Mementos hook; never a key value").option("--dry-run", "Preview what would be installed without writing to settings", false).option("--apply-codewith", "Explicitly append Codewith TOML to a config file (prefer configs for managed configs)", false).option("--codewith-config <path>", "Explicit Codewith config path required with --apply-codewith").option("-j, --json", "Output as JSON", false).description("Install one or more hooks (registry names, local paths, git URLs, manifest URLs, or <name>@<version> pinned installs)").action(async (hooks, options) => {
13986
+ program3.command("install").alias("add").argument("[hooks...]", "Hooks to install").option("-o, --overwrite", "Overwrite existing hooks", false).option("-a, --all", "Install all available hooks", false).option("-c, --category <category>", "Install all hooks in a category").option("-g, --global", "Install globally (~/.claude/settings.json)", false).option("-p, --project", "Install for current project (.claude/settings.json)", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").option("--enable-mementos", "Persistently enable mementos-context (provider assistance remains separately opt-in)").option("--disable-mementos", "Persistently disable mementos-context").option("--openrouter-secret-ref <reference>", "Secrets reference for the enabled Mementos hook; never a key value").option("--dry-run", "Preview what would be installed without writing to settings", false).option("--apply-codewith", "Explicitly append Codewith TOML to a config file (prefer configs for managed configs)", false).option("--codewith-config <path>", "Explicit Codewith config path required with --apply-codewith").option("-j, --json", "Output as JSON", false).description("Install one or more hooks (registry names, local paths, git URLs, manifest URLs, or <name>@<version> pinned installs)").action(async (hooks, options) => {
13375
13987
  const scope = resolveScope(options);
13376
13988
  const target = resolveTarget(options);
13377
13989
  let toInstall = hooks;
@@ -13437,10 +14049,10 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
13437
14049
  return { name, version };
13438
14050
  }
13439
14051
  const pinnedRequests = toInstall.map((arg) => ({ arg, pinned: parseNameVersion(arg) })).filter((x) => x.pinned !== null);
13440
- const customSources = toInstall.filter((n) => !getHook(n) && isCustomSource2(n));
14052
+ const customSources = toInstall.filter((n2) => !getHook(n2) && isCustomSource2(n2));
13441
14053
  if (options.dryRun) {
13442
- const known = toInstall.filter((n) => getHook(n));
13443
- const unknown = toInstall.filter((n) => !getHook(n) && !isCustomSource2(n) && !parseNameVersion(n) && !readCustomManifest2(n));
14054
+ const known = toInstall.filter((n2) => getHook(n2));
14055
+ const unknown = toInstall.filter((n2) => !getHook(n2) && !isCustomSource2(n2) && !parseNameVersion(n2) && !readCustomManifest2(n2));
13444
14056
  if (options.json) {
13445
14057
  console.log(JSON.stringify({
13446
14058
  dryRun: true,
@@ -13514,7 +14126,7 @@ Dry run \u2014 would install (${scope}, ${target}):
13514
14126
  for (const { arg, pinned } of pinnedRequests) {
13515
14127
  try {
13516
14128
  const transport = resolveHooksTransport2();
13517
- const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority.apiKey) : await installPinnedFromBundled2(pinned.name, pinned.version);
14129
+ const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority) : await installPinnedFromBundled2(pinned.name, pinned.version);
13518
14130
  installedPinned.push(pinnedInstall.name);
13519
14131
  if (options.json) {
13520
14132
  console.log(JSON.stringify({ pinned: { request: arg, name: pinnedInstall.name, version: pinnedInstall.version, sha256: pinnedInstall.sha256, source: pinnedInstall.source } }));
@@ -13533,7 +14145,7 @@ Dry run \u2014 would install (${scope}, ${target}):
13533
14145
  }
13534
14146
  }
13535
14147
  const namesToRegister = [...new Set([
13536
- ...toInstall.filter((n) => !isCustomSource2(n) && !parseNameVersion(n)),
14148
+ ...toInstall.filter((n2) => !isCustomSource2(n2) && !parseNameVersion(n2)),
13537
14149
  ...installedCustom,
13538
14150
  ...installedPinned
13539
14151
  ])];
@@ -13557,22 +14169,22 @@ Dry run \u2014 would install (${scope}, ${target}):
13557
14169
  }
13558
14170
  if (options.json) {
13559
14171
  console.log(JSON.stringify({
13560
- installed: results.filter((r) => r.success).map((r) => r.hook),
13561
- failed: results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error })),
13562
- fragments: results.filter((r) => r.success && r.fragment).map((r) => ({ hook: r.hook, fragment: r.fragment, applied: r.applied, configPath: r.configPath, note: r.note })),
14172
+ installed: results.filter((r2) => r2.success).map((r2) => r2.hook),
14173
+ failed: results.filter((r2) => !r2.success).map((r2) => ({ hook: r2.hook, error: r2.error })),
14174
+ fragments: results.filter((r2) => r2.success && r2.fragment).map((r2) => ({ hook: r2.hook, fragment: r2.fragment, applied: r2.applied, configPath: r2.configPath, note: r2.note })),
13563
14175
  total: results.length,
13564
- success: results.filter((r) => r.success).length,
14176
+ success: results.filter((r2) => r2.success).length,
13565
14177
  scope,
13566
14178
  target,
13567
- applied: results.some((r) => r.applied),
13568
- ...target === "codex" ? { notes: results.filter((r) => r.success && r.note).map((r) => r.note) } : {}
14179
+ applied: results.some((r2) => r2.applied),
14180
+ ...target === "codex" ? { notes: results.filter((r2) => r2.success && r2.note).map((r2) => r2.note) } : {}
13569
14181
  }));
13570
- if (results.some((r) => !r.success)) {
14182
+ if (results.some((r2) => !r2.success)) {
13571
14183
  process.exitCode = 1;
13572
14184
  }
13573
14185
  return;
13574
14186
  }
13575
- const successCount = results.filter((r) => r.success).length;
14187
+ const successCount = results.filter((r2) => r2.success).length;
13576
14188
  const settingsFile = target === "codewith" ? options.applyCodewith ? options.codewithConfig : "TOML fragment only (configs should apply)" : formatSettingsPath(scope, target);
13577
14189
  console.log(chalk2.bold(`
13578
14190
  Installing hooks (${scope}, ${target})...
@@ -13615,7 +14227,7 @@ Installing hooks (${scope}, ${target})...
13615
14227
  console.log(chalk2.dim(`
13616
14228
  Registered in ${settingsFile}`));
13617
14229
  });
13618
- program2.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex (default: claude)", "claude").option("-n, --limit <n>", "Max rows to show in compact output", "20").option("--verbose", "Show descriptions and full detail columns", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks (bundled + custom/registry)").action((options) => {
14230
+ program3.command("list").alias("ls").option("-c, --category <category>", "Filter by category").option("-a, --all", "Show all available hooks", false).option("-i, --installed", "Show only installed hooks", false).option("-r, --registered", "Show registered hooks", false).option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex (default: claude)", "claude").option("-n, --limit <n>", "Max rows to show in compact output", "20").option("--verbose", "Show descriptions and full detail columns", false).option("-j, --json", "Output as JSON", false).description("List available or installed hooks (bundled + custom/registry)").action((options) => {
13619
14231
  const scope = resolveScope(options);
13620
14232
  const limit = options.all ? Number.MAX_SAFE_INTEGER : parseLimit(options.limit, 20, 200);
13621
14233
  const storeHooks = listStoreHooks();
@@ -13724,7 +14336,7 @@ Custom / Registry hooks (${storeHooks.length}, showing ${storeVisible.length}):
13724
14336
  printDisclosureHint(storeHooks.length - storeVisible.length, "hooks info <name>", { includeAll: true });
13725
14337
  }
13726
14338
  });
13727
- program2.command("search").argument("<query>", "Search term").option("-n, --limit <n>", "Max rows to show in compact output", "10").option("--verbose", "Show descriptions for search results", false).option("-j, --json", "Output as JSON", false).description("Search for hooks").action((query, options) => {
14339
+ program3.command("search").argument("<query>", "Search term").option("-n, --limit <n>", "Max rows to show in compact output", "10").option("--verbose", "Show descriptions for search results", false).option("-j, --json", "Output as JSON", false).description("Search for hooks").action((query, options) => {
13728
14340
  const results = searchHooks(query);
13729
14341
  if (options.json) {
13730
14342
  console.log(JSON.stringify(results));
@@ -13743,7 +14355,7 @@ Found ${results.length} hook(s), showing ${visible.length}:
13743
14355
  console.log(hookSummaryLine(h, { verbose: options.verbose }));
13744
14356
  printDisclosureHint(results.length - visible.length, "hooks info <name>");
13745
14357
  });
13746
- program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-g, --global", "Remove from global settings", false).option("-p, --project", "Remove from project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex, all (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("Remove an installed hook (settings registration + store + lock + DB)").action(async (hook, options) => {
14358
+ program3.command("remove").alias("rm").argument("<hook>", "Hook to remove").option("-g, --global", "Remove from global settings", false).option("-p, --project", "Remove from project settings", false).option("-t, --target <target>", "Agent target: claude, gemini, codewith, codex, all (default: claude)", "claude").option("-j, --json", "Output as JSON", false).description("Remove an installed hook (settings registration + store + lock + DB)").action(async (hook, options) => {
13747
14359
  const scope = resolveScope(options);
13748
14360
  const target = resolveTarget(options);
13749
14361
  const { uninstallHook: uninstallHook2 } = await Promise.resolve().then(() => (init_installer(), exports_installer));
@@ -13779,7 +14391,7 @@ program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").opti
13779
14391
  console.log(chalk2.red(`\u2717 Hook '${hook}' not found${hint}`));
13780
14392
  }
13781
14393
  });
13782
- program2.command("categories").option("-j, --json", "Output as JSON", false).description("List all categories").action((options) => {
14394
+ program3.command("categories").option("-j, --json", "Output as JSON", false).description("List all categories").action((options) => {
13783
14395
  if (options.json) {
13784
14396
  const result = CATEGORIES.map((cat) => ({
13785
14397
  name: cat,
@@ -13796,7 +14408,7 @@ Categories:
13796
14408
  console.log(` ${category} (${count})`);
13797
14409
  }
13798
14410
  });
13799
- program2.command("info").argument("<hook>", "Hook name").option("-j, --json", "Output as JSON", false).description("Show detailed info about a hook").action(async (hook, options) => {
14411
+ program3.command("info").argument("<hook>", "Hook name").option("-j, --json", "Output as JSON", false).description("Show detailed info about a hook").action(async (hook, options) => {
13800
14412
  const { resolveHookMeta: resolveHookMeta2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
13801
14413
  const meta = resolveHookMeta2(hook) ?? getHook(hook);
13802
14414
  if (!meta) {
@@ -13844,7 +14456,7 @@ ${meta.displayName}
13844
14456
  console.log(chalk2.dim(" \u25CB Not installed in project"));
13845
14457
  }
13846
14458
  });
13847
- program2.command("doctor").option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-f, --fix", "Remove stale registrations (a .bak is written beside each settings file)", false).option("-j, --json", "Output as JSON", false).description("Check health of installed hooks; --fix removes registrations whose hook cannot resolve").action((options) => {
14459
+ program3.command("doctor").option("-g, --global", "Check global settings", false).option("-p, --project", "Check project settings", false).option("-f, --fix", "Remove stale registrations (a .bak is written beside each settings file)", false).option("-j, --json", "Output as JSON", false).description("Check health of installed hooks; --fix removes registrations whose hook cannot resolve").action((options) => {
13848
14460
  const scope = resolveScope(options);
13849
14461
  const settingsPath = getSettingsPath(scope);
13850
14462
  const issues = [];
@@ -13859,7 +14471,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
13859
14471
  });
13860
14472
  }
13861
14473
  const staleNames = new Set(stale.map((finding) => finding.hook));
13862
- const settingsExist = existsSync16(settingsPath);
14474
+ const settingsExist = existsSync17(settingsPath);
13863
14475
  if (!settingsExist) {
13864
14476
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
13865
14477
  }
@@ -13867,7 +14479,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
13867
14479
  let wiringCount = 0;
13868
14480
  if (settingsExist) {
13869
14481
  try {
13870
- const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
14482
+ const settings = JSON.parse(readFileSync13(settingsPath, "utf-8"));
13871
14483
  wiringCount = countSettingsWiring(settings);
13872
14484
  } catch {}
13873
14485
  }
@@ -13878,7 +14490,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
13878
14490
  const meta = custom ? resolveHookMeta(name) : getHook(name);
13879
14491
  let hookHealthy = true;
13880
14492
  if (custom) {
13881
- if (!existsSync16(custom.scriptPath)) {
14493
+ if (!existsSync17(custom.scriptPath)) {
13882
14494
  issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
13883
14495
  hookHealthy = false;
13884
14496
  }
@@ -13889,15 +14501,15 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
13889
14501
  continue;
13890
14502
  }
13891
14503
  const hookDir = getHookPath(name);
13892
- const hookScript = join17(hookDir, "src", "hook.ts");
13893
- if (!existsSync16(hookScript)) {
14504
+ const hookScript = join18(hookDir, "src", "hook.ts");
14505
+ if (!existsSync17(hookScript)) {
13894
14506
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
13895
14507
  hookHealthy = false;
13896
14508
  }
13897
14509
  }
13898
14510
  if (meta && settingsExist) {
13899
14511
  try {
13900
- const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
14512
+ const settings = JSON.parse(readFileSync13(settingsPath, "utf-8"));
13901
14513
  if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
13902
14514
  const eventName = meta.event.split(":")[0];
13903
14515
  issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
@@ -13984,7 +14596,7 @@ Hook Health Check (${scope})
13984
14596
  }
13985
14597
  console.log();
13986
14598
  });
13987
- program2.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-g, --global", "Update global hooks", false).option("-p, --project", "Update project hooks", false).option("-j, --json", "Output as JSON", false).description("Re-register hooks (picks up new package version), refresh lock pins, or install a pinned registry version (<name>@<version>)").action(async (hooks, options) => {
14599
+ program3.command("update").argument("[hooks...]", "Hooks to update (defaults to all installed)").option("-g, --global", "Update global hooks", false).option("-p, --project", "Update project hooks", false).option("-j, --json", "Output as JSON", false).description("Re-register hooks (picks up new package version), refresh lock pins, or install a pinned registry version (<name>@<version>)").action(async (hooks, options) => {
13988
14600
  const scope = resolveScope(options);
13989
14601
  const installed = getInstalledHooks(scope);
13990
14602
  const toUpdate = hooks.length > 0 ? hooks : installed;
@@ -14024,7 +14636,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
14024
14636
  if (pinned) {
14025
14637
  try {
14026
14638
  const transport = resolveHooksTransport2();
14027
- const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority.apiKey) : await installPinnedFromBundled2(pinned.name, pinned.version);
14639
+ const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority) : await installPinnedFromBundled2(pinned.name, pinned.version);
14028
14640
  const registered = installHook(pinnedInstall.name, { scope, overwrite: true });
14029
14641
  if (!registered.success) {
14030
14642
  results.push({ hook: arg, success: false, error: registered.error ?? "registration failed" });
@@ -14065,10 +14677,10 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
14065
14677
  }
14066
14678
  if (options.json) {
14067
14679
  console.log(JSON.stringify({
14068
- updated: results.filter((r) => r.success).map((r) => r.hook),
14069
- failed: results.filter((r) => !r.success).map((r) => ({ hook: r.hook, error: r.error }))
14680
+ updated: results.filter((r2) => r2.success).map((r2) => r2.hook),
14681
+ failed: results.filter((r2) => !r2.success).map((r2) => ({ hook: r2.hook, error: r2.error }))
14070
14682
  }));
14071
- if (results.some((r) => !r.success)) {
14683
+ if (results.some((r2) => !r2.success)) {
14072
14684
  process.exitCode = 1;
14073
14685
  }
14074
14686
  return;
@@ -14083,11 +14695,11 @@ Updating hooks...
14083
14695
  console.log(chalk2.red(`\u2717 ${result.hook}: ${result.error}`));
14084
14696
  }
14085
14697
  }
14086
- if (results.some((r) => !r.success)) {
14698
+ if (results.some((r2) => !r2.success)) {
14087
14699
  process.exitCode = 1;
14088
14700
  }
14089
14701
  });
14090
- program2.command("docs").argument("[hook]", "Hook name (shows general docs if omitted)").option("--verbose", "Print full hook README content", false).option("-j, --json", "Output as JSON", false).description("Show documentation for hooks").action((hook, options) => {
14702
+ program3.command("docs").argument("[hook]", "Hook name (shows general docs if omitted)").option("--verbose", "Print full hook README content", false).option("-j, --json", "Output as JSON", false).description("Show documentation for hooks").action((hook, options) => {
14091
14703
  if (hook) {
14092
14704
  const meta = getHook(hook);
14093
14705
  if (!meta) {
@@ -14100,10 +14712,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
14100
14712
  return;
14101
14713
  }
14102
14714
  const hookPath = getHookPath(hook);
14103
- const readmePath = join17(hookPath, "README.md");
14715
+ const readmePath = join18(hookPath, "README.md");
14104
14716
  let readme = "";
14105
- if (existsSync16(readmePath)) {
14106
- readme = readFileSync12(readmePath, "utf-8");
14717
+ if (existsSync17(readmePath)) {
14718
+ readme = readFileSync13(readmePath, "utf-8");
14107
14719
  }
14108
14720
  if (options.json) {
14109
14721
  console.log(JSON.stringify({ ...meta, readme }));
@@ -14221,7 +14833,7 @@ ${meta.displayName} v${meta.version}
14221
14833
  console.log(` hooks docs --json Machine-readable documentation`);
14222
14834
  console.log();
14223
14835
  });
14224
- program2.command("upgrade").option("-c, --check", "Check for updates without installing", false).option("-j, --json", "Output as JSON", false).description("Update the @hasna/hooks package to the latest version").action(async (options) => {
14836
+ program3.command("upgrade").option("-c, --check", "Check for updates without installing", false).option("-j, --json", "Output as JSON", false).description("Update the @hasna/hooks package to the latest version").action(async (options) => {
14225
14837
  const current2 = pkg2.version;
14226
14838
  let pm = "npm";
14227
14839
  try {
@@ -14283,7 +14895,7 @@ Upgrading @hasna/hooks (${pm})...
14283
14895
  \u2713 Upgraded: ${current2} \u2192 ${latest}`));
14284
14896
  }
14285
14897
  });
14286
- program2.command("profile-export").description("Export all agent profiles as JSON (for backup/cross-machine setup)").option("-o, --output <file>", "Write to file instead of stdout").option("-j, --json", "Output as JSON (default: true)", false).action(async (options) => {
14898
+ program3.command("profile-export").description("Export all agent profiles as JSON (for backup/cross-machine setup)").option("-o, --output <file>", "Write to file instead of stdout").option("-j, --json", "Output as JSON (default: true)", false).action(async (options) => {
14287
14899
  const profiles = exportProfiles();
14288
14900
  const json2 = JSON.stringify(profiles, null, 2);
14289
14901
  if (options.output) {
@@ -14295,14 +14907,14 @@ program2.command("profile-export").description("Export all agent profiles as JSO
14295
14907
  console.log(json2);
14296
14908
  }
14297
14909
  });
14298
- program2.command("profile-import").argument("<file>", "JSON file to import profiles from (use - for stdin)").description("Import agent profiles from a JSON export file").option("-j, --json", "Output result as JSON", false).action(async (file, options) => {
14910
+ program3.command("profile-import").argument("<file>", "JSON file to import profiles from (use - for stdin)").description("Import agent profiles from a JSON export file").option("-j, --json", "Output result as JSON", false).action(async (file, options) => {
14299
14911
  let raw;
14300
14912
  if (file === "-") {
14301
14913
  raw = await new Response(Bun.stdin.stream()).text();
14302
14914
  } else {
14303
- const { readFileSync: readFileSync13 } = await import("fs");
14915
+ const { readFileSync: readFileSync14 } = await import("fs");
14304
14916
  try {
14305
- raw = readFileSync13(file, "utf-8");
14917
+ raw = readFileSync14(file, "utf-8");
14306
14918
  } catch {
14307
14919
  if (options.json) {
14308
14920
  console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
@@ -14333,7 +14945,7 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
14333
14945
  console.log(chalk2.dim(` Skipped ${result.skipped} (already exist or invalid)`));
14334
14946
  }
14335
14947
  });
14336
- var logCmd = program2.command("log").description(`Query hook event logs from SQLite (${getReportedDbPath()})`);
14948
+ var logCmd = program3.command("log").description(`Query hook event logs from SQLite (${getReportedDbPath()})`);
14337
14949
  logCmd.command("list").description("List hook events").option("--hook <name>", "Filter by hook name").option("--session <id>", "Filter by session ID").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (options) => {
14338
14950
  const db = await openLocalStoreOrExit(options.json);
14339
14951
  const limit = parseInt(options.limit) || 50;
@@ -14428,16 +15040,16 @@ logCmd.command("errors").description("Show hook events that contain errors").opt
14428
15040
  const m = s.match(/^(\d+)(s|m|h|d)$/);
14429
15041
  if (!m)
14430
15042
  return 86400000;
14431
- const n = parseInt(m[1]);
15043
+ const n2 = parseInt(m[1]);
14432
15044
  switch (m[2]) {
14433
15045
  case "s":
14434
- return n * 1000;
15046
+ return n2 * 1000;
14435
15047
  case "m":
14436
- return n * 60 * 1000;
15048
+ return n2 * 60 * 1000;
14437
15049
  case "h":
14438
- return n * 60 * 60 * 1000;
15050
+ return n2 * 60 * 60 * 1000;
14439
15051
  case "d":
14440
- return n * 24 * 60 * 60 * 1000;
15052
+ return n2 * 24 * 60 * 60 * 1000;
14441
15053
  default:
14442
15054
  return 86400000;
14443
15055
  }
@@ -14484,7 +15096,7 @@ logCmd.command("clear").description("Delete hook event logs").option("--hook <na
14484
15096
  }
14485
15097
  console.log(chalk2.green(`\u2713 Cleared ${count} event(s).`));
14486
15098
  });
14487
- var storageCmd = program2.command("storage").description("Sync local hook data with storage PostgreSQL");
15099
+ var storageCmd = program3.command("storage").description("Sync local hook data with storage PostgreSQL");
14488
15100
  storageCmd.command("status").description("Show storage sync status").option("-j, --json", "Output as JSON", false).action(async (options) => {
14489
15101
  try {
14490
15102
  const { getStorageStatus: getStorageStatus2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
@@ -14567,7 +15179,7 @@ storageCmd.command("sync").description("Bidirectional storage sync: pull then pu
14567
15179
  process.exitCode = 1;
14568
15180
  }
14569
15181
  });
14570
- program2.command("trust").argument("<hook>", "Hook to trust").option("-j, --json", "Output as JSON", false).description("Trust the current script content of a hook (re-pins its sha256)").action(async (hook, options) => {
15182
+ program3.command("trust").argument("<hook>", "Hook to trust").option("-j, --json", "Output as JSON", false).description("Trust the current script content of a hook (re-pins its sha256)").action(async (hook, options) => {
14571
15183
  const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
14572
15184
  const { retrustHook: retrustHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
14573
15185
  const resolved = resolveHook2(hook);
@@ -14586,14 +15198,14 @@ program2.command("trust").argument("<hook>", "Hook to trust").option("-j, --json
14586
15198
  console.log(chalk2.green(`\u2713 Trusted '${resolved.name}' v${resolved.version} (${resolved.source})`));
14587
15199
  console.log(chalk2.dim(` sha256 ${result.actual}`));
14588
15200
  });
14589
- program2.command("serve").option("-p, --port <port>", "Port (default 39428)").option("--host <host>", "Host to bind (default 127.0.0.1)").description("Serve the local hook registry over HTTP (catalog, artifacts, lock). The publish API key resolves from the @hasna/contracts chain per request (HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials) \u2014 there is deliberately no --api-key flag (P1-8).").action(async (options) => {
15201
+ program3.command("serve").option("-p, --port <port>", "Port (default 39428)").option("--host <host>", "Host to bind (default 127.0.0.1)").description("Serve the local hook registry over HTTP (catalog, artifacts, lock). The publish API key resolves from the @hasna/contracts chain per request (HASNA_HOOKS_API_KEY, the Keychain item hasna.credentials.hooks.api-key, or ~/.hasna/hooks/config/credentials) \u2014 there is deliberately no --api-key flag (P1-8).").action(async (options) => {
14590
15202
  const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
14591
15203
  startServeServer2({
14592
15204
  port: options.port ? parseInt(options.port, 10) : undefined,
14593
15205
  host: options.host
14594
15206
  });
14595
15207
  });
14596
- program2.command("sync").option("--dry-run", "Print the plan without changing anything", false).option("-j, --json", "Output as JSON", false).description("Sync local hooks with the remote registry (or bundled registry when no registry credential resolves and local mode is opted into)").action(async (options) => {
15208
+ program3.command("sync").option("--dry-run", "Print the plan without changing anything", false).option("-j, --json", "Output as JSON", false).description("Sync local hooks with the remote registry (or bundled registry when no registry credential resolves and local mode is opted into)").action(async (options) => {
14597
15209
  const { syncHooks: syncHooks2, planSync: planSync2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
14598
15210
  try {
14599
15211
  const plan = options.dryRun ? await syncHooks2({ dryRun: true }) : await syncHooks2();
@@ -14637,7 +15249,7 @@ Hook sync (${source})${plan.dryRun ? " \u2014 dry run" : ""}
14637
15249
  process.exitCode = 1;
14638
15250
  }
14639
15251
  });
14640
- program2.command("cf").description("Provision and deploy the hooks registry to Cloudflare").option("--deploy", "Provision D1 + R2 via the Cloudflare API", true).option("--dry-run", "Print the plan without calling the Cloudflare API", false).option("--account-id <id>", "Cloudflare account id (defaults to CF_ACCOUNT_ID env)").option("--database-name <name>", "D1 database name (default hooks-registry)").option("--bucket-name <name>", "R2 bucket name (default hooks-registry-artifacts)").option("--token-file <path>", "Read the API token from a file (defaults to CF_API_TOKEN env)").option("-j, --json", "Output as JSON", false).action(async (options) => {
15252
+ program3.command("cf").description("Provision and deploy the hooks registry to Cloudflare").option("--deploy", "Provision D1 + R2 via the Cloudflare API", true).option("--dry-run", "Print the plan without calling the Cloudflare API", false).option("--account-id <id>", "Cloudflare account id (defaults to CF_ACCOUNT_ID env)").option("--database-name <name>", "D1 database name (default hooks-registry)").option("--bucket-name <name>", "R2 bucket name (default hooks-registry-artifacts)").option("--token-file <path>", "Read the API token from a file (defaults to CF_API_TOKEN env)").option("-j, --json", "Output as JSON", false).action(async (options) => {
14641
15253
  const token = readToken(options.tokenFile);
14642
15254
  const accountId = options.accountId ?? process.env.CF_ACCOUNT_ID;
14643
15255
  if (!options.dryRun) {
@@ -14686,7 +15298,7 @@ Cloudflare provisioning complete:
14686
15298
  console.log(chalk2.dim(" wrangler.toml, fill in the D1 database id, then run the commands above."));
14687
15299
  console.log();
14688
15300
  });
14689
- program2.command("migrate").description("Apply PostgreSQL migrations to the storage database (HASNA_HOOKS_DATABASE_URL)").option("-j, --json", "Output as JSON", false).action(async (options) => {
15301
+ program3.command("migrate").description("Apply PostgreSQL migrations to the storage database (HASNA_HOOKS_DATABASE_URL)").option("-j, --json", "Output as JSON", false).action(async (options) => {
14690
15302
  try {
14691
15303
  const { getStoragePg: getStoragePg2, runStorageMigrations: runStorageMigrations2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
14692
15304
  const remote = await getStoragePg2();
@@ -14709,7 +15321,7 @@ program2.command("migrate").description("Apply PostgreSQL migrations to the stor
14709
15321
  process.exitCode = 1;
14710
15322
  }
14711
15323
  });
14712
- program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process per agent)", false).option("--sse", "Use legacy SSE transport (port 39427)", false).option("--http", "Use Streamable HTTP transport (explicit; this is also the default)", false).option("-p, --port <port>", "Port for HTTP/SSE transport (defaults to 8847 for HTTP, 39427 for SSE)").description("Start MCP server for AI agent integration (default: shared Streamable HTTP)").action(async (options) => {
15324
+ program3.command("mcp").option("-s, --stdio", "Use stdio transport (one process per agent)", false).option("--sse", "Use legacy SSE transport (port 39427)", false).option("--http", "Use Streamable HTTP transport (explicit; this is also the default)", false).option("-p, --port <port>", "Port for HTTP/SSE transport (defaults to 8847 for HTTP, 39427 for SSE)").description("Start MCP server for AI agent integration (default: shared Streamable HTTP)").action(async (options) => {
14713
15325
  if (options.stdio) {
14714
15326
  const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
14715
15327
  await startStdioServer2();
@@ -14730,8 +15342,20 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
14730
15342
  startMcpHttpServer2({ name: "hooks", port: resolveMcpHttpPort2(args), buildServer: createHooksServer2 });
14731
15343
  }
14732
15344
  });
14733
- registerEventsCommands(program2, { source: "hooks" });
15345
+ registerEventsCommands(program3, { source: "hooks" });
15346
+ program3.command("safety").description("Register bundled native deletion guards without a registry credential").command("install <guard>").option("--target <target>", "codex or claude", "codex").option("--scope <scope>", "global or project", "global").option("--overwrite", "Replace this guard in place; native trust is still required").action((guard, options) => {
15347
+ if (!isNativeSafetyName(guard) || !["codex", "claude"].includes(options.target) || !["global", "project"].includes(options.scope)) {
15348
+ console.error("Unsupported bundled safety registration");
15349
+ process.exitCode = 1;
15350
+ return;
15351
+ }
15352
+ const result = installHook(guard, { target: options.target, scope: options.scope, overwrite: options.overwrite });
15353
+ console.log(JSON.stringify(result));
15354
+ if (!result.success)
15355
+ process.exitCode = 1;
15356
+ });
14734
15357
  var API_INDEPENDENT_COMMANDS = new Set([
15358
+ "safety",
14735
15359
  "serve",
14736
15360
  "mcp",
14737
15361
  "cf",
@@ -14792,4 +15416,4 @@ function enforceTransportGate() {
14792
15416
  refuseLocalStore(hooksHostedRouteLocalStoreRefusal(transport.authority.v1BaseUrl));
14793
15417
  }
14794
15418
  enforceTransportGate();
14795
- program2.parse();
15419
+ program3.parse();