@hasna/hooks 0.9.4 → 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");
@@ -1418,14 +1419,14 @@ var init_redact = __esm(() => {
1418
1419
  ];
1419
1420
  });
1420
1421
 
1421
- // ../../node_modules/.bun/@hasna+contracts@1.0.2+e0db24ae03127387/node_modules/@hasna/contracts/dist/client/transport.js
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, openSync as openSync2, readFileSync as readFileSync7 } from "fs";
1425
- import { O_NOFOLLOW, O_NONBLOCK, O_RDONLY } from "constants";
1426
- import { createRequire } from "module";
1425
+ import { closeSync as closeSync22, fstatSync as fstatSync2, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
1426
+ import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
1427
1427
  import { hostname as osHostname } from "os";
1428
- import { isAbsolute as isAbsolute2, join as join9 } from "path";
1428
+ import { isAbsolute as isAbsolute3, join as join22 } from "path";
1429
+ import { createRequire } from "module";
1429
1430
  function envToken(name) {
1430
1431
  return name.toUpperCase().replace(/-/g, "_");
1431
1432
  }
@@ -1448,21 +1449,21 @@ 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);
1455
1456
  if (override)
1456
1457
  return override;
1457
1458
  const home = homeDir(env);
1458
- return home ? join9(home, HASNA_HOME_DIR) : null;
1459
+ return home ? join22(home, HASNA_HOME_DIR) : null;
1459
1460
  }
1460
1461
  function appConfigDir(name, env) {
1461
1462
  const configRoot = absoluteOverride(env, HASNA_CONFIG_HOME_ENV_KEY);
1462
1463
  if (configRoot)
1463
- return join9(configRoot, name);
1464
+ return join22(configRoot, name);
1464
1465
  const root = hasnaHomeDir(env);
1465
- return root ? join9(root, name, CONFIG_SUBDIR) : null;
1466
+ return root ? join22(root, name, CONFIG_SUBDIR) : null;
1466
1467
  }
1467
1468
  function credentialDiskSourceList(name, env, profile = null) {
1468
1469
  if (!SAFE_APP_SLUG.test(name))
@@ -1471,7 +1472,7 @@ function credentialDiskSourceList(name, env, profile = null) {
1471
1472
  if (!directory)
1472
1473
  return [];
1473
1474
  const file = profile ? `${CREDENTIALS_FILE}-${profile}` : CREDENTIALS_FILE;
1474
- return [{ path: join9(directory, file), tier: "disk" }];
1475
+ return [{ path: join22(directory, file), tier: "disk" }];
1475
1476
  }
1476
1477
  function credentialDiskSources(name, env) {
1477
1478
  return credentialDiskSourceList(name, env, null).map((s) => s.path);
@@ -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_RDONLY | O_NOFOLLOW | O_NONBLOCK);
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")
@@ -1535,7 +1536,7 @@ function readAppConfigFile(path) {
1535
1536
  unsafe(`the path could not be opened (${code ?? "unknown error"})`);
1536
1537
  }
1537
1538
  try {
1538
- const before = fstatSync(fd);
1539
+ const before = fstatSync2(fd);
1539
1540
  if (!before.isFile())
1540
1541
  unsafe("the path is not a regular file");
1541
1542
  if (!configFileModeAllowed(before.mode)) {
@@ -1546,31 +1547,39 @@ function readAppConfigFile(path) {
1546
1547
  unsafe("the file is not owned by the current user");
1547
1548
  if (before.size > MAX_CREDENTIAL_FILE_BYTES)
1548
1549
  unsafe("the file exceeds the size limit");
1549
- const bytes = readFileSync7(fd);
1550
- const after = fstatSync(fd);
1550
+ const bytes = readFileSync22(fd);
1551
+ const after = fstatSync2(fd);
1551
1552
  if (!configFileReadsCoherent(before, after)) {
1552
1553
  unsafe("the file changed while being read");
1553
1554
  }
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
- function readCredentialFile(path, apiKeyKeys) {
1561
+ function readCredentialFile(path, apiKeyKeys, pointerKey) {
1561
1562
  const parsed = readAppConfigFile(path);
1562
1563
  if (!parsed)
1563
1564
  return null;
1564
- for (const key of apiKeyKeys) {
1565
+ for (const key of [...apiKeyKeys, pointerKey]) {
1565
1566
  if (parsed.unusable.has(key)) {
1566
1567
  throw new CredentialFileUnsafeError(path, `${key} is declared but blank or malformed`);
1567
1568
  }
1568
1569
  }
1569
1570
  const values = apiKeyKeys.map((key) => parsed.values.get(key)?.trim()).filter((value) => Boolean(value));
1571
+ const pointer = parsed.values.get(pointerKey)?.trim();
1572
+ if (pointer !== undefined) {
1573
+ if (!VAULT_POINTER_SHAPE.test(pointer))
1574
+ throw new CredentialFileUnsafeError(path, `${pointerKey} must name a vault item`);
1575
+ if (values.length)
1576
+ throw new CredentialFileUnsafeError(path, "a credential file cannot select both a literal key and a vault reference");
1577
+ return { apiKey: "", pointerVaultKey: pointer };
1578
+ }
1570
1579
  if (new Set(values).size > 1) {
1571
1580
  throw new CredentialFileUnsafeError(path, "credential aliases disagree");
1572
1581
  }
1573
- return values[0] ?? null;
1582
+ return values[0] === undefined ? null : { apiKey: values[0] };
1574
1583
  }
1575
1584
  function appConfigDiskValue(name, env, keys) {
1576
1585
  const wanted = keys.filter((key) => !CREDENTIAL_SHAPED_KEY.test(key));
@@ -1827,12 +1836,13 @@ function resolveCredential(name, env, options = {}) {
1827
1836
  }
1828
1837
  const paths = profileDiskSources(name, env, profile);
1829
1838
  for (const path of paths) {
1830
- const value = readCredentialFile(path, apiKeyKeys);
1839
+ const value = readCredentialFile(path, apiKeyKeys, pointerKeyName);
1831
1840
  if (value) {
1832
- assertUsableCredential(name, path, value);
1841
+ if (!value.pointerVaultKey)
1842
+ assertUsableCredential(name, path, value.apiKey);
1833
1843
  return sealCredential({
1834
- apiKey: value,
1835
- tier: "profile",
1844
+ ...value,
1845
+ tier: value.pointerVaultKey ? "pointer" : "profile",
1836
1846
  source: path,
1837
1847
  deliberate: true,
1838
1848
  diskCandidates: paths,
@@ -1840,7 +1850,7 @@ function resolveCredential(name, env, options = {}) {
1840
1850
  });
1841
1851
  }
1842
1852
  }
1843
- throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
1853
+ throw new CredentialResolutionError(name, `Profile '${profile}' (from ${profileSource}) has no ${apiKeyKeys[0]} or ${pointerKeyName} for '${name}'. ` + `Looked in: ${paths.join(", ") || "<no HOME in this environment>"}. ` + `A profile names WHICH identity to use, so it is never resolved around \u2014 ` + `create the profile's credential file or unset ${CREDENTIAL_PROFILE_ENV_KEY}.`, paths);
1844
1854
  }
1845
1855
  const definedEnvEntries = apiKeyKeys.filter((key) => Object.prototype.hasOwnProperty.call(env, key) && env[key] !== undefined).map((key) => ({ key, value: String(env[key]).trim() }));
1846
1856
  const blankEnv = definedEnvEntries.find((entry) => entry.value.length === 0);
@@ -1865,17 +1875,27 @@ function resolveCredential(name, env, options = {}) {
1865
1875
  });
1866
1876
  }
1867
1877
  const diskSourceList = credentialDiskSourceList(name, env, null);
1868
- const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys) })).filter((hit) => hit.value !== null);
1878
+ const diskHits = diskSourceList.map((src) => ({ src, value: readCredentialFile(src.path, apiKeyKeys, pointerKeyName) })).filter((hit) => hit.value !== null);
1869
1879
  if (diskHits.length > 0) {
1870
1880
  const winner = diskHits[0];
1871
- assertUsableCredential(name, winner.src.path, winner.value);
1881
+ if (winner.value.pointerVaultKey) {
1882
+ return sealCredential({
1883
+ ...winner.value,
1884
+ tier: "pointer",
1885
+ source: winner.src.path,
1886
+ deliberate: false,
1887
+ diskCandidates: diskPaths,
1888
+ warning: null
1889
+ });
1890
+ }
1891
+ assertUsableCredential(name, winner.src.path, winner.value.apiKey);
1872
1892
  const divergentSources = [
1873
- ...diskHits.slice(1).filter((hit) => hit.value !== winner.value).map((hit) => hit.src.path),
1874
- ...envHit && envHit.value !== winner.value ? [envHit.key] : []
1893
+ ...diskHits.slice(1).filter((hit) => hit.value.apiKey !== winner.value.apiKey || hit.value.pointerVaultKey !== winner.value.pointerVaultKey).map((hit) => hit.src.path),
1894
+ ...envHit && envHit.value !== winner.value.apiKey ? [envHit.key] : []
1875
1895
  ];
1876
1896
  const warning = divergentSources.length > 0 ? `Credential sources disagree for '${name}': ${winner.src.path} and ` + `${divergentSources.join(", ")} hold different keys. ${winner.src.path} wins, because a file on ` + `disk is re-read on every call while an environment variable is a snapshot. Reconcile them \u2014 ` + `a rotation that updated only one leaves the other to fail 401 wherever it is loaded first.` : null;
1877
1897
  return sealCredential({
1878
- apiKey: winner.value,
1898
+ apiKey: winner.value.apiKey,
1879
1899
  tier: winner.src.tier,
1880
1900
  source: winner.src.path,
1881
1901
  deliberate: false,
@@ -2098,8 +2118,9 @@ function credentialDiskSourcesForMessage(name, env) {
2098
2118
  const paths = credentialDiskSources(name, env);
2099
2119
  return paths.length > 0 ? paths.join(" or ") : "<no HOME or HASNA_HOME set in this environment, so no credential file was consulted>";
2100
2120
  }
2101
- var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM, CREDENTIAL_SEAL, AMBIENT_ENVIRONMENT, SECRETS_PACKAGE_SPECIFIER, requireSecretsSdk, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
2121
+ var CREDENTIAL_PROFILE_ENV_KEY = "HASNA_PROFILE", MAX_PACKAGE_BYTES, CredentialResolutionError, CredentialFileUnsafeError, HASNA_HOME_ENV_KEY = "HASNA_HOME", HASNA_CONFIG_HOME_ENV_KEY = "HASNA_CONFIG_HOME", KEYCHAIN_STATION_ENV_KEY = "HASNA_STATION", HASNA_HOME_DIR = ".hasna", CONFIG_SUBDIR = "config", CREDENTIALS_FILE = "credentials", KEYCHAIN_SECURITY_BIN = "/usr/bin/security", KEYCHAIN_SERVICE_PREFIX = "hasna.credentials", KEYCHAIN_ITEM_NOT_FOUND_STATUS = 44, KEYCHAIN_SPAWN_TIMEOUT_MS = 1e4, MAX_CREDENTIAL_FILE_BYTES, SAFE_APP_SLUG, SAFE_PROFILE, ILLEGAL_IN_HEADER_VALUE, VAULT_POINTER_SHAPE, CREDENTIAL_SHAPED_KEY, INSPECT_CUSTOM, CREDENTIAL_SEAL, AMBIENT_ENVIRONMENT, requireSecretsSdk, DEFAULT_FLEET_GATEWAY_ORIGIN = "https://api.hasna.com", DEFAULT_AUTHORITY_SOURCE = "default", ASCII_CONTROL_PATTERN, DNS_LABEL_PATTERN, ClientTransportConfigurationError, IDEMPOTENT_METHODS, AUTHORITY_OVERRIDE_HEADERS;
2102
2122
  var init_transport = __esm(() => {
2123
+ MAX_PACKAGE_BYTES = 1024 * 1024;
2103
2124
  CredentialResolutionError = class CredentialResolutionError extends Error {
2104
2125
  appName;
2105
2126
  attempted;
@@ -2127,7 +2148,6 @@ var init_transport = __esm(() => {
2127
2148
  INSPECT_CUSTOM = Symbol.for("nodejs.util.inspect.custom");
2128
2149
  CREDENTIAL_SEAL = Symbol.for("hasna:contracts:sealedCredential");
2129
2150
  AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
2130
- SECRETS_PACKAGE_SPECIFIER = "@hasna/" + "secrets";
2131
2151
  requireSecretsSdk = createRequire(import.meta.url);
2132
2152
  ASCII_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
2133
2153
  DNS_LABEL_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
@@ -2209,12 +2229,12 @@ var init_local_opt_in = __esm(() => {
2209
2229
  });
2210
2230
 
2211
2231
  // src/lib/hook-env.ts
2212
- import { dirname as dirname5, isAbsolute as isAbsolute3 } from "path";
2213
- 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";
2214
2234
  import { tmpdir } from "os";
2215
2235
  function isDeniedEnvName(name) {
2216
2236
  const upper = name.toUpperCase();
2217
- if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
2237
+ if (DENY_PREFIXES.some((prefix2) => upper.startsWith(prefix2)))
2218
2238
  return true;
2219
2239
  if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
2220
2240
  return true;
@@ -2223,13 +2243,13 @@ function isDeniedEnvName(name) {
2223
2243
  function isInterpreterInjectionEnvName(name) {
2224
2244
  if (INTERPRETER_INJECTION.has(name))
2225
2245
  return true;
2226
- return INTERPRETER_INJECTION_PREFIXES.some((prefix) => name.startsWith(prefix));
2246
+ return INTERPRETER_INJECTION_PREFIXES.some((prefix2) => name.startsWith(prefix2));
2227
2247
  }
2228
2248
  function isBashFunctionEnvName(name) {
2229
2249
  return name.startsWith("BASH_FUNC_");
2230
2250
  }
2231
2251
  function isUnsafePathEntry(entry, home) {
2232
- if (entry === "" || !isAbsolute3(entry))
2252
+ if (entry === "" || !isAbsolute4(entry))
2233
2253
  return true;
2234
2254
  if (home && (entry === home || entry.startsWith(home + "/")))
2235
2255
  return true;
@@ -2239,7 +2259,7 @@ function isUnsafePathEntry(entry, home) {
2239
2259
  return true;
2240
2260
  const canonical = (path) => {
2241
2261
  try {
2242
- return realpathSync2(path);
2262
+ return realpathSync3(path);
2243
2263
  } catch {
2244
2264
  return path;
2245
2265
  }
@@ -2258,9 +2278,9 @@ function isUnsafePathEntry(entry, home) {
2258
2278
  }
2259
2279
  function sanitizeHookPath(pathValue, home) {
2260
2280
  const baseline = PATH_BASELINE.slice();
2261
- if (process.platform === "darwin" && existsSync10("/opt/homebrew/bin"))
2281
+ if (process.platform === "darwin" && existsSync11("/opt/homebrew/bin"))
2262
2282
  baseline.push("/opt/homebrew/bin");
2263
- const bunDir = dirname5(process.execPath);
2283
+ const bunDir = dirname6(process.execPath);
2264
2284
  const seen = new Set;
2265
2285
  const out = [];
2266
2286
  for (const dir of baseline) {
@@ -2385,7 +2405,7 @@ __export(exports_run, {
2385
2405
  });
2386
2406
  import { randomBytes as randomBytes2 } from "crypto";
2387
2407
  import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
2388
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join10 } from "path";
2408
+ import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute5, join as join10 } from "path";
2389
2409
  function extensionOf(scriptPath) {
2390
2410
  const base = basename2(scriptPath);
2391
2411
  const dot = base.lastIndexOf(".");
@@ -2454,7 +2474,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
2454
2474
  return chunks.join("");
2455
2475
  }
2456
2476
  async function executeVerifiedScript(options) {
2457
- const scriptDir = dirname6(options.scriptPath);
2477
+ const scriptDir = dirname7(options.scriptPath);
2458
2478
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
2459
2479
  const tempPath = join10(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
2460
2480
  let proc = null;
@@ -2465,7 +2485,7 @@ async function executeVerifiedScript(options) {
2465
2485
  }, options.timeout) : null;
2466
2486
  try {
2467
2487
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
2468
- proc = Bun.spawn([...interpreter.command, isAbsolute4(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
2488
+ proc = Bun.spawn([...interpreter.command, isAbsolute5(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
2469
2489
  stdin: new Response(options.stdin),
2470
2490
  stdout: "pipe",
2471
2491
  stderr: "pipe",
@@ -2664,19 +2684,150 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
2664
2684
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2665
2685
  import { z as z2 } from "zod";
2666
2686
  init_registry();
2667
- import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
2668
- import { join as join11, 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";
2669
2689
  import { fileURLToPath as fileURLToPath3 } from "url";
2670
2690
 
2671
2691
  // src/lib/installer.ts
2672
2692
  init_registry();
2673
2693
  init_resolve();
2674
2694
  init_manifest();
2675
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
2676
- import { join as join7, dirname as dirname4 } from "path";
2677
- 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";
2678
2698
  import { fileURLToPath as fileURLToPath2 } from "url";
2679
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
+
2680
2831
  // src/lib/registration.ts
2681
2832
  init_resolve();
2682
2833
 
@@ -2741,6 +2892,9 @@ var REGISTRATION_COMMAND_RE = /^(?:hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?|
2741
2892
  function registrationName(command) {
2742
2893
  if (typeof command !== "string")
2743
2894
  return;
2895
+ const native = nativeSafetyRegistration(command);
2896
+ if (native)
2897
+ return native;
2744
2898
  if (/^hooks run mementos-context(?:\s|$)/.test(command.trim())) {
2745
2899
  try {
2746
2900
  retainedMementosRegistration([command.trim()]);
@@ -2795,8 +2949,7 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
2795
2949
  if (!Array.isArray(hooks))
2796
2950
  return false;
2797
2951
  return hooks.some((entry) => entry?.hooks?.some((h) => {
2798
- const match = h?.command?.match(/^hooks run ([\w-]+)/);
2799
- if (!match || match[1] !== name)
2952
+ if (registrationName(h?.command) !== name)
2800
2953
  return false;
2801
2954
  if (!matcher)
2802
2955
  return true;
@@ -2808,13 +2961,13 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
2808
2961
  }
2809
2962
 
2810
2963
  // src/lib/store.ts
2811
- import { createHash } from "crypto";
2812
- 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";
2813
2966
  init_db();
2814
2967
  init_config();
2815
2968
  import { randomBytes } from "crypto";
2816
2969
  function sha256Of(input) {
2817
- return createHash("sha256").update(input).digest("hex");
2970
+ return createHash2("sha256").update(input).digest("hex");
2818
2971
  }
2819
2972
  function storeDb() {
2820
2973
  return isLocalStoreRefused() ? null : getDb();
@@ -2870,11 +3023,11 @@ function assertLockShape(parsed, path) {
2870
3023
  }
2871
3024
  function readLock() {
2872
3025
  const path = getLockPath();
2873
- if (!existsSync6(path))
3026
+ if (!existsSync7(path))
2874
3027
  return { hooks: {} };
2875
3028
  let raw;
2876
3029
  try {
2877
- raw = readFileSync3(path, "utf-8");
3030
+ raw = readFileSync4(path, "utf-8");
2878
3031
  } catch {
2879
3032
  throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
2880
3033
  }
@@ -2946,16 +3099,16 @@ function checkScriptHash(name, actual) {
2946
3099
  }
2947
3100
 
2948
3101
  // src/lib/codex-settings.ts
2949
- import { closeSync, existsSync as existsSync7, lstatSync, mkdirSync as mkdirSync4, openSync, readFileSync as readFileSync4, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
2950
- 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";
2951
3104
  import { randomUUID } from "crypto";
2952
3105
  var snapshots = new WeakMap;
2953
3106
  function current(path) {
2954
3107
  try {
2955
- const stat = lstatSync(path);
3108
+ const stat = lstatSync2(path);
2956
3109
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
2957
3110
  throw new Error("invalid_file");
2958
- return readFileSync4(path, "utf8");
3111
+ return readFileSync5(path, "utf8");
2959
3112
  } catch (error) {
2960
3113
  if (error.code === "ENOENT")
2961
3114
  return null;
@@ -2987,11 +3140,11 @@ function writeCodexSettings(path, settings) {
2987
3140
  const snapshot = snapshots.get(settings);
2988
3141
  if (!snapshot || snapshot.path !== path)
2989
3142
  throw new Error("Codex settings require a complete current read before writing.");
2990
- mkdirSync4(dirname3(path), { recursive: true });
3143
+ mkdirSync4(dirname4(path), { recursive: true });
2991
3144
  const lock = `${path}.hooks-write.lock`;
2992
3145
  let fd;
2993
3146
  try {
2994
- fd = openSync(lock, "wx", 384);
3147
+ fd = openSync2(lock, "wx", 384);
2995
3148
  } catch {
2996
3149
  throw new Error("Codex settings are being edited; retry after the active writer finishes.");
2997
3150
  }
@@ -3007,16 +3160,16 @@ function writeCodexSettings(path, settings) {
3007
3160
  renameSync2(temp, path);
3008
3161
  snapshots.set(settings, { path, raw: next });
3009
3162
  } finally {
3010
- if (existsSync7(temp))
3163
+ if (existsSync8(temp))
3011
3164
  unlinkSync(temp);
3012
- closeSync(fd);
3165
+ closeSync2(fd);
3013
3166
  unlinkSync(lock);
3014
3167
  }
3015
3168
  }
3016
3169
 
3017
3170
  // src/lib/installer.ts
3018
- var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
3019
- 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");
3020
3173
  function normalizeHookName(name) {
3021
3174
  return name.startsWith("hook-") ? name : `hook-${name}`;
3022
3175
  }
@@ -3028,8 +3181,7 @@ function removeHookEntriesByName(entries, hookName) {
3028
3181
  if (!Array.isArray(entry.hooks))
3029
3182
  return [entry];
3030
3183
  const kept = entry.hooks.filter((h) => {
3031
- const match = h.command?.match(/^hooks run ([\w-]+)/);
3032
- return !(match && match[1] === hookName);
3184
+ return registrationName(h.command) !== hookName;
3033
3185
  });
3034
3186
  return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
3035
3187
  });
@@ -3105,29 +3257,29 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
3105
3257
  return globalOverride;
3106
3258
  const dir = getTargetSettingsDir(target);
3107
3259
  if (target === "codex")
3108
- return join7(scope === "project" ? process.cwd() : homedir3(), dir, "hooks.json");
3260
+ return join8(scope === "project" ? process.cwd() : homedir4(), dir, "hooks.json");
3109
3261
  if (scope === "project") {
3110
- 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");
3111
3263
  }
3112
- 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");
3113
3265
  }
3114
3266
  function getHookPath(name) {
3115
3267
  const shortName = shortHookName(name);
3116
3268
  const resolved = resolveHookDir(shortName);
3117
3269
  if (resolved)
3118
3270
  return resolved;
3119
- return join7(HOOKS_DIR, normalizeHookName(shortName));
3271
+ return join8(HOOKS_DIR, normalizeHookName(shortName));
3120
3272
  }
3121
3273
  function hookExists(name) {
3122
- return existsSync8(getHookPath(name));
3274
+ return existsSync9(getHookPath(name));
3123
3275
  }
3124
3276
  function readSettings(scope = "global", target = "claude") {
3125
3277
  const path = getSettingsPath(scope, target);
3126
3278
  if (target === "codex")
3127
3279
  return readCodexSettings(path);
3128
3280
  try {
3129
- if (existsSync8(path)) {
3130
- return JSON.parse(readFileSync5(path, "utf-8"));
3281
+ if (existsSync9(path)) {
3282
+ return JSON.parse(readFileSync6(path, "utf-8"));
3131
3283
  }
3132
3284
  } catch (error) {
3133
3285
  console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
@@ -3138,8 +3290,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
3138
3290
  const path = getSettingsPath(scope, target);
3139
3291
  if (target === "codex")
3140
3292
  return writeCodexSettings(path, settings);
3141
- const dir = dirname4(path);
3142
- if (!existsSync8(dir)) {
3293
+ const dir = dirname5(path);
3294
+ if (!existsSync9(dir)) {
3143
3295
  mkdirSync5(dir, { recursive: true });
3144
3296
  }
3145
3297
  writeFileSync4(path, JSON.stringify(settings, null, 2) + `
@@ -3167,6 +3319,9 @@ function codewithTimeout(name) {
3167
3319
  switch (name) {
3168
3320
  case "mementos-context":
3169
3321
  return 12;
3322
+ case "trash-guard":
3323
+ case "workspace-repos-guard":
3324
+ return 10;
3170
3325
  case "knowledge-context":
3171
3326
  return 6;
3172
3327
  case "session-start":
@@ -3176,7 +3331,6 @@ function codewithTimeout(name) {
3176
3331
  case "prompt-guard":
3177
3332
  return 3;
3178
3333
  case "worktree-guard":
3179
- case "trash-guard":
3180
3334
  case "stop-sync":
3181
3335
  return 5;
3182
3336
  default:
@@ -3205,11 +3359,13 @@ function codewithStatusMessage(name) {
3205
3359
  }
3206
3360
  function buildCodewithTomlFragment(name, profile, mementos) {
3207
3361
  const shortName = shortHookName(name);
3208
- 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);
3209
3365
  if (!meta)
3210
3366
  throw new Error(`Hook '${shortName}' not found`);
3211
3367
  validateMementosRegistration(shortName, mementos);
3212
- 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);
3213
3369
  const matcher = codewithMatcher(meta.matcher);
3214
3370
  const fragments = [];
3215
3371
  for (const event of getHookEvents(meta)) {
@@ -3234,17 +3390,17 @@ function buildCodewithTomlFragment(name, profile, mementos) {
3234
3390
  function readCodewithConfig(scope, configPath) {
3235
3391
  const path = getSettingsPath(scope, "codewith", configPath);
3236
3392
  try {
3237
- return existsSync8(path) ? readFileSync5(path, "utf-8") : "";
3393
+ return existsSync9(path) ? readFileSync6(path, "utf-8") : "";
3238
3394
  } catch {
3239
3395
  return "";
3240
3396
  }
3241
3397
  }
3242
3398
  function appendCodewithFragment(fragment, scope, configPath) {
3243
3399
  const path = getSettingsPath(scope, "codewith", configPath);
3244
- const dir = dirname4(path);
3245
- if (!existsSync8(dir))
3400
+ const dir = dirname5(path);
3401
+ if (!existsSync9(dir))
3246
3402
  mkdirSync5(dir, { recursive: true });
3247
- const existing = existsSync8(path) ? readFileSync5(path, "utf-8") : "";
3403
+ const existing = existsSync9(path) ? readFileSync6(path, "utf-8") : "";
3248
3404
  const sep2 = existing.trim() ? `
3249
3405
 
3250
3406
  ` : "";
@@ -3252,7 +3408,7 @@ function appendCodewithFragment(fragment, scope, configPath) {
3252
3408
  return path;
3253
3409
  }
3254
3410
  function detectConflict(name, scope, target) {
3255
- const meta = resolveHookMeta(name);
3411
+ const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
3256
3412
  if (!meta || !meta.matcher)
3257
3413
  return;
3258
3414
  const events = new Set(getHookEvents(meta));
@@ -3272,7 +3428,7 @@ function detectConflict(name, scope, target) {
3272
3428
  return;
3273
3429
  }
3274
3430
  function detectRewriteConflict(name, scope, target, lookup = getHook) {
3275
- const meta = resolveHookMeta(name);
3431
+ const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
3276
3432
  if (!meta?.matcher || !meta.rewritesInput)
3277
3433
  return;
3278
3434
  if (!getHookEvents(meta).includes("PreToolUse"))
@@ -3293,7 +3449,10 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
3293
3449
  }
3294
3450
  function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
3295
3451
  const shortName = shortHookName(name);
3296
- 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)) {
3297
3456
  return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
3298
3457
  }
3299
3458
  if (target === "codewith") {
@@ -3385,7 +3544,7 @@ function installHook(name, options = {}) {
3385
3544
  }
3386
3545
  if (target === "all") {
3387
3546
  const shortName = shortHookName(name);
3388
- const meta = resolveHookMeta(shortName);
3547
+ const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
3389
3548
  if (meta) {
3390
3549
  const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
3391
3550
  if (unsupportedTargets.length > 0) {
@@ -3418,7 +3577,7 @@ function installHook(name, options = {}) {
3418
3577
  return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
3419
3578
  }
3420
3579
  function registerHook(name, scope = "global", target = "claude", profile, mementos) {
3421
- const meta = resolveHookMeta(name);
3580
+ const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
3422
3581
  if (!meta)
3423
3582
  return;
3424
3583
  const eventKeys = getHookEvents(meta).map((event) => {
@@ -3435,8 +3594,17 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
3435
3594
  const settings = readSettings(scope, target);
3436
3595
  if (!settings.hooks)
3437
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
+ }
3438
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;
3439
- 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);
3440
3608
  if (target === "codex") {
3441
3609
  const positions = [];
3442
3610
  for (const [event, groups] of Object.entries(settings.hooks)) {
@@ -3444,9 +3612,11 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
3444
3612
  for (let index = 0;index < entry.hooks.length; index++) {
3445
3613
  const command = entry.hooks[index]?.command;
3446
3614
  const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
3447
- if (match && (match[1] ?? match[2]) === name) {
3615
+ if (match && (match[1] ?? match[2]) === name || registrationName(command) === name) {
3448
3616
  if (registrationName(command) !== name)
3449
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.");
3450
3620
  positions.push({ event, entry, index });
3451
3621
  }
3452
3622
  }
@@ -3507,12 +3677,20 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
3507
3677
  if (target === "codewith") {
3508
3678
  const config = readCodewithConfig(scope);
3509
3679
  const registered2 = [];
3510
- const re = /^\s*command\s*=\s*["']([^"'\n]+)["']\s*$/gm;
3511
- let match;
3512
- while (match = re.exec(config)) {
3513
- const name = registrationName(match[1]);
3514
- if (name)
3515
- 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 [];
3516
3694
  }
3517
3695
  return [...new Set(registered2)];
3518
3696
  }
@@ -3558,12 +3736,12 @@ function removeHookForTarget(name, scope, target) {
3558
3736
 
3559
3737
  // src/lib/profiles.ts
3560
3738
  init_app_home();
3561
- import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
3562
- 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";
3563
3741
  function getProfilesDir() {
3564
- const newDir = join8(getEffectiveDataRoot(), "profiles");
3565
- const oldDir = join8(getHomeDir(), ".hooks", "profiles");
3566
- if (!existsSync9(newDir) && existsSync9(oldDir)) {
3742
+ const newDir = join9(getEffectiveDataRoot(), "profiles");
3743
+ const oldDir = join9(getHomeDir(), ".hooks", "profiles");
3744
+ if (!existsSync10(newDir) && existsSync10(oldDir)) {
3567
3745
  mkdirSync6(newDir, { recursive: true });
3568
3746
  cpSync2(oldDir, newDir, { recursive: true });
3569
3747
  }
@@ -3571,12 +3749,12 @@ function getProfilesDir() {
3571
3749
  }
3572
3750
  function ensureProfilesDir() {
3573
3751
  const dir = getProfilesDir();
3574
- if (!existsSync9(dir)) {
3752
+ if (!existsSync10(dir)) {
3575
3753
  mkdirSync6(dir, { recursive: true });
3576
3754
  }
3577
3755
  }
3578
3756
  function profilePath(id) {
3579
- return join8(getProfilesDir(), `${id}.json`);
3757
+ return join9(getProfilesDir(), `${id}.json`);
3580
3758
  }
3581
3759
  function shortUuid() {
3582
3760
  return crypto.randomUUID().slice(0, 8);
@@ -3602,23 +3780,23 @@ function createProfile(input) {
3602
3780
  function getProfile(id) {
3603
3781
  const path = profilePath(id);
3604
3782
  try {
3605
- if (!existsSync9(path))
3783
+ if (!existsSync10(path))
3606
3784
  return null;
3607
- return JSON.parse(readFileSync6(path, "utf-8"));
3785
+ return JSON.parse(readFileSync7(path, "utf-8"));
3608
3786
  } catch {
3609
3787
  return null;
3610
3788
  }
3611
3789
  }
3612
3790
  function listProfiles() {
3613
3791
  const dir = getProfilesDir();
3614
- if (!existsSync9(dir))
3792
+ if (!existsSync10(dir))
3615
3793
  return [];
3616
3794
  try {
3617
3795
  const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
3618
3796
  const profiles = [];
3619
3797
  for (const file of files) {
3620
3798
  try {
3621
- const content = readFileSync6(join8(dir, file), "utf-8");
3799
+ const content = readFileSync7(join9(dir, file), "utf-8");
3622
3800
  profiles.push(JSON.parse(content));
3623
3801
  } catch {}
3624
3802
  }
@@ -3670,6 +3848,7 @@ function rethrowHooksAuthorityFailure(error) {
3670
3848
  }
3671
3849
  throw new Error(`REMOTE_API_URL_INVALID: ${message} local mode fallback is disabled`, { cause: error });
3672
3850
  }
3851
+ var authorityBindings = new WeakMap;
3673
3852
  function resolveHooksTransport(env = process.env, options = {}) {
3674
3853
  if (selectsHooksLocalStore(env)) {
3675
3854
  announceLocal(options.notice, "HASNA_HOOKS_LOCAL is set and nothing configures a registry authority");
@@ -3683,7 +3862,7 @@ function resolveHooksTransport(env = process.env, options = {}) {
3683
3862
  try {
3684
3863
  credential = resolveCredential("hooks", resolverEnv, credentialOptions);
3685
3864
  resolution = resolveClientTransport("hooks", resolverEnv, {
3686
- credentials: credential ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
3865
+ credentials: credential && credential.tier !== "pointer" ? { ...credentialOptions, apiKey: credential.apiKey } : credentialOptions
3687
3866
  });
3688
3867
  } catch (error) {
3689
3868
  rethrowHooksAuthorityFailure(error);
@@ -3691,18 +3870,20 @@ function resolveHooksTransport(env = process.env, options = {}) {
3691
3870
  if (!credential) {
3692
3871
  throw new Error("REMOTE_API_CONFIG_MISSING: no hooks registry credential resolved; " + "local mode fallback is disabled");
3693
3872
  }
3873
+ const authority = {
3874
+ origin: hooksRegistryOrigin(resolution.baseUrl),
3875
+ apiKey: credential.apiKey,
3876
+ apiUrlSource: resolution.apiUrlSource,
3877
+ apiKeySource: credential.source,
3878
+ apiKeyTier: credential.tier,
3879
+ v1BaseUrl: resolution.baseUrl,
3880
+ warning: resolution.warning
3881
+ };
3882
+ authorityBindings.set(authority, { env, options, credential });
3694
3883
  return {
3695
3884
  mode: "remote",
3696
3885
  source: `${credential.source}+${resolution.apiUrlSource ?? "default"}`,
3697
- authority: {
3698
- origin: hooksRegistryOrigin(resolution.baseUrl),
3699
- apiKey: credential.apiKey,
3700
- apiUrlSource: resolution.apiUrlSource,
3701
- apiKeySource: credential.source,
3702
- apiKeyTier: credential.tier,
3703
- v1BaseUrl: resolution.baseUrl,
3704
- warning: resolution.warning
3705
- }
3886
+ authority
3706
3887
  };
3707
3888
  }
3708
3889
 
@@ -4150,12 +4331,12 @@ function coerceForSqlite(value) {
4150
4331
  return String(value);
4151
4332
  }
4152
4333
  // src/mcp/server.ts
4153
- var __dirname3 = dirname7(fileURLToPath3(import.meta.url));
4334
+ var __dirname3 = dirname8(fileURLToPath3(import.meta.url));
4154
4335
  var pkg = { name: "@hasna/hooks", version: "0.0.0" };
4155
4336
  try {
4156
4337
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
4157
4338
  const p = join11(__dirname3, rel);
4158
- if (existsSync11(p)) {
4339
+ if (existsSync12(p)) {
4159
4340
  pkg = JSON.parse(readFileSync8(p, "utf-8"));
4160
4341
  break;
4161
4342
  }
@@ -4165,7 +4346,7 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
4165
4346
  const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
4166
4347
  const resolved = resolveHook2(name);
4167
4348
  const hookScript = resolveScriptPath2(name);
4168
- if (!resolved || !hookScript || !existsSync11(hookScript)) {
4349
+ if (!resolved || !hookScript || !existsSync12(hookScript)) {
4169
4350
  throw new Error(`Hook '${name}' not found`);
4170
4351
  }
4171
4352
  const content = readFileSync8(hookScript);
@@ -4374,7 +4555,7 @@ function createHooksServer() {
4374
4555
  const settingsPath = getSettingsPath(scope);
4375
4556
  const issues = [];
4376
4557
  const healthy = [];
4377
- const settingsExist = existsSync11(settingsPath);
4558
+ const settingsExist = existsSync12(settingsPath);
4378
4559
  if (!settingsExist) {
4379
4560
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
4380
4561
  }
@@ -4384,7 +4565,7 @@ function createHooksServer() {
4384
4565
  const meta = custom ? resolveHookMeta(name) : getHook(name);
4385
4566
  let hookHealthy = true;
4386
4567
  if (custom) {
4387
- if (!existsSync11(custom.scriptPath)) {
4568
+ if (!existsSync12(custom.scriptPath)) {
4388
4569
  issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
4389
4570
  hookHealthy = false;
4390
4571
  }
@@ -4394,7 +4575,7 @@ function createHooksServer() {
4394
4575
  continue;
4395
4576
  }
4396
4577
  const hookDir = getHookPath(name);
4397
- if (!existsSync11(join11(hookDir, "src", "hook.ts"))) {
4578
+ if (!existsSync12(join11(hookDir, "src", "hook.ts"))) {
4398
4579
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
4399
4580
  hookHealthy = false;
4400
4581
  }
@@ -4440,7 +4621,7 @@ function createHooksServer() {
4440
4621
  const hookPath = getHookPath(name);
4441
4622
  const readmePath = join11(hookPath, "README.md");
4442
4623
  let readme = "";
4443
- if (existsSync11(readmePath)) {
4624
+ if (existsSync12(readmePath)) {
4444
4625
  readme = readFileSync8(readmePath, "utf-8");
4445
4626
  }
4446
4627
  if (verbose) {
@@ -4624,7 +4805,7 @@ function createHooksServer() {
4624
4805
  const ctx = {
4625
4806
  scope,
4626
4807
  settings_path: settingsPath,
4627
- settings_exists: existsSync11(settingsPath),
4808
+ settings_exists: existsSync12(settingsPath),
4628
4809
  registered_hooks: hooks,
4629
4810
  hook_count: hooks.length,
4630
4811
  healthy,
@@ -4770,7 +4951,7 @@ function createHooksServer() {
4770
4951
  const settingsPath = getSettingsPath(scope);
4771
4952
  let settings = {};
4772
4953
  try {
4773
- if (existsSync11(settingsPath))
4954
+ if (existsSync12(settingsPath))
4774
4955
  settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
4775
4956
  } catch {}
4776
4957
  if (!settings.hooks)
@@ -4780,8 +4961,8 @@ function createHooksServer() {
4780
4961
  disabled.push(name);
4781
4962
  settings.hooks.__disabled = disabled;
4782
4963
  const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync7 } = await import("fs");
4783
- const { dirname: dirname8 } = await import("path");
4784
- mkdirSync7(dirname8(settingsPath), { recursive: true });
4964
+ const { dirname: dirname9 } = await import("path");
4965
+ mkdirSync7(dirname9(settingsPath), { recursive: true });
4785
4966
  writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
4786
4967
  `);
4787
4968
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
@@ -4793,7 +4974,7 @@ function createHooksServer() {
4793
4974
  const settingsPath = getSettingsPath(scope);
4794
4975
  let settings = {};
4795
4976
  try {
4796
- if (existsSync11(settingsPath))
4977
+ if (existsSync12(settingsPath))
4797
4978
  settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
4798
4979
  } catch {}
4799
4980
  if (settings.hooks?.__disabled) {