@hasna/hooks 0.10.5 → 0.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6202,8 +6202,8 @@ var init_db_writer = __esm(() => {
6202
6202
  });
6203
6203
 
6204
6204
  // src/lib/hook-env.ts
6205
- import { dirname as dirname19, isAbsolute as isAbsolute15 } from "path";
6206
- import { existsSync as existsSync13, realpathSync as realpathSync8, statSync as statSync2 } from "fs";
6205
+ import { dirname as dirname24, isAbsolute as isAbsolute21 } from "path";
6206
+ import { existsSync as existsSync14, realpathSync as realpathSync13, statSync as statSync2 } from "fs";
6207
6207
  import { tmpdir as tmpdir2 } from "os";
6208
6208
  function isDeniedEnvName(name) {
6209
6209
  const upper = name.toUpperCase();
@@ -6222,7 +6222,7 @@ function isBashFunctionEnvName(name) {
6222
6222
  return name.startsWith("BASH_FUNC_");
6223
6223
  }
6224
6224
  function isUnsafePathEntry(entry, home) {
6225
- if (entry === "" || !isAbsolute15(entry))
6225
+ if (entry === "" || !isAbsolute21(entry))
6226
6226
  return true;
6227
6227
  if (home && (entry === home || entry.startsWith(home + "/")))
6228
6228
  return true;
@@ -6230,16 +6230,16 @@ function isUnsafePathEntry(entry, home) {
6230
6230
  return true;
6231
6231
  if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
6232
6232
  return true;
6233
- const canonical4 = (path) => {
6233
+ const canonical5 = (path) => {
6234
6234
  try {
6235
- return realpathSync8(path);
6235
+ return realpathSync13(path);
6236
6236
  } catch {
6237
6237
  return path;
6238
6238
  }
6239
6239
  };
6240
- const resolved = canonical4(entry);
6240
+ const resolved = canonical5(entry);
6241
6241
  for (const root of [home, tmpdir2(), "/tmp", "/var/tmp"].filter((value) => Boolean(value))) {
6242
- const blocked = canonical4(root);
6242
+ const blocked = canonical5(root);
6243
6243
  if (resolved === blocked || resolved.startsWith(blocked + "/"))
6244
6244
  return true;
6245
6245
  }
@@ -6251,9 +6251,9 @@ function isUnsafePathEntry(entry, home) {
6251
6251
  }
6252
6252
  function sanitizeHookPath(pathValue, home) {
6253
6253
  const baseline = PATH_BASELINE.slice();
6254
- if (process.platform === "darwin" && existsSync13("/opt/homebrew/bin"))
6254
+ if (process.platform === "darwin" && existsSync14("/opt/homebrew/bin"))
6255
6255
  baseline.push("/opt/homebrew/bin");
6256
- const bunDir = dirname19(process.execPath);
6256
+ const bunDir = dirname24(process.execPath);
6257
6257
  const seen = new Set;
6258
6258
  const out = [];
6259
6259
  for (const dir of baseline) {
@@ -6377,10 +6377,10 @@ __export(exports_run, {
6377
6377
  HookTimeoutError: () => HookTimeoutError
6378
6378
  });
6379
6379
  import { randomBytes as randomBytes2 } from "crypto";
6380
- import { rmSync as rmSync4, writeFileSync as writeFileSync10 } from "fs";
6381
- import { basename as basename9, dirname as dirname20, isAbsolute as isAbsolute17, join as join20 } from "path";
6380
+ import { rmSync as rmSync4, writeFileSync as writeFileSync13 } from "fs";
6381
+ import { basename as basename11, dirname as dirname25, isAbsolute as isAbsolute23, join as join30 } from "path";
6382
6382
  function extensionOf(scriptPath) {
6383
- const base = basename9(scriptPath);
6383
+ const base = basename11(scriptPath);
6384
6384
  const dot = base.lastIndexOf(".");
6385
6385
  return dot > 0 ? base.slice(dot + 1) : "ts";
6386
6386
  }
@@ -6392,9 +6392,9 @@ function shebangInterpreter(firstLine) {
6392
6392
  return null;
6393
6393
  const parts = rest.split(/\s+/);
6394
6394
  if (parts[0] === "env" || parts[0] === "/usr/bin/env") {
6395
- return parts[1] && parts[1] !== "-S" ? basename9(parts[1]) : parts[2] ? basename9(parts[2]) : null;
6395
+ return parts[1] && parts[1] !== "-S" ? basename11(parts[1]) : parts[2] ? basename11(parts[2]) : null;
6396
6396
  }
6397
- return basename9(parts[0]);
6397
+ return basename11(parts[0]);
6398
6398
  }
6399
6399
  function interpreterFor(name, scriptPath, content) {
6400
6400
  const ext = extensionOf(scriptPath);
@@ -6447,9 +6447,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
6447
6447
  return chunks.join("");
6448
6448
  }
6449
6449
  async function executeVerifiedScript(options) {
6450
- const scriptDir = dirname20(options.scriptPath);
6450
+ const scriptDir = dirname25(options.scriptPath);
6451
6451
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
6452
- const tempPath = join20(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
6452
+ const tempPath = join30(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
6453
6453
  let proc = null;
6454
6454
  let timedOut = false;
6455
6455
  const timeoutTimer = options.timeout ? setTimeout(() => {
@@ -6457,8 +6457,8 @@ async function executeVerifiedScript(options) {
6457
6457
  killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
6458
6458
  }, options.timeout) : null;
6459
6459
  try {
6460
- writeFileSync10(tempPath, options.content, { flag: "wx", mode: 384 });
6461
- proc = Bun.spawn([...interpreter.command, isAbsolute17(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
6460
+ writeFileSync13(tempPath, options.content, { flag: "wx", mode: 384 });
6461
+ proc = Bun.spawn([...interpreter.command, isAbsolute23(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
6462
6462
  stdin: new Response(options.stdin),
6463
6463
  stdout: "pipe",
6464
6464
  stderr: "pipe",
@@ -7854,7 +7854,7 @@ init_store();
7854
7854
  // src/lib/codex-settings.ts
7855
7855
  import { closeSync as closeSync2, existsSync as existsSync8, lstatSync as lstatSync2, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
7856
7856
  import { dirname as dirname4 } from "path";
7857
- import { randomUUID } from "crypto";
7857
+ import { createHash as createHash3, randomUUID } from "crypto";
7858
7858
  var snapshots = new WeakMap;
7859
7859
  function current(path) {
7860
7860
  try {
@@ -7889,11 +7889,30 @@ function readCodexSettings(path) {
7889
7889
  snapshots.set(settings, { path, raw });
7890
7890
  return settings;
7891
7891
  }
7892
- function writeCodexSettings(path, settings) {
7892
+ function capturedCodexSettings(settings) {
7893
+ const snapshot = snapshots.get(settings);
7894
+ if (!snapshot)
7895
+ throw new Error("Hook settings require a complete current read.");
7896
+ return snapshot.raw;
7897
+ }
7898
+ function assertSettingsPreimage(raw, expected) {
7899
+ if (expected === undefined)
7900
+ return;
7901
+ if (!/^(?:absent|[a-f0-9]{64})$/.test(expected) || (raw === null ? "absent" : createHash3("sha256").update(raw).digest("hex")) !== expected) {
7902
+ throw new Error("Hook settings changed since planning; no settings were written.");
7903
+ }
7904
+ }
7905
+ function writeCodexSettings(path, settings, expectedSha256) {
7893
7906
  const snapshot = snapshots.get(settings);
7894
7907
  if (!snapshot || snapshot.path !== path)
7895
7908
  throw new Error("Codex settings require a complete current read before writing.");
7896
- mkdirSync4(dirname4(path), { recursive: true });
7909
+ assertSettingsPreimage(snapshot.raw, expectedSha256);
7910
+ writeHookSettingsPreimage(path, settings, snapshot.raw);
7911
+ snapshots.set(settings, { path, raw: JSON.stringify(settings, null, 2) + `
7912
+ ` });
7913
+ }
7914
+ function writeHookSettingsPreimage(path, settings, before) {
7915
+ mkdirSync4(dirname4(path), { recursive: true, mode: 448 });
7897
7916
  const lock = `${path}.hooks-write.lock`;
7898
7917
  let fd;
7899
7918
  try {
@@ -7903,15 +7922,14 @@ function writeCodexSettings(path, settings) {
7903
7922
  }
7904
7923
  const temp = `${path}.${randomUUID()}.tmp`;
7905
7924
  try {
7906
- if (current(path) !== snapshot.raw)
7907
- throw new Error("Codex settings changed since reading; re-read before retrying.");
7925
+ if (current(path) !== before)
7926
+ throw new Error("Hook settings changed since reading; re-read before retrying.");
7908
7927
  const next = JSON.stringify(settings, null, 2) + `
7909
7928
  `;
7910
7929
  writeFileSync3(temp, next, { flag: "wx", mode: 384 });
7911
- if (current(path) !== snapshot.raw)
7912
- throw new Error("Codex settings changed since reading; re-read before retrying.");
7930
+ if (current(path) !== before)
7931
+ throw new Error("Hook settings changed since reading; re-read before retrying.");
7913
7932
  renameSync2(temp, path);
7914
- snapshots.set(settings, { path, raw: next });
7915
7933
  } finally {
7916
7934
  if (existsSync8(temp))
7917
7935
  unlinkSync(temp);
@@ -7929,7 +7947,7 @@ import { homedir as homedir4 } from "os";
7929
7947
  import { join as join8, resolve as resolve3 } from "path";
7930
7948
  import { homedir as pathsResolverHomedir2 } from "os";
7931
7949
  import { join as pathsResolverJoin2 } from "path";
7932
- import { createHash as createHash3 } from "crypto";
7950
+ import { createHash as createHash4 } from "crypto";
7933
7951
  import { existsSync as existsSync52, readdirSync as readdirSync3, readFileSync as readFileSync42, statSync as statSync3 } from "fs";
7934
7952
  import { join as join52, sep as sep2 } from "path";
7935
7953
  import { basename as basename2, isAbsolute as isAbsolute22, resolve as resolve22 } from "path";
@@ -7944,7 +7962,7 @@ import { dirname as dirname42, isAbsolute as isAbsolute4, join as join82, resolv
7944
7962
  import { createHash as createHash32 } from "crypto";
7945
7963
  import { closeSync as closeSync3, constants as constants2, fstatSync as fstatSync2, lstatSync as lstatSync5, openSync as openSync3, readSync } from "fs";
7946
7964
  import { dirname as dirname5, isAbsolute as isAbsolute5, resolve as resolve5 } from "path";
7947
- import { createHash as createHash4 } from "crypto";
7965
+ import { createHash as createHash42 } from "crypto";
7948
7966
  import { lstatSync as lstatSync6, readlinkSync } from "fs";
7949
7967
  import { dirname as dirname6, isAbsolute as isAbsolute6, join as join9, parse, resolve as resolve6, sep as sep22 } from "path";
7950
7968
  import { closeSync as closeSync12, constants as constants9, fstatSync as fstatSync12, lstatSync as lstatSync17, openSync as openSync12, opendirSync as opendirSync5, readSync as readSync8 } from "fs";
@@ -15072,7 +15090,7 @@ function* bundleHashParts(files) {
15072
15090
  yield new TextEncoder().encode("\x00");
15073
15091
  }
15074
15092
  function hashBundleFiles(files) {
15075
- const hash = createHash3(CONTENT_HASH_ALGORITHM);
15093
+ const hash = createHash4(CONTENT_HASH_ALGORITHM);
15076
15094
  for (const part of bundleHashParts(files))
15077
15095
  hash.update(part);
15078
15096
  return hash.digest("hex");
@@ -15908,7 +15926,7 @@ function hashDiscoveryPathFile(path, budget, changes) {
15908
15926
  const after = trace(path);
15909
15927
  if (JSON.stringify(before.rows) !== JSON.stringify(after.rows) || before.file !== after.file)
15910
15928
  throw new Error("Discovery path input changed during read");
15911
- return createHash4("sha256").update(JSON.stringify({ version: 1, path, rows: before.rows, content })).digest("hex");
15929
+ return createHash42("sha256").update(JSON.stringify({ version: 1, path, rows: before.rows, content })).digest("hex");
15912
15930
  }
15913
15931
  var package_default = {
15914
15932
  name: "@hasna/skills",
@@ -39139,9 +39157,9 @@ function getTargetSettingsDir(target) {
39139
39157
  }
39140
39158
  function getGlobalSettingsPathOverride(target) {
39141
39159
  if (target === "codex")
39142
- return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH;
39160
+ return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH ?? (process.env.CODEX_HOME ? resolve7(process.env.CODEX_HOME, "hooks.json") : undefined);
39143
39161
  if (target === "claude")
39144
- return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
39162
+ return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH ?? (process.env.CLAUDE_CONFIG_DIR ? resolve7(process.env.CLAUDE_CONFIG_DIR, "settings.json") : undefined);
39145
39163
  if (target === "gemini")
39146
39164
  return process.env.HASNA_HOOKS_GEMINI_SETTINGS_PATH;
39147
39165
  return;
@@ -39185,7 +39203,7 @@ function readSettings2(scope = "global", target = "claude") {
39185
39203
  return value;
39186
39204
  }
39187
39205
  } catch (error61) {
39188
- console.warn(`[hooks] Failed to read settings at ${path}: ${error61 instanceof Error ? error61.message : String(error61)}`);
39206
+ console.warn("[hooks] Cannot read hook settings; repair the selected file before installation.");
39189
39207
  }
39190
39208
  const empty = {};
39191
39209
  if (!existsSync10(path))
@@ -39197,10 +39215,16 @@ function settingsIdentity(path) {
39197
39215
  return realpathSync5(path);
39198
39216
  return existsSync10(dirname7(path)) ? join10(realpathSync5(dirname7(path)), basename3(path)) : resolve7(path);
39199
39217
  }
39200
- function writeSettings(settings, scope = "global", target = "claude") {
39218
+ function writeSettings(settings, scope = "global", target = "claude", expectedSettingsSha256) {
39201
39219
  const path = getSettingsPath(scope, target);
39202
39220
  if (target === "codex")
39203
- return writeCodexSettings(path, settings);
39221
+ return writeCodexSettings(path, settings, expectedSettingsSha256);
39222
+ const captured = settingsPreimages.get(settings);
39223
+ if (expectedSettingsSha256 !== undefined) {
39224
+ if (!captured || captured.path !== path)
39225
+ throw new Error("Hook settings require a valid captured preimage.");
39226
+ assertSettingsPreimage(captured.text, expectedSettingsSha256);
39227
+ }
39204
39228
  if (target === "claude" && settingsIdentity(path) === settingsIdentity(join10(homedir5(), ".claude/settings.json"))) {
39205
39229
  const before = settingsPreimages.get(settings);
39206
39230
  if (!before || before.path !== path)
@@ -39212,6 +39236,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
39212
39236
  `, { home: homedir5() }))
39213
39237
  return;
39214
39238
  }
39239
+ if (expectedSettingsSha256 !== undefined)
39240
+ return writeHookSettingsPreimage(path, settings, captured.text);
39215
39241
  const dir = dirname7(path);
39216
39242
  if (!existsSync10(dir)) {
39217
39243
  mkdirSync6(dir, { recursive: true });
@@ -39369,7 +39395,7 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
39369
39395
  }
39370
39396
  return;
39371
39397
  }
39372
- function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
39398
+ function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos, expectedSettingsSha256) {
39373
39399
  const shortName = shortHookName(name);
39374
39400
  if (isNativeSafetyName(shortName) && (profile || target === "gemini" || target === "codewith" && codewithMode === "write")) {
39375
39401
  return { hook: shortName, success: false, error: "Bundled native safety guards support Claude/Codex and managed Codewith fragments, without registry profiles.", target };
@@ -39436,7 +39462,7 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
39436
39462
  if (rewriteConflict) {
39437
39463
  return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
39438
39464
  }
39439
- registerHook(shortName, scope, target, profile, mementos);
39465
+ registerHook(shortName, scope, target, profile, mementos, (settings, selectedScope, selectedTarget) => writeSettings(settings, selectedScope, selectedTarget, expectedSettingsSha256));
39440
39466
  return {
39441
39467
  hook: shortName,
39442
39468
  success: true,
@@ -39455,7 +39481,10 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
39455
39481
  }
39456
39482
  }
39457
39483
  function installHook(name, options = {}) {
39458
- const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos } = options;
39484
+ const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos, expectedSettingsSha256 } = options;
39485
+ if (expectedSettingsSha256 !== undefined && (!/^(?:absent|[a-f0-9]{64})$/.test(expectedSettingsSha256) || !isNativeSafetyName(shortHookName(name)) || target !== "codex" && target !== "claude")) {
39486
+ return { hook: name, success: false, target, error: "An exact settings predecessor requires a Claude or Codex native safety guard." };
39487
+ }
39459
39488
  try {
39460
39489
  validateMementosRegistration(name, mementos);
39461
39490
  } catch (error61) {
@@ -39496,9 +39525,26 @@ function installHook(name, options = {}) {
39496
39525
  }
39497
39526
  return { ...claudeResult, target: "all" };
39498
39527
  }
39499
- return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
39528
+ return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos, expectedSettingsSha256);
39500
39529
  }
39501
- function registerHook(name, scope = "global", target = "claude", profile, mementos) {
39530
+ function previewNativeSafetyRegistration(target, scope = "global") {
39531
+ const conflict = detectRewriteConflict("trash-guard", scope, target);
39532
+ if (conflict)
39533
+ throw new Error("Conflicting input rewriter; reconcile it before installing the safety guard.");
39534
+ let result;
39535
+ registerHook("trash-guard", scope, target, undefined, undefined, (settings) => {
39536
+ const before = target === "codex" ? capturedCodexSettings(settings) : settingsPreimages.get(settings)?.text;
39537
+ if (before === undefined)
39538
+ throw new Error("Hook settings require a valid captured preimage.");
39539
+ const command = installedNativeSafetyCommand("trash-guard");
39540
+ result = { before, after: JSON.stringify(settings, null, 2) + `
39541
+ `, command };
39542
+ });
39543
+ if (!result)
39544
+ throw new Error("Native safety registration could not be rendered.");
39545
+ return result;
39546
+ }
39547
+ function registerHook(name, scope = "global", target = "claude", profile, mementos, write = writeSettings) {
39502
39548
  const meta3 = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
39503
39549
  if (!meta3)
39504
39550
  return;
@@ -39527,7 +39573,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
39527
39573
  }
39528
39574
  const retained = name === "mementos-context" && mementos === undefined ? retainedMementosRegistration(Object.values(settings.hooks).flatMap((groups) => Array.isArray(groups) ? groups.flatMap((group) => Array.isArray(group.hooks) ? group.hooks.map((hook) => hook.command).filter((command) => typeof command === "string") : []) : [])) : undefined;
39529
39575
  const hookCommand = isNativeSafetyName(name) ? installedNativeSafetyCommand(name) : (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
39530
- if (target === "codex") {
39576
+ if (target === "codex" || target === "claude" && isNativeSafetyName(name)) {
39531
39577
  const positions = [];
39532
39578
  for (const [event, groups] of Object.entries(settings.hooks)) {
39533
39579
  for (const entry of groups) {
@@ -39553,7 +39599,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
39553
39599
  if (typeof meta3.timeoutSeconds === "number" && meta3.timeoutSeconds > 0)
39554
39600
  entry.hooks[index].timeout = meta3.timeoutSeconds;
39555
39601
  }
39556
- writeSettings(settings, scope, target);
39602
+ write(settings, scope, target);
39557
39603
  return;
39558
39604
  }
39559
39605
  }
@@ -39573,7 +39619,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
39573
39619
  }
39574
39620
  settings.hooks[eventKey].push(entry);
39575
39621
  }
39576
- writeSettings(settings, scope, target);
39622
+ write(settings, scope, target);
39577
39623
  }
39578
39624
  function removeHookFromAllEvents(settings, name) {
39579
39625
  if (!settings.hooks)
@@ -39807,7 +39853,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
39807
39853
  // src/index.ts
39808
39854
  init_profiles();
39809
39855
  init_config();
39810
- import { existsSync as existsSync14, readFileSync as readFileSync19 } from "fs";
39856
+ import { existsSync as existsSync15, readFileSync as readFileSync26 } from "fs";
39811
39857
 
39812
39858
  // src/lib/transport.ts
39813
39859
  init_transport();
@@ -40046,16 +40092,16 @@ async function installManifestUrl(url2) {
40046
40092
  return { name, version: manifest.version, kind: "url", source: url2, dir, scriptPath };
40047
40093
  }
40048
40094
  // src/lib/sync.ts
40049
- import { createHash as createHash7 } from "crypto";
40095
+ import { createHash as createHash6 } from "crypto";
40050
40096
  import { readFileSync as readFileSync15 } from "fs";
40051
40097
  init_db();
40052
40098
  init_store();
40053
40099
  init_manifest();
40054
40100
  function sha256OfText(text3) {
40055
- return createHash7("sha256").update(text3).digest("hex");
40101
+ return createHash6("sha256").update(text3).digest("hex");
40056
40102
  }
40057
40103
  function sha256OfPath(path) {
40058
- return createHash7("sha256").update(readFileSync15(path)).digest("hex");
40104
+ return createHash6("sha256").update(readFileSync15(path)).digest("hex");
40059
40105
  }
40060
40106
  function collectBundledCatalog() {
40061
40107
  const out = [];
@@ -40977,6 +41023,933 @@ async function provisionCloudflareResources(options) {
40977
41023
  ];
40978
41024
  return result;
40979
41025
  }
41026
+ // src/lib/codex-safety-trust.ts
41027
+ import { createHash as createHash12, randomUUID as randomUUID3 } from "crypto";
41028
+ import { constants as constants12, closeSync as closeSync7, fsyncSync as fsyncSync5, fstatSync as fstatSync6, lstatSync as lstatSync15, mkdirSync as mkdirSync13, openSync as openSync7, readFileSync as readFileSync21, realpathSync as realpathSync10, writeFileSync as writeFileSync11 } from "fs";
41029
+ import { dirname as dirname21, isAbsolute as isAbsolute18, join as join24, relative as relative2 } from "path";
41030
+ import { homedir as homedir7 } from "os";
41031
+
41032
+ // src/lib/codex-safety-check.ts
41033
+ import { spawn, execFile as execFile2 } from "child_process";
41034
+ import { createHash as createHash7 } from "crypto";
41035
+ import { constants as constants4, openSync as openSync6, closeSync as closeSync6, fstatSync as fstatSync5, lstatSync as lstatSync10, readFileSync as readFileSync20, realpathSync as realpathSync9 } from "fs";
41036
+ import { dirname as dirname20, isAbsolute as isAbsolute17, resolve as resolve17 } from "path";
41037
+ import { promisify } from "util";
41038
+
41039
+ // src/native-safety.ts
41040
+ import { execFile } from "child_process";
41041
+ import { closeSync as closeSync5, constants as constants3, existsSync as existsSync13, fstatSync as fstatSync4, fsyncSync, linkSync as linkSync3, lstatSync as lstatSync9, mkdirSync as mkdirSync12, openSync as openSync5, readFileSync as readFileSync19, realpathSync as realpathSync8, renameSync as renameSync3, unlinkSync as unlinkSync5, writeFileSync as writeFileSync10 } from "fs";
41042
+ import { homedir as homedir6 } from "os";
41043
+ import { dirname as dirname19, isAbsolute as isAbsolute15, join as join20, resolve as resolve15 } from "path";
41044
+ class NativeSafetyError extends Error {
41045
+ code;
41046
+ reason;
41047
+ constructor(code2, reason) {
41048
+ super(`Native deletion safety refused (${code2}). Run the reviewed Hooks safety setup before using shell or patch tools.`);
41049
+ this.code = code2;
41050
+ this.reason = reason;
41051
+ this.name = "NativeSafetyError";
41052
+ }
41053
+ }
41054
+ var executionSchema = "hasna.hooks.execution.v1";
41055
+ var owner2 = (uid2) => uid2 === 0 || uid2 === process.getuid?.();
41056
+ function safeDirectory(path) {
41057
+ for (let cursor = path;; cursor = dirname19(cursor)) {
41058
+ const stat2 = lstatSync9(cursor);
41059
+ if (!stat2.isDirectory() || !owner2(stat2.uid) || stat2.mode & 18)
41060
+ throw new NativeSafetyError("binding_invalid");
41061
+ if (dirname19(cursor) === cursor)
41062
+ return;
41063
+ }
41064
+ }
41065
+ function canonicalHome(home) {
41066
+ if (!isAbsolute15(home) || home.includes("\x00") || realpathSync8(home) !== resolve15(home))
41067
+ throw new NativeSafetyError("binding_invalid");
41068
+ safeDirectory(home);
41069
+ return resolve15(home);
41070
+ }
41071
+ function parseRecord(text3, home) {
41072
+ const record3 = JSON.parse(text3);
41073
+ if (!record3 || Array.isArray(record3) || Object.keys(record3).sort().join() !== "command,harness,schema" || record3.schema !== "hasna.hooks.native-safety.v1" || record3.harness !== "sumi" || typeof record3.command !== "string")
41074
+ throw new NativeSafetyError("binding_invalid");
41075
+ const binding = nativeSafetyCommandBinding(record3.command);
41076
+ if (binding?.name !== "trash-guard" || binding.home !== home)
41077
+ throw new NativeSafetyError("binding_invalid");
41078
+ return record3;
41079
+ }
41080
+ async function evaluateRecord(event, record3, signal, executionAgent) {
41081
+ if (!event || event.hook_event_name !== "PreToolUse" || !["Bash", "apply_patch"].includes(event.tool_name) || typeof event.cwd !== "string" || !isAbsolute15(event.cwd) || !event.tool_input || typeof event.tool_input !== "object" || Array.isArray(event.tool_input))
41082
+ throw new NativeSafetyError("input_invalid");
41083
+ const input2 = JSON.stringify(executionAgent === undefined ? event : { ...event, hasna_execution: { schema: executionSchema, agent: executionAgent } });
41084
+ if (Buffer.byteLength(input2) > 1048576)
41085
+ throw new NativeSafetyError("input_invalid");
41086
+ const output2 = await new Promise((accept, reject) => {
41087
+ const child = execFile("/bin/sh", ["-c", record3.command], { cwd: "/", env: {}, encoding: "utf8", timeout: 8000, maxBuffer: 1048576, signal }, (error61, stdout, stderr) => {
41088
+ if (error61 || stderr) {
41089
+ reject(new NativeSafetyError("guard_failed"));
41090
+ return;
41091
+ }
41092
+ accept(stdout);
41093
+ });
41094
+ child.stdin?.on("error", () => {});
41095
+ child.stdin?.end(input2);
41096
+ });
41097
+ if (!output2)
41098
+ return { decision: "continue" };
41099
+ try {
41100
+ const value = JSON.parse(output2);
41101
+ if (Object.keys(value).join() !== "hookSpecificOutput")
41102
+ throw new Error;
41103
+ const verdict = value.hookSpecificOutput;
41104
+ if (verdict.hookEventName !== "PreToolUse")
41105
+ throw new Error;
41106
+ if (verdict.permissionDecision === "deny" && Object.keys(verdict).sort().join() === "hookEventName,permissionDecision,permissionDecisionReason" && typeof verdict.permissionDecisionReason === "string" && verdict.permissionDecisionReason.trim())
41107
+ return { decision: "deny", reason: verdict.permissionDecisionReason };
41108
+ if (event.tool_name === "Bash" && verdict.permissionDecision === "allow" && Object.keys(verdict).sort().join() === "hookEventName,permissionDecision,updatedInput" && typeof verdict.updatedInput?.command === "string" && verdict.updatedInput.command.trim() && (executionAgent === undefined || verdict.updatedInput.hasna_execution === executionSchema))
41109
+ return { decision: "rewrite", command: verdict.updatedInput.command };
41110
+ } catch {}
41111
+ throw new NativeSafetyError("guard_failed");
41112
+ }
41113
+ async function verifyNativeSafetyCommand(command, home = homedir6()) {
41114
+ home = canonicalHome(home);
41115
+ const record3 = parseRecord(JSON.stringify({ schema: "hasna.hooks.native-safety.v1", harness: "sumi", command }), home);
41116
+ const verdict = await evaluateRecord({ hook_event_name: "PreToolUse", tool_name: "Bash", cwd: home, tool_input: { command: "rm -- .hasna-trash-verification-only" } }, record3);
41117
+ if (verdict.decision !== "rewrite")
41118
+ throw new NativeSafetyError("guard_failed", verdict.decision === "deny" ? verdict.reason.slice(0, 512) : undefined);
41119
+ }
41120
+
41121
+ // src/lib/codex-safety-check.ts
41122
+ var versions = new Set(["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.155.0", "codex-cli 0.155.1", "codex-cli 0.156.1"]);
41123
+ var matcher = "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$";
41124
+
41125
+ class CodexSafetyError extends Error {
41126
+ code;
41127
+ constructor(code2) {
41128
+ super(`Codex deletion safety check failed (${code2}).`);
41129
+ this.code = code2;
41130
+ this.name = "CodexSafetyError";
41131
+ }
41132
+ }
41133
+ function need5(condition, code2) {
41134
+ if (!condition)
41135
+ throw new CodexSafetyError(code2);
41136
+ }
41137
+ var digest3 = (bytes) => createHash7("sha256").update(bytes).digest("hex");
41138
+ function nativeHeader(bytes) {
41139
+ if (bytes.length < 64 || !["arm64", "x64"].includes(process.arch))
41140
+ return false;
41141
+ if (process.platform === "darwin") {
41142
+ const cpu = process.arch === "arm64" ? 16777228 : 16777223;
41143
+ return bytes.readUInt32LE(0) === 4277009103 && bytes.readUInt32LE(4) === cpu && bytes.readUInt32LE(12) === 2 && bytes.readUInt32LE(16) > 0 && bytes.readUInt32LE(16) <= 4096 && bytes.readUInt32LE(20) > 0 && bytes.readUInt32LE(20) <= bytes.length - 32;
41144
+ }
41145
+ if (process.platform === "linux") {
41146
+ const offset = bytes.readBigUInt64LE(32), count = bytes.readUInt16LE(56);
41147
+ return bytes.subarray(0, 7).equals(Buffer.from([127, 69, 76, 70, 2, 1, 1])) && [2, 3].includes(bytes.readUInt16LE(16)) && bytes.readUInt16LE(18) === (process.arch === "arm64" ? 183 : 62) && bytes.readUInt32LE(20) === 1 && bytes.readUInt16LE(52) === 64 && bytes.readUInt16LE(54) === 56 && count > 0 && count <= 1024 && offset >= 64n && offset + BigInt(count * 56) <= BigInt(bytes.length);
41148
+ }
41149
+ return false;
41150
+ }
41151
+ function executable(command) {
41152
+ const found = isAbsolute17(command) ? command : Bun.which(command);
41153
+ need5(found, "native_unavailable");
41154
+ try {
41155
+ const path = realpathSync9(found);
41156
+ for (let parent = dirname20(path);; parent = dirname20(parent)) {
41157
+ const stat2 = lstatSync10(parent);
41158
+ need5(stat2.isDirectory() && [0, process.getuid?.()].includes(stat2.uid) && !(stat2.mode & 18), "native_unsafe_executable");
41159
+ if (parent === dirname20(parent))
41160
+ break;
41161
+ }
41162
+ const fd = openSync6(path, constants4.O_RDONLY | constants4.O_NOFOLLOW | constants4.O_NONBLOCK);
41163
+ try {
41164
+ const stat2 = fstatSync5(fd);
41165
+ need5(stat2.isFile() && [0, process.getuid?.()].includes(stat2.uid) && !(stat2.mode & 18) && !!(stat2.mode & 64) && stat2.size <= 256000000, "native_unsafe_executable");
41166
+ const bytes = readFileSync20(fd);
41167
+ need5(bytes.length <= 256000000, "native_unavailable");
41168
+ need5(nativeHeader(bytes), "native_executable_required");
41169
+ return { path, sha256: digest3(bytes) };
41170
+ } finally {
41171
+ closeSync6(fd);
41172
+ }
41173
+ } catch (error61) {
41174
+ if (error61 instanceof CodexSafetyError)
41175
+ throw error61;
41176
+ throw new CodexSafetyError("native_unavailable");
41177
+ }
41178
+ }
41179
+ async function connectCodexSafety(options) {
41180
+ const command = options.command ?? "codex", before = executable(command);
41181
+ const env = { ...process.env, HOME: options.home, NO_COLOR: "1" };
41182
+ for (const key of ["NODE_OPTIONS", "NODE_PATH", "BUN_OPTIONS", "BASH_ENV", "ENV", "DYLD_INSERT_LIBRARIES", "LD_PRELOAD"])
41183
+ delete env[key];
41184
+ if (options.codexHome === undefined)
41185
+ delete env.CODEX_HOME;
41186
+ else
41187
+ env.CODEX_HOME = options.codexHome;
41188
+ let version2;
41189
+ try {
41190
+ version2 = (await promisify(execFile2)(before.path, ["--version"], { env, cwd: options.home, timeout: 5000, killSignal: "SIGKILL", maxBuffer: 4096 })).stdout.trim();
41191
+ } catch {
41192
+ throw new CodexSafetyError("native_unavailable");
41193
+ }
41194
+ need5(versions.has(version2), "native_unsupported");
41195
+ need5(JSON.stringify(executable(command)) === JSON.stringify(before), "guard_changed");
41196
+ const child = spawn(before.path, ["app-server", "--strict-config", "--stdio"], { env, cwd: options.home, stdio: ["pipe", "pipe", "pipe"] });
41197
+ let stopped = false, buffer = Buffer.alloc(0), total = 0, nextId = 1;
41198
+ const pending = new Map;
41199
+ const fail = () => {
41200
+ stopped = true;
41201
+ for (const waiter of pending.values())
41202
+ waiter.reject(new CodexSafetyError("native_failed"));
41203
+ pending.clear();
41204
+ };
41205
+ const deadline = setTimeout(() => {
41206
+ fail();
41207
+ child.kill("SIGKILL");
41208
+ }, 15000);
41209
+ const exited = new Promise((done) => {
41210
+ child.once("exit", () => {
41211
+ fail();
41212
+ clearTimeout(deadline);
41213
+ done();
41214
+ });
41215
+ child.once("error", () => {
41216
+ fail();
41217
+ clearTimeout(deadline);
41218
+ done();
41219
+ });
41220
+ });
41221
+ child.stdin.on("error", fail);
41222
+ const account = (length) => {
41223
+ total += length;
41224
+ if (total > 8388608) {
41225
+ fail();
41226
+ child.kill("SIGKILL");
41227
+ return false;
41228
+ }
41229
+ return true;
41230
+ };
41231
+ child.stderr.on("data", (bytes) => {
41232
+ account(bytes.length);
41233
+ });
41234
+ child.stdout.on("data", (bytes) => {
41235
+ if (stopped || !account(bytes.length))
41236
+ return;
41237
+ buffer = Buffer.concat([buffer, bytes]);
41238
+ for (;; ) {
41239
+ const end = buffer.indexOf(10);
41240
+ if (end < 0)
41241
+ return;
41242
+ const line = buffer.subarray(0, end);
41243
+ buffer = buffer.subarray(end + 1);
41244
+ try {
41245
+ const value = JSON.parse(line.toString("utf8"));
41246
+ if (!value || typeof value !== "object" || Array.isArray(value))
41247
+ throw Error();
41248
+ if (!Object.hasOwn(value, "id"))
41249
+ continue;
41250
+ const waiter = pending.get(value.id);
41251
+ if (!waiter)
41252
+ throw Error();
41253
+ pending.delete(value.id);
41254
+ if (value.error || !Object.hasOwn(value, "result"))
41255
+ waiter.reject(new CodexSafetyError("native_failed"));
41256
+ else
41257
+ waiter.resolve(value.result);
41258
+ } catch {
41259
+ fail();
41260
+ child.kill("SIGKILL");
41261
+ return;
41262
+ }
41263
+ }
41264
+ });
41265
+ const request = (method, params) => new Promise((accept, reject) => {
41266
+ if (stopped) {
41267
+ reject(new CodexSafetyError("native_failed"));
41268
+ return;
41269
+ }
41270
+ const id = nextId++;
41271
+ pending.set(id, { resolve: accept, reject });
41272
+ child.stdin.write(JSON.stringify({ id, method, params }) + `
41273
+ `);
41274
+ });
41275
+ const rpc = {
41276
+ version: version2,
41277
+ request,
41278
+ async close() {
41279
+ fail();
41280
+ child.stdin.end();
41281
+ const force = setTimeout(() => child.kill("SIGKILL"), 1000);
41282
+ await exited;
41283
+ clearTimeout(force);
41284
+ clearTimeout(deadline);
41285
+ need5(JSON.stringify(executable(command)) === JSON.stringify(before), "guard_changed");
41286
+ }
41287
+ };
41288
+ try {
41289
+ await request("initialize", { clientInfo: { name: "hooks-safety-check", version: "1" }, capabilities: { experimentalApi: true } });
41290
+ child.stdin.write(`{"method":"initialized"}
41291
+ `);
41292
+ return rpc;
41293
+ } catch {
41294
+ await rpc.close();
41295
+ throw new CodexSafetyError("native_failed");
41296
+ }
41297
+ }
41298
+ function supportedDiscoveryWarnings(hooks, warnings) {
41299
+ if (warnings.length === 0)
41300
+ return true;
41301
+ if (warnings.length > 128)
41302
+ return false;
41303
+ const allowed = new Set;
41304
+ const sources = new Set(hooks.map((hook) => JSON.stringify([hook?.source, hook?.sourcePath])));
41305
+ for (const hook of hooks) {
41306
+ const path = hook?.sourcePath;
41307
+ if (typeof path !== "string" || path.length > 4096 || !isAbsolute17(path) || path !== resolve17(dirname20(path), "hooks.json") || !["user", "project"].includes(hook.source))
41308
+ continue;
41309
+ const toml = resolve17(dirname20(path), "config.toml");
41310
+ if (sources.has(JSON.stringify([hook.source, toml]))) {
41311
+ allowed.add(`loading hooks from both ${path} and ${toml}; prefer a single representation for this layer`);
41312
+ }
41313
+ }
41314
+ return warnings.every((warning) => typeof warning === "string" && allowed.delete(warning));
41315
+ }
41316
+ function selectCodexSafety(discovery, configuration, home, cwd, featureEnabled, requireReady = true) {
41317
+ const data = discovery?.data;
41318
+ need5(Array.isArray(data) && data.length === 1 && data[0]?.cwd === cwd && Array.isArray(data[0]?.hooks) && data[0].hooks.length <= 1e4 && Array.isArray(data[0].errors) && data[0].errors.length === 0 && Array.isArray(data[0].warnings) && supportedDiscoveryWarnings(data[0].hooks, data[0].warnings), "native_discovery_failed");
41319
+ const config2 = configuration?.config;
41320
+ need5(config2 && typeof config2 === "object" && !Array.isArray(config2), "native_discovery_failed");
41321
+ need5((config2.features?.hooks ?? featureEnabled) === true, "hooks_disabled");
41322
+ for (const hook2 of data[0].hooks)
41323
+ if (typeof hook2?.command === "string") {
41324
+ if (/\bhasna-native-safety-v\d+\b/.test(hook2.command))
41325
+ need5(nativeSafetyCommandBinding(hook2.command), "guard_definition_invalid");
41326
+ need5(!/^(?:hooks run trash-guard|hook-trash-guard)(?:\s|$)/.test(hook2.command.trim()), "guard_definition_invalid");
41327
+ }
41328
+ const selected = data[0].hooks.filter((hook2) => typeof hook2?.command === "string" && nativeSafetyCommandBinding(hook2.command)?.name === "trash-guard");
41329
+ need5(selected.length !== 0, "guard_missing");
41330
+ need5(selected.length === 1, "guard_ambiguous");
41331
+ const hook = selected[0], binding = nativeSafetyCommandBinding(hook.command);
41332
+ need5(binding.home === home && hook.eventName === "preToolUse" && hook.handlerType === "command" && hook.matcher === matcher && hook.timeoutSec === 10 && hook.async === false && hook.statusMessage === null && hook.additionalContextLimit === null && typeof hook.key === "string" && hook.key.length <= 4096 && typeof hook.sourcePath === "string" && isAbsolute17(hook.sourcePath) && hook.pluginId === null && hook.isManaged === false && ["user", "project"].includes(hook.source) && typeof hook.currentHash === "string" && /^sha256:[a-f0-9]{64}$/.test(hook.currentHash), "guard_definition_invalid");
41333
+ need5(typeof hook.enabled === "boolean" && ["trusted", "untrusted", "modified"].includes(hook.trustStatus), "guard_definition_invalid");
41334
+ if (requireReady) {
41335
+ need5(hook.enabled === true, "guard_disabled");
41336
+ need5(hook.trustStatus === "trusted", "guard_untrusted");
41337
+ }
41338
+ return { key: hook.key, command: hook.command, currentHash: hook.currentHash, sourcePath: hook.sourcePath, enabled: hook.enabled, trustStatus: hook.trustStatus };
41339
+ }
41340
+ async function readCodexSafety(rpc, home, cwd, options = {}) {
41341
+ need5(versions.has(rpc.version), "native_unsupported");
41342
+ const hooks = await rpc.request("hooks/list", { cwds: [cwd] });
41343
+ const configuration = await rpc.request("config/read", { cwd, includeLayers: options.includeLayers ?? false });
41344
+ let enabled;
41345
+ if (configuration?.config?.features?.hooks === undefined) {
41346
+ let cursor = null, count = 0;
41347
+ const seen = new Set;
41348
+ do {
41349
+ const page3 = await rpc.request("experimentalFeature/list", { cursor, limit: 100 });
41350
+ need5(Array.isArray(page3?.data) && page3.data.length <= 100 && (page3.nextCursor === null || typeof page3.nextCursor === "string"), "native_discovery_failed");
41351
+ count += page3.data.length;
41352
+ need5(count <= 1000, "native_discovery_failed");
41353
+ for (const feature of page3.data)
41354
+ if (feature?.name === "hooks") {
41355
+ need5(enabled === undefined && typeof feature.enabled === "boolean", "native_discovery_failed");
41356
+ enabled = feature.enabled;
41357
+ }
41358
+ cursor = page3.nextCursor;
41359
+ if (cursor !== null) {
41360
+ need5(cursor.length > 0 && cursor.length <= 4096 && !seen.has(cursor) && page3.data.length > 0, "native_discovery_failed");
41361
+ seen.add(cursor);
41362
+ }
41363
+ } while (cursor !== null);
41364
+ }
41365
+ return { guard: selectCodexSafety(hooks, configuration, home, cwd, enabled, options.requireReady ?? true), discovery: hooks, configuration };
41366
+ }
41367
+
41368
+ // src/lib/codex-safety-trust.ts
41369
+ class CodexTrustError extends Error {
41370
+ code;
41371
+ operationId;
41372
+ constructor(code2, operationId) {
41373
+ super("Codex safety trust failed (" + code2 + ").");
41374
+ this.code = code2;
41375
+ this.operationId = operationId;
41376
+ this.name = "CodexTrustError";
41377
+ }
41378
+ }
41379
+ function need7(value, code2 = "native_changed") {
41380
+ if (!value)
41381
+ throw new CodexTrustError(code2);
41382
+ }
41383
+ var sha4 = (value) => createHash12("sha256").update(value).digest("hex");
41384
+ function canonical4(value) {
41385
+ if (Array.isArray(value))
41386
+ return "[" + value.map(canonical4).join(",") + "]";
41387
+ if (value !== null && typeof value === "object")
41388
+ return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + canonical4(value[key])).join(",") + "}";
41389
+ return JSON.stringify(value);
41390
+ }
41391
+ function mergedConfiguration(value) {
41392
+ const copy = structuredClone(value);
41393
+ if (copy?.hooks && typeof copy.hooks === "object" && !Array.isArray(copy.hooks)) {
41394
+ for (const event of [
41395
+ "PreToolUse",
41396
+ "PermissionRequest",
41397
+ "PostToolUse",
41398
+ "PreCompact",
41399
+ "PostCompact",
41400
+ "SessionStart",
41401
+ "SessionEnd",
41402
+ "UserPromptSubmit",
41403
+ "SubagentStart",
41404
+ "SubagentStop",
41405
+ "Stop",
41406
+ "Interrupt"
41407
+ ]) {
41408
+ if (!Object.hasOwn(copy.hooks, event))
41409
+ copy.hooks[event] = [];
41410
+ }
41411
+ }
41412
+ return canonical4(copy);
41413
+ }
41414
+ function safeDirectory2(path) {
41415
+ for (let p = path;; p = dirname21(p)) {
41416
+ const s = lstatSync15(p);
41417
+ need7(s.isDirectory() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18), "preimage_unsafe");
41418
+ if (dirname21(p) === p)
41419
+ return;
41420
+ }
41421
+ }
41422
+ function readPrivate(path) {
41423
+ safeDirectory2(dirname21(path));
41424
+ const fd = openSync7(path, constants12.O_RDONLY | constants12.O_NOFOLLOW | constants12.O_NONBLOCK);
41425
+ try {
41426
+ const s = fstatSync6(fd);
41427
+ need7(s.isFile() && s.uid === process.getuid?.() && !(s.mode & 63) && s.size <= 4194304, "preimage_unsafe");
41428
+ const bytes = readFileSync21(fd);
41429
+ need7(bytes.length <= 4194304, "preimage_unsafe");
41430
+ return bytes;
41431
+ } finally {
41432
+ closeSync7(fd);
41433
+ }
41434
+ }
41435
+ function syncDirectory(path) {
41436
+ const fd = openSync7(path, constants12.O_RDONLY | constants12.O_DIRECTORY | constants12.O_NOFOLLOW);
41437
+ try {
41438
+ fsyncSync5(fd);
41439
+ } finally {
41440
+ closeSync7(fd);
41441
+ }
41442
+ }
41443
+ function exclusive(path, bytes) {
41444
+ const fd = openSync7(path, constants12.O_CREAT | constants12.O_EXCL | constants12.O_WRONLY | constants12.O_NOFOLLOW, 384);
41445
+ try {
41446
+ writeFileSync11(fd, bytes);
41447
+ fsyncSync5(fd);
41448
+ } finally {
41449
+ closeSync7(fd);
41450
+ }
41451
+ syncDirectory(dirname21(path));
41452
+ }
41453
+ async function inspect(rpc, home, cwd) {
41454
+ const observed = await readCodexSafety(rpc, home, cwd, { includeLayers: true, requireReady: false });
41455
+ const { guard, configuration, discovery } = observed;
41456
+ const layers = configuration?.layers;
41457
+ need7(Array.isArray(layers) && layers.length <= 1000, "native_config_invalid");
41458
+ const userLayers = layers.filter((layer2) => layer2?.name?.type === "user" && layer2.name.profile === null);
41459
+ need7(userLayers.length === 1, "native_config_invalid");
41460
+ const layer = userLayers[0], path = layer.name.file;
41461
+ need7(typeof path === "string" && isAbsolute18(path) && typeof layer.version === "string" && /^sha256:[a-f0-9]{64}$/.test(layer.version), "native_config_invalid");
41462
+ const canonicalPath = realpathSync10(path), inside = relative2(home, canonicalPath);
41463
+ need7(inside !== "" && !inside.startsWith("..") && !isAbsolute18(inside), "native_config_invalid");
41464
+ const definition = discovery.data[0].hooks.find((h) => h.key === guard.key);
41465
+ need7(definition.source === "user", "native_config_invalid");
41466
+ const before = readPrivate(canonicalPath);
41467
+ const rawState = layer.config?.hooks?.state?.[guard.key];
41468
+ need7(rawState === undefined || rawState && typeof rawState === "object" && !Array.isArray(rawState), "native_config_invalid");
41469
+ const desired = { ...rawState, enabled: true, trusted_hash: guard.currentHash };
41470
+ const planBase = {
41471
+ schema: "hasna.hooks.codex-safety-trust/v1",
41472
+ nativeVersion: rpc.version,
41473
+ cwd,
41474
+ key: guard.key,
41475
+ currentHash: guard.currentHash,
41476
+ configPath: path,
41477
+ configVersion: layer.version,
41478
+ action: guard.enabled && guard.trustStatus === "trusted" ? "unchanged" : "trust_and_enable",
41479
+ guardVerified: true,
41480
+ nativeAdoptionVerified: false
41481
+ };
41482
+ const plan = { ...planBase, planDigest: sha4(canonical4({ planBase, configuration, discovery, raw: sha4(before) })) };
41483
+ return { ...observed, layer, layers, canonicalPath, before, desired, plan };
41484
+ }
41485
+ async function context2(options, dependencies) {
41486
+ const home = realpathSync10(options.home ?? homedir7()), cwd = realpathSync10(options.cwd ?? process.cwd());
41487
+ need7(isAbsolute18(home) && isAbsolute18(cwd), "input_invalid");
41488
+ return { home, cwd, rpc: await (dependencies.connect ?? connectCodexSafety)({ ...options, home }) };
41489
+ }
41490
+ async function planCodexSafetyTrust(options = {}, dependencies = {}) {
41491
+ const { home, cwd, rpc } = await context2(options, dependencies);
41492
+ try {
41493
+ const before = await inspect(rpc, home, cwd);
41494
+ await (dependencies.verify ?? verifyNativeSafetyCommand)(before.guard.command, home);
41495
+ need7((await inspect(rpc, home, cwd)).plan.planDigest === before.plan.planDigest);
41496
+ return before.plan;
41497
+ } finally {
41498
+ await rpc.close();
41499
+ }
41500
+ }
41501
+ async function applyCodexSafetyTrust(options, dependencies = {}) {
41502
+ need7(/^[a-f0-9]{64}$/.test(options.expectedPlanDigest), "input_invalid");
41503
+ const selected = { ...options }, { home, cwd, rpc } = await context2(selected, dependencies);
41504
+ let operationId;
41505
+ try {
41506
+ const before = await inspect(rpc, home, cwd);
41507
+ need7(before.plan.planDigest === selected.expectedPlanDigest);
41508
+ await (dependencies.verify ?? verifyNativeSafetyCommand)(before.guard.command, home);
41509
+ need7((await inspect(rpc, home, cwd)).plan.planDigest === before.plan.planDigest);
41510
+ if (before.plan.action === "unchanged")
41511
+ return { ok: true, changed: false, ...before.plan };
41512
+ const expectedConfiguration = structuredClone(before.configuration);
41513
+ const expectedLayer = expectedConfiguration.layers.find((layer) => layer.name?.type === "user" && layer.name.profile === null);
41514
+ for (const config2 of [expectedConfiguration.config, expectedLayer.config]) {
41515
+ config2.hooks ??= {};
41516
+ config2.hooks.state ??= {};
41517
+ config2.hooks.state[before.guard.key] = before.desired;
41518
+ }
41519
+ const expectedDiscovery = structuredClone(before.discovery);
41520
+ const expectedGuard = expectedDiscovery.data[0].hooks.find((h) => h.key === before.guard.key);
41521
+ expectedGuard.enabled = true;
41522
+ expectedGuard.trustStatus = "trusted";
41523
+ operationId = randomUUID3();
41524
+ let directory2 = home;
41525
+ for (const part of [".hasna", "hooks", "native", "codex-trash-trust", operationId]) {
41526
+ directory2 = join24(directory2, part);
41527
+ try {
41528
+ mkdirSync13(directory2, { mode: 448 });
41529
+ syncDirectory(dirname21(directory2));
41530
+ } catch (error61) {
41531
+ if (error61.code !== "EEXIST")
41532
+ throw error61;
41533
+ }
41534
+ safeDirectory2(directory2);
41535
+ }
41536
+ exclusive(join24(directory2, "config.before"), before.before);
41537
+ exclusive(join24(directory2, "intent.json"), JSON.stringify({ operationId, plan: before.plan, configSHA256: sha4(before.before) }) + `
41538
+ `);
41539
+ need7(sha4(readPrivate(before.canonicalPath)) === sha4(before.before));
41540
+ const result = await rpc.request("config/batchWrite", {
41541
+ filePath: before.plan.configPath,
41542
+ expectedVersion: before.plan.configVersion,
41543
+ reloadUserConfig: true,
41544
+ edits: [{ keyPath: "hooks.state." + JSON.stringify(before.guard.key), value: before.desired, mergeStrategy: "replace" }]
41545
+ });
41546
+ need7(result?.status === "ok" && result.filePath === before.plan.configPath && result.overriddenMetadata === null && /^sha256:[a-f0-9]{64}$/.test(result.version));
41547
+ const after = await inspect(rpc, home, cwd);
41548
+ expectedLayer.version = result.version;
41549
+ need7(after.layer.version === result.version && after.plan.action === "unchanged" && mergedConfiguration(after.configuration.config) === mergedConfiguration(expectedConfiguration.config) && canonical4(after.configuration.layers) === canonical4(expectedConfiguration.layers) && canonical4(after.discovery) === canonical4(expectedDiscovery));
41550
+ const receipt = {
41551
+ ok: true,
41552
+ changed: true,
41553
+ operationId,
41554
+ planDigest: before.plan.planDigest,
41555
+ configVersion: result.version,
41556
+ guardVerified: true,
41557
+ nativeDiscoveryVerified: true,
41558
+ nativeAdoptionVerified: false,
41559
+ unrelatedConfigurationPreserved: true
41560
+ };
41561
+ exclusive(join24(directory2, "receipt.json"), JSON.stringify(receipt) + `
41562
+ `);
41563
+ return receipt;
41564
+ } catch (error61) {
41565
+ if (operationId)
41566
+ throw new CodexTrustError("trust_outcome_uncertain", operationId);
41567
+ throw error61;
41568
+ } finally {
41569
+ await rpc.close();
41570
+ }
41571
+ }
41572
+ // src/lib/claude-safety-check.ts
41573
+ import { constants as constants14, closeSync as closeSync15, fstatSync as fstatSync7, lstatSync as lstatSync21, openSync as openSync15, readFileSync as readFileSync23, realpathSync as realpathSync11 } from "fs";
41574
+ import { createHash as createHash17 } from "crypto";
41575
+ import { dirname as dirname22, isAbsolute as isAbsolute19, join as join27, resolve as resolve18 } from "path";
41576
+ import { homedir as homedir8 } from "os";
41577
+ class ClaudeSafetyError extends Error {
41578
+ code;
41579
+ constructor(code2) {
41580
+ super("Claude safety configuration check failed (" + code2 + ").");
41581
+ this.code = code2;
41582
+ this.name = "ClaudeSafetyError";
41583
+ }
41584
+ }
41585
+ function need8(value, code2) {
41586
+ if (!value)
41587
+ throw new ClaudeSafetyError(code2);
41588
+ }
41589
+ var sha6 = (value) => createHash17("sha256").update(value).digest("hex");
41590
+ function read3(path) {
41591
+ try {
41592
+ const resolved = realpathSync11(path);
41593
+ for (let parent = dirname22(resolved);; parent = dirname22(parent)) {
41594
+ const s = lstatSync21(parent);
41595
+ need8(s.isDirectory() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18), "settings_unsafe");
41596
+ if (dirname22(parent) === parent)
41597
+ break;
41598
+ }
41599
+ const fd = openSync15(resolved, constants14.O_RDONLY | constants14.O_NOFOLLOW | constants14.O_NONBLOCK);
41600
+ try {
41601
+ const s = fstatSync7(fd);
41602
+ need8(s.isFile() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18) && s.size <= 1048576, "settings_unsafe");
41603
+ const raw = readFileSync23(fd, "utf8");
41604
+ need8(Buffer.byteLength(raw) <= 1048576, "settings_unsafe");
41605
+ return { path, resolved, sha256: sha6(raw), raw };
41606
+ } finally {
41607
+ closeSync15(fd);
41608
+ }
41609
+ } catch (error61) {
41610
+ if (error61.code === "ENOENT")
41611
+ return { path, resolved: null, sha256: null, raw: null };
41612
+ if (error61 instanceof ClaudeSafetyError)
41613
+ throw error61;
41614
+ throw new ClaudeSafetyError("settings_unsafe");
41615
+ }
41616
+ }
41617
+ async function verifyClaudeSafetyConfiguration(options = {}, dependencies = {}) {
41618
+ const home = realpathSync11(options.home ?? homedir8()), cwd = realpathSync11(options.cwd ?? process.cwd());
41619
+ const paths = [
41620
+ options.userSettingsPath ?? join27(home, ".claude/settings.json"),
41621
+ join27(cwd, ".claude/settings.json"),
41622
+ options.localSettingsPath ?? join27(cwd, ".claude/settings.local.json")
41623
+ ];
41624
+ need8(paths.every((path) => isAbsolute19(path) && resolve18(path) === path), "settings_unsafe");
41625
+ const inspect2 = () => [...new Set(paths)].map(read3);
41626
+ const before = inspect2(), unique = [...new Map(before.filter((file2) => file2.resolved !== null).map((file2) => [file2.resolved, file2])).values()];
41627
+ const guards = [];
41628
+ for (const file2 of unique) {
41629
+ let settings;
41630
+ try {
41631
+ settings = JSON.parse(file2.raw);
41632
+ } catch {
41633
+ throw new ClaudeSafetyError("settings_invalid");
41634
+ }
41635
+ need8(settings && typeof settings === "object" && !Array.isArray(settings), "settings_invalid");
41636
+ need8(settings.disableAllHooks === undefined || settings.disableAllHooks === false, "hooks_disabled");
41637
+ if (settings.hooks === undefined)
41638
+ continue;
41639
+ need8(settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks), "settings_invalid");
41640
+ let count = 0;
41641
+ for (const [event, groups] of Object.entries(settings.hooks)) {
41642
+ need8(Array.isArray(groups), "settings_invalid");
41643
+ for (const group of groups) {
41644
+ need8(group && Array.isArray(group.hooks), "settings_invalid");
41645
+ for (const handler of group.hooks) {
41646
+ need8(++count <= 1e4 && handler && typeof handler === "object", "settings_invalid");
41647
+ if (typeof handler.command !== "string")
41648
+ continue;
41649
+ const binding = nativeSafetyCommandBinding(handler.command);
41650
+ if (/\bhasna-native-safety-v\d+\b/.test(handler.command))
41651
+ need8(binding, "guard_definition_invalid");
41652
+ need8(!/^(?:hooks run trash-guard|hook-trash-guard)(?:\s|$)/.test(handler.command.trim()), "guard_definition_invalid");
41653
+ if (binding?.name !== "trash-guard")
41654
+ continue;
41655
+ need8(binding.home === home && event === "PreToolUse" && handler.type === "command" && group.matcher === "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$" && handler.timeout === 10 && (handler.async === undefined || handler.async === false) && handler.asyncRewake !== true, "guard_definition_invalid");
41656
+ guards.push({ command: handler.command, path: file2.path });
41657
+ }
41658
+ }
41659
+ }
41660
+ }
41661
+ need8(guards.length !== 0, "guard_missing");
41662
+ need8(guards.length === 1, "guard_ambiguous");
41663
+ try {
41664
+ await (dependencies.verify ?? verifyNativeSafetyCommand)(guards[0].command, home);
41665
+ } catch {
41666
+ throw new ClaudeSafetyError("guard_failed");
41667
+ }
41668
+ need8(JSON.stringify(inspect2()) === JSON.stringify(before), "settings_changed");
41669
+ return {
41670
+ ok: true,
41671
+ target: "claude",
41672
+ cwd,
41673
+ configurationVerified: true,
41674
+ guardVerified: true,
41675
+ sources: before.map(({ path, sha256 }) => ({ path, sha256 })),
41676
+ guardSHA256: sha6(guards[0].command),
41677
+ runtimePolicyVerified: false,
41678
+ nativeDiscoveryVerified: false,
41679
+ nativeAdoptionVerified: false
41680
+ };
41681
+ }
41682
+ // src/lib/native-safety-registration.ts
41683
+ import { createHash as createHash19, randomUUID as randomUUID5 } from "crypto";
41684
+ import { constants as constants15, closeSync as closeSync17, fstatSync as fstatSync15, fsyncSync as fsyncSync7, lstatSync as lstatSync23, mkdirSync as mkdirSync14, openSync as openSync17, readFileSync as readFileSync25, realpathSync as realpathSync12, writeFileSync as writeFileSync12 } from "fs";
41685
+ import { basename as basename9, dirname as dirname23, isAbsolute as isAbsolute20, join as join29, relative as relative3, resolve as resolve19 } from "path";
41686
+ import { homedir as homedir9 } from "os";
41687
+ class NativeRegistrationError extends Error {
41688
+ code;
41689
+ operationId;
41690
+ constructor(code2, operationId) {
41691
+ super("Native safety registration failed (" + code2 + ").");
41692
+ this.code = code2;
41693
+ this.operationId = operationId;
41694
+ this.name = "NativeRegistrationError";
41695
+ }
41696
+ }
41697
+ var need9 = (value, code2) => {
41698
+ if (!value)
41699
+ throw new NativeRegistrationError(code2);
41700
+ };
41701
+ var sha7 = (value) => createHash19("sha256").update(value).digest("hex");
41702
+ function canonical6(value) {
41703
+ if (Array.isArray(value))
41704
+ return "[" + value.map(canonical6).join(",") + "]";
41705
+ if (value !== null && typeof value === "object")
41706
+ return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + canonical6(value[key])).join(",") + "}";
41707
+ return JSON.stringify(value);
41708
+ }
41709
+ function safeDirectory3(path) {
41710
+ for (let p = path;; p = dirname23(p)) {
41711
+ const s = lstatSync23(p);
41712
+ need9(s.isDirectory() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18), "settings_unsafe");
41713
+ if (dirname23(p) === p)
41714
+ return;
41715
+ }
41716
+ }
41717
+ function read5(path) {
41718
+ let resolved;
41719
+ try {
41720
+ resolved = realpathSync12(path);
41721
+ } catch (error61) {
41722
+ if (error61.code !== "ENOENT")
41723
+ throw new NativeRegistrationError("settings_unsafe");
41724
+ try {
41725
+ lstatSync23(path);
41726
+ throw new NativeRegistrationError("settings_unsafe");
41727
+ } catch (missing2) {
41728
+ if (missing2.code !== "ENOENT")
41729
+ throw missing2;
41730
+ }
41731
+ let ancestor = dirname23(path);
41732
+ const suffix = [basename9(path)];
41733
+ for (;; ) {
41734
+ try {
41735
+ resolved = join29(realpathSync12(ancestor), ...suffix);
41736
+ break;
41737
+ } catch (missing2) {
41738
+ if (missing2.code !== "ENOENT" || dirname23(ancestor) === ancestor || suffix.length >= 64)
41739
+ throw new NativeRegistrationError("settings_unsafe");
41740
+ try {
41741
+ lstatSync23(ancestor);
41742
+ throw new NativeRegistrationError("settings_unsafe");
41743
+ } catch (absent) {
41744
+ if (absent.code !== "ENOENT")
41745
+ throw absent;
41746
+ }
41747
+ suffix.unshift(basename9(ancestor));
41748
+ ancestor = dirname23(ancestor);
41749
+ }
41750
+ }
41751
+ safeDirectory3(realpathSync12(ancestor));
41752
+ return { resolved, raw: null, sha256: "absent" };
41753
+ }
41754
+ safeDirectory3(dirname23(resolved));
41755
+ let fd;
41756
+ try {
41757
+ fd = openSync17(resolved, constants15.O_RDONLY | constants15.O_NOFOLLOW | constants15.O_NONBLOCK);
41758
+ } catch (error61) {
41759
+ if (error61.code === "ENOENT")
41760
+ return { resolved, raw: null, sha256: "absent" };
41761
+ throw new NativeRegistrationError("settings_unsafe");
41762
+ }
41763
+ try {
41764
+ const s = fstatSync15(fd);
41765
+ need9(s.isFile() && s.uid === process.getuid?.() && !(s.mode & 18) && s.nlink === 1 && s.size <= 1048576, "settings_unsafe");
41766
+ const bytes = readFileSync25(fd);
41767
+ need9(bytes.length <= 1048576, "settings_unsafe");
41768
+ const raw = bytes.toString("utf8");
41769
+ need9(Buffer.from(raw).equals(bytes), "settings_invalid");
41770
+ return { resolved, raw, sha256: sha7(bytes) };
41771
+ } finally {
41772
+ closeSync17(fd);
41773
+ }
41774
+ }
41775
+ function assertLosslessJson(raw) {
41776
+ const tokens = /"(?:\\[\s\S]|[^"\\])*"|-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?|true|false|null|[{}\[\],:]/g;
41777
+ const stack = [];
41778
+ function decimal(text3) {
41779
+ const match = /^(-?)([0-9]+)(?:\.([0-9]+))?(?:[eE]([+-]?[0-9]+))?$/.exec(text3);
41780
+ need9((match[4]?.replace(/^[+-]/, "").length ?? 0) <= 6, "settings_invalid");
41781
+ let digits = (match[2] + (match[3] ?? "")).replace(/^0+/, "");
41782
+ if (!digits)
41783
+ return match[1] ? "-0" : "0";
41784
+ let exponent = Number(match[4] ?? "0") - (match[3]?.length ?? 0);
41785
+ while (digits.endsWith("0")) {
41786
+ digits = digits.slice(0, -1);
41787
+ exponent++;
41788
+ }
41789
+ return match[1] + digits + "e" + exponent;
41790
+ }
41791
+ for (const match of raw.matchAll(tokens)) {
41792
+ const token = match[0], current2 = stack.at(-1);
41793
+ if (token === "{")
41794
+ stack.push({ kind: "object", key: true, names: new Set });
41795
+ else if (token === "[")
41796
+ stack.push({ kind: "array", key: false, names: new Set });
41797
+ else if (token === "}" || token === "]")
41798
+ stack.pop();
41799
+ else if (token === "," && current2?.kind === "object")
41800
+ current2.key = true;
41801
+ else if (token.startsWith('"') && current2?.kind === "object" && current2.key) {
41802
+ const key = JSON.parse(token);
41803
+ need9(!current2.names.has(key), "settings_invalid");
41804
+ current2.names.add(key);
41805
+ current2.key = false;
41806
+ } else if (/^-?[0-9]/.test(token)) {
41807
+ const parsed = Number(token);
41808
+ need9(Number.isFinite(parsed) && decimal(token) === decimal(JSON.stringify(parsed)), "settings_invalid");
41809
+ }
41810
+ }
41811
+ }
41812
+ function validateSettings(raw) {
41813
+ let settings;
41814
+ try {
41815
+ settings = JSON.parse(raw ?? "{}");
41816
+ } catch {
41817
+ throw new NativeRegistrationError("settings_invalid");
41818
+ }
41819
+ need9(settings && typeof settings === "object" && !Array.isArray(settings), "settings_invalid");
41820
+ assertLosslessJson(raw ?? "{}");
41821
+ need9(settings.disableAllHooks === undefined || settings.disableAllHooks === false, "hooks_disabled");
41822
+ if (settings.hooks !== undefined) {
41823
+ need9(settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks), "settings_invalid");
41824
+ for (const groups of Object.values(settings.hooks)) {
41825
+ need9(Array.isArray(groups), "settings_invalid");
41826
+ for (const group of groups) {
41827
+ need9(group && Array.isArray(group.hooks), "settings_invalid");
41828
+ for (const hook of group.hooks) {
41829
+ need9(hook && typeof hook === "object", "settings_invalid");
41830
+ if (registrationName(hook.command) === "trash-guard") {
41831
+ need9((hook.async === undefined || hook.async === false) && hook.asyncRewake !== true, "registration_conflict");
41832
+ }
41833
+ }
41834
+ }
41835
+ }
41836
+ }
41837
+ return settings;
41838
+ }
41839
+ function inspect2(options) {
41840
+ need9(options?.target === "codex" || options?.target === "claude", "input_invalid");
41841
+ const home = realpathSync12(homedir9()), path = getSettingsPath("global", options.target);
41842
+ need9(isAbsolute20(path) && resolve19(path) === path, "input_invalid");
41843
+ const nativeHome = options.target === "codex" ? process.env.CODEX_HOME : process.env.CLAUDE_CONFIG_DIR;
41844
+ if (nativeHome !== undefined) {
41845
+ need9(nativeHome.length > 0 && isAbsolute20(nativeHome), "input_invalid");
41846
+ const selectedNative = read5(resolve19(nativeHome, options.target === "codex" ? "hooks.json" : "settings.json"));
41847
+ need9(read5(path).resolved === selectedNative.resolved, "input_invalid");
41848
+ }
41849
+ const before = read5(path), inside = relative3(home, before.resolved);
41850
+ need9(inside !== "" && !inside.startsWith("..") && !isAbsolute20(inside), "settings_unsafe");
41851
+ const parsed = validateSettings(before.raw);
41852
+ let preview2;
41853
+ try {
41854
+ preview2 = previewNativeSafetyRegistration(options.target);
41855
+ } catch {
41856
+ throw new NativeRegistrationError("registration_conflict");
41857
+ }
41858
+ need9(preview2.before === before.raw, "settings_changed");
41859
+ validateSettings(preview2.after);
41860
+ const changed = canonical6(parsed) !== canonical6(JSON.parse(preview2.after));
41861
+ const base = {
41862
+ schema: "hasna.hooks.native-safety-registration/v1",
41863
+ target: options.target,
41864
+ settingsPath: path,
41865
+ resolvedPath: before.resolved,
41866
+ beforeSHA256: before.sha256,
41867
+ desiredSHA256: changed ? sha7(preview2.after) : before.sha256,
41868
+ commandSHA256: sha7(preview2.command),
41869
+ action: changed ? "register" : "unchanged",
41870
+ guardVerified: true,
41871
+ nativeAdoptionVerified: false
41872
+ };
41873
+ const plan = { ...base, planDigest: sha7(canonical6(base)) };
41874
+ need9(canonical6(read5(path)) === canonical6(before), "settings_changed");
41875
+ return { home, before, preview: preview2, plan };
41876
+ }
41877
+ function syncDirectory2(path) {
41878
+ const fd = openSync17(path, constants15.O_RDONLY | constants15.O_DIRECTORY | constants15.O_NOFOLLOW);
41879
+ try {
41880
+ fsyncSync7(fd);
41881
+ } finally {
41882
+ closeSync17(fd);
41883
+ }
41884
+ }
41885
+ function exclusive2(path, bytes) {
41886
+ const fd = openSync17(path, constants15.O_CREAT | constants15.O_EXCL | constants15.O_WRONLY | constants15.O_NOFOLLOW, 384);
41887
+ try {
41888
+ writeFileSync12(fd, bytes);
41889
+ fsyncSync7(fd);
41890
+ } finally {
41891
+ closeSync17(fd);
41892
+ }
41893
+ syncDirectory2(dirname23(path));
41894
+ }
41895
+ async function planNativeSafetyRegistration(options, dependencies = {}) {
41896
+ const selected = { ...options }, before = inspect2(selected);
41897
+ try {
41898
+ await (dependencies.verify ?? verifyNativeSafetyCommand)(before.preview.command, before.home);
41899
+ } catch {
41900
+ throw new NativeRegistrationError("guard_failed");
41901
+ }
41902
+ need9(inspect2(selected).plan.planDigest === before.plan.planDigest, "settings_changed");
41903
+ return before.plan;
41904
+ }
41905
+ async function applyNativeSafetyRegistration(options, dependencies = {}) {
41906
+ const selected = { ...options };
41907
+ need9(/^[a-f0-9]{64}$/.test(selected.expectedPlanDigest), "input_invalid");
41908
+ const plan = await planNativeSafetyRegistration(selected, dependencies);
41909
+ need9(plan.planDigest === selected.expectedPlanDigest, "settings_changed");
41910
+ const before = inspect2(selected);
41911
+ need9(before.plan.planDigest === plan.planDigest, "settings_changed");
41912
+ if (plan.action === "unchanged")
41913
+ return { ok: true, changed: false, ...plan };
41914
+ const operationId = randomUUID5();
41915
+ let directory2 = before.home;
41916
+ for (const part of [".hasna", "hooks", "native", "trash-registration", operationId]) {
41917
+ directory2 = join29(directory2, part);
41918
+ try {
41919
+ mkdirSync14(directory2, { mode: 448 });
41920
+ syncDirectory2(dirname23(directory2));
41921
+ } catch (error61) {
41922
+ if (error61.code !== "EEXIST")
41923
+ throw new NativeRegistrationError("settings_unsafe");
41924
+ }
41925
+ safeDirectory3(directory2);
41926
+ }
41927
+ if (before.before.raw !== null)
41928
+ exclusive2(join29(directory2, "settings.before"), before.before.raw);
41929
+ exclusive2(join29(directory2, "intent.json"), JSON.stringify({ operationId, plan }) + `
41930
+ `);
41931
+ try {
41932
+ need9(inspect2(selected).plan.planDigest === plan.planDigest, "settings_changed");
41933
+ const result = installHook("trash-guard", { target: selected.target, overwrite: true, expectedSettingsSha256: plan.beforeSHA256 });
41934
+ need9(result.success, "apply_outcome_uncertain");
41935
+ const after = inspect2(selected);
41936
+ need9(after.before.sha256 === plan.desiredSHA256 && after.plan.resolvedPath === plan.resolvedPath && after.plan.action === "unchanged" && after.plan.commandSHA256 === plan.commandSHA256, "apply_outcome_uncertain");
41937
+ const receipt = {
41938
+ ok: true,
41939
+ changed: true,
41940
+ operationId,
41941
+ planDigest: plan.planDigest,
41942
+ settingsSHA256: after.before.sha256,
41943
+ guardVerified: true,
41944
+ nativeAdoptionVerified: false
41945
+ };
41946
+ exclusive2(join29(directory2, "receipt.json"), JSON.stringify(receipt) + `
41947
+ `);
41948
+ return receipt;
41949
+ } catch {
41950
+ throw new NativeRegistrationError("apply_outcome_uncertain", operationId);
41951
+ }
41952
+ }
40980
41953
 
40981
41954
  // src/index.ts
40982
41955
  function installHookForProject(name, options = {}) {
@@ -40999,11 +41972,11 @@ async function runHook(name, input2, options = {}) {
40999
41972
  throw new Error(`Hook '${name}' not found`);
41000
41973
  const hookScript = getHookPath(name) + "/src/hook.ts";
41001
41974
  const resolvedScript = custom3?.scriptPath;
41002
- const script = resolvedScript ?? (existsSync14(hookScript) ? hookScript : undefined);
41975
+ const script = resolvedScript ?? (existsSync15(hookScript) ? hookScript : undefined);
41003
41976
  if (!script)
41004
41977
  throw new Error(`Hook script not found: ${name}`);
41005
41978
  const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
41006
- const content = readFileSync19(script);
41979
+ const content = readFileSync26(script);
41007
41980
  const check2 = checkScriptHash2(name, sha256Of2(content));
41008
41981
  if (!check2.ok) {
41009
41982
  throw new Error(`Hook '${name}' script changed since it was trusted (sha256 ${check2.expected} != ${check2.actual}). Run 'hooks trust ${name}' to trust the new content.`);
@@ -41095,6 +42068,7 @@ export {
41095
42068
  writeLock,
41096
42069
  writeCustomHook,
41097
42070
  verifyScriptHash,
42071
+ verifyClaudeSafetyConfiguration,
41098
42072
  upsertHookRecord,
41099
42073
  updateProfile,
41100
42074
  uninstallHook,
@@ -41133,6 +42107,8 @@ export {
41133
42107
  readCustomManifest,
41134
42108
  provisionCloudflareResources,
41135
42109
  planSync,
42110
+ planNativeSafetyRegistration,
42111
+ planCodexSafetyTrust,
41136
42112
  pinInstalledHook,
41137
42113
  parseStorageTables,
41138
42114
  parseManifest,
@@ -41186,12 +42162,15 @@ export {
41186
42162
  createProfile,
41187
42163
  checkScriptHash,
41188
42164
  buildCodewithTomlFragment,
42165
+ applyNativeSafetyRegistration,
42166
+ applyCodexSafetyTrust,
41189
42167
  allowLocalStore,
41190
42168
  STORAGE_TABLES,
41191
42169
  STORAGE_DATABASE_ENV,
41192
42170
  STORAGE_BACKEND_ENV,
41193
42171
  STORAGE_BACKENDS,
41194
42172
  RETIRED_STORAGE_MODE_ENV,
42173
+ NativeRegistrationError,
41195
42174
  HOOK_EVENTS,
41196
42175
  HOOKS_STORAGE_TABLES,
41197
42176
  HOOKS_STORAGE_FALLBACK_ENV,
@@ -41201,5 +42180,7 @@ export {
41201
42180
  HOOKS_LOCAL_OPT_IN_ENV_KEYS,
41202
42181
  HOOKS,
41203
42182
  DEFAULT_SERVE_PORT,
42183
+ CodexTrustError,
42184
+ ClaudeSafetyError,
41204
42185
  CATEGORIES
41205
42186
  };