@hasna/hooks 0.9.5 → 0.9.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.
package/bin/hooks-mcp.js CHANGED
@@ -206,6 +206,7 @@ var init_registry = __esm(() => {
206
206
  },
207
207
  {
208
208
  name: "workspace-repos-guard",
209
+ timeoutSeconds: 10,
209
210
  displayName: "Workspace Repos Guard",
210
211
  description: "Blocks writes and deletes that break the protected repo-checkout structure (~/.hasna/repos/clones canonical, plus legacy ~/workspace/repos during the migration window): writes to a protected root, top-level entry creation, non-org folders, and any delete under a protected root",
211
212
  version: "0.1.0",
@@ -224,7 +225,7 @@ var init_registry = __esm(() => {
224
225
  matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
225
226
  tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
226
227
  rewritesInput: true,
227
- timeoutSeconds: 5
228
+ timeoutSeconds: 10
228
229
  },
229
230
  {
230
231
  name: "checktests",
@@ -1129,9 +1130,9 @@ var init_migrations = __esm(() => {
1129
1130
  });
1130
1131
 
1131
1132
  // src/db/legacy-import.ts
1132
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
1133
- import { join as join5 } from "path";
1134
- import { homedir as homedir2 } from "os";
1133
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
1134
+ import { join as join6 } from "path";
1135
+ import { homedir as homedir3 } from "os";
1135
1136
  function ensureMetaTable(db) {
1136
1137
  db.exec(`
1137
1138
  CREATE TABLE IF NOT EXISTS _meta (
@@ -1154,7 +1155,7 @@ function nanoid() {
1154
1155
  function importJsonlFile(db, filePath) {
1155
1156
  let count = 0;
1156
1157
  try {
1157
- const lines = readFileSync2(filePath, "utf-8").split(`
1158
+ const lines = readFileSync3(filePath, "utf-8").split(`
1158
1159
  `).filter(Boolean);
1159
1160
  for (const line of lines) {
1160
1161
  try {
@@ -1180,7 +1181,7 @@ function importJsonlFile(db, filePath) {
1180
1181
  function importErrorsLog(db, filePath) {
1181
1182
  let count = 0;
1182
1183
  try {
1183
- const lines = readFileSync2(filePath, "utf-8").split(`
1184
+ const lines = readFileSync3(filePath, "utf-8").split(`
1184
1185
  `).filter(Boolean);
1185
1186
  const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
1186
1187
  for (const line of lines) {
@@ -1205,25 +1206,25 @@ function importErrorsLog(db, filePath) {
1205
1206
  } catch {}
1206
1207
  return count;
1207
1208
  }
1208
- function runLegacyImport(db, homeDir = homedir2()) {
1209
+ function runLegacyImport(db, homeDir = homedir3()) {
1209
1210
  try {
1210
1211
  if (isAlreadyDone(db))
1211
1212
  return;
1212
1213
  let total = 0;
1213
- const claudeProjectsDir = join5(homeDir, ".claude", "projects");
1214
- if (existsSync4(claudeProjectsDir)) {
1214
+ const claudeProjectsDir = join6(homeDir, ".claude", "projects");
1215
+ if (existsSync5(claudeProjectsDir)) {
1215
1216
  try {
1216
1217
  const projectDirs = readdirSync2(claudeProjectsDir);
1217
1218
  for (const dir of projectDirs) {
1218
- const projectDir = join5(claudeProjectsDir, dir);
1219
+ const projectDir = join6(claudeProjectsDir, dir);
1219
1220
  try {
1220
1221
  const files = readdirSync2(projectDir);
1221
1222
  for (const file of files) {
1222
1223
  if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
1223
- total += importJsonlFile(db, join5(projectDir, file));
1224
+ total += importJsonlFile(db, join6(projectDir, file));
1224
1225
  }
1225
1226
  if (file === "errors.log") {
1226
- total += importErrorsLog(db, join5(projectDir, file));
1227
+ total += importErrorsLog(db, join6(projectDir, file));
1227
1228
  }
1228
1229
  }
1229
1230
  } catch {}
@@ -1270,8 +1271,8 @@ __export(exports_db, {
1270
1271
  allowLocalStore: () => allowLocalStore
1271
1272
  });
1272
1273
  import { Database } from "bun:sqlite";
1273
- import { existsSync as existsSync5, mkdirSync as mkdirSync2, cpSync } from "fs";
1274
- import { join as join6 } from "path";
1274
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, cpSync } from "fs";
1275
+ import { join as join7 } from "path";
1275
1276
  function refuseLocalStore(message) {
1276
1277
  localStoreRefusal = message;
1277
1278
  }
@@ -1286,8 +1287,8 @@ function localStoreRefusalMessage() {
1286
1287
  }
1287
1288
  function resolveDataDir() {
1288
1289
  const effective = getEffectiveDataRoot();
1289
- const oldDir = join6(getHomeDir(), ".hooks");
1290
- if (!existsSync5(effective) && existsSync5(oldDir)) {
1290
+ const oldDir = join7(getHomeDir(), ".hooks");
1291
+ if (!existsSync6(effective) && existsSync6(oldDir)) {
1291
1292
  mkdirSync2(effective, { recursive: true });
1292
1293
  cpSync(oldDir, effective, { recursive: true });
1293
1294
  }
@@ -1298,11 +1299,11 @@ function getDbPath() {
1298
1299
  if (explicitDb)
1299
1300
  return explicitDb;
1300
1301
  const dataDir2 = resolveDataDir();
1301
- return join6(dataDir2, "hooks.db");
1302
+ return join7(dataDir2, "hooks.db");
1302
1303
  }
1303
1304
  function ensureDir(dbPath) {
1304
1305
  const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
1305
- if (dir && !existsSync5(dir)) {
1306
+ if (dir && !existsSync6(dir)) {
1306
1307
  mkdirSync2(dir, { recursive: true });
1307
1308
  }
1308
1309
  }
@@ -1312,7 +1313,7 @@ function getDb() {
1312
1313
  if (instance)
1313
1314
  return instance;
1314
1315
  const dbPath = getDbPath();
1315
- const isNew = dbPath === ":memory:" || !existsSync5(dbPath);
1316
+ const isNew = dbPath === ":memory:" || !existsSync6(dbPath);
1316
1317
  ensureDir(dbPath);
1317
1318
  instance = new Database(dbPath);
1318
1319
  instance.exec("PRAGMA busy_timeout=5000");
@@ -1421,10 +1422,10 @@ var init_redact = __esm(() => {
1421
1422
  // ../contracts/dist/client/transport.js
1422
1423
  import { isIP } from "net";
1423
1424
  import { spawnSync } from "child_process";
1424
- import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync22 } from "fs";
1425
+ import { closeSync as closeSync22, fstatSync as fstatSync2, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
1425
1426
  import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
1426
1427
  import { hostname as osHostname } from "os";
1427
- import { isAbsolute as isAbsolute2, join as join22 } from "path";
1428
+ import { isAbsolute as isAbsolute3, join as join22 } from "path";
1428
1429
  import { createRequire } from "module";
1429
1430
  function envToken(name) {
1430
1431
  return name.toUpperCase().replace(/-/g, "_");
@@ -1448,7 +1449,7 @@ function homeDir(env) {
1448
1449
  }
1449
1450
  function absoluteOverride(env, key) {
1450
1451
  const value = env[key]?.trim();
1451
- return value && isAbsolute2(value) ? value : null;
1452
+ return value && isAbsolute3(value) ? value : null;
1452
1453
  }
1453
1454
  function hasnaHomeDir(env) {
1454
1455
  const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
@@ -1494,9 +1495,9 @@ function parseEnvFile(text) {
1494
1495
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
1495
1496
  continue;
1496
1497
  let value = withoutExport.slice(equals + 1).trim();
1497
- const quote = value[0];
1498
- if (quote === '"' || quote === "'") {
1499
- if (value.length < 2 || !value.endsWith(quote)) {
1498
+ const quote2 = value[0];
1499
+ if (quote2 === '"' || quote2 === "'") {
1500
+ if (value.length < 2 || !value.endsWith(quote2)) {
1500
1501
  unusable.add(key);
1501
1502
  continue;
1502
1503
  }
@@ -1525,7 +1526,7 @@ function readAppConfigFile(path) {
1525
1526
  };
1526
1527
  let fd = -1;
1527
1528
  try {
1528
- fd = openSync2(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
1529
+ fd = openSync22(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
1529
1530
  } catch (error) {
1530
1531
  const code = error.code;
1531
1532
  if (code === "ENOENT" || code === "ENOTDIR")
@@ -1554,7 +1555,7 @@ function readAppConfigFile(path) {
1554
1555
  return parseEnvFile(bytes.toString("utf8"));
1555
1556
  } finally {
1556
1557
  if (fd !== -1)
1557
- closeSync2(fd);
1558
+ closeSync22(fd);
1558
1559
  }
1559
1560
  }
1560
1561
  function readCredentialFile(path, apiKeyKeys, pointerKey) {
@@ -2228,12 +2229,12 @@ var init_local_opt_in = __esm(() => {
2228
2229
  });
2229
2230
 
2230
2231
  // src/lib/hook-env.ts
2231
- import { dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
2232
- import { existsSync as existsSync10, realpathSync as realpathSync2, statSync } from "fs";
2232
+ import { dirname as dirname6, isAbsolute as isAbsolute4 } from "path";
2233
+ import { existsSync as existsSync11, realpathSync as realpathSync3, statSync } from "fs";
2233
2234
  import { tmpdir } from "os";
2234
2235
  function isDeniedEnvName(name) {
2235
2236
  const upper = name.toUpperCase();
2236
- if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
2237
+ if (DENY_PREFIXES.some((prefix2) => upper.startsWith(prefix2)))
2237
2238
  return true;
2238
2239
  if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
2239
2240
  return true;
@@ -2242,13 +2243,13 @@ function isDeniedEnvName(name) {
2242
2243
  function isInterpreterInjectionEnvName(name) {
2243
2244
  if (INTERPRETER_INJECTION.has(name))
2244
2245
  return true;
2245
- return INTERPRETER_INJECTION_PREFIXES.some((prefix) => name.startsWith(prefix));
2246
+ return INTERPRETER_INJECTION_PREFIXES.some((prefix2) => name.startsWith(prefix2));
2246
2247
  }
2247
2248
  function isBashFunctionEnvName(name) {
2248
2249
  return name.startsWith("BASH_FUNC_");
2249
2250
  }
2250
2251
  function isUnsafePathEntry(entry, home) {
2251
- if (entry === "" || !isAbsolute3(entry))
2252
+ if (entry === "" || !isAbsolute4(entry))
2252
2253
  return true;
2253
2254
  if (home && (entry === home || entry.startsWith(home + "/")))
2254
2255
  return true;
@@ -2258,7 +2259,7 @@ function isUnsafePathEntry(entry, home) {
2258
2259
  return true;
2259
2260
  const canonical = (path) => {
2260
2261
  try {
2261
- return realpathSync2(path);
2262
+ return realpathSync3(path);
2262
2263
  } catch {
2263
2264
  return path;
2264
2265
  }
@@ -2277,9 +2278,9 @@ function isUnsafePathEntry(entry, home) {
2277
2278
  }
2278
2279
  function sanitizeHookPath(pathValue, home) {
2279
2280
  const baseline = PATH_BASELINE.slice();
2280
- if (process.platform === "darwin" && existsSync10("/opt/homebrew/bin"))
2281
+ if (process.platform === "darwin" && existsSync11("/opt/homebrew/bin"))
2281
2282
  baseline.push("/opt/homebrew/bin");
2282
- const bunDir = dirname5(process.execPath);
2283
+ const bunDir = dirname6(process.execPath);
2283
2284
  const seen = new Set;
2284
2285
  const out = [];
2285
2286
  for (const dir of baseline) {
@@ -2404,7 +2405,7 @@ __export(exports_run, {
2404
2405
  });
2405
2406
  import { randomBytes as randomBytes2 } from "crypto";
2406
2407
  import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
2407
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join9 } from "path";
2408
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute5, join as join10 } from "path";
2408
2409
  function extensionOf(scriptPath) {
2409
2410
  const base = basename2(scriptPath);
2410
2411
  const dot = base.lastIndexOf(".");
@@ -2473,9 +2474,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
2473
2474
  return chunks.join("");
2474
2475
  }
2475
2476
  async function executeVerifiedScript(options) {
2476
- const scriptDir = dirname6(options.scriptPath);
2477
+ const scriptDir = dirname7(options.scriptPath);
2477
2478
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
2478
- const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
2479
+ const tempPath = join10(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
2479
2480
  let proc = null;
2480
2481
  let timedOut = false;
2481
2482
  const timeoutTimer = options.timeout ? setTimeout(() => {
@@ -2484,7 +2485,7 @@ async function executeVerifiedScript(options) {
2484
2485
  }, options.timeout) : null;
2485
2486
  try {
2486
2487
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
2487
- proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
2488
+ proc = Bun.spawn([...interpreter.command, isAbsolute5(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
2488
2489
  stdin: new Response(options.stdin),
2489
2490
  stdout: "pipe",
2490
2491
  stderr: "pipe",
@@ -2683,19 +2684,150 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
2683
2684
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2684
2685
  import { z as z2 } from "zod";
2685
2686
  init_registry();
2686
- import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
2687
- import { join as join10, dirname as dirname7 } from "path";
2687
+ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
2688
+ import { join as join11, dirname as dirname8 } from "path";
2688
2689
  import { fileURLToPath as fileURLToPath3 } from "url";
2689
2690
 
2690
2691
  // src/lib/installer.ts
2691
2692
  init_registry();
2692
2693
  init_resolve();
2693
2694
  init_manifest();
2694
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
2695
- import { join as join7, dirname as dirname4 } from "path";
2696
- import { homedir as homedir3 } from "os";
2695
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
2696
+ import { join as join8, dirname as dirname5 } from "path";
2697
+ import { homedir as homedir4 } from "os";
2697
2698
  import { fileURLToPath as fileURLToPath2 } from "url";
2698
2699
 
2700
+ // src/lib/native-safety.ts
2701
+ import { createHash } from "crypto";
2702
+ import { closeSync, constants, existsSync as existsSync4, fstatSync, lstatSync, openSync, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
2703
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join5 } from "path";
2704
+ import { homedir as homedir2 } from "os";
2705
+ function isNativeSafetyName(name) {
2706
+ return name === "trash-guard" || name === "workspace-repos-guard";
2707
+ }
2708
+ var supervisorSource = String.raw`async function supervise(encoded) {
2709
+ const fs = require("node:fs");
2710
+ const path = require("node:path");
2711
+ const crypto = require("node:crypto");
2712
+ const cp = require("node:child_process");
2713
+ const fail = () => { process.stderr.write("Native safety guard refused: verification or execution failed.\n"); process.exit(2); };
2714
+ try {
2715
+ const b = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
2716
+ const check = (file, hash, maximum) => {
2717
+ if (!path.isAbsolute(file) || fs.realpathSync(file) !== file) throw Error();
2718
+ for (let parent = path.dirname(file);;) {
2719
+ const s = fs.lstatSync(parent);
2720
+ if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022)) throw Error();
2721
+ if (parent === path.dirname(parent)) break;
2722
+ parent = path.dirname(parent);
2723
+ }
2724
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
2725
+ try {
2726
+ const s = fs.fstatSync(fd);
2727
+ if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022) || s.size > maximum) throw Error();
2728
+ const bytes = fs.readFileSync(fd);
2729
+ if (crypto.createHash("sha256").update(bytes).digest("hex") !== hash) throw Error();
2730
+ return bytes;
2731
+ } finally { fs.closeSync(fd); }
2732
+ };
2733
+ check(b.runtime, b.runtimeSHA256, 200_000_000);
2734
+ const bytes = check(b.worker, b.workerSHA256, 256_000);
2735
+ const deadline = setTimeout(fail, 1500);
2736
+ const chunks = []; let size = 0;
2737
+ for await (const chunk of process.stdin) {
2738
+ size += chunk.length;
2739
+ if (size > 1_048_576) throw Error();
2740
+ chunks.push(chunk);
2741
+ }
2742
+ clearTimeout(deadline);
2743
+ const input = Buffer.concat(chunks);
2744
+ const child = cp.spawnSync(b.runtime, ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", "--eval", bytes.toString("utf8"), b.name], {
2745
+ input, env: { HOME: b.home, PATH: b.path }, cwd: "/", timeout: 2500, killSignal: "SIGKILL", maxBuffer: 1_048_576,
2746
+ });
2747
+ if (child.error || child.status !== 0 || child.signal || child.stderr.length) throw Error();
2748
+ const result = JSON.parse(child.stdout.toString("utf8"));
2749
+ if (!result || Array.isArray(result) || Object.keys(result).length !== 1) throw Error();
2750
+ if (result.verdict === null) return;
2751
+ const output = result.verdict?.hookSpecificOutput;
2752
+ if (!output || Object.keys(result.verdict).length !== 1 || output.hookEventName !== "PreToolUse") throw Error();
2753
+ if (output.permissionDecision === "deny") {
2754
+ if (Object.keys(output).sort().join() !== "hookEventName,permissionDecision,permissionDecisionReason" || typeof output.permissionDecisionReason !== "string" || !output.permissionDecisionReason.trim()) throw Error();
2755
+ } else if (output.permissionDecision === "allow") {
2756
+ if (b.name !== "trash-guard" || Object.keys(output).sort().join() !== "hookEventName,permissionDecision,updatedInput" || !output.updatedInput || typeof output.updatedInput.command !== "string" || !output.updatedInput.command.trim()) throw Error();
2757
+ } else throw Error();
2758
+ process.stdout.write(JSON.stringify(result.verdict));
2759
+ } catch { fail(); }
2760
+ }`;
2761
+ var quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'";
2762
+ var program = `(${supervisorSource})(process.argv[1]);`;
2763
+ var shell = `cd / || exit 2
2764
+ exec 3<&0 || exit 2
2765
+ /usr/bin/env -i HOME="$3" PATH="$4" "$2" --config=/dev/null --no-env-file --no-macros --no-install --eval ${quote(program)} "$1" <&3 3<&- 2>/dev/null &
2766
+ runner=$!
2767
+ exec 3<&-
2768
+ (trap 'kill "$sleeper" 2>/dev/null; exit 0' TERM; /bin/sleep 6 & sleeper=$!; wait "$sleeper"; kill -KILL "$runner" 2>/dev/null) &
2769
+ watchdog=$!
2770
+ wait "$runner"; result=$?
2771
+ kill "$watchdog" 2>/dev/null; wait "$watchdog" 2>/dev/null
2772
+ if [ "$result" -ne 0 ]; then printf '%s\\n' 'Native safety guard refused: verification or execution failed.' >&2; exit 2; fi`;
2773
+ var prefix = `/usr/bin/env -i /bin/sh -c ${quote(shell)} hasna-native-safety-v1 `;
2774
+ function validateBinding(b) {
2775
+ if (!b || !isNativeSafetyName(b.name) || ![b.runtime, b.worker, b.home].every((p) => typeof p === "string" && isAbsolute2(p) && !/[\0\r\n]/.test(p)) || ![b.runtimeSHA256, b.workerSHA256].every((h) => /^[0-9a-f]{64}$/.test(h)) || typeof b.path !== "string" || b.path.split(":").some((p) => !isAbsolute2(p) || /[\0\r\n]/.test(p)))
2776
+ throw new Error("Invalid native safety binding");
2777
+ }
2778
+ function buildNativeSafetyCommand(binding) {
2779
+ validateBinding(binding);
2780
+ const encoded = Buffer.from(JSON.stringify(binding)).toString("base64url");
2781
+ return prefix + [encoded, binding.runtime, binding.home, binding.path].map(quote).join(" ");
2782
+ }
2783
+ function nativeSafetyRegistration(command) {
2784
+ if (!command.startsWith(prefix))
2785
+ return;
2786
+ const encoded = command.slice(prefix.length).match(/^'([A-Za-z0-9_-]+)' /)?.[1];
2787
+ if (!encoded || encoded.length > 16384)
2788
+ return;
2789
+ try {
2790
+ const binding = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
2791
+ return buildNativeSafetyCommand(binding) === command ? binding.name : undefined;
2792
+ } catch {
2793
+ return;
2794
+ }
2795
+ }
2796
+ function pinEligibleFile(file, maximum, executable = false) {
2797
+ if (!isAbsolute2(file) || realpathSync2(file) !== file)
2798
+ throw new Error("Native safety requires canonical files");
2799
+ for (let parent = dirname3(file);; ) {
2800
+ const s = lstatSync(parent);
2801
+ if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || s.mode & 18)
2802
+ throw new Error("Native safety requires trusted, non-writable file ancestry");
2803
+ if (parent === dirname3(parent))
2804
+ break;
2805
+ parent = dirname3(parent);
2806
+ }
2807
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
2808
+ try {
2809
+ const s = fstatSync(fd);
2810
+ if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || s.mode & 18 || s.size > maximum || executable && !(s.mode & 64))
2811
+ throw new Error("Native safety requires an eligible installed file");
2812
+ const bytes = readFileSync2(fd);
2813
+ if (bytes.length > maximum)
2814
+ throw new Error("Native safety file exceeds its bound");
2815
+ return createHash("sha256").update(bytes).digest("hex");
2816
+ } finally {
2817
+ closeSync(fd);
2818
+ }
2819
+ }
2820
+ function installedNativeSafetyCommand(name) {
2821
+ const home = realpathSync2(homedir2());
2822
+ const runtime = realpathSync2(process.execPath);
2823
+ const candidates = [join5(import.meta.dir, "../../bin/native-safety-entry.js"), join5(import.meta.dir, "../bin/native-safety-entry.js"), join5(import.meta.dir, "native-safety-entry.js")];
2824
+ const worker = candidates.find(existsSync4);
2825
+ if (!worker)
2826
+ throw new Error("Bundled native safety runner is missing; build or reinstall @hasna/hooks before installing its guards.");
2827
+ const canonicalWorker = realpathSync2(worker);
2828
+ return buildNativeSafetyCommand({ name, runtime, runtimeSHA256: pinEligibleFile(runtime, 200000000, true), worker: canonicalWorker, workerSHA256: pinEligibleFile(canonicalWorker, 256000), home, path: `${dirname3(runtime)}:${join5(home, ".bun/bin")}:/usr/local/bin:/usr/bin:/bin` });
2829
+ }
2830
+
2699
2831
  // src/lib/registration.ts
2700
2832
  init_resolve();
2701
2833
 
@@ -2760,6 +2892,9 @@ var REGISTRATION_COMMAND_RE = /^(?:hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?|
2760
2892
  function registrationName(command) {
2761
2893
  if (typeof command !== "string")
2762
2894
  return;
2895
+ const native = nativeSafetyRegistration(command);
2896
+ if (native)
2897
+ return native;
2763
2898
  if (/^hooks run mementos-context(?:\s|$)/.test(command.trim())) {
2764
2899
  try {
2765
2900
  retainedMementosRegistration([command.trim()]);
@@ -2814,8 +2949,7 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
2814
2949
  if (!Array.isArray(hooks))
2815
2950
  return false;
2816
2951
  return hooks.some((entry) => entry?.hooks?.some((h) => {
2817
- const match = h?.command?.match(/^hooks run ([\w-]+)/);
2818
- if (!match || match[1] !== name)
2952
+ if (registrationName(h?.command) !== name)
2819
2953
  return false;
2820
2954
  if (!matcher)
2821
2955
  return true;
@@ -2827,13 +2961,13 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
2827
2961
  }
2828
2962
 
2829
2963
  // src/lib/store.ts
2830
- import { createHash } from "crypto";
2831
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync } from "fs";
2964
+ import { createHash as createHash2 } from "crypto";
2965
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync } from "fs";
2832
2966
  init_db();
2833
2967
  init_config();
2834
2968
  import { randomBytes } from "crypto";
2835
2969
  function sha256Of(input) {
2836
- return createHash("sha256").update(input).digest("hex");
2970
+ return createHash2("sha256").update(input).digest("hex");
2837
2971
  }
2838
2972
  function storeDb() {
2839
2973
  return isLocalStoreRefused() ? null : getDb();
@@ -2889,11 +3023,11 @@ function assertLockShape(parsed, path) {
2889
3023
  }
2890
3024
  function readLock() {
2891
3025
  const path = getLockPath();
2892
- if (!existsSync6(path))
3026
+ if (!existsSync7(path))
2893
3027
  return { hooks: {} };
2894
3028
  let raw;
2895
3029
  try {
2896
- raw = readFileSync3(path, "utf-8");
3030
+ raw = readFileSync4(path, "utf-8");
2897
3031
  } catch {
2898
3032
  throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
2899
3033
  }
@@ -2965,16 +3099,16 @@ function checkScriptHash(name, actual) {
2965
3099
  }
2966
3100
 
2967
3101
  // src/lib/codex-settings.ts
2968
- import { closeSync, existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
2969
- import { dirname as dirname3 } from "path";
3102
+ 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";
3103
+ import { dirname as dirname4 } from "path";
2970
3104
  import { randomUUID } from "crypto";
2971
3105
  var snapshots = new WeakMap;
2972
3106
  function current(path) {
2973
3107
  try {
2974
- const stat = lstatSync(path);
3108
+ const stat = lstatSync2(path);
2975
3109
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
2976
3110
  throw new Error("invalid_file");
2977
- return readFileSync4(path, "utf8");
3111
+ return readFileSync5(path, "utf8");
2978
3112
  } catch (error) {
2979
3113
  if (error.code === "ENOENT")
2980
3114
  return null;
@@ -3006,11 +3140,11 @@ function writeCodexSettings(path, settings) {
3006
3140
  const snapshot = snapshots.get(settings);
3007
3141
  if (!snapshot || snapshot.path !== path)
3008
3142
  throw new Error("Codex settings require a complete current read before writing.");
3009
- mkdirSync4(dirname3(path), { recursive: true });
3143
+ mkdirSync4(dirname4(path), { recursive: true });
3010
3144
  const lock = `${path}.hooks-write.lock`;
3011
3145
  let fd;
3012
3146
  try {
3013
- fd = openSync(lock, "wx", 384);
3147
+ fd = openSync2(lock, "wx", 384);
3014
3148
  } catch {
3015
3149
  throw new Error("Codex settings are being edited; retry after the active writer finishes.");
3016
3150
  }
@@ -3026,16 +3160,16 @@ function writeCodexSettings(path, settings) {
3026
3160
  renameSync2(temp, path);
3027
3161
  snapshots.set(settings, { path, raw: next });
3028
3162
  } finally {
3029
- if (existsSync7(temp))
3163
+ if (existsSync8(temp))
3030
3164
  unlinkSync(temp);
3031
- closeSync(fd);
3165
+ closeSync2(fd);
3032
3166
  unlinkSync(lock);
3033
3167
  }
3034
3168
  }
3035
3169
 
3036
3170
  // src/lib/installer.ts
3037
- var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
3038
- var HOOKS_DIR = existsSync8(join7(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join7(__dirname2, "..", "..", "hooks") : join7(__dirname2, "..", "hooks");
3171
+ var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
3172
+ var HOOKS_DIR = existsSync9(join8(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join8(__dirname2, "..", "..", "hooks") : join8(__dirname2, "..", "hooks");
3039
3173
  function normalizeHookName(name) {
3040
3174
  return name.startsWith("hook-") ? name : `hook-${name}`;
3041
3175
  }
@@ -3047,8 +3181,7 @@ function removeHookEntriesByName(entries, hookName) {
3047
3181
  if (!Array.isArray(entry.hooks))
3048
3182
  return [entry];
3049
3183
  const kept = entry.hooks.filter((h) => {
3050
- const match = h.command?.match(/^hooks run ([\w-]+)/);
3051
- return !(match && match[1] === hookName);
3184
+ return registrationName(h.command) !== hookName;
3052
3185
  });
3053
3186
  return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
3054
3187
  });
@@ -3124,29 +3257,29 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
3124
3257
  return globalOverride;
3125
3258
  const dir = getTargetSettingsDir(target);
3126
3259
  if (target === "codex")
3127
- return join7(scope === "project" ? process.cwd() : homedir3(), dir, "hooks.json");
3260
+ return join8(scope === "project" ? process.cwd() : homedir4(), dir, "hooks.json");
3128
3261
  if (scope === "project") {
3129
- return target === "codewith" ? join7(process.cwd(), dir, "config.toml") : join7(process.cwd(), dir, "settings.json");
3262
+ return target === "codewith" ? join8(process.cwd(), dir, "config.toml") : join8(process.cwd(), dir, "settings.json");
3130
3263
  }
3131
- return target === "codewith" ? join7(homedir3(), dir, "config.toml") : join7(homedir3(), dir, "settings.json");
3264
+ return target === "codewith" ? join8(homedir4(), dir, "config.toml") : join8(homedir4(), dir, "settings.json");
3132
3265
  }
3133
3266
  function getHookPath(name) {
3134
3267
  const shortName = shortHookName(name);
3135
3268
  const resolved = resolveHookDir(shortName);
3136
3269
  if (resolved)
3137
3270
  return resolved;
3138
- return join7(HOOKS_DIR, normalizeHookName(shortName));
3271
+ return join8(HOOKS_DIR, normalizeHookName(shortName));
3139
3272
  }
3140
3273
  function hookExists(name) {
3141
- return existsSync8(getHookPath(name));
3274
+ return existsSync9(getHookPath(name));
3142
3275
  }
3143
3276
  function readSettings(scope = "global", target = "claude") {
3144
3277
  const path = getSettingsPath(scope, target);
3145
3278
  if (target === "codex")
3146
3279
  return readCodexSettings(path);
3147
3280
  try {
3148
- if (existsSync8(path)) {
3149
- return JSON.parse(readFileSync5(path, "utf-8"));
3281
+ if (existsSync9(path)) {
3282
+ return JSON.parse(readFileSync6(path, "utf-8"));
3150
3283
  }
3151
3284
  } catch (error) {
3152
3285
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -3157,8 +3290,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
3157
3290
  const path = getSettingsPath(scope, target);
3158
3291
  if (target === "codex")
3159
3292
  return writeCodexSettings(path, settings);
3160
- const dir = dirname4(path);
3161
- if (!existsSync8(dir)) {
3293
+ const dir = dirname5(path);
3294
+ if (!existsSync9(dir)) {
3162
3295
  mkdirSync5(dir, { recursive: true });
3163
3296
  }
3164
3297
  writeFileSync4(path, JSON.stringify(settings, null, 2) + `
@@ -3186,6 +3319,9 @@ function codewithTimeout(name) {
3186
3319
  switch (name) {
3187
3320
  case "mementos-context":
3188
3321
  return 12;
3322
+ case "trash-guard":
3323
+ case "workspace-repos-guard":
3324
+ return 10;
3189
3325
  case "knowledge-context":
3190
3326
  return 6;
3191
3327
  case "session-start":
@@ -3195,7 +3331,6 @@ function codewithTimeout(name) {
3195
3331
  case "prompt-guard":
3196
3332
  return 3;
3197
3333
  case "worktree-guard":
3198
- case "trash-guard":
3199
3334
  case "stop-sync":
3200
3335
  return 5;
3201
3336
  default:
@@ -3224,11 +3359,13 @@ function codewithStatusMessage(name) {
3224
3359
  }
3225
3360
  function buildCodewithTomlFragment(name, profile, mementos) {
3226
3361
  const shortName = shortHookName(name);
3227
- const meta = resolveHookMeta(shortName);
3362
+ if (isNativeSafetyName(shortName) && profile)
3363
+ throw new Error("Bundled native safety guards do not select registry profiles");
3364
+ const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
3228
3365
  if (!meta)
3229
3366
  throw new Error(`Hook '${shortName}' not found`);
3230
3367
  validateMementosRegistration(shortName, mementos);
3231
- const command = (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
3368
+ const command = isNativeSafetyName(shortName) ? installedNativeSafetyCommand(shortName) : (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
3232
3369
  const matcher = codewithMatcher(meta.matcher);
3233
3370
  const fragments = [];
3234
3371
  for (const event of getHookEvents(meta)) {
@@ -3253,17 +3390,17 @@ function buildCodewithTomlFragment(name, profile, mementos) {
3253
3390
  function readCodewithConfig(scope, configPath) {
3254
3391
  const path = getSettingsPath(scope, "codewith", configPath);
3255
3392
  try {
3256
- return existsSync8(path) ? readFileSync5(path, "utf-8") : "";
3393
+ return existsSync9(path) ? readFileSync6(path, "utf-8") : "";
3257
3394
  } catch {
3258
3395
  return "";
3259
3396
  }
3260
3397
  }
3261
3398
  function appendCodewithFragment(fragment, scope, configPath) {
3262
3399
  const path = getSettingsPath(scope, "codewith", configPath);
3263
- const dir = dirname4(path);
3264
- if (!existsSync8(dir))
3400
+ const dir = dirname5(path);
3401
+ if (!existsSync9(dir))
3265
3402
  mkdirSync5(dir, { recursive: true });
3266
- const existing = existsSync8(path) ? readFileSync5(path, "utf-8") : "";
3403
+ const existing = existsSync9(path) ? readFileSync6(path, "utf-8") : "";
3267
3404
  const sep2 = existing.trim() ? `
3268
3405
 
3269
3406
  ` : "";
@@ -3271,7 +3408,7 @@ function appendCodewithFragment(fragment, scope, configPath) {
3271
3408
  return path;
3272
3409
  }
3273
3410
  function detectConflict(name, scope, target) {
3274
- const meta = resolveHookMeta(name);
3411
+ const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
3275
3412
  if (!meta || !meta.matcher)
3276
3413
  return;
3277
3414
  const events = new Set(getHookEvents(meta));
@@ -3291,7 +3428,7 @@ function detectConflict(name, scope, target) {
3291
3428
  return;
3292
3429
  }
3293
3430
  function detectRewriteConflict(name, scope, target, lookup = getHook) {
3294
- const meta = resolveHookMeta(name);
3431
+ const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
3295
3432
  if (!meta?.matcher || !meta.rewritesInput)
3296
3433
  return;
3297
3434
  if (!getHookEvents(meta).includes("PreToolUse"))
@@ -3312,7 +3449,10 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
3312
3449
  }
3313
3450
  function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
3314
3451
  const shortName = shortHookName(name);
3315
- if (!hookExists(shortName)) {
3452
+ if (isNativeSafetyName(shortName) && (profile || target === "gemini" || target === "codewith" && codewithMode === "write")) {
3453
+ return { hook: shortName, success: false, error: "Bundled native safety guards support Claude/Codex and managed Codewith fragments, without registry profiles.", target };
3454
+ }
3455
+ if (!isNativeSafetyName(shortName) && !hookExists(shortName)) {
3316
3456
  return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
3317
3457
  }
3318
3458
  if (target === "codewith") {
@@ -3404,7 +3544,7 @@ function installHook(name, options = {}) {
3404
3544
  }
3405
3545
  if (target === "all") {
3406
3546
  const shortName = shortHookName(name);
3407
- const meta = resolveHookMeta(shortName);
3547
+ const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
3408
3548
  if (meta) {
3409
3549
  const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
3410
3550
  if (unsupportedTargets.length > 0) {
@@ -3437,7 +3577,7 @@ function installHook(name, options = {}) {
3437
3577
  return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
3438
3578
  }
3439
3579
  function registerHook(name, scope = "global", target = "claude", profile, mementos) {
3440
- const meta = resolveHookMeta(name);
3580
+ const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
3441
3581
  if (!meta)
3442
3582
  return;
3443
3583
  const eventKeys = getHookEvents(meta).map((event) => {
@@ -3454,8 +3594,17 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
3454
3594
  const settings = readSettings(scope, target);
3455
3595
  if (!settings.hooks)
3456
3596
  settings.hooks = {};
3597
+ if (isNativeSafetyName(name)) {
3598
+ for (const groups of Object.values(settings.hooks))
3599
+ for (const group of groups)
3600
+ for (const hook of group.hooks ?? []) {
3601
+ if (typeof hook.command === "string" && /(?:^|\s)hasna-native-safety-v\d+(?:\s|$)/.test(hook.command) && !registrationName(hook.command)) {
3602
+ throw new Error("Unknown managed safety definition; reconcile its exact version before replacing it.");
3603
+ }
3604
+ }
3605
+ }
3457
3606
  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;
3458
- const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
3607
+ const hookCommand = isNativeSafetyName(name) ? installedNativeSafetyCommand(name) : (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
3459
3608
  if (target === "codex") {
3460
3609
  const positions = [];
3461
3610
  for (const [event, groups] of Object.entries(settings.hooks)) {
@@ -3463,9 +3612,11 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
3463
3612
  for (let index = 0;index < entry.hooks.length; index++) {
3464
3613
  const command = entry.hooks[index]?.command;
3465
3614
  const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
3466
- if (match && (match[1] ?? match[2]) === name) {
3615
+ if (match && (match[1] ?? match[2]) === name || registrationName(command) === name) {
3467
3616
  if (registrationName(command) !== name)
3468
3617
  throw new Error("Unrecognized Codex registration; reconcile it before updating this hook.");
3618
+ if (isNativeSafetyName(name) && entry.hooks[index].async !== undefined && entry.hooks[index].async !== false)
3619
+ throw new Error("Asynchronous safety hooks cannot block; reconcile this registration before updating it.");
3469
3620
  positions.push({ event, entry, index });
3470
3621
  }
3471
3622
  }
@@ -3526,12 +3677,20 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
3526
3677
  if (target === "codewith") {
3527
3678
  const config = readCodewithConfig(scope);
3528
3679
  const registered2 = [];
3529
- const re = /^\s*command\s*=\s*["']([^"'\n]+)["']\s*$/gm;
3530
- let match;
3531
- while (match = re.exec(config)) {
3532
- const name = registrationName(match[1]);
3533
- if (name)
3534
- registered2.push(name);
3680
+ try {
3681
+ const parsed = Bun.TOML.parse(config);
3682
+ for (const groups of Object.values(parsed.hooks ?? {})) {
3683
+ if (!Array.isArray(groups))
3684
+ continue;
3685
+ for (const group of groups)
3686
+ for (const hook of group.hooks ?? []) {
3687
+ const name = registrationName(hook.command);
3688
+ if (name)
3689
+ registered2.push(name);
3690
+ }
3691
+ }
3692
+ } catch {
3693
+ return [];
3535
3694
  }
3536
3695
  return [...new Set(registered2)];
3537
3696
  }
@@ -3577,12 +3736,12 @@ function removeHookForTarget(name, scope, target) {
3577
3736
 
3578
3737
  // src/lib/profiles.ts
3579
3738
  init_app_home();
3580
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
3581
- import { join as join8 } from "path";
3739
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
3740
+ import { join as join9 } from "path";
3582
3741
  function getProfilesDir() {
3583
- const newDir = join8(getEffectiveDataRoot(), "profiles");
3584
- const oldDir = join8(getHomeDir(), ".hooks", "profiles");
3585
- if (!existsSync9(newDir) && existsSync9(oldDir)) {
3742
+ const newDir = join9(getEffectiveDataRoot(), "profiles");
3743
+ const oldDir = join9(getHomeDir(), ".hooks", "profiles");
3744
+ if (!existsSync10(newDir) && existsSync10(oldDir)) {
3586
3745
  mkdirSync6(newDir, { recursive: true });
3587
3746
  cpSync2(oldDir, newDir, { recursive: true });
3588
3747
  }
@@ -3590,12 +3749,12 @@ function getProfilesDir() {
3590
3749
  }
3591
3750
  function ensureProfilesDir() {
3592
3751
  const dir = getProfilesDir();
3593
- if (!existsSync9(dir)) {
3752
+ if (!existsSync10(dir)) {
3594
3753
  mkdirSync6(dir, { recursive: true });
3595
3754
  }
3596
3755
  }
3597
3756
  function profilePath(id) {
3598
- return join8(getProfilesDir(), `${id}.json`);
3757
+ return join9(getProfilesDir(), `${id}.json`);
3599
3758
  }
3600
3759
  function shortUuid() {
3601
3760
  return crypto.randomUUID().slice(0, 8);
@@ -3621,23 +3780,23 @@ function createProfile(input) {
3621
3780
  function getProfile(id) {
3622
3781
  const path = profilePath(id);
3623
3782
  try {
3624
- if (!existsSync9(path))
3783
+ if (!existsSync10(path))
3625
3784
  return null;
3626
- return JSON.parse(readFileSync6(path, "utf-8"));
3785
+ return JSON.parse(readFileSync7(path, "utf-8"));
3627
3786
  } catch {
3628
3787
  return null;
3629
3788
  }
3630
3789
  }
3631
3790
  function listProfiles() {
3632
3791
  const dir = getProfilesDir();
3633
- if (!existsSync9(dir))
3792
+ if (!existsSync10(dir))
3634
3793
  return [];
3635
3794
  try {
3636
3795
  const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
3637
3796
  const profiles = [];
3638
3797
  for (const file of files) {
3639
3798
  try {
3640
- const content = readFileSync6(join8(dir, file), "utf-8");
3799
+ const content = readFileSync7(join9(dir, file), "utf-8");
3641
3800
  profiles.push(JSON.parse(content));
3642
3801
  } catch {}
3643
3802
  }
@@ -4172,13 +4331,13 @@ function coerceForSqlite(value) {
4172
4331
  return String(value);
4173
4332
  }
4174
4333
  // src/mcp/server.ts
4175
- var __dirname3 = dirname7(fileURLToPath3(import.meta.url));
4334
+ var __dirname3 = dirname8(fileURLToPath3(import.meta.url));
4176
4335
  var pkg = { name: "@hasna/hooks", version: "0.0.0" };
4177
4336
  try {
4178
4337
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
4179
- const p = join10(__dirname3, rel);
4180
- if (existsSync11(p)) {
4181
- pkg = JSON.parse(readFileSync7(p, "utf-8"));
4338
+ const p = join11(__dirname3, rel);
4339
+ if (existsSync12(p)) {
4340
+ pkg = JSON.parse(readFileSync8(p, "utf-8"));
4182
4341
  break;
4183
4342
  }
4184
4343
  }
@@ -4187,10 +4346,10 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
4187
4346
  const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
4188
4347
  const resolved = resolveHook2(name);
4189
4348
  const hookScript = resolveScriptPath2(name);
4190
- if (!resolved || !hookScript || !existsSync11(hookScript)) {
4349
+ if (!resolved || !hookScript || !existsSync12(hookScript)) {
4191
4350
  throw new Error(`Hook '${name}' not found`);
4192
4351
  }
4193
- const content = readFileSync7(hookScript);
4352
+ const content = readFileSync8(hookScript);
4194
4353
  const sha = sha256Of(content);
4195
4354
  const check = checkScriptHash(name, sha);
4196
4355
  if (!check.ok) {
@@ -4396,7 +4555,7 @@ function createHooksServer() {
4396
4555
  const settingsPath = getSettingsPath(scope);
4397
4556
  const issues = [];
4398
4557
  const healthy = [];
4399
- const settingsExist = existsSync11(settingsPath);
4558
+ const settingsExist = existsSync12(settingsPath);
4400
4559
  if (!settingsExist) {
4401
4560
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
4402
4561
  }
@@ -4406,7 +4565,7 @@ function createHooksServer() {
4406
4565
  const meta = custom ? resolveHookMeta(name) : getHook(name);
4407
4566
  let hookHealthy = true;
4408
4567
  if (custom) {
4409
- if (!existsSync11(custom.scriptPath)) {
4568
+ if (!existsSync12(custom.scriptPath)) {
4410
4569
  issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
4411
4570
  hookHealthy = false;
4412
4571
  }
@@ -4416,14 +4575,14 @@ function createHooksServer() {
4416
4575
  continue;
4417
4576
  }
4418
4577
  const hookDir = getHookPath(name);
4419
- if (!existsSync11(join10(hookDir, "src", "hook.ts"))) {
4578
+ if (!existsSync12(join11(hookDir, "src", "hook.ts"))) {
4420
4579
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
4421
4580
  hookHealthy = false;
4422
4581
  }
4423
4582
  }
4424
4583
  if (meta && settingsExist) {
4425
4584
  try {
4426
- const settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
4585
+ const settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
4427
4586
  if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
4428
4587
  const eventName = meta.event.split(":")[0];
4429
4588
  issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
@@ -4460,10 +4619,10 @@ function createHooksServer() {
4460
4619
  return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
4461
4620
  }
4462
4621
  const hookPath = getHookPath(name);
4463
- const readmePath = join10(hookPath, "README.md");
4622
+ const readmePath = join11(hookPath, "README.md");
4464
4623
  let readme = "";
4465
- if (existsSync11(readmePath)) {
4466
- readme = readFileSync7(readmePath, "utf-8");
4624
+ if (existsSync12(readmePath)) {
4625
+ readme = readFileSync8(readmePath, "utf-8");
4467
4626
  }
4468
4627
  if (verbose) {
4469
4628
  return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
@@ -4646,7 +4805,7 @@ function createHooksServer() {
4646
4805
  const ctx = {
4647
4806
  scope,
4648
4807
  settings_path: settingsPath,
4649
- settings_exists: existsSync11(settingsPath),
4808
+ settings_exists: existsSync12(settingsPath),
4650
4809
  registered_hooks: hooks,
4651
4810
  hook_count: hooks.length,
4652
4811
  healthy,
@@ -4792,8 +4951,8 @@ function createHooksServer() {
4792
4951
  const settingsPath = getSettingsPath(scope);
4793
4952
  let settings = {};
4794
4953
  try {
4795
- if (existsSync11(settingsPath))
4796
- settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
4954
+ if (existsSync12(settingsPath))
4955
+ settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
4797
4956
  } catch {}
4798
4957
  if (!settings.hooks)
4799
4958
  settings.hooks = {};
@@ -4802,8 +4961,8 @@ function createHooksServer() {
4802
4961
  disabled.push(name);
4803
4962
  settings.hooks.__disabled = disabled;
4804
4963
  const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync7 } = await import("fs");
4805
- const { dirname: dirname8 } = await import("path");
4806
- mkdirSync7(dirname8(settingsPath), { recursive: true });
4964
+ const { dirname: dirname9 } = await import("path");
4965
+ mkdirSync7(dirname9(settingsPath), { recursive: true });
4807
4966
  writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
4808
4967
  `);
4809
4968
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
@@ -4815,8 +4974,8 @@ function createHooksServer() {
4815
4974
  const settingsPath = getSettingsPath(scope);
4816
4975
  let settings = {};
4817
4976
  try {
4818
- if (existsSync11(settingsPath))
4819
- settings = JSON.parse(readFileSync7(settingsPath, "utf-8"));
4977
+ if (existsSync12(settingsPath))
4978
+ settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
4820
4979
  } catch {}
4821
4980
  if (settings.hooks?.__disabled) {
4822
4981
  settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);