@node9/proxy 1.27.1 → 1.28.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.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  Node9 sits between your AI agent and the tools it can use — **discover** what it's already been doing, **protect** against risky actions in real time, and **review** what happened over any time window.
12
12
 
13
- Works with **Claude Code · Codex CLI · Gemini CLI · Cursor · Windsurf · VSCode · Claude Desktop · Opencode · Pi · any MCP server**.
13
+ Works with **Claude Code · Codex CLI · Gemini CLI · Cursor · Windsurf · VSCode · Claude Desktop · Opencode · Pi · Hermes Agent · any MCP server**.
14
14
 
15
15
  ## What Node9 does
16
16
 
package/dist/cli.js CHANGED
@@ -119,9 +119,11 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
119
119
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
120
120
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
121
121
  const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
122
+ const agentToolNameField = meta?.agentToolName ? { agentToolName: meta.agentToolName } : {};
122
123
  appendToLog(LOCAL_AUDIT_LOG, {
123
124
  ts: (/* @__PURE__ */ new Date()).toISOString(),
124
125
  tool: toolName,
126
+ ...agentToolNameField,
125
127
  ...argsField,
126
128
  decision,
127
129
  checkedBy,
@@ -3945,7 +3947,14 @@ var init_config = __esm({
3945
3947
  "edit_file",
3946
3948
  "create_file",
3947
3949
  "edit",
3948
- "replace"
3950
+ "replace",
3951
+ // Claude / canonicalised Hermes — shouldSnapshot lowercases the
3952
+ // incoming name before set-membership, so we list the lowercase
3953
+ // forms of `Bash`/`Write`/`Edit`/`MultiEdit`. Without these,
3954
+ // post-canonicalisation Hermes `patch` / `write_file` (which now
3955
+ // arrive as `Edit` / `Write`) silently skipped snapshotting.
3956
+ "write",
3957
+ "multiedit"
3949
3958
  ],
3950
3959
  onlyPaths: [],
3951
3960
  ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
@@ -7340,7 +7349,13 @@ function detectAgents(homeDir2 = import_os12.default.homedir()) {
7340
7349
  // dir lazily on first launch — same class of bug as opencode's #186
7341
7350
  // (design R6) — so fall back to PATH lookup for installed-but-never-
7342
7351
  // launched pi.
7343
- pi: exists(import_path15.default.join(homeDir2, ".pi", "agent")) || binaryInPath("pi")
7352
+ pi: exists(import_path15.default.join(homeDir2, ".pi", "agent")) || binaryInPath("pi"),
7353
+ // Hermes Agent (https://github.com/NousResearch/hermes-agent): home dir
7354
+ // is $HERMES_HOME (default ~/.hermes) per hermes_constants.py:30. config.yaml
7355
+ // appears after `hermes setup` has run; the directory alone exists from
7356
+ // install time. PATH fallback covers the rare case where the user blew
7357
+ // away ~/.hermes but kept the binary.
7358
+ hermes: exists(hermesHomeDir(homeDir2)) || binaryInPath("hermes")
7344
7359
  };
7345
7360
  }
7346
7361
  async function setupCursor() {
@@ -8147,6 +8162,169 @@ function teardownPi() {
8147
8162
  console.log(import_chalk.default.yellow(` \u26A0\uFE0F Could not remove ${extensionPath}: ${String(err2)}`));
8148
8163
  }
8149
8164
  }
8165
+ function hermesHomeDir(homeDir2 = import_os12.default.homedir()) {
8166
+ const env = process.env.HERMES_HOME?.trim();
8167
+ if (env && import_path15.default.isAbsolute(env)) return env;
8168
+ return import_path15.default.join(homeDir2, ".hermes");
8169
+ }
8170
+ function hermesConfigPath(homeDir2 = import_os12.default.homedir()) {
8171
+ return import_path15.default.join(hermesHomeDir(homeDir2), HERMES_CONFIG_FILENAME);
8172
+ }
8173
+ function hermesAllowlistPath(homeDir2 = import_os12.default.homedir()) {
8174
+ return import_path15.default.join(hermesHomeDir(homeDir2), HERMES_ALLOWLIST_FILENAME);
8175
+ }
8176
+ function setupHermes() {
8177
+ const homeDir2 = import_os12.default.homedir();
8178
+ const configPath = hermesConfigPath(homeDir2);
8179
+ const allowlistPath = hermesAllowlistPath(homeDir2);
8180
+ if (!import_fs13.default.existsSync(configPath)) {
8181
+ console.log(import_chalk.default.yellow(` \u26A0\uFE0F Hermes config not found at ${configPath}`));
8182
+ console.log(import_chalk.default.gray(" Run `hermes setup` first, then re-run node9 setup hermes."));
8183
+ return;
8184
+ }
8185
+ let anythingChanged = false;
8186
+ const raw = import_fs13.default.readFileSync(configPath, "utf-8");
8187
+ const doc = yaml.parseDocument(raw);
8188
+ if (doc.errors.length > 0) {
8189
+ console.log(import_chalk.default.yellow(` \u26A0\uFE0F Hermes config.yaml has YAML parse errors:`));
8190
+ for (const err2 of doc.errors.slice(0, 3)) {
8191
+ console.log(import_chalk.default.gray(` \u2022 ${err2.message}`));
8192
+ }
8193
+ console.log(
8194
+ import_chalk.default.gray(" Fix the file (or run `hermes config edit`), then re-run node9 setup hermes.")
8195
+ );
8196
+ return;
8197
+ }
8198
+ const current = doc.toJS() ?? {};
8199
+ for (const { event, subcmd } of HERMES_HOOK_PLAN) {
8200
+ const command = fullPathCommand(subcmd);
8201
+ const existing = current.hooks?.[event] ?? [];
8202
+ const node9Idx = existing.findIndex(
8203
+ (e) => typeof e?.command === "string" && isNode9Hook(e.command)
8204
+ );
8205
+ if (node9Idx === -1) {
8206
+ const newEntries = [...existing, { command, timeout: 10 }];
8207
+ doc.setIn(["hooks", event], newEntries);
8208
+ console.log(import_chalk.default.green(` \u2705 Hermes ${event} hook added \u2192 node9 ${subcmd}`));
8209
+ anythingChanged = true;
8210
+ } else if (existing[node9Idx].command !== command || isStaleHookCommand(existing[node9Idx].command ?? "")) {
8211
+ const newEntries = [...existing];
8212
+ newEntries[node9Idx] = { ...newEntries[node9Idx], command };
8213
+ doc.setIn(["hooks", event], newEntries);
8214
+ console.log(import_chalk.default.yellow(` \u{1F527} Hermes ${event} hook repaired (stale path \u2192 current binary)`));
8215
+ anythingChanged = true;
8216
+ }
8217
+ }
8218
+ if (current.hooks_auto_accept !== true) {
8219
+ doc.set("hooks_auto_accept", true);
8220
+ console.log(import_chalk.default.green(" \u2705 hooks_auto_accept set to true"));
8221
+ anythingChanged = true;
8222
+ }
8223
+ if (anythingChanged) {
8224
+ import_fs13.default.writeFileSync(configPath, doc.toString());
8225
+ }
8226
+ let allowlist = {};
8227
+ if (import_fs13.default.existsSync(allowlistPath)) {
8228
+ try {
8229
+ allowlist = JSON.parse(import_fs13.default.readFileSync(allowlistPath, "utf-8"));
8230
+ } catch {
8231
+ allowlist = {};
8232
+ }
8233
+ }
8234
+ if (!Array.isArray(allowlist.approvals)) allowlist.approvals = [];
8235
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
8236
+ let allowlistChanged = false;
8237
+ for (const { event, subcmd } of HERMES_HOOK_PLAN) {
8238
+ const command = fullPathCommand(subcmd);
8239
+ const existingIdx = allowlist.approvals.findIndex(
8240
+ (e) => e?.event === event && typeof e?.command === "string" && isNode9Hook(e.command)
8241
+ );
8242
+ if (existingIdx === -1) {
8243
+ allowlist.approvals.push({ event, command, approved_at: nowIso });
8244
+ allowlistChanged = true;
8245
+ } else if (allowlist.approvals[existingIdx].command !== command) {
8246
+ allowlist.approvals[existingIdx] = { event, command, approved_at: nowIso };
8247
+ allowlistChanged = true;
8248
+ }
8249
+ }
8250
+ if (allowlistChanged) {
8251
+ import_fs13.default.mkdirSync(import_path15.default.dirname(allowlistPath), { recursive: true });
8252
+ import_fs13.default.writeFileSync(allowlistPath, JSON.stringify(allowlist, null, 2) + "\n");
8253
+ console.log(import_chalk.default.green(" \u2705 Hermes shell-hooks allowlist populated"));
8254
+ anythingChanged = true;
8255
+ }
8256
+ if (anythingChanged) {
8257
+ console.log(import_chalk.default.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Hermes Agent!"));
8258
+ console.log(import_chalk.default.gray(" Restart Hermes for changes to take effect."));
8259
+ printDaemonTip();
8260
+ } else {
8261
+ console.log(import_chalk.default.blue("\u2139\uFE0F Node9 is already fully configured for Hermes Agent."));
8262
+ printDaemonTip();
8263
+ }
8264
+ }
8265
+ function teardownHermes() {
8266
+ const homeDir2 = import_os12.default.homedir();
8267
+ const configPath = hermesConfigPath(homeDir2);
8268
+ const allowlistPath = hermesAllowlistPath(homeDir2);
8269
+ if (!import_fs13.default.existsSync(configPath)) {
8270
+ console.log(import_chalk.default.blue(` \u2139\uFE0F ${configPath} not found \u2014 nothing to remove`));
8271
+ return;
8272
+ }
8273
+ const raw = import_fs13.default.readFileSync(configPath, "utf-8");
8274
+ const doc = yaml.parseDocument(raw);
8275
+ if (doc.errors.length > 0) {
8276
+ console.log(
8277
+ import_chalk.default.yellow(` \u26A0\uFE0F Skipping ${configPath} \u2014 file has YAML parse errors, fix it manually.`)
8278
+ );
8279
+ } else {
8280
+ teardownHermesConfigDoc(doc, configPath);
8281
+ }
8282
+ teardownHermesAllowlist(allowlistPath);
8283
+ }
8284
+ function teardownHermesConfigDoc(doc, configPath) {
8285
+ let anythingChanged = false;
8286
+ const current = doc.toJS() ?? {};
8287
+ for (const { event } of HERMES_HOOK_PLAN) {
8288
+ const existing = current.hooks?.[event] ?? [];
8289
+ const filtered = existing.filter(
8290
+ (e) => !(typeof e?.command === "string" && isNode9Hook(e.command))
8291
+ );
8292
+ if (filtered.length === existing.length) continue;
8293
+ if (filtered.length === 0) {
8294
+ doc.deleteIn(["hooks", event]);
8295
+ } else {
8296
+ doc.setIn(["hooks", event], filtered);
8297
+ }
8298
+ anythingChanged = true;
8299
+ }
8300
+ const afterHooks = doc.toJS()?.hooks;
8301
+ if (afterHooks && typeof afterHooks === "object" && Object.keys(afterHooks).length === 0) {
8302
+ doc.delete("hooks");
8303
+ anythingChanged = true;
8304
+ }
8305
+ if (anythingChanged) {
8306
+ import_fs13.default.writeFileSync(configPath, doc.toString());
8307
+ console.log(import_chalk.default.green(` \u2705 Removed Node9 hooks from ${configPath}`));
8308
+ } else {
8309
+ console.log(import_chalk.default.blue(` \u2139\uFE0F No Node9 hooks found in ${configPath}`));
8310
+ }
8311
+ }
8312
+ function teardownHermesAllowlist(allowlistPath) {
8313
+ if (!import_fs13.default.existsSync(allowlistPath)) return;
8314
+ try {
8315
+ const raw = import_fs13.default.readFileSync(allowlistPath, "utf-8");
8316
+ const allowlist = JSON.parse(raw);
8317
+ if (!Array.isArray(allowlist.approvals)) return;
8318
+ const before = allowlist.approvals.length;
8319
+ allowlist.approvals = allowlist.approvals.filter(
8320
+ (e) => !(typeof e?.command === "string" && isNode9Hook(e.command))
8321
+ );
8322
+ if (allowlist.approvals.length === before) return;
8323
+ import_fs13.default.writeFileSync(allowlistPath, JSON.stringify(allowlist, null, 2) + "\n");
8324
+ console.log(import_chalk.default.green(` \u2705 Removed Node9 entries from ${allowlistPath}`));
8325
+ } catch {
8326
+ }
8327
+ }
8150
8328
  function getAgentsStatus(homeDir2 = import_os12.default.homedir()) {
8151
8329
  const detected = detectAgents(homeDir2);
8152
8330
  const claudeWired = (() => {
@@ -8258,10 +8436,29 @@ function getAgentsStatus(homeDir2 = import_os12.default.homedir()) {
8258
8436
  // simple existence check on the canonical install location.
8259
8437
  wired: import_fs13.default.existsSync(import_path15.default.join(homeDir2, ".pi", "agent", "extensions", PI_EXTENSION_NAME)),
8260
8438
  mode: detected.pi ? "hooks" : null
8439
+ },
8440
+ {
8441
+ name: "hermes",
8442
+ label: "Hermes Agent",
8443
+ installed: detected.hermes,
8444
+ // Wired = node9 hook entry exists in the parsed config.yaml.
8445
+ // Reading the YAML cheaply via yaml.parse — we don't need the
8446
+ // Document API for a boolean status check.
8447
+ wired: (() => {
8448
+ try {
8449
+ const raw = import_fs13.default.readFileSync(hermesConfigPath(homeDir2), "utf-8");
8450
+ const cfg = yaml.parse(raw);
8451
+ const pre = cfg?.hooks?.["pre_tool_call"] ?? [];
8452
+ return pre.some((e) => typeof e?.command === "string" && isNode9Hook(e.command));
8453
+ } catch {
8454
+ return false;
8455
+ }
8456
+ })(),
8457
+ mode: detected.hermes ? "hooks" : null
8261
8458
  }
8262
8459
  ];
8263
8460
  }
8264
- var import_fs13, import_path15, import_os12, import_chalk, import_prompts, import_smol_toml, NODE9_MCP_SERVER_ENTRY, CODEX_PRE_TOOL_MATCHERS, OPENCODE_PLUGIN_NAME, PI_EXTENSION_NAME;
8461
+ var import_fs13, import_path15, import_os12, import_chalk, import_prompts, import_smol_toml, yaml, NODE9_MCP_SERVER_ENTRY, CODEX_PRE_TOOL_MATCHERS, OPENCODE_PLUGIN_NAME, PI_EXTENSION_NAME, HERMES_CONFIG_FILENAME, HERMES_ALLOWLIST_FILENAME, HERMES_HOOK_PLAN;
8265
8462
  var init_setup = __esm({
8266
8463
  "src/setup.ts"() {
8267
8464
  "use strict";
@@ -8271,6 +8468,7 @@ var init_setup = __esm({
8271
8468
  import_chalk = __toESM(require("chalk"));
8272
8469
  import_prompts = require("@inquirer/prompts");
8273
8470
  import_smol_toml = require("smol-toml");
8471
+ yaml = __toESM(require("yaml"));
8274
8472
  init_mcp_pin();
8275
8473
  init_setup_opencode_shim();
8276
8474
  init_setup_pi_shim();
@@ -8278,6 +8476,12 @@ var init_setup = __esm({
8278
8476
  CODEX_PRE_TOOL_MATCHERS = ["^Bash$", "^apply_patch$", "^mcp__.*"];
8279
8477
  OPENCODE_PLUGIN_NAME = "node9.js";
8280
8478
  PI_EXTENSION_NAME = "node9.js";
8479
+ HERMES_CONFIG_FILENAME = "config.yaml";
8480
+ HERMES_ALLOWLIST_FILENAME = "shell-hooks-allowlist.json";
8481
+ HERMES_HOOK_PLAN = [
8482
+ { event: "pre_tool_call", subcmd: "check" },
8483
+ { event: "post_tool_call", subcmd: "log" }
8484
+ ];
8281
8485
  }
8282
8486
  });
8283
8487
 
@@ -16575,6 +16779,33 @@ function resolveUserSkillRoot(entry, cwd) {
16575
16779
  // src/cli/commands/check.ts
16576
16780
  init_dlp();
16577
16781
  init_audit();
16782
+
16783
+ // src/utils/hook-payload.ts
16784
+ function extractToolName(payload, defaultValue = "") {
16785
+ return payload.tool_name ?? payload.name ?? defaultValue;
16786
+ }
16787
+ function extractToolInput(payload) {
16788
+ return payload.tool_input ?? payload.args ?? {};
16789
+ }
16790
+ function canonicalToolName(name) {
16791
+ switch (name) {
16792
+ // Hermes Agent
16793
+ case "terminal":
16794
+ return "Bash";
16795
+ case "write_file":
16796
+ return "Write";
16797
+ case "patch":
16798
+ return "Edit";
16799
+ case "read_file":
16800
+ return "Read";
16801
+ case "search_files":
16802
+ return "Grep";
16803
+ default:
16804
+ return name;
16805
+ }
16806
+ }
16807
+
16808
+ // src/cli/commands/check.ts
16578
16809
  function sanitize2(value) {
16579
16810
  return value.replace(/[\x00-\x1F\x7F]/g, "");
16580
16811
  }
@@ -16593,9 +16824,15 @@ function detectAiAgent(payload) {
16593
16824
  if (payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0) {
16594
16825
  return "Gemini CLI";
16595
16826
  }
16827
+ if (payload.hook_event_name === "pre_tool_call" || payload.hook_event_name === "post_tool_call") {
16828
+ return "Hermes";
16829
+ }
16596
16830
  if (process.env.CLAUDECODE === "1" || process.env.CLAUDE_CODE_SESSION_ID) {
16597
16831
  return "Claude Code";
16598
16832
  }
16833
+ if (process.env.HERMES_SESSION_ID || process.env.HERMES_HOME || process.env.HERMES_INTERACTIVE) {
16834
+ return "Hermes";
16835
+ }
16599
16836
  if (process.env.GEMINI_CLI_VERSION || process.env.GEMINI_API_KEY) {
16600
16837
  return "Gemini CLI";
16601
16838
  }
@@ -16742,8 +16979,8 @@ RAW: ${raw}
16742
16979
  import_fs32.default.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
16743
16980
  `);
16744
16981
  }
16745
- const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
16746
- const toolInput = payload.tool_input ?? payload.args ?? {};
16982
+ const toolName = canonicalToolName(sanitize2(extractToolName(payload)));
16983
+ const toolInput = extractToolInput(payload);
16747
16984
  const agent = detectAiAgent(payload);
16748
16985
  const mcpMatch = toolName.match(/^mcp__([^_](?:[^_]|_(?!_))*?)__/i);
16749
16986
  const mcpServer = mcpMatch?.[1];
@@ -17075,8 +17312,9 @@ function registerLogCommand(program2) {
17075
17312
  try {
17076
17313
  if (!raw || raw.trim() === "") process.exit(0);
17077
17314
  const payload = JSON.parse(raw);
17078
- const tool = sanitize3(payload.tool_name ?? payload.name ?? "unknown");
17079
- const rawInput = payload.tool_input ?? payload.args ?? {};
17315
+ const rawToolName = sanitize3(extractToolName(payload, "unknown"));
17316
+ const tool = canonicalToolName(rawToolName);
17317
+ const rawInput = extractToolInput(payload);
17080
17318
  const metaTag = (() => {
17081
17319
  const m = payload.meta;
17082
17320
  if (m && typeof m === "object") {
@@ -17085,7 +17323,7 @@ function registerLogCommand(program2) {
17085
17323
  }
17086
17324
  return void 0;
17087
17325
  })();
17088
- const agent = metaTag !== void 0 ? metaTag : 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;
17326
+ const agent = metaTag !== void 0 ? metaTag : payload.turn_id !== void 0 ? "Codex" : payload.hook_event_name === "pre_tool_call" || payload.hook_event_name === "post_tool_call" ? "Hermes" : 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" : process.env.HERMES_SESSION_ID || process.env.HERMES_HOME || process.env.HERMES_INTERACTIVE ? "Hermes" : void 0;
17089
17327
  const entry = {
17090
17328
  ts: (/* @__PURE__ */ new Date()).toISOString(),
17091
17329
  tool,
@@ -17094,6 +17332,7 @@ function registerLogCommand(program2) {
17094
17332
  source: "post-hook"
17095
17333
  };
17096
17334
  if (agent) entry.agent = agent;
17335
+ if (rawToolName !== tool) entry.agentToolName = rawToolName;
17097
17336
  if (payload.session_id) entry.sessionId = payload.session_id;
17098
17337
  const logPath = import_path34.default.join(import_os29.default.homedir(), ".node9", "audit.log");
17099
17338
  if (!import_fs33.default.existsSync(import_path34.default.dirname(logPath)))
@@ -19219,12 +19458,12 @@ function registerInitCommand(program2) {
19219
19458
  if (found.length === 0) {
19220
19459
  console.log(
19221
19460
  import_chalk16.default.gray(
19222
- "No AI agents detected. Install one of the supported agents (Claude Code, Codex, Gemini CLI, Cursor, Windsurf, VSCode, Claude Desktop, Opencode, or Pi)."
19461
+ "No AI agents detected. Install one of the supported agents (Claude Code, Codex, Gemini CLI, Cursor, Windsurf, VSCode, Claude Desktop, Opencode, Pi, or Hermes Agent)."
19223
19462
  )
19224
19463
  );
19225
19464
  console.log(
19226
19465
  import_chalk16.default.gray(
19227
- "then run: node9 agents add <claude|codex|gemini|cursor|windsurf|vscode|claudeDesktop|opencode|pi>"
19466
+ "then run: node9 agents add <claude|codex|gemini|cursor|windsurf|vscode|claudeDesktop|opencode|pi|hermes>"
19228
19467
  )
19229
19468
  );
19230
19469
  return;
@@ -19245,6 +19484,7 @@ function registerInitCommand(program2) {
19245
19484
  else if (agent === "claudeDesktop") await setupClaudeDesktop();
19246
19485
  else if (agent === "opencode") await setupOpencode();
19247
19486
  else if (agent === "pi") await setupPi();
19487
+ else if (agent === "hermes") setupHermes();
19248
19488
  console.log("");
19249
19489
  }
19250
19490
  if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
@@ -21006,7 +21246,8 @@ var SETUP_FN = {
21006
21246
  vscode: setupVSCode,
21007
21247
  claudeDesktop: setupClaudeDesktop,
21008
21248
  opencode: setupOpencode,
21009
- pi: setupPi
21249
+ pi: setupPi,
21250
+ hermes: setupHermes
21010
21251
  };
21011
21252
  var TEARDOWN_FN = {
21012
21253
  claude: teardownClaude,
@@ -21017,7 +21258,8 @@ var TEARDOWN_FN = {
21017
21258
  vscode: teardownVSCode,
21018
21259
  claudeDesktop: teardownClaudeDesktop,
21019
21260
  opencode: teardownOpencode,
21020
- pi: teardownPi
21261
+ pi: teardownPi,
21262
+ hermes: teardownHermes
21021
21263
  };
21022
21264
  var AGENT_NAMES = Object.keys(SETUP_FN);
21023
21265
  function registerAgentsCommand(program2) {
@@ -22355,10 +22597,11 @@ program.command("addto", { hidden: true }).description("Integrate Node9 with an
22355
22597
  if (target === "codex") return await setupCodex();
22356
22598
  if (target === "windsurf") return await setupWindsurf();
22357
22599
  if (target === "vscode") return await setupVSCode();
22600
+ if (target === "hermes") return setupHermes();
22358
22601
  if (target === "hud") return setupHud();
22359
22602
  console.error(
22360
22603
  import_chalk30.default.red(
22361
- `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
22604
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hermes, hud`
22362
22605
  )
22363
22606
  );
22364
22607
  process.exit(1);
@@ -22380,6 +22623,7 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
22380
22623
  console.log(" " + import_chalk30.default.green("codex") + " \u2014 OpenAI Codex CLI (MCP proxy)");
22381
22624
  console.log(" " + import_chalk30.default.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
22382
22625
  console.log(" " + import_chalk30.default.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
22626
+ console.log(" " + import_chalk30.default.green("hermes") + " \u2014 Hermes Agent (hook mode)");
22383
22627
  process.stdout.write(
22384
22628
  " " + import_chalk30.default.green("hud") + " \u2014 Claude Code security statusline\n"
22385
22629
  );
@@ -22393,10 +22637,11 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
22393
22637
  if (t === "codex") return await setupCodex();
22394
22638
  if (t === "windsurf") return await setupWindsurf();
22395
22639
  if (t === "vscode") return await setupVSCode();
22640
+ if (t === "hermes") return setupHermes();
22396
22641
  if (t === "hud") return setupHud();
22397
22642
  console.error(
22398
22643
  import_chalk30.default.red(
22399
- `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
22644
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hermes, hud`
22400
22645
  )
22401
22646
  );
22402
22647
  process.exit(1);
@@ -22415,11 +22660,12 @@ program.command("removefrom", { hidden: true }).description("Remove Node9 hooks
22415
22660
  else if (target === "codex") fn = teardownCodex;
22416
22661
  else if (target === "windsurf") fn = teardownWindsurf;
22417
22662
  else if (target === "vscode") fn = teardownVSCode;
22663
+ else if (target === "hermes") fn = teardownHermes;
22418
22664
  else if (target === "hud") fn = teardownHud;
22419
22665
  else {
22420
22666
  console.error(
22421
22667
  import_chalk30.default.red(
22422
- `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
22668
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hermes, hud`
22423
22669
  )
22424
22670
  );
22425
22671
  process.exit(1);
@@ -22452,7 +22698,8 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
22452
22698
  ["Cursor", teardownCursor],
22453
22699
  ["Codex", teardownCodex],
22454
22700
  ["Windsurf", teardownWindsurf],
22455
- ["VSCode", teardownVSCode]
22701
+ ["VSCode", teardownVSCode],
22702
+ ["Hermes", teardownHermes]
22456
22703
  ]) {
22457
22704
  try {
22458
22705
  fn();
package/dist/cli.mjs CHANGED
@@ -100,9 +100,11 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
100
100
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
101
101
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
102
102
  const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
103
+ const agentToolNameField = meta?.agentToolName ? { agentToolName: meta.agentToolName } : {};
103
104
  appendToLog(LOCAL_AUDIT_LOG, {
104
105
  ts: (/* @__PURE__ */ new Date()).toISOString(),
105
106
  tool: toolName,
107
+ ...agentToolNameField,
106
108
  ...argsField,
107
109
  decision,
108
110
  checkedBy,
@@ -3923,7 +3925,14 @@ var init_config = __esm({
3923
3925
  "edit_file",
3924
3926
  "create_file",
3925
3927
  "edit",
3926
- "replace"
3928
+ "replace",
3929
+ // Claude / canonicalised Hermes — shouldSnapshot lowercases the
3930
+ // incoming name before set-membership, so we list the lowercase
3931
+ // forms of `Bash`/`Write`/`Edit`/`MultiEdit`. Without these,
3932
+ // post-canonicalisation Hermes `patch` / `write_file` (which now
3933
+ // arrive as `Edit` / `Write`) silently skipped snapshotting.
3934
+ "write",
3935
+ "multiedit"
3927
3936
  ],
3928
3937
  onlyPaths: [],
3929
3938
  ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
@@ -6823,6 +6832,7 @@ import os12 from "os";
6823
6832
  import chalk from "chalk";
6824
6833
  import { confirm } from "@inquirer/prompts";
6825
6834
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
6835
+ import * as yaml from "yaml";
6826
6836
  function hasNode9McpServer(servers) {
6827
6837
  const entry = servers["node9"];
6828
6838
  return !!entry && entry.command === "node9" && Array.isArray(entry.args) && entry.args[0] === "mcp-server";
@@ -7321,7 +7331,13 @@ function detectAgents(homeDir2 = os12.homedir()) {
7321
7331
  // dir lazily on first launch — same class of bug as opencode's #186
7322
7332
  // (design R6) — so fall back to PATH lookup for installed-but-never-
7323
7333
  // launched pi.
7324
- pi: exists(path15.join(homeDir2, ".pi", "agent")) || binaryInPath("pi")
7334
+ pi: exists(path15.join(homeDir2, ".pi", "agent")) || binaryInPath("pi"),
7335
+ // Hermes Agent (https://github.com/NousResearch/hermes-agent): home dir
7336
+ // is $HERMES_HOME (default ~/.hermes) per hermes_constants.py:30. config.yaml
7337
+ // appears after `hermes setup` has run; the directory alone exists from
7338
+ // install time. PATH fallback covers the rare case where the user blew
7339
+ // away ~/.hermes but kept the binary.
7340
+ hermes: exists(hermesHomeDir(homeDir2)) || binaryInPath("hermes")
7325
7341
  };
7326
7342
  }
7327
7343
  async function setupCursor() {
@@ -8128,6 +8144,169 @@ function teardownPi() {
8128
8144
  console.log(chalk.yellow(` \u26A0\uFE0F Could not remove ${extensionPath}: ${String(err2)}`));
8129
8145
  }
8130
8146
  }
8147
+ function hermesHomeDir(homeDir2 = os12.homedir()) {
8148
+ const env = process.env.HERMES_HOME?.trim();
8149
+ if (env && path15.isAbsolute(env)) return env;
8150
+ return path15.join(homeDir2, ".hermes");
8151
+ }
8152
+ function hermesConfigPath(homeDir2 = os12.homedir()) {
8153
+ return path15.join(hermesHomeDir(homeDir2), HERMES_CONFIG_FILENAME);
8154
+ }
8155
+ function hermesAllowlistPath(homeDir2 = os12.homedir()) {
8156
+ return path15.join(hermesHomeDir(homeDir2), HERMES_ALLOWLIST_FILENAME);
8157
+ }
8158
+ function setupHermes() {
8159
+ const homeDir2 = os12.homedir();
8160
+ const configPath = hermesConfigPath(homeDir2);
8161
+ const allowlistPath = hermesAllowlistPath(homeDir2);
8162
+ if (!fs13.existsSync(configPath)) {
8163
+ console.log(chalk.yellow(` \u26A0\uFE0F Hermes config not found at ${configPath}`));
8164
+ console.log(chalk.gray(" Run `hermes setup` first, then re-run node9 setup hermes."));
8165
+ return;
8166
+ }
8167
+ let anythingChanged = false;
8168
+ const raw = fs13.readFileSync(configPath, "utf-8");
8169
+ const doc = yaml.parseDocument(raw);
8170
+ if (doc.errors.length > 0) {
8171
+ console.log(chalk.yellow(` \u26A0\uFE0F Hermes config.yaml has YAML parse errors:`));
8172
+ for (const err2 of doc.errors.slice(0, 3)) {
8173
+ console.log(chalk.gray(` \u2022 ${err2.message}`));
8174
+ }
8175
+ console.log(
8176
+ chalk.gray(" Fix the file (or run `hermes config edit`), then re-run node9 setup hermes.")
8177
+ );
8178
+ return;
8179
+ }
8180
+ const current = doc.toJS() ?? {};
8181
+ for (const { event, subcmd } of HERMES_HOOK_PLAN) {
8182
+ const command = fullPathCommand(subcmd);
8183
+ const existing = current.hooks?.[event] ?? [];
8184
+ const node9Idx = existing.findIndex(
8185
+ (e) => typeof e?.command === "string" && isNode9Hook(e.command)
8186
+ );
8187
+ if (node9Idx === -1) {
8188
+ const newEntries = [...existing, { command, timeout: 10 }];
8189
+ doc.setIn(["hooks", event], newEntries);
8190
+ console.log(chalk.green(` \u2705 Hermes ${event} hook added \u2192 node9 ${subcmd}`));
8191
+ anythingChanged = true;
8192
+ } else if (existing[node9Idx].command !== command || isStaleHookCommand(existing[node9Idx].command ?? "")) {
8193
+ const newEntries = [...existing];
8194
+ newEntries[node9Idx] = { ...newEntries[node9Idx], command };
8195
+ doc.setIn(["hooks", event], newEntries);
8196
+ console.log(chalk.yellow(` \u{1F527} Hermes ${event} hook repaired (stale path \u2192 current binary)`));
8197
+ anythingChanged = true;
8198
+ }
8199
+ }
8200
+ if (current.hooks_auto_accept !== true) {
8201
+ doc.set("hooks_auto_accept", true);
8202
+ console.log(chalk.green(" \u2705 hooks_auto_accept set to true"));
8203
+ anythingChanged = true;
8204
+ }
8205
+ if (anythingChanged) {
8206
+ fs13.writeFileSync(configPath, doc.toString());
8207
+ }
8208
+ let allowlist = {};
8209
+ if (fs13.existsSync(allowlistPath)) {
8210
+ try {
8211
+ allowlist = JSON.parse(fs13.readFileSync(allowlistPath, "utf-8"));
8212
+ } catch {
8213
+ allowlist = {};
8214
+ }
8215
+ }
8216
+ if (!Array.isArray(allowlist.approvals)) allowlist.approvals = [];
8217
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
8218
+ let allowlistChanged = false;
8219
+ for (const { event, subcmd } of HERMES_HOOK_PLAN) {
8220
+ const command = fullPathCommand(subcmd);
8221
+ const existingIdx = allowlist.approvals.findIndex(
8222
+ (e) => e?.event === event && typeof e?.command === "string" && isNode9Hook(e.command)
8223
+ );
8224
+ if (existingIdx === -1) {
8225
+ allowlist.approvals.push({ event, command, approved_at: nowIso });
8226
+ allowlistChanged = true;
8227
+ } else if (allowlist.approvals[existingIdx].command !== command) {
8228
+ allowlist.approvals[existingIdx] = { event, command, approved_at: nowIso };
8229
+ allowlistChanged = true;
8230
+ }
8231
+ }
8232
+ if (allowlistChanged) {
8233
+ fs13.mkdirSync(path15.dirname(allowlistPath), { recursive: true });
8234
+ fs13.writeFileSync(allowlistPath, JSON.stringify(allowlist, null, 2) + "\n");
8235
+ console.log(chalk.green(" \u2705 Hermes shell-hooks allowlist populated"));
8236
+ anythingChanged = true;
8237
+ }
8238
+ if (anythingChanged) {
8239
+ console.log(chalk.green.bold("\u{1F6E1}\uFE0F Node9 is now protecting Hermes Agent!"));
8240
+ console.log(chalk.gray(" Restart Hermes for changes to take effect."));
8241
+ printDaemonTip();
8242
+ } else {
8243
+ console.log(chalk.blue("\u2139\uFE0F Node9 is already fully configured for Hermes Agent."));
8244
+ printDaemonTip();
8245
+ }
8246
+ }
8247
+ function teardownHermes() {
8248
+ const homeDir2 = os12.homedir();
8249
+ const configPath = hermesConfigPath(homeDir2);
8250
+ const allowlistPath = hermesAllowlistPath(homeDir2);
8251
+ if (!fs13.existsSync(configPath)) {
8252
+ console.log(chalk.blue(` \u2139\uFE0F ${configPath} not found \u2014 nothing to remove`));
8253
+ return;
8254
+ }
8255
+ const raw = fs13.readFileSync(configPath, "utf-8");
8256
+ const doc = yaml.parseDocument(raw);
8257
+ if (doc.errors.length > 0) {
8258
+ console.log(
8259
+ chalk.yellow(` \u26A0\uFE0F Skipping ${configPath} \u2014 file has YAML parse errors, fix it manually.`)
8260
+ );
8261
+ } else {
8262
+ teardownHermesConfigDoc(doc, configPath);
8263
+ }
8264
+ teardownHermesAllowlist(allowlistPath);
8265
+ }
8266
+ function teardownHermesConfigDoc(doc, configPath) {
8267
+ let anythingChanged = false;
8268
+ const current = doc.toJS() ?? {};
8269
+ for (const { event } of HERMES_HOOK_PLAN) {
8270
+ const existing = current.hooks?.[event] ?? [];
8271
+ const filtered = existing.filter(
8272
+ (e) => !(typeof e?.command === "string" && isNode9Hook(e.command))
8273
+ );
8274
+ if (filtered.length === existing.length) continue;
8275
+ if (filtered.length === 0) {
8276
+ doc.deleteIn(["hooks", event]);
8277
+ } else {
8278
+ doc.setIn(["hooks", event], filtered);
8279
+ }
8280
+ anythingChanged = true;
8281
+ }
8282
+ const afterHooks = doc.toJS()?.hooks;
8283
+ if (afterHooks && typeof afterHooks === "object" && Object.keys(afterHooks).length === 0) {
8284
+ doc.delete("hooks");
8285
+ anythingChanged = true;
8286
+ }
8287
+ if (anythingChanged) {
8288
+ fs13.writeFileSync(configPath, doc.toString());
8289
+ console.log(chalk.green(` \u2705 Removed Node9 hooks from ${configPath}`));
8290
+ } else {
8291
+ console.log(chalk.blue(` \u2139\uFE0F No Node9 hooks found in ${configPath}`));
8292
+ }
8293
+ }
8294
+ function teardownHermesAllowlist(allowlistPath) {
8295
+ if (!fs13.existsSync(allowlistPath)) return;
8296
+ try {
8297
+ const raw = fs13.readFileSync(allowlistPath, "utf-8");
8298
+ const allowlist = JSON.parse(raw);
8299
+ if (!Array.isArray(allowlist.approvals)) return;
8300
+ const before = allowlist.approvals.length;
8301
+ allowlist.approvals = allowlist.approvals.filter(
8302
+ (e) => !(typeof e?.command === "string" && isNode9Hook(e.command))
8303
+ );
8304
+ if (allowlist.approvals.length === before) return;
8305
+ fs13.writeFileSync(allowlistPath, JSON.stringify(allowlist, null, 2) + "\n");
8306
+ console.log(chalk.green(` \u2705 Removed Node9 entries from ${allowlistPath}`));
8307
+ } catch {
8308
+ }
8309
+ }
8131
8310
  function getAgentsStatus(homeDir2 = os12.homedir()) {
8132
8311
  const detected = detectAgents(homeDir2);
8133
8312
  const claudeWired = (() => {
@@ -8239,10 +8418,29 @@ function getAgentsStatus(homeDir2 = os12.homedir()) {
8239
8418
  // simple existence check on the canonical install location.
8240
8419
  wired: fs13.existsSync(path15.join(homeDir2, ".pi", "agent", "extensions", PI_EXTENSION_NAME)),
8241
8420
  mode: detected.pi ? "hooks" : null
8421
+ },
8422
+ {
8423
+ name: "hermes",
8424
+ label: "Hermes Agent",
8425
+ installed: detected.hermes,
8426
+ // Wired = node9 hook entry exists in the parsed config.yaml.
8427
+ // Reading the YAML cheaply via yaml.parse — we don't need the
8428
+ // Document API for a boolean status check.
8429
+ wired: (() => {
8430
+ try {
8431
+ const raw = fs13.readFileSync(hermesConfigPath(homeDir2), "utf-8");
8432
+ const cfg = yaml.parse(raw);
8433
+ const pre = cfg?.hooks?.["pre_tool_call"] ?? [];
8434
+ return pre.some((e) => typeof e?.command === "string" && isNode9Hook(e.command));
8435
+ } catch {
8436
+ return false;
8437
+ }
8438
+ })(),
8439
+ mode: detected.hermes ? "hooks" : null
8242
8440
  }
8243
8441
  ];
8244
8442
  }
8245
- var NODE9_MCP_SERVER_ENTRY, CODEX_PRE_TOOL_MATCHERS, OPENCODE_PLUGIN_NAME, PI_EXTENSION_NAME;
8443
+ var NODE9_MCP_SERVER_ENTRY, CODEX_PRE_TOOL_MATCHERS, OPENCODE_PLUGIN_NAME, PI_EXTENSION_NAME, HERMES_CONFIG_FILENAME, HERMES_ALLOWLIST_FILENAME, HERMES_HOOK_PLAN;
8246
8444
  var init_setup = __esm({
8247
8445
  "src/setup.ts"() {
8248
8446
  "use strict";
@@ -8253,6 +8451,12 @@ var init_setup = __esm({
8253
8451
  CODEX_PRE_TOOL_MATCHERS = ["^Bash$", "^apply_patch$", "^mcp__.*"];
8254
8452
  OPENCODE_PLUGIN_NAME = "node9.js";
8255
8453
  PI_EXTENSION_NAME = "node9.js";
8454
+ HERMES_CONFIG_FILENAME = "config.yaml";
8455
+ HERMES_ALLOWLIST_FILENAME = "shell-hooks-allowlist.json";
8456
+ HERMES_HOOK_PLAN = [
8457
+ { event: "pre_tool_call", subcmd: "check" },
8458
+ { event: "post_tool_call", subcmd: "log" }
8459
+ ];
8256
8460
  }
8257
8461
  });
8258
8462
 
@@ -16547,6 +16751,33 @@ function resolveUserSkillRoot(entry, cwd) {
16547
16751
  // src/cli/commands/check.ts
16548
16752
  init_dlp();
16549
16753
  init_audit();
16754
+
16755
+ // src/utils/hook-payload.ts
16756
+ function extractToolName(payload, defaultValue = "") {
16757
+ return payload.tool_name ?? payload.name ?? defaultValue;
16758
+ }
16759
+ function extractToolInput(payload) {
16760
+ return payload.tool_input ?? payload.args ?? {};
16761
+ }
16762
+ function canonicalToolName(name) {
16763
+ switch (name) {
16764
+ // Hermes Agent
16765
+ case "terminal":
16766
+ return "Bash";
16767
+ case "write_file":
16768
+ return "Write";
16769
+ case "patch":
16770
+ return "Edit";
16771
+ case "read_file":
16772
+ return "Read";
16773
+ case "search_files":
16774
+ return "Grep";
16775
+ default:
16776
+ return name;
16777
+ }
16778
+ }
16779
+
16780
+ // src/cli/commands/check.ts
16550
16781
  function sanitize2(value) {
16551
16782
  return value.replace(/[\x00-\x1F\x7F]/g, "");
16552
16783
  }
@@ -16565,9 +16796,15 @@ function detectAiAgent(payload) {
16565
16796
  if (payload.hook_event_name === "BeforeTool" || payload.hook_event_name === "AfterTool" || payload.timestamp !== void 0) {
16566
16797
  return "Gemini CLI";
16567
16798
  }
16799
+ if (payload.hook_event_name === "pre_tool_call" || payload.hook_event_name === "post_tool_call") {
16800
+ return "Hermes";
16801
+ }
16568
16802
  if (process.env.CLAUDECODE === "1" || process.env.CLAUDE_CODE_SESSION_ID) {
16569
16803
  return "Claude Code";
16570
16804
  }
16805
+ if (process.env.HERMES_SESSION_ID || process.env.HERMES_HOME || process.env.HERMES_INTERACTIVE) {
16806
+ return "Hermes";
16807
+ }
16571
16808
  if (process.env.GEMINI_CLI_VERSION || process.env.GEMINI_API_KEY) {
16572
16809
  return "Gemini CLI";
16573
16810
  }
@@ -16714,8 +16951,8 @@ RAW: ${raw}
16714
16951
  fs32.appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] STDIN: ${raw}
16715
16952
  `);
16716
16953
  }
16717
- const toolName = sanitize2(payload.tool_name ?? payload.name ?? "");
16718
- const toolInput = payload.tool_input ?? payload.args ?? {};
16954
+ const toolName = canonicalToolName(sanitize2(extractToolName(payload)));
16955
+ const toolInput = extractToolInput(payload);
16719
16956
  const agent = detectAiAgent(payload);
16720
16957
  const mcpMatch = toolName.match(/^mcp__([^_](?:[^_]|_(?!_))*?)__/i);
16721
16958
  const mcpServer = mcpMatch?.[1];
@@ -17047,8 +17284,9 @@ function registerLogCommand(program2) {
17047
17284
  try {
17048
17285
  if (!raw || raw.trim() === "") process.exit(0);
17049
17286
  const payload = JSON.parse(raw);
17050
- const tool = sanitize3(payload.tool_name ?? payload.name ?? "unknown");
17051
- const rawInput = payload.tool_input ?? payload.args ?? {};
17287
+ const rawToolName = sanitize3(extractToolName(payload, "unknown"));
17288
+ const tool = canonicalToolName(rawToolName);
17289
+ const rawInput = extractToolInput(payload);
17052
17290
  const metaTag = (() => {
17053
17291
  const m = payload.meta;
17054
17292
  if (m && typeof m === "object") {
@@ -17057,7 +17295,7 @@ function registerLogCommand(program2) {
17057
17295
  }
17058
17296
  return void 0;
17059
17297
  })();
17060
- const agent = metaTag !== void 0 ? metaTag : 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;
17298
+ const agent = metaTag !== void 0 ? metaTag : payload.turn_id !== void 0 ? "Codex" : payload.hook_event_name === "pre_tool_call" || payload.hook_event_name === "post_tool_call" ? "Hermes" : 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" : process.env.HERMES_SESSION_ID || process.env.HERMES_HOME || process.env.HERMES_INTERACTIVE ? "Hermes" : void 0;
17061
17299
  const entry = {
17062
17300
  ts: (/* @__PURE__ */ new Date()).toISOString(),
17063
17301
  tool,
@@ -17066,6 +17304,7 @@ function registerLogCommand(program2) {
17066
17304
  source: "post-hook"
17067
17305
  };
17068
17306
  if (agent) entry.agent = agent;
17307
+ if (rawToolName !== tool) entry.agentToolName = rawToolName;
17069
17308
  if (payload.session_id) entry.sessionId = payload.session_id;
17070
17309
  const logPath = path34.join(os29.homedir(), ".node9", "audit.log");
17071
17310
  if (!fs33.existsSync(path34.dirname(logPath)))
@@ -19191,12 +19430,12 @@ function registerInitCommand(program2) {
19191
19430
  if (found.length === 0) {
19192
19431
  console.log(
19193
19432
  chalk16.gray(
19194
- "No AI agents detected. Install one of the supported agents (Claude Code, Codex, Gemini CLI, Cursor, Windsurf, VSCode, Claude Desktop, Opencode, or Pi)."
19433
+ "No AI agents detected. Install one of the supported agents (Claude Code, Codex, Gemini CLI, Cursor, Windsurf, VSCode, Claude Desktop, Opencode, Pi, or Hermes Agent)."
19195
19434
  )
19196
19435
  );
19197
19436
  console.log(
19198
19437
  chalk16.gray(
19199
- "then run: node9 agents add <claude|codex|gemini|cursor|windsurf|vscode|claudeDesktop|opencode|pi>"
19438
+ "then run: node9 agents add <claude|codex|gemini|cursor|windsurf|vscode|claudeDesktop|opencode|pi|hermes>"
19200
19439
  )
19201
19440
  );
19202
19441
  return;
@@ -19217,6 +19456,7 @@ function registerInitCommand(program2) {
19217
19456
  else if (agent === "claudeDesktop") await setupClaudeDesktop();
19218
19457
  else if (agent === "opencode") await setupOpencode();
19219
19458
  else if (agent === "pi") await setupPi();
19459
+ else if (agent === "hermes") setupHermes();
19220
19460
  console.log("");
19221
19461
  }
19222
19462
  if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
@@ -20978,7 +21218,8 @@ var SETUP_FN = {
20978
21218
  vscode: setupVSCode,
20979
21219
  claudeDesktop: setupClaudeDesktop,
20980
21220
  opencode: setupOpencode,
20981
- pi: setupPi
21221
+ pi: setupPi,
21222
+ hermes: setupHermes
20982
21223
  };
20983
21224
  var TEARDOWN_FN = {
20984
21225
  claude: teardownClaude,
@@ -20989,7 +21230,8 @@ var TEARDOWN_FN = {
20989
21230
  vscode: teardownVSCode,
20990
21231
  claudeDesktop: teardownClaudeDesktop,
20991
21232
  opencode: teardownOpencode,
20992
- pi: teardownPi
21233
+ pi: teardownPi,
21234
+ hermes: teardownHermes
20993
21235
  };
20994
21236
  var AGENT_NAMES = Object.keys(SETUP_FN);
20995
21237
  function registerAgentsCommand(program2) {
@@ -22327,10 +22569,11 @@ program.command("addto", { hidden: true }).description("Integrate Node9 with an
22327
22569
  if (target === "codex") return await setupCodex();
22328
22570
  if (target === "windsurf") return await setupWindsurf();
22329
22571
  if (target === "vscode") return await setupVSCode();
22572
+ if (target === "hermes") return setupHermes();
22330
22573
  if (target === "hud") return setupHud();
22331
22574
  console.error(
22332
22575
  chalk30.red(
22333
- `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
22576
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hermes, hud`
22334
22577
  )
22335
22578
  );
22336
22579
  process.exit(1);
@@ -22352,6 +22595,7 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
22352
22595
  console.log(" " + chalk30.green("codex") + " \u2014 OpenAI Codex CLI (MCP proxy)");
22353
22596
  console.log(" " + chalk30.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
22354
22597
  console.log(" " + chalk30.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
22598
+ console.log(" " + chalk30.green("hermes") + " \u2014 Hermes Agent (hook mode)");
22355
22599
  process.stdout.write(
22356
22600
  " " + chalk30.green("hud") + " \u2014 Claude Code security statusline\n"
22357
22601
  );
@@ -22365,10 +22609,11 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
22365
22609
  if (t === "codex") return await setupCodex();
22366
22610
  if (t === "windsurf") return await setupWindsurf();
22367
22611
  if (t === "vscode") return await setupVSCode();
22612
+ if (t === "hermes") return setupHermes();
22368
22613
  if (t === "hud") return setupHud();
22369
22614
  console.error(
22370
22615
  chalk30.red(
22371
- `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
22616
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hermes, hud`
22372
22617
  )
22373
22618
  );
22374
22619
  process.exit(1);
@@ -22387,11 +22632,12 @@ program.command("removefrom", { hidden: true }).description("Remove Node9 hooks
22387
22632
  else if (target === "codex") fn = teardownCodex;
22388
22633
  else if (target === "windsurf") fn = teardownWindsurf;
22389
22634
  else if (target === "vscode") fn = teardownVSCode;
22635
+ else if (target === "hermes") fn = teardownHermes;
22390
22636
  else if (target === "hud") fn = teardownHud;
22391
22637
  else {
22392
22638
  console.error(
22393
22639
  chalk30.red(
22394
- `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
22640
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hermes, hud`
22395
22641
  )
22396
22642
  );
22397
22643
  process.exit(1);
@@ -22424,7 +22670,8 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
22424
22670
  ["Cursor", teardownCursor],
22425
22671
  ["Codex", teardownCodex],
22426
22672
  ["Windsurf", teardownWindsurf],
22427
- ["VSCode", teardownVSCode]
22673
+ ["VSCode", teardownVSCode],
22674
+ ["Hermes", teardownHermes]
22428
22675
  ]) {
22429
22676
  try {
22430
22677
  fn();
@@ -2395,7 +2395,14 @@ var init_config = __esm({
2395
2395
  "edit_file",
2396
2396
  "create_file",
2397
2397
  "edit",
2398
- "replace"
2398
+ "replace",
2399
+ // Claude / canonicalised Hermes — shouldSnapshot lowercases the
2400
+ // incoming name before set-membership, so we list the lowercase
2401
+ // forms of `Bash`/`Write`/`Edit`/`MultiEdit`. Without these,
2402
+ // post-canonicalisation Hermes `patch` / `write_file` (which now
2403
+ // arrive as `Edit` / `Write`) silently skipped snapshotting.
2404
+ "write",
2405
+ "multiedit"
2399
2406
  ],
2400
2407
  onlyPaths: [],
2401
2408
  ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
@@ -3394,6 +3401,7 @@ var init_setup_pi_shim = __esm({
3394
3401
  import chalk2 from "chalk";
3395
3402
  import { confirm } from "@inquirer/prompts";
3396
3403
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
3404
+ import * as yaml from "yaml";
3397
3405
  var init_setup = __esm({
3398
3406
  "src/setup.ts"() {
3399
3407
  "use strict";
package/dist/index.js CHANGED
@@ -119,9 +119,11 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
119
119
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
120
120
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
121
121
  const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
122
+ const agentToolNameField = meta?.agentToolName ? { agentToolName: meta.agentToolName } : {};
122
123
  appendToLog(LOCAL_AUDIT_LOG, {
123
124
  ts: (/* @__PURE__ */ new Date()).toISOString(),
124
125
  tool: toolName,
126
+ ...agentToolNameField,
125
127
  ...argsField,
126
128
  decision,
127
129
  checkedBy,
@@ -2998,7 +3000,14 @@ var DEFAULT_CONFIG = {
2998
3000
  "edit_file",
2999
3001
  "create_file",
3000
3002
  "edit",
3001
- "replace"
3003
+ "replace",
3004
+ // Claude / canonicalised Hermes — shouldSnapshot lowercases the
3005
+ // incoming name before set-membership, so we list the lowercase
3006
+ // forms of `Bash`/`Write`/`Edit`/`MultiEdit`. Without these,
3007
+ // post-canonicalisation Hermes `patch` / `write_file` (which now
3008
+ // arrive as `Edit` / `Write`) silently skipped snapshotting.
3009
+ "write",
3010
+ "multiedit"
3002
3011
  ],
3003
3012
  onlyPaths: [],
3004
3013
  ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
package/dist/index.mjs CHANGED
@@ -99,9 +99,11 @@ function appendLocalAudit(toolName, args, decision, checkedBy, meta, auditHashAr
99
99
  const argsField = auditHashArgsEnabled ? { argsHash: hashArgs(args) } : { args: args ? JSON.parse(redactSecrets(JSON.stringify(args))) : {} };
100
100
  const testRun = isTestCall(toolName, args) || process.env.NODE9_TESTING === "1" ? { testRun: true } : {};
101
101
  const ruleNameField = meta?.ruleName ? { ruleName: meta.ruleName } : {};
102
+ const agentToolNameField = meta?.agentToolName ? { agentToolName: meta.agentToolName } : {};
102
103
  appendToLog(LOCAL_AUDIT_LOG, {
103
104
  ts: (/* @__PURE__ */ new Date()).toISOString(),
104
105
  tool: toolName,
106
+ ...agentToolNameField,
105
107
  ...argsField,
106
108
  decision,
107
109
  checkedBy,
@@ -2968,7 +2970,14 @@ var DEFAULT_CONFIG = {
2968
2970
  "edit_file",
2969
2971
  "create_file",
2970
2972
  "edit",
2971
- "replace"
2973
+ "replace",
2974
+ // Claude / canonicalised Hermes — shouldSnapshot lowercases the
2975
+ // incoming name before set-membership, so we list the lowercase
2976
+ // forms of `Bash`/`Write`/`Edit`/`MultiEdit`. Without these,
2977
+ // post-canonicalisation Hermes `patch` / `write_file` (which now
2978
+ // arrive as `Edit` / `Write`) silently skipped snapshotting.
2979
+ "write",
2980
+ "multiedit"
2972
2981
  ],
2973
2982
  onlyPaths: [],
2974
2983
  ignorePaths: ["**/node_modules/**", "dist/**", "build/**", ".next/**", "**/*.log"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.27.1",
3
+ "version": "1.28.0",
4
4
  "description": "The Sudo Command for AI Agents. Execution Security for Claude Code, Codex, Gemini, Cursor, Opencode, Pi, and any MCP server.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -44,6 +44,7 @@
44
44
  "vscode",
45
45
  "opencode",
46
46
  "pi-agent",
47
+ "hermes-agent",
47
48
  "sudo",
48
49
  "security-proxy",
49
50
  "human-in-the-loop",
@@ -90,6 +91,7 @@
90
91
  "safe-regex2": "^5.1.0",
91
92
  "smol-toml": "^1.6.1",
92
93
  "string-width": "^4.2.3",
94
+ "yaml": "^2.9.0",
93
95
  "zod": "^3.25.76"
94
96
  },
95
97
  "bundleDependencies": [