@hasna/hooks 0.10.5 → 0.10.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +30 -0
  2. package/bin/index.js +331 -56
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -92,6 +92,36 @@ Repair unsafe installation permissions with a verified package installer, then
92
92
  check again. This check alone does not prove hosted authentication, native hook
93
93
  trust or a running agent's interception.
94
94
 
95
+ ## Codex safety readiness
96
+
97
+ ```bash
98
+ hooks safety verify --target codex --cwd /path/to/project --codex-command /absolute/path/to/native/codex
99
+ ```
100
+
101
+ This bounded, read-only check starts its own fresh Codex app-server and asks the
102
+ native consumer for the selected project's hook definitions and feature state.
103
+ It requires exactly one enabled, trusted, synchronous bundled Trash guard, checks
104
+ its installed supervisor by classifying data, and verifies that the native
105
+ definition stayed unchanged. It returns compact JSON and a nonzero exit on
106
+ missing, disabled, untrusted, changed or unsafe state. No deletion is executed.
107
+
108
+ The command accepts a native Linux ELF or macOS Mach-O executable for the host
109
+ architecture (arm64 or x64); script launchers are refused before execution. When
110
+ Codex is installed through npm, pass the platform package's native binary rather
111
+ than its JavaScript launcher. Executable ownership, permissions, ancestry and
112
+ bytes are checked. Supported protocol versions are 0.153.0, 0.154.0, 0.155.0,
113
+ 0.155.1 and 0.156.1; other versions refuse pending compatibility validation.
114
+ The version probe has a five-second forced timeout and native discovery a
115
+ fifteen-second lifetime budget. Neither step restarts an existing agent.
116
+
117
+ Success reports `nativeDiscoveryVerified: true`, `guardVerified: true` and
118
+ `nativeAdoptionVerified: false`. It does not enroll trust, change configuration,
119
+ authenticate Trash, verify MCP or Instructions, or prove interception by an
120
+ already-running session. Complete those separate setup checks and a fresh native
121
+ interception proof before declaring station support. Use this small readiness
122
+ check after installation, meaningful upgrades and during fresh-agent startup;
123
+ never substitute repeated deletion smoke tests for a startup check.
124
+
95
125
  ## Optional Mementos prompt context
96
126
 
97
127
  `hooks install mementos-context --target codex` registers a native prompt hook.
package/bin/index.js CHANGED
@@ -37388,7 +37388,7 @@ var init_redact = __esm(() => {
37388
37388
  });
37389
37389
 
37390
37390
  // src/lib/profiles.ts
37391
- import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync10, readdirSync as readdirSync8, rmSync, cpSync as cpSync2 } from "fs";
37391
+ import { existsSync as existsSync15, mkdirSync as mkdirSync11, readFileSync as readFileSync15, writeFileSync as writeFileSync10, readdirSync as readdirSync8, rmSync, cpSync as cpSync2 } from "fs";
37392
37392
  import { join as join19 } from "path";
37393
37393
  function getProfilesDir() {
37394
37394
  const newDir = join19(getEffectiveDataRoot(), "profiles");
@@ -37434,7 +37434,7 @@ function getProfile(id) {
37434
37434
  try {
37435
37435
  if (!existsSync15(path))
37436
37436
  return null;
37437
- return JSON.parse(readFileSync14(path, "utf-8"));
37437
+ return JSON.parse(readFileSync15(path, "utf-8"));
37438
37438
  } catch {
37439
37439
  return null;
37440
37440
  }
@@ -37448,7 +37448,7 @@ function listProfiles() {
37448
37448
  const profiles = [];
37449
37449
  for (const file2 of files) {
37450
37450
  try {
37451
- const content = readFileSync14(join19(dir, file2), "utf-8");
37451
+ const content = readFileSync15(join19(dir, file2), "utf-8");
37452
37452
  profiles.push(JSON.parse(content));
37453
37453
  } catch {}
37454
37454
  }
@@ -37498,12 +37498,12 @@ import { spawnSync as spawnSync2 } from "child_process";
37498
37498
  import { closeSync as closeSync22, fstatSync as fstatSync22, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
37499
37499
  import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
37500
37500
  import { hostname as osHostname } from "os";
37501
- import { isAbsolute as isAbsolute17, join as join22 } from "path";
37501
+ import { isAbsolute as isAbsolute18, join as join22 } from "path";
37502
37502
  import { createRequire as createRequire3 } from "module";
37503
37503
  import { fileURLToPath as fileURLToPath22 } from "url";
37504
- import { closeSync as closeSync6, fstatSync as fstatSync5, lstatSync as lstatSync10, openSync as openSync6, readFileSync as readFileSync15, realpathSync as realpathSync9, statSync as statSync2 } from "fs";
37504
+ import { closeSync as closeSync7, fstatSync as fstatSync6, lstatSync as lstatSync15, openSync as openSync7, readFileSync as readFileSync17, realpathSync as realpathSync10, statSync as statSync2 } from "fs";
37505
37505
  import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
37506
- import { basename as basename5, dirname as dirname19, join as join20, relative, sep as sep3 } from "path";
37506
+ import { basename as basename5, dirname as dirname20, join as join20, relative, sep as sep3 } from "path";
37507
37507
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "url";
37508
37508
  function envToken2(name) {
37509
37509
  return name.toUpperCase().replace(/-/g, "_");
@@ -37577,16 +37577,16 @@ function o(e2, r2, t2) {
37577
37577
  }
37578
37578
  }
37579
37579
  function entryPoint(directory2) {
37580
- const root = realpathSync9(directory2);
37581
- const fd = openSync6(join20(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
37580
+ const root = realpathSync10(directory2);
37581
+ const fd = openSync7(join20(root, "package.json"), O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
37582
37582
  let pkg;
37583
37583
  try {
37584
- const stat2 = fstatSync5(fd);
37584
+ const stat2 = fstatSync6(fd);
37585
37585
  if (!stat2.isFile() || stat2.size > MAX_PACKAGE_BYTES3)
37586
37586
  throw new Error("Invalid Secrets package metadata");
37587
- pkg = JSON.parse(readFileSync15(fd, "utf8"));
37587
+ pkg = JSON.parse(readFileSync17(fd, "utf8"));
37588
37588
  } finally {
37589
- closeSync6(fd);
37589
+ closeSync7(fd);
37590
37590
  }
37591
37591
  if (!pkg || typeof pkg !== "object" || !("name" in pkg) || pkg.name !== PACKAGE) {
37592
37592
  throw new Error("Invalid Secrets package identity");
@@ -37598,7 +37598,7 @@ function entryPoint(directory2) {
37598
37598
  if (/[\\%?#\0]/.test(target) || parts.some((part) => !part || part === "." || part === ".." || part === "node_modules")) {
37599
37599
  throw new Error("Invalid Secrets import export");
37600
37600
  }
37601
- const file2 = realpathSync9(join20(root, ...parts));
37601
+ const file2 = realpathSync10(join20(root, ...parts));
37602
37602
  const within = relative(root, file2);
37603
37603
  if (!within || within === ".." || within.startsWith(`..${sep3}`) || !statSync2(file2).isFile()) {
37604
37604
  throw new Error("Secrets import export escapes its package or is not a file");
@@ -37606,13 +37606,13 @@ function entryPoint(directory2) {
37606
37606
  return pathToFileURL(file2).href;
37607
37607
  }
37608
37608
  function resolveInstalledSecrets(parentUrl) {
37609
- let directory2 = dirname19(fileURLToPath3(parentUrl));
37609
+ let directory2 = dirname20(fileURLToPath3(parentUrl));
37610
37610
  for (;; ) {
37611
37611
  if (basename5(directory2) !== "node_modules") {
37612
37612
  const candidate = join20(directory2, "node_modules", PACKAGE);
37613
37613
  let absent = false;
37614
37614
  try {
37615
- lstatSync10(candidate);
37615
+ lstatSync15(candidate);
37616
37616
  } catch (error61) {
37617
37617
  if (error61.code !== "ENOENT")
37618
37618
  throw error61;
@@ -37621,7 +37621,7 @@ function resolveInstalledSecrets(parentUrl) {
37621
37621
  if (!absent)
37622
37622
  return entryPoint(candidate);
37623
37623
  }
37624
- const parent = dirname19(directory2);
37624
+ const parent = dirname20(directory2);
37625
37625
  if (parent === directory2)
37626
37626
  throw new Error("Secrets SDK is not installed for this consumer");
37627
37627
  directory2 = parent;
@@ -37633,7 +37633,7 @@ function homeDir(env) {
37633
37633
  }
37634
37634
  function absoluteOverride(env, key) {
37635
37635
  const value = env[key]?.trim();
37636
- return value && isAbsolute17(value) ? value : null;
37636
+ return value && isAbsolute18(value) ? value : null;
37637
37637
  }
37638
37638
  function hasnaHomeDir(env) {
37639
37639
  const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
@@ -38740,8 +38740,8 @@ var init_db_writer = __esm(() => {
38740
38740
  });
38741
38741
 
38742
38742
  // src/lib/hook-env.ts
38743
- import { dirname as dirname20, isAbsolute as isAbsolute18 } from "path";
38744
- import { existsSync as existsSync16, realpathSync as realpathSync10, statSync as statSync4 } from "fs";
38743
+ import { dirname as dirname21, isAbsolute as isAbsolute19 } from "path";
38744
+ import { existsSync as existsSync16, realpathSync as realpathSync11, statSync as statSync4 } from "fs";
38745
38745
  import { tmpdir } from "os";
38746
38746
  function isDeniedEnvName(name) {
38747
38747
  const upper = name.toUpperCase();
@@ -38760,7 +38760,7 @@ function isBashFunctionEnvName(name) {
38760
38760
  return name.startsWith("BASH_FUNC_");
38761
38761
  }
38762
38762
  function isUnsafePathEntry(entry, home) {
38763
- if (entry === "" || !isAbsolute18(entry))
38763
+ if (entry === "" || !isAbsolute19(entry))
38764
38764
  return true;
38765
38765
  if (home && (entry === home || entry.startsWith(home + "/")))
38766
38766
  return true;
@@ -38770,7 +38770,7 @@ function isUnsafePathEntry(entry, home) {
38770
38770
  return true;
38771
38771
  const canonical4 = (path) => {
38772
38772
  try {
38773
- return realpathSync10(path);
38773
+ return realpathSync11(path);
38774
38774
  } catch {
38775
38775
  return path;
38776
38776
  }
@@ -38791,7 +38791,7 @@ function sanitizeHookPath(pathValue, home) {
38791
38791
  const baseline = PATH_BASELINE.slice();
38792
38792
  if (process.platform === "darwin" && existsSync16("/opt/homebrew/bin"))
38793
38793
  baseline.push("/opt/homebrew/bin");
38794
- const bunDir = dirname20(process.execPath);
38794
+ const bunDir = dirname21(process.execPath);
38795
38795
  const seen = new Set;
38796
38796
  const out = [];
38797
38797
  for (const dir of baseline) {
@@ -38916,7 +38916,7 @@ __export(exports_run, {
38916
38916
  });
38917
38917
  import { randomBytes as randomBytes2 } from "crypto";
38918
38918
  import { rmSync as rmSync2, writeFileSync as writeFileSync11 } from "fs";
38919
- import { basename as basename9, dirname as dirname21, isAbsolute as isAbsolute19, join as join24 } from "path";
38919
+ import { basename as basename9, dirname as dirname22, isAbsolute as isAbsolute20, join as join24 } from "path";
38920
38920
  function extensionOf(scriptPath) {
38921
38921
  const base = basename9(scriptPath);
38922
38922
  const dot = base.lastIndexOf(".");
@@ -38985,7 +38985,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
38985
38985
  return chunks.join("");
38986
38986
  }
38987
38987
  async function executeVerifiedScript(options) {
38988
- const scriptDir = dirname21(options.scriptPath);
38988
+ const scriptDir = dirname22(options.scriptPath);
38989
38989
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
38990
38990
  const tempPath = join24(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
38991
38991
  let proc = null;
@@ -38996,7 +38996,7 @@ async function executeVerifiedScript(options) {
38996
38996
  }, options.timeout) : null;
38997
38997
  try {
38998
38998
  writeFileSync11(tempPath, options.content, { flag: "wx", mode: 384 });
38999
- proc = Bun.spawn([...interpreter.command, isAbsolute19(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
38999
+ proc = Bun.spawn([...interpreter.command, isAbsolute20(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
39000
39000
  stdin: new Response(options.stdin),
39001
39001
  stdout: "pipe",
39002
39002
  stderr: "pipe",
@@ -39078,7 +39078,7 @@ __export(exports_custom_install, {
39078
39078
  installCustomSource: () => installCustomSource,
39079
39079
  ensureCustomDir: () => ensureCustomDir
39080
39080
  });
39081
- import { existsSync as existsSync17, mkdirSync as mkdirSync12, mkdtempSync, readFileSync as readFileSync17, rmSync as rmSync4 } from "fs";
39081
+ import { existsSync as existsSync17, mkdirSync as mkdirSync12, mkdtempSync, readFileSync as readFileSync19, rmSync as rmSync4 } from "fs";
39082
39082
  import { join as join27 } from "path";
39083
39083
  import { tmpdir as tmpdir2 } from "os";
39084
39084
  function manifestAt(dir) {
@@ -39086,7 +39086,7 @@ function manifestAt(dir) {
39086
39086
  if (!existsSync17(path))
39087
39087
  return;
39088
39088
  try {
39089
- return parseManifest(readFileSync17(path, "utf-8"));
39089
+ return parseManifest(readFileSync19(path, "utf-8"));
39090
39090
  } catch {
39091
39091
  return;
39092
39092
  }
@@ -39147,7 +39147,7 @@ async function installCustomSource(source) {
39147
39147
  throw new Error(`No such local path: ${source}`);
39148
39148
  }
39149
39149
  if (source.endsWith("manifest.json")) {
39150
- const manifest = parseManifest(readFileSync17(source, "utf-8"));
39150
+ const manifest = parseManifest(readFileSync19(source, "utf-8"));
39151
39151
  return writeFromManifest(manifest, source.substring(0, source.lastIndexOf("/")), source, "local");
39152
39152
  }
39153
39153
  return copyManifestDir(source, source, "local");
@@ -39196,13 +39196,13 @@ __export(exports_sync, {
39196
39196
  fetchPinnedHook: () => fetchPinnedHook,
39197
39197
  commitSyncArtifacts: () => commitSyncArtifacts
39198
39198
  });
39199
- import { createHash as createHash12 } from "crypto";
39200
- import { readFileSync as readFileSync19 } from "fs";
39199
+ import { createHash as createHash17 } from "crypto";
39200
+ import { readFileSync as readFileSync20 } from "fs";
39201
39201
  function sha256OfText(text3) {
39202
- return createHash12("sha256").update(text3).digest("hex");
39202
+ return createHash17("sha256").update(text3).digest("hex");
39203
39203
  }
39204
39204
  function sha256OfPath(path) {
39205
- return createHash12("sha256").update(readFileSync19(path)).digest("hex");
39205
+ return createHash17("sha256").update(readFileSync20(path)).digest("hex");
39206
39206
  }
39207
39207
  function collectBundledCatalog() {
39208
39208
  const out = [];
@@ -40357,13 +40357,13 @@ __export(exports_serve, {
40357
40357
  SERVE_HOST: () => SERVE_HOST,
40358
40358
  DEFAULT_SERVE_PORT: () => DEFAULT_SERVE_PORT
40359
40359
  });
40360
- import { readFileSync as readFileSync20 } from "fs";
40361
- import { dirname as dirname22, join as join29 } from "path";
40360
+ import { readFileSync as readFileSync21 } from "fs";
40361
+ import { dirname as dirname23, join as join29 } from "path";
40362
40362
  import { fileURLToPath as fileURLToPath4 } from "url";
40363
40363
  function packageVersion() {
40364
40364
  try {
40365
- const pkgPath = join29(dirname22(fileURLToPath4(import.meta.url)), "..", "package.json");
40366
- return JSON.parse(readFileSync20(pkgPath, "utf-8")).version || "0.0.0";
40365
+ const pkgPath = join29(dirname23(fileURLToPath4(import.meta.url)), "..", "package.json");
40366
+ return JSON.parse(readFileSync21(pkgPath, "utf-8")).version || "0.0.0";
40367
40367
  } catch {
40368
40368
  return "0.0.0";
40369
40369
  }
@@ -40663,8 +40663,8 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
40663
40663
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
40664
40664
  import { z as z3 } from "zod";
40665
40665
  import { createServer } from "http";
40666
- import { existsSync as existsSync18, readFileSync as readFileSync21 } from "fs";
40667
- import { join as join30, dirname as dirname23 } from "path";
40666
+ import { existsSync as existsSync18, readFileSync as readFileSync23 } from "fs";
40667
+ import { join as join30, dirname as dirname24 } from "path";
40668
40668
  import { fileURLToPath as fileURLToPath5 } from "url";
40669
40669
  async function runVerifiedHook(name, stdin, timeoutMs) {
40670
40670
  const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
@@ -40673,7 +40673,7 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
40673
40673
  if (!resolved || !hookScript || !existsSync18(hookScript)) {
40674
40674
  throw new Error(`Hook '${name}' not found`);
40675
40675
  }
40676
- const content = readFileSync21(hookScript);
40676
+ const content = readFileSync23(hookScript);
40677
40677
  const sha4 = sha256Of(content);
40678
40678
  const check2 = checkScriptHash(name, sha4);
40679
40679
  if (!check2.ok) {
@@ -40905,7 +40905,7 @@ function createHooksServer() {
40905
40905
  }
40906
40906
  if (meta3 && settingsExist) {
40907
40907
  try {
40908
- const settings = JSON.parse(readFileSync21(settingsPath, "utf-8"));
40908
+ const settings = JSON.parse(readFileSync23(settingsPath, "utf-8"));
40909
40909
  if (!hookRegisteredInSettings(settings, name, meta3.event, meta3.matcher)) {
40910
40910
  const eventName = meta3.event.split(":")[0];
40911
40911
  issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
@@ -40945,7 +40945,7 @@ function createHooksServer() {
40945
40945
  const readmePath = join30(hookPath, "README.md");
40946
40946
  let readme = "";
40947
40947
  if (existsSync18(readmePath)) {
40948
- readme = readFileSync21(readmePath, "utf-8");
40948
+ readme = readFileSync23(readmePath, "utf-8");
40949
40949
  }
40950
40950
  if (verbose) {
40951
40951
  return { content: [{ type: "text", text: JSON.stringify({ ...meta3, readme }) }] };
@@ -41275,7 +41275,7 @@ function createHooksServer() {
41275
41275
  let settings = {};
41276
41276
  try {
41277
41277
  if (existsSync18(settingsPath))
41278
- settings = JSON.parse(readFileSync21(settingsPath, "utf-8"));
41278
+ settings = JSON.parse(readFileSync23(settingsPath, "utf-8"));
41279
41279
  } catch {}
41280
41280
  if (!settings.hooks)
41281
41281
  settings.hooks = {};
@@ -41298,7 +41298,7 @@ function createHooksServer() {
41298
41298
  let settings = {};
41299
41299
  try {
41300
41300
  if (existsSync18(settingsPath))
41301
- settings = JSON.parse(readFileSync21(settingsPath, "utf-8"));
41301
+ settings = JSON.parse(readFileSync23(settingsPath, "utf-8"));
41302
41302
  } catch {}
41303
41303
  if (settings.hooks?.__disabled) {
41304
41304
  settings.hooks.__disabled = settings.hooks.__disabled.filter((n2) => n2 !== name);
@@ -41668,13 +41668,13 @@ var init_server = __esm(() => {
41668
41668
  init_redact();
41669
41669
  init_authority();
41670
41670
  init_storage();
41671
- __dirname3 = dirname23(fileURLToPath5(import.meta.url));
41671
+ __dirname3 = dirname24(fileURLToPath5(import.meta.url));
41672
41672
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
41673
41673
  try {
41674
41674
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
41675
41675
  const p = join30(__dirname3, rel);
41676
41676
  if (existsSync18(p)) {
41677
- pkg = JSON.parse(readFileSync21(p, "utf-8"));
41677
+ pkg = JSON.parse(readFileSync23(p, "utf-8"));
41678
41678
  break;
41679
41679
  }
41680
41680
  }
@@ -43416,8 +43416,8 @@ var {
43416
43416
 
43417
43417
  // src/cli/index.tsx
43418
43418
  import chalk2 from "chalk";
43419
- import { existsSync as existsSync19, readFileSync as readFileSync23, rmSync as rmSync6 } from "fs";
43420
- import { join as join31, dirname as dirname24 } from "path";
43419
+ import { existsSync as existsSync19, readFileSync as readFileSync25, rmSync as rmSync6 } from "fs";
43420
+ import { join as join31, dirname as dirname25 } from "path";
43421
43421
  import { fileURLToPath as fileURLToPath6 } from "url";
43422
43422
 
43423
43423
  // src/cli/components/App.tsx
@@ -45023,6 +45023,268 @@ function inspectTrashBinary(env = process.env) {
45023
45023
  return { ok: false, code: failure };
45024
45024
  }
45025
45025
 
45026
+ // src/lib/codex-safety-check.ts
45027
+ init_native_safety();
45028
+ import { spawn as spawn2, execFile as execFile2 } from "child_process";
45029
+ import { createHash as createHash12 } from "crypto";
45030
+ import { constants as constants12, openSync as openSync6, closeSync as closeSync6, fstatSync as fstatSync5, lstatSync as lstatSync10, readFileSync as readFileSync14, realpathSync as realpathSync9 } from "fs";
45031
+ import { homedir as homedir8 } from "os";
45032
+ import { dirname as dirname19, isAbsolute as isAbsolute17 } from "path";
45033
+ import { promisify } from "util";
45034
+ 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"]);
45035
+ var matcher = "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$";
45036
+
45037
+ class CodexSafetyError extends Error {
45038
+ code;
45039
+ constructor(code2) {
45040
+ super(`Codex deletion safety check failed (${code2}).`);
45041
+ this.code = code2;
45042
+ this.name = "CodexSafetyError";
45043
+ }
45044
+ }
45045
+ function need5(condition, code2) {
45046
+ if (!condition)
45047
+ throw new CodexSafetyError(code2);
45048
+ }
45049
+ var digest3 = (bytes) => createHash12("sha256").update(bytes).digest("hex");
45050
+ function nativeHeader(bytes) {
45051
+ if (bytes.length < 64 || !["arm64", "x64"].includes(process.arch))
45052
+ return false;
45053
+ if (process.platform === "darwin") {
45054
+ const cpu = process.arch === "arm64" ? 16777228 : 16777223;
45055
+ 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;
45056
+ }
45057
+ if (process.platform === "linux") {
45058
+ const offset = bytes.readBigUInt64LE(32), count = bytes.readUInt16LE(56);
45059
+ 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);
45060
+ }
45061
+ return false;
45062
+ }
45063
+ function executable(command) {
45064
+ const found = isAbsolute17(command) ? command : Bun.which(command);
45065
+ need5(found, "native_unavailable");
45066
+ try {
45067
+ const path = realpathSync9(found);
45068
+ for (let parent = dirname19(path);; parent = dirname19(parent)) {
45069
+ const stat2 = lstatSync10(parent);
45070
+ need5(stat2.isDirectory() && [0, process.getuid?.()].includes(stat2.uid) && !(stat2.mode & 18), "native_unsafe_executable");
45071
+ if (parent === dirname19(parent))
45072
+ break;
45073
+ }
45074
+ const fd = openSync6(path, constants12.O_RDONLY | constants12.O_NOFOLLOW | constants12.O_NONBLOCK);
45075
+ try {
45076
+ const stat2 = fstatSync5(fd);
45077
+ need5(stat2.isFile() && [0, process.getuid?.()].includes(stat2.uid) && !(stat2.mode & 18) && !!(stat2.mode & 64) && stat2.size <= 256000000, "native_unsafe_executable");
45078
+ const bytes = readFileSync14(fd);
45079
+ need5(bytes.length <= 256000000, "native_unavailable");
45080
+ need5(nativeHeader(bytes), "native_executable_required");
45081
+ return { path, sha256: digest3(bytes) };
45082
+ } finally {
45083
+ closeSync6(fd);
45084
+ }
45085
+ } catch (error61) {
45086
+ if (error61 instanceof CodexSafetyError)
45087
+ throw error61;
45088
+ throw new CodexSafetyError("native_unavailable");
45089
+ }
45090
+ }
45091
+ async function connectCodexSafety(options) {
45092
+ const command = options.command ?? "codex", before = executable(command);
45093
+ const env = { ...process.env, HOME: options.home, NO_COLOR: "1" };
45094
+ for (const key of ["NODE_OPTIONS", "NODE_PATH", "BUN_OPTIONS", "BASH_ENV", "ENV", "DYLD_INSERT_LIBRARIES", "LD_PRELOAD"])
45095
+ delete env[key];
45096
+ if (options.codexHome === undefined)
45097
+ delete env.CODEX_HOME;
45098
+ else
45099
+ env.CODEX_HOME = options.codexHome;
45100
+ let version2;
45101
+ try {
45102
+ version2 = (await promisify(execFile2)(before.path, ["--version"], { env, cwd: options.home, timeout: 5000, killSignal: "SIGKILL", maxBuffer: 4096 })).stdout.trim();
45103
+ } catch {
45104
+ throw new CodexSafetyError("native_unavailable");
45105
+ }
45106
+ need5(versions.has(version2), "native_unsupported");
45107
+ need5(JSON.stringify(executable(command)) === JSON.stringify(before), "guard_changed");
45108
+ const child = spawn2(before.path, ["app-server", "--strict-config", "--stdio"], { env, cwd: options.home, stdio: ["pipe", "pipe", "pipe"] });
45109
+ let stopped = false, buffer = Buffer.alloc(0), total = 0, nextId = 1;
45110
+ const pending = new Map;
45111
+ const fail2 = () => {
45112
+ stopped = true;
45113
+ for (const waiter of pending.values())
45114
+ waiter.reject(new CodexSafetyError("native_failed"));
45115
+ pending.clear();
45116
+ };
45117
+ const deadline = setTimeout(() => {
45118
+ fail2();
45119
+ child.kill("SIGKILL");
45120
+ }, 15000);
45121
+ const exited = new Promise((done) => {
45122
+ child.once("exit", () => {
45123
+ fail2();
45124
+ clearTimeout(deadline);
45125
+ done();
45126
+ });
45127
+ child.once("error", () => {
45128
+ fail2();
45129
+ clearTimeout(deadline);
45130
+ done();
45131
+ });
45132
+ });
45133
+ child.stdin.on("error", fail2);
45134
+ const account = (length) => {
45135
+ total += length;
45136
+ if (total > 8388608) {
45137
+ fail2();
45138
+ child.kill("SIGKILL");
45139
+ return false;
45140
+ }
45141
+ return true;
45142
+ };
45143
+ child.stderr.on("data", (bytes) => {
45144
+ account(bytes.length);
45145
+ });
45146
+ child.stdout.on("data", (bytes) => {
45147
+ if (stopped || !account(bytes.length))
45148
+ return;
45149
+ buffer = Buffer.concat([buffer, bytes]);
45150
+ for (;; ) {
45151
+ const end = buffer.indexOf(10);
45152
+ if (end < 0)
45153
+ return;
45154
+ const line = buffer.subarray(0, end);
45155
+ buffer = buffer.subarray(end + 1);
45156
+ try {
45157
+ const value = JSON.parse(line.toString("utf8"));
45158
+ if (!value || typeof value !== "object" || Array.isArray(value))
45159
+ throw Error();
45160
+ if (!Object.hasOwn(value, "id"))
45161
+ continue;
45162
+ const waiter = pending.get(value.id);
45163
+ if (!waiter)
45164
+ throw Error();
45165
+ pending.delete(value.id);
45166
+ if (value.error || !Object.hasOwn(value, "result"))
45167
+ waiter.reject(new CodexSafetyError("native_failed"));
45168
+ else
45169
+ waiter.resolve(value.result);
45170
+ } catch {
45171
+ fail2();
45172
+ child.kill("SIGKILL");
45173
+ return;
45174
+ }
45175
+ }
45176
+ });
45177
+ const request = (method, params) => new Promise((accept, reject) => {
45178
+ if (stopped) {
45179
+ reject(new CodexSafetyError("native_failed"));
45180
+ return;
45181
+ }
45182
+ const id = nextId++;
45183
+ pending.set(id, { resolve: accept, reject });
45184
+ child.stdin.write(JSON.stringify({ id, method, params }) + `
45185
+ `);
45186
+ });
45187
+ const rpc = {
45188
+ version: version2,
45189
+ request,
45190
+ async close() {
45191
+ fail2();
45192
+ child.stdin.end();
45193
+ const force = setTimeout(() => child.kill("SIGKILL"), 1000);
45194
+ await exited;
45195
+ clearTimeout(force);
45196
+ clearTimeout(deadline);
45197
+ need5(JSON.stringify(executable(command)) === JSON.stringify(before), "guard_changed");
45198
+ }
45199
+ };
45200
+ try {
45201
+ await request("initialize", { clientInfo: { name: "hooks-safety-check", version: "1" }, capabilities: { experimentalApi: true } });
45202
+ child.stdin.write(`{"method":"initialized"}
45203
+ `);
45204
+ return rpc;
45205
+ } catch {
45206
+ await rpc.close();
45207
+ throw new CodexSafetyError("native_failed");
45208
+ }
45209
+ }
45210
+ function selectCodexSafety(discovery, configuration, home, cwd, featureEnabled) {
45211
+ const data = discovery?.data;
45212
+ 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) && data[0].warnings.length === 0, "native_discovery_failed");
45213
+ const config2 = configuration?.config;
45214
+ need5(config2 && typeof config2 === "object" && !Array.isArray(config2), "native_discovery_failed");
45215
+ need5((config2.features?.hooks ?? featureEnabled) === true, "hooks_disabled");
45216
+ for (const hook2 of data[0].hooks)
45217
+ if (typeof hook2?.command === "string") {
45218
+ if (/\bhasna-native-safety-v\d+\b/.test(hook2.command))
45219
+ need5(nativeSafetyCommandBinding(hook2.command), "guard_definition_invalid");
45220
+ need5(!/^(?:hooks run trash-guard|hook-trash-guard)(?:\s|$)/.test(hook2.command.trim()), "guard_definition_invalid");
45221
+ }
45222
+ const selected = data[0].hooks.filter((hook2) => typeof hook2?.command === "string" && nativeSafetyCommandBinding(hook2.command)?.name === "trash-guard");
45223
+ need5(selected.length !== 0, "guard_missing");
45224
+ need5(selected.length === 1, "guard_ambiguous");
45225
+ const hook = selected[0], binding = nativeSafetyCommandBinding(hook.command);
45226
+ 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");
45227
+ need5(hook.enabled === true, "guard_disabled");
45228
+ need5(hook.trustStatus === "trusted", "guard_untrusted");
45229
+ return { key: hook.key, command: hook.command, currentHash: hook.currentHash, sourcePath: hook.sourcePath, enabled: true, trustStatus: "trusted" };
45230
+ }
45231
+ async function verifyCodexNativeSafety(options = {}, dependencies = {}) {
45232
+ const home = realpathSync9(options.home ?? homedir8()), cwd = realpathSync9(options.cwd ?? process.cwd());
45233
+ need5(isAbsolute17(home) && isAbsolute17(cwd), "native_unavailable");
45234
+ const rpc = await (dependencies.connect ?? connectCodexSafety)({ ...options, home });
45235
+ try {
45236
+ need5(versions.has(rpc.version), "native_unsupported");
45237
+ const inspect = async () => {
45238
+ const hooks = await rpc.request("hooks/list", { cwds: [cwd] });
45239
+ const configuration = await rpc.request("config/read", { cwd, includeLayers: false });
45240
+ let enabled;
45241
+ if (configuration?.config?.features?.hooks === undefined) {
45242
+ let cursor = null, count = 0;
45243
+ const seen = new Set;
45244
+ do {
45245
+ const page3 = await rpc.request("experimentalFeature/list", { cursor, limit: 100 });
45246
+ need5(Array.isArray(page3?.data) && page3.data.length <= 100 && (page3.nextCursor === null || typeof page3.nextCursor === "string"), "native_discovery_failed");
45247
+ count += page3.data.length;
45248
+ need5(count <= 1000, "native_discovery_failed");
45249
+ for (const feature of page3.data)
45250
+ if (feature?.name === "hooks") {
45251
+ need5(enabled === undefined && typeof feature.enabled === "boolean", "native_discovery_failed");
45252
+ enabled = feature.enabled;
45253
+ }
45254
+ cursor = page3.nextCursor;
45255
+ if (cursor !== null) {
45256
+ need5(cursor.length > 0 && cursor.length <= 4096 && !seen.has(cursor) && page3.data.length > 0, "native_discovery_failed");
45257
+ seen.add(cursor);
45258
+ }
45259
+ } while (cursor !== null);
45260
+ }
45261
+ return selectCodexSafety(hooks, configuration, home, cwd, enabled);
45262
+ };
45263
+ const before = await inspect();
45264
+ try {
45265
+ await (dependencies.verify ?? verifyNativeSafetyCommand)(before.command, home);
45266
+ } catch {
45267
+ throw new CodexSafetyError("guard_failed");
45268
+ }
45269
+ need5(JSON.stringify(await inspect()) === JSON.stringify(before), "guard_changed");
45270
+ return {
45271
+ ok: true,
45272
+ target: "codex",
45273
+ nativeVersion: rpc.version,
45274
+ cwd,
45275
+ key: before.key,
45276
+ currentHash: before.currentHash,
45277
+ enabled: true,
45278
+ trusted: true,
45279
+ guardVerified: true,
45280
+ nativeDiscoveryVerified: true,
45281
+ nativeAdoptionVerified: false
45282
+ };
45283
+ } finally {
45284
+ await rpc.close();
45285
+ }
45286
+ }
45287
+
45026
45288
  // src/cli/index.tsx
45027
45289
  init_registry();
45028
45290
  init_installer();
@@ -45039,9 +45301,9 @@ init_local_opt_in();
45039
45301
  init_transport2();
45040
45302
  init_db();
45041
45303
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
45042
- var __dirname4 = dirname24(fileURLToPath6(import.meta.url));
45304
+ var __dirname4 = dirname25(fileURLToPath6(import.meta.url));
45043
45305
  var pkgPath = existsSync19(join31(__dirname4, "..", "package.json")) ? join31(__dirname4, "..", "package.json") : join31(__dirname4, "..", "..", "package.json");
45044
- var pkg2 = JSON.parse(readFileSync23(pkgPath, "utf-8"));
45306
+ var pkg2 = JSON.parse(readFileSync25(pkgPath, "utf-8"));
45045
45307
  var program3 = new Command;
45046
45308
  function resolveScope(options) {
45047
45309
  if (options.project)
@@ -45121,7 +45383,7 @@ async function openLocalStoreOrExit(json3) {
45121
45383
  function readToken(tokenFile) {
45122
45384
  if (tokenFile) {
45123
45385
  try {
45124
- return readFileSync23(tokenFile, "utf-8").trim();
45386
+ return readFileSync25(tokenFile, "utf-8").trim();
45125
45387
  } catch {
45126
45388
  return;
45127
45389
  }
@@ -45135,9 +45397,9 @@ function readmePreview(readme, max = 280) {
45135
45397
  return preview2 ? truncateText2(preview2, max) : undefined;
45136
45398
  }
45137
45399
  function hookSummaryLine(hook, options = {}) {
45138
- const matcher = hook.matcher ? ` ${hook.matcher}` : "";
45400
+ const matcher2 = hook.matcher ? ` ${hook.matcher}` : "";
45139
45401
  const description = options.verbose ? ` - ${truncateText2(hook.description, 110)}` : "";
45140
- return ` ${chalk2.cyan(hook.name.padEnd(17))} ${chalk2.dim(`[${hook.event}${matcher}]`)} ${chalk2.dim(hook.category)}${description}`;
45402
+ return ` ${chalk2.cyan(hook.name.padEnd(17))} ${chalk2.dim(`[${hook.event}${matcher2}]`)} ${chalk2.dim(hook.category)}${description}`;
45141
45403
  }
45142
45404
  function listStoreHooks() {
45143
45405
  const out = [];
@@ -45287,7 +45549,7 @@ program3.command("run").argument("<hook>", "Hook to run").option("--profile <id>
45287
45549
  console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
45288
45550
  process.exit(1);
45289
45551
  }
45290
- const content = readFileSync23(hookScript);
45552
+ const content = readFileSync25(hookScript);
45291
45553
  const sha4 = sha256Of2(content);
45292
45554
  const check2 = checkScriptHash2(hook, sha4);
45293
45555
  if (!check2.ok) {
@@ -45889,7 +46151,7 @@ program3.command("doctor").option("-g, --global", "Check global settings", false
45889
46151
  let wiringCount = 0;
45890
46152
  if (settingsExist) {
45891
46153
  try {
45892
- const settings = JSON.parse(readFileSync23(settingsPath, "utf-8"));
46154
+ const settings = JSON.parse(readFileSync25(settingsPath, "utf-8"));
45893
46155
  wiringCount = countSettingsWiring(settings);
45894
46156
  } catch {}
45895
46157
  }
@@ -45919,7 +46181,7 @@ program3.command("doctor").option("-g, --global", "Check global settings", false
45919
46181
  }
45920
46182
  if (meta3 && settingsExist) {
45921
46183
  try {
45922
- const settings = JSON.parse(readFileSync23(settingsPath, "utf-8"));
46184
+ const settings = JSON.parse(readFileSync25(settingsPath, "utf-8"));
45923
46185
  if (!hookRegisteredInSettings(settings, name, meta3.event, meta3.matcher)) {
45924
46186
  const eventName = meta3.event.split(":")[0];
45925
46187
  issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
@@ -46125,7 +46387,7 @@ program3.command("docs").argument("[hook]", "Hook name (shows general docs if om
46125
46387
  const readmePath = join31(hookPath, "README.md");
46126
46388
  let readme = "";
46127
46389
  if (existsSync19(readmePath)) {
46128
- readme = readFileSync23(readmePath, "utf-8");
46390
+ readme = readFileSync25(readmePath, "utf-8");
46129
46391
  }
46130
46392
  if (options.json) {
46131
46393
  console.log(JSON.stringify({ ...meta3, readme }));
@@ -46795,8 +47057,21 @@ safety.command("install <guard>").option("--target <target>", "codex, claude or
46795
47057
  if (!result.success)
46796
47058
  process.exitCode = 1;
46797
47059
  });
46798
- safety.command("verify").requiredOption("--target <target>", "sumi").option("--execution", "Also require the pinned native execution capability; no file operation is run").description("Verify a required native binding and its supervisor without running a file operation").action(async (options) => {
47060
+ safety.command("verify").requiredOption("--target <target>", "sumi or codex").option("--cwd <path>", "Codex project context to inspect (defaults to the current directory)").option("--codex-command <path>", "Installed Codex executable").option("--execution", "Also require the pinned native execution capability; no file operation is run").description("Verify a required native binding and its supervisor without running a file operation").action(async (options) => {
47061
+ if (options.target === "codex") {
47062
+ try {
47063
+ if (options.execution)
47064
+ throw new CodexSafetyError("native_unsupported");
47065
+ console.log(JSON.stringify(await verifyCodexNativeSafety({ command: options.codexCommand, cwd: options.cwd, codexHome: process.env.CODEX_HOME })));
47066
+ } catch (error61) {
47067
+ console.log(JSON.stringify({ ok: false, target: "codex", code: error61 instanceof CodexSafetyError ? error61.code : "native_failed" }));
47068
+ process.exitCode = 1;
47069
+ }
47070
+ return;
47071
+ }
46799
47072
  try {
47073
+ if (options.cwd || options.codexCommand)
47074
+ throw new NativeSafetyError("input_invalid");
46800
47075
  if (options.target !== "sumi")
46801
47076
  throw new NativeSafetyError("input_invalid");
46802
47077
  const current2 = await (options.execution ? verifyNativeSafetyExecutionRegistration() : verifyNativeSafetyRegistration());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.10.5",
3
+ "version": "0.10.6",
4
4
  "description": "Open source hooks library for AI coding agents - Install safety, quality, and automation hooks with a single command",
5
5
  "type": "module",
6
6
  "bin": {