@hasna/hooks 0.6.6 → 0.6.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -47,7 +47,7 @@ var __export = (target, all) => {
47
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
48
  var __require = import.meta.require;
49
49
 
50
- // node_modules/commander/lib/error.js
50
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/error.js
51
51
  var require_error = __commonJS((exports) => {
52
52
  class CommanderError extends Error {
53
53
  constructor(exitCode, code, message) {
@@ -71,7 +71,7 @@ var require_error = __commonJS((exports) => {
71
71
  exports.InvalidArgumentError = InvalidArgumentError;
72
72
  });
73
73
 
74
- // node_modules/commander/lib/argument.js
74
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/argument.js
75
75
  var require_argument = __commonJS((exports) => {
76
76
  var { InvalidArgumentError } = require_error();
77
77
 
@@ -150,7 +150,7 @@ var require_argument = __commonJS((exports) => {
150
150
  exports.humanReadableArgName = humanReadableArgName;
151
151
  });
152
152
 
153
- // node_modules/commander/lib/help.js
153
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/help.js
154
154
  var require_help = __commonJS((exports) => {
155
155
  var { humanReadableArgName } = require_argument();
156
156
 
@@ -399,7 +399,7 @@ var require_help = __commonJS((exports) => {
399
399
  exports.Help = Help;
400
400
  });
401
401
 
402
- // node_modules/commander/lib/option.js
402
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/option.js
403
403
  var require_option = __commonJS((exports) => {
404
404
  var { InvalidArgumentError } = require_error();
405
405
 
@@ -550,7 +550,7 @@ var require_option = __commonJS((exports) => {
550
550
  exports.DualOptions = DualOptions;
551
551
  });
552
552
 
553
- // node_modules/commander/lib/suggestSimilar.js
553
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/suggestSimilar.js
554
554
  var require_suggestSimilar = __commonJS((exports) => {
555
555
  var maxDistance = 3;
556
556
  function editDistance(a, b) {
@@ -623,7 +623,7 @@ var require_suggestSimilar = __commonJS((exports) => {
623
623
  exports.suggestSimilar = suggestSimilar;
624
624
  });
625
625
 
626
- // node_modules/commander/lib/command.js
626
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/command.js
627
627
  var require_command = __commonJS((exports) => {
628
628
  var EventEmitter = __require("events").EventEmitter;
629
629
  var childProcess = __require("child_process");
@@ -1866,7 +1866,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1866
1866
  exports.Command = Command;
1867
1867
  });
1868
1868
 
1869
- // node_modules/commander/index.js
1869
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/index.js
1870
1870
  var require_commander = __commonJS((exports) => {
1871
1871
  var { Argument } = require_argument();
1872
1872
  var { Command } = require_command();
@@ -2419,7 +2419,7 @@ var init_registry = __esm(() => {
2419
2419
  ];
2420
2420
  });
2421
2421
 
2422
- // node_modules/cli-spinners/spinners.json
2422
+ // ../../node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/spinners.json
2423
2423
  var require_spinners = __commonJS((exports, module) => {
2424
2424
  module.exports = {
2425
2425
  dots: {
@@ -4045,7 +4045,7 @@ var require_spinners = __commonJS((exports, module) => {
4045
4045
  };
4046
4046
  });
4047
4047
 
4048
- // node_modules/cli-spinners/index.js
4048
+ // ../../node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/index.js
4049
4049
  var require_cli_spinners = __commonJS((exports, module) => {
4050
4050
  var spinners = Object.assign({}, require_spinners());
4051
4051
  var spinnersList = Object.keys(spinners);
@@ -4125,9 +4125,9 @@ function resolveApiKey() {
4125
4125
  var init_config = () => {};
4126
4126
 
4127
4127
  // src/lib/semver.ts
4128
- var SEMVER_PATTERN;
4128
+ var NUMERIC_ID = "(0|[1-9]\\d{0,15})", ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*", SEMVER_PATTERN;
4129
4129
  var init_semver = __esm(() => {
4130
- SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
4130
+ SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
4131
4131
  });
4132
4132
 
4133
4133
  // src/lib/manifest.ts
@@ -4289,7 +4289,8 @@ var init_manifest = __esm(() => {
4289
4289
  script: z.string().min(1, "script must name a relative path or inline content"),
4290
4290
  script_kind: z.enum(["inline", "file"]).optional(),
4291
4291
  args: z.array(z.string()).optional(),
4292
- timeout_ms: z.number().int().positive().optional()
4292
+ timeout_ms: z.number().int().positive().optional(),
4293
+ env: z.record(z.string(), z.string()).optional()
4293
4294
  });
4294
4295
  ScriptContainmentError = class ScriptContainmentError extends Error {
4295
4296
  constructor(path, hookDir, raw) {
@@ -5921,6 +5922,8 @@ var init_db_writer = __esm(() => {
5921
5922
  });
5922
5923
 
5923
5924
  // src/lib/hook-env.ts
5925
+ import { dirname as dirname4, isAbsolute as isAbsolute2 } from "path";
5926
+ import { existsSync as existsSync10, statSync } from "fs";
5924
5927
  function isDeniedEnvName(name) {
5925
5928
  const upper = name.toUpperCase();
5926
5929
  if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
@@ -5929,13 +5932,74 @@ function isDeniedEnvName(name) {
5929
5932
  return true;
5930
5933
  return DENY_CONTAINS.some((token) => upper.includes(token));
5931
5934
  }
5935
+ function isInterpreterInjectionEnvName(name) {
5936
+ if (INTERPRETER_INJECTION.has(name))
5937
+ return true;
5938
+ return INTERPRETER_INJECTION_PREFIXES.some((prefix) => name.startsWith(prefix));
5939
+ }
5940
+ function isBashFunctionEnvName(name) {
5941
+ return name.startsWith("BASH_FUNC_");
5942
+ }
5943
+ function isUnsafePathEntry(entry, home) {
5944
+ if (entry === "" || !isAbsolute2(entry))
5945
+ return true;
5946
+ if (home && (entry === home || entry.startsWith(home + "/")))
5947
+ return true;
5948
+ if (entry === "/tmp" || entry.startsWith("/tmp/"))
5949
+ return true;
5950
+ if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
5951
+ return true;
5952
+ try {
5953
+ if ((statSync(entry).mode & 2) !== 0)
5954
+ return true;
5955
+ } catch {}
5956
+ return false;
5957
+ }
5958
+ function sanitizeHookPath(pathValue, home) {
5959
+ const baseline = PATH_BASELINE.slice();
5960
+ if (process.platform === "darwin" && existsSync10("/opt/homebrew/bin"))
5961
+ baseline.push("/opt/homebrew/bin");
5962
+ const bunDir = dirname4(process.execPath);
5963
+ const seen = new Set;
5964
+ const out = [];
5965
+ for (const dir of baseline) {
5966
+ if (seen.has(dir))
5967
+ continue;
5968
+ seen.add(dir);
5969
+ out.push(dir);
5970
+ }
5971
+ if (!seen.has(bunDir)) {
5972
+ seen.add(bunDir);
5973
+ out.push(bunDir);
5974
+ }
5975
+ for (const entry of pathValue.split(":")) {
5976
+ if (seen.has(entry))
5977
+ continue;
5978
+ if (isUnsafePathEntry(entry, home))
5979
+ continue;
5980
+ seen.add(entry);
5981
+ out.push(entry);
5982
+ }
5983
+ return out.join(":");
5984
+ }
5932
5985
  function buildHookEnv(source, extra) {
5933
5986
  const out = {};
5934
5987
  for (const name of ALLOWLIST) {
5988
+ if (name === "PATH")
5989
+ continue;
5935
5990
  const value = source[name] ?? extra?.[name];
5936
5991
  if (value !== undefined && value !== null)
5937
5992
  out[name] = value;
5938
5993
  }
5994
+ const pathOverride = extra?.PATH;
5995
+ if (pathOverride !== undefined && pathOverride !== null) {
5996
+ out.PATH = pathOverride;
5997
+ } else {
5998
+ const parentPath = source.PATH;
5999
+ if (parentPath !== undefined && parentPath !== null) {
6000
+ out.PATH = sanitizeHookPath(parentPath, source.HOME ?? extra?.HOME);
6001
+ }
6002
+ }
5939
6003
  for (const [from, to] of CONFIG_PROJECTIONS) {
5940
6004
  const value = source[from] ?? extra?.[from];
5941
6005
  if (value !== undefined && value !== null)
@@ -5947,14 +6011,44 @@ function buildHookEnv(source, extra) {
5947
6011
  continue;
5948
6012
  if (isDeniedEnvName(name))
5949
6013
  continue;
6014
+ if (isInterpreterInjectionEnvName(name))
6015
+ continue;
6016
+ if (isBashFunctionEnvName(name))
6017
+ continue;
5950
6018
  if (value !== undefined && value !== null)
5951
6019
  out[name] = value;
5952
6020
  }
5953
6021
  return out;
5954
6022
  }
5955
- var ALLOWLIST, DENY_SUFFIXES, DENY_PREFIXES, DENY_CONTAINS, CONFIG_PROJECTIONS;
6023
+ var ALLOWLIST, INTERPRETER_INJECTION, INTERPRETER_INJECTION_PREFIXES, DENY_SUFFIXES, DENY_PREFIXES, DENY_CONTAINS, PATH_BASELINE, CONFIG_PROJECTIONS;
5956
6024
  var init_hook_env = __esm(() => {
5957
6025
  ALLOWLIST = new Set(["PATH", "HOME", "LANG", "TZ", "SHELL", "TERM", "USER", "PWD"]);
6026
+ INTERPRETER_INJECTION = new Set([
6027
+ "BASH_ENV",
6028
+ "ENV",
6029
+ "BASHOPTS",
6030
+ "SHELLOPTS",
6031
+ "NODE_OPTIONS",
6032
+ "NODE_PATH",
6033
+ "PYTHONSTARTUP",
6034
+ "PYTHONINSPECT",
6035
+ "PYTHONPATH",
6036
+ "PYTHONHOME",
6037
+ "LD_PRELOAD",
6038
+ "LD_LIBRARY_PATH",
6039
+ "GCONV_PATH",
6040
+ "LOCPATH",
6041
+ "PERL5OPT",
6042
+ "RUBYOPT",
6043
+ "NODE_EXTRA_CA_CERTS",
6044
+ "NODE_TLS_REJECT_UNAUTHORIZED",
6045
+ "SSL_CERT_FILE",
6046
+ "SSL_CERT_DIR",
6047
+ "CURL_CA_BUNDLE",
6048
+ "GIT_SSL_CAINFO",
6049
+ "AWS_CA_BUNDLE"
6050
+ ]);
6051
+ INTERPRETER_INJECTION_PREFIXES = ["GIT_CONFIG_"];
5958
6052
  DENY_SUFFIXES = ["KEY", "TOKEN", "SECRET", "PASSWORD", "PASSWD", "CREDENTIAL", "CREDENTIALS", "URL", "URI"];
5959
6053
  DENY_PREFIXES = [
5960
6054
  "HASNA_",
@@ -5972,6 +6066,7 @@ var init_hook_env = __esm(() => {
5972
6066
  "MEMENTOS_"
5973
6067
  ];
5974
6068
  DENY_CONTAINS = ["DATABASE_URL"];
6069
+ PATH_BASELINE = ["/usr/bin", "/bin", "/usr/local/bin", "/sbin", "/usr/sbin"];
5975
6070
  CONFIG_PROJECTIONS = [
5976
6071
  ["HASNA_HOOKS_DATA_DIR", "HOOKS_DATA_DIR"],
5977
6072
  ["HASNA_HOOKS_DB_PATH", "HOOKS_DB_PATH"],
@@ -5988,7 +6083,7 @@ __export(exports_run, {
5988
6083
  });
5989
6084
  import { randomBytes as randomBytes2 } from "crypto";
5990
6085
  import { rmSync as rmSync3, writeFileSync as writeFileSync6 } from "fs";
5991
- import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join9 } from "path";
6086
+ import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join9 } from "path";
5992
6087
  function extensionOf(scriptPath) {
5993
6088
  const base = basename2(scriptPath);
5994
6089
  const dot = base.lastIndexOf(".");
@@ -6015,13 +6110,13 @@ function interpreterFor(name, scriptPath, content) {
6015
6110
  if (interp === "bash" || interp === "sh")
6016
6111
  return { command: ["/bin/bash"], tempExt: "sh" };
6017
6112
  if (interp === "node" || interp === "bun")
6018
- return { command: ["bun", "run"], tempExt: BUN_EXTENSIONS.has(ext) ? ext : "ts" };
6113
+ return { command: [process.execPath, "run"], tempExt: BUN_EXTENSIONS.has(ext) ? ext : "ts" };
6019
6114
  throw new Error(`Refusing to run hook '${name}': shebang '${firstLine}' is not a recognized interpreter (supported: bash/sh, node/bun)`);
6020
6115
  }
6021
6116
  if (BASH_EXTENSIONS.has(ext))
6022
6117
  return { command: ["/bin/bash"], tempExt: "sh" };
6023
6118
  if (BUN_EXTENSIONS.has(ext))
6024
- return { command: ["bun", "run"], tempExt: ext };
6119
+ return { command: [process.execPath, "run"], tempExt: ext };
6025
6120
  throw new Error(`Refusing to run hook '${name}': unsupported script extension '.${ext}' (supported: .sh .bash .ts .tsx .js .jsx .mjs .cjs .mts .cts, or a recognized shebang)`);
6026
6121
  }
6027
6122
  async function readPipeWithDeadline(stream, deadlineMs) {
@@ -6057,7 +6152,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
6057
6152
  return chunks.join("");
6058
6153
  }
6059
6154
  async function executeVerifiedScript(options) {
6060
- const scriptDir = dirname4(options.scriptPath);
6155
+ const scriptDir = dirname5(options.scriptPath);
6061
6156
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
6062
6157
  const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
6063
6158
  let proc = null;
@@ -6068,7 +6163,7 @@ async function executeVerifiedScript(options) {
6068
6163
  }, options.timeout) : null;
6069
6164
  try {
6070
6165
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
6071
- proc = Bun.spawn([...interpreter.command, isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
6166
+ proc = Bun.spawn([...interpreter.command, isAbsolute3(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
6072
6167
  stdin: new Response(options.stdin),
6073
6168
  stdout: "pipe",
6074
6169
  stderr: "pipe",
@@ -6150,12 +6245,12 @@ __export(exports_custom_install, {
6150
6245
  installCustomSource: () => installCustomSource,
6151
6246
  ensureCustomDir: () => ensureCustomDir
6152
6247
  });
6153
- import { existsSync as existsSync10, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
6248
+ import { existsSync as existsSync11, mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync7, rmSync as rmSync4 } from "fs";
6154
6249
  import { join as join10 } from "path";
6155
6250
  import { tmpdir } from "os";
6156
6251
  function manifestAt(dir) {
6157
6252
  const path = join10(dir, "manifest.json");
6158
- if (!existsSync10(path))
6253
+ if (!existsSync11(path))
6159
6254
  return;
6160
6255
  try {
6161
6256
  return parseManifest(readFileSync7(path, "utf-8"));
@@ -6201,7 +6296,7 @@ function isCustomSource(value) {
6201
6296
  return true;
6202
6297
  if (value.startsWith("http://") || value.startsWith("https://"))
6203
6298
  return true;
6204
- return existsSync10(value);
6299
+ return existsSync11(value);
6205
6300
  }
6206
6301
  async function installCustomSource(source) {
6207
6302
  if (isGitUrl(source)) {
@@ -6215,7 +6310,7 @@ async function installCustomSource(source) {
6215
6310
  if (source.startsWith("http://") || source.startsWith("https://")) {
6216
6311
  return installManifestUrl(source);
6217
6312
  }
6218
- if (!existsSync10(source)) {
6313
+ if (!existsSync11(source)) {
6219
6314
  throw new Error(`No such local path: ${source}`);
6220
6315
  }
6221
6316
  if (source.endsWith("manifest.json")) {
@@ -7278,14 +7373,14 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
7278
7373
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7279
7374
  import { z as z2 } from "zod";
7280
7375
  import { createServer } from "http";
7281
- import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
7282
- import { join as join11, dirname as dirname5 } from "path";
7376
+ import { existsSync as existsSync12, readFileSync as readFileSync9 } from "fs";
7377
+ import { join as join11, dirname as dirname6 } from "path";
7283
7378
  import { fileURLToPath as fileURLToPath3 } from "url";
7284
7379
  async function runVerifiedHook(name, stdin, timeoutMs) {
7285
7380
  const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
7286
7381
  const resolved = resolveHook2(name);
7287
7382
  const hookScript = resolveScriptPath2(name);
7288
- if (!resolved || !hookScript || !existsSync11(hookScript)) {
7383
+ if (!resolved || !hookScript || !existsSync12(hookScript)) {
7289
7384
  throw new Error(`Hook '${name}' not found`);
7290
7385
  }
7291
7386
  const content = readFileSync9(hookScript);
@@ -7493,7 +7588,7 @@ function createHooksServer() {
7493
7588
  const settingsPath = getSettingsPath(scope);
7494
7589
  const issues = [];
7495
7590
  const healthy = [];
7496
- const settingsExist = existsSync11(settingsPath);
7591
+ const settingsExist = existsSync12(settingsPath);
7497
7592
  if (!settingsExist) {
7498
7593
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
7499
7594
  }
@@ -7503,7 +7598,7 @@ function createHooksServer() {
7503
7598
  const meta = custom ? resolveHookMeta(name) : getHook(name);
7504
7599
  let hookHealthy = true;
7505
7600
  if (custom) {
7506
- if (!existsSync11(custom.scriptPath)) {
7601
+ if (!existsSync12(custom.scriptPath)) {
7507
7602
  issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
7508
7603
  hookHealthy = false;
7509
7604
  }
@@ -7513,7 +7608,7 @@ function createHooksServer() {
7513
7608
  continue;
7514
7609
  }
7515
7610
  const hookDir = getHookPath(name);
7516
- if (!existsSync11(join11(hookDir, "src", "hook.ts"))) {
7611
+ if (!existsSync12(join11(hookDir, "src", "hook.ts"))) {
7517
7612
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
7518
7613
  hookHealthy = false;
7519
7614
  }
@@ -7552,7 +7647,7 @@ function createHooksServer() {
7552
7647
  const hookPath = getHookPath(name);
7553
7648
  const readmePath = join11(hookPath, "README.md");
7554
7649
  let readme = "";
7555
- if (existsSync11(readmePath)) {
7650
+ if (existsSync12(readmePath)) {
7556
7651
  readme = readFileSync9(readmePath, "utf-8");
7557
7652
  }
7558
7653
  if (verbose) {
@@ -7736,7 +7831,7 @@ function createHooksServer() {
7736
7831
  const ctx = {
7737
7832
  scope,
7738
7833
  settings_path: settingsPath,
7739
- settings_exists: existsSync11(settingsPath),
7834
+ settings_exists: existsSync12(settingsPath),
7740
7835
  registered_hooks: hooks,
7741
7836
  hook_count: hooks.length,
7742
7837
  healthy,
@@ -7882,7 +7977,7 @@ function createHooksServer() {
7882
7977
  const settingsPath = getSettingsPath(scope);
7883
7978
  let settings = {};
7884
7979
  try {
7885
- if (existsSync11(settingsPath))
7980
+ if (existsSync12(settingsPath))
7886
7981
  settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
7887
7982
  } catch {}
7888
7983
  if (!settings.hooks)
@@ -7892,8 +7987,8 @@ function createHooksServer() {
7892
7987
  disabled.push(name);
7893
7988
  settings.hooks.__disabled = disabled;
7894
7989
  const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync8 } = await import("fs");
7895
- const { dirname: dirname6 } = await import("path");
7896
- mkdirSync8(dirname6(settingsPath), { recursive: true });
7990
+ const { dirname: dirname7 } = await import("path");
7991
+ mkdirSync8(dirname7(settingsPath), { recursive: true });
7897
7992
  writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
7898
7993
  `);
7899
7994
  return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
@@ -7905,7 +8000,7 @@ function createHooksServer() {
7905
8000
  const settingsPath = getSettingsPath(scope);
7906
8001
  let settings = {};
7907
8002
  try {
7908
- if (existsSync11(settingsPath))
8003
+ if (existsSync12(settingsPath))
7909
8004
  settings = JSON.parse(readFileSync9(settingsPath, "utf-8"));
7910
8005
  } catch {}
7911
8006
  if (settings.hooks?.__disabled) {
@@ -8266,12 +8361,12 @@ var init_server = __esm(() => {
8266
8361
  init_store();
8267
8362
  init_redact();
8268
8363
  init_storage();
8269
- __dirname3 = dirname5(fileURLToPath3(import.meta.url));
8364
+ __dirname3 = dirname6(fileURLToPath3(import.meta.url));
8270
8365
  pkg = { name: "@hasna/hooks", version: "0.0.0" };
8271
8366
  try {
8272
8367
  for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
8273
8368
  const p = join11(__dirname3, rel);
8274
- if (existsSync11(p)) {
8369
+ if (existsSync12(p)) {
8275
8370
  pkg = JSON.parse(readFileSync9(p, "utf-8"));
8276
8371
  break;
8277
8372
  }
@@ -8340,7 +8435,7 @@ function startMcpHttpServer(options) {
8340
8435
  var DEFAULT_MCP_HTTP_PORT = 8847, MCP_HTTP_HOST = "127.0.0.1", MCP_SERVICE_NAME = "hooks";
8341
8436
  var init_http = () => {};
8342
8437
 
8343
- // node_modules/@hasna/events/dist/commander.js
8438
+ // ../events/dist/commander.js
8344
8439
  import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
8345
8440
  import { Buffer as Buffer2 } from "buffer";
8346
8441
  import { existsSync } from "fs";
@@ -9525,7 +9620,7 @@ function replaySummary(events, deliveries, nextCursor) {
9525
9620
  // src/cli/index.tsx
9526
9621
  import { render } from "ink";
9527
9622
 
9528
- // node_modules/commander/esm.mjs
9623
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/esm.mjs
9529
9624
  var import__ = __toESM(require_commander(), 1);
9530
9625
  var {
9531
9626
  program,
@@ -9543,19 +9638,19 @@ var {
9543
9638
 
9544
9639
  // src/cli/index.tsx
9545
9640
  import chalk2 from "chalk";
9546
- import { existsSync as existsSync12, readFileSync as readFileSync10, rmSync as rmSync5 } from "fs";
9547
- import { join as join12, dirname as dirname6 } from "path";
9641
+ import { existsSync as existsSync13, readFileSync as readFileSync10, rmSync as rmSync5 } from "fs";
9642
+ import { join as join12, dirname as dirname7 } from "path";
9548
9643
  import { fileURLToPath as fileURLToPath4 } from "url";
9549
9644
 
9550
9645
  // src/cli/components/App.tsx
9551
9646
  import { useState as useState7 } from "react";
9552
9647
  import { Box as Box8, Text as Text10, useApp, useInput as useInput5 } from "ink";
9553
9648
 
9554
- // node_modules/ink-select-input/build/Indicator.js
9649
+ // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
9555
9650
  import React from "react";
9556
9651
  import { Box, Text } from "ink";
9557
9652
 
9558
- // node_modules/is-unicode-supported/index.js
9653
+ // ../../node_modules/.bun/is-unicode-supported@2.1.0/node_modules/is-unicode-supported/index.js
9559
9654
  import process2 from "process";
9560
9655
  function isUnicodeSupported() {
9561
9656
  const { env } = process2;
@@ -9566,7 +9661,7 @@ function isUnicodeSupported() {
9566
9661
  return Boolean(env.WT_SESSION) || Boolean(env.TERMINUS_SUBLIME) || env.ConEmuTask === "{cmd::Cmder}" || TERM_PROGRAM === "Terminus-Sublime" || TERM_PROGRAM === "vscode" || TERM === "xterm-256color" || TERM === "alacritty" || TERM === "rxvt-unicode" || TERM === "rxvt-unicode-256color" || env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
9567
9662
  }
9568
9663
 
9569
- // node_modules/figures/index.js
9664
+ // ../../node_modules/.bun/figures@6.1.0/node_modules/figures/index.js
9570
9665
  var common = {
9571
9666
  circleQuestionMark: "(?)",
9572
9667
  questionMarkPrefix: "(?)",
@@ -9842,23 +9937,23 @@ var figures = shouldUseMain ? mainSymbols : fallbackSymbols;
9842
9937
  var figures_default = figures;
9843
9938
  var replacements = Object.entries(specialMainSymbols);
9844
9939
 
9845
- // node_modules/ink-select-input/build/Indicator.js
9940
+ // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
9846
9941
  function Indicator({ isSelected = false }) {
9847
9942
  return React.createElement(Box, { marginRight: 1 }, isSelected ? React.createElement(Text, { color: "blue" }, figures_default.pointer) : React.createElement(Text, null, " "));
9848
9943
  }
9849
9944
  var Indicator_default = Indicator;
9850
- // node_modules/ink-select-input/build/Item.js
9945
+ // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Item.js
9851
9946
  import * as React2 from "react";
9852
9947
  import { Text as Text2 } from "ink";
9853
9948
  function Item({ isSelected = false, label }) {
9854
9949
  return React2.createElement(Text2, { color: isSelected ? "blue" : undefined }, label);
9855
9950
  }
9856
9951
  var Item_default = Item;
9857
- // node_modules/ink-select-input/build/SelectInput.js
9952
+ // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
9858
9953
  import { isDeepStrictEqual } from "util";
9859
9954
  import React3, { useState, useEffect, useRef, useCallback } from "react";
9860
9955
 
9861
- // node_modules/to-rotated/index.js
9956
+ // ../../node_modules/.bun/to-rotated@1.0.0/node_modules/to-rotated/index.js
9862
9957
  function toRotated(array, steps) {
9863
9958
  if (!Array.isArray(array)) {
9864
9959
  throw new TypeError(`Expected an array, got \`${typeof array}\`.`);
@@ -9880,7 +9975,7 @@ function toRotated(array, steps) {
9880
9975
  ];
9881
9976
  }
9882
9977
 
9883
- // node_modules/ink-select-input/build/SelectInput.js
9978
+ // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
9884
9979
  import { Box as Box2, useInput } from "ink";
9885
9980
  function SelectInput({ items = [], isFocused = true, initialIndex = 0, indicatorComponent = Indicator_default, itemComponent = Item_default, limit: customLimit, onSelect, onHighlight }) {
9886
9981
  const hasLimit = typeof customLimit === "number" && items.length > customLimit;
@@ -10176,7 +10271,7 @@ function HookSelect({
10176
10271
  import { useState as useState4, useEffect as useEffect3 } from "react";
10177
10272
  import { Box as Box6, Text as Text7, useInput as useInput4 } from "ink";
10178
10273
 
10179
- // node_modules/ink-text-input/build/index.js
10274
+ // ../../node_modules/.bun/ink-text-input@6.0.0+fa875fef9f949ebd/node_modules/ink-text-input/build/index.js
10180
10275
  import React5, { useState as useState3, useEffect as useEffect2 } from "react";
10181
10276
  import { Text as Text6, useInput as useInput3 } from "ink";
10182
10277
  import chalk from "chalk";
@@ -10354,7 +10449,7 @@ function SearchView({
10354
10449
  import { useState as useState6, useEffect as useEffect5 } from "react";
10355
10450
  import { Box as Box7, Text as Text9 } from "ink";
10356
10451
 
10357
- // node_modules/ink-spinner/build/index.js
10452
+ // ../../node_modules/.bun/ink-spinner@5.0.0+fa875fef9f949ebd/node_modules/ink-spinner/build/index.js
10358
10453
  var import_cli_spinners = __toESM(require_cli_spinners(), 1);
10359
10454
  import React7, { useState as useState5, useEffect as useEffect4 } from "react";
10360
10455
  import { Text as Text8 } from "ink";
@@ -10726,8 +10821,8 @@ init_resolve();
10726
10821
  init_store();
10727
10822
  init_semver();
10728
10823
  import { jsxDEV as jsxDEV8 } from "react/jsx-dev-runtime";
10729
- var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
10730
- var pkgPath = existsSync12(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
10824
+ var __dirname4 = dirname7(fileURLToPath4(import.meta.url));
10825
+ var pkgPath = existsSync13(join12(__dirname4, "..", "package.json")) ? join12(__dirname4, "..", "package.json") : join12(__dirname4, "..", "..", "package.json");
10731
10826
  var pkg2 = JSON.parse(readFileSync10(pkgPath, "utf-8"));
10732
10827
  var program2 = new Command;
10733
10828
  function resolveScope(options) {
@@ -10895,7 +10990,7 @@ program2.command("run").argument("<hook>", "Hook to run").option("--profile <id>
10895
10990
  process.exit(1);
10896
10991
  }
10897
10992
  const hookScript = resolveScriptPath2(hook);
10898
- if (!hookScript || !existsSync12(hookScript)) {
10993
+ if (!hookScript || !existsSync13(hookScript)) {
10899
10994
  console.error(JSON.stringify({ error: `Hook script not found: ${hookScript ?? hook}` }));
10900
10995
  process.exit(1);
10901
10996
  }
@@ -11467,7 +11562,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
11467
11562
  const settingsPath = getSettingsPath(scope);
11468
11563
  const issues = [];
11469
11564
  const healthy = [];
11470
- const settingsExist = existsSync12(settingsPath);
11565
+ const settingsExist = existsSync13(settingsPath);
11471
11566
  if (!settingsExist) {
11472
11567
  issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
11473
11568
  }
@@ -11484,7 +11579,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
11484
11579
  const meta = custom ? resolveHookMeta(name) : getHook(name);
11485
11580
  let hookHealthy = true;
11486
11581
  if (custom) {
11487
- if (!existsSync12(custom.scriptPath)) {
11582
+ if (!existsSync13(custom.scriptPath)) {
11488
11583
  issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
11489
11584
  hookHealthy = false;
11490
11585
  }
@@ -11496,7 +11591,7 @@ program2.command("doctor").option("-g, --global", "Check global settings", false
11496
11591
  }
11497
11592
  const hookDir = getHookPath(name);
11498
11593
  const hookScript = join12(hookDir, "src", "hook.ts");
11499
- if (!existsSync12(hookScript)) {
11594
+ if (!existsSync13(hookScript)) {
11500
11595
  issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
11501
11596
  hookHealthy = false;
11502
11597
  }
@@ -11672,7 +11767,7 @@ program2.command("docs").argument("[hook]", "Hook name (shows general docs if om
11672
11767
  const hookPath = getHookPath(hook);
11673
11768
  const readmePath = join12(hookPath, "README.md");
11674
11769
  let readme = "";
11675
- if (existsSync12(readmePath)) {
11770
+ if (existsSync13(readmePath)) {
11676
11771
  readme = readFileSync10(readmePath, "utf-8");
11677
11772
  }
11678
11773
  if (options.json) {
package/bin/serve.js CHANGED
@@ -527,7 +527,7 @@ function getHook(name) {
527
527
  import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "fs";
528
528
  import { basename, dirname, isAbsolute, join as join2, normalize, resolve, sep } from "path";
529
529
 
530
- // node_modules/zod/v3/external.js
530
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
531
531
  var exports_external = {};
532
532
  __export(exports_external, {
533
533
  void: () => voidType,
@@ -639,7 +639,7 @@ __export(exports_external, {
639
639
  BRAND: () => BRAND
640
640
  });
641
641
 
642
- // node_modules/zod/v3/helpers/util.js
642
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
643
643
  var util;
644
644
  (function(util2) {
645
645
  util2.assertEqual = (_) => {};
@@ -770,7 +770,7 @@ var getParsedType = (data) => {
770
770
  }
771
771
  };
772
772
 
773
- // node_modules/zod/v3/ZodError.js
773
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/ZodError.js
774
774
  var ZodIssueCode = util.arrayToEnum([
775
775
  "invalid_type",
776
776
  "invalid_literal",
@@ -889,7 +889,7 @@ ZodError.create = (issues) => {
889
889
  return error;
890
890
  };
891
891
 
892
- // node_modules/zod/v3/locales/en.js
892
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
893
893
  var errorMap = (issue, _ctx) => {
894
894
  let message;
895
895
  switch (issue.code) {
@@ -992,7 +992,7 @@ var errorMap = (issue, _ctx) => {
992
992
  };
993
993
  var en_default = errorMap;
994
994
 
995
- // node_modules/zod/v3/errors.js
995
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.js
996
996
  var overrideErrorMap = en_default;
997
997
  function setErrorMap(map) {
998
998
  overrideErrorMap = map;
@@ -1000,7 +1000,7 @@ function setErrorMap(map) {
1000
1000
  function getErrorMap() {
1001
1001
  return overrideErrorMap;
1002
1002
  }
1003
- // node_modules/zod/v3/helpers/parseUtil.js
1003
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
1004
1004
  var makeIssue = (params) => {
1005
1005
  const { data, path, errorMaps, issueData } = params;
1006
1006
  const fullPath = [...path, ...issueData.path || []];
@@ -1106,14 +1106,14 @@ var isAborted = (x) => x.status === "aborted";
1106
1106
  var isDirty = (x) => x.status === "dirty";
1107
1107
  var isValid = (x) => x.status === "valid";
1108
1108
  var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
1109
- // node_modules/zod/v3/helpers/errorUtil.js
1109
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
1110
1110
  var errorUtil;
1111
1111
  (function(errorUtil2) {
1112
1112
  errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
1113
1113
  errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
1114
1114
  })(errorUtil || (errorUtil = {}));
1115
1115
 
1116
- // node_modules/zod/v3/types.js
1116
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
1117
1117
  class ParseInputLazyPath {
1118
1118
  constructor(parent, value, path, key) {
1119
1119
  this._cachedPath = [];
@@ -4523,7 +4523,9 @@ function resolveApiKey() {
4523
4523
  }
4524
4524
 
4525
4525
  // src/lib/semver.ts
4526
- var SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
4526
+ var NUMERIC_ID = "(0|[1-9]\\d{0,15})";
4527
+ var ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*";
4528
+ var SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
4527
4529
 
4528
4530
  // src/lib/manifest.ts
4529
4531
  var HOOK_NAME_RE = /^[\w-]+$/;
@@ -4535,7 +4537,8 @@ var manifestSchema = exports_external.object({
4535
4537
  script: exports_external.string().min(1, "script must name a relative path or inline content"),
4536
4538
  script_kind: exports_external.enum(["inline", "file"]).optional(),
4537
4539
  args: exports_external.array(exports_external.string()).optional(),
4538
- timeout_ms: exports_external.number().int().positive().optional()
4540
+ timeout_ms: exports_external.number().int().positive().optional(),
4541
+ env: exports_external.record(exports_external.string(), exports_external.string()).optional()
4539
4542
  });
4540
4543
  function parseManifest(json) {
4541
4544
  const parsed = JSON.parse(json);
package/dist/index.js CHANGED
@@ -453,7 +453,7 @@ var init_redact = __esm(() => {
453
453
  ];
454
454
  });
455
455
 
456
- // node_modules/zod/v3/helpers/util.js
456
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
457
457
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
458
458
  const t = typeof data;
459
459
  switch (t) {
@@ -584,7 +584,7 @@ var init_util = __esm(() => {
584
584
  ]);
585
585
  });
586
586
 
587
- // node_modules/zod/v3/ZodError.js
587
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/ZodError.js
588
588
  var ZodIssueCode, quotelessJson = (obj) => {
589
589
  const json = JSON.stringify(obj, null, 2);
590
590
  return json.replace(/"([^"]+)":/g, "$1:");
@@ -705,7 +705,7 @@ var init_ZodError = __esm(() => {
705
705
  };
706
706
  });
707
707
 
708
- // node_modules/zod/v3/locales/en.js
708
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
709
709
  var errorMap = (issue, _ctx) => {
710
710
  let message;
711
711
  switch (issue.code) {
@@ -812,7 +812,7 @@ var init_en = __esm(() => {
812
812
  en_default = errorMap;
813
813
  });
814
814
 
815
- // node_modules/zod/v3/errors.js
815
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.js
816
816
  function setErrorMap(map) {
817
817
  overrideErrorMap = map;
818
818
  }
@@ -825,7 +825,7 @@ var init_errors = __esm(() => {
825
825
  overrideErrorMap = en_default;
826
826
  });
827
827
 
828
- // node_modules/zod/v3/helpers/parseUtil.js
828
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
829
829
  function addIssueToContext(ctx, issueData) {
830
830
  const overrideMap = getErrorMap();
831
831
  const issue = makeIssue({
@@ -930,10 +930,10 @@ var init_parseUtil = __esm(() => {
930
930
  });
931
931
  });
932
932
 
933
- // node_modules/zod/v3/helpers/typeAliases.js
933
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/typeAliases.js
934
934
  var init_typeAliases = () => {};
935
935
 
936
- // node_modules/zod/v3/helpers/errorUtil.js
936
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
937
937
  var errorUtil;
938
938
  var init_errorUtil = __esm(() => {
939
939
  (function(errorUtil2) {
@@ -942,7 +942,7 @@ var init_errorUtil = __esm(() => {
942
942
  })(errorUtil || (errorUtil = {}));
943
943
  });
944
944
 
945
- // node_modules/zod/v3/types.js
945
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
946
946
  class ParseInputLazyPath {
947
947
  constructor(parent, value, path, key) {
948
948
  this._cachedPath = [];
@@ -4293,7 +4293,7 @@ var init_types = __esm(() => {
4293
4293
  NEVER = INVALID;
4294
4294
  });
4295
4295
 
4296
- // node_modules/zod/v3/external.js
4296
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
4297
4297
  var exports_external = {};
4298
4298
  __export(exports_external, {
4299
4299
  void: () => voidType,
@@ -4413,7 +4413,7 @@ var init_external = __esm(() => {
4413
4413
  init_ZodError();
4414
4414
  });
4415
4415
 
4416
- // node_modules/zod/index.js
4416
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/index.js
4417
4417
  var init_zod = __esm(() => {
4418
4418
  init_external();
4419
4419
  init_external();
@@ -4474,9 +4474,9 @@ function resolveApiKey() {
4474
4474
  var init_config = () => {};
4475
4475
 
4476
4476
  // src/lib/semver.ts
4477
- var SEMVER_PATTERN;
4477
+ var NUMERIC_ID = "(0|[1-9]\\d{0,15})", ALNUM_ID = "\\d*[A-Za-z-][0-9A-Za-z-]*", SEMVER_PATTERN;
4478
4478
  var init_semver = __esm(() => {
4479
- SEMVER_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
4479
+ SEMVER_PATTERN = new RegExp(`^${NUMERIC_ID}\\.${NUMERIC_ID}\\.${NUMERIC_ID}` + `(?:-(?:${NUMERIC_ID}|${ALNUM_ID})(?:\\.(?:${NUMERIC_ID}|${ALNUM_ID}))*)?` + `(?:\\+[0-9A-Za-z.-]+)?$`);
4480
4480
  });
4481
4481
 
4482
4482
  // src/lib/manifest.ts
@@ -4638,7 +4638,8 @@ var init_manifest = __esm(() => {
4638
4638
  script: exports_external.string().min(1, "script must name a relative path or inline content"),
4639
4639
  script_kind: exports_external.enum(["inline", "file"]).optional(),
4640
4640
  args: exports_external.array(exports_external.string()).optional(),
4641
- timeout_ms: exports_external.number().int().positive().optional()
4641
+ timeout_ms: exports_external.number().int().positive().optional(),
4642
+ env: exports_external.record(exports_external.string(), exports_external.string()).optional()
4642
4643
  });
4643
4644
  ScriptContainmentError = class ScriptContainmentError extends Error {
4644
4645
  constructor(path, hookDir, raw) {
@@ -5001,6 +5002,8 @@ var init_profiles = __esm(() => {
5001
5002
  });
5002
5003
 
5003
5004
  // src/lib/hook-env.ts
5005
+ import { dirname as dirname4, isAbsolute as isAbsolute2 } from "path";
5006
+ import { existsSync as existsSync10, statSync } from "fs";
5004
5007
  function isDeniedEnvName(name) {
5005
5008
  const upper = name.toUpperCase();
5006
5009
  if (DENY_PREFIXES.some((prefix) => upper.startsWith(prefix)))
@@ -5009,13 +5012,74 @@ function isDeniedEnvName(name) {
5009
5012
  return true;
5010
5013
  return DENY_CONTAINS.some((token) => upper.includes(token));
5011
5014
  }
5015
+ function isInterpreterInjectionEnvName(name) {
5016
+ if (INTERPRETER_INJECTION.has(name))
5017
+ return true;
5018
+ return INTERPRETER_INJECTION_PREFIXES.some((prefix) => name.startsWith(prefix));
5019
+ }
5020
+ function isBashFunctionEnvName(name) {
5021
+ return name.startsWith("BASH_FUNC_");
5022
+ }
5023
+ function isUnsafePathEntry(entry, home) {
5024
+ if (entry === "" || !isAbsolute2(entry))
5025
+ return true;
5026
+ if (home && (entry === home || entry.startsWith(home + "/")))
5027
+ return true;
5028
+ if (entry === "/tmp" || entry.startsWith("/tmp/"))
5029
+ return true;
5030
+ if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
5031
+ return true;
5032
+ try {
5033
+ if ((statSync(entry).mode & 2) !== 0)
5034
+ return true;
5035
+ } catch {}
5036
+ return false;
5037
+ }
5038
+ function sanitizeHookPath(pathValue, home) {
5039
+ const baseline = PATH_BASELINE.slice();
5040
+ if (process.platform === "darwin" && existsSync10("/opt/homebrew/bin"))
5041
+ baseline.push("/opt/homebrew/bin");
5042
+ const bunDir = dirname4(process.execPath);
5043
+ const seen = new Set;
5044
+ const out = [];
5045
+ for (const dir of baseline) {
5046
+ if (seen.has(dir))
5047
+ continue;
5048
+ seen.add(dir);
5049
+ out.push(dir);
5050
+ }
5051
+ if (!seen.has(bunDir)) {
5052
+ seen.add(bunDir);
5053
+ out.push(bunDir);
5054
+ }
5055
+ for (const entry of pathValue.split(":")) {
5056
+ if (seen.has(entry))
5057
+ continue;
5058
+ if (isUnsafePathEntry(entry, home))
5059
+ continue;
5060
+ seen.add(entry);
5061
+ out.push(entry);
5062
+ }
5063
+ return out.join(":");
5064
+ }
5012
5065
  function buildHookEnv(source, extra) {
5013
5066
  const out = {};
5014
5067
  for (const name of ALLOWLIST) {
5068
+ if (name === "PATH")
5069
+ continue;
5015
5070
  const value = source[name] ?? extra?.[name];
5016
5071
  if (value !== undefined && value !== null)
5017
5072
  out[name] = value;
5018
5073
  }
5074
+ const pathOverride = extra?.PATH;
5075
+ if (pathOverride !== undefined && pathOverride !== null) {
5076
+ out.PATH = pathOverride;
5077
+ } else {
5078
+ const parentPath = source.PATH;
5079
+ if (parentPath !== undefined && parentPath !== null) {
5080
+ out.PATH = sanitizeHookPath(parentPath, source.HOME ?? extra?.HOME);
5081
+ }
5082
+ }
5019
5083
  for (const [from, to] of CONFIG_PROJECTIONS) {
5020
5084
  const value = source[from] ?? extra?.[from];
5021
5085
  if (value !== undefined && value !== null)
@@ -5027,14 +5091,44 @@ function buildHookEnv(source, extra) {
5027
5091
  continue;
5028
5092
  if (isDeniedEnvName(name))
5029
5093
  continue;
5094
+ if (isInterpreterInjectionEnvName(name))
5095
+ continue;
5096
+ if (isBashFunctionEnvName(name))
5097
+ continue;
5030
5098
  if (value !== undefined && value !== null)
5031
5099
  out[name] = value;
5032
5100
  }
5033
5101
  return out;
5034
5102
  }
5035
- var ALLOWLIST, DENY_SUFFIXES, DENY_PREFIXES, DENY_CONTAINS, CONFIG_PROJECTIONS;
5103
+ var ALLOWLIST, INTERPRETER_INJECTION, INTERPRETER_INJECTION_PREFIXES, DENY_SUFFIXES, DENY_PREFIXES, DENY_CONTAINS, PATH_BASELINE, CONFIG_PROJECTIONS;
5036
5104
  var init_hook_env = __esm(() => {
5037
5105
  ALLOWLIST = new Set(["PATH", "HOME", "LANG", "TZ", "SHELL", "TERM", "USER", "PWD"]);
5106
+ INTERPRETER_INJECTION = new Set([
5107
+ "BASH_ENV",
5108
+ "ENV",
5109
+ "BASHOPTS",
5110
+ "SHELLOPTS",
5111
+ "NODE_OPTIONS",
5112
+ "NODE_PATH",
5113
+ "PYTHONSTARTUP",
5114
+ "PYTHONINSPECT",
5115
+ "PYTHONPATH",
5116
+ "PYTHONHOME",
5117
+ "LD_PRELOAD",
5118
+ "LD_LIBRARY_PATH",
5119
+ "GCONV_PATH",
5120
+ "LOCPATH",
5121
+ "PERL5OPT",
5122
+ "RUBYOPT",
5123
+ "NODE_EXTRA_CA_CERTS",
5124
+ "NODE_TLS_REJECT_UNAUTHORIZED",
5125
+ "SSL_CERT_FILE",
5126
+ "SSL_CERT_DIR",
5127
+ "CURL_CA_BUNDLE",
5128
+ "GIT_SSL_CAINFO",
5129
+ "AWS_CA_BUNDLE"
5130
+ ]);
5131
+ INTERPRETER_INJECTION_PREFIXES = ["GIT_CONFIG_"];
5038
5132
  DENY_SUFFIXES = ["KEY", "TOKEN", "SECRET", "PASSWORD", "PASSWD", "CREDENTIAL", "CREDENTIALS", "URL", "URI"];
5039
5133
  DENY_PREFIXES = [
5040
5134
  "HASNA_",
@@ -5052,6 +5146,7 @@ var init_hook_env = __esm(() => {
5052
5146
  "MEMENTOS_"
5053
5147
  ];
5054
5148
  DENY_CONTAINS = ["DATABASE_URL"];
5149
+ PATH_BASELINE = ["/usr/bin", "/bin", "/usr/local/bin", "/sbin", "/usr/sbin"];
5055
5150
  CONFIG_PROJECTIONS = [
5056
5151
  ["HASNA_HOOKS_DATA_DIR", "HOOKS_DATA_DIR"],
5057
5152
  ["HASNA_HOOKS_DB_PATH", "HOOKS_DB_PATH"],
@@ -5068,7 +5163,7 @@ __export(exports_run, {
5068
5163
  });
5069
5164
  import { randomBytes as randomBytes2 } from "crypto";
5070
5165
  import { rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
5071
- import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute2, join as join9 } from "path";
5166
+ import { basename as basename2, dirname as dirname5, isAbsolute as isAbsolute3, join as join9 } from "path";
5072
5167
  function extensionOf(scriptPath) {
5073
5168
  const base = basename2(scriptPath);
5074
5169
  const dot = base.lastIndexOf(".");
@@ -5095,13 +5190,13 @@ function interpreterFor(name, scriptPath, content) {
5095
5190
  if (interp === "bash" || interp === "sh")
5096
5191
  return { command: ["/bin/bash"], tempExt: "sh" };
5097
5192
  if (interp === "node" || interp === "bun")
5098
- return { command: ["bun", "run"], tempExt: BUN_EXTENSIONS.has(ext) ? ext : "ts" };
5193
+ return { command: [process.execPath, "run"], tempExt: BUN_EXTENSIONS.has(ext) ? ext : "ts" };
5099
5194
  throw new Error(`Refusing to run hook '${name}': shebang '${firstLine}' is not a recognized interpreter (supported: bash/sh, node/bun)`);
5100
5195
  }
5101
5196
  if (BASH_EXTENSIONS.has(ext))
5102
5197
  return { command: ["/bin/bash"], tempExt: "sh" };
5103
5198
  if (BUN_EXTENSIONS.has(ext))
5104
- return { command: ["bun", "run"], tempExt: ext };
5199
+ return { command: [process.execPath, "run"], tempExt: ext };
5105
5200
  throw new Error(`Refusing to run hook '${name}': unsupported script extension '.${ext}' (supported: .sh .bash .ts .tsx .js .jsx .mjs .cjs .mts .cts, or a recognized shebang)`);
5106
5201
  }
5107
5202
  async function readPipeWithDeadline(stream, deadlineMs) {
@@ -5137,7 +5232,7 @@ async function readPipeWithDeadline(stream, deadlineMs) {
5137
5232
  return chunks.join("");
5138
5233
  }
5139
5234
  async function executeVerifiedScript(options) {
5140
- const scriptDir = dirname4(options.scriptPath);
5235
+ const scriptDir = dirname5(options.scriptPath);
5141
5236
  const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
5142
5237
  const tempPath = join9(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
5143
5238
  let proc = null;
@@ -5148,7 +5243,7 @@ async function executeVerifiedScript(options) {
5148
5243
  }, options.timeout) : null;
5149
5244
  try {
5150
5245
  writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
5151
- proc = Bun.spawn([...interpreter.command, isAbsolute2(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
5246
+ proc = Bun.spawn([...interpreter.command, isAbsolute3(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
5152
5247
  stdin: new Response(options.stdin),
5153
5248
  stdout: "pipe",
5154
5249
  stderr: "pipe",
@@ -6964,7 +7059,7 @@ init_profiles();
6964
7059
  init_config();
6965
7060
  init_store();
6966
7061
  init_manifest();
6967
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
7062
+ import { existsSync as existsSync11, readFileSync as readFileSync9 } from "fs";
6968
7063
 
6969
7064
  // src/lib/custom-install.ts
6970
7065
  init_config();
@@ -7594,7 +7689,7 @@ async function runHook(name, input, options = {}) {
7594
7689
  throw new Error(`Hook '${name}' not found`);
7595
7690
  const hookScript = getHookPath(name) + "/src/hook.ts";
7596
7691
  const resolvedScript = custom2?.scriptPath;
7597
- const script = resolvedScript ?? (existsSync10(hookScript) ? hookScript : undefined);
7692
+ const script = resolvedScript ?? (existsSync11(hookScript) ? hookScript : undefined);
7598
7693
  if (!script)
7599
7694
  throw new Error(`Hook script not found: ${name}`);
7600
7695
  const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
@@ -7629,6 +7724,7 @@ async function runHook(name, input, options = {}) {
7629
7724
  scriptPath: script,
7630
7725
  content,
7631
7726
  args: custom2?.manifest.args ?? [],
7727
+ env: custom2?.manifest.env,
7632
7728
  stdin: JSON.stringify(hookInput),
7633
7729
  timeout: effectiveTimeout ?? undefined
7634
7730
  }));
@@ -6,8 +6,10 @@
6
6
  * parent's process.env wholesale hands every credential the agent can reach
7
7
  * to arbitrary code. The child gets:
8
8
  *
9
- * 1. a fixed allowlist of non-secret session variables (PATH, HOME, LANG,
10
- * TZ, SHELL, TERM, USER, PWD);
9
+ * 1. a fixed allowlist of non-secret session variables (HOME, LANG, TZ,
10
+ * SHELL, TERM, USER, PWD) plus PATH — PATH rebuilt from a trusted
11
+ * baseline with writable-directory entries removed (or a per-hook
12
+ * manifest env.PATH override, verbatim);
11
13
  * 2. non-secret HOOKS_* projections of the parent's HASNA_HOOKS_* config
12
14
  * (data dir, DB path, lock path, config path) so in-process DB/config
13
15
  * resolution inside the hook still finds the store the parent configured;
@@ -24,6 +26,20 @@
24
26
  * ANTHROPIC_, POSTGRES_, MYSQL_, REDIS_, MONGO_, MEMENTOS_
25
27
  * contains: DATABASE_URL (any variant)
26
28
  *
29
+ * The deny list alone is NOT the whole boundary (bug cf99cf76): it strips
30
+ * credential-shaped NAMES from the parent env, but a credential can be
31
+ * re-imported from a FILE through interpreter machinery. BASH_ENV tells bash
32
+ * to source a file before running any non-interactive script, and ENV does
33
+ * the same for interactive shells — a parent whose BASH_ENV points at e.g.
34
+ * hasna-cloud-env.sh hands the hook child a process that re-exports the
35
+ * fleet credential env AFTER the deny list ran. The interpreter-injection
36
+ * set below strips every variable that makes a child interpreter run or
37
+ * source code the hook did not ask for (sourcing vectors, interpreter option
38
+ * lists, module/startup injection, dynamic-loader preloads, TLS-trust
39
+ * redirection, gconv/locale module injection). Exact case-sensitive names:
40
+ * POSIX interpreters read these literals, and a differently-cased variant is
41
+ * an inert, unrelated variable.
42
+ *
27
43
  * Names that merely resemble credentials (e.g. HOOKS_DATA_DIR) survive; a
28
44
  * path is not a credential.
29
45
  */
@@ -32,11 +48,51 @@
32
48
  * hook child process. Case-insensitive.
33
49
  */
34
50
  export declare function isDeniedEnvName(name: string): boolean;
51
+ /**
52
+ * True when a variable NAME is interpreter-injection (bug cf99cf76): it can
53
+ * make a child interpreter run or source code the hook did not ask for.
54
+ * Exact case-sensitive match — interpreters read these literals verbatim.
55
+ */
56
+ export declare function isInterpreterInjectionEnvName(name: string): boolean;
57
+ /**
58
+ * True when a variable NAME is a bash exported-function entry
59
+ * (BASH_FUNC_<name>%%): bash imports EXPORTED FUNCTIONS from the environment
60
+ * into the child shell, where they shadow commands (env/cat/git/node) and
61
+ * run attacker code on the hook's first command. The %% suffix is bash's
62
+ * export encoding; the prefix test covers every form (%% , single %, none).
63
+ *
64
+ * Aliases are deliberately NOT denied: bash cannot import aliases from the
65
+ * environment (BASH_ALIASES is an associative array and is never exported) —
66
+ * functions are the only env-importable code vector for bash, so the strip
67
+ * is precisely the BASH_FUNC_* prefix.
68
+ */
69
+ export declare function isBashFunctionEnvName(name: string): boolean;
70
+ /**
71
+ * True when a PATH entry may hold attacker-controlled executables: an empty
72
+ * or relative entry (resolves against the child's cwd), an entry under the
73
+ * user's home, an entry under a shared tmp tree, or a world-writable
74
+ * directory (any user can drop a fake binary there).
75
+ */
76
+ export declare function isUnsafePathEntry(entry: string, home: string | undefined): boolean;
77
+ /**
78
+ * Rebuild a PATH for the hook child from a trusted baseline: the system
79
+ * directories (plus /opt/homebrew/bin on macOS when present) and the
80
+ * runner's own bun directory (the interpreter the hook runs under, trusted
81
+ * by definition even when it lives under $HOME). Parent entries are kept
82
+ * only when they do not live under $HOME, /tmp, /var/tmp, or a world-writable
83
+ * path — a fake `node`/`git` planted in a writable directory must never
84
+ * execute on the hook's first command. An explicit per-hook env.PATH
85
+ * (manifest) override bypasses this entirely: the hook author declares the
86
+ * complete PATH it needs.
87
+ */
88
+ export declare function sanitizeHookPath(pathValue: string, home: string | undefined): string;
35
89
  /**
36
90
  * Build the sanitized environment for a hook child process.
37
91
  *
38
92
  * `source` is the parent's environment; `extra` is any caller-supplied env
39
- * the hook may need. The deny list applies to BOTH — a caller cannot
40
- * reintroduce a credential-bearing name.
93
+ * the hook may need. The deny list AND the interpreter-injection set apply
94
+ * to BOTH — a caller cannot reintroduce a credential-bearing name, and
95
+ * cannot reintroduce a variable that would make the child's interpreter
96
+ * source or run code (bug cf99cf76).
41
97
  */
42
98
  export declare function buildHookEnv(source: Record<string, string | undefined>, extra?: Record<string, string | undefined>): Record<string, string>;
@@ -18,11 +18,21 @@ export declare const manifestSchema: z.ZodObject<{
18
18
  script_kind: z.ZodOptional<z.ZodEnum<["inline", "file"]>>;
19
19
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
20
20
  timeout_ms: z.ZodOptional<z.ZodNumber>;
21
+ /**
22
+ * Per-hook environment passed to the hook child (reviewer efcad315).
23
+ * Every name is still filtered through the credential deny list and the
24
+ * interpreter-injection strip; `env.PATH` here is the documented way to
25
+ * override the sanitized default PATH — it is passed verbatim, bypassing
26
+ * PATH sanitization (the hook author declares the complete PATH the hook
27
+ * needs).
28
+ */
29
+ env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
21
30
  }, "strip", z.ZodTypeAny, {
22
31
  version: string;
23
32
  events: string[];
24
33
  name: string;
25
34
  script: string;
35
+ env?: Record<string, string> | undefined;
26
36
  description?: string | undefined;
27
37
  script_kind?: "inline" | "file" | undefined;
28
38
  args?: string[] | undefined;
@@ -32,6 +42,7 @@ export declare const manifestSchema: z.ZodObject<{
32
42
  events: string[];
33
43
  name: string;
34
44
  script: string;
45
+ env?: Record<string, string> | undefined;
35
46
  description?: string | undefined;
36
47
  script_kind?: "inline" | "file" | undefined;
37
48
  args?: string[] | undefined;
@@ -7,6 +7,13 @@
7
7
  * while the artifact routes demanded a bare `\d+\.\d+\.\d+` at the end of
8
8
  * the path, so a pinned prerelease install 404'd. This is the one pattern:
9
9
  * full semver with optional prerelease and build metadata.
10
+ *
11
+ * Numeric identifiers are STRICT (reviewer efcad315): per semver.org §2,
12
+ * numeric identifiers MUST NOT carry leading zeroes ("1.0.0-01" is not
13
+ * "1.0.0-1" — it is not semver at all), and identifiers longer than 16
14
+ * digits are rejected as invalid so no surface can ever store a version the
15
+ * comparator cannot order exactly (Number() precision ends at
16
+ * MAX_SAFE_INTEGER; the comparator itself uses BigInt, see compareVersions).
10
17
  */
11
18
  export declare const SEMVER_PATTERN: RegExp;
12
19
  /**
@@ -17,3 +24,23 @@ export declare const SEMVER_PATTERN: RegExp;
17
24
  export declare const SEMVER_PATH_SEGMENT = "[0-9A-Za-z.+_-]+";
18
25
  export declare function isValidSemver(value: string): boolean;
19
26
  export declare function semverError(value: string): string;
27
+ /**
28
+ * Full semver precedence (semver.org §11), shared by every surface that
29
+ * must ORDER published versions — the registry latest pointer (bug
30
+ * 6e412e52) compares with this and nothing else.
31
+ *
32
+ * Returns <0 when a sorts before b, 0 when equal, >0 when a sorts after b.
33
+ * Build metadata never participates. Prerelease identifiers compare by the
34
+ * spec's rules: numeric identifiers numerically, numeric before alphanumeric,
35
+ * fewer identifiers before more, release before any prerelease.
36
+ *
37
+ * Numeric identifiers compare as BigInt (reviewer efcad315): Number()
38
+ * loses precision past MAX_SAFE_INTEGER, which made near-16-digit numeric
39
+ * identifiers compare equal and made "1.0.0-01" vs "1.0.0-1" compare >0 in
40
+ * BOTH directions (Number("01") === Number("1") while the strings differ).
41
+ * BigInt keeps the comparison exact and antisymmetric at any length; the
42
+ * validation pattern rejects leading-zero and >16-digit identifiers as
43
+ * invalid, and this comparator stays correct even if an old invalid value
44
+ * ever reaches it.
45
+ */
46
+ export declare function compareVersions(a: string, b: string): number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "Open source hooks library for AI coding agents - Install safety, quality, and automation hooks with a single command",
5
5
  "type": "module",
6
6
  "bin": {