@hasna/hooks 0.9.1 → 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/serve.js CHANGED
@@ -345,6 +345,17 @@ var HOOKS = [
345
345
  matcher: "",
346
346
  tags: ["knowledge", "context", "codewith", "deterministic", "session-start", "prompt", "subagent"]
347
347
  },
348
+ {
349
+ name: "mementos-context",
350
+ displayName: "Mementos Prompt Context",
351
+ description: "Retrieves scoped memories for a user prompt, with optional relevance judgments",
352
+ version: "0.1.0",
353
+ category: "Context Management",
354
+ event: "UserPromptSubmit",
355
+ matcher: "",
356
+ timeoutSeconds: 12,
357
+ tags: ["mementos", "memory", "prompt", "codex", "claude", "codewith", "opt-in"]
358
+ },
348
359
  {
349
360
  name: "autoformat",
350
361
  displayName: "Auto Format",
package/dist/index.js CHANGED
@@ -4974,21 +4974,21 @@ __export(exports_profiles, {
4974
4974
  deleteProfile: () => deleteProfile,
4975
4975
  createProfile: () => createProfile
4976
4976
  });
4977
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
4977
+ import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
4978
4978
  import { join as join8 } from "path";
4979
4979
  function getProfilesDir() {
4980
4980
  const newDir = join8(getEffectiveDataRoot(), "profiles");
4981
4981
  const oldDir = join8(getHomeDir(), ".hooks", "profiles");
4982
- if (!existsSync8(newDir) && existsSync8(oldDir)) {
4983
- mkdirSync5(newDir, { recursive: true });
4982
+ if (!existsSync9(newDir) && existsSync9(oldDir)) {
4983
+ mkdirSync6(newDir, { recursive: true });
4984
4984
  cpSync2(oldDir, newDir, { recursive: true });
4985
4985
  }
4986
4986
  return newDir;
4987
4987
  }
4988
4988
  function ensureProfilesDir() {
4989
4989
  const dir = getProfilesDir();
4990
- if (!existsSync8(dir)) {
4991
- mkdirSync5(dir, { recursive: true });
4990
+ if (!existsSync9(dir)) {
4991
+ mkdirSync6(dir, { recursive: true });
4992
4992
  }
4993
4993
  }
4994
4994
  function profilePath(id) {
@@ -5011,30 +5011,30 @@ function createProfile(input) {
5011
5011
  if (input.name) {
5012
5012
  profile.name = input.name;
5013
5013
  }
5014
- writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
5014
+ writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
5015
5015
  `);
5016
5016
  return profile;
5017
5017
  }
5018
5018
  function getProfile(id) {
5019
5019
  const path = profilePath(id);
5020
5020
  try {
5021
- if (!existsSync8(path))
5021
+ if (!existsSync9(path))
5022
5022
  return null;
5023
- return JSON.parse(readFileSync5(path, "utf-8"));
5023
+ return JSON.parse(readFileSync6(path, "utf-8"));
5024
5024
  } catch {
5025
5025
  return null;
5026
5026
  }
5027
5027
  }
5028
5028
  function listProfiles() {
5029
5029
  const dir = getProfilesDir();
5030
- if (!existsSync8(dir))
5030
+ if (!existsSync9(dir))
5031
5031
  return [];
5032
5032
  try {
5033
5033
  const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
5034
5034
  const profiles = [];
5035
5035
  for (const file of files) {
5036
5036
  try {
5037
- const content = readFileSync5(join8(dir, file), "utf-8");
5037
+ const content = readFileSync6(join8(dir, file), "utf-8");
5038
5038
  profiles.push(JSON.parse(content));
5039
5039
  } catch {}
5040
5040
  }
@@ -5051,13 +5051,13 @@ function updateProfile(id, data) {
5051
5051
  profile.name = data.name;
5052
5052
  if (data.preferences !== undefined)
5053
5053
  profile.preferences = data.preferences;
5054
- writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
5054
+ writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
5055
5055
  `);
5056
5056
  return profile;
5057
5057
  }
5058
5058
  function deleteProfile(id) {
5059
5059
  const path = profilePath(id);
5060
- if (!existsSync8(path))
5060
+ if (!existsSync9(path))
5061
5061
  return false;
5062
5062
  try {
5063
5063
  rmSync(path);
@@ -5071,7 +5071,7 @@ function touchProfile(id) {
5071
5071
  if (!profile)
5072
5072
  return;
5073
5073
  profile.last_seen_at = new Date().toISOString();
5074
- writeFileSync4(profilePath(id), JSON.stringify(profile, null, 2) + `
5074
+ writeFileSync5(profilePath(id), JSON.stringify(profile, null, 2) + `
5075
5075
  `);
5076
5076
  }
5077
5077
  function exportProfiles() {
@@ -5087,11 +5087,11 @@ function importProfiles(profiles) {
5087
5087
  continue;
5088
5088
  }
5089
5089
  const path = profilePath(profile.agent_id);
5090
- if (existsSync8(path)) {
5090
+ if (existsSync9(path)) {
5091
5091
  skipped++;
5092
5092
  continue;
5093
5093
  }
5094
- writeFileSync4(path, JSON.stringify(profile, null, 2) + `
5094
+ writeFileSync5(path, JSON.stringify(profile, null, 2) + `
5095
5095
  `);
5096
5096
  imported++;
5097
5097
  }
@@ -5104,7 +5104,7 @@ var init_profiles = __esm(() => {
5104
5104
  // ../../node_modules/.bun/@hasna+contracts@1.0.2+e0db24ae03127387/node_modules/@hasna/contracts/dist/client/transport.js
5105
5105
  import { isIP } from "net";
5106
5106
  import { spawnSync } from "child_process";
5107
- import { closeSync, fstatSync, openSync, readFileSync as readFileSync6 } from "fs";
5107
+ import { closeSync as closeSync2, fstatSync, openSync as openSync2, readFileSync as readFileSync7 } from "fs";
5108
5108
  import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
5109
5109
  import { createRequire } from "module";
5110
5110
  import { hostname as osHostname } from "os";
@@ -5208,7 +5208,7 @@ function readAppConfigFile(path) {
5208
5208
  };
5209
5209
  let fd = -1;
5210
5210
  try {
5211
- fd = openSync(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
5211
+ fd = openSync2(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
5212
5212
  } catch (error) {
5213
5213
  const code = error.code;
5214
5214
  if (code === "ENOENT" || code === "ENOTDIR")
@@ -5229,7 +5229,7 @@ function readAppConfigFile(path) {
5229
5229
  unsafe("the file is not owned by the current user");
5230
5230
  if (before.size > MAX_CREDENTIAL_FILE_BYTES)
5231
5231
  unsafe("the file exceeds the size limit");
5232
- const bytes = readFileSync6(fd);
5232
+ const bytes = readFileSync7(fd);
5233
5233
  const after = fstatSync(fd);
5234
5234
  if (!configFileReadsCoherent(before, after)) {
5235
5235
  unsafe("the file changed while being read");
@@ -5237,7 +5237,7 @@ function readAppConfigFile(path) {
5237
5237
  return parseEnvFile(bytes.toString("utf8"));
5238
5238
  } finally {
5239
5239
  if (fd !== -1)
5240
- closeSync(fd);
5240
+ closeSync2(fd);
5241
5241
  }
5242
5242
  }
5243
5243
  function readCredentialFile(path, apiKeyKeys) {
@@ -6013,8 +6013,8 @@ var init_db_writer = __esm(() => {
6013
6013
  });
6014
6014
 
6015
6015
  // src/lib/hook-env.ts
6016
- import { dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
6017
- import { existsSync as existsSync10, realpathSync as realpathSync2, statSync } from "fs";
6016
+ import { dirname as dirname6, isAbsolute as isAbsolute3 } from "path";
6017
+ import { existsSync as existsSync11, realpathSync as realpathSync2, statSync } from "fs";
6018
6018
  import { tmpdir as tmpdir2 } from "os";
6019
6019
  function isDeniedEnvName(name) {
6020
6020
  const upper = name.toUpperCase();
@@ -6062,9 +6062,9 @@ function isUnsafePathEntry(entry, home) {
6062
6062
  }
6063
6063
  function sanitizeHookPath(pathValue, home) {
6064
6064
  const baseline = PATH_BASELINE.slice();
6065
- if (process.platform === "darwin" && existsSync10("/opt/homebrew/bin"))
6065
+ if (process.platform === "darwin" && existsSync11("/opt/homebrew/bin"))
6066
6066
  baseline.push("/opt/homebrew/bin");
6067
- const bunDir = dirname5(process.execPath);
6067
+ const bunDir = dirname6(process.execPath);
6068
6068
  const seen = new Set;
6069
6069
  const out = [];
6070
6070
  for (const dir of baseline) {
@@ -6188,8 +6188,8 @@ __export(exports_run, {
6188
6188
  HookTimeoutError: () => HookTimeoutError
6189
6189
  });
6190
6190
  import { randomBytes as randomBytes2 } from "crypto";
6191
- import { rmSync as rmSync3, writeFileSync as writeFileSync5 } from "fs";
6192
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join12 } from "path";
6191
+ import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
6192
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute4, join as join12 } from "path";
6193
6193
  function extensionOf(scriptPath) {
6194
6194
  const base = basename2(scriptPath);
6195
6195
  const dot = base.lastIndexOf(".");
@@ -6258,7 +6258,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
6258
6258
  return chunks.join("");
6259
6259
  }
6260
6260
  async function executeVerifiedScript(options) {
6261
- const scriptDir = dirname6(options.scriptPath);
6261
+ const scriptDir = dirname7(options.scriptPath);
6262
6262
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
6263
6263
  const tempPath = join12(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
6264
6264
  let proc = null;
@@ -6268,7 +6268,7 @@ async function executeVerifiedScript(options) {
6268
6268
  killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
6269
6269
  }, options.timeout) : null;
6270
6270
  try {
6271
- writeFileSync5(tempPath, options.content, { flag: "wx", mode: 384 });
6271
+ writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
6272
6272
  proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
6273
6273
  stdin: new Response(options.stdin),
6274
6274
  stdout: "pipe",
@@ -7016,6 +7016,17 @@ var HOOKS = [
7016
7016
  matcher: "",
7017
7017
  tags: ["knowledge", "context", "codewith", "deterministic", "session-start", "prompt", "subagent"]
7018
7018
  },
7019
+ {
7020
+ name: "mementos-context",
7021
+ displayName: "Mementos Prompt Context",
7022
+ description: "Retrieves scoped memories for a user prompt, with optional relevance judgments",
7023
+ version: "0.1.0",
7024
+ category: "Context Management",
7025
+ event: "UserPromptSubmit",
7026
+ matcher: "",
7027
+ timeoutSeconds: 12,
7028
+ tags: ["mementos", "memory", "prompt", "codex", "claude", "codewith", "opt-in"]
7029
+ },
7019
7030
  {
7020
7031
  name: "autoformat",
7021
7032
  displayName: "Auto Format",
@@ -7341,8 +7352,8 @@ function getHook(name) {
7341
7352
  return HOOKS.find((h) => h.name === name);
7342
7353
  }
7343
7354
  // src/lib/installer.ts
7344
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, readdirSync as readdirSync3, rmdirSync, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
7345
- import { join as join7, dirname as dirname3 } from "path";
7355
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
7356
+ import { join as join7, dirname as dirname4 } from "path";
7346
7357
  import { homedir as homedir3 } from "os";
7347
7358
  import { fileURLToPath as fileURLToPath2 } from "url";
7348
7359
 
@@ -7441,8 +7452,79 @@ function matchersOverlap(a, b) {
7441
7452
 
7442
7453
  // src/lib/installer.ts
7443
7454
  init_store();
7444
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
7445
- var HOOKS_DIR = existsSync7(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
7455
+
7456
+ // src/lib/codex-settings.ts
7457
+ import { closeSync, existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
7458
+ import { dirname as dirname3 } from "path";
7459
+ import { randomUUID } from "crypto";
7460
+ var snapshots = new WeakMap;
7461
+ function current(path) {
7462
+ try {
7463
+ const stat = lstatSync(path);
7464
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
7465
+ throw new Error("invalid_file");
7466
+ return readFileSync4(path, "utf8");
7467
+ } catch (error) {
7468
+ if (error.code === "ENOENT")
7469
+ return null;
7470
+ throw new Error("Cannot safely read Codex hooks.json; no settings were changed.");
7471
+ }
7472
+ }
7473
+ function readCodexSettings(path) {
7474
+ const raw = current(path);
7475
+ let settings;
7476
+ try {
7477
+ settings = raw === null ? {} : JSON.parse(raw);
7478
+ if (!settings || typeof settings !== "object" || Array.isArray(settings))
7479
+ throw new Error;
7480
+ if (settings.hooks !== undefined) {
7481
+ if (!settings.hooks || typeof settings.hooks !== "object" || Array.isArray(settings.hooks))
7482
+ throw new Error;
7483
+ for (const entries of Object.values(settings.hooks)) {
7484
+ if (!Array.isArray(entries) || entries.some((entry) => !entry || !Array.isArray(entry.hooks) || entry.hooks.some((hook) => !hook || typeof hook !== "object")))
7485
+ throw new Error;
7486
+ }
7487
+ }
7488
+ } catch {
7489
+ throw new Error("Invalid Codex hooks.json; repair it before installing or removing hooks.");
7490
+ }
7491
+ snapshots.set(settings, { path, raw });
7492
+ return settings;
7493
+ }
7494
+ function writeCodexSettings(path, settings) {
7495
+ const snapshot = snapshots.get(settings);
7496
+ if (!snapshot || snapshot.path !== path)
7497
+ throw new Error("Codex settings require a complete current read before writing.");
7498
+ mkdirSync4(dirname3(path), { recursive: true });
7499
+ const lock = `${path}.hooks-write.lock`;
7500
+ let fd;
7501
+ try {
7502
+ fd = openSync(lock, "wx", 384);
7503
+ } catch {
7504
+ throw new Error("Codex settings are being edited; retry after the active writer finishes.");
7505
+ }
7506
+ const temp = `${path}.${randomUUID()}.tmp`;
7507
+ try {
7508
+ if (current(path) !== snapshot.raw)
7509
+ throw new Error("Codex settings changed since reading; re-read before retrying.");
7510
+ const next = JSON.stringify(settings, null, 2) + `
7511
+ `;
7512
+ writeFileSync3(temp, next, { flag: "wx", mode: 384 });
7513
+ if (current(path) !== snapshot.raw)
7514
+ throw new Error("Codex settings changed since reading; re-read before retrying.");
7515
+ renameSync2(temp, path);
7516
+ snapshots.set(settings, { path, raw: next });
7517
+ } finally {
7518
+ if (existsSync7(temp))
7519
+ unlinkSync(temp);
7520
+ closeSync(fd);
7521
+ unlinkSync(lock);
7522
+ }
7523
+ }
7524
+
7525
+ // src/lib/installer.ts
7526
+ var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
7527
+ var HOOKS_DIR = existsSync8(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
7446
7528
  function normalizeHookName(name) {
7447
7529
  return name.startsWith("hook-") ? name : `hook-${name}`;
7448
7530
  }
@@ -7450,10 +7532,15 @@ function shortHookName(name) {
7450
7532
  return name.startsWith("hook-") ? name.slice("hook-".length) : name;
7451
7533
  }
7452
7534
  function removeHookEntriesByName(entries, hookName) {
7453
- return entries.filter((entry) => !entry.hooks?.some((h) => {
7454
- const match = h.command?.match(/^hooks run ([\w-]+)/);
7455
- return match && match[1] === hookName;
7456
- }));
7535
+ return entries.flatMap((entry) => {
7536
+ if (!Array.isArray(entry.hooks))
7537
+ return [entry];
7538
+ const kept = entry.hooks.filter((h) => {
7539
+ const match = h.command?.match(/^hooks run ([\w-]+)/);
7540
+ return !(match && match[1] === hookName);
7541
+ });
7542
+ return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
7543
+ });
7457
7544
  }
7458
7545
  var EVENT_MAP = {
7459
7546
  claude: {
@@ -7463,7 +7550,7 @@ var EVENT_MAP = {
7463
7550
  Notification: "Notification",
7464
7551
  SessionStart: "SessionStart",
7465
7552
  SessionEnd: "SessionEnd",
7466
- UserPromptSubmit: null,
7553
+ UserPromptSubmit: "UserPromptSubmit",
7467
7554
  SubagentStart: null
7468
7555
  },
7469
7556
  gemini: {
@@ -7485,9 +7572,21 @@ var EVENT_MAP = {
7485
7572
  SessionEnd: null,
7486
7573
  UserPromptSubmit: "UserPromptSubmit",
7487
7574
  SubagentStart: "SubagentStart"
7575
+ },
7576
+ codex: {
7577
+ PreToolUse: "PreToolUse",
7578
+ PostToolUse: "PostToolUse",
7579
+ Stop: "Stop",
7580
+ Notification: null,
7581
+ SessionStart: "SessionStart",
7582
+ SessionEnd: "SessionEnd",
7583
+ UserPromptSubmit: "UserPromptSubmit",
7584
+ SubagentStart: "SubagentStart"
7488
7585
  }
7489
7586
  };
7490
7587
  function getTargetSettingsDir(target) {
7588
+ if (target === "codex")
7589
+ return ".codex";
7491
7590
  if (target === "codewith")
7492
7591
  return ".codewith";
7493
7592
  if (target === "gemini")
@@ -7495,6 +7594,8 @@ function getTargetSettingsDir(target) {
7495
7594
  return ".claude";
7496
7595
  }
7497
7596
  function getGlobalSettingsPathOverride(target) {
7597
+ if (target === "codex")
7598
+ return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH;
7498
7599
  if (target === "claude")
7499
7600
  return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
7500
7601
  if (target === "gemini")
@@ -7511,6 +7612,8 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
7511
7612
  if (globalOverride)
7512
7613
  return globalOverride;
7513
7614
  const dir = getTargetSettingsDir(target);
7615
+ if (target === "codex")
7616
+ return join7(scope === "project" ? process.cwd() : homedir3(), dir, "hooks.json");
7514
7617
  if (scope === "project") {
7515
7618
  return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
7516
7619
  }
@@ -7524,13 +7627,15 @@ function getHookPath(name) {
7524
7627
  return join7(HOOKS_DIR, normalizeHookName(shortName));
7525
7628
  }
7526
7629
  function hookExists(name) {
7527
- return existsSync7(getHookPath(name));
7630
+ return existsSync8(getHookPath(name));
7528
7631
  }
7529
7632
  function readSettings(scope = "global", target = "claude") {
7530
7633
  const path = getSettingsPath(scope, target);
7634
+ if (target === "codex")
7635
+ return readCodexSettings(path);
7531
7636
  try {
7532
- if (existsSync7(path)) {
7533
- return JSON.parse(readFileSync4(path, "utf-8"));
7637
+ if (existsSync8(path)) {
7638
+ return JSON.parse(readFileSync5(path, "utf-8"));
7534
7639
  }
7535
7640
  } catch (error) {
7536
7641
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -7539,11 +7644,13 @@ function readSettings(scope = "global", target = "claude") {
7539
7644
  }
7540
7645
  function writeSettings(settings, scope = "global", target = "claude") {
7541
7646
  const path = getSettingsPath(scope, target);
7542
- const dir = dirname3(path);
7543
- if (!existsSync7(dir)) {
7544
- mkdirSync4(dir, { recursive: true });
7647
+ if (target === "codex")
7648
+ return writeCodexSettings(path, settings);
7649
+ const dir = dirname4(path);
7650
+ if (!existsSync8(dir)) {
7651
+ mkdirSync5(dir, { recursive: true });
7545
7652
  }
7546
- writeFileSync3(path, JSON.stringify(settings, null, 2) + `
7653
+ writeFileSync4(path, JSON.stringify(settings, null, 2) + `
7547
7654
  `);
7548
7655
  }
7549
7656
  function getTargetEventName(internalEvent, target) {
@@ -7566,6 +7673,8 @@ function codewithMatcher(matcher) {
7566
7673
  }
7567
7674
  function codewithTimeout(name) {
7568
7675
  switch (name) {
7676
+ case "mementos-context":
7677
+ return 12;
7569
7678
  case "knowledge-context":
7570
7679
  return 6;
7571
7680
  case "session-start":
@@ -7632,21 +7741,21 @@ function buildCodewithTomlFragment(name, profile) {
7632
7741
  function readCodewithConfig(scope, configPath) {
7633
7742
  const path = getSettingsPath(scope, "codewith", configPath);
7634
7743
  try {
7635
- return existsSync7(path) ? readFileSync4(path, "utf-8") : "";
7744
+ return existsSync8(path) ? readFileSync5(path, "utf-8") : "";
7636
7745
  } catch {
7637
7746
  return "";
7638
7747
  }
7639
7748
  }
7640
7749
  function appendCodewithFragment(fragment, scope, configPath) {
7641
7750
  const path = getSettingsPath(scope, "codewith", configPath);
7642
- const dir = dirname3(path);
7643
- if (!existsSync7(dir))
7644
- mkdirSync4(dir, { recursive: true });
7645
- const existing = existsSync7(path) ? readFileSync4(path, "utf-8") : "";
7751
+ const dir = dirname4(path);
7752
+ if (!existsSync8(dir))
7753
+ mkdirSync5(dir, { recursive: true });
7754
+ const existing = existsSync8(path) ? readFileSync5(path, "utf-8") : "";
7646
7755
  const sep2 = existing.trim() ? `
7647
7756
 
7648
7757
  ` : "";
7649
- writeFileSync3(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
7758
+ writeFileSync4(path, `${existing.replace(/\s*$/, "")}${sep2}${fragment}`);
7650
7759
  return path;
7651
7760
  }
7652
7761
  function detectConflict(name, scope, target) {
@@ -7743,18 +7852,25 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
7743
7852
  };
7744
7853
  }
7745
7854
  }
7746
- const registered = getRegisteredHooksForTarget(scope, target);
7747
- if (registered.includes(shortName) && !overwrite) {
7748
- return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
7749
- }
7750
- const conflict = detectConflict(shortName, scope, target);
7751
- const rewriteConflict = detectRewriteConflict(shortName, scope, target);
7752
- if (rewriteConflict) {
7753
- return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
7754
- }
7755
7855
  try {
7856
+ const registered = getRegisteredHooksForTarget(scope, target);
7857
+ if (registered.includes(shortName) && !overwrite) {
7858
+ return { hook: shortName, success: false, error: "Already installed. Use --overwrite to replace.", scope, target };
7859
+ }
7860
+ const conflict = detectConflict(shortName, scope, target);
7861
+ const rewriteConflict = detectRewriteConflict(shortName, scope, target);
7862
+ if (rewriteConflict) {
7863
+ return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
7864
+ }
7756
7865
  registerHook(shortName, scope, target, profile);
7757
- return { hook: shortName, success: true, scope, target, ...conflict ? { conflict } : {} };
7866
+ return {
7867
+ hook: shortName,
7868
+ success: true,
7869
+ scope,
7870
+ target,
7871
+ ...conflict ? { conflict } : {},
7872
+ ...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." } : {}
7873
+ };
7758
7874
  } catch (error) {
7759
7875
  return {
7760
7876
  hook: shortName,
@@ -7766,6 +7882,9 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
7766
7882
  }
7767
7883
  function installHook(name, options = {}) {
7768
7884
  const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath } = options;
7885
+ if (!/^[\w-]+$/.test(name) || profile !== undefined && !/^[\w-]+$/.test(profile)) {
7886
+ return { hook: name, success: false, error: "Hook and profile names must contain only letters, digits, underscores or hyphens.", target };
7887
+ }
7769
7888
  if (target === "all") {
7770
7889
  const shortName = shortHookName(name);
7771
7890
  const meta = resolveHookMeta(shortName);
@@ -7898,7 +8017,8 @@ function removeHook(name, scope = "global", target = "claude") {
7898
8017
  if (target === "all") {
7899
8018
  const claudeRemoved = removeHookForTarget(shortName, scope, "claude");
7900
8019
  const geminiRemoved = removeHookForTarget(shortName, scope, "gemini");
7901
- return claudeRemoved || geminiRemoved;
8020
+ const codexRemoved = removeHookForTarget(shortName, scope, "codex");
8021
+ return claudeRemoved || geminiRemoved || codexRemoved;
7902
8022
  }
7903
8023
  if (target === "codewith") {
7904
8024
  return false;
@@ -7913,6 +8033,7 @@ function removeHookForTarget(name, scope, target) {
7913
8033
  unregisterHook(name, scope, target);
7914
8034
  return true;
7915
8035
  }
8036
+ var JSON_SETTINGS_TARGETS = ["claude", "gemini", "codex"];
7916
8037
  function removeCodewithHookEntry(configText, name) {
7917
8038
  const sections = configText.split(/\n\s*\n/);
7918
8039
  const nameRe = new RegExp(`^command\\s*=\\s*"hooks run ${escapeRegExp(name)}(?:\\s+--profile\\s+[\\w-]+)?"$`);
@@ -7969,10 +8090,10 @@ function codewithHasHookEntryInText(configText, name) {
7969
8090
  }
7970
8091
  function codewithHasHookEntry(name, scope = "global") {
7971
8092
  const path = getSettingsPath(scope, "codewith");
7972
- if (!existsSync7(path))
8093
+ if (!existsSync8(path))
7973
8094
  return false;
7974
8095
  try {
7975
- return codewithHasHookEntryInText(readFileSync4(path, "utf-8"), name);
8096
+ return codewithHasHookEntryInText(readFileSync5(path, "utf-8"), name);
7976
8097
  } catch {
7977
8098
  return false;
7978
8099
  }
@@ -7986,7 +8107,7 @@ function removeStoreDirFailClosed(dir) {
7986
8107
  }
7987
8108
  for (const entry of entries) {
7988
8109
  if (!entry.isDirectory()) {
7989
- unlinkSync(join7(dir, entry.name));
8110
+ unlinkSync2(join7(dir, entry.name));
7990
8111
  }
7991
8112
  }
7992
8113
  rmdirSync(dir);
@@ -7996,14 +8117,14 @@ function uninstallHook(name, scope = "global", target = "claude") {
7996
8117
  const custom2 = readCustomManifest(shortName);
7997
8118
  const bundledMeta = custom2 ? undefined : getHook(shortName);
7998
8119
  const existsInTarget = (t) => getRegisteredHooksForTarget("global", t).includes(shortName) || getRegisteredHooksForTarget("project", t).includes(shortName);
7999
- const registeredClaudeOrGemini = target === "all" ? existsInTarget("claude") || existsInTarget("gemini") : false;
8120
+ const registeredClaudeOrGemini = target === "all" ? JSON_SETTINGS_TARGETS.some(existsInTarget) : false;
8000
8121
  const registeredGlobal = registeredClaudeOrGemini || getRegisteredHooksForTarget("global", target === "all" ? "claude" : target).includes(shortName);
8001
8122
  const registeredProject = registeredClaudeOrGemini || getRegisteredHooksForTarget("project", target === "all" ? "claude" : target).includes(shortName);
8002
8123
  const codewithOnly = (target === "codewith" || target === "all") && codewithHasHookEntry(shortName, scope);
8003
8124
  if (!custom2 && !bundledMeta && !registeredGlobal && !registeredProject && !codewithOnly) {
8004
8125
  return { name: shortName, removed: false, source: null, settingsScopes: [], storeDirRemoved: false, pinRemoved: false, dbRecordRemoved: false, registrationsRemaining: [], error: `Hook '${shortName}' not found` };
8005
8126
  }
8006
- const writableTargets = target === "all" ? ["claude", "gemini"] : target === "codewith" ? [] : [target];
8127
+ const writableTargets = target === "all" ? JSON_SETTINGS_TARGETS : target === "codewith" ? [] : [target];
8007
8128
  const scopes = [];
8008
8129
  for (const s of ["global", "project"]) {
8009
8130
  for (const t of writableTargets) {
@@ -8017,11 +8138,11 @@ function uninstallHook(name, scope = "global", target = "claude") {
8017
8138
  const registrationsRemaining = [];
8018
8139
  if (target === "codewith" || target === "all") {
8019
8140
  const codewithPath = getSettingsPath(scope, "codewith");
8020
- if (existsSync7(codewithPath)) {
8021
- const before = readFileSync4(codewithPath, "utf-8");
8141
+ if (existsSync8(codewithPath)) {
8142
+ const before = readFileSync5(codewithPath, "utf-8");
8022
8143
  const after = removeCodewithHookEntry(before, shortName);
8023
8144
  if (after.removed) {
8024
- writeFileSync3(codewithPath, after.text, "utf-8");
8145
+ writeFileSync4(codewithPath, after.text, "utf-8");
8025
8146
  }
8026
8147
  if (codewithHasHookEntryInText(after.removed ? after.text : before, shortName)) {
8027
8148
  registrationsRemaining.push("codewith");
@@ -8060,7 +8181,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
8060
8181
  // src/index.ts
8061
8182
  init_profiles();
8062
8183
  init_config();
8063
- import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
8184
+ import { existsSync as existsSync12, readFileSync as readFileSync11 } from "fs";
8064
8185
 
8065
8186
  // src/lib/transport.ts
8066
8187
  init_transport();
@@ -8147,15 +8268,15 @@ init_manifest();
8147
8268
  // src/lib/custom-install.ts
8148
8269
  init_config();
8149
8270
  init_manifest();
8150
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync2 } from "fs";
8271
+ import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync8, rmSync as rmSync2 } from "fs";
8151
8272
  import { join as join10 } from "path";
8152
8273
  import { tmpdir } from "os";
8153
8274
  function manifestAt(dir) {
8154
8275
  const path = join10(dir, "manifest.json");
8155
- if (!existsSync9(path))
8276
+ if (!existsSync10(path))
8156
8277
  return;
8157
8278
  try {
8158
- return parseManifest(readFileSync7(path, "utf-8"));
8279
+ return parseManifest(readFileSync8(path, "utf-8"));
8159
8280
  } catch {
8160
8281
  return;
8161
8282
  }
@@ -8198,7 +8319,7 @@ function isCustomSource(value) {
8198
8319
  return true;
8199
8320
  if (value.startsWith("http://") || value.startsWith("https://"))
8200
8321
  return true;
8201
- return existsSync9(value);
8322
+ return existsSync10(value);
8202
8323
  }
8203
8324
  async function installCustomSource(source) {
8204
8325
  if (isGitUrl(source)) {
@@ -8212,11 +8333,11 @@ async function installCustomSource(source) {
8212
8333
  if (source.startsWith("http://") || source.startsWith("https://")) {
8213
8334
  return installManifestUrl(source);
8214
8335
  }
8215
- if (!existsSync9(source)) {
8336
+ if (!existsSync10(source)) {
8216
8337
  throw new Error(`No such local path: ${source}`);
8217
8338
  }
8218
8339
  if (source.endsWith("manifest.json")) {
8219
- const manifest = parseManifest(readFileSync7(source, "utf-8"));
8340
+ const manifest = parseManifest(readFileSync8(source, "utf-8"));
8220
8341
  return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
8221
8342
  }
8222
8343
  return copyManifestDir(source, source, "local");
@@ -8247,7 +8368,7 @@ async function installManifestUrl(url) {
8247
8368
  }
8248
8369
  // src/lib/sync.ts
8249
8370
  import { createHash as createHash2 } from "crypto";
8250
- import { readFileSync as readFileSync8 } from "fs";
8371
+ import { readFileSync as readFileSync9 } from "fs";
8251
8372
  init_db();
8252
8373
  init_store();
8253
8374
  init_manifest();
@@ -8255,7 +8376,7 @@ function sha256OfText(text) {
8255
8376
  return createHash2("sha256").update(text).digest("hex");
8256
8377
  }
8257
8378
  function sha256OfPath(path) {
8258
- return createHash2("sha256").update(readFileSync8(path)).digest("hex");
8379
+ return createHash2("sha256").update(readFileSync9(path)).digest("hex");
8259
8380
  }
8260
8381
  function collectBundledCatalog() {
8261
8382
  const out = [];
@@ -8549,8 +8670,8 @@ async function installPinnedFromBundled(name, version) {
8549
8670
  // src/serve.ts
8550
8671
  init_manifest();
8551
8672
  init_store();
8552
- import { readFileSync as readFileSync9 } from "fs";
8553
- import { dirname as dirname4, join as join11 } from "path";
8673
+ import { readFileSync as readFileSync10 } from "fs";
8674
+ import { dirname as dirname5, join as join11 } from "path";
8554
8675
  import { fileURLToPath as fileURLToPath3 } from "url";
8555
8676
 
8556
8677
  // src/lib/secure-compare.ts
@@ -8724,8 +8845,8 @@ var SERVE_HOST = "127.0.0.1";
8724
8845
  var SERVE_SERVICE_NAME = "hooks-registry";
8725
8846
  function packageVersion() {
8726
8847
  try {
8727
- const pkgPath = join11(dirname4(fileURLToPath3(import.meta.url)), "..", "package.json");
8728
- return JSON.parse(readFileSync9(pkgPath, "utf-8")).version || "0.0.0";
8848
+ const pkgPath = join11(dirname5(fileURLToPath3(import.meta.url)), "..", "package.json");
8849
+ return JSON.parse(readFileSync10(pkgPath, "utf-8")).version || "0.0.0";
8729
8850
  } catch {
8730
8851
  return "0.0.0";
8731
8852
  }
@@ -8991,11 +9112,11 @@ async function runHook(name, input, options = {}) {
8991
9112
  throw new Error(`Hook '${name}' not found`);
8992
9113
  const hookScript = getHookPath(name) + "/src/hook.ts";
8993
9114
  const resolvedScript = custom2?.scriptPath;
8994
- const script = resolvedScript ?? (existsSync11(hookScript) ? hookScript : undefined);
9115
+ const script = resolvedScript ?? (existsSync12(hookScript) ? hookScript : undefined);
8995
9116
  if (!script)
8996
9117
  throw new Error(`Hook script not found: ${name}`);
8997
9118
  const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
8998
- const content = readFileSync10(script);
9119
+ const content = readFileSync11(script);
8999
9120
  const check = checkScriptHash2(name, sha256Of2(content));
9000
9121
  if (!check.ok) {
9001
9122
  throw new Error(`Hook '${name}' script changed since it was trusted (sha256 ${check.expected} != ${check.actual}). Run 'hooks trust ${name}' to trust the new content.`);