@hasna/hooks 0.9.0 → 0.9.2

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
@@ -2087,10 +2087,10 @@ var init_registry = __esm(() => {
2087
2087
  name: "trash-guard",
2088
2088
  displayName: "Trash Guard",
2089
2089
  description: "Rewrites rm issued through Bash into `trash guard`, so the delete lands in a recoverable trash store; refuses the delete when there is nothing to redirect to (trash absent, a delete verb it cannot rewrite, or the protected class: /, ~, ~/.hasna, ~/.ssh, ~/.aws)",
2090
- version: "0.1.0",
2090
+ version: "0.1.1",
2091
2091
  category: "Git Safety",
2092
2092
  event: "PreToolUse",
2093
- matcher: "Bash",
2093
+ matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
2094
2094
  tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
2095
2095
  rewritesInput: true,
2096
2096
  timeoutSeconds: 5
@@ -2246,6 +2246,17 @@ var init_registry = __esm(() => {
2246
2246
  matcher: "",
2247
2247
  tags: ["knowledge", "context", "codewith", "deterministic", "session-start", "prompt", "subagent"]
2248
2248
  },
2249
+ {
2250
+ name: "mementos-context",
2251
+ displayName: "Mementos Prompt Context",
2252
+ description: "Retrieves scoped memories for a user prompt, with optional relevance judgments",
2253
+ version: "0.1.0",
2254
+ category: "Context Management",
2255
+ event: "UserPromptSubmit",
2256
+ matcher: "",
2257
+ timeoutSeconds: 12,
2258
+ tags: ["mementos", "memory", "prompt", "codex", "claude", "codewith", "opt-in"]
2259
+ },
2249
2260
  {
2250
2261
  name: "autoformat",
2251
2262
  displayName: "Auto Format",
@@ -4919,14 +4930,30 @@ function runRetention(db, days) {
4919
4930
  // src/db/index.ts
4920
4931
  var exports_db = {};
4921
4932
  __export(exports_db, {
4933
+ refuseLocalStore: () => refuseLocalStore,
4934
+ localStoreRefusalMessage: () => localStoreRefusalMessage,
4935
+ isLocalStoreRefused: () => isLocalStoreRefused,
4922
4936
  getDbPath: () => getDbPath,
4923
4937
  getDb: () => getDb,
4924
4938
  createTestDb: () => createTestDb,
4925
- closeDb: () => closeDb
4939
+ closeDb: () => closeDb,
4940
+ allowLocalStore: () => allowLocalStore
4926
4941
  });
4927
4942
  import { Database } from "bun:sqlite";
4928
4943
  import { existsSync as existsSync8, mkdirSync as mkdirSync2, cpSync } from "fs";
4929
4944
  import { join as join9 } from "path";
4945
+ function refuseLocalStore(message) {
4946
+ localStoreRefusal = message;
4947
+ }
4948
+ function allowLocalStore() {
4949
+ localStoreRefusal = null;
4950
+ }
4951
+ function isLocalStoreRefused() {
4952
+ return localStoreRefusal !== null;
4953
+ }
4954
+ function localStoreRefusalMessage() {
4955
+ return localStoreRefusal;
4956
+ }
4930
4957
  function resolveDataDir() {
4931
4958
  const effective = getEffectiveDataRoot();
4932
4959
  const oldDir = join9(getHomeDir(), ".hooks");
@@ -4950,6 +4977,8 @@ function ensureDir(dbPath) {
4950
4977
  }
4951
4978
  }
4952
4979
  function getDb() {
4980
+ if (localStoreRefusal !== null)
4981
+ throw new Error(localStoreRefusal);
4953
4982
  if (instance)
4954
4983
  return instance;
4955
4984
  const dbPath = getDbPath();
@@ -4987,7 +5016,7 @@ function createTestDb() {
4987
5016
  db.exec("PRAGMA foreign_keys=ON");
4988
5017
  return db;
4989
5018
  }
4990
- var instance = null;
5019
+ var instance = null, localStoreRefusal = null;
4991
5020
  var init_db = __esm(() => {
4992
5021
  init_app_home();
4993
5022
  init_migrations();
@@ -5026,6 +5055,9 @@ async function sha256File(path) {
5026
5055
  const buf = await readFile2(path);
5027
5056
  return sha256Of(buf);
5028
5057
  }
5058
+ function storeDb() {
5059
+ return isLocalStoreRefused() ? null : getDb();
5060
+ }
5029
5061
  function getHookRecord(db, name) {
5030
5062
  return db.query("SELECT * FROM hooks WHERE name = ?").get(name);
5031
5063
  }
@@ -5126,26 +5158,28 @@ function removePinnedHook(name) {
5126
5158
  }
5127
5159
  function pinInstalledHook(name, version, sha256, source, sourceRef) {
5128
5160
  const now2 = new Date().toISOString();
5129
- const db = getDb();
5130
- upsertHookRecord(db, {
5131
- name,
5132
- version,
5133
- sha256,
5134
- source_type: source,
5135
- source_ref: sourceRef ?? null,
5136
- last_verified_at: now2
5137
- });
5161
+ const db = storeDb();
5162
+ if (db) {
5163
+ upsertHookRecord(db, {
5164
+ name,
5165
+ version,
5166
+ sha256,
5167
+ source_type: source,
5168
+ source_ref: sourceRef ?? null,
5169
+ last_verified_at: now2
5170
+ });
5171
+ }
5138
5172
  setPinnedHook(name, { version, sha256, source });
5139
5173
  }
5140
5174
  function removeHookFromStore(name) {
5141
5175
  const removedPin = removePinnedHook(name);
5142
- const db = getDb();
5143
- const removedRecord = removeHookRecord(db, name);
5176
+ const db = storeDb();
5177
+ const removedRecord = db ? removeHookRecord(db, name) : false;
5144
5178
  return { removedPin, removedRecord };
5145
5179
  }
5146
5180
  function checkScriptHash(name, actual) {
5147
- const db = getDb();
5148
- const record = getHookRecord(db, name);
5181
+ const db = storeDb();
5182
+ const record = db ? getHookRecord(db, name) : null;
5149
5183
  const pin = getPinnedHook(name);
5150
5184
  const expected = record?.sha256 ?? pin?.sha256 ?? null;
5151
5185
  if (expected !== null && expected !== actual) {
@@ -5155,24 +5189,28 @@ function checkScriptHash(name, actual) {
5155
5189
  const now3 = new Date().toISOString();
5156
5190
  const version = pin?.version ?? record?.version ?? "0.0.0";
5157
5191
  const source = pin?.source ?? record?.source_type ?? "local";
5158
- upsertHookRecord(db, {
5159
- name,
5160
- version,
5161
- sha256: actual,
5162
- source_type: source,
5163
- last_verified_at: now3
5164
- });
5192
+ if (db) {
5193
+ upsertHookRecord(db, {
5194
+ name,
5195
+ version,
5196
+ sha256: actual,
5197
+ source_type: source,
5198
+ last_verified_at: now3
5199
+ });
5200
+ }
5165
5201
  setPinnedHook(name, { version, sha256: actual, source });
5166
5202
  return { ok: true, pinned: false, expected: null, actual, name };
5167
5203
  }
5168
5204
  const now2 = new Date().toISOString();
5169
- upsertHookRecord(db, {
5170
- name,
5171
- version: record?.version ?? pin?.version ?? "0.0.0",
5172
- sha256: expected,
5173
- source_type: record?.source_type ?? pin?.source ?? "local",
5174
- last_verified_at: now2
5175
- });
5205
+ if (db) {
5206
+ upsertHookRecord(db, {
5207
+ name,
5208
+ version: record?.version ?? pin?.version ?? "0.0.0",
5209
+ sha256: expected,
5210
+ source_type: record?.source_type ?? pin?.source ?? "local",
5211
+ last_verified_at: now2
5212
+ });
5213
+ }
5176
5214
  return { ok: true, pinned: true, expected, actual, name };
5177
5215
  }
5178
5216
  async function verifyScriptHash(name, scriptPath) {
@@ -5180,14 +5218,16 @@ async function verifyScriptHash(name, scriptPath) {
5180
5218
  }
5181
5219
  function retrustHook(name, scriptPath, version, source) {
5182
5220
  const actual = sha256Of(readFileSync3(scriptPath));
5183
- const db = getDb();
5184
- upsertHookRecord(db, {
5185
- name,
5186
- version,
5187
- sha256: actual,
5188
- source_type: source,
5189
- last_verified_at: new Date().toISOString()
5190
- });
5221
+ const db = storeDb();
5222
+ if (db) {
5223
+ upsertHookRecord(db, {
5224
+ name,
5225
+ version,
5226
+ sha256: actual,
5227
+ source_type: source,
5228
+ last_verified_at: new Date().toISOString()
5229
+ });
5230
+ }
5191
5231
  setPinnedHook(name, { version, sha256: actual, source });
5192
5232
  return { ok: true, pinned: true, expected: actual, actual, name };
5193
5233
  }
@@ -5203,6 +5243,78 @@ var init_store = __esm(() => {
5203
5243
  };
5204
5244
  });
5205
5245
 
5246
+ // src/lib/codex-settings.ts
5247
+ import { closeSync, existsSync as existsSync10, lstatSync, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
5248
+ import { dirname as dirname3 } from "path";
5249
+ import { randomUUID as randomUUID3 } from "crypto";
5250
+ function current(path) {
5251
+ try {
5252
+ const stat = lstatSync(path);
5253
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
5254
+ throw new Error("invalid_file");
5255
+ return readFileSync4(path, "utf8");
5256
+ } catch (error) {
5257
+ if (error.code === "ENOENT")
5258
+ return null;
5259
+ throw new Error("Cannot safely read Codex hooks.json; no settings were changed.");
5260
+ }
5261
+ }
5262
+ function readCodexSettings(path) {
5263
+ const raw = current(path);
5264
+ let settings;
5265
+ try {
5266
+ settings = raw === null ? {} : JSON.parse(raw);
5267
+ if (!settings || typeof settings !== "object" || Array.isArray(settings))
5268
+ throw new Error;
5269
+ if (settings.hooks !== undefined) {
5270
+ if (!settings.hooks || typeof settings.hooks !== "object" || Array.isArray(settings.hooks))
5271
+ throw new Error;
5272
+ for (const entries of Object.values(settings.hooks)) {
5273
+ if (!Array.isArray(entries) || entries.some((entry) => !entry || !Array.isArray(entry.hooks) || entry.hooks.some((hook) => !hook || typeof hook !== "object")))
5274
+ throw new Error;
5275
+ }
5276
+ }
5277
+ } catch {
5278
+ throw new Error("Invalid Codex hooks.json; repair it before installing or removing hooks.");
5279
+ }
5280
+ snapshots.set(settings, { path, raw });
5281
+ return settings;
5282
+ }
5283
+ function writeCodexSettings(path, settings) {
5284
+ const snapshot = snapshots.get(settings);
5285
+ if (!snapshot || snapshot.path !== path)
5286
+ throw new Error("Codex settings require a complete current read before writing.");
5287
+ mkdirSync4(dirname3(path), { recursive: true });
5288
+ const lock = `${path}.hooks-write.lock`;
5289
+ let fd;
5290
+ try {
5291
+ fd = openSync(lock, "wx", 384);
5292
+ } catch {
5293
+ throw new Error("Codex settings are being edited; retry after the active writer finishes.");
5294
+ }
5295
+ const temp = `${path}.${randomUUID3()}.tmp`;
5296
+ try {
5297
+ if (current(path) !== snapshot.raw)
5298
+ throw new Error("Codex settings changed since reading; re-read before retrying.");
5299
+ const next = JSON.stringify(settings, null, 2) + `
5300
+ `;
5301
+ writeFileSync3(temp, next, { flag: "wx", mode: 384 });
5302
+ if (current(path) !== snapshot.raw)
5303
+ throw new Error("Codex settings changed since reading; re-read before retrying.");
5304
+ renameSync2(temp, path);
5305
+ snapshots.set(settings, { path, raw: next });
5306
+ } finally {
5307
+ if (existsSync10(temp))
5308
+ unlinkSync(temp);
5309
+ closeSync(fd);
5310
+ unlinkSync(lock);
5311
+ }
5312
+ }
5313
+ var snapshots;
5314
+ var init_codex_settings = __esm(() => {
5315
+ snapshots = new WeakMap;
5316
+ });
5317
+
5206
5318
  // src/lib/installer.ts
5207
5319
  var exports_installer = {};
5208
5320
  __export(exports_installer, {
@@ -5223,8 +5335,8 @@ __export(exports_installer, {
5223
5335
  detectRewriteConflict: () => detectRewriteConflict,
5224
5336
  buildCodewithTomlFragment: () => buildCodewithTomlFragment
5225
5337
  });
5226
- import { existsSync as existsSync10, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, rmdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
5227
- import { join as join10, dirname as dirname3 } from "path";
5338
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
5339
+ import { join as join10, dirname as dirname4 } from "path";
5228
5340
  import { homedir as homedir4 } from "os";
5229
5341
  import { fileURLToPath as fileURLToPath2 } from "url";
5230
5342
  function normalizeHookName(name) {
@@ -5234,12 +5346,19 @@ function shortHookName(name) {
5234
5346
  return name.startsWith("hook-") ? name.slice("hook-".length) : name;
5235
5347
  }
5236
5348
  function removeHookEntriesByName(entries, hookName) {
5237
- return entries.filter((entry) => !entry.hooks?.some((h) => {
5238
- const match = h.command?.match(/^hooks run ([\w-]+)/);
5239
- return match && match[1] === hookName;
5240
- }));
5349
+ return entries.flatMap((entry) => {
5350
+ if (!Array.isArray(entry.hooks))
5351
+ return [entry];
5352
+ const kept = entry.hooks.filter((h) => {
5353
+ const match = h.command?.match(/^hooks run ([\w-]+)/);
5354
+ return !(match && match[1] === hookName);
5355
+ });
5356
+ return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
5357
+ });
5241
5358
  }
5242
5359
  function getTargetSettingsDir(target) {
5360
+ if (target === "codex")
5361
+ return ".codex";
5243
5362
  if (target === "codewith")
5244
5363
  return ".codewith";
5245
5364
  if (target === "gemini")
@@ -5247,6 +5366,8 @@ function getTargetSettingsDir(target) {
5247
5366
  return ".claude";
5248
5367
  }
5249
5368
  function getGlobalSettingsPathOverride(target) {
5369
+ if (target === "codex")
5370
+ return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH;
5250
5371
  if (target === "claude")
5251
5372
  return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
5252
5373
  if (target === "gemini")
@@ -5263,6 +5384,8 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
5263
5384
  if (globalOverride)
5264
5385
  return globalOverride;
5265
5386
  const dir = getTargetSettingsDir(target);
5387
+ if (target === "codex")
5388
+ return join10(scope === "project" ? process.cwd() : homedir4(), dir, "hooks.json");
5266
5389
  if (scope === "project") {
5267
5390
  return target === "codewith" ? join10(process.cwd(), dir, "config.toml") : join10(process.cwd(), dir, "settings.json");
5268
5391
  }
@@ -5276,13 +5399,15 @@ function getHookPath(name) {
5276
5399
  return join10(HOOKS_DIR, normalizeHookName(shortName));
5277
5400
  }
5278
5401
  function hookExists(name) {
5279
- return existsSync10(getHookPath(name));
5402
+ return existsSync11(getHookPath(name));
5280
5403
  }
5281
5404
  function readSettings(scope = "global", target = "claude") {
5282
5405
  const path = getSettingsPath(scope, target);
5406
+ if (target === "codex")
5407
+ return readCodexSettings(path);
5283
5408
  try {
5284
- if (existsSync10(path)) {
5285
- return JSON.parse(readFileSync4(path, "utf-8"));
5409
+ if (existsSync11(path)) {
5410
+ return JSON.parse(readFileSync5(path, "utf-8"));
5286
5411
  }
5287
5412
  } catch (error) {
5288
5413
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -5291,11 +5416,13 @@ function readSettings(scope = "global", target = "claude") {
5291
5416
  }
5292
5417
  function writeSettings(settings, scope = "global", target = "claude") {
5293
5418
  const path = getSettingsPath(scope, target);
5294
- const dir = dirname3(path);
5295
- if (!existsSync10(dir)) {
5296
- mkdirSync4(dir, { recursive: true });
5419
+ if (target === "codex")
5420
+ return writeCodexSettings(path, settings);
5421
+ const dir = dirname4(path);
5422
+ if (!existsSync11(dir)) {
5423
+ mkdirSync5(dir, { recursive: true });
5297
5424
  }
5298
- writeFileSync3(path, JSON.stringify(settings, null, 2) + `
5425
+ writeFileSync4(path, JSON.stringify(settings, null, 2) + `
5299
5426
  `);
5300
5427
  }
5301
5428
  function getTargetEventName(internalEvent, target) {
@@ -5318,6 +5445,8 @@ function codewithMatcher(matcher) {
5318
5445
  }
5319
5446
  function codewithTimeout(name) {
5320
5447
  switch (name) {
5448
+ case "mementos-context":
5449
+ return 12;
5321
5450
  case "knowledge-context":
5322
5451
  return 6;
5323
5452
  case "session-start":
@@ -5384,21 +5513,21 @@ function buildCodewithTomlFragment(name, profile) {
5384
5513
  function readCodewithConfig(scope, configPath) {
5385
5514
  const path = getSettingsPath(scope, "codewith", configPath);
5386
5515
  try {
5387
- return existsSync10(path) ? readFileSync4(path, "utf-8") : "";
5516
+ return existsSync11(path) ? readFileSync5(path, "utf-8") : "";
5388
5517
  } catch {
5389
5518
  return "";
5390
5519
  }
5391
5520
  }
5392
5521
  function appendCodewithFragment(fragment, scope, configPath) {
5393
5522
  const path = getSettingsPath(scope, "codewith", configPath);
5394
- const dir = dirname3(path);
5395
- if (!existsSync10(dir))
5396
- mkdirSync4(dir, { recursive: true });
5397
- const existing = existsSync10(path) ? readFileSync4(path, "utf-8") : "";
5523
+ const dir = dirname4(path);
5524
+ if (!existsSync11(dir))
5525
+ mkdirSync5(dir, { recursive: true });
5526
+ const existing = existsSync11(path) ? readFileSync5(path, "utf-8") : "";
5398
5527
  const sep2 = existing.trim() ? `
5399
5528
 
5400
5529
  ` : "";
5401
- writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
5530
+ writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
5402
5531
  return path;
5403
5532
  }
5404
5533
  function detectConflict(name, scope, target) {
@@ -5495,18 +5624,25 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
5495
5624
  };
5496
5625
  }
5497
5626
  }
5498
- const registered = getRegisteredHooksForTarget(scope, target);
5499
- if (registered.includes(shortName) && !overwrite) {
5500
- return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
5501
- }
5502
- const conflict = detectConflict(shortName, scope, target);
5503
- const rewriteConflict = detectRewriteConflict(shortName, scope, target);
5504
- if (rewriteConflict) {
5505
- return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
5506
- }
5507
5627
  try {
5628
+ const registered = getRegisteredHooksForTarget(scope, target);
5629
+ if (registered.includes(shortName) && !overwrite) {
5630
+ return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
5631
+ }
5632
+ const conflict = detectConflict(shortName, scope, target);
5633
+ const rewriteConflict = detectRewriteConflict(shortName, scope, target);
5634
+ if (rewriteConflict) {
5635
+ return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
5636
+ }
5508
5637
  registerHook(shortName, scope, target, profile);
5509
- return { hook: shortName, success: true, scope, target, ...conflict ? { conflict } : {} };
5638
+ return {
5639
+ hook: shortName,
5640
+ success: true,
5641
+ scope,
5642
+ target,
5643
+ ...conflict ? { conflict } : {},
5644
+ ...target === "codex" ? { applied: true, configPath: getSettingsPath(scope, target), note: "Registered in Codex hooks.json. Review and trust the exact definition in Codex /hooks; project hooks also require a trusted project. Registration alone does not prove execution." } : {}
5645
+ };
5510
5646
  } catch (error) {
5511
5647
  return {
5512
5648
  hook: shortName,
@@ -5518,6 +5654,9 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
5518
5654
  }
5519
5655
  function installHook(name, options = {}) {
5520
5656
  const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
5657
+ if (!/^[\w-]+$/.test(name) || profile !== undefined && !/^[\w-]+$/.test(profile)) {
5658
+ return { hook: name, success: false, error: "Hook and profile names must contain only letters, digits, underscores or hyphens.", target };
5659
+ }
5521
5660
  if (target === "all") {
5522
5661
  const shortName = shortHookName(name);
5523
5662
  const meta = resolveHookMeta(shortName);
@@ -5649,7 +5788,8 @@ function removeHook(name, scope = "global", target = "claude") {
5649
5788
  if (target === "all") {
5650
5789
  const claudeRemoved = removeHookForTarget(shortName, scope, "claude");
5651
5790
  const geminiRemoved = removeHookForTarget(shortName, scope, "gemini");
5652
- return claudeRemoved || geminiRemoved;
5791
+ const codexRemoved = removeHookForTarget(shortName, scope, "codex");
5792
+ return claudeRemoved || geminiRemoved || codexRemoved;
5653
5793
  }
5654
5794
  if (target === "codewith") {
5655
5795
  return false;
@@ -5666,9 +5806,9 @@ function removeHookForTarget(name, scope, target) {
5666
5806
  }
5667
5807
  function readSettingsAt(path) {
5668
5808
  try {
5669
- if (!existsSync10(path))
5809
+ if (!existsSync11(path))
5670
5810
  return;
5671
- const raw = readFileSync4(path, "utf-8");
5811
+ const raw = readFileSync5(path, "utf-8");
5672
5812
  return { raw, settings: JSON.parse(raw) };
5673
5813
  } catch {
5674
5814
  return;
@@ -5701,7 +5841,9 @@ function pruneStaleRegistrations(scope = "global") {
5701
5841
  commands.add(finding.command);
5702
5842
  staleCommandsByEvent.set(finding.event, commands);
5703
5843
  }
5704
- const settings = parsed.settings;
5844
+ const settings = target === "codex" ? readCodexSettings(path) : parsed.settings;
5845
+ if (target === "codex" && readFileSync5(path, "utf8") !== parsed.raw)
5846
+ throw new Error("Codex settings changed before pruning; retry.");
5705
5847
  for (const [eventKey, commands] of staleCommandsByEvent) {
5706
5848
  const entries = settings.hooks?.[eventKey];
5707
5849
  if (!Array.isArray(entries))
@@ -5726,7 +5868,7 @@ function pruneStaleRegistrations(scope = "global") {
5726
5868
  if (settings.hooks && Object.keys(settings.hooks).length === 0)
5727
5869
  delete settings.hooks;
5728
5870
  const backupPath = `${path}.bak`;
5729
- writeFileSync3(backupPath, parsed.raw);
5871
+ writeFileSync4(backupPath, parsed.raw);
5730
5872
  writeSettings(settings, scope, target);
5731
5873
  results.push({ file: path, removed: stale, backupPath });
5732
5874
  }
@@ -5788,10 +5930,10 @@ function codewithHasHookEntryInText(configText, name) {
5788
5930
  }
5789
5931
  function codewithHasHookEntry(name, scope = "global") {
5790
5932
  const path = getSettingsPath(scope, "codewith");
5791
- if (!existsSync10(path))
5933
+ if (!existsSync11(path))
5792
5934
  return false;
5793
5935
  try {
5794
- return codewithHasHookEntryInText(readFileSync4(path, "utf-8"), name);
5936
+ return codewithHasHookEntryInText(readFileSync5(path, "utf-8"), name);
5795
5937
  } catch {
5796
5938
  return false;
5797
5939
  }
@@ -5805,7 +5947,7 @@ function removeStoreDirFailClosed(dir) {
5805
5947
  }
5806
5948
  for (const entry of entries) {
5807
5949
  if (!entry.isDirectory()) {
5808
- unlinkSync(join10(dir, entry.name));
5950
+ unlinkSync2(join10(dir, entry.name));
5809
5951
  }
5810
5952
  }
5811
5953
  rmdirSync(dir);
@@ -5815,14 +5957,14 @@ function uninstallHook(name, scope = "global", target = "claude") {
5815
5957
  const custom = readCustomManifest(shortName);
5816
5958
  const bundledMeta = custom ? undefined : getHook(shortName);
5817
5959
  const existsInTarget = (t) => getRegisteredHooksForTarget("global", t).includes(shortName) || getRegisteredHooksForTarget("project", t).includes(shortName);
5818
- const registeredClaudeOrGemini = target === "all" ? existsInTarget("claude") || existsInTarget("gemini") : false;
5960
+ const registeredClaudeOrGemini = target === "all" ? JSON_SETTINGS_TARGETS.some(existsInTarget) : false;
5819
5961
  const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
5820
5962
  const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
5821
5963
  const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName, scope);
5822
5964
  if (!custom && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
5823
5965
  return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
5824
5966
  }
5825
- const writableTargets = target === "all" ? ["claude", "gemini"] : target === "codewith" ? [] : [target];
5967
+ const writableTargets = target === "all" ? JSON_SETTINGS_TARGETS : target === "codewith" ? [] : [target];
5826
5968
  const scopes = [];
5827
5969
  for (const s of ["global", "project"]) {
5828
5970
  for (const t of writableTargets) {
@@ -5836,11 +5978,11 @@ function uninstallHook(name, scope = "global", target = "claude") {
5836
5978
  const registrationsRemaining = [];
5837
5979
  if (target === "codewith" || target === "all") {
5838
5980
  const codewithPath = getSettingsPath(scope, "codewith");
5839
- if (existsSync10(codewithPath)) {
5840
- const before = readFileSync4(codewithPath, "utf-8");
5981
+ if (existsSync11(codewithPath)) {
5982
+ const before = readFileSync5(codewithPath, "utf-8");
5841
5983
  const after = removeCodewithHookEntry(before, shortName);
5842
5984
  if (after.removed) {
5843
- writeFileSync3(codewithPath, after.text, "utf-8");
5985
+ writeFileSync4(codewithPath, after.text, "utf-8");
5844
5986
  }
5845
5987
  if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
5846
5988
  registrationsRemaining.push("codewith");
@@ -5883,8 +6025,9 @@ var init_installer = __esm(() => {
5883
6025
  init_manifest();
5884
6026
  init_registration();
5885
6027
  init_store();
5886
- __dirname2 = dirname3(fileURLToPath2(import.meta.url));
5887
- HOOKS_DIR = existsSync10(join10(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join10(__dirname2, "..", "..", "hooks") : join10(__dirname2, "..", "hooks");
6028
+ init_codex_settings();
6029
+ __dirname2 = dirname4(fileURLToPath2(import.meta.url));
6030
+ HOOKS_DIR = existsSync11(join10(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join10(__dirname2, "..", "..", "hooks") : join10(__dirname2, "..", "hooks");
5888
6031
  EVENT_MAP = {
5889
6032
  claude: {
5890
6033
  PreToolUse: "PreToolUse",
@@ -5893,7 +6036,7 @@ var init_installer = __esm(() => {
5893
6036
  Notification: "Notification",
5894
6037
  SessionStart: "SessionStart",
5895
6038
  SessionEnd: "SessionEnd",
5896
- UserPromptSubmit: null,
6039
+ UserPromptSubmit: "UserPromptSubmit",
5897
6040
  SubagentStart: null
5898
6041
  },
5899
6042
  gemini: {
@@ -5915,10 +6058,20 @@ var init_installer = __esm(() => {
5915
6058
  SessionEnd: null,
5916
6059
  UserPromptSubmit: "UserPromptSubmit",
5917
6060
  SubagentStart: "SubagentStart"
6061
+ },
6062
+ codex: {
6063
+ PreToolUse: "PreToolUse",
6064
+ PostToolUse: "PostToolUse",
6065
+ Stop: "Stop",
6066
+ Notification: null,
6067
+ SessionStart: "SessionStart",
6068
+ SessionEnd: "SessionEnd",
6069
+ UserPromptSubmit: "UserPromptSubmit",
6070
+ SubagentStart: "SubagentStart"
5918
6071
  }
5919
6072
  };
5920
6073
  getInstalledHooks = getRegisteredHooks;
5921
- JSON_SETTINGS_TARGETS = ["claude", "gemini"];
6074
+ JSON_SETTINGS_TARGETS = ["claude", "gemini", "codex"];
5922
6075
  });
5923
6076
 
5924
6077
  // src/lib/redact.ts
@@ -5987,21 +6140,21 @@ var init_redact = __esm(() => {
5987
6140
  });
5988
6141
 
5989
6142
  // src/lib/profiles.ts
5990
- import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
6143
+ import { existsSync as existsSync12, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
5991
6144
  import { join as join11 } from "path";
5992
6145
  function getProfilesDir() {
5993
6146
  const newDir = join11(getEffectiveDataRoot(), "profiles");
5994
6147
  const oldDir = join11(getHomeDir(), ".hooks", "profiles");
5995
- if (!existsSync11(newDir) && existsSync11(oldDir)) {
5996
- mkdirSync5(newDir, { recursive: true });
6148
+ if (!existsSync12(newDir) && existsSync12(oldDir)) {
6149
+ mkdirSync6(newDir, { recursive: true });
5997
6150
  cpSync2(oldDir, newDir, { recursive: true });
5998
6151
  }
5999
6152
  return newDir;
6000
6153
  }
6001
6154
  function ensureProfilesDir() {
6002
6155
  const dir = getProfilesDir();
6003
- if (!existsSync11(dir)) {
6004
- mkdirSync5(dir, { recursive: true });
6156
+ if (!existsSync12(dir)) {
6157
+ mkdirSync6(dir, { recursive: true });
6005
6158
  }
6006
6159
  }
6007
6160
  function profilePath(id) {
@@ -6024,30 +6177,30 @@ function createProfile(input) {
6024
6177
  if (input.name) {
6025
6178
  profile.name = input.name;
6026
6179
  }
6027
- writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
6180
+ writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
6028
6181
  `);
6029
6182
  return profile;
6030
6183
  }
6031
6184
  function getProfile(id) {
6032
6185
  const path = profilePath(id);
6033
6186
  try {
6034
- if (!existsSync11(path))
6187
+ if (!existsSync12(path))
6035
6188
  return null;
6036
- return JSON.parse(readFileSync5(path, "utf-8"));
6189
+ return JSON.parse(readFileSync6(path, "utf-8"));
6037
6190
  } catch {
6038
6191
  return null;
6039
6192
  }
6040
6193
  }
6041
6194
  function listProfiles() {
6042
6195
  const dir = getProfilesDir();
6043
- if (!existsSync11(dir))
6196
+ if (!existsSync12(dir))
6044
6197
  return [];
6045
6198
  try {
6046
6199
  const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
6047
6200
  const profiles = [];
6048
6201
  for (const file of files) {
6049
6202
  try {
6050
- const content = readFileSync5(join11(dir, file), "utf-8");
6203
+ const content = readFileSync6(join11(dir, file), "utf-8");
6051
6204
  profiles.push(JSON.parse(content));
6052
6205
  } catch {}
6053
6206
  }
@@ -6061,7 +6214,7 @@ function touchProfile(id) {
6061
6214
  if (!profile)
6062
6215
  return;
6063
6216
  profile.last_seen_at = new Date().toISOString();
6064
- writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
6217
+ writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
6065
6218
  `);
6066
6219
  }
6067
6220
  function exportProfiles() {
@@ -6077,11 +6230,11 @@ function importProfiles(profiles) {
6077
6230
  continue;
6078
6231
  }
6079
6232
  const path = profilePath(profile.agent_id);
6080
- if (existsSync11(path)) {
6233
+ if (existsSync12(path)) {
6081
6234
  skipped++;
6082
6235
  continue;
6083
6236
  }
6084
- writeFileSync4(path, JSON.stringify(profile, null, 2) + `
6237
+ writeFileSync5(path, JSON.stringify(profile, null, 2) + `
6085
6238
  `);
6086
6239
  imported++;
6087
6240
  }
@@ -6091,10 +6244,10 @@ var init_profiles = __esm(() => {
6091
6244
  init_app_home();
6092
6245
  });
6093
6246
 
6094
- // ../contracts/dist/client/transport.js
6247
+ // ../../node_modules/.bun/@hasna+contracts@1.0.2+e0db24ae03127387/node_modules/@hasna/contracts/dist/client/transport.js
6095
6248
  import { isIP as isIP2 } from "net";
6096
6249
  import { spawnSync } from "child_process";
6097
- import { closeSync, fstatSync, openSync, readFileSync as readFileSync6 } from "fs";
6250
+ import { closeSync as closeSync2, fstatSync, openSync as openSync2, readFileSync as readFileSync7 } from "fs";
6098
6251
  import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
6099
6252
  import { createRequire as createRequire2 } from "module";
6100
6253
  import { hostname as osHostname } from "os";
@@ -6198,7 +6351,7 @@ function readAppConfigFile(path) {
6198
6351
  };
6199
6352
  let fd = -1;
6200
6353
  try {
6201
- fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
6354
+ fd = openSync2(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
6202
6355
  } catch (error) {
6203
6356
  const code = error.code;
6204
6357
  if (code === "ENOENT" || code === "ENOTDIR")
@@ -6219,7 +6372,7 @@ function readAppConfigFile(path) {
6219
6372
  unsafe("the file is not owned by the current user");
6220
6373
  if (before.size > MAX_CREDENTIAL_FILE_BYTES2)
6221
6374
  unsafe("the file exceeds the size limit");
6222
- const bytes = readFileSync6(fd);
6375
+ const bytes = readFileSync7(fd);
6223
6376
  const after = fstatSync(fd);
6224
6377
  if (!configFileReadsCoherent(before, after)) {
6225
6378
  unsafe("the file changed while being read");
@@ -6227,7 +6380,7 @@ function readAppConfigFile(path) {
6227
6380
  return parseEnvFile(bytes.toString("utf8"));
6228
6381
  } finally {
6229
6382
  if (fd !== -1)
6230
- closeSync(fd);
6383
+ closeSync2(fd);
6231
6384
  }
6232
6385
  }
6233
6386
  function readCredentialFile(path, apiKeyKeys) {
@@ -6771,7 +6924,7 @@ function credentialDiskSourcesForMessage(name, env) {
6771
6924
  const paths = credentialDiskSources(name, env);
6772
6925
  return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
6773
6926
  }
6774
- var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2, SECRETS_PACKAGE_SPECIFIER2, requireSecretsSdk2, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2;
6927
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES2, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM2, CREDENTIAL_SEAL2, AMBIENT_ENVIRONMENT2, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk2, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS2, AUTHORITY_OVERRIDE_HEADERS2;
6775
6928
  var init_transport = __esm(() => {
6776
6929
  CredentialResolutionError = class CredentialResolutionError extends Error {
6777
6930
  appName;
@@ -6800,7 +6953,7 @@ var init_transport = __esm(() => {
6800
6953
  INSPECT_CUSTOM2 = Symbol.for("nodejs.util.inspect.custom");
6801
6954
  CREDENTIAL_SEAL2 = Symbol.for("hasna:contracts:sealedCredential");
6802
6955
  AMBIENT_ENVIRONMENT2 = Symbol.for("hasna:contracts:ambientClientEnvironment");
6803
- SECRETS_PACKAGE_SPECIFIER2 = "@hasna/" + "secrets";
6956
+ SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
6804
6957
  requireSecretsSdk2 = createRequire2(import.meta.url);
6805
6958
  ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
6806
6959
  DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -6844,6 +6997,15 @@ function hasHooksEnvAuthorityIntent(env = process.env) {
6844
6997
  function selectsHooksLocalStore(env = process.env) {
6845
6998
  return !hasHooksEnvAuthorityIntent(env) && isHooksLocalOptIn(env);
6846
6999
  }
7000
+ function hooksFailClosedLine(detail) {
7001
+ return `REMOTE_API_CONFIG_MISSING: hooks: no registry credential resolved from ${HOOKS_CREDENTIAL_TIERS} ` + `(with HASNA_HOOKS_API_URL or the fleet gateway default), and local mode is not enabled ` + `(${HOOKS_LOCAL_OPT_IN_HINT}) \u2014 refusing to silently fall back to local storage.` + (detail ? ` ${detail}` : "");
7002
+ }
7003
+ function hooksHostedRouteLocalStoreRefusal(authority) {
7004
+ return "REMOTE_COMMAND_UNSUPPORTED: this command is local-only \u2014 it reads the on-box SQLite store " + `(hooks.db), which the hosted hooks registry authority (${authority}) does not serve; local SQLite ` + `is opt-in only (${HOOKS_LOCAL_OPT_IN_HINT}) and is disabled by default \u2014 refusing to open a local ` + "store on the hosted route.";
7005
+ }
7006
+ function hooksEventSinkRefusal() {
7007
+ return "REMOTE_COMMAND_UNSUPPORTED: hook event not recorded \u2014 the hook event log is local-only (on-box " + "SQLite hooks.db) and the hosted hooks registry has no event route; local SQLite is opt-in only " + `(${HOOKS_LOCAL_OPT_IN_HINT}) and is disabled by default. Nothing was written.`;
7008
+ }
6847
7009
  function hooksResolverEnv(env) {
6848
7010
  const blanks = hooksAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
6849
7011
  if (blanks.length === 0)
@@ -6868,7 +7030,7 @@ function hooksResolverInputs(env, credentials = {}) {
6868
7030
  }
6869
7031
  return { env: normalised, credentials: { ...credentials, keychain } };
6870
7032
  }
6871
- var HOOKS_LOCAL_OPT_IN_ENV_KEYS, CONTRACTS_AMBIENT_ENVIRONMENT;
7033
+ var HOOKS_LOCAL_OPT_IN_ENV_KEYS, HOOKS_CREDENTIAL_TIERS = "the Keychain item hasna.credentials.hooks.api-key, ~/.hasna/hooks/config/credentials, or HASNA_HOOKS_API_KEY", HOOKS_LOCAL_OPT_IN_HINT = "HASNA_HOOKS_LOCAL=1 (alias HOOKS_LOCAL=1)", CONTRACTS_AMBIENT_ENVIRONMENT;
6872
7034
  var init_local_opt_in = __esm(() => {
6873
7035
  init_transport();
6874
7036
  HOOKS_LOCAL_OPT_IN_ENV_KEYS = ["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"];
@@ -6971,14 +7133,35 @@ var init_transport2 = __esm(() => {
6971
7133
  var exports_db_writer = {};
6972
7134
  __export(exports_db_writer, {
6973
7135
  writeHookEvent: () => writeHookEvent,
7136
+ resolveHookEventSink: () => resolveHookEventSink,
6974
7137
  resolveEventType: () => resolveEventType,
6975
7138
  recordHookRun: () => recordHookRun,
6976
- normalizeEventType: () => normalizeEventType
7139
+ normalizeEventType: () => normalizeEventType,
7140
+ __resetHookEventSinkNotice: () => __resetHookEventSinkNotice
6977
7141
  });
7142
+ function resolveHookEventSink(env = process.env) {
7143
+ if (isLocalStoreRefused())
7144
+ return { kind: "refused", reason: localStoreRefusalMessage() ?? hooksEventSinkRefusal() };
7145
+ if (selectsHooksLocalStore(env))
7146
+ return { kind: "local" };
7147
+ return { kind: "refused", reason: hooksEventSinkRefusal() };
7148
+ }
7149
+ function __resetHookEventSinkNotice() {
7150
+ sinkRefusalPrinted = false;
7151
+ }
6978
7152
  function nanoid2() {
6979
7153
  return crypto.randomUUID().replace(/-/g, "").slice(0, 21);
6980
7154
  }
6981
7155
  function writeHookEvent(event) {
7156
+ const sink = resolveHookEventSink();
7157
+ if (sink.kind !== "local") {
7158
+ if (!sinkRefusalPrinted) {
7159
+ sinkRefusalPrinted = true;
7160
+ process.stderr.write(`[hooks db-writer] ${sink.reason}
7161
+ `);
7162
+ }
7163
+ return;
7164
+ }
6982
7165
  try {
6983
7166
  const db = getDb();
6984
7167
  const id = nanoid2();
@@ -7047,9 +7230,10 @@ function recordHookRun(record) {
7047
7230
  metadata: JSON.stringify(metadata)
7048
7231
  });
7049
7232
  }
7050
- var ALLOWED_EVENT_TYPES;
7233
+ var sinkRefusalPrinted = false, ALLOWED_EVENT_TYPES;
7051
7234
  var init_db_writer = __esm(() => {
7052
7235
  init_db();
7236
+ init_local_opt_in();
7053
7237
  init_redact();
7054
7238
  ALLOWED_EVENT_TYPES = new Set([
7055
7239
  "PreToolUse",
@@ -7064,8 +7248,9 @@ var init_db_writer = __esm(() => {
7064
7248
  });
7065
7249
 
7066
7250
  // src/lib/hook-env.ts
7067
- import { dirname as dirname4, isAbsolute as isAbsolute3 } from "path";
7068
- import { existsSync as existsSync12, statSync } from "fs";
7251
+ import { dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
7252
+ import { existsSync as existsSync13, realpathSync as realpathSync2, statSync } from "fs";
7253
+ import { tmpdir } from "os";
7069
7254
  function isDeniedEnvName(name) {
7070
7255
  const upper = name.toUpperCase();
7071
7256
  if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
@@ -7091,6 +7276,19 @@ function isUnsafePathEntry(entry, home) {
7091
7276
  return true;
7092
7277
  if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
7093
7278
  return true;
7279
+ const canonical = (path) => {
7280
+ try {
7281
+ return realpathSync2(path);
7282
+ } catch {
7283
+ return path;
7284
+ }
7285
+ };
7286
+ const resolved = canonical(entry);
7287
+ for (const root of [home, tmpdir(), "/tmp", "/var/tmp"].filter((value) => Boolean(value))) {
7288
+ const blocked = canonical(root);
7289
+ if (resolved === blocked || resolved.startsWith(blocked + "/"))
7290
+ return true;
7291
+ }
7094
7292
  try {
7095
7293
  if ((statSync(entry).mode & 2) !== 0)
7096
7294
  return true;
@@ -7099,9 +7297,9 @@ function isUnsafePathEntry(entry, home) {
7099
7297
  }
7100
7298
  function sanitizeHookPath(pathValue, home) {
7101
7299
  const baseline = PATH_BASELINE.slice();
7102
- if (process.platform === "darwin" && existsSync12("/opt/homebrew/bin"))
7300
+ if (process.platform === "darwin" && existsSync13("/opt/homebrew/bin"))
7103
7301
  baseline.push("/opt/homebrew/bin");
7104
- const bunDir = dirname4(process.execPath);
7302
+ const bunDir = dirname5(process.execPath);
7105
7303
  const seen = new Set;
7106
7304
  const out = [];
7107
7305
  for (const dir of baseline) {
@@ -7210,6 +7408,7 @@ var init_hook_env = __esm(() => {
7210
7408
  DENY_CONTAINS = ["DATABASE_URL"];
7211
7409
  PATH_BASELINE = ["/usr/bin", "/bin", "/usr/local/bin", "/sbin", "/usr/sbin"];
7212
7410
  CONFIG_PROJECTIONS = [
7411
+ ["HASNA_HOOKS_LOCAL", "HOOKS_LOCAL"],
7213
7412
  ["HASNA_HOOKS_DATA_DIR", "HOOKS_DATA_DIR"],
7214
7413
  ["HASNA_HOOKS_DB_PATH", "HOOKS_DB_PATH"],
7215
7414
  ["HASNA_HOOKS_LOCK_PATH", "HOOKS_LOCK_PATH"],
@@ -7224,8 +7423,8 @@ __export(exports_run, {
7224
7423
  HookTimeoutError: () => HookTimeoutError
7225
7424
  });
7226
7425
  import { randomBytes as randomBytes2 } from "crypto";
7227
- import { rmSync as rmSync2, writeFileSync as writeFileSync5 } from "fs";
7228
- import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute4, join as join13 } from "path";
7426
+ import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
7427
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join13 } from "path";
7229
7428
  function extensionOf(scriptPath) {
7230
7429
  const base = basename2(scriptPath);
7231
7430
  const dot = base.lastIndexOf(".");
@@ -7294,7 +7493,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
7294
7493
  return chunks.join("");
7295
7494
  }
7296
7495
  async function executeVerifiedScript(options) {
7297
- const scriptDir = dirname5(options.scriptPath);
7496
+ const scriptDir = dirname6(options.scriptPath);
7298
7497
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
7299
7498
  const tempPath = join13(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
7300
7499
  let proc = null;
@@ -7304,7 +7503,7 @@ async function executeVerifiedScript(options) {
7304
7503
  killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
7305
7504
  }, options.timeout) : null;
7306
7505
  try {
7307
- writeFileSync5(tempPath, options.content, { flag: "wx", mode: 384 });
7506
+ writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
7308
7507
  proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
7309
7508
  stdin: new Response(options.stdin),
7310
7509
  stdout: "pipe",
@@ -7387,15 +7586,15 @@ __export(exports_custom_install, {
7387
7586
  installCustomSource: () => installCustomSource,
7388
7587
  ensureCustomDir: () => ensureCustomDir
7389
7588
  });
7390
- import { existsSync as existsSync13, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
7589
+ import { existsSync as existsSync14, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync3 } from "fs";
7391
7590
  import { join as join14 } from "path";
7392
- import { tmpdir } from "os";
7591
+ import { tmpdir as tmpdir2 } from "os";
7393
7592
  function manifestAt(dir) {
7394
7593
  const path = join14(dir, "manifest.json");
7395
- if (!existsSync13(path))
7594
+ if (!existsSync14(path))
7396
7595
  return;
7397
7596
  try {
7398
- return parseManifest(readFileSync7(path, "utf-8"));
7597
+ return parseManifest(readFileSync8(path, "utf-8"));
7399
7598
  } catch {
7400
7599
  return;
7401
7600
  }
@@ -7414,7 +7613,7 @@ function writeFromManifest(manifest, srcDir, source, kind) {
7414
7613
  return { name, version: manifest.version, kind, source, dir, scriptPath };
7415
7614
  }
7416
7615
  async function cloneToTemp(gitUrl) {
7417
- const tmp = mkdtempSync(join14(tmpdir(), "hooks-install-"));
7616
+ const tmp = mkdtempSync(join14(tmpdir2(), "hooks-install-"));
7418
7617
  const proc = Bun.spawn(["git", "clone", "--depth", "1", "--quiet", gitUrl, join14(tmp, "repo")], {
7419
7618
  stdout: "pipe",
7420
7619
  stderr: "pipe"
@@ -7438,7 +7637,7 @@ function isCustomSource(value) {
7438
7637
  return true;
7439
7638
  if (value.startsWith("http://") || value.startsWith("https://"))
7440
7639
  return true;
7441
- return existsSync13(value);
7640
+ return existsSync14(value);
7442
7641
  }
7443
7642
  async function installCustomSource(source) {
7444
7643
  if (isGitUrl(source)) {
@@ -7452,11 +7651,11 @@ async function installCustomSource(source) {
7452
7651
  if (source.startsWith("http://") || source.startsWith("https://")) {
7453
7652
  return installManifestUrl(source);
7454
7653
  }
7455
- if (!existsSync13(source)) {
7654
+ if (!existsSync14(source)) {
7456
7655
  throw new Error(`No such local path: ${source}`);
7457
7656
  }
7458
7657
  if (source.endsWith("manifest.json")) {
7459
- const manifest = parseManifest(readFileSync7(source, "utf-8"));
7658
+ const manifest = parseManifest(readFileSync8(source, "utf-8"));
7460
7659
  return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
7461
7660
  }
7462
7661
  return copyManifestDir(source, source, "local");
@@ -7487,7 +7686,7 @@ async function installManifestUrl(url) {
7487
7686
  }
7488
7687
  function ensureCustomDir() {
7489
7688
  const dir = getCustomHooksDir();
7490
- mkdirSync6(dir, { recursive: true });
7689
+ mkdirSync7(dir, { recursive: true });
7491
7690
  return dir;
7492
7691
  }
7493
7692
  var init_custom_install = __esm(() => {
@@ -7501,16 +7700,17 @@ __export(exports_sync, {
7501
7700
  syncHooks: () => syncHooks,
7502
7701
  stageSyncArtifacts: () => stageSyncArtifacts,
7503
7702
  planSync: () => planSync,
7703
+ installPinnedFromBundled: () => installPinnedFromBundled,
7504
7704
  fetchPinnedHook: () => fetchPinnedHook,
7505
7705
  commitSyncArtifacts: () => commitSyncArtifacts
7506
7706
  });
7507
7707
  import { createHash as createHash2 } from "crypto";
7508
- import { readFileSync as readFileSync8 } from "fs";
7708
+ import { readFileSync as readFileSync9 } from "fs";
7509
7709
  function sha256OfText(text) {
7510
7710
  return createHash2("sha256").update(text).digest("hex");
7511
7711
  }
7512
7712
  function sha256OfPath(path) {
7513
- return createHash2("sha256").update(readFileSync8(path)).digest("hex");
7713
+ return createHash2("sha256").update(readFileSync9(path)).digest("hex");
7514
7714
  }
7515
7715
  function collectBundledCatalog() {
7516
7716
  const out = [];
@@ -7664,7 +7864,7 @@ async function stageSyncArtifacts(apiUrl, apiKey, names, remoteLock) {
7664
7864
  return staged;
7665
7865
  }
7666
7866
  async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
7667
- const db = getDb();
7867
+ const db = isLocalStoreRefused() ? null : getDb();
7668
7868
  for (const item of staged) {
7669
7869
  const entry = remoteLock.hooks[item.name];
7670
7870
  if (!entry)
@@ -7684,6 +7884,8 @@ async function commitSyncArtifacts(staged, apiUrl, remoteLock) {
7684
7884
  lock.hooks[item.name] = { version: entry.version, sha256: entry.sha256, source: entry.source ?? "remote" };
7685
7885
  }
7686
7886
  writeLock(lock);
7887
+ if (!db)
7888
+ return;
7687
7889
  db.exec("BEGIN");
7688
7890
  try {
7689
7891
  for (const item of staged) {
@@ -7746,23 +7948,56 @@ async function fetchPinnedHook(name, version, apiUrl, apiKey) {
7746
7948
  if (actual !== expectedSha) {
7747
7949
  throw new Error(`sha256 mismatch after write for '${name}@${version}': expected ${expectedSha}, on disk ${actual}`);
7748
7950
  }
7749
- const db = getDb();
7750
7951
  setPinnedHook(name, { version, sha256: expectedSha, source: entry.source ?? "remote", pinned: true });
7751
- upsertHookRecord(db, {
7952
+ if (!isLocalStoreRefused()) {
7953
+ upsertHookRecord(getDb(), {
7954
+ name,
7955
+ version,
7956
+ sha256: expectedSha,
7957
+ source_type: entry.source ?? "remote",
7958
+ source_ref: apiUrl,
7959
+ last_verified_at: new Date().toISOString()
7960
+ });
7961
+ }
7962
+ return {
7752
7963
  name,
7753
7964
  version,
7754
7965
  sha256: expectedSha,
7755
- source_type: entry.source ?? "remote",
7966
+ source: entry.source ?? "remote",
7756
7967
  source_ref: apiUrl,
7968
+ artifact,
7969
+ scriptPath
7970
+ };
7971
+ }
7972
+ async function installPinnedFromBundled(name, version) {
7973
+ const bundled = collectBundledCatalog().find((entry) => entry.name === name);
7974
+ if (!bundled) {
7975
+ throw new Error(`Hook '${name}' is not in the bundled registry`);
7976
+ }
7977
+ if (bundled.version !== version) {
7978
+ throw new Error(`Hook '${name}' version ${version} is not in the bundled registry (available: ${bundled.version})`);
7979
+ }
7980
+ const scriptPath = resolveScriptPath(name);
7981
+ if (!scriptPath) {
7982
+ throw new Error(`Hook '${name}' has no script in the bundled registry`);
7983
+ }
7984
+ const db = getDb();
7985
+ setPinnedHook(name, { version, sha256: bundled.sha256, source: "bundled", pinned: true });
7986
+ upsertHookRecord(db, {
7987
+ name,
7988
+ version,
7989
+ sha256: bundled.sha256,
7990
+ source_type: "bundled",
7991
+ source_ref: "bundled registry",
7757
7992
  last_verified_at: new Date().toISOString()
7758
7993
  });
7759
7994
  return {
7760
7995
  name,
7761
7996
  version,
7762
- sha256: expectedSha,
7763
- source: entry.source ?? "remote",
7764
- source_ref: apiUrl,
7765
- artifact,
7997
+ sha256: bundled.sha256,
7998
+ source: "bundled",
7999
+ source_ref: "bundled registry",
8000
+ artifact: null,
7766
8001
  scriptPath
7767
8002
  };
7768
8003
  }
@@ -8425,13 +8660,13 @@ __export(exports_serve, {
8425
8660
  SERVE_HOST: () => SERVE_HOST,
8426
8661
  DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
8427
8662
  });
8428
- import { readFileSync as readFileSync9 } from "fs";
8429
- import { dirname as dirname6, join as join15 } from "path";
8663
+ import { readFileSync as readFileSync10 } from "fs";
8664
+ import { dirname as dirname7, join as join15 } from "path";
8430
8665
  import { fileURLToPath as fileURLToPath3 } from "url";
8431
8666
  function packageVersion() {
8432
8667
  try {
8433
- const pkgPath = join15(dirname6(fileURLToPath3(import.meta.url)), "..", "package.json");
8434
- return JSON.parse(readFileSync9(pkgPath, "utf-8")).version || "0.0.0";
8668
+ const pkgPath = join15(dirname7(fileURLToPath3(import.meta.url)), "..", "package.json");
8669
+ return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
8435
8670
  } catch {
8436
8671
  return "0.0.0";
8437
8672
  }
@@ -8595,7 +8830,7 @@ function startServeServer(options) {
8595
8830
  return handleServeRequest(req, resolveHooksServePublishKey());
8596
8831
  }
8597
8832
  });
8598
- console.error(`hooks registry listening on http://${host}:${port} (publish requires an API key)`);
8833
+ console.error(`hooks registry listening on http://${host}:${server.port} (publish requires an API key)`);
8599
8834
  return server;
8600
8835
  }
8601
8836
  var DEFAULT_SERVE_PORT = 39428, SERVE_HOST = "127.0.0.1", SERVE_SERVICE_NAME = "hooks-registry";
@@ -8691,6 +8926,32 @@ async function provisionCloudflareResources(options) {
8691
8926
  }
8692
8927
  var CF_API = "https://api.cloudflare.com/client/v4";
8693
8928
 
8929
+ // src/mcp/authority.ts
8930
+ var exports_authority = {};
8931
+ __export(exports_authority, {
8932
+ resolveHooksMcpAuthority: () => resolveHooksMcpAuthority,
8933
+ decideHooksMcpAuthority: () => decideHooksMcpAuthority
8934
+ });
8935
+ function resolveHooksMcpAuthority(env = process.env) {
8936
+ const transport = resolveHooksTransport(env);
8937
+ if (transport.mode === "local" || !transport.authority) {
8938
+ return { route: "local", source: "local-opt-in" };
8939
+ }
8940
+ return { route: "hosted", v1BaseUrl: transport.authority.v1BaseUrl, source: transport.source };
8941
+ }
8942
+ function decideHooksMcpAuthority(env = process.env) {
8943
+ const authority = resolveHooksMcpAuthority(env);
8944
+ if (authority.route === "hosted") {
8945
+ refuseLocalStore(hooksHostedRouteLocalStoreRefusal(authority.v1BaseUrl));
8946
+ }
8947
+ return authority;
8948
+ }
8949
+ var init_authority = __esm(() => {
8950
+ init_db();
8951
+ init_local_opt_in();
8952
+ init_transport2();
8953
+ });
8954
+
8694
8955
  // src/mcp/server.ts
8695
8956
  var exports_server = {};
8696
8957
  __export(exports_server, {
@@ -8706,17 +8967,17 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
8706
8967
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8707
8968
  import { z as z2 } from "zod";
8708
8969
  import { createServer } from "http";
8709
- import { existsSync as existsSync14, readFileSync as readFileSync10 } from "fs";
8710
- import { join as join16, dirname as dirname7 } from "path";
8970
+ import { existsSync as existsSync15, readFileSync as readFileSync11 } from "fs";
8971
+ import { join as join16, dirname as dirname8 } from "path";
8711
8972
  import { fileURLToPath as fileURLToPath4 } from "url";
8712
8973
  async function runVerifiedHook(name, stdin, timeoutMs) {
8713
8974
  const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
8714
8975
  const resolved = resolveHook2(name);
8715
8976
  const hookScript = resolveScriptPath2(name);
8716
- if (!resolved || !hookScript || !existsSync14(hookScript)) {
8977
+ if (!resolved || !hookScript || !existsSync15(hookScript)) {
8717
8978
  throw new Error(`Hook '${name}' not found`);
8718
8979
  }
8719
- const content = readFileSync10(hookScript);
8980
+ const content = readFileSync11(hookScript);
8720
8981
  const sha = sha256Of(content);
8721
8982
  const check = checkScriptHash(name, sha);
8722
8983
  if (!check.ok) {
@@ -8921,7 +9182,7 @@ function createHooksServer() {
8921
9182
  const settingsPath = getSettingsPath(scope);
8922
9183
  const issues = [];
8923
9184
  const healthy = [];
8924
- const settingsExist = existsSync14(settingsPath);
9185
+ const settingsExist = existsSync15(settingsPath);
8925
9186
  if (!settingsExist) {
8926
9187
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
8927
9188
  }
@@ -8931,7 +9192,7 @@ function createHooksServer() {
8931
9192
  const meta = custom ? resolveHookMeta(name) : getHook(name);
8932
9193
  let hookHealthy = true;
8933
9194
  if (custom) {
8934
- if (!existsSync14(custom.scriptPath)) {
9195
+ if (!existsSync15(custom.scriptPath)) {
8935
9196
  issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
8936
9197
  hookHealthy = false;
8937
9198
  }
@@ -8941,14 +9202,14 @@ function createHooksServer() {
8941
9202
  continue;
8942
9203
  }
8943
9204
  const hookDir = getHookPath(name);
8944
- if (!existsSync14(join16(hookDir, "src", "hook.ts"))) {
9205
+ if (!existsSync15(join16(hookDir, "src", "hook.ts"))) {
8945
9206
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
8946
9207
  hookHealthy = false;
8947
9208
  }
8948
9209
  }
8949
9210
  if (meta && settingsExist) {
8950
9211
  try {
8951
- const settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
9212
+ const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
8952
9213
  if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
8953
9214
  const eventName = meta.event.split(":")[0];
8954
9215
  issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
@@ -8987,8 +9248,8 @@ function createHooksServer() {
8987
9248
  const hookPath = getHookPath(name);
8988
9249
  const readmePath = join16(hookPath, "README.md");
8989
9250
  let readme = "";
8990
- if (existsSync14(readmePath)) {
8991
- readme = readFileSync10(readmePath, "utf-8");
9251
+ if (existsSync15(readmePath)) {
9252
+ readme = readFileSync11(readmePath, "utf-8");
8992
9253
  }
8993
9254
  if (verbose) {
8994
9255
  return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
@@ -9171,7 +9432,7 @@ function createHooksServer() {
9171
9432
  const ctx = {
9172
9433
  scope,
9173
9434
  settings_path: settingsPath,
9174
- settings_exists: existsSync14(settingsPath),
9435
+ settings_exists: existsSync15(settingsPath),
9175
9436
  registered_hooks: hooks,
9176
9437
  hook_count: hooks.length,
9177
9438
  healthy,
@@ -9317,8 +9578,8 @@ function createHooksServer() {
9317
9578
  const settingsPath = getSettingsPath(scope);
9318
9579
  let settings = {};
9319
9580
  try {
9320
- if (existsSync14(settingsPath))
9321
- settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
9581
+ if (existsSync15(settingsPath))
9582
+ settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
9322
9583
  } catch {}
9323
9584
  if (!settings.hooks)
9324
9585
  settings.hooks = {};
@@ -9326,10 +9587,10 @@ function createHooksServer() {
9326
9587
  if (!disabled.includes(name))
9327
9588
  disabled.push(name);
9328
9589
  settings.hooks.__disabled = disabled;
9329
- const { writeFileSync: writeFileSync6, mkdirSync: mkdirSync7 } = await import("fs");
9330
- const { dirname: dirname8 } = await import("path");
9331
- mkdirSync7(dirname8(settingsPath), { recursive: true });
9332
- writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + `
9590
+ const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
9591
+ const { dirname: dirname9 } = await import("path");
9592
+ mkdirSync8(dirname9(settingsPath), { recursive: true });
9593
+ writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
9333
9594
  `);
9334
9595
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
9335
9596
  });
@@ -9340,15 +9601,15 @@ function createHooksServer() {
9340
9601
  const settingsPath = getSettingsPath(scope);
9341
9602
  let settings = {};
9342
9603
  try {
9343
- if (existsSync14(settingsPath))
9344
- settings = JSON.parse(readFileSync10(settingsPath, "utf-8"));
9604
+ if (existsSync15(settingsPath))
9605
+ settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
9345
9606
  } catch {}
9346
9607
  if (settings.hooks?.__disabled) {
9347
9608
  settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
9348
9609
  if (settings.hooks.__disabled.length === 0)
9349
9610
  delete settings.hooks.__disabled;
9350
- const { writeFileSync: writeFileSync6 } = await import("fs");
9351
- writeFileSync6(settingsPath, JSON.stringify(settings, null, 2) + `
9611
+ const { writeFileSync: writeFileSync7 } = await import("fs");
9612
+ writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
9352
9613
  `);
9353
9614
  }
9354
9615
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: false, scope }) }] };
@@ -9632,6 +9893,7 @@ async function startSSEServer(options = {}) {
9632
9893
  if (!loopback && !authToken) {
9633
9894
  throw new Error(`Refusing to serve MCP SSE on non-loopback host '${host}' without an auth token. Set HASNA_HOOKS_MCP_TOKEN (or HOOKS_MCP_TOKEN) to authorize a non-loopback bind.`);
9634
9895
  }
9896
+ decideHooksMcpAuthority();
9635
9897
  const server = createHooksServer();
9636
9898
  const transports = new Map;
9637
9899
  function authorized2(headers) {
@@ -9681,6 +9943,13 @@ async function startSSEServer(options = {}) {
9681
9943
  });
9682
9944
  }
9683
9945
  async function startStdioServer() {
9946
+ try {
9947
+ decideHooksMcpAuthority();
9948
+ } catch (err) {
9949
+ process.stderr.write(`${err instanceof Error ? err.message : String(err)}
9950
+ `);
9951
+ process.exit(1);
9952
+ }
9684
9953
  try {
9685
9954
  const server = createHooksServer();
9686
9955
  const transport = new StdioServerTransport;
@@ -9701,14 +9970,15 @@ var init_server = __esm(() => {
9701
9970
  init_registration();
9702
9971
  init_store();
9703
9972
  init_redact();
9973
+ init_authority();
9704
9974
  init_storage();
9705
- __dirname3 = dirname7(fileURLToPath4(import.meta.url));
9975
+ __dirname3 = dirname8(fileURLToPath4(import.meta.url));
9706
9976
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
9707
9977
  try {
9708
9978
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
9709
9979
  const p = join16(__dirname3, rel);
9710
- if (existsSync14(p)) {
9711
- pkg = JSON.parse(readFileSync10(p, "utf-8"));
9980
+ if (existsSync15(p)) {
9981
+ pkg = JSON.parse(readFileSync11(p, "utf-8"));
9712
9982
  break;
9713
9983
  }
9714
9984
  }
@@ -10892,11 +11162,11 @@ function setPath(input, path, replacement) {
10892
11162
  if (last && last in cursor)
10893
11163
  cursor[last] = replacement;
10894
11164
  }
11165
+ var MAX_PACKAGE_BYTES = 1024 * 1024;
10895
11166
  var MAX_CREDENTIAL_FILE_BYTES = 64 * 1024;
10896
11167
  var INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
10897
11168
  var CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
10898
11169
  var AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
10899
- var SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
10900
11170
  var requireSecretsSdk = createRequire(import.meta.url);
10901
11171
  var IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
10902
11172
  var AUTHORITY_OVERRIDE_HEADERS = new Set([
@@ -11450,8 +11720,8 @@ var {
11450
11720
 
11451
11721
  // src/cli/index.tsx
11452
11722
  import chalk2 from "chalk";
11453
- import { existsSync as existsSync15, readFileSync as readFileSync11, rmSync as rmSync4 } from "fs";
11454
- import { join as join17, dirname as dirname8 } from "path";
11723
+ import { existsSync as existsSync16, readFileSync as readFileSync12, rmSync as rmSync4 } from "fs";
11724
+ import { join as join17, dirname as dirname9 } from "path";
11455
11725
  import { fileURLToPath as fileURLToPath5 } from "url";
11456
11726
 
11457
11727
  // src/cli/components/App.tsx
@@ -12293,7 +12563,7 @@ function InstallProgress({
12293
12563
  onComplete
12294
12564
  }) {
12295
12565
  const [results, setResults] = useState6([]);
12296
- const [current, setCurrent] = useState6(0);
12566
+ const [current2, setCurrent] = useState6(0);
12297
12567
  const [installing, setInstalling] = useState6(true);
12298
12568
  useEffect5(() => {
12299
12569
  const install = async () => {
@@ -12317,12 +12587,12 @@ function InstallProgress({
12317
12587
  marginBottom: 1,
12318
12588
  children: /* @__PURE__ */ jsxDEV6(Text9, {
12319
12589
  bold: true,
12320
- children: installing ? `Installing hooks (${current + 1}/${hooks.length})...` : "Installation complete!"
12590
+ children: installing ? `Installing hooks (${current2 + 1}/${hooks.length})...` : "Installation complete!"
12321
12591
  }, undefined, false, undefined, this)
12322
12592
  }, undefined, false, undefined, this),
12323
12593
  hooks.map((name, i) => {
12324
12594
  const result = results[i];
12325
- const isCurrent = i === current && installing;
12595
+ const isCurrent = i === current2 && installing;
12326
12596
  const meta = getHook(name);
12327
12597
  return /* @__PURE__ */ jsxDEV6(Box7, {
12328
12598
  flexDirection: "column",
@@ -12637,10 +12907,11 @@ init_app_home();
12637
12907
  init_config();
12638
12908
  init_local_opt_in();
12639
12909
  init_transport2();
12910
+ init_db();
12640
12911
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
12641
- var __dirname4 = dirname8(fileURLToPath5(import.meta.url));
12642
- var pkgPath = existsSync15(join17(__dirname4, "..", "package.json")) ? join17(__dirname4, "..", "package.json") : join17(__dirname4, "..", "..", "package.json");
12643
- var pkg2 = JSON.parse(readFileSync11(pkgPath, "utf-8"));
12912
+ var __dirname4 = dirname9(fileURLToPath5(import.meta.url));
12913
+ var pkgPath = existsSync16(join17(__dirname4, "..", "package.json")) ? join17(__dirname4, "..", "package.json") : join17(__dirname4, "..", "..", "package.json");
12914
+ var pkg2 = JSON.parse(readFileSync12(pkgPath, "utf-8"));
12644
12915
  var program2 = new Command;
12645
12916
  function resolveScope(options) {
12646
12917
  if (options.project)
@@ -12650,12 +12921,45 @@ function resolveScope(options) {
12650
12921
  function resolveTarget(options) {
12651
12922
  if (options.target === "gemini")
12652
12923
  return "gemini";
12924
+ if (options.target === "codex")
12925
+ return "codex";
12653
12926
  if (options.target === "codewith")
12654
12927
  return "codewith";
12655
12928
  if (options.target === "all")
12656
12929
  return "all";
12657
12930
  return "claude";
12658
12931
  }
12932
+ function resolveConcreteTarget(options) {
12933
+ if (options.target === "gemini")
12934
+ return "gemini";
12935
+ if (options.target === "codex")
12936
+ return "codex";
12937
+ if (options.target === "codewith")
12938
+ return "codewith";
12939
+ return "claude";
12940
+ }
12941
+ function formatSettingsPath(scope, target) {
12942
+ if (target === "all")
12943
+ return "target-specific settings";
12944
+ const actual = getSettingsPath(scope, target);
12945
+ if (scope === "project") {
12946
+ if (target === "codewith")
12947
+ return ".codewith/config.toml";
12948
+ if (target === "gemini")
12949
+ return ".gemini/settings.json";
12950
+ if (target === "codex")
12951
+ return ".codex/hooks.json";
12952
+ return ".claude/settings.json";
12953
+ }
12954
+ if (target === "codewith") {
12955
+ return process.env.HASNA_HOOKS_CODEWITH_CONFIG_PATH ? "$HASNA_HOOKS_CODEWITH_CONFIG_PATH" : "~/.codewith/config.toml";
12956
+ }
12957
+ if (target === "gemini")
12958
+ return "~/.gemini/settings.json";
12959
+ if (target === "codex")
12960
+ return "~/.codex/hooks.json";
12961
+ return actual === getSettingsPath("global", "claude") ? "~/.claude/settings.json" : actual;
12962
+ }
12659
12963
  function parseLimit(value, fallback, max) {
12660
12964
  const parsed = value ? parseInt(value, 10) : fallback;
12661
12965
  if (!Number.isFinite(parsed) || parsed <= 0)
@@ -12671,10 +12975,23 @@ function truncateText2(value, max = 96) {
12671
12975
  function projectLogRows2(rows) {
12672
12976
  return rows.map((row) => projectEventRowForRead({ ...row }));
12673
12977
  }
12978
+ async function openLocalStoreOrExit(json2) {
12979
+ try {
12980
+ const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
12981
+ return getDb2();
12982
+ } catch (error) {
12983
+ const message = error instanceof Error ? error.message : String(error);
12984
+ if (json2)
12985
+ console.log(JSON.stringify({ error: message }));
12986
+ else
12987
+ console.error(chalk2.red(`\u2717 ${message}`));
12988
+ process.exit(1);
12989
+ }
12990
+ }
12674
12991
  function readToken(tokenFile) {
12675
12992
  if (tokenFile) {
12676
12993
  try {
12677
- return readFileSync11(tokenFile, "utf-8").trim();
12994
+ return readFileSync12(tokenFile, "utf-8").trim();
12678
12995
  } catch {
12679
12996
  return;
12680
12997
  }
@@ -12735,6 +13052,11 @@ function suggestHooks(name, max = 3) {
12735
13052
  }
12736
13053
  program2.name("hooks").description("Install hooks for AI coding agents").version(pkg2.version);
12737
13054
  program2.command("interactive", { isDefault: true }).alias("i").description("Interactive hook browser").action(() => {
13055
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
13056
+ console.error(chalk2.red("The interactive hook browser requires a TTY terminal.") + `
13057
+ ` + chalk2.dim("Use `hooks search <query>`, `hooks list`, or `hooks categories` for non-interactive use."));
13058
+ process.exit(1);
13059
+ }
12738
13060
  render(/* @__PURE__ */ jsxDEV8(App, {}, undefined, false, undefined, this));
12739
13061
  });
12740
13062
  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) => {
@@ -12823,11 +13145,11 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
12823
13145
  process.exit(1);
12824
13146
  }
12825
13147
  const hookScript = resolveScriptPath2(hook);
12826
- if (!hookScript || !existsSync15(hookScript)) {
13148
+ if (!hookScript || !existsSync16(hookScript)) {
12827
13149
  console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
12828
13150
  process.exit(1);
12829
13151
  }
12830
- const content = readFileSync11(hookScript);
13152
+ const content = readFileSync12(hookScript);
12831
13153
  const sha = sha256Of2(content);
12832
13154
  const check = checkScriptHash2(hook, sha);
12833
13155
  if (!check.ok) {
@@ -12933,7 +13255,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
12933
13255
  process.stderr.write(stderr);
12934
13256
  process.exit(exitCode);
12935
13257
  });
12936
- 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, all (default: claude)", "claude").option("--profile <id>", "Agent profile ID to scope hooks to").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) => {
13258
+ 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("--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) => {
12937
13259
  const scope = resolveScope(options);
12938
13260
  const target = resolveTarget(options);
12939
13261
  let toInstall = hooks;
@@ -12970,6 +13292,7 @@ program2.command("install").alias("add").argument("[hooks...]", "Hooks to instal
12970
13292
  const { readCustomManifest: readCustomManifest2, HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
12971
13293
  const { resolveHooksTransport: resolveHooksTransport2 } = await Promise.resolve().then(() => (init_transport2(), exports_transport));
12972
13294
  const { sha256Of: sha256Of2, pinInstalledHook: pinInstalledHook2 } = await Promise.resolve().then(() => (init_store(), exports_store));
13295
+ const { installPinnedFromBundled: installPinnedFromBundled2, fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
12973
13296
  const { readFileSync: readFileSyncFs } = await import("fs");
12974
13297
  function parseNameVersion(arg) {
12975
13298
  if (isCustomSource2(arg))
@@ -13062,17 +13385,13 @@ Dry run \u2014 would install (${scope}, ${target}):
13062
13385
  for (const { arg, pinned } of pinnedRequests) {
13063
13386
  try {
13064
13387
  const transport = resolveHooksTransport2();
13065
- if (transport.mode !== "remote" || !transport.authority) {
13066
- throw new Error(`Cannot install '${arg}': no remote registry configured (set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or opt into local mode with HASNA_HOOKS_LOCAL=1)`);
13067
- }
13068
- const { origin, apiKey } = transport.authority;
13069
- const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
13070
- const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, origin, apiKey);
13388
+ const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority.apiKey) : await installPinnedFromBundled2(pinned.name, pinned.version);
13071
13389
  installedPinned.push(pinnedInstall.name);
13072
13390
  if (options.json) {
13073
13391
  console.log(JSON.stringify({ pinned: { request: arg, name: pinnedInstall.name, version: pinnedInstall.version, sha256: pinnedInstall.sha256, source: pinnedInstall.source } }));
13074
13392
  } else {
13075
- console.log(chalk2.green(`\u2713 Installed '${pinnedInstall.name}' v${pinnedInstall.version} from registry (sha256 ${pinnedInstall.sha256.slice(0, 12)}\u2026)`));
13393
+ const registryLabel = transport.mode === "remote" ? "registry" : "bundled registry";
13394
+ console.log(chalk2.green(`\u2713 Installed '${pinnedInstall.name}' v${pinnedInstall.version} from ${registryLabel} (sha256 ${pinnedInstall.sha256.slice(0, 12)}\u2026)`));
13076
13395
  }
13077
13396
  } catch (error) {
13078
13397
  const message = error instanceof Error ? error.message : String(error);
@@ -13115,7 +13434,8 @@ Dry run \u2014 would install (${scope}, ${target}):
13115
13434
  success: results.filter((r) => r.success).length,
13116
13435
  scope,
13117
13436
  target,
13118
- applied: results.some((r) => r.applied)
13437
+ applied: results.some((r) => r.applied),
13438
+ ...target === "codex" ? { notes: results.filter((r) => r.success && r.note).map((r) => r.note) } : {}
13119
13439
  }));
13120
13440
  if (results.some((r) => !r.success)) {
13121
13441
  process.exitCode = 1;
@@ -13123,7 +13443,7 @@ Dry run \u2014 would install (${scope}, ${target}):
13123
13443
  return;
13124
13444
  }
13125
13445
  const successCount = results.filter((r) => r.success).length;
13126
- const settingsFile = target === "codewith" ? options.applyCodewith ? options.codewithConfig : "TOML fragment only (configs should apply)" : scope === "project" ? ".claude/settings.json" : "~/.claude/settings.json";
13446
+ const settingsFile = target === "codewith" ? options.applyCodewith ? options.codewithConfig : "TOML fragment only (configs should apply)" : formatSettingsPath(scope, target);
13127
13447
  console.log(chalk2.bold(`
13128
13448
  Installing hooks (${scope}, ${target})...
13129
13449
  `));
@@ -13137,6 +13457,8 @@ Installing hooks (${scope}, ${target})...
13137
13457
  if (result.conflict) {
13138
13458
  console.log(chalk2.yellow(` \u26A0 Warning: ${result.conflict}`));
13139
13459
  }
13460
+ if (result.note && target === "codex")
13461
+ console.log(chalk2.dim(` ${result.note}`));
13140
13462
  if (result.fragment && target === "codewith") {
13141
13463
  console.log(chalk2.dim(" Codewith TOML fragment:"));
13142
13464
  console.log(chalk2.cyan(result.fragment.trimEnd().split(`
@@ -13163,12 +13485,12 @@ Installing hooks (${scope}, ${target})...
13163
13485
  console.log(chalk2.dim(`
13164
13486
  Registered in ${settingsFile}`));
13165
13487
  });
13166
- 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 (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) => {
13488
+ 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) => {
13167
13489
  const scope = resolveScope(options);
13168
13490
  const limit = options.all ? Number.MAX_SAFE_INTEGER : parseLimit(options.limit, 20, 200);
13169
13491
  const storeHooks = listStoreHooks();
13170
13492
  if (options.registered || options.installed) {
13171
- const target = options.target === "gemini" ? "gemini" : options.target === "codewith" ? "codewith" : "claude";
13493
+ const target = resolveConcreteTarget(options);
13172
13494
  const registered = getRegisteredHooksForTarget(scope, target);
13173
13495
  const installed = [...new Set([...registered, ...storeHooks.map((s) => s.meta.name)])];
13174
13496
  const shown = options.registered ? registered : installed;
@@ -13291,7 +13613,7 @@ Found ${results.length} hook(s), showing ${visible.length}:
13291
13613
  console.log(hookSummaryLine(h, { verbose: options.verbose }));
13292
13614
  printDisclosureHint(results.length - visible.length, "hooks info <name>");
13293
13615
  });
13294
- 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, 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) => {
13616
+ 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) => {
13295
13617
  const scope = resolveScope(options);
13296
13618
  const target = resolveTarget(options);
13297
13619
  const { uninstallHook: uninstallHook2 } = await Promise.resolve().then(() => (init_installer(), exports_installer));
@@ -13407,7 +13729,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
13407
13729
  });
13408
13730
  }
13409
13731
  const staleNames = new Set(stale.map((finding) => finding.hook));
13410
- const settingsExist = existsSync15(settingsPath);
13732
+ const settingsExist = existsSync16(settingsPath);
13411
13733
  if (!settingsExist) {
13412
13734
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
13413
13735
  }
@@ -13415,7 +13737,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
13415
13737
  let wiringCount = 0;
13416
13738
  if (settingsExist) {
13417
13739
  try {
13418
- const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
13740
+ const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
13419
13741
  wiringCount = countSettingsWiring(settings);
13420
13742
  } catch {}
13421
13743
  }
@@ -13426,7 +13748,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
13426
13748
  const meta = custom ? resolveHookMeta(name) : getHook(name);
13427
13749
  let hookHealthy = true;
13428
13750
  if (custom) {
13429
- if (!existsSync15(custom.scriptPath)) {
13751
+ if (!existsSync16(custom.scriptPath)) {
13430
13752
  issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
13431
13753
  hookHealthy = false;
13432
13754
  }
@@ -13438,14 +13760,14 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
13438
13760
  }
13439
13761
  const hookDir = getHookPath(name);
13440
13762
  const hookScript = join17(hookDir, "src", "hook.ts");
13441
- if (!existsSync15(hookScript)) {
13763
+ if (!existsSync16(hookScript)) {
13442
13764
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
13443
13765
  hookHealthy = false;
13444
13766
  }
13445
13767
  }
13446
13768
  if (meta && settingsExist) {
13447
13769
  try {
13448
- const settings = JSON.parse(readFileSync11(settingsPath, "utf-8"));
13770
+ const settings = JSON.parse(readFileSync12(settingsPath, "utf-8"));
13449
13771
  if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
13450
13772
  const eventName = meta.event.split(":")[0];
13451
13773
  issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
@@ -13547,8 +13869,9 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
13547
13869
  }
13548
13870
  const { resolveHook: resolveHook2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
13549
13871
  const { sha256File: sha256File2, setPinnedHook: setPinnedHook2, upsertHookRecord: upsertHookRecord2 } = await Promise.resolve().then(() => (init_store(), exports_store));
13550
- const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
13872
+ const { getDb: getDb2, isLocalStoreRefused: isLocalStoreRefused2 } = await Promise.resolve().then(() => (init_db(), exports_db));
13551
13873
  const { resolveHooksTransport: resolveHooksTransport2 } = await Promise.resolve().then(() => (init_transport2(), exports_transport));
13874
+ const { installPinnedFromBundled: installPinnedFromBundled2, fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
13552
13875
  const { isCustomSource: isCustomSource2 } = await Promise.resolve().then(() => (init_custom_install(), exports_custom_install));
13553
13876
  const { HOOK_NAME_RE: HOOK_NAME_RE2 } = await Promise.resolve().then(() => (init_manifest(), exports_manifest));
13554
13877
  function parseNameVersion(arg) {
@@ -13571,12 +13894,7 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
13571
13894
  if (pinned) {
13572
13895
  try {
13573
13896
  const transport = resolveHooksTransport2();
13574
- if (transport.mode !== "remote" || !transport.authority) {
13575
- throw new Error(`Cannot update '${arg}': no remote registry configured (set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or opt into local mode with HASNA_HOOKS_LOCAL=1)`);
13576
- }
13577
- const { origin, apiKey } = transport.authority;
13578
- const { fetchPinnedHook: fetchPinnedHook2 } = await Promise.resolve().then(() => (init_sync(), exports_sync));
13579
- const pinnedInstall = await fetchPinnedHook2(pinned.name, pinned.version, origin, apiKey);
13897
+ const pinnedInstall = transport.mode === "remote" && transport.authority ? await fetchPinnedHook2(pinned.name, pinned.version, transport.authority.origin, transport.authority.apiKey) : await installPinnedFromBundled2(pinned.name, pinned.version);
13580
13898
  const registered = installHook(pinnedInstall.name, { scope, overwrite: true });
13581
13899
  if (!registered.success) {
13582
13900
  results.push({ hook: arg, success: false, error: registered.error ?? "registration failed" });
@@ -13601,13 +13919,15 @@ program2.command("update").argument("[hooks...]", "Hooks to update (defaults to
13601
13919
  if (result.success && resolved) {
13602
13920
  const hash = await sha256File2(resolved.scriptPath);
13603
13921
  setPinnedHook2(resolved.name, { version: resolved.version, sha256: hash, source: resolved.source });
13604
- upsertHookRecord2(getDb2(), {
13605
- name: resolved.name,
13606
- version: resolved.version,
13607
- sha256: hash,
13608
- source_type: resolved.source,
13609
- last_verified_at: new Date().toISOString()
13610
- });
13922
+ if (!isLocalStoreRefused2()) {
13923
+ upsertHookRecord2(getDb2(), {
13924
+ name: resolved.name,
13925
+ version: resolved.version,
13926
+ sha256: hash,
13927
+ source_type: resolved.source,
13928
+ last_verified_at: new Date().toISOString()
13929
+ });
13930
+ }
13611
13931
  results.push({ ...result, pinned: { version: resolved.version, sha256: hash } });
13612
13932
  continue;
13613
13933
  }
@@ -13652,8 +13972,8 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
13652
13972
  const hookPath = getHookPath(hook);
13653
13973
  const readmePath = join17(hookPath, "README.md");
13654
13974
  let readme = "";
13655
- if (existsSync15(readmePath)) {
13656
- readme = readFileSync11(readmePath, "utf-8");
13975
+ if (existsSync16(readmePath)) {
13976
+ readme = readFileSync12(readmePath, "utf-8");
13657
13977
  }
13658
13978
  if (options.json) {
13659
13979
  console.log(JSON.stringify({ ...meta, readme }));
@@ -13772,7 +14092,7 @@ ${meta.displayName} v${meta.version}
13772
14092
  console.log();
13773
14093
  });
13774
14094
  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) => {
13775
- const current = pkg2.version;
14095
+ const current2 = pkg2.version;
13776
14096
  let pm = "npm";
13777
14097
  try {
13778
14098
  const which = Bun.spawnSync(["which", "bun"]);
@@ -13790,13 +14110,13 @@ program2.command("upgrade").option("-c, --check", "Check for updates without ins
13790
14110
  }
13791
14111
  process.exit(1);
13792
14112
  }
13793
- const upToDate = current === latest2;
14113
+ const upToDate = current2 === latest2;
13794
14114
  if (options.json) {
13795
- console.log(JSON.stringify({ current, latest: latest2, upToDate }));
14115
+ console.log(JSON.stringify({ current: current2, latest: latest2, upToDate }));
13796
14116
  } else if (upToDate) {
13797
- console.log(chalk2.green(`\u2713 Already on latest version (${current})`));
14117
+ console.log(chalk2.green(`\u2713 Already on latest version (${current2})`));
13798
14118
  } else {
13799
- console.log(chalk2.yellow(`Update available: ${current} \u2192 ${latest2}`));
14119
+ console.log(chalk2.yellow(`Update available: ${current2} \u2192 ${latest2}`));
13800
14120
  console.log(chalk2.dim(` Run: hooks upgrade`));
13801
14121
  }
13802
14122
  return;
@@ -13817,7 +14137,7 @@ Upgrading @hasna/hooks (${pm})...
13817
14137
  const exitCode = await proc.exited;
13818
14138
  if (exitCode !== 0) {
13819
14139
  if (options.json) {
13820
- console.log(JSON.stringify({ current, updated: false, error: `${pm} exited with code ${exitCode}` }));
14140
+ console.log(JSON.stringify({ current: current2, updated: false, error: `${pm} exited with code ${exitCode}` }));
13821
14141
  } else {
13822
14142
  console.log(chalk2.red(`
13823
14143
  \u2717 Upgrade failed (exit code ${exitCode})`));
@@ -13827,18 +14147,18 @@ Upgrading @hasna/hooks (${pm})...
13827
14147
  const versionProc = Bun.spawnSync(["npm", "view", "@hasna/hooks", "version"]);
13828
14148
  const latest = new TextDecoder().decode(versionProc.stdout).trim() || "unknown";
13829
14149
  if (options.json) {
13830
- console.log(JSON.stringify({ current, latest, updated: true }));
14150
+ console.log(JSON.stringify({ current: current2, latest, updated: true }));
13831
14151
  } else {
13832
14152
  console.log(chalk2.green(`
13833
- \u2713 Upgraded: ${current} \u2192 ${latest}`));
14153
+ \u2713 Upgraded: ${current2} \u2192 ${latest}`));
13834
14154
  }
13835
14155
  });
13836
14156
  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) => {
13837
14157
  const profiles = exportProfiles();
13838
14158
  const json2 = JSON.stringify(profiles, null, 2);
13839
14159
  if (options.output) {
13840
- const { writeFileSync: writeFileSync6 } = await import("fs");
13841
- writeFileSync6(options.output, json2 + `
14160
+ const { writeFileSync: writeFileSync7 } = await import("fs");
14161
+ writeFileSync7(options.output, json2 + `
13842
14162
  `);
13843
14163
  console.log(chalk2.green(`\u2713 Exported ${profiles.length} profile(s) to ${options.output}`));
13844
14164
  } else {
@@ -13850,9 +14170,9 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
13850
14170
  if (file === "-") {
13851
14171
  raw = await new Response(Bun.stdin.stream()).text();
13852
14172
  } else {
13853
- const { readFileSync: readFileSync12 } = await import("fs");
14173
+ const { readFileSync: readFileSync13 } = await import("fs");
13854
14174
  try {
13855
- raw = readFileSync12(file, "utf-8");
14175
+ raw = readFileSync13(file, "utf-8");
13856
14176
  } catch {
13857
14177
  if (options.json) {
13858
14178
  console.log(JSON.stringify({ error: `Cannot read file: ${file}` }));
@@ -13885,8 +14205,7 @@ program2.command("profile-import").argument("<file>", "JSON file to import profi
13885
14205
  });
13886
14206
  var logCmd = program2.command("log").description(`Query hook event logs from SQLite (${getReportedDbPath()})`);
13887
14207
  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) => {
13888
- const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
13889
- const db = getDb2();
14208
+ const db = await openLocalStoreOrExit(options.json);
13890
14209
  const limit = parseInt(options.limit) || 50;
13891
14210
  let sql = "SELECT * FROM hook_events WHERE 1=1";
13892
14211
  const params = [];
@@ -13923,8 +14242,7 @@ logCmd.command("list").description("List hook events").option("--hook <name>", "
13923
14242
  Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
13924
14243
  });
13925
14244
  logCmd.command("search <text>").description("Search hook events by tool_input or error text").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (text, options) => {
13926
- const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
13927
- const db = getDb2();
14245
+ const db = await openLocalStoreOrExit(options.json);
13928
14246
  const limit = parseInt(options.limit) || 50;
13929
14247
  const q = `%${text}%`;
13930
14248
  const rows = db.query("SELECT * FROM hook_events WHERE tool_input LIKE ? OR error LIKE ? ORDER BY timestamp DESC LIMIT ?").all(q, q, limit);
@@ -13949,8 +14267,7 @@ logCmd.command("search <text>").description("Search hook events by tool_input or
13949
14267
  Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
13950
14268
  });
13951
14269
  logCmd.command("tail").description("Show most recent hook events").option("-n <n>", "Number of rows", "20").option("-j, --json", "Output as JSON", false).action(async (options) => {
13952
- const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
13953
- const db = getDb2();
14270
+ const db = await openLocalStoreOrExit(options.json);
13954
14271
  const limit = parseInt(options.n) || 20;
13955
14272
  const rows = db.query("SELECT * FROM hook_events ORDER BY timestamp DESC LIMIT ?").all(limit);
13956
14273
  const projected = projectLogRows2(rows);
@@ -13975,8 +14292,7 @@ logCmd.command("tail").description("Show most recent hook events").option("-n <n
13975
14292
  Compact rows shown. Use --json for full event records or -n <n> to change row count.`));
13976
14293
  });
13977
14294
  logCmd.command("errors").description("Show hook events that contain errors").option("--since <duration>", "Only show errors since this duration (e.g. 1h, 30m, 7d)", "24h").option("-n, --limit <n>", "Number of rows to show", "50").option("-j, --json", "Output as JSON", false).action(async (options) => {
13978
- const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
13979
- const db = getDb2();
14295
+ const db = await openLocalStoreOrExit(options.json);
13980
14296
  const limit = parseInt(options.limit) || 50;
13981
14297
  function parseDuration(s) {
13982
14298
  const m = s.match(/^(\d+)(s|m|h|d)$/);
@@ -14018,8 +14334,7 @@ logCmd.command("errors").description("Show hook events that contain errors").opt
14018
14334
  Compact rows shown. Use --json for full event records or --limit <n> to change row count.`));
14019
14335
  });
14020
14336
  logCmd.command("clear").description("Delete hook event logs").option("--hook <name>", "Only delete events for this hook").option("-y, --yes", "Skip confirmation prompt", false).action(async (options) => {
14021
- const { getDb: getDb2 } = await Promise.resolve().then(() => (init_db(), exports_db));
14022
- const db = getDb2();
14337
+ const db = await openLocalStoreOrExit(false);
14023
14338
  const countRow = options.hook ? db.query("SELECT COUNT(*) as n FROM hook_events WHERE hook_name = ?").get(options.hook) : db.query("SELECT COUNT(*) as n FROM hook_events").get();
14024
14339
  const count = countRow?.n ?? 0;
14025
14340
  if (count === 0) {
@@ -14041,19 +14356,28 @@ logCmd.command("clear").description("Delete hook event logs").option("--hook <na
14041
14356
  });
14042
14357
  var storageCmd = program2.command("storage").description("Sync local hook data with storage PostgreSQL");
14043
14358
  storageCmd.command("status").description("Show storage sync status").option("-j, --json", "Output as JSON", false).action(async (options) => {
14044
- const { getStorageStatus: getStorageStatus2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
14045
- const status = getStorageStatus2();
14046
- if (options.json) {
14047
- console.log(JSON.stringify(status, null, 2));
14048
- return;
14049
- }
14050
- console.log(chalk2.bold(`
14359
+ try {
14360
+ const { getStorageStatus: getStorageStatus2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
14361
+ const status = getStorageStatus2();
14362
+ if (options.json) {
14363
+ console.log(JSON.stringify(status, null, 2));
14364
+ return;
14365
+ }
14366
+ console.log(chalk2.bold(`
14051
14367
  Storage Status
14052
14368
  `));
14053
- console.log(` Configured: ${status.configured ? chalk2.green(`yes (${status.activeEnv})`) : chalk2.red("no")}`);
14054
- console.log(` Backend: ${status.backend}`);
14055
- console.log(` Tables: ${status.tables.join(", ")}`);
14056
- console.log(` Sync rows: ${status.sync.length}`);
14369
+ console.log(` Configured: ${status.configured ? chalk2.green(`yes (${status.activeEnv})`) : chalk2.red("no")}`);
14370
+ console.log(` Backend: ${status.backend}`);
14371
+ console.log(` Tables: ${status.tables.join(", ")}`);
14372
+ console.log(` Sync rows: ${status.sync.length}`);
14373
+ } catch (error) {
14374
+ const message = error instanceof Error ? error.message : String(error);
14375
+ if (options.json)
14376
+ console.log(JSON.stringify({ error: message }));
14377
+ else
14378
+ console.error(chalk2.red(`\u2717 ${message}`));
14379
+ process.exitCode = 1;
14380
+ }
14057
14381
  });
14058
14382
  storageCmd.command("push").description("Push local hook data to storage PostgreSQL").option("-t, --tables <tables>", "Comma-separated table names").option("-j, --json", "Output as JSON", false).action(async (options) => {
14059
14383
  try {
@@ -14263,6 +14587,13 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
14263
14587
  const { startSSEServer: startSSEServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
14264
14588
  await startSSEServer2({ port: options.port ? parseInt(options.port) : 39427 });
14265
14589
  } else {
14590
+ const { decideHooksMcpAuthority: decideHooksMcpAuthority2 } = await Promise.resolve().then(() => (init_authority(), exports_authority));
14591
+ try {
14592
+ decideHooksMcpAuthority2();
14593
+ } catch (error) {
14594
+ console.error(error instanceof Error ? error.message : String(error));
14595
+ process.exit(1);
14596
+ }
14266
14597
  const { createHooksServer: createHooksServer2 } = await Promise.resolve().then(() => (init_server(), exports_server));
14267
14598
  const { resolveMcpHttpPort: resolveMcpHttpPort2, startMcpHttpServer: startMcpHttpServer2 } = await Promise.resolve().then(() => (init_http(), exports_http));
14268
14599
  const args = options.port ? ["--port", options.port] : [];
@@ -14271,7 +14602,6 @@ program2.command("mcp").option("-s, --stdio", "Use stdio transport (one process
14271
14602
  });
14272
14603
  registerEventsCommands(program2, { source: "hooks" });
14273
14604
  var API_INDEPENDENT_COMMANDS = new Set([
14274
- "run",
14275
14605
  "serve",
14276
14606
  "mcp",
14277
14607
  "cf",
@@ -14283,11 +14613,11 @@ var API_INDEPENDENT_COMMANDS = new Set([
14283
14613
  "events"
14284
14614
  ]);
14285
14615
  function failClosedForMissingApiEnv(detail) {
14286
- console.error(chalk2.red(`hooks: no registry credential resolved and local mode is not explicitly enabled.
14616
+ console.error(chalk2.red(hooksFailClosedLine() + `
14287
14617
  ` + `The remote registry requires a STRICT pair \u2014 set HASNA_HOOKS_API_URL and HASNA_HOOKS_API_KEY, or the
14288
14618
  ` + `Keychain items hasna.credentials.hooks.api-url / .api-key, or ~/.hasna/hooks/config/credentials.
14289
- ` + `config.json (api_url / api_key_ref) is RETIRED and no longer read. Or set
14290
- ` + `HASNA_HOOKS_LOCAL=1 to explicitly opt into local mode (bundled registry + local store).
14619
+ config.json (api_url / api_key_ref) is RETIRED and no longer read. Or set
14620
+ HASNA_HOOKS_LOCAL=1 to explicitly opt into local mode (bundled registry + local store).
14291
14621
  ` + (detail ? `${detail}
14292
14622
  ` : "") + "Refusing to silently fall back to local storage."));
14293
14623
  process.exit(1);
@@ -14311,21 +14641,25 @@ function enforceTransportGate() {
14311
14641
  return;
14312
14642
  if (token !== null && API_INDEPENDENT_COMMANDS.has(token))
14313
14643
  return;
14314
- if (hasHooksEnvAuthorityIntent(process.env))
14644
+ if (hasHooksEnvAuthorityIntent(process.env)) {
14645
+ refuseLocalStore(hooksHostedRouteLocalStoreRefusal("configured by HASNA_HOOKS_API_URL / HASNA_HOOKS_API_KEY"));
14315
14646
  return;
14647
+ }
14316
14648
  if (isHooksLocalOptIn(process.env)) {
14317
14649
  announceHooksLocalMode();
14318
14650
  return;
14319
14651
  }
14652
+ let transport;
14320
14653
  try {
14321
- const transport = resolveHooksTransport(process.env);
14322
- if (transport.mode !== "remote" || !transport.authority) {
14323
- failClosedForMissingApiEnv();
14324
- }
14654
+ transport = resolveHooksTransport(process.env);
14325
14655
  } catch (error) {
14326
14656
  const message = error instanceof Error ? error.message : String(error);
14327
14657
  failClosedForMissingApiEnv(message);
14328
14658
  }
14659
+ if (transport.mode !== "remote" || !transport.authority) {
14660
+ failClosedForMissingApiEnv();
14661
+ }
14662
+ refuseLocalStore(hooksHostedRouteLocalStoreRefusal(transport.authority.v1BaseUrl));
14329
14663
  }
14330
14664
  enforceTransportGate();
14331
14665
  program2.parse();