@node9/proxy 1.21.4 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +211 -14
  2. package/dist/cli.mjs +211 -14
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -6359,7 +6359,7 @@ function teardownClaude() {
6359
6359
  let changed = false;
6360
6360
  const settings = readJson(hooksPath);
6361
6361
  if (settings?.hooks) {
6362
- for (const event of ["PreToolUse", "PostToolUse"]) {
6362
+ for (const event of ["PreToolUse", "PostToolUse", "UserPromptSubmit"]) {
6363
6363
  const before = settings.hooks[event]?.length ?? 0;
6364
6364
  settings.hooks[event] = settings.hooks[event]?.filter(
6365
6365
  (m) => !m.hooks.some((h) => isNode9Hook(h.command))
@@ -6540,6 +6540,33 @@ async function setupClaude() {
6540
6540
  }
6541
6541
  }
6542
6542
  }
6543
+ const hasPromptHook = settings.hooks.UserPromptSubmit?.some(
6544
+ (m) => m.hooks.some((h) => isNode9Hook(h.command))
6545
+ );
6546
+ if (!hasPromptHook) {
6547
+ if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
6548
+ settings.hooks.UserPromptSubmit.push({
6549
+ matcher: ".*",
6550
+ hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 600 }]
6551
+ });
6552
+ console.log(import_chalk.default.green(" \u2705 UserPromptSubmit hook added \u2192 node9 check (prompt DLP)"));
6553
+ hooksChanged = true;
6554
+ anythingChanged = true;
6555
+ } else if (settings.hooks.UserPromptSubmit) {
6556
+ for (const matcher of settings.hooks.UserPromptSubmit) {
6557
+ for (const h of matcher.hooks) {
6558
+ const cmd = h.command ?? "";
6559
+ if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
6560
+ h.command = fullPathCommand("check");
6561
+ console.log(
6562
+ import_chalk.default.yellow(" \u{1F527} UserPromptSubmit hook repaired (stale path \u2192 current binary)")
6563
+ );
6564
+ hooksChanged = true;
6565
+ anythingChanged = true;
6566
+ }
6567
+ }
6568
+ }
6569
+ }
6543
6570
  if (!hasNode9McpServer(servers)) {
6544
6571
  servers["node9"] = NODE9_MCP_SERVER_ENTRY;
6545
6572
  claudeConfig.mcpServers = servers;
@@ -6826,9 +6853,79 @@ function writeToml(filePath, data) {
6826
6853
  async function setupCodex() {
6827
6854
  const homeDir2 = import_os11.default.homedir();
6828
6855
  const configPath = import_path14.default.join(homeDir2, ".codex", "config.toml");
6856
+ const hooksPath = import_path14.default.join(homeDir2, ".codex", "hooks.json");
6829
6857
  const config = readToml(configPath) ?? {};
6830
6858
  const servers = config.mcp_servers ?? {};
6831
6859
  let anythingChanged = false;
6860
+ const hooksFile = readJson(hooksPath) ?? {};
6861
+ if (!hooksFile.hooks) hooksFile.hooks = {};
6862
+ let hooksChanged = false;
6863
+ if (!hooksFile.hooks.PreToolUse) hooksFile.hooks.PreToolUse = [];
6864
+ for (const matcher of CODEX_PRE_TOOL_MATCHERS) {
6865
+ const existing = hooksFile.hooks.PreToolUse.find((m) => m.matcher === matcher);
6866
+ if (!existing) {
6867
+ hooksFile.hooks.PreToolUse.push({
6868
+ matcher,
6869
+ hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 600 }]
6870
+ });
6871
+ hooksChanged = true;
6872
+ } else {
6873
+ for (const h of existing.hooks) {
6874
+ const cmd = h.command ?? "";
6875
+ if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
6876
+ h.command = fullPathCommand("check");
6877
+ hooksChanged = true;
6878
+ }
6879
+ }
6880
+ }
6881
+ }
6882
+ if (!hooksFile.hooks.UserPromptSubmit) hooksFile.hooks.UserPromptSubmit = [];
6883
+ const hasPromptHook = hooksFile.hooks.UserPromptSubmit.some(
6884
+ (m) => m.hooks.some((h) => isNode9Hook(h.command))
6885
+ );
6886
+ if (!hasPromptHook) {
6887
+ hooksFile.hooks.UserPromptSubmit.push({
6888
+ hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 600 }]
6889
+ });
6890
+ hooksChanged = true;
6891
+ } else {
6892
+ for (const m of hooksFile.hooks.UserPromptSubmit) {
6893
+ for (const h of m.hooks) {
6894
+ const cmd = h.command ?? "";
6895
+ if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
6896
+ h.command = fullPathCommand("check");
6897
+ hooksChanged = true;
6898
+ }
6899
+ }
6900
+ }
6901
+ }
6902
+ if (!hooksFile.hooks.PostToolUse) hooksFile.hooks.PostToolUse = [];
6903
+ const hasPostHook = hooksFile.hooks.PostToolUse.some(
6904
+ (m) => m.hooks.some((h) => isNode9Hook(h.command))
6905
+ );
6906
+ if (!hasPostHook) {
6907
+ hooksFile.hooks.PostToolUse.push({
6908
+ matcher: ".*",
6909
+ hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
6910
+ });
6911
+ hooksChanged = true;
6912
+ } else {
6913
+ for (const m of hooksFile.hooks.PostToolUse) {
6914
+ for (const h of m.hooks) {
6915
+ const cmd = h.command ?? "";
6916
+ if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
6917
+ h.command = fullPathCommand("log");
6918
+ hooksChanged = true;
6919
+ }
6920
+ }
6921
+ }
6922
+ }
6923
+ if (hooksChanged) {
6924
+ writeJson(hooksPath, hooksFile);
6925
+ console.log(import_chalk.default.green(" \u2705 Codex hooks added \u2192 node9 check / node9 log"));
6926
+ anythingChanged = true;
6927
+ }
6928
+ const hooksInstalled = (hooksFile.hooks?.PreToolUse?.length ?? 0) > 0;
6832
6929
  if (!hasNode9McpServer(servers)) {
6833
6930
  servers["node9"] = NODE9_MCP_SERVER_ENTRY;
6834
6931
  config.mcp_servers = servers;
@@ -6868,23 +6965,39 @@ async function setupCodex() {
6868
6965
  }
6869
6966
  console.log("");
6870
6967
  }
6871
- console.log(
6872
- import_chalk.default.yellow(
6873
- " \u26A0\uFE0F Note: Codex does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Codex.\n Native bash and file operations are not monitored."
6874
- )
6875
- );
6876
- console.log("");
6877
- if (!anythingChanged && serversToWrap.length === 0) {
6968
+ const hooksDisabled = config.features?.hooks === false || config.codex_hooks === false;
6969
+ if (hooksDisabled) {
6878
6970
  console.log(
6879
- import_chalk.default.blue(
6880
- "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.codex/config.toml and re-run."
6971
+ import_chalk.default.yellow(
6972
+ " \u26A0\uFE0F Codex hooks are disabled in ~/.codex/config.toml ([features].hooks = false).\n Re-enable hooks to activate Node9 shield evaluation on Bash, apply_patch,\n MCP tool calls, and prompt submissions. Until then, only MCP proxy wrapping\n is active."
6973
+ )
6974
+ );
6975
+ console.log("");
6976
+ }
6977
+ const printCodexTrustReminder = () => {
6978
+ console.log(
6979
+ import_chalk.default.yellow(
6980
+ " \u279C Open Codex and run /hooks to review and trust the Node9 entries.\n Until trusted, only MCP proxy wrapping is active."
6881
6981
  )
6882
6982
  );
6983
+ };
6984
+ if (!anythingChanged && serversToWrap.length === 0) {
6985
+ if (hooksInstalled) {
6986
+ console.log(import_chalk.default.blue("\u2139\uFE0F Codex hooks already installed."));
6987
+ printCodexTrustReminder();
6988
+ } else {
6989
+ console.log(
6990
+ import_chalk.default.blue(
6991
+ "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.codex/config.toml and re-run."
6992
+ )
6993
+ );
6994
+ }
6883
6995
  printDaemonTip();
6884
6996
  return;
6885
6997
  }
6886
6998
  if (anythingChanged) {
6887
- console.log(import_chalk.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Codex via MCP proxy!"));
6999
+ console.log(import_chalk.default.green.bold("\u{1F6E1}\uFE0F Node9 hooks installed for Codex."));
7000
+ printCodexTrustReminder();
6888
7001
  console.log(import_chalk.default.gray(" Restart Codex for changes to take effect."));
6889
7002
  printDaemonTip();
6890
7003
  }
@@ -6892,6 +7005,23 @@ async function setupCodex() {
6892
7005
  function teardownCodex() {
6893
7006
  const homeDir2 = import_os11.default.homedir();
6894
7007
  const configPath = import_path14.default.join(homeDir2, ".codex", "config.toml");
7008
+ const hooksPath = import_path14.default.join(homeDir2, ".codex", "hooks.json");
7009
+ const hooksFile = readJson(hooksPath);
7010
+ if (hooksFile?.hooks) {
7011
+ let hooksChanged = false;
7012
+ for (const event of ["PreToolUse", "PostToolUse", "UserPromptSubmit"]) {
7013
+ const before = hooksFile.hooks[event]?.length ?? 0;
7014
+ hooksFile.hooks[event] = hooksFile.hooks[event]?.filter(
7015
+ (m) => !m.hooks.some((h) => isNode9Hook(h.command))
7016
+ );
7017
+ if ((hooksFile.hooks[event]?.length ?? 0) < before) hooksChanged = true;
7018
+ if (hooksFile.hooks[event]?.length === 0) delete hooksFile.hooks[event];
7019
+ }
7020
+ if (hooksChanged) {
7021
+ writeJson(hooksPath, hooksFile);
7022
+ console.log(import_chalk.default.green(" \u2705 Removed Node9 hooks from ~/.codex/hooks.json"));
7023
+ }
7024
+ }
6895
7025
  const config = readToml(configPath);
6896
7026
  if (!config?.mcp_servers) {
6897
7027
  console.log(import_chalk.default.blue(" \u2139\uFE0F ~/.codex/config.toml not found \u2014 nothing to remove"));
@@ -7350,7 +7480,7 @@ function getAgentsStatus(homeDir2 = import_os11.default.homedir()) {
7350
7480
  }
7351
7481
  ];
7352
7482
  }
7353
- var import_fs12, import_path14, import_os11, import_chalk, import_prompts, import_smol_toml, NODE9_MCP_SERVER_ENTRY;
7483
+ var import_fs12, import_path14, import_os11, import_chalk, import_prompts, import_smol_toml, NODE9_MCP_SERVER_ENTRY, CODEX_PRE_TOOL_MATCHERS;
7354
7484
  var init_setup = __esm({
7355
7485
  "src/setup.ts"() {
7356
7486
  "use strict";
@@ -7361,6 +7491,7 @@ var init_setup = __esm({
7361
7491
  import_prompts = require("@inquirer/prompts");
7362
7492
  import_smol_toml = require("smol-toml");
7363
7493
  NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7494
+ CODEX_PRE_TOOL_MATCHERS = ["^Bash$", "^apply_patch$", "^mcp__.*"];
7364
7495
  }
7365
7496
  });
7366
7497
 
@@ -15655,10 +15786,15 @@ function resolveUserSkillRoot(entry, cwd) {
15655
15786
  }
15656
15787
 
15657
15788
  // src/cli/commands/check.ts
15789
+ init_dlp();
15790
+ init_audit();
15658
15791
  function sanitize2(value) {
15659
15792
  return value.replace(/[\x00-\x1F\x7F]/g, "");
15660
15793
  }
15661
15794
  function detectAiAgent(payload) {
15795
+ if (payload.turn_id !== void 0) {
15796
+ return "Codex";
15797
+ }
15662
15798
  if (payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0) {
15663
15799
  return "Claude Code";
15664
15800
  }
@@ -15704,7 +15840,68 @@ RAW: ${raw}
15704
15840
  }
15705
15841
  process.exit(0);
15706
15842
  }
15707
- const config = getConfig(payload.cwd || void 0);
15843
+ if (payload.hook_event_name === "UserPromptSubmit") {
15844
+ const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
15845
+ if (process.env.NODE9_DEBUG === "1") {
15846
+ try {
15847
+ const logPath = import_path32.default.join(import_os27.default.homedir(), ".node9", "hook-debug.log");
15848
+ if (!import_fs31.default.existsSync(import_path32.default.dirname(logPath)))
15849
+ import_fs31.default.mkdirSync(import_path32.default.dirname(logPath), { recursive: true });
15850
+ const sanitized = JSON.stringify({
15851
+ ...payload,
15852
+ prompt: `<redacted, ${prompt.length} bytes>`
15853
+ });
15854
+ import_fs31.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${sanitized}
15855
+ `);
15856
+ } catch {
15857
+ }
15858
+ }
15859
+ if (!prompt) process.exit(0);
15860
+ const dlpMatch = scanArgs({ prompt });
15861
+ if (!dlpMatch) process.exit(0);
15862
+ const agent2 = detectAiAgent(payload);
15863
+ const sessionId2 = typeof payload.session_id === "string" ? payload.session_id : void 0;
15864
+ appendLocalAudit(
15865
+ "UserPromptSubmit",
15866
+ { prompt },
15867
+ "deny",
15868
+ "dlp-block",
15869
+ { agent: agent2, sessionId: sessionId2 },
15870
+ true
15871
+ );
15872
+ const reason = `\u{1F6A8} Node9 DLP: ${dlpMatch.patternName} detected in prompt (${dlpMatch.redactedSample}). Prompt was not submitted \u2014 remove the credential and try again.`;
15873
+ try {
15874
+ const ttyFd = import_fs31.default.openSync("/dev/tty", "w");
15875
+ import_fs31.default.writeSync(
15876
+ ttyFd,
15877
+ import_chalk9.default.bgRed.white.bold(`
15878
+ \u{1F6A8} NODE9 DLP \u2014 PROMPT BLOCKED
15879
+ `) + import_chalk9.default.red(` ${dlpMatch.patternName} detected in your prompt.
15880
+ `) + import_chalk9.default.gray(` Match: ${dlpMatch.redactedSample}
15881
+ `) + import_chalk9.default.cyan(` Edit the prompt to remove the credential and resubmit.
15882
+
15883
+ `)
15884
+ );
15885
+ import_fs31.default.closeSync(ttyFd);
15886
+ } catch {
15887
+ }
15888
+ const isCodex = agent2 === "Codex";
15889
+ process.stdout.write(
15890
+ JSON.stringify({
15891
+ decision: "block",
15892
+ reason,
15893
+ systemMessage: reason,
15894
+ hookSpecificOutput: isCodex ? { hookEventName: "UserPromptSubmit" } : {
15895
+ hookEventName: "UserPromptSubmit",
15896
+ permissionDecision: "deny",
15897
+ permissionDecisionReason: reason
15898
+ }
15899
+ }) + "\n"
15900
+ );
15901
+ process.exit(2);
15902
+ }
15903
+ const safeCwdForConfig = typeof payload.cwd === "string" && import_path32.default.isAbsolute(payload.cwd) ? payload.cwd : void 0;
15904
+ const config = getConfig(safeCwdForConfig);
15708
15905
  if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
15709
15906
  try {
15710
15907
  const scriptPath = process.argv[1];
@@ -16088,7 +16285,7 @@ function registerLogCommand(program2) {
16088
16285
  const payload = JSON.parse(raw);
16089
16286
  const tool = sanitize3(payload.tool_name ?? payload.name ?? "unknown");
16090
16287
  const rawInput = payload.tool_input ?? payload.args ?? {};
16091
- const agent = payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0 ? "Claude Code" : payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0 ? "Gemini CLI" : void 0;
16288
+ const agent = payload.turn_id !== void 0 ? "Codex" : payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0 ? "Claude Code" : payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0 ? "Gemini CLI" : void 0;
16092
16289
  const entry = {
16093
16290
  ts: (/* @__PURE__ */ new Date()).toISOString(),
16094
16291
  tool,
package/dist/cli.mjs CHANGED
@@ -6341,7 +6341,7 @@ function teardownClaude() {
6341
6341
  let changed = false;
6342
6342
  const settings = readJson(hooksPath);
6343
6343
  if (settings?.hooks) {
6344
- for (const event of ["PreToolUse", "PostToolUse"]) {
6344
+ for (const event of ["PreToolUse", "PostToolUse", "UserPromptSubmit"]) {
6345
6345
  const before = settings.hooks[event]?.length ?? 0;
6346
6346
  settings.hooks[event] = settings.hooks[event]?.filter(
6347
6347
  (m) => !m.hooks.some((h) => isNode9Hook(h.command))
@@ -6522,6 +6522,33 @@ async function setupClaude() {
6522
6522
  }
6523
6523
  }
6524
6524
  }
6525
+ const hasPromptHook = settings.hooks.UserPromptSubmit?.some(
6526
+ (m) => m.hooks.some((h) => isNode9Hook(h.command))
6527
+ );
6528
+ if (!hasPromptHook) {
6529
+ if (!settings.hooks.UserPromptSubmit) settings.hooks.UserPromptSubmit = [];
6530
+ settings.hooks.UserPromptSubmit.push({
6531
+ matcher: ".*",
6532
+ hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 600 }]
6533
+ });
6534
+ console.log(chalk.green(" \u2705 UserPromptSubmit hook added \u2192 node9 check (prompt DLP)"));
6535
+ hooksChanged = true;
6536
+ anythingChanged = true;
6537
+ } else if (settings.hooks.UserPromptSubmit) {
6538
+ for (const matcher of settings.hooks.UserPromptSubmit) {
6539
+ for (const h of matcher.hooks) {
6540
+ const cmd = h.command ?? "";
6541
+ if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
6542
+ h.command = fullPathCommand("check");
6543
+ console.log(
6544
+ chalk.yellow(" \u{1F527} UserPromptSubmit hook repaired (stale path \u2192 current binary)")
6545
+ );
6546
+ hooksChanged = true;
6547
+ anythingChanged = true;
6548
+ }
6549
+ }
6550
+ }
6551
+ }
6525
6552
  if (!hasNode9McpServer(servers)) {
6526
6553
  servers["node9"] = NODE9_MCP_SERVER_ENTRY;
6527
6554
  claudeConfig.mcpServers = servers;
@@ -6808,9 +6835,79 @@ function writeToml(filePath, data) {
6808
6835
  async function setupCodex() {
6809
6836
  const homeDir2 = os11.homedir();
6810
6837
  const configPath = path14.join(homeDir2, ".codex", "config.toml");
6838
+ const hooksPath = path14.join(homeDir2, ".codex", "hooks.json");
6811
6839
  const config = readToml(configPath) ?? {};
6812
6840
  const servers = config.mcp_servers ?? {};
6813
6841
  let anythingChanged = false;
6842
+ const hooksFile = readJson(hooksPath) ?? {};
6843
+ if (!hooksFile.hooks) hooksFile.hooks = {};
6844
+ let hooksChanged = false;
6845
+ if (!hooksFile.hooks.PreToolUse) hooksFile.hooks.PreToolUse = [];
6846
+ for (const matcher of CODEX_PRE_TOOL_MATCHERS) {
6847
+ const existing = hooksFile.hooks.PreToolUse.find((m) => m.matcher === matcher);
6848
+ if (!existing) {
6849
+ hooksFile.hooks.PreToolUse.push({
6850
+ matcher,
6851
+ hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 600 }]
6852
+ });
6853
+ hooksChanged = true;
6854
+ } else {
6855
+ for (const h of existing.hooks) {
6856
+ const cmd = h.command ?? "";
6857
+ if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
6858
+ h.command = fullPathCommand("check");
6859
+ hooksChanged = true;
6860
+ }
6861
+ }
6862
+ }
6863
+ }
6864
+ if (!hooksFile.hooks.UserPromptSubmit) hooksFile.hooks.UserPromptSubmit = [];
6865
+ const hasPromptHook = hooksFile.hooks.UserPromptSubmit.some(
6866
+ (m) => m.hooks.some((h) => isNode9Hook(h.command))
6867
+ );
6868
+ if (!hasPromptHook) {
6869
+ hooksFile.hooks.UserPromptSubmit.push({
6870
+ hooks: [{ type: "command", command: fullPathCommand("check"), timeout: 600 }]
6871
+ });
6872
+ hooksChanged = true;
6873
+ } else {
6874
+ for (const m of hooksFile.hooks.UserPromptSubmit) {
6875
+ for (const h of m.hooks) {
6876
+ const cmd = h.command ?? "";
6877
+ if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
6878
+ h.command = fullPathCommand("check");
6879
+ hooksChanged = true;
6880
+ }
6881
+ }
6882
+ }
6883
+ }
6884
+ if (!hooksFile.hooks.PostToolUse) hooksFile.hooks.PostToolUse = [];
6885
+ const hasPostHook = hooksFile.hooks.PostToolUse.some(
6886
+ (m) => m.hooks.some((h) => isNode9Hook(h.command))
6887
+ );
6888
+ if (!hasPostHook) {
6889
+ hooksFile.hooks.PostToolUse.push({
6890
+ matcher: ".*",
6891
+ hooks: [{ type: "command", command: fullPathCommand("log"), timeout: 600 }]
6892
+ });
6893
+ hooksChanged = true;
6894
+ } else {
6895
+ for (const m of hooksFile.hooks.PostToolUse) {
6896
+ for (const h of m.hooks) {
6897
+ const cmd = h.command ?? "";
6898
+ if (isNode9Hook(cmd) && isStaleHookCommand(cmd)) {
6899
+ h.command = fullPathCommand("log");
6900
+ hooksChanged = true;
6901
+ }
6902
+ }
6903
+ }
6904
+ }
6905
+ if (hooksChanged) {
6906
+ writeJson(hooksPath, hooksFile);
6907
+ console.log(chalk.green(" \u2705 Codex hooks added \u2192 node9 check / node9 log"));
6908
+ anythingChanged = true;
6909
+ }
6910
+ const hooksInstalled = (hooksFile.hooks?.PreToolUse?.length ?? 0) > 0;
6814
6911
  if (!hasNode9McpServer(servers)) {
6815
6912
  servers["node9"] = NODE9_MCP_SERVER_ENTRY;
6816
6913
  config.mcp_servers = servers;
@@ -6850,23 +6947,39 @@ async function setupCodex() {
6850
6947
  }
6851
6948
  console.log("");
6852
6949
  }
6853
- console.log(
6854
- chalk.yellow(
6855
- " \u26A0\uFE0F Note: Codex does not yet support native pre-execution hooks.\n MCP proxy wrapping is the only supported protection mode for Codex.\n Native bash and file operations are not monitored."
6856
- )
6857
- );
6858
- console.log("");
6859
- if (!anythingChanged && serversToWrap.length === 0) {
6950
+ const hooksDisabled = config.features?.hooks === false || config.codex_hooks === false;
6951
+ if (hooksDisabled) {
6860
6952
  console.log(
6861
- chalk.blue(
6862
- "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.codex/config.toml and re-run."
6953
+ chalk.yellow(
6954
+ " \u26A0\uFE0F Codex hooks are disabled in ~/.codex/config.toml ([features].hooks = false).\n Re-enable hooks to activate Node9 shield evaluation on Bash, apply_patch,\n MCP tool calls, and prompt submissions. Until then, only MCP proxy wrapping\n is active."
6955
+ )
6956
+ );
6957
+ console.log("");
6958
+ }
6959
+ const printCodexTrustReminder = () => {
6960
+ console.log(
6961
+ chalk.yellow(
6962
+ " \u279C Open Codex and run /hooks to review and trust the Node9 entries.\n Until trusted, only MCP proxy wrapping is active."
6863
6963
  )
6864
6964
  );
6965
+ };
6966
+ if (!anythingChanged && serversToWrap.length === 0) {
6967
+ if (hooksInstalled) {
6968
+ console.log(chalk.blue("\u2139\uFE0F Codex hooks already installed."));
6969
+ printCodexTrustReminder();
6970
+ } else {
6971
+ console.log(
6972
+ chalk.blue(
6973
+ "\u2139\uFE0F No MCP servers found to wrap. Add MCP servers to ~/.codex/config.toml and re-run."
6974
+ )
6975
+ );
6976
+ }
6865
6977
  printDaemonTip();
6866
6978
  return;
6867
6979
  }
6868
6980
  if (anythingChanged) {
6869
- console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Codex via MCP proxy!"));
6981
+ console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 hooks installed for Codex."));
6982
+ printCodexTrustReminder();
6870
6983
  console.log(chalk.gray(" Restart Codex for changes to take effect."));
6871
6984
  printDaemonTip();
6872
6985
  }
@@ -6874,6 +6987,23 @@ async function setupCodex() {
6874
6987
  function teardownCodex() {
6875
6988
  const homeDir2 = os11.homedir();
6876
6989
  const configPath = path14.join(homeDir2, ".codex", "config.toml");
6990
+ const hooksPath = path14.join(homeDir2, ".codex", "hooks.json");
6991
+ const hooksFile = readJson(hooksPath);
6992
+ if (hooksFile?.hooks) {
6993
+ let hooksChanged = false;
6994
+ for (const event of ["PreToolUse", "PostToolUse", "UserPromptSubmit"]) {
6995
+ const before = hooksFile.hooks[event]?.length ?? 0;
6996
+ hooksFile.hooks[event] = hooksFile.hooks[event]?.filter(
6997
+ (m) => !m.hooks.some((h) => isNode9Hook(h.command))
6998
+ );
6999
+ if ((hooksFile.hooks[event]?.length ?? 0) < before) hooksChanged = true;
7000
+ if (hooksFile.hooks[event]?.length === 0) delete hooksFile.hooks[event];
7001
+ }
7002
+ if (hooksChanged) {
7003
+ writeJson(hooksPath, hooksFile);
7004
+ console.log(chalk.green(" \u2705 Removed Node9 hooks from ~/.codex/hooks.json"));
7005
+ }
7006
+ }
6877
7007
  const config = readToml(configPath);
6878
7008
  if (!config?.mcp_servers) {
6879
7009
  console.log(chalk.blue(" \u2139\uFE0F ~/.codex/config.toml not found \u2014 nothing to remove"));
@@ -7332,11 +7462,12 @@ function getAgentsStatus(homeDir2 = os11.homedir()) {
7332
7462
  }
7333
7463
  ];
7334
7464
  }
7335
- var NODE9_MCP_SERVER_ENTRY;
7465
+ var NODE9_MCP_SERVER_ENTRY, CODEX_PRE_TOOL_MATCHERS;
7336
7466
  var init_setup = __esm({
7337
7467
  "src/setup.ts"() {
7338
7468
  "use strict";
7339
7469
  NODE9_MCP_SERVER_ENTRY = { command: "node9", args: ["mcp-server"] };
7470
+ CODEX_PRE_TOOL_MATCHERS = ["^Bash$", "^apply_patch$", "^mcp__.*"];
7340
7471
  }
7341
7472
  });
7342
7473
 
@@ -15628,10 +15759,15 @@ function resolveUserSkillRoot(entry, cwd) {
15628
15759
  }
15629
15760
 
15630
15761
  // src/cli/commands/check.ts
15762
+ init_dlp();
15763
+ init_audit();
15631
15764
  function sanitize2(value) {
15632
15765
  return value.replace(/[\x00-\x1F\x7F]/g, "");
15633
15766
  }
15634
15767
  function detectAiAgent(payload) {
15768
+ if (payload.turn_id !== void 0) {
15769
+ return "Codex";
15770
+ }
15635
15771
  if (payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0) {
15636
15772
  return "Claude Code";
15637
15773
  }
@@ -15677,7 +15813,68 @@ RAW: ${raw}
15677
15813
  }
15678
15814
  process.exit(0);
15679
15815
  }
15680
- const config = getConfig(payload.cwd || void 0);
15816
+ if (payload.hook_event_name === "UserPromptSubmit") {
15817
+ const prompt = typeof payload.prompt === "string" ? payload.prompt : "";
15818
+ if (process.env.NODE9_DEBUG === "1") {
15819
+ try {
15820
+ const logPath = path32.join(os27.homedir(), ".node9", "hook-debug.log");
15821
+ if (!fs31.existsSync(path32.dirname(logPath)))
15822
+ fs31.mkdirSync(path32.dirname(logPath), { recursive: true });
15823
+ const sanitized = JSON.stringify({
15824
+ ...payload,
15825
+ prompt: `<redacted, ${prompt.length} bytes>`
15826
+ });
15827
+ fs31.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${sanitized}
15828
+ `);
15829
+ } catch {
15830
+ }
15831
+ }
15832
+ if (!prompt) process.exit(0);
15833
+ const dlpMatch = scanArgs({ prompt });
15834
+ if (!dlpMatch) process.exit(0);
15835
+ const agent2 = detectAiAgent(payload);
15836
+ const sessionId2 = typeof payload.session_id === "string" ? payload.session_id : void 0;
15837
+ appendLocalAudit(
15838
+ "UserPromptSubmit",
15839
+ { prompt },
15840
+ "deny",
15841
+ "dlp-block",
15842
+ { agent: agent2, sessionId: sessionId2 },
15843
+ true
15844
+ );
15845
+ const reason = `\u{1F6A8} Node9 DLP: ${dlpMatch.patternName} detected in prompt (${dlpMatch.redactedSample}). Prompt was not submitted \u2014 remove the credential and try again.`;
15846
+ try {
15847
+ const ttyFd = fs31.openSync("/dev/tty", "w");
15848
+ fs31.writeSync(
15849
+ ttyFd,
15850
+ chalk9.bgRed.white.bold(`
15851
+ \u{1F6A8} NODE9 DLP \u2014 PROMPT BLOCKED
15852
+ `) + chalk9.red(` ${dlpMatch.patternName} detected in your prompt.
15853
+ `) + chalk9.gray(` Match: ${dlpMatch.redactedSample}
15854
+ `) + chalk9.cyan(` Edit the prompt to remove the credential and resubmit.
15855
+
15856
+ `)
15857
+ );
15858
+ fs31.closeSync(ttyFd);
15859
+ } catch {
15860
+ }
15861
+ const isCodex = agent2 === "Codex";
15862
+ process.stdout.write(
15863
+ JSON.stringify({
15864
+ decision: "block",
15865
+ reason,
15866
+ systemMessage: reason,
15867
+ hookSpecificOutput: isCodex ? { hookEventName: "UserPromptSubmit" } : {
15868
+ hookEventName: "UserPromptSubmit",
15869
+ permissionDecision: "deny",
15870
+ permissionDecisionReason: reason
15871
+ }
15872
+ }) + "\n"
15873
+ );
15874
+ process.exit(2);
15875
+ }
15876
+ const safeCwdForConfig = typeof payload.cwd === "string" && path32.isAbsolute(payload.cwd) ? payload.cwd : void 0;
15877
+ const config = getConfig(safeCwdForConfig);
15681
15878
  if (config.settings.autoStartDaemon && !isDaemonRunning() && !process.env.NODE9_NO_AUTO_DAEMON) {
15682
15879
  try {
15683
15880
  const scriptPath = process.argv[1];
@@ -16061,7 +16258,7 @@ function registerLogCommand(program2) {
16061
16258
  const payload = JSON.parse(raw);
16062
16259
  const tool = sanitize3(payload.tool_name ?? payload.name ?? "unknown");
16063
16260
  const rawInput = payload.tool_input ?? payload.args ?? {};
16064
- const agent = payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0 ? "Claude Code" : payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0 ? "Gemini CLI" : void 0;
16261
+ const agent = payload.turn_id !== void 0 ? "Codex" : payload.hook_event_name === "PreToolUse" || payload.hook_event_name === "PostToolUse" || payload.tool_use_id !== void 0 || payload.permission_mode !== void 0 ? "Claude Code" : payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0 ? "Gemini CLI" : void 0;
16065
16262
  const entry = {
16066
16263
  ts: (/* @__PURE__ */ new Date()).toISOString(),
16067
16264
  tool,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.21.4",
3
+ "version": "1.22.0",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",