@hasna/hooks 0.10.6 → 0.10.8

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);
@@ -7923,19 +7941,20 @@ function writeCodexSettings(path, settings) {
7923
7941
  // src/lib/claude-skills-coordination.ts
7924
7942
  import { createHash as createHash5 } from "crypto";
7925
7943
 
7926
- // ../../node_modules/.bun/@hasna+skills@0.9.14+a00d3064559565bb/node_modules/@hasna/skills/dist/index.js
7944
+ // ../../node_modules/.bun/@hasna+skills@0.9.17+a00d3064559565bb/node_modules/@hasna/skills/dist/index.js
7927
7945
  import { existsSync as existsSync9 } from "fs";
7928
7946
  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";
7936
7954
  import { createHash as createHash16 } from "crypto";
7937
7955
  import { lstatSync as lstatSync20, readFileSync as readFileSync16, readdirSync as readdirSync9, realpathSync as realpathSync4 } from "fs";
7938
7956
  import { basename as basename8, dirname as dirname16, isAbsolute as isAbsolute14, join as join26, resolve as resolve14, sep as sep5 } from "path";
7957
+ import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
7939
7958
  import { dirname as dirname32, isAbsolute as isAbsolute3, join as join72, resolve as resolve32 } from "path";
7940
7959
  import { lstatSync as lstatSync3, readFileSync as readFileSync6, opendirSync } from "fs";
7941
7960
  import { createHash as createHash22 } from "crypto";
@@ -7944,7 +7963,7 @@ import { dirname as dirname42, isAbsolute as isAbsolute4, join as join82, resolv
7944
7963
  import { createHash as createHash32 } from "crypto";
7945
7964
  import { closeSync as closeSync3, constants as constants2, fstatSync as fstatSync2, lstatSync as lstatSync5, openSync as openSync3, readSync } from "fs";
7946
7965
  import { dirname as dirname5, isAbsolute as isAbsolute5, resolve as resolve5 } from "path";
7947
- import { createHash as createHash4 } from "crypto";
7966
+ import { createHash as createHash42 } from "crypto";
7948
7967
  import { lstatSync as lstatSync6, readlinkSync } from "fs";
7949
7968
  import { dirname as dirname6, isAbsolute as isAbsolute6, join as join9, parse, resolve as resolve6, sep as sep22 } from "path";
7950
7969
  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 +15091,7 @@ function* bundleHashParts(files) {
15072
15091
  yield new TextEncoder().encode("\x00");
15073
15092
  }
15074
15093
  function hashBundleFiles(files) {
15075
- const hash = createHash3(CONTENT_HASH_ALGORITHM);
15094
+ const hash = createHash4(CONTENT_HASH_ALGORITHM);
15076
15095
  for (const part of bundleHashParts(files))
15077
15096
  hash.update(part);
15078
15097
  return hash.digest("hex");
@@ -15416,8 +15435,8 @@ function assertAgentPolicyCollections(policy) {
15416
15435
  text(source.path);
15417
15436
  requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
15418
15437
  if (source.hashMode !== undefined)
15419
- requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
15420
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
15438
+ requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
15439
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
15421
15440
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute22(source.path) && resolve22(source.path) === source.path && basename2(source.path) === "settings.json");
15422
15441
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
15423
15442
  requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute22(source.path) && resolve22(source.path) === source.path && basename2(source.path) === "known_marketplaces.json");
@@ -15908,11 +15927,11 @@ function hashDiscoveryPathFile(path, budget, changes) {
15908
15927
  const after = trace(path);
15909
15928
  if (JSON.stringify(before.rows) !== JSON.stringify(after.rows) || before.file !== after.file)
15910
15929
  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");
15930
+ return createHash42("sha256").update(JSON.stringify({ version: 1, path, rows: before.rows, content })).digest("hex");
15912
15931
  }
15913
15932
  var package_default = {
15914
15933
  name: "@hasna/skills",
15915
- version: "0.9.14",
15934
+ version: "0.9.17",
15916
15935
  description: "Skills library for AI coding agents",
15917
15936
  type: "module",
15918
15937
  bin: {
@@ -16767,7 +16786,7 @@ function assertClaudeHookEventsReplacement(before, replacement, events) {
16767
16786
  need2(JSON.stringify(withoutOwnedEvents(original)) === JSON.stringify(withoutOwnedEvents(next)), `${label} update changed settings outside hooks.${label}`);
16768
16787
  }
16769
16788
  var PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
16770
- function inferencePreference(key, child) {
16789
+ function inferencePreference(key, child, version) {
16771
16790
  if (key === "skipDangerousModePermissionPrompt") {
16772
16791
  need2(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
16773
16792
  return false;
@@ -16780,7 +16799,7 @@ function inferencePreference(key, child) {
16780
16799
  return false;
16781
16800
  need2(child.kind === "object", "requires an object of model preferences");
16782
16801
  child.entries = child.entries.filter(([model, preferences]) => {
16783
- if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
16802
+ if (version === 2 && !BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
16784
16803
  return true;
16785
16804
  const effort = preferences.entries[0][1];
16786
16805
  need2(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
@@ -16791,7 +16810,7 @@ function inferencePreference(key, child) {
16791
16810
  function settingsDigest(text2, version = 1) {
16792
16811
  const value = parse3(text2);
16793
16812
  value.entries = value.entries.filter(([key, child]) => {
16794
- if (version === 2 && inferencePreference(key, child))
16813
+ if (version !== 1 && inferencePreference(key, child, version))
16795
16814
  return false;
16796
16815
  if (key === "model") {
16797
16816
  need2(child.kind === "string", "contains an invalid model selection");
@@ -16829,6 +16848,16 @@ function hashClaudeSettingsReplacementV2(text2, budget) {
16829
16848
  budget.remaining -= bytes;
16830
16849
  return settingsDigest(text2, 2);
16831
16850
  }
16851
+ function captureClaudeSettingsV3(path, budget = { remaining: 256 * 1024 * 1024 }) {
16852
+ return { path, hashMode: "claude-settings-v3", sha256: settingsDigest(readSettings(path, budget), 3) };
16853
+ }
16854
+ function hashClaudeSettingsReplacementV3(text2, budget) {
16855
+ need2(typeof text2 === "string", "requires settings text");
16856
+ const bytes = Buffer.byteLength(text2);
16857
+ need2(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
16858
+ budget.remaining -= bytes;
16859
+ return settingsDigest(text2, 3);
16860
+ }
16832
16861
  var sha = (data) => createHash10("sha256").update(data).digest("hex");
16833
16862
  var ROOTS = NATIVE_SKILL_ROOTS;
16834
16863
  function assertSafePath(path) {
@@ -17282,6 +17311,8 @@ function applyAgentIntegration(plan) {
17282
17311
  parseManagedSkillPolicy(change.after);
17283
17312
  const aliases = plan.rootAliases ?? [];
17284
17313
  recheckRootAliases(aliases);
17314
+ for (const agent of plan.retainedReviewChecks?.agents ?? [])
17315
+ assertManagedAgentBridge(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
17285
17316
  for (const agent of plan.managedAgentChecks?.agents ?? [])
17286
17317
  assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
17287
17318
  for (const binding of plan.discoveryBefore ?? []) {
@@ -18295,13 +18326,17 @@ function unchanged2(before) {
18295
18326
  const after = snapshot(before.file, before.privateFile, before.readOnlyPackage ? { readOnlyPackage: true } : {});
18296
18327
  need4(sameStat(before.stat, after.stat) && after.sha256 === before.sha256, "INPUT_CHANGED");
18297
18328
  }
18298
- var sha2 = (text2) => createHash15("sha256").update(text2).digest("hex");
18329
+ var events = { UserPromptSubmit: ["userPromptSubmit", "user_prompt_submit"], SessionStart: ["sessionStart", "session_start"], SubagentStart: ["subagentStart", "subagent_start"] };
18330
+ var matchedEvents = { ...events, PreToolUse: ["preToolUse", "pre_tool_use"] };
18299
18331
  var proofs = new WeakMap;
18332
+ var sha2 = (text2) => createHash15("sha256").update(text2).digest("hex");
18333
+ var proofs2 = new WeakMap;
18300
18334
  function assertCodexHookDiscoveryRecovery(binding, witness) {
18301
- const proof = proofs.get(witness);
18335
+ const proof = proofs2.get(witness);
18302
18336
  need4(!!proof && binding.agent === "codex" && witness.discoverySha256 === sha2(JSON.stringify(binding)), "RECONCILE_DISCOVERY_RECOVERY_CHANGED");
18303
18337
  unchanged2(proof.before);
18304
18338
  unchanged2(proof.current);
18339
+ proof.recheckAdditions();
18305
18340
  }
18306
18341
  function verifiesCodexHookDiscoverySource(binding, source, currentDigest, witness) {
18307
18342
  if (!witness || source.path !== witness.configPath || source.hashMode !== "bytes")
@@ -18343,13 +18378,13 @@ function read2(path, changes) {
18343
18378
  return readFileSync16(path, "utf8");
18344
18379
  }
18345
18380
  function projected(source, changes, budget = discoveryByteBudget()) {
18346
- if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
18381
+ if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
18347
18382
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
18348
18383
  throw new Error("Claude settings witnesses require exact typed metadata");
18349
- const current2 = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
18384
+ const current2 = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
18350
18385
  if (current2 !== source.sha256)
18351
18386
  throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
18352
- return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current2;
18387
+ return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current2;
18353
18388
  }
18354
18389
  if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
18355
18390
  if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256) || changes?.has(source.path))
@@ -18411,7 +18446,7 @@ function verifyAgentDiscovery(binding, codexRecovery) {
18411
18446
  safe4(root2);
18412
18447
  }
18413
18448
  function assertMarketplaceBinding(binding, source) {
18414
- if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
18449
+ if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
18415
18450
  throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
18416
18451
  if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
18417
18452
  throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
@@ -18420,7 +18455,10 @@ function rebindAgentDiscovery(binding, changes) {
18420
18455
  const budget = discoveryByteBudget();
18421
18456
  return { ...binding, sources: binding.sources.map((source) => {
18422
18457
  assertMarketplaceBinding(binding, source);
18423
- return { ...source, sha256: projected(source, changes, budget) };
18458
+ const sha2562 = projected(source, changes, budget);
18459
+ if (!changes.has(source.path) && sha2562 !== source.sha256)
18460
+ throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
18461
+ return { ...source, sha256: sha2562 };
18424
18462
  }) };
18425
18463
  }
18426
18464
  function assertProjectDiscovery(agent, directories, home, canonical4 = resolve14, reviewed) {
@@ -18532,6 +18570,19 @@ function resolveAgentDiscovery(options) {
18532
18570
  }
18533
18571
  }
18534
18572
  }
18573
+ if (options.reviewed === undefined && options.retainedReview !== undefined) {
18574
+ const retained = options.retainedReview;
18575
+ if (retained.agent !== agent || retained.method !== "reviewed")
18576
+ unresolved("invalid retained review");
18577
+ verifyAgentDiscovery(retained);
18578
+ if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
18579
+ unresolved("retained review is missing the full agent configuration source");
18580
+ if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
18581
+ unresolved("retained settings witness names another configuration source");
18582
+ if (sources.some((source) => !retained.sources.some((saved) => isDeepStrictEqual3(saved, source))) || [...roots].some((root2) => !retained.roots.includes(root2)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
18583
+ unresolved("retained review is missing current runtime discovery coverage");
18584
+ return retained;
18585
+ }
18535
18586
  if (options.reviewed && (options.reviewed.version !== 1 || !Array.isArray(options.reviewed.agents) || options.reviewed.agents.some((item) => !item || typeof item !== "object") || new Set(options.reviewed.agents.map((item) => item.agent)).size !== options.reviewed.agents.length))
18536
18587
  throw new Error("Invalid --discovery-inputs version or agents");
18537
18588
  const review = options.reviewed?.agents.find((item) => item.agent === agent);
@@ -18542,7 +18593,7 @@ function resolveAgentDiscovery(options) {
18542
18593
  if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
18543
18594
  throw new Error("Explicit discovery reviews require full source-file hashes");
18544
18595
  verifyAgentDiscovery(supplied);
18545
- if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
18596
+ if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
18546
18597
  throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
18547
18598
  if (!review.sources.some((source) => source.path === canonical4(configPath)))
18548
18599
  throw new Error("Discovery review must include the agent configuration source");
@@ -39052,8 +39103,8 @@ var SESSION_RECONCILIATION_PLAN_TTL_MS = 5 * 60 * 1000;
39052
39103
  // src/lib/claude-skills-coordination.ts
39053
39104
  function coordinateClaudeSettings(before, replacement, options) {
39054
39105
  const prior = JSON.parse(before ?? "{}"), next = JSON.parse(replacement);
39055
- const events = new Set([...Object.keys(prior.hooks ?? {}), ...Object.keys(next.hooks ?? {})]);
39056
- const changed = [...events].filter((event) => JSON.stringify(prior.hooks?.[event]) !== JSON.stringify(next.hooks?.[event]));
39106
+ const events2 = new Set([...Object.keys(prior.hooks ?? {}), ...Object.keys(next.hooks ?? {})]);
39107
+ const changed = [...events2].filter((event) => JSON.stringify(prior.hooks?.[event]) !== JSON.stringify(next.hooks?.[event]));
39057
39108
  const selected = changed.length ? changed : ["PreToolUse"];
39058
39109
  const plan = planClaudeHookEventsUpdate({
39059
39110
  ...options,
@@ -39139,9 +39190,9 @@ function getTargetSettingsDir(target) {
39139
39190
  }
39140
39191
  function getGlobalSettingsPathOverride(target) {
39141
39192
  if (target === "codex")
39142
- return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH;
39193
+ return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH ?? (process.env.CODEX_HOME ? resolve7(process.env.CODEX_HOME, "hooks.json") : undefined);
39143
39194
  if (target === "claude")
39144
- return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
39195
+ return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH ?? (process.env.CLAUDE_CONFIG_DIR ? resolve7(process.env.CLAUDE_CONFIG_DIR, "settings.json") : undefined);
39145
39196
  if (target === "gemini")
39146
39197
  return process.env.HASNA_HOOKS_GEMINI_SETTINGS_PATH;
39147
39198
  return;
@@ -39185,7 +39236,7 @@ function readSettings2(scope = "global", target = "claude") {
39185
39236
  return value;
39186
39237
  }
39187
39238
  } catch (error61) {
39188
- console.warn(`[hooks] Failed to read settings at ${path}: ${error61 instanceof Error ? error61.message : String(error61)}`);
39239
+ console.warn("[hooks] Cannot read hook settings; repair the selected file before installation.");
39189
39240
  }
39190
39241
  const empty = {};
39191
39242
  if (!existsSync10(path))
@@ -39197,10 +39248,16 @@ function settingsIdentity(path) {
39197
39248
  return realpathSync5(path);
39198
39249
  return existsSync10(dirname7(path)) ? join10(realpathSync5(dirname7(path)), basename3(path)) : resolve7(path);
39199
39250
  }
39200
- function writeSettings(settings, scope = "global", target = "claude") {
39251
+ function writeSettings(settings, scope = "global", target = "claude", expectedSettingsSha256) {
39201
39252
  const path = getSettingsPath(scope, target);
39202
39253
  if (target === "codex")
39203
- return writeCodexSettings(path, settings);
39254
+ return writeCodexSettings(path, settings, expectedSettingsSha256);
39255
+ const captured = settingsPreimages.get(settings);
39256
+ if (expectedSettingsSha256 !== undefined) {
39257
+ if (!captured || captured.path !== path)
39258
+ throw new Error("Hook settings require a valid captured preimage.");
39259
+ assertSettingsPreimage(captured.text, expectedSettingsSha256);
39260
+ }
39204
39261
  if (target === "claude" && settingsIdentity(path) === settingsIdentity(join10(homedir5(), ".claude/settings.json"))) {
39205
39262
  const before = settingsPreimages.get(settings);
39206
39263
  if (!before || before.path !== path)
@@ -39212,6 +39269,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
39212
39269
  `, { home: homedir5() }))
39213
39270
  return;
39214
39271
  }
39272
+ if (expectedSettingsSha256 !== undefined)
39273
+ return writeHookSettingsPreimage(path, settings, captured.text);
39215
39274
  const dir = dirname7(path);
39216
39275
  if (!existsSync10(dir)) {
39217
39276
  mkdirSync6(dir, { recursive: true });
@@ -39333,7 +39392,7 @@ function detectConflict(name, scope, target) {
39333
39392
  const meta3 = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
39334
39393
  if (!meta3 || !meta3.matcher)
39335
39394
  return;
39336
- const events = new Set(getHookEvents(meta3));
39395
+ const events2 = new Set(getHookEvents(meta3));
39337
39396
  const registered = getRegisteredHooksForTarget(scope, target);
39338
39397
  for (const existingName of registered) {
39339
39398
  if (existingName === name)
@@ -39341,7 +39400,7 @@ function detectConflict(name, scope, target) {
39341
39400
  const existing = getHook(existingName);
39342
39401
  if (!existing || !existing.matcher)
39343
39402
  continue;
39344
- if (!getHookEvents(existing).some((event) => events.has(event)))
39403
+ if (!getHookEvents(existing).some((event) => events2.has(event)))
39345
39404
  continue;
39346
39405
  if (matchersOverlap(meta3.matcher.toLowerCase(), existing.matcher.toLowerCase())) {
39347
39406
  return `conflicts with '${existingName}' (same event ${meta3.event}, overlapping matcher '${existing.matcher}')`;
@@ -39369,7 +39428,7 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
39369
39428
  }
39370
39429
  return;
39371
39430
  }
39372
- function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
39431
+ function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos, expectedSettingsSha256) {
39373
39432
  const shortName = shortHookName(name);
39374
39433
  if (isNativeSafetyName(shortName) && (profile || target === "gemini" || target === "codewith" && codewithMode === "write")) {
39375
39434
  return { hook: shortName, success: false, error: "Bundled native safety guards support Claude/Codex and managed Codewith fragments, without registry profiles.", target };
@@ -39436,7 +39495,7 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
39436
39495
  if (rewriteConflict) {
39437
39496
  return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
39438
39497
  }
39439
- registerHook(shortName, scope, target, profile, mementos);
39498
+ registerHook(shortName, scope, target, profile, mementos, (settings, selectedScope, selectedTarget) => writeSettings(settings, selectedScope, selectedTarget, expectedSettingsSha256));
39440
39499
  return {
39441
39500
  hook: shortName,
39442
39501
  success: true,
@@ -39455,7 +39514,10 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
39455
39514
  }
39456
39515
  }
39457
39516
  function installHook(name, options = {}) {
39458
- const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos } = options;
39517
+ const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos, expectedSettingsSha256 } = options;
39518
+ if (expectedSettingsSha256 !== undefined && (!/^(?:absent|[a-f0-9]{64})$/.test(expectedSettingsSha256) || !isNativeSafetyName(shortHookName(name)) || target !== "codex" && target !== "claude")) {
39519
+ return { hook: name, success: false, target, error: "An exact settings predecessor requires a Claude or Codex native safety guard." };
39520
+ }
39459
39521
  try {
39460
39522
  validateMementosRegistration(name, mementos);
39461
39523
  } catch (error61) {
@@ -39496,9 +39558,26 @@ function installHook(name, options = {}) {
39496
39558
  }
39497
39559
  return { ...claudeResult, target: "all" };
39498
39560
  }
39499
- return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
39561
+ return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos, expectedSettingsSha256);
39500
39562
  }
39501
- function registerHook(name, scope = "global", target = "claude", profile, mementos) {
39563
+ function previewNativeSafetyRegistration(target, scope = "global") {
39564
+ const conflict = detectRewriteConflict("trash-guard", scope, target);
39565
+ if (conflict)
39566
+ throw new Error("Conflicting input rewriter; reconcile it before installing the safety guard.");
39567
+ let result;
39568
+ registerHook("trash-guard", scope, target, undefined, undefined, (settings) => {
39569
+ const before = target === "codex" ? capturedCodexSettings(settings) : settingsPreimages.get(settings)?.text;
39570
+ if (before === undefined)
39571
+ throw new Error("Hook settings require a valid captured preimage.");
39572
+ const command = installedNativeSafetyCommand("trash-guard");
39573
+ result = { before, after: JSON.stringify(settings, null, 2) + `
39574
+ `, command };
39575
+ });
39576
+ if (!result)
39577
+ throw new Error("Native safety registration could not be rendered.");
39578
+ return result;
39579
+ }
39580
+ function registerHook(name, scope = "global", target = "claude", profile, mementos, write = writeSettings) {
39502
39581
  const meta3 = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
39503
39582
  if (!meta3)
39504
39583
  return;
@@ -39527,7 +39606,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
39527
39606
  }
39528
39607
  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
39608
  const hookCommand = isNativeSafetyName(name) ? installedNativeSafetyCommand(name) : (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
39530
- if (target === "codex") {
39609
+ if (target === "codex" || target === "claude" && isNativeSafetyName(name)) {
39531
39610
  const positions = [];
39532
39611
  for (const [event, groups] of Object.entries(settings.hooks)) {
39533
39612
  for (const entry of groups) {
@@ -39553,7 +39632,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
39553
39632
  if (typeof meta3.timeoutSeconds === "number" && meta3.timeoutSeconds > 0)
39554
39633
  entry.hooks[index].timeout = meta3.timeoutSeconds;
39555
39634
  }
39556
- writeSettings(settings, scope, target);
39635
+ write(settings, scope, target);
39557
39636
  return;
39558
39637
  }
39559
39638
  }
@@ -39573,7 +39652,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
39573
39652
  }
39574
39653
  settings.hooks[eventKey].push(entry);
39575
39654
  }
39576
- writeSettings(settings, scope, target);
39655
+ write(settings, scope, target);
39577
39656
  }
39578
39657
  function removeHookFromAllEvents(settings, name) {
39579
39658
  if (!settings.hooks)
@@ -39807,7 +39886,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
39807
39886
  // src/index.ts
39808
39887
  init_profiles();
39809
39888
  init_config();
39810
- import { existsSync as existsSync14, readFileSync as readFileSync19 } from "fs";
39889
+ import { existsSync as existsSync15, readFileSync as readFileSync26 } from "fs";
39811
39890
 
39812
39891
  // src/lib/transport.ts
39813
39892
  init_transport();
@@ -40046,16 +40125,16 @@ async function installManifestUrl(url2) {
40046
40125
  return { name, version: manifest.version, kind: "url", source: url2, dir, scriptPath };
40047
40126
  }
40048
40127
  // src/lib/sync.ts
40049
- import { createHash as createHash7 } from "crypto";
40128
+ import { createHash as createHash6 } from "crypto";
40050
40129
  import { readFileSync as readFileSync15 } from "fs";
40051
40130
  init_db();
40052
40131
  init_store();
40053
40132
  init_manifest();
40054
40133
  function sha256OfText(text3) {
40055
- return createHash7("sha256").update(text3).digest("hex");
40134
+ return createHash6("sha256").update(text3).digest("hex");
40056
40135
  }
40057
40136
  function sha256OfPath(path) {
40058
- return createHash7("sha256").update(readFileSync15(path)).digest("hex");
40137
+ return createHash6("sha256").update(readFileSync15(path)).digest("hex");
40059
40138
  }
40060
40139
  function collectBundledCatalog() {
40061
40140
  const out = [];
@@ -40977,6 +41056,933 @@ async function provisionCloudflareResources(options) {
40977
41056
  ];
40978
41057
  return result;
40979
41058
  }
41059
+ // src/lib/codex-safety-trust.ts
41060
+ import { createHash as createHash12, randomUUID as randomUUID3 } from "crypto";
41061
+ 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";
41062
+ import { dirname as dirname21, isAbsolute as isAbsolute18, join as join24, relative as relative2 } from "path";
41063
+ import { homedir as homedir7 } from "os";
41064
+
41065
+ // src/lib/codex-safety-check.ts
41066
+ import { spawn, execFile as execFile2 } from "child_process";
41067
+ import { createHash as createHash7 } from "crypto";
41068
+ import { constants as constants4, openSync as openSync6, closeSync as closeSync6, fstatSync as fstatSync5, lstatSync as lstatSync10, readFileSync as readFileSync20, realpathSync as realpathSync9 } from "fs";
41069
+ import { dirname as dirname20, isAbsolute as isAbsolute17, resolve as resolve17 } from "path";
41070
+ import { promisify } from "util";
41071
+
41072
+ // src/native-safety.ts
41073
+ import { execFile } from "child_process";
41074
+ 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";
41075
+ import { homedir as homedir6 } from "os";
41076
+ import { dirname as dirname19, isAbsolute as isAbsolute15, join as join20, resolve as resolve15 } from "path";
41077
+ class NativeSafetyError extends Error {
41078
+ code;
41079
+ reason;
41080
+ constructor(code2, reason) {
41081
+ super(`Native deletion safety refused (${code2}). Run the reviewed Hooks safety setup before using shell or patch tools.`);
41082
+ this.code = code2;
41083
+ this.reason = reason;
41084
+ this.name = "NativeSafetyError";
41085
+ }
41086
+ }
41087
+ var executionSchema = "hasna.hooks.execution.v1";
41088
+ var owner2 = (uid2) => uid2 === 0 || uid2 === process.getuid?.();
41089
+ function safeDirectory(path) {
41090
+ for (let cursor = path;; cursor = dirname19(cursor)) {
41091
+ const stat2 = lstatSync9(cursor);
41092
+ if (!stat2.isDirectory() || !owner2(stat2.uid) || stat2.mode & 18)
41093
+ throw new NativeSafetyError("binding_invalid");
41094
+ if (dirname19(cursor) === cursor)
41095
+ return;
41096
+ }
41097
+ }
41098
+ function canonicalHome(home) {
41099
+ if (!isAbsolute15(home) || home.includes("\x00") || realpathSync8(home) !== resolve15(home))
41100
+ throw new NativeSafetyError("binding_invalid");
41101
+ safeDirectory(home);
41102
+ return resolve15(home);
41103
+ }
41104
+ function parseRecord(text3, home) {
41105
+ const record3 = JSON.parse(text3);
41106
+ 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")
41107
+ throw new NativeSafetyError("binding_invalid");
41108
+ const binding = nativeSafetyCommandBinding(record3.command);
41109
+ if (binding?.name !== "trash-guard" || binding.home !== home)
41110
+ throw new NativeSafetyError("binding_invalid");
41111
+ return record3;
41112
+ }
41113
+ async function evaluateRecord(event, record3, signal, executionAgent) {
41114
+ 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))
41115
+ throw new NativeSafetyError("input_invalid");
41116
+ const input2 = JSON.stringify(executionAgent === undefined ? event : { ...event, hasna_execution: { schema: executionSchema, agent: executionAgent } });
41117
+ if (Buffer.byteLength(input2) > 1048576)
41118
+ throw new NativeSafetyError("input_invalid");
41119
+ const output2 = await new Promise((accept, reject) => {
41120
+ const child = execFile("/bin/sh", ["-c", record3.command], { cwd: "/", env: {}, encoding: "utf8", timeout: 8000, maxBuffer: 1048576, signal }, (error61, stdout, stderr) => {
41121
+ if (error61 || stderr) {
41122
+ reject(new NativeSafetyError("guard_failed"));
41123
+ return;
41124
+ }
41125
+ accept(stdout);
41126
+ });
41127
+ child.stdin?.on("error", () => {});
41128
+ child.stdin?.end(input2);
41129
+ });
41130
+ if (!output2)
41131
+ return { decision: "continue" };
41132
+ try {
41133
+ const value = JSON.parse(output2);
41134
+ if (Object.keys(value).join() !== "hookSpecificOutput")
41135
+ throw new Error;
41136
+ const verdict = value.hookSpecificOutput;
41137
+ if (verdict.hookEventName !== "PreToolUse")
41138
+ throw new Error;
41139
+ if (verdict.permissionDecision === "deny" && Object.keys(verdict).sort().join() === "hookEventName,permissionDecision,permissionDecisionReason" && typeof verdict.permissionDecisionReason === "string" && verdict.permissionDecisionReason.trim())
41140
+ return { decision: "deny", reason: verdict.permissionDecisionReason };
41141
+ 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))
41142
+ return { decision: "rewrite", command: verdict.updatedInput.command };
41143
+ } catch {}
41144
+ throw new NativeSafetyError("guard_failed");
41145
+ }
41146
+ async function verifyNativeSafetyCommand(command, home = homedir6()) {
41147
+ home = canonicalHome(home);
41148
+ const record3 = parseRecord(JSON.stringify({ schema: "hasna.hooks.native-safety.v1", harness: "sumi", command }), home);
41149
+ const verdict = await evaluateRecord({ hook_event_name: "PreToolUse", tool_name: "Bash", cwd: home, tool_input: { command: "rm -- .hasna-trash-verification-only" } }, record3);
41150
+ if (verdict.decision !== "rewrite")
41151
+ throw new NativeSafetyError("guard_failed", verdict.decision === "deny" ? verdict.reason.slice(0, 512) : undefined);
41152
+ }
41153
+
41154
+ // src/lib/codex-safety-check.ts
41155
+ 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"]);
41156
+ var matcher = "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$";
41157
+
41158
+ class CodexSafetyError extends Error {
41159
+ code;
41160
+ constructor(code2) {
41161
+ super(`Codex deletion safety check failed (${code2}).`);
41162
+ this.code = code2;
41163
+ this.name = "CodexSafetyError";
41164
+ }
41165
+ }
41166
+ function need5(condition, code2) {
41167
+ if (!condition)
41168
+ throw new CodexSafetyError(code2);
41169
+ }
41170
+ var digest3 = (bytes) => createHash7("sha256").update(bytes).digest("hex");
41171
+ function nativeHeader(bytes) {
41172
+ if (bytes.length < 64 || !["arm64", "x64"].includes(process.arch))
41173
+ return false;
41174
+ if (process.platform === "darwin") {
41175
+ const cpu = process.arch === "arm64" ? 16777228 : 16777223;
41176
+ 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;
41177
+ }
41178
+ if (process.platform === "linux") {
41179
+ const offset = bytes.readBigUInt64LE(32), count = bytes.readUInt16LE(56);
41180
+ 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);
41181
+ }
41182
+ return false;
41183
+ }
41184
+ function executable(command) {
41185
+ const found = isAbsolute17(command) ? command : Bun.which(command);
41186
+ need5(found, "native_unavailable");
41187
+ try {
41188
+ const path = realpathSync9(found);
41189
+ for (let parent = dirname20(path);; parent = dirname20(parent)) {
41190
+ const stat2 = lstatSync10(parent);
41191
+ need5(stat2.isDirectory() && [0, process.getuid?.()].includes(stat2.uid) && !(stat2.mode & 18), "native_unsafe_executable");
41192
+ if (parent === dirname20(parent))
41193
+ break;
41194
+ }
41195
+ const fd = openSync6(path, constants4.O_RDONLY | constants4.O_NOFOLLOW | constants4.O_NONBLOCK);
41196
+ try {
41197
+ const stat2 = fstatSync5(fd);
41198
+ need5(stat2.isFile() && [0, process.getuid?.()].includes(stat2.uid) && !(stat2.mode & 18) && !!(stat2.mode & 64) && stat2.size <= 256000000, "native_unsafe_executable");
41199
+ const bytes = readFileSync20(fd);
41200
+ need5(bytes.length <= 256000000, "native_unavailable");
41201
+ need5(nativeHeader(bytes), "native_executable_required");
41202
+ return { path, sha256: digest3(bytes) };
41203
+ } finally {
41204
+ closeSync6(fd);
41205
+ }
41206
+ } catch (error61) {
41207
+ if (error61 instanceof CodexSafetyError)
41208
+ throw error61;
41209
+ throw new CodexSafetyError("native_unavailable");
41210
+ }
41211
+ }
41212
+ async function connectCodexSafety(options) {
41213
+ const command = options.command ?? "codex", before = executable(command);
41214
+ const env = { ...process.env, HOME: options.home, NO_COLOR: "1" };
41215
+ for (const key of ["NODE_OPTIONS", "NODE_PATH", "BUN_OPTIONS", "BASH_ENV", "ENV", "DYLD_INSERT_LIBRARIES", "LD_PRELOAD"])
41216
+ delete env[key];
41217
+ if (options.codexHome === undefined)
41218
+ delete env.CODEX_HOME;
41219
+ else
41220
+ env.CODEX_HOME = options.codexHome;
41221
+ let version2;
41222
+ try {
41223
+ version2 = (await promisify(execFile2)(before.path, ["--version"], { env, cwd: options.home, timeout: 5000, killSignal: "SIGKILL", maxBuffer: 4096 })).stdout.trim();
41224
+ } catch {
41225
+ throw new CodexSafetyError("native_unavailable");
41226
+ }
41227
+ need5(versions.has(version2), "native_unsupported");
41228
+ need5(JSON.stringify(executable(command)) === JSON.stringify(before), "guard_changed");
41229
+ const child = spawn(before.path, ["app-server", "--strict-config", "--stdio"], { env, cwd: options.home, stdio: ["pipe", "pipe", "pipe"] });
41230
+ let stopped = false, buffer = Buffer.alloc(0), total = 0, nextId = 1;
41231
+ const pending = new Map;
41232
+ const fail = () => {
41233
+ stopped = true;
41234
+ for (const waiter of pending.values())
41235
+ waiter.reject(new CodexSafetyError("native_failed"));
41236
+ pending.clear();
41237
+ };
41238
+ const deadline = setTimeout(() => {
41239
+ fail();
41240
+ child.kill("SIGKILL");
41241
+ }, 15000);
41242
+ const exited = new Promise((done) => {
41243
+ child.once("exit", () => {
41244
+ fail();
41245
+ clearTimeout(deadline);
41246
+ done();
41247
+ });
41248
+ child.once("error", () => {
41249
+ fail();
41250
+ clearTimeout(deadline);
41251
+ done();
41252
+ });
41253
+ });
41254
+ child.stdin.on("error", fail);
41255
+ const account = (length) => {
41256
+ total += length;
41257
+ if (total > 8388608) {
41258
+ fail();
41259
+ child.kill("SIGKILL");
41260
+ return false;
41261
+ }
41262
+ return true;
41263
+ };
41264
+ child.stderr.on("data", (bytes) => {
41265
+ account(bytes.length);
41266
+ });
41267
+ child.stdout.on("data", (bytes) => {
41268
+ if (stopped || !account(bytes.length))
41269
+ return;
41270
+ buffer = Buffer.concat([buffer, bytes]);
41271
+ for (;; ) {
41272
+ const end = buffer.indexOf(10);
41273
+ if (end < 0)
41274
+ return;
41275
+ const line = buffer.subarray(0, end);
41276
+ buffer = buffer.subarray(end + 1);
41277
+ try {
41278
+ const value = JSON.parse(line.toString("utf8"));
41279
+ if (!value || typeof value !== "object" || Array.isArray(value))
41280
+ throw Error();
41281
+ if (!Object.hasOwn(value, "id"))
41282
+ continue;
41283
+ const waiter = pending.get(value.id);
41284
+ if (!waiter)
41285
+ throw Error();
41286
+ pending.delete(value.id);
41287
+ if (value.error || !Object.hasOwn(value, "result"))
41288
+ waiter.reject(new CodexSafetyError("native_failed"));
41289
+ else
41290
+ waiter.resolve(value.result);
41291
+ } catch {
41292
+ fail();
41293
+ child.kill("SIGKILL");
41294
+ return;
41295
+ }
41296
+ }
41297
+ });
41298
+ const request = (method, params) => new Promise((accept, reject) => {
41299
+ if (stopped) {
41300
+ reject(new CodexSafetyError("native_failed"));
41301
+ return;
41302
+ }
41303
+ const id = nextId++;
41304
+ pending.set(id, { resolve: accept, reject });
41305
+ child.stdin.write(JSON.stringify({ id, method, params }) + `
41306
+ `);
41307
+ });
41308
+ const rpc = {
41309
+ version: version2,
41310
+ request,
41311
+ async close() {
41312
+ fail();
41313
+ child.stdin.end();
41314
+ const force = setTimeout(() => child.kill("SIGKILL"), 1000);
41315
+ await exited;
41316
+ clearTimeout(force);
41317
+ clearTimeout(deadline);
41318
+ need5(JSON.stringify(executable(command)) === JSON.stringify(before), "guard_changed");
41319
+ }
41320
+ };
41321
+ try {
41322
+ await request("initialize", { clientInfo: { name: "hooks-safety-check", version: "1" }, capabilities: { experimentalApi: true } });
41323
+ child.stdin.write(`{"method":"initialized"}
41324
+ `);
41325
+ return rpc;
41326
+ } catch {
41327
+ await rpc.close();
41328
+ throw new CodexSafetyError("native_failed");
41329
+ }
41330
+ }
41331
+ function supportedDiscoveryWarnings(hooks, warnings) {
41332
+ if (warnings.length === 0)
41333
+ return true;
41334
+ if (warnings.length > 128)
41335
+ return false;
41336
+ const allowed = new Set;
41337
+ const sources = new Set(hooks.map((hook) => JSON.stringify([hook?.source, hook?.sourcePath])));
41338
+ for (const hook of hooks) {
41339
+ const path = hook?.sourcePath;
41340
+ if (typeof path !== "string" || path.length > 4096 || !isAbsolute17(path) || path !== resolve17(dirname20(path), "hooks.json") || !["user", "project"].includes(hook.source))
41341
+ continue;
41342
+ const toml = resolve17(dirname20(path), "config.toml");
41343
+ if (sources.has(JSON.stringify([hook.source, toml]))) {
41344
+ allowed.add(`loading hooks from both ${path} and ${toml}; prefer a single representation for this layer`);
41345
+ }
41346
+ }
41347
+ return warnings.every((warning) => typeof warning === "string" && allowed.delete(warning));
41348
+ }
41349
+ function selectCodexSafety(discovery, configuration, home, cwd, featureEnabled, requireReady = true) {
41350
+ const data = discovery?.data;
41351
+ 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");
41352
+ const config2 = configuration?.config;
41353
+ need5(config2 && typeof config2 === "object" && !Array.isArray(config2), "native_discovery_failed");
41354
+ need5((config2.features?.hooks ?? featureEnabled) === true, "hooks_disabled");
41355
+ for (const hook2 of data[0].hooks)
41356
+ if (typeof hook2?.command === "string") {
41357
+ if (/\bhasna-native-safety-v\d+\b/.test(hook2.command))
41358
+ need5(nativeSafetyCommandBinding(hook2.command), "guard_definition_invalid");
41359
+ need5(!/^(?:hooks run trash-guard|hook-trash-guard)(?:\s|$)/.test(hook2.command.trim()), "guard_definition_invalid");
41360
+ }
41361
+ const selected = data[0].hooks.filter((hook2) => typeof hook2?.command === "string" && nativeSafetyCommandBinding(hook2.command)?.name === "trash-guard");
41362
+ need5(selected.length !== 0, "guard_missing");
41363
+ need5(selected.length === 1, "guard_ambiguous");
41364
+ const hook = selected[0], binding = nativeSafetyCommandBinding(hook.command);
41365
+ 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");
41366
+ need5(typeof hook.enabled === "boolean" && ["trusted", "untrusted", "modified"].includes(hook.trustStatus), "guard_definition_invalid");
41367
+ if (requireReady) {
41368
+ need5(hook.enabled === true, "guard_disabled");
41369
+ need5(hook.trustStatus === "trusted", "guard_untrusted");
41370
+ }
41371
+ return { key: hook.key, command: hook.command, currentHash: hook.currentHash, sourcePath: hook.sourcePath, enabled: hook.enabled, trustStatus: hook.trustStatus };
41372
+ }
41373
+ async function readCodexSafety(rpc, home, cwd, options = {}) {
41374
+ need5(versions.has(rpc.version), "native_unsupported");
41375
+ const hooks = await rpc.request("hooks/list", { cwds: [cwd] });
41376
+ const configuration = await rpc.request("config/read", { cwd, includeLayers: options.includeLayers ?? false });
41377
+ let enabled;
41378
+ if (configuration?.config?.features?.hooks === undefined) {
41379
+ let cursor = null, count = 0;
41380
+ const seen = new Set;
41381
+ do {
41382
+ const page3 = await rpc.request("experimentalFeature/list", { cursor, limit: 100 });
41383
+ need5(Array.isArray(page3?.data) && page3.data.length <= 100 && (page3.nextCursor === null || typeof page3.nextCursor === "string"), "native_discovery_failed");
41384
+ count += page3.data.length;
41385
+ need5(count <= 1000, "native_discovery_failed");
41386
+ for (const feature of page3.data)
41387
+ if (feature?.name === "hooks") {
41388
+ need5(enabled === undefined && typeof feature.enabled === "boolean", "native_discovery_failed");
41389
+ enabled = feature.enabled;
41390
+ }
41391
+ cursor = page3.nextCursor;
41392
+ if (cursor !== null) {
41393
+ need5(cursor.length > 0 && cursor.length <= 4096 && !seen.has(cursor) && page3.data.length > 0, "native_discovery_failed");
41394
+ seen.add(cursor);
41395
+ }
41396
+ } while (cursor !== null);
41397
+ }
41398
+ return { guard: selectCodexSafety(hooks, configuration, home, cwd, enabled, options.requireReady ?? true), discovery: hooks, configuration };
41399
+ }
41400
+
41401
+ // src/lib/codex-safety-trust.ts
41402
+ class CodexTrustError extends Error {
41403
+ code;
41404
+ operationId;
41405
+ constructor(code2, operationId) {
41406
+ super("Codex safety trust failed (" + code2 + ").");
41407
+ this.code = code2;
41408
+ this.operationId = operationId;
41409
+ this.name = "CodexTrustError";
41410
+ }
41411
+ }
41412
+ function need7(value, code2 = "native_changed") {
41413
+ if (!value)
41414
+ throw new CodexTrustError(code2);
41415
+ }
41416
+ var sha4 = (value) => createHash12("sha256").update(value).digest("hex");
41417
+ function canonical4(value) {
41418
+ if (Array.isArray(value))
41419
+ return "[" + value.map(canonical4).join(",") + "]";
41420
+ if (value !== null && typeof value === "object")
41421
+ return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + canonical4(value[key])).join(",") + "}";
41422
+ return JSON.stringify(value);
41423
+ }
41424
+ function mergedConfiguration(value) {
41425
+ const copy = structuredClone(value);
41426
+ if (copy?.hooks && typeof copy.hooks === "object" && !Array.isArray(copy.hooks)) {
41427
+ for (const event of [
41428
+ "PreToolUse",
41429
+ "PermissionRequest",
41430
+ "PostToolUse",
41431
+ "PreCompact",
41432
+ "PostCompact",
41433
+ "SessionStart",
41434
+ "SessionEnd",
41435
+ "UserPromptSubmit",
41436
+ "SubagentStart",
41437
+ "SubagentStop",
41438
+ "Stop",
41439
+ "Interrupt"
41440
+ ]) {
41441
+ if (!Object.hasOwn(copy.hooks, event))
41442
+ copy.hooks[event] = [];
41443
+ }
41444
+ }
41445
+ return canonical4(copy);
41446
+ }
41447
+ function safeDirectory2(path) {
41448
+ for (let p = path;; p = dirname21(p)) {
41449
+ const s = lstatSync15(p);
41450
+ need7(s.isDirectory() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18), "preimage_unsafe");
41451
+ if (dirname21(p) === p)
41452
+ return;
41453
+ }
41454
+ }
41455
+ function readPrivate(path) {
41456
+ safeDirectory2(dirname21(path));
41457
+ const fd = openSync7(path, constants12.O_RDONLY | constants12.O_NOFOLLOW | constants12.O_NONBLOCK);
41458
+ try {
41459
+ const s = fstatSync6(fd);
41460
+ need7(s.isFile() && s.uid === process.getuid?.() && !(s.mode & 63) && s.size <= 4194304, "preimage_unsafe");
41461
+ const bytes = readFileSync21(fd);
41462
+ need7(bytes.length <= 4194304, "preimage_unsafe");
41463
+ return bytes;
41464
+ } finally {
41465
+ closeSync7(fd);
41466
+ }
41467
+ }
41468
+ function syncDirectory(path) {
41469
+ const fd = openSync7(path, constants12.O_RDONLY | constants12.O_DIRECTORY | constants12.O_NOFOLLOW);
41470
+ try {
41471
+ fsyncSync5(fd);
41472
+ } finally {
41473
+ closeSync7(fd);
41474
+ }
41475
+ }
41476
+ function exclusive(path, bytes) {
41477
+ const fd = openSync7(path, constants12.O_CREAT | constants12.O_EXCL | constants12.O_WRONLY | constants12.O_NOFOLLOW, 384);
41478
+ try {
41479
+ writeFileSync11(fd, bytes);
41480
+ fsyncSync5(fd);
41481
+ } finally {
41482
+ closeSync7(fd);
41483
+ }
41484
+ syncDirectory(dirname21(path));
41485
+ }
41486
+ async function inspect(rpc, home, cwd) {
41487
+ const observed = await readCodexSafety(rpc, home, cwd, { includeLayers: true, requireReady: false });
41488
+ const { guard, configuration, discovery } = observed;
41489
+ const layers = configuration?.layers;
41490
+ need7(Array.isArray(layers) && layers.length <= 1000, "native_config_invalid");
41491
+ const userLayers = layers.filter((layer2) => layer2?.name?.type === "user" && layer2.name.profile === null);
41492
+ need7(userLayers.length === 1, "native_config_invalid");
41493
+ const layer = userLayers[0], path = layer.name.file;
41494
+ need7(typeof path === "string" && isAbsolute18(path) && typeof layer.version === "string" && /^sha256:[a-f0-9]{64}$/.test(layer.version), "native_config_invalid");
41495
+ const canonicalPath = realpathSync10(path), inside = relative2(home, canonicalPath);
41496
+ need7(inside !== "" && !inside.startsWith("..") && !isAbsolute18(inside), "native_config_invalid");
41497
+ const definition = discovery.data[0].hooks.find((h) => h.key === guard.key);
41498
+ need7(definition.source === "user", "native_config_invalid");
41499
+ const before = readPrivate(canonicalPath);
41500
+ const rawState = layer.config?.hooks?.state?.[guard.key];
41501
+ need7(rawState === undefined || rawState && typeof rawState === "object" && !Array.isArray(rawState), "native_config_invalid");
41502
+ const desired = { ...rawState, enabled: true, trusted_hash: guard.currentHash };
41503
+ const planBase = {
41504
+ schema: "hasna.hooks.codex-safety-trust/v1",
41505
+ nativeVersion: rpc.version,
41506
+ cwd,
41507
+ key: guard.key,
41508
+ currentHash: guard.currentHash,
41509
+ configPath: path,
41510
+ configVersion: layer.version,
41511
+ action: guard.enabled && guard.trustStatus === "trusted" ? "unchanged" : "trust_and_enable",
41512
+ guardVerified: true,
41513
+ nativeAdoptionVerified: false
41514
+ };
41515
+ const plan = { ...planBase, planDigest: sha4(canonical4({ planBase, configuration, discovery, raw: sha4(before) })) };
41516
+ return { ...observed, layer, layers, canonicalPath, before, desired, plan };
41517
+ }
41518
+ async function context2(options, dependencies) {
41519
+ const home = realpathSync10(options.home ?? homedir7()), cwd = realpathSync10(options.cwd ?? process.cwd());
41520
+ need7(isAbsolute18(home) && isAbsolute18(cwd), "input_invalid");
41521
+ return { home, cwd, rpc: await (dependencies.connect ?? connectCodexSafety)({ ...options, home }) };
41522
+ }
41523
+ async function planCodexSafetyTrust(options = {}, dependencies = {}) {
41524
+ const { home, cwd, rpc } = await context2(options, dependencies);
41525
+ try {
41526
+ const before = await inspect(rpc, home, cwd);
41527
+ await (dependencies.verify ?? verifyNativeSafetyCommand)(before.guard.command, home);
41528
+ need7((await inspect(rpc, home, cwd)).plan.planDigest === before.plan.planDigest);
41529
+ return before.plan;
41530
+ } finally {
41531
+ await rpc.close();
41532
+ }
41533
+ }
41534
+ async function applyCodexSafetyTrust(options, dependencies = {}) {
41535
+ need7(/^[a-f0-9]{64}$/.test(options.expectedPlanDigest), "input_invalid");
41536
+ const selected = { ...options }, { home, cwd, rpc } = await context2(selected, dependencies);
41537
+ let operationId;
41538
+ try {
41539
+ const before = await inspect(rpc, home, cwd);
41540
+ need7(before.plan.planDigest === selected.expectedPlanDigest);
41541
+ await (dependencies.verify ?? verifyNativeSafetyCommand)(before.guard.command, home);
41542
+ need7((await inspect(rpc, home, cwd)).plan.planDigest === before.plan.planDigest);
41543
+ if (before.plan.action === "unchanged")
41544
+ return { ok: true, changed: false, ...before.plan };
41545
+ const expectedConfiguration = structuredClone(before.configuration);
41546
+ const expectedLayer = expectedConfiguration.layers.find((layer) => layer.name?.type === "user" && layer.name.profile === null);
41547
+ for (const config2 of [expectedConfiguration.config, expectedLayer.config]) {
41548
+ config2.hooks ??= {};
41549
+ config2.hooks.state ??= {};
41550
+ config2.hooks.state[before.guard.key] = before.desired;
41551
+ }
41552
+ const expectedDiscovery = structuredClone(before.discovery);
41553
+ const expectedGuard = expectedDiscovery.data[0].hooks.find((h) => h.key === before.guard.key);
41554
+ expectedGuard.enabled = true;
41555
+ expectedGuard.trustStatus = "trusted";
41556
+ operationId = randomUUID3();
41557
+ let directory2 = home;
41558
+ for (const part of [".hasna", "hooks", "native", "codex-trash-trust", operationId]) {
41559
+ directory2 = join24(directory2, part);
41560
+ try {
41561
+ mkdirSync13(directory2, { mode: 448 });
41562
+ syncDirectory(dirname21(directory2));
41563
+ } catch (error61) {
41564
+ if (error61.code !== "EEXIST")
41565
+ throw error61;
41566
+ }
41567
+ safeDirectory2(directory2);
41568
+ }
41569
+ exclusive(join24(directory2, "config.before"), before.before);
41570
+ exclusive(join24(directory2, "intent.json"), JSON.stringify({ operationId, plan: before.plan, configSHA256: sha4(before.before) }) + `
41571
+ `);
41572
+ need7(sha4(readPrivate(before.canonicalPath)) === sha4(before.before));
41573
+ const result = await rpc.request("config/batchWrite", {
41574
+ filePath: before.plan.configPath,
41575
+ expectedVersion: before.plan.configVersion,
41576
+ reloadUserConfig: true,
41577
+ edits: [{ keyPath: "hooks.state." + JSON.stringify(before.guard.key), value: before.desired, mergeStrategy: "replace" }]
41578
+ });
41579
+ need7(result?.status === "ok" && result.filePath === before.plan.configPath && result.overriddenMetadata === null && /^sha256:[a-f0-9]{64}$/.test(result.version));
41580
+ const after = await inspect(rpc, home, cwd);
41581
+ expectedLayer.version = result.version;
41582
+ 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));
41583
+ const receipt = {
41584
+ ok: true,
41585
+ changed: true,
41586
+ operationId,
41587
+ planDigest: before.plan.planDigest,
41588
+ configVersion: result.version,
41589
+ guardVerified: true,
41590
+ nativeDiscoveryVerified: true,
41591
+ nativeAdoptionVerified: false,
41592
+ unrelatedConfigurationPreserved: true
41593
+ };
41594
+ exclusive(join24(directory2, "receipt.json"), JSON.stringify(receipt) + `
41595
+ `);
41596
+ return receipt;
41597
+ } catch (error61) {
41598
+ if (operationId)
41599
+ throw new CodexTrustError("trust_outcome_uncertain", operationId);
41600
+ throw error61;
41601
+ } finally {
41602
+ await rpc.close();
41603
+ }
41604
+ }
41605
+ // src/lib/claude-safety-check.ts
41606
+ import { constants as constants14, closeSync as closeSync15, fstatSync as fstatSync7, lstatSync as lstatSync21, openSync as openSync15, readFileSync as readFileSync23, realpathSync as realpathSync11 } from "fs";
41607
+ import { createHash as createHash17 } from "crypto";
41608
+ import { dirname as dirname22, isAbsolute as isAbsolute19, join as join27, resolve as resolve18 } from "path";
41609
+ import { homedir as homedir8 } from "os";
41610
+ class ClaudeSafetyError extends Error {
41611
+ code;
41612
+ constructor(code2) {
41613
+ super("Claude safety configuration check failed (" + code2 + ").");
41614
+ this.code = code2;
41615
+ this.name = "ClaudeSafetyError";
41616
+ }
41617
+ }
41618
+ function need8(value, code2) {
41619
+ if (!value)
41620
+ throw new ClaudeSafetyError(code2);
41621
+ }
41622
+ var sha6 = (value) => createHash17("sha256").update(value).digest("hex");
41623
+ function read3(path) {
41624
+ try {
41625
+ const resolved = realpathSync11(path);
41626
+ for (let parent = dirname22(resolved);; parent = dirname22(parent)) {
41627
+ const s = lstatSync21(parent);
41628
+ need8(s.isDirectory() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18), "settings_unsafe");
41629
+ if (dirname22(parent) === parent)
41630
+ break;
41631
+ }
41632
+ const fd = openSync15(resolved, constants14.O_RDONLY | constants14.O_NOFOLLOW | constants14.O_NONBLOCK);
41633
+ try {
41634
+ const s = fstatSync7(fd);
41635
+ need8(s.isFile() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18) && s.size <= 1048576, "settings_unsafe");
41636
+ const raw = readFileSync23(fd, "utf8");
41637
+ need8(Buffer.byteLength(raw) <= 1048576, "settings_unsafe");
41638
+ return { path, resolved, sha256: sha6(raw), raw };
41639
+ } finally {
41640
+ closeSync15(fd);
41641
+ }
41642
+ } catch (error61) {
41643
+ if (error61.code === "ENOENT")
41644
+ return { path, resolved: null, sha256: null, raw: null };
41645
+ if (error61 instanceof ClaudeSafetyError)
41646
+ throw error61;
41647
+ throw new ClaudeSafetyError("settings_unsafe");
41648
+ }
41649
+ }
41650
+ async function verifyClaudeSafetyConfiguration(options = {}, dependencies = {}) {
41651
+ const home = realpathSync11(options.home ?? homedir8()), cwd = realpathSync11(options.cwd ?? process.cwd());
41652
+ const paths = [
41653
+ options.userSettingsPath ?? join27(home, ".claude/settings.json"),
41654
+ join27(cwd, ".claude/settings.json"),
41655
+ options.localSettingsPath ?? join27(cwd, ".claude/settings.local.json")
41656
+ ];
41657
+ need8(paths.every((path) => isAbsolute19(path) && resolve18(path) === path), "settings_unsafe");
41658
+ const inspect2 = () => [...new Set(paths)].map(read3);
41659
+ const before = inspect2(), unique = [...new Map(before.filter((file2) => file2.resolved !== null).map((file2) => [file2.resolved, file2])).values()];
41660
+ const guards = [];
41661
+ for (const file2 of unique) {
41662
+ let settings;
41663
+ try {
41664
+ settings = JSON.parse(file2.raw);
41665
+ } catch {
41666
+ throw new ClaudeSafetyError("settings_invalid");
41667
+ }
41668
+ need8(settings && typeof settings === "object" && !Array.isArray(settings), "settings_invalid");
41669
+ need8(settings.disableAllHooks === undefined || settings.disableAllHooks === false, "hooks_disabled");
41670
+ if (settings.hooks === undefined)
41671
+ continue;
41672
+ need8(settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks), "settings_invalid");
41673
+ let count = 0;
41674
+ for (const [event, groups] of Object.entries(settings.hooks)) {
41675
+ need8(Array.isArray(groups), "settings_invalid");
41676
+ for (const group of groups) {
41677
+ need8(group && Array.isArray(group.hooks), "settings_invalid");
41678
+ for (const handler of group.hooks) {
41679
+ need8(++count <= 1e4 && handler && typeof handler === "object", "settings_invalid");
41680
+ if (typeof handler.command !== "string")
41681
+ continue;
41682
+ const binding = nativeSafetyCommandBinding(handler.command);
41683
+ if (/\bhasna-native-safety-v\d+\b/.test(handler.command))
41684
+ need8(binding, "guard_definition_invalid");
41685
+ need8(!/^(?:hooks run trash-guard|hook-trash-guard)(?:\s|$)/.test(handler.command.trim()), "guard_definition_invalid");
41686
+ if (binding?.name !== "trash-guard")
41687
+ continue;
41688
+ 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");
41689
+ guards.push({ command: handler.command, path: file2.path });
41690
+ }
41691
+ }
41692
+ }
41693
+ }
41694
+ need8(guards.length !== 0, "guard_missing");
41695
+ need8(guards.length === 1, "guard_ambiguous");
41696
+ try {
41697
+ await (dependencies.verify ?? verifyNativeSafetyCommand)(guards[0].command, home);
41698
+ } catch {
41699
+ throw new ClaudeSafetyError("guard_failed");
41700
+ }
41701
+ need8(JSON.stringify(inspect2()) === JSON.stringify(before), "settings_changed");
41702
+ return {
41703
+ ok: true,
41704
+ target: "claude",
41705
+ cwd,
41706
+ configurationVerified: true,
41707
+ guardVerified: true,
41708
+ sources: before.map(({ path, sha256 }) => ({ path, sha256 })),
41709
+ guardSHA256: sha6(guards[0].command),
41710
+ runtimePolicyVerified: false,
41711
+ nativeDiscoveryVerified: false,
41712
+ nativeAdoptionVerified: false
41713
+ };
41714
+ }
41715
+ // src/lib/native-safety-registration.ts
41716
+ import { createHash as createHash19, randomUUID as randomUUID5 } from "crypto";
41717
+ 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";
41718
+ import { basename as basename9, dirname as dirname23, isAbsolute as isAbsolute20, join as join29, relative as relative3, resolve as resolve19 } from "path";
41719
+ import { homedir as homedir9 } from "os";
41720
+ class NativeRegistrationError extends Error {
41721
+ code;
41722
+ operationId;
41723
+ constructor(code2, operationId) {
41724
+ super("Native safety registration failed (" + code2 + ").");
41725
+ this.code = code2;
41726
+ this.operationId = operationId;
41727
+ this.name = "NativeRegistrationError";
41728
+ }
41729
+ }
41730
+ var need9 = (value, code2) => {
41731
+ if (!value)
41732
+ throw new NativeRegistrationError(code2);
41733
+ };
41734
+ var sha7 = (value) => createHash19("sha256").update(value).digest("hex");
41735
+ function canonical6(value) {
41736
+ if (Array.isArray(value))
41737
+ return "[" + value.map(canonical6).join(",") + "]";
41738
+ if (value !== null && typeof value === "object")
41739
+ return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + canonical6(value[key])).join(",") + "}";
41740
+ return JSON.stringify(value);
41741
+ }
41742
+ function safeDirectory3(path) {
41743
+ for (let p = path;; p = dirname23(p)) {
41744
+ const s = lstatSync23(p);
41745
+ need9(s.isDirectory() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18), "settings_unsafe");
41746
+ if (dirname23(p) === p)
41747
+ return;
41748
+ }
41749
+ }
41750
+ function read5(path) {
41751
+ let resolved;
41752
+ try {
41753
+ resolved = realpathSync12(path);
41754
+ } catch (error61) {
41755
+ if (error61.code !== "ENOENT")
41756
+ throw new NativeRegistrationError("settings_unsafe");
41757
+ try {
41758
+ lstatSync23(path);
41759
+ throw new NativeRegistrationError("settings_unsafe");
41760
+ } catch (missing2) {
41761
+ if (missing2.code !== "ENOENT")
41762
+ throw missing2;
41763
+ }
41764
+ let ancestor = dirname23(path);
41765
+ const suffix = [basename9(path)];
41766
+ for (;; ) {
41767
+ try {
41768
+ resolved = join29(realpathSync12(ancestor), ...suffix);
41769
+ break;
41770
+ } catch (missing2) {
41771
+ if (missing2.code !== "ENOENT" || dirname23(ancestor) === ancestor || suffix.length >= 64)
41772
+ throw new NativeRegistrationError("settings_unsafe");
41773
+ try {
41774
+ lstatSync23(ancestor);
41775
+ throw new NativeRegistrationError("settings_unsafe");
41776
+ } catch (absent) {
41777
+ if (absent.code !== "ENOENT")
41778
+ throw absent;
41779
+ }
41780
+ suffix.unshift(basename9(ancestor));
41781
+ ancestor = dirname23(ancestor);
41782
+ }
41783
+ }
41784
+ safeDirectory3(realpathSync12(ancestor));
41785
+ return { resolved, raw: null, sha256: "absent" };
41786
+ }
41787
+ safeDirectory3(dirname23(resolved));
41788
+ let fd;
41789
+ try {
41790
+ fd = openSync17(resolved, constants15.O_RDONLY | constants15.O_NOFOLLOW | constants15.O_NONBLOCK);
41791
+ } catch (error61) {
41792
+ if (error61.code === "ENOENT")
41793
+ return { resolved, raw: null, sha256: "absent" };
41794
+ throw new NativeRegistrationError("settings_unsafe");
41795
+ }
41796
+ try {
41797
+ const s = fstatSync15(fd);
41798
+ need9(s.isFile() && s.uid === process.getuid?.() && !(s.mode & 18) && s.nlink === 1 && s.size <= 1048576, "settings_unsafe");
41799
+ const bytes = readFileSync25(fd);
41800
+ need9(bytes.length <= 1048576, "settings_unsafe");
41801
+ const raw = bytes.toString("utf8");
41802
+ need9(Buffer.from(raw).equals(bytes), "settings_invalid");
41803
+ return { resolved, raw, sha256: sha7(bytes) };
41804
+ } finally {
41805
+ closeSync17(fd);
41806
+ }
41807
+ }
41808
+ function assertLosslessJson(raw) {
41809
+ const tokens = /"(?:\\[\s\S]|[^"\\])*"|-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?|true|false|null|[{}\[\],:]/g;
41810
+ const stack = [];
41811
+ function decimal(text3) {
41812
+ const match = /^(-?)([0-9]+)(?:\.([0-9]+))?(?:[eE]([+-]?[0-9]+))?$/.exec(text3);
41813
+ need9((match[4]?.replace(/^[+-]/, "").length ?? 0) <= 6, "settings_invalid");
41814
+ let digits = (match[2] + (match[3] ?? "")).replace(/^0+/, "");
41815
+ if (!digits)
41816
+ return match[1] ? "-0" : "0";
41817
+ let exponent = Number(match[4] ?? "0") - (match[3]?.length ?? 0);
41818
+ while (digits.endsWith("0")) {
41819
+ digits = digits.slice(0, -1);
41820
+ exponent++;
41821
+ }
41822
+ return match[1] + digits + "e" + exponent;
41823
+ }
41824
+ for (const match of raw.matchAll(tokens)) {
41825
+ const token = match[0], current2 = stack.at(-1);
41826
+ if (token === "{")
41827
+ stack.push({ kind: "object", key: true, names: new Set });
41828
+ else if (token === "[")
41829
+ stack.push({ kind: "array", key: false, names: new Set });
41830
+ else if (token === "}" || token === "]")
41831
+ stack.pop();
41832
+ else if (token === "," && current2?.kind === "object")
41833
+ current2.key = true;
41834
+ else if (token.startsWith('"') && current2?.kind === "object" && current2.key) {
41835
+ const key = JSON.parse(token);
41836
+ need9(!current2.names.has(key), "settings_invalid");
41837
+ current2.names.add(key);
41838
+ current2.key = false;
41839
+ } else if (/^-?[0-9]/.test(token)) {
41840
+ const parsed = Number(token);
41841
+ need9(Number.isFinite(parsed) && decimal(token) === decimal(JSON.stringify(parsed)), "settings_invalid");
41842
+ }
41843
+ }
41844
+ }
41845
+ function validateSettings(raw) {
41846
+ let settings;
41847
+ try {
41848
+ settings = JSON.parse(raw ?? "{}");
41849
+ } catch {
41850
+ throw new NativeRegistrationError("settings_invalid");
41851
+ }
41852
+ need9(settings && typeof settings === "object" && !Array.isArray(settings), "settings_invalid");
41853
+ assertLosslessJson(raw ?? "{}");
41854
+ need9(settings.disableAllHooks === undefined || settings.disableAllHooks === false, "hooks_disabled");
41855
+ if (settings.hooks !== undefined) {
41856
+ need9(settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks), "settings_invalid");
41857
+ for (const groups of Object.values(settings.hooks)) {
41858
+ need9(Array.isArray(groups), "settings_invalid");
41859
+ for (const group of groups) {
41860
+ need9(group && Array.isArray(group.hooks), "settings_invalid");
41861
+ for (const hook of group.hooks) {
41862
+ need9(hook && typeof hook === "object", "settings_invalid");
41863
+ if (registrationName(hook.command) === "trash-guard") {
41864
+ need9((hook.async === undefined || hook.async === false) && hook.asyncRewake !== true, "registration_conflict");
41865
+ }
41866
+ }
41867
+ }
41868
+ }
41869
+ }
41870
+ return settings;
41871
+ }
41872
+ function inspect2(options) {
41873
+ need9(options?.target === "codex" || options?.target === "claude", "input_invalid");
41874
+ const home = realpathSync12(homedir9()), path = getSettingsPath("global", options.target);
41875
+ need9(isAbsolute20(path) && resolve19(path) === path, "input_invalid");
41876
+ const nativeHome = options.target === "codex" ? process.env.CODEX_HOME : process.env.CLAUDE_CONFIG_DIR;
41877
+ if (nativeHome !== undefined) {
41878
+ need9(nativeHome.length > 0 && isAbsolute20(nativeHome), "input_invalid");
41879
+ const selectedNative = read5(resolve19(nativeHome, options.target === "codex" ? "hooks.json" : "settings.json"));
41880
+ need9(read5(path).resolved === selectedNative.resolved, "input_invalid");
41881
+ }
41882
+ const before = read5(path), inside = relative3(home, before.resolved);
41883
+ need9(inside !== "" && !inside.startsWith("..") && !isAbsolute20(inside), "settings_unsafe");
41884
+ const parsed = validateSettings(before.raw);
41885
+ let preview2;
41886
+ try {
41887
+ preview2 = previewNativeSafetyRegistration(options.target);
41888
+ } catch {
41889
+ throw new NativeRegistrationError("registration_conflict");
41890
+ }
41891
+ need9(preview2.before === before.raw, "settings_changed");
41892
+ validateSettings(preview2.after);
41893
+ const changed = canonical6(parsed) !== canonical6(JSON.parse(preview2.after));
41894
+ const base = {
41895
+ schema: "hasna.hooks.native-safety-registration/v1",
41896
+ target: options.target,
41897
+ settingsPath: path,
41898
+ resolvedPath: before.resolved,
41899
+ beforeSHA256: before.sha256,
41900
+ desiredSHA256: changed ? sha7(preview2.after) : before.sha256,
41901
+ commandSHA256: sha7(preview2.command),
41902
+ action: changed ? "register" : "unchanged",
41903
+ guardVerified: true,
41904
+ nativeAdoptionVerified: false
41905
+ };
41906
+ const plan = { ...base, planDigest: sha7(canonical6(base)) };
41907
+ need9(canonical6(read5(path)) === canonical6(before), "settings_changed");
41908
+ return { home, before, preview: preview2, plan };
41909
+ }
41910
+ function syncDirectory2(path) {
41911
+ const fd = openSync17(path, constants15.O_RDONLY | constants15.O_DIRECTORY | constants15.O_NOFOLLOW);
41912
+ try {
41913
+ fsyncSync7(fd);
41914
+ } finally {
41915
+ closeSync17(fd);
41916
+ }
41917
+ }
41918
+ function exclusive2(path, bytes) {
41919
+ const fd = openSync17(path, constants15.O_CREAT | constants15.O_EXCL | constants15.O_WRONLY | constants15.O_NOFOLLOW, 384);
41920
+ try {
41921
+ writeFileSync12(fd, bytes);
41922
+ fsyncSync7(fd);
41923
+ } finally {
41924
+ closeSync17(fd);
41925
+ }
41926
+ syncDirectory2(dirname23(path));
41927
+ }
41928
+ async function planNativeSafetyRegistration(options, dependencies = {}) {
41929
+ const selected = { ...options }, before = inspect2(selected);
41930
+ try {
41931
+ await (dependencies.verify ?? verifyNativeSafetyCommand)(before.preview.command, before.home);
41932
+ } catch {
41933
+ throw new NativeRegistrationError("guard_failed");
41934
+ }
41935
+ need9(inspect2(selected).plan.planDigest === before.plan.planDigest, "settings_changed");
41936
+ return before.plan;
41937
+ }
41938
+ async function applyNativeSafetyRegistration(options, dependencies = {}) {
41939
+ const selected = { ...options };
41940
+ need9(/^[a-f0-9]{64}$/.test(selected.expectedPlanDigest), "input_invalid");
41941
+ const plan = await planNativeSafetyRegistration(selected, dependencies);
41942
+ need9(plan.planDigest === selected.expectedPlanDigest, "settings_changed");
41943
+ const before = inspect2(selected);
41944
+ need9(before.plan.planDigest === plan.planDigest, "settings_changed");
41945
+ if (plan.action === "unchanged")
41946
+ return { ok: true, changed: false, ...plan };
41947
+ const operationId = randomUUID5();
41948
+ let directory2 = before.home;
41949
+ for (const part of [".hasna", "hooks", "native", "trash-registration", operationId]) {
41950
+ directory2 = join29(directory2, part);
41951
+ try {
41952
+ mkdirSync14(directory2, { mode: 448 });
41953
+ syncDirectory2(dirname23(directory2));
41954
+ } catch (error61) {
41955
+ if (error61.code !== "EEXIST")
41956
+ throw new NativeRegistrationError("settings_unsafe");
41957
+ }
41958
+ safeDirectory3(directory2);
41959
+ }
41960
+ if (before.before.raw !== null)
41961
+ exclusive2(join29(directory2, "settings.before"), before.before.raw);
41962
+ exclusive2(join29(directory2, "intent.json"), JSON.stringify({ operationId, plan }) + `
41963
+ `);
41964
+ try {
41965
+ need9(inspect2(selected).plan.planDigest === plan.planDigest, "settings_changed");
41966
+ const result = installHook("trash-guard", { target: selected.target, overwrite: true, expectedSettingsSha256: plan.beforeSHA256 });
41967
+ need9(result.success, "apply_outcome_uncertain");
41968
+ const after = inspect2(selected);
41969
+ need9(after.before.sha256 === plan.desiredSHA256 && after.plan.resolvedPath === plan.resolvedPath && after.plan.action === "unchanged" && after.plan.commandSHA256 === plan.commandSHA256, "apply_outcome_uncertain");
41970
+ const receipt = {
41971
+ ok: true,
41972
+ changed: true,
41973
+ operationId,
41974
+ planDigest: plan.planDigest,
41975
+ settingsSHA256: after.before.sha256,
41976
+ guardVerified: true,
41977
+ nativeAdoptionVerified: false
41978
+ };
41979
+ exclusive2(join29(directory2, "receipt.json"), JSON.stringify(receipt) + `
41980
+ `);
41981
+ return receipt;
41982
+ } catch {
41983
+ throw new NativeRegistrationError("apply_outcome_uncertain", operationId);
41984
+ }
41985
+ }
40980
41986
 
40981
41987
  // src/index.ts
40982
41988
  function installHookForProject(name, options = {}) {
@@ -40999,11 +42005,11 @@ async function runHook(name, input2, options = {}) {
40999
42005
  throw new Error(`Hook '${name}' not found`);
41000
42006
  const hookScript = getHookPath(name) + "/src/hook.ts";
41001
42007
  const resolvedScript = custom3?.scriptPath;
41002
- const script = resolvedScript ?? (existsSync14(hookScript) ? hookScript : undefined);
42008
+ const script = resolvedScript ?? (existsSync15(hookScript) ? hookScript : undefined);
41003
42009
  if (!script)
41004
42010
  throw new Error(`Hook script not found: ${name}`);
41005
42011
  const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
41006
- const content = readFileSync19(script);
42012
+ const content = readFileSync26(script);
41007
42013
  const check2 = checkScriptHash2(name, sha256Of2(content));
41008
42014
  if (!check2.ok) {
41009
42015
  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 +42101,7 @@ export {
41095
42101
  writeLock,
41096
42102
  writeCustomHook,
41097
42103
  verifyScriptHash,
42104
+ verifyClaudeSafetyConfiguration,
41098
42105
  upsertHookRecord,
41099
42106
  updateProfile,
41100
42107
  uninstallHook,
@@ -41133,6 +42140,8 @@ export {
41133
42140
  readCustomManifest,
41134
42141
  provisionCloudflareResources,
41135
42142
  planSync,
42143
+ planNativeSafetyRegistration,
42144
+ planCodexSafetyTrust,
41136
42145
  pinInstalledHook,
41137
42146
  parseStorageTables,
41138
42147
  parseManifest,
@@ -41186,12 +42195,15 @@ export {
41186
42195
  createProfile,
41187
42196
  checkScriptHash,
41188
42197
  buildCodewithTomlFragment,
42198
+ applyNativeSafetyRegistration,
42199
+ applyCodexSafetyTrust,
41189
42200
  allowLocalStore,
41190
42201
  STORAGE_TABLES,
41191
42202
  STORAGE_DATABASE_ENV,
41192
42203
  STORAGE_BACKEND_ENV,
41193
42204
  STORAGE_BACKENDS,
41194
42205
  RETIRED_STORAGE_MODE_ENV,
42206
+ NativeRegistrationError,
41195
42207
  HOOK_EVENTS,
41196
42208
  HOOKS_STORAGE_TABLES,
41197
42209
  HOOKS_STORAGE_FALLBACK_ENV,
@@ -41201,5 +42213,7 @@ export {
41201
42213
  HOOKS_LOCAL_OPT_IN_ENV_KEYS,
41202
42214
  HOOKS,
41203
42215
  DEFAULT_SERVE_PORT,
42216
+ CodexTrustError,
42217
+ ClaudeSafetyError,
41204
42218
  CATEGORIES
41205
42219
  };