@hasna/hooks 0.9.5 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
  }
@@ -6355,15 +6518,15 @@ var init_profiles = __esm(() => {
6355
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 as fstatSync2, openSync as openSync2, readFileSync as readFileSync22 } from "fs";
6521
+ import { closeSync as closeSync22, fstatSync as fstatSync2, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
6359
6522
  import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
6360
6523
  import { hostname as osHostname } from "os";
6361
- import { isAbsolute as isAbsolute2, join as join22 } from "path";
6524
+ import { isAbsolute as isAbsolute3, join as join22 } from "path";
6362
6525
  import { createRequire as createRequire2 } from "module";
6363
6526
  import { fileURLToPath as fileURLToPath22 } from "url";
6364
- import { closeSync as closeSync3, fstatSync, lstatSync as lstatSync2, openSync as openSync3, readFileSync as readFileSync7, realpathSync as realpathSync2, statSync } from "fs";
6527
+ import { closeSync as closeSync3, fstatSync as fstatSync3, lstatSync as lstatSync3, openSync as openSync3, readFileSync as readFileSync8, realpathSync as realpathSync3, statSync } from "fs";
6365
6528
  import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
6366
- import { basename as basename2, dirname as dirname5, join as join12, relative, sep as sep2 } from "path";
6529
+ import { basename as basename2, dirname as dirname6, join as join13, relative, sep as sep2 } from "path";
6367
6530
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
6368
6531
  function envToken(name) {
6369
6532
  return name.toUpperCase().replace(/-/g, "_");
@@ -6437,14 +6600,14 @@ function o(e2, r2, t2) {
6437
6600
  }
6438
6601
  }
6439
6602
  function entryPoint(directory) {
6440
- const root = realpathSync2(directory);
6441
- const fd = openSync3(join12(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
6603
+ const root = realpathSync3(directory);
6604
+ const fd = openSync3(join13(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
6442
6605
  let pkg;
6443
6606
  try {
6444
- const stat = fstatSync(fd);
6607
+ const stat = fstatSync3(fd);
6445
6608
  if (!stat.isFile() || stat.size > MAX_PACKAGE_BYTES2)
6446
6609
  throw new Error("Invalid Secrets package metadata");
6447
- pkg = JSON.parse(readFileSync7(fd, "utf8"));
6610
+ pkg = JSON.parse(readFileSync8(fd, "utf8"));
6448
6611
  } finally {
6449
6612
  closeSync3(fd);
6450
6613
  }
@@ -6458,7 +6621,7 @@ function entryPoint(directory) {
6458
6621
  if (/[\\%?#\0]/.test(target) || parts.some((part) => !part || part === "." || part === ".." || part === "node_modules")) {
6459
6622
  throw new Error("Invalid Secrets import export");
6460
6623
  }
6461
- const file = realpathSync2(join12(root, ...parts));
6624
+ const file = realpathSync3(join13(root, ...parts));
6462
6625
  const within = relative(root, file);
6463
6626
  if (!within || within === ".." || within.startsWith(`..${sep2}`) || !statSync(file).isFile()) {
6464
6627
  throw new Error("Secrets import export escapes its package or is not a file");
@@ -6466,13 +6629,13 @@ function entryPoint(directory) {
6466
6629
  return pathToFileURL(file).href;
6467
6630
  }
6468
6631
  function resolveInstalledSecrets(parentUrl) {
6469
- let directory = dirname5(fileURLToPath3(parentUrl));
6632
+ let directory = dirname6(fileURLToPath3(parentUrl));
6470
6633
  for (;; ) {
6471
6634
  if (basename2(directory) !== "node_modules") {
6472
- const candidate = join12(directory, "node_modules", PACKAGE);
6635
+ const candidate = join13(directory, "node_modules", PACKAGE);
6473
6636
  let absent = false;
6474
6637
  try {
6475
- lstatSync2(candidate);
6638
+ lstatSync3(candidate);
6476
6639
  } catch (error) {
6477
6640
  if (error.code !== "ENOENT")
6478
6641
  throw error;
@@ -6481,7 +6644,7 @@ function resolveInstalledSecrets(parentUrl) {
6481
6644
  if (!absent)
6482
6645
  return entryPoint(candidate);
6483
6646
  }
6484
- const parent = dirname5(directory);
6647
+ const parent = dirname6(directory);
6485
6648
  if (parent === directory)
6486
6649
  throw new Error("Secrets SDK is not installed for this consumer");
6487
6650
  directory = parent;
@@ -6493,7 +6656,7 @@ function homeDir(env) {
6493
6656
  }
6494
6657
  function absoluteOverride(env, key) {
6495
6658
  const value = env[key]?.trim();
6496
- return value && isAbsolute2(value) ? value : null;
6659
+ return value && isAbsolute3(value) ? value : null;
6497
6660
  }
6498
6661
  function hasnaHomeDir(env) {
6499
6662
  const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
@@ -6539,9 +6702,9 @@ function parseEnvFile(text) {
6539
6702
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
6540
6703
  continue;
6541
6704
  let value = withoutExport.slice(equals + 1).trim();
6542
- const quote = value[0];
6543
- if (quote === '"' || quote === "'") {
6544
- if (value.length < 2 || !value.endsWith(quote)) {
6705
+ const quote2 = value[0];
6706
+ if (quote2 === '"' || quote2 === "'") {
6707
+ if (value.length < 2 || !value.endsWith(quote2)) {
6545
6708
  unusable.add(key);
6546
6709
  continue;
6547
6710
  }
@@ -6570,7 +6733,7 @@ function readAppConfigFile(path) {
6570
6733
  };
6571
6734
  let fd = -1;
6572
6735
  try {
6573
- fd = openSync2(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
6736
+ fd = openSync22(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
6574
6737
  } catch (error) {
6575
6738
  const code = error.code;
6576
6739
  if (code === "ENOENT" || code === "ENOTDIR")
@@ -6599,7 +6762,7 @@ function readAppConfigFile(path) {
6599
6762
  return parseEnvFile(bytes.toString("utf8"));
6600
6763
  } finally {
6601
6764
  if (fd !== -1)
6602
- closeSync2(fd);
6765
+ closeSync22(fd);
6603
6766
  }
6604
6767
  }
6605
6768
  function readCredentialFile(path, apiKeyKeys, pointerKey) {
@@ -7600,12 +7763,12 @@ var init_db_writer = __esm(() => {
7600
7763
  });
7601
7764
 
7602
7765
  // src/lib/hook-env.ts
7603
- import { dirname as dirname6, isAbsolute as isAbsolute3 } from "path";
7604
- import { existsSync as existsSync13, realpathSync as realpathSync3, statSync as statSync2 } 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";
7605
7768
  import { tmpdir } from "os";
7606
7769
  function isDeniedEnvName(name) {
7607
7770
  const upper = name.toUpperCase();
7608
- if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
7771
+ if (DENY_PREFIXES.some((prefix2) => upper.startsWith(prefix2)))
7609
7772
  return true;
7610
7773
  if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
7611
7774
  return true;
@@ -7614,13 +7777,13 @@ function isDeniedEnvName(name) {
7614
7777
  function isInterpreterInjectionEnvName(name) {
7615
7778
  if (INTERPRETER_INJECTION.has(name))
7616
7779
  return true;
7617
- return INTERPRETER_INJECTION_PREFIXES.some((prefix) => name.startsWith(prefix));
7780
+ return INTERPRETER_INJECTION_PREFIXES.some((prefix2) => name.startsWith(prefix2));
7618
7781
  }
7619
7782
  function isBashFunctionEnvName(name) {
7620
7783
  return name.startsWith("BASH_FUNC_");
7621
7784
  }
7622
7785
  function isUnsafePathEntry(entry, home) {
7623
- if (entry === "" || !isAbsolute3(entry))
7786
+ if (entry === "" || !isAbsolute4(entry))
7624
7787
  return true;
7625
7788
  if (home && (entry === home || entry.startsWith(home + "/")))
7626
7789
  return true;
@@ -7630,7 +7793,7 @@ function isUnsafePathEntry(entry, home) {
7630
7793
  return true;
7631
7794
  const canonical = (path) => {
7632
7795
  try {
7633
- return realpathSync3(path);
7796
+ return realpathSync4(path);
7634
7797
  } catch {
7635
7798
  return path;
7636
7799
  }
@@ -7649,9 +7812,9 @@ function isUnsafePathEntry(entry, home) {
7649
7812
  }
7650
7813
  function sanitizeHookPath(pathValue, home) {
7651
7814
  const baseline = PATH_BASELINE.slice();
7652
- if (process.platform === "darwin" && existsSync13("/opt/homebrew/bin"))
7815
+ if (process.platform === "darwin" && existsSync14("/opt/homebrew/bin"))
7653
7816
  baseline.push("/opt/homebrew/bin");
7654
- const bunDir = dirname6(process.execPath);
7817
+ const bunDir = dirname7(process.execPath);
7655
7818
  const seen = new Set;
7656
7819
  const out = [];
7657
7820
  for (const dir of baseline) {
@@ -7776,7 +7939,7 @@ __export(exports_run, {
7776
7939
  });
7777
7940
  import { randomBytes as randomBytes2 } from "crypto";
7778
7941
  import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
7779
- import { basename as basename3, dirname as dirname7, isAbsolute as isAbsolute4, join as join13 } from "path";
7942
+ import { basename as basename3, dirname as dirname8, isAbsolute as isAbsolute5, join as join14 } from "path";
7780
7943
  function extensionOf(scriptPath) {
7781
7944
  const base = basename3(scriptPath);
7782
7945
  const dot = base.lastIndexOf(".");
@@ -7845,9 +8008,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
7845
8008
  return chunks.join("");
7846
8009
  }
7847
8010
  async function executeVerifiedScript(options) {
7848
- const scriptDir = dirname7(options.scriptPath);
8011
+ const scriptDir = dirname8(options.scriptPath);
7849
8012
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
7850
- 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}`);
7851
8014
  let proc = null;
7852
8015
  let timedOut = false;
7853
8016
  const timeoutTimer = options.timeout ? setTimeout(() => {
@@ -7856,7 +8019,7 @@ async function executeVerifiedScript(options) {
7856
8019
  }, options.timeout) : null;
7857
8020
  try {
7858
8021
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
7859
- proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
8022
+ proc = Bun.spawn([...interpreter.command, isAbsolute5(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
7860
8023
  stdin: new Response(options.stdin),
7861
8024
  stdout: "pipe",
7862
8025
  stderr: "pipe",
@@ -7938,15 +8101,15 @@ __export(exports_custom_install, {
7938
8101
  installCustomSource: () => installCustomSource,
7939
8102
  ensureCustomDir: () => ensureCustomDir
7940
8103
  });
7941
- import { existsSync as existsSync14, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync3 } from "fs";
7942
- 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";
7943
8106
  import { tmpdir as tmpdir2 } from "os";
7944
8107
  function manifestAt(dir) {
7945
- const path = join14(dir, "manifest.json");
7946
- if (!existsSync14(path))
8108
+ const path = join15(dir, "manifest.json");
8109
+ if (!existsSync15(path))
7947
8110
  return;
7948
8111
  try {
7949
- return parseManifest(readFileSync8(path, "utf-8"));
8112
+ return parseManifest(readFileSync9(path, "utf-8"));
7950
8113
  } catch {
7951
8114
  return;
7952
8115
  }
@@ -7965,8 +8128,8 @@ function writeFromManifest(manifest, srcDir, source, kind) {
7965
8128
  return { name, version: manifest.version, kind, source, dir, scriptPath };
7966
8129
  }
7967
8130
  async function cloneToTemp(gitUrl) {
7968
- const tmp = mkdtempSync(join14(tmpdir2(), "hooks-install-"));
7969
- 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")], {
7970
8133
  stdout: "pipe",
7971
8134
  stderr: "pipe"
7972
8135
  });
@@ -7979,7 +8142,7 @@ async function cloneToTemp(gitUrl) {
7979
8142
  rmSync3(tmp, { recursive: true, force: true });
7980
8143
  throw new Error(`git clone failed (${exitCode}): ${stderr.trim()}`);
7981
8144
  }
7982
- return join14(tmp, "repo");
8145
+ return join15(tmp, "repo");
7983
8146
  }
7984
8147
  function isGitUrl(value) {
7985
8148
  return value.startsWith("git@") || value.startsWith("ssh://") || value.startsWith("file://") || value.endsWith(".git");
@@ -7989,7 +8152,7 @@ function isCustomSource(value) {
7989
8152
  return true;
7990
8153
  if (value.startsWith("http://") || value.startsWith("https://"))
7991
8154
  return true;
7992
- return existsSync14(value);
8155
+ return existsSync15(value);
7993
8156
  }
7994
8157
  async function installCustomSource(source) {
7995
8158
  if (isGitUrl(source)) {
@@ -8003,11 +8166,11 @@ async function installCustomSource(source) {
8003
8166
  if (source.startsWith("http://") || source.startsWith("https://")) {
8004
8167
  return installManifestUrl(source);
8005
8168
  }
8006
- if (!existsSync14(source)) {
8169
+ if (!existsSync15(source)) {
8007
8170
  throw new Error(`No such local path: ${source}`);
8008
8171
  }
8009
8172
  if (source.endsWith("manifest.json")) {
8010
- const manifest = parseManifest(readFileSync8(source, "utf-8"));
8173
+ const manifest = parseManifest(readFileSync9(source, "utf-8"));
8011
8174
  return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
8012
8175
  }
8013
8176
  return copyManifestDir(source, source, "local");
@@ -8056,13 +8219,13 @@ __export(exports_sync, {
8056
8219
  fetchPinnedHook: () => fetchPinnedHook,
8057
8220
  commitSyncArtifacts: () => commitSyncArtifacts
8058
8221
  });
8059
- import { createHash as createHash2 } from "crypto";
8060
- import { readFileSync as readFileSync9 } from "fs";
8222
+ import { createHash as createHash3 } from "crypto";
8223
+ import { readFileSync as readFileSync10 } from "fs";
8061
8224
  function sha256OfText(text) {
8062
- return createHash2("sha256").update(text).digest("hex");
8225
+ return createHash3("sha256").update(text).digest("hex");
8063
8226
  }
8064
8227
  function sha256OfPath(path) {
8065
- return createHash2("sha256").update(readFileSync9(path)).digest("hex");
8228
+ return createHash3("sha256").update(readFileSync10(path)).digest("hex");
8066
8229
  }
8067
8230
  function collectBundledCatalog() {
8068
8231
  const out = [];
@@ -8856,13 +9019,12 @@ function secureEqual(a, b) {
8856
9019
  }
8857
9020
 
8858
9021
  // src/openapi.ts
8859
- var openApiDocument;
8860
- var init_openapi = __esm(() => {
8861
- openApiDocument = {
9022
+ function buildOpenApiDocument(version) {
9023
+ return {
8862
9024
  openapi: "3.1.0",
8863
9025
  info: {
8864
9026
  title: "Hooks Registry API",
8865
- version: "0.1.0",
9027
+ version,
8866
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)."
8867
9029
  },
8868
9030
  servers: [{ url: "/" }],
@@ -8876,7 +9038,7 @@ var init_openapi = __esm(() => {
8876
9038
  description: "Service is alive",
8877
9039
  content: {
8878
9040
  "application/json": {
8879
- schema: { type: "object", additionalProperties: true }
9041
+ schema: { $ref: "#/components/schemas/HooksHealth" }
8880
9042
  }
8881
9043
  }
8882
9044
  }
@@ -8888,8 +9050,22 @@ var init_openapi = __esm(() => {
8888
9050
  summary: "Readiness probe \u2014 the hooks.lock store is readable",
8889
9051
  operationId: "getReady",
8890
9052
  responses: {
8891
- "200": { description: "Lock store is readable" },
8892
- "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
+ }
8893
9069
  }
8894
9070
  }
8895
9071
  },
@@ -8902,7 +9078,7 @@ var init_openapi = __esm(() => {
8902
9078
  description: "The installed @hasna/hooks version",
8903
9079
  content: {
8904
9080
  "application/json": {
8905
- schema: { type: "object", properties: { version: { type: "string" } } }
9081
+ schema: { $ref: "#/components/schemas/HooksVersion" }
8906
9082
  }
8907
9083
  }
8908
9084
  }
@@ -8913,7 +9089,16 @@ var init_openapi = __esm(() => {
8913
9089
  get: {
8914
9090
  summary: "This OpenAPI document",
8915
9091
  operationId: "getOpenApi",
8916
- 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
+ }
8917
9102
  }
8918
9103
  },
8919
9104
  "/api/v1/catalog": {
@@ -8925,23 +9110,7 @@ var init_openapi = __esm(() => {
8925
9110
  description: "Catalog entries",
8926
9111
  content: {
8927
9112
  "application/json": {
8928
- schema: {
8929
- type: "object",
8930
- properties: {
8931
- hooks: {
8932
- type: "array",
8933
- items: {
8934
- type: "object",
8935
- properties: {
8936
- name: { type: "string" },
8937
- version: { type: "string" },
8938
- sha256: { type: "string" },
8939
- events: { type: "array", items: { type: "string" } }
8940
- }
8941
- }
8942
- }
8943
- }
8944
- }
9113
+ schema: { $ref: "#/components/schemas/HooksCatalog" }
8945
9114
  }
8946
9115
  }
8947
9116
  }
@@ -8952,7 +9121,16 @@ var init_openapi = __esm(() => {
8952
9121
  get: {
8953
9122
  summary: "Read the published hooks.lock with per-hook versions",
8954
9123
  operationId: "getLock",
8955
- 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
+ }
8956
9134
  }
8957
9135
  },
8958
9136
  "/api/v1/hooks/{name}/{version}": {
@@ -8965,10 +9143,35 @@ var init_openapi = __esm(() => {
8965
9143
  ],
8966
9144
  responses: {
8967
9145
  "200": {
8968
- 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
+ }
8969
9158
  },
8970
- "400": { description: "Invalid URL encoding or semver version" },
8971
- "404": { description: "Hook not found locally" }
9159
+ "400": {
9160
+ description: "Invalid URL encoding or semver version",
9161
+ content: {
9162
+ "application/json": {
9163
+ schema: { $ref: "#/components/schemas/HooksError" }
9164
+ }
9165
+ }
9166
+ },
9167
+ "404": {
9168
+ description: "Hook not found locally",
9169
+ content: {
9170
+ "application/json": {
9171
+ schema: { $ref: "#/components/schemas/HooksError" }
9172
+ }
9173
+ }
9174
+ }
8972
9175
  }
8973
9176
  }
8974
9177
  },
@@ -8980,18 +9183,51 @@ var init_openapi = __esm(() => {
8980
9183
  required: true,
8981
9184
  content: {
8982
9185
  "application/json": {
8983
- schema: {
8984
- type: "object",
8985
- properties: { name: { type: "string" }, version: { type: "string" } }
8986
- }
9186
+ schema: { $ref: "#/components/schemas/HooksPublishRequest" }
8987
9187
  }
8988
9188
  }
8989
9189
  },
8990
9190
  responses: {
8991
- "200": { description: "Hook retrusted and pin updated" },
8992
- "401": { description: "Missing or invalid API key" },
8993
- "404": { description: "Hook not found in local store" },
8994
- "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
+ }
8995
9231
  },
8996
9232
  security: [{ apiKey: [] }]
8997
9233
  }
@@ -9004,10 +9240,135 @@ var init_openapi = __esm(() => {
9004
9240
  scheme: "bearer",
9005
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."
9006
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
+ }
9007
9368
  }
9008
9369
  }
9009
9370
  };
9010
- });
9371
+ }
9011
9372
 
9012
9373
  // src/serve.ts
9013
9374
  var exports_serve = {};
@@ -9019,13 +9380,13 @@ __export(exports_serve, {
9019
9380
  SERVE_HOST: () => SERVE_HOST,
9020
9381
  DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
9021
9382
  });
9022
- import { readFileSync as readFileSync10 } from "fs";
9023
- import { dirname as dirname8, join as join15 } from "path";
9383
+ import { readFileSync as readFileSync11 } from "fs";
9384
+ import { dirname as dirname9, join as join16 } from "path";
9024
9385
  import { fileURLToPath as fileURLToPath4 } from "url";
9025
9386
  function packageVersion() {
9026
9387
  try {
9027
- const pkgPath = join15(dirname8(fileURLToPath4(import.meta.url)), "..", "package.json");
9028
- 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";
9029
9390
  } catch {
9030
9391
  return "0.0.0";
9031
9392
  }
@@ -9114,7 +9475,7 @@ function handleServeRequest(req, apiKey) {
9114
9475
  return Promise.resolve(json({ version: packageVersion() }));
9115
9476
  }
9116
9477
  if (url.pathname === "/openapi.json" && req.method === "GET") {
9117
- return Promise.resolve(json(openApiDocument));
9478
+ return Promise.resolve(json(buildOpenApiDocument(packageVersion())));
9118
9479
  }
9119
9480
  if (url.pathname === "/api/v1/catalog" && req.method === "GET") {
9120
9481
  return buildCatalog().then((catalog) => json({ hooks: catalog }));
@@ -9199,7 +9560,6 @@ var init_serve = __esm(() => {
9199
9560
  init_store();
9200
9561
  init_resolve();
9201
9562
  init_transport2();
9202
- init_openapi();
9203
9563
  init_semver();
9204
9564
  if (false) {}
9205
9565
  });
@@ -9326,17 +9686,17 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
9326
9686
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9327
9687
  import { z as z2 } from "zod";
9328
9688
  import { createServer } from "http";
9329
- import { existsSync as existsSync15, readFileSync as readFileSync11 } from "fs";
9330
- import { join as join16, dirname as dirname9 } from "path";
9689
+ import { existsSync as existsSync16, readFileSync as readFileSync12 } from "fs";
9690
+ import { join as join17, dirname as dirname10 } from "path";
9331
9691
  import { fileURLToPath as fileURLToPath5 } from "url";
9332
9692
  async function runVerifiedHook(name, stdin, timeoutMs) {
9333
9693
  const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
9334
9694
  const resolved = resolveHook2(name);
9335
9695
  const hookScript = resolveScriptPath2(name);
9336
- if (!resolved || !hookScript || !existsSync15(hookScript)) {
9696
+ if (!resolved || !hookScript || !existsSync16(hookScript)) {
9337
9697
  throw new Error(`Hook '${name}' not found`);
9338
9698
  }
9339
- const content = readFileSync11(hookScript);
9699
+ const content = readFileSync12(hookScript);
9340
9700
  const sha = sha256Of(content);
9341
9701
  const check = checkScriptHash(name, sha);
9342
9702
  if (!check.ok) {
@@ -9541,7 +9901,7 @@ function createHooksServer() {
9541
9901
  const settingsPath = getSettingsPath(scope);
9542
9902
  const issues = [];
9543
9903
  const healthy = [];
9544
- const settingsExist = existsSync15(settingsPath);
9904
+ const settingsExist = existsSync16(settingsPath);
9545
9905
  if (!settingsExist) {
9546
9906
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
9547
9907
  }
@@ -9551,7 +9911,7 @@ function createHooksServer() {
9551
9911
  const meta = custom ? resolveHookMeta(name) : getHook(name);
9552
9912
  let hookHealthy = true;
9553
9913
  if (custom) {
9554
- if (!existsSync15(custom.scriptPath)) {
9914
+ if (!existsSync16(custom.scriptPath)) {
9555
9915
  issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
9556
9916
  hookHealthy = false;
9557
9917
  }
@@ -9561,14 +9921,14 @@ function createHooksServer() {
9561
9921
  continue;
9562
9922
  }
9563
9923
  const hookDir = getHookPath(name);
9564
- if (!existsSync15(join16(hookDir, "src", "hook.ts"))) {
9924
+ if (!existsSync16(join17(hookDir, "src", "hook.ts"))) {
9565
9925
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
9566
9926
  hookHealthy = false;
9567
9927
  }
9568
9928
  }
9569
9929
  if (meta && settingsExist) {
9570
9930
  try {
9571
- const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
9931
+ const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
9572
9932
  if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
9573
9933
  const eventName = meta.event.split(":")[0];
9574
9934
  issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
@@ -9605,10 +9965,10 @@ function createHooksServer() {
9605
9965
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
9606
9966
  }
9607
9967
  const hookPath = getHookPath(name);
9608
- const readmePath = join16(hookPath, "README.md");
9968
+ const readmePath = join17(hookPath, "README.md");
9609
9969
  let readme = "";
9610
- if (existsSync15(readmePath)) {
9611
- readme = readFileSync11(readmePath, "utf-8");
9970
+ if (existsSync16(readmePath)) {
9971
+ readme = readFileSync12(readmePath, "utf-8");
9612
9972
  }
9613
9973
  if (verbose) {
9614
9974
  return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
@@ -9791,7 +10151,7 @@ function createHooksServer() {
9791
10151
  const ctx = {
9792
10152
  scope,
9793
10153
  settings_path: settingsPath,
9794
- settings_exists: existsSync15(settingsPath),
10154
+ settings_exists: existsSync16(settingsPath),
9795
10155
  registered_hooks: hooks,
9796
10156
  hook_count: hooks.length,
9797
10157
  healthy,
@@ -9937,8 +10297,8 @@ function createHooksServer() {
9937
10297
  const settingsPath = getSettingsPath(scope);
9938
10298
  let settings = {};
9939
10299
  try {
9940
- if (existsSync15(settingsPath))
9941
- settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
10300
+ if (existsSync16(settingsPath))
10301
+ settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
9942
10302
  } catch {}
9943
10303
  if (!settings.hooks)
9944
10304
  settings.hooks = {};
@@ -9947,8 +10307,8 @@ function createHooksServer() {
9947
10307
  disabled.push(name);
9948
10308
  settings.hooks.__disabled = disabled;
9949
10309
  const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
9950
- const { dirname: dirname10 } = await import("path");
9951
- mkdirSync8(dirname10(settingsPath), { recursive: true });
10310
+ const { dirname: dirname11 } = await import("path");
10311
+ mkdirSync8(dirname11(settingsPath), { recursive: true });
9952
10312
  writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
9953
10313
  `);
9954
10314
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
@@ -9960,8 +10320,8 @@ function createHooksServer() {
9960
10320
  const settingsPath = getSettingsPath(scope);
9961
10321
  let settings = {};
9962
10322
  try {
9963
- if (existsSync15(settingsPath))
9964
- settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
10323
+ if (existsSync16(settingsPath))
10324
+ settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
9965
10325
  } catch {}
9966
10326
  if (settings.hooks?.__disabled) {
9967
10327
  settings.hooks.__disabled = settings.hooks.__disabled.filter((n2) => n2 !== name);
@@ -10331,13 +10691,13 @@ var init_server = __esm(() => {
10331
10691
  init_redact();
10332
10692
  init_authority();
10333
10693
  init_storage();
10334
- __dirname3 = dirname9(fileURLToPath5(import.meta.url));
10694
+ __dirname3 = dirname10(fileURLToPath5(import.meta.url));
10335
10695
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
10336
10696
  try {
10337
10697
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
10338
- const p = join16(__dirname3, rel);
10339
- if (existsSync15(p)) {
10340
- 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"));
10341
10701
  break;
10342
10702
  }
10343
10703
  }
@@ -12079,8 +12439,8 @@ var {
12079
12439
 
12080
12440
  // src/cli/index.tsx
12081
12441
  import chalk2 from "chalk";
12082
- import { existsSync as existsSync16, readFileSync as readFileSync12, rmSync as rmSync4 } from "fs";
12083
- import { join as join17, dirname as dirname10 } from "path";
12442
+ import { existsSync as existsSync17, readFileSync as readFileSync13, rmSync as rmSync4 } from "fs";
12443
+ import { join as join18, dirname as dirname11 } from "path";
12084
12444
  import { fileURLToPath as fileURLToPath6 } from "url";
12085
12445
 
12086
12446
  // src/cli/components/App.tsx
@@ -13253,6 +13613,7 @@ function App({ initialHooks, overwrite = false }) {
13253
13613
  }
13254
13614
 
13255
13615
  // src/cli/index.tsx
13616
+ init_native_safety();
13256
13617
  init_registry();
13257
13618
  init_installer();
13258
13619
  init_registration();
@@ -13268,10 +13629,10 @@ init_local_opt_in();
13268
13629
  init_transport2();
13269
13630
  init_db();
13270
13631
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
13271
- var __dirname4 = dirname10(fileURLToPath6(import.meta.url));
13272
- var pkgPath = existsSync16(join17(__dirname4, "..", "package.json")) ? join17(__dirname4, "..", "package.json") : join17(__dirname4, "..", "..", "package.json");
13273
- var pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
13274
- 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;
13275
13636
  function resolveScope(options) {
13276
13637
  if (options.project)
13277
13638
  return "project";
@@ -13350,7 +13711,7 @@ async function openLocalStoreOrExit(json2) {
13350
13711
  function readToken(tokenFile) {
13351
13712
  if (tokenFile) {
13352
13713
  try {
13353
- return readFileSync12(tokenFile, "utf-8").trim();
13714
+ return readFileSync13(tokenFile, "utf-8").trim();
13354
13715
  } catch {
13355
13716
  return;
13356
13717
  }
@@ -13409,8 +13770,8 @@ function editDistance(a, b) {
13409
13770
  function suggestHooks(name, max = 3) {
13410
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);
13411
13772
  }
13412
- program2.name("hooks").description("Install hooks for AI coding agents").version(pkg2.version);
13413
- 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(() => {
13414
13775
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
13415
13776
  console.error(chalk2.red("The interactive hook browser requires a TTY terminal.") + `
13416
13777
  ` + chalk2.dim("Use `hooks search <query>`, `hooks list`, or `hooks categories` for non-interactive use."));
@@ -13418,7 +13779,7 @@ program2.command("interactive", { isDefault: true }).alias("i").description("Int
13418
13779
  }
13419
13780
  render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
13420
13781
  });
13421
- 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) => {
13422
13783
  if (options.cloudflare) {
13423
13784
  if (!options.apiUrl) {
13424
13785
  const message = "--cloudflare requires --api-url <url>";
@@ -13488,13 +13849,13 @@ program2.command("init").description("Register a new agent profile with a unique
13488
13849
  if (profile.name) {
13489
13850
  console.log(` ${chalk2.dim("Name:")} ${profile.name}`);
13490
13851
  }
13491
- console.log(` ${chalk2.dim("Profile:")} ${join17(getProfilesDir(), `${profile.agent_id}.json`)}`);
13852
+ console.log(` ${chalk2.dim("Profile:")} ${join18(getProfilesDir(), `${profile.agent_id}.json`)}`);
13492
13853
  console.log();
13493
13854
  console.log(chalk2.dim(" Install hooks with this profile:"));
13494
13855
  console.log(` hooks install gitguard --profile ${profile.agent_id}`);
13495
13856
  console.log();
13496
13857
  });
13497
- 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) => {
13498
13859
  let mementos;
13499
13860
  try {
13500
13861
  mementos = mementosRegistrationFromFlags(hook, options);
@@ -13512,11 +13873,11 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
13512
13873
  process.exit(1);
13513
13874
  }
13514
13875
  const hookScript = resolveScriptPath2(hook);
13515
- if (!hookScript || !existsSync16(hookScript)) {
13876
+ if (!hookScript || !existsSync17(hookScript)) {
13516
13877
  console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
13517
13878
  process.exit(1);
13518
13879
  }
13519
- const content = readFileSync12(hookScript);
13880
+ const content = readFileSync13(hookScript);
13520
13881
  const sha = sha256Of2(content);
13521
13882
  const check = checkScriptHash2(hook, sha);
13522
13883
  if (!check.ok) {
@@ -13622,7 +13983,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
13622
13983
  process.stderr.write(stderr);
13623
13984
  process.exit(exitCode);
13624
13985
  });
13625
- 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) => {
13626
13987
  const scope = resolveScope(options);
13627
13988
  const target = resolveTarget(options);
13628
13989
  let toInstall = hooks;
@@ -13866,7 +14227,7 @@ Installing hooks (${scope}, ${target})...
13866
14227
  console.log(chalk2.dim(`
13867
14228
  Registered in ${settingsFile}`));
13868
14229
  });
13869
- 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) => {
13870
14231
  const scope = resolveScope(options);
13871
14232
  const limit = options.all ? Number.MAX_SAFE_INTEGER : parseLimit(options.limit, 20, 200);
13872
14233
  const storeHooks = listStoreHooks();
@@ -13975,7 +14336,7 @@ Custom / Registry hooks (${storeHooks.length}, showing ${storeVisible.length}):
13975
14336
  printDisclosureHint(storeHooks.length - storeVisible.length, "hooks info <name>", { includeAll: true });
13976
14337
  }
13977
14338
  });
13978
- 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) => {
13979
14340
  const results = searchHooks(query);
13980
14341
  if (options.json) {
13981
14342
  console.log(JSON.stringify(results));
@@ -13994,7 +14355,7 @@ Found ${results.length} hook(s), showing ${visible.length}:
13994
14355
  console.log(hookSummaryLine(h, { verbose: options.verbose }));
13995
14356
  printDisclosureHint(results.length - visible.length, "hooks info <name>");
13996
14357
  });
13997
- 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) => {
13998
14359
  const scope = resolveScope(options);
13999
14360
  const target = resolveTarget(options);
14000
14361
  const { uninstallHook: uninstallHook2 } = await Promise.resolve().then(() => (init_installer(), exports_installer));
@@ -14030,7 +14391,7 @@ program2.command("remove").alias("rm").argument("<hook>", "Hook to remove").opti
14030
14391
  console.log(chalk2.red(`\u2717 Hook '${hook}' not found${hint}`));
14031
14392
  }
14032
14393
  });
14033
- 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) => {
14034
14395
  if (options.json) {
14035
14396
  const result = CATEGORIES.map((cat) => ({
14036
14397
  name: cat,
@@ -14047,7 +14408,7 @@ Categories:
14047
14408
  console.log(` ${category} (${count})`);
14048
14409
  }
14049
14410
  });
14050
- 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) => {
14051
14412
  const { resolveHookMeta: resolveHookMeta2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
14052
14413
  const meta = resolveHookMeta2(hook) ?? getHook(hook);
14053
14414
  if (!meta) {
@@ -14095,7 +14456,7 @@ ${meta.displayName}
14095
14456
  console.log(chalk2.dim(" \u25CB Not installed in project"));
14096
14457
  }
14097
14458
  });
14098
- 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) => {
14099
14460
  const scope = resolveScope(options);
14100
14461
  const settingsPath = getSettingsPath(scope);
14101
14462
  const issues = [];
@@ -14110,7 +14471,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
14110
14471
  });
14111
14472
  }
14112
14473
  const staleNames = new Set(stale.map((finding) => finding.hook));
14113
- const settingsExist = existsSync16(settingsPath);
14474
+ const settingsExist = existsSync17(settingsPath);
14114
14475
  if (!settingsExist) {
14115
14476
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
14116
14477
  }
@@ -14118,7 +14479,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
14118
14479
  let wiringCount = 0;
14119
14480
  if (settingsExist) {
14120
14481
  try {
14121
- const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
14482
+ const settings = JSON.parse(readFileSync13(settingsPath, "utf-8"));
14122
14483
  wiringCount = countSettingsWiring(settings);
14123
14484
  } catch {}
14124
14485
  }
@@ -14129,7 +14490,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
14129
14490
  const meta = custom ? resolveHookMeta(name) : getHook(name);
14130
14491
  let hookHealthy = true;
14131
14492
  if (custom) {
14132
- if (!existsSync16(custom.scriptPath)) {
14493
+ if (!existsSync17(custom.scriptPath)) {
14133
14494
  issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
14134
14495
  hookHealthy = false;
14135
14496
  }
@@ -14140,15 +14501,15 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
14140
14501
  continue;
14141
14502
  }
14142
14503
  const hookDir = getHookPath(name);
14143
- const hookScript = join17(hookDir, "src", "hook.ts");
14144
- if (!existsSync16(hookScript)) {
14504
+ const hookScript = join18(hookDir, "src", "hook.ts");
14505
+ if (!existsSync17(hookScript)) {
14145
14506
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
14146
14507
  hookHealthy = false;
14147
14508
  }
14148
14509
  }
14149
14510
  if (meta && settingsExist) {
14150
14511
  try {
14151
- const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
14512
+ const settings = JSON.parse(readFileSync13(settingsPath, "utf-8"));
14152
14513
  if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
14153
14514
  const eventName = meta.event.split(":")[0];
14154
14515
  issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
@@ -14235,7 +14596,7 @@ Hook Health Check (${scope})
14235
14596
  }
14236
14597
  console.log();
14237
14598
  });
14238
- 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) => {
14239
14600
  const scope = resolveScope(options);
14240
14601
  const installed = getInstalledHooks(scope);
14241
14602
  const toUpdate = hooks.length > 0 ? hooks : installed;
@@ -14338,7 +14699,7 @@ Updating hooks...
14338
14699
  process.exitCode = 1;
14339
14700
  }
14340
14701
  });
14341
- 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) => {
14342
14703
  if (hook) {
14343
14704
  const meta = getHook(hook);
14344
14705
  if (!meta) {
@@ -14351,10 +14712,10 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
14351
14712
  return;
14352
14713
  }
14353
14714
  const hookPath = getHookPath(hook);
14354
- const readmePath = join17(hookPath, "README.md");
14715
+ const readmePath = join18(hookPath, "README.md");
14355
14716
  let readme = "";
14356
- if (existsSync16(readmePath)) {
14357
- readme = readFileSync12(readmePath, "utf-8");
14717
+ if (existsSync17(readmePath)) {
14718
+ readme = readFileSync13(readmePath, "utf-8");
14358
14719
  }
14359
14720
  if (options.json) {
14360
14721
  console.log(JSON.stringify({ ...meta, readme }));
@@ -14472,7 +14833,7 @@ ${meta.displayName} v${meta.version}
14472
14833
  console.log(` hooks docs --json Machine-readable documentation`);
14473
14834
  console.log();
14474
14835
  });
14475
- 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) => {
14476
14837
  const current2 = pkg2.version;
14477
14838
  let pm = "npm";
14478
14839
  try {
@@ -14534,7 +14895,7 @@ Upgrading @hasna/hooks (${pm})...
14534
14895
  \u2713 Upgraded: ${current2} \u2192 ${latest}`));
14535
14896
  }
14536
14897
  });
14537
- 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) => {
14538
14899
  const profiles = exportProfiles();
14539
14900
  const json2 = JSON.stringify(profiles, null, 2);
14540
14901
  if (options.output) {
@@ -14546,14 +14907,14 @@ program2.command("profile-export").description("Export all agent profiles as JSO
14546
14907
  console.log(json2);
14547
14908
  }
14548
14909
  });
14549
- 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) => {
14550
14911
  let raw;
14551
14912
  if (file === "-") {
14552
14913
  raw = await new Response(Bun.stdin.stream()).text();
14553
14914
  } else {
14554
- const { readFileSync: readFileSync13 } = await import("fs");
14915
+ const { readFileSync: readFileSync14 } = await import("fs");
14555
14916
  try {
14556
- raw = readFileSync13(file, "utf-8");
14917
+ raw = readFileSync14(file, "utf-8");
14557
14918
  } catch {
14558
14919
  if (options.json) {
14559
14920
  console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
@@ -14584,7 +14945,7 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
14584
14945
  console.log(chalk2.dim(` Skipped ${result.skipped} (already exist or invalid)`));
14585
14946
  }
14586
14947
  });
14587
- 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()})`);
14588
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) => {
14589
14950
  const db = await openLocalStoreOrExit(options.json);
14590
14951
  const limit = parseInt(options.limit) || 50;
@@ -14735,7 +15096,7 @@ logCmd.command("clear").description("Delete hook event logs").option("--hook <na
14735
15096
  }
14736
15097
  console.log(chalk2.green(`\u2713 Cleared ${count} event(s).`));
14737
15098
  });
14738
- 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");
14739
15100
  storageCmd.command("status").description("Show storage sync status").option("-j, --json", "Output as JSON", false).action(async (options) => {
14740
15101
  try {
14741
15102
  const { getStorageStatus: getStorageStatus2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
@@ -14818,7 +15179,7 @@ storageCmd.command("sync").description("Bidirectional storage sync: pull then pu
14818
15179
  process.exitCode = 1;
14819
15180
  }
14820
15181
  });
14821
- 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) => {
14822
15183
  const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
14823
15184
  const { retrustHook: retrustHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
14824
15185
  const resolved = resolveHook2(hook);
@@ -14837,14 +15198,14 @@ program2.command("trust").argument("<hook>", "Hook to trust").option("-j, --json
14837
15198
  console.log(chalk2.green(`\u2713 Trusted '${resolved.name}' v${resolved.version} (${resolved.source})`));
14838
15199
  console.log(chalk2.dim(` sha256 ${result.actual}`));
14839
15200
  });
14840
- 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) => {
14841
15202
  const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_serve(), exports_serve));
14842
15203
  startServeServer2({
14843
15204
  port: options.port ? parseInt(options.port, 10) : undefined,
14844
15205
  host: options.host
14845
15206
  });
14846
15207
  });
14847
- 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) => {
14848
15209
  const { syncHooks: syncHooks2, planSync: planSync2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
14849
15210
  try {
14850
15211
  const plan = options.dryRun ? await syncHooks2({ dryRun: true }) : await syncHooks2();
@@ -14888,7 +15249,7 @@ Hook sync (${source})${plan.dryRun ? " \u2014 dry run" : ""}
14888
15249
  process.exitCode = 1;
14889
15250
  }
14890
15251
  });
14891
- 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) => {
14892
15253
  const token = readToken(options.tokenFile);
14893
15254
  const accountId = options.accountId ?? process.env.CF_ACCOUNT_ID;
14894
15255
  if (!options.dryRun) {
@@ -14937,7 +15298,7 @@ Cloudflare provisioning complete:
14937
15298
  console.log(chalk2.dim(" wrangler.toml, fill in the D1 database id, then run the commands above."));
14938
15299
  console.log();
14939
15300
  });
14940
- 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) => {
14941
15302
  try {
14942
15303
  const { getStoragePg: getStoragePg2, runStorageMigrations: runStorageMigrations2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
14943
15304
  const remote = await getStoragePg2();
@@ -14960,7 +15321,7 @@ program2.command("migrate").description("Apply PostgreSQL migrations to the stor
14960
15321
  process.exitCode = 1;
14961
15322
  }
14962
15323
  });
14963
- 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) => {
14964
15325
  if (options.stdio) {
14965
15326
  const { startStdioServer: startStdioServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
14966
15327
  await startStdioServer2();
@@ -14981,8 +15342,20 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
14981
15342
  startMcpHttpServer2({ name: "hooks", port: resolveMcpHttpPort2(args), buildServer: createHooksServer2 });
14982
15343
  }
14983
15344
  });
14984
- 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
+ });
14985
15357
  var API_INDEPENDENT_COMMANDS = new Set([
15358
+ "safety",
14986
15359
  "serve",
14987
15360
  "mcp",
14988
15361
  "cf",
@@ -15043,4 +15416,4 @@ function enforceTransportGate() {
15043
15416
  refuseLocalStore(hooksHostedRouteLocalStoreRefusal(transport.authority.v1BaseUrl));
15044
15417
  }
15045
15418
  enforceTransportGate();
15046
- program2.parse();
15419
+ program3.parse();