@node9/proxy 1.27.0 → 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 · 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
 
@@ -66,7 +66,7 @@ npm install -g node9-ai
66
66
  ```
67
67
 
68
68
  ```bash
69
- node9 init # auto-wires Claude Code, Gemini CLI, Cursor, Codex, MCP servers
69
+ node9 init # auto-wires all detected agents + MCP servers
70
70
  node9 doctor # verify everything is wired correctly
71
71
  ```
72
72
 
@@ -195,7 +195,7 @@ def run_command(cmd: str) -> str:
195
195
  ## Under the hood
196
196
 
197
197
  - **Scan** reads raw agent history from `~/.claude/projects/`, `~/.gemini/tmp/`, `~/.codex/sessions/` — no API calls, fully offline
198
- - **Runtime** wires PreToolUse hooks into Claude Code, Gemini CLI, and Codex hooks write to `~/.node9/audit.log` atomically
198
+ - **Runtime** intercepts tool calls via pre-execution hooks (Claude Code, Codex, Gemini CLI, Opencode, Pi) or via the MCP gateway (Cursor, Windsurf, VSCode, Claude Desktop). All decisions land in `~/.node9/audit.log` atomically.
199
199
  - **MCP gateway** is a stdio proxy; intercepts `tools/list` + `tools/call` JSON-RPC, forwards the rest
200
200
  - **Policy engine** uses [mvdan-sh](https://github.com/mvdan/sh) for bash AST analysis — defeats obfuscation via backslash escaping, variable substitution, eval of remote download
201
201
  - **Shadow repo** for auto-undo lives at `~/.node9/snapshots/<hash16>/` — never touches your `.git`
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,11 +19458,13 @@ 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 Claude Code, Gemini CLI, Cursor, Windsurf, VSCode, or Codex"
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
- import_chalk16.default.gray("then run: node9 agents add <claude|gemini|cursor|windsurf|vscode|codex>")
19465
+ import_chalk16.default.gray(
19466
+ "then run: node9 agents add <claude|codex|gemini|cursor|windsurf|vscode|claudeDesktop|opencode|pi|hermes>"
19467
+ )
19227
19468
  );
19228
19469
  return;
19229
19470
  }
@@ -19243,6 +19484,7 @@ function registerInitCommand(program2) {
19243
19484
  else if (agent === "claudeDesktop") await setupClaudeDesktop();
19244
19485
  else if (agent === "opencode") await setupOpencode();
19245
19486
  else if (agent === "pi") await setupPi();
19487
+ else if (agent === "hermes") setupHermes();
19246
19488
  console.log("");
19247
19489
  }
19248
19490
  if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
@@ -21004,7 +21246,8 @@ var SETUP_FN = {
21004
21246
  vscode: setupVSCode,
21005
21247
  claudeDesktop: setupClaudeDesktop,
21006
21248
  opencode: setupOpencode,
21007
- pi: setupPi
21249
+ pi: setupPi,
21250
+ hermes: setupHermes
21008
21251
  };
21009
21252
  var TEARDOWN_FN = {
21010
21253
  claude: teardownClaude,
@@ -21015,7 +21258,8 @@ var TEARDOWN_FN = {
21015
21258
  vscode: teardownVSCode,
21016
21259
  claudeDesktop: teardownClaudeDesktop,
21017
21260
  opencode: teardownOpencode,
21018
- pi: teardownPi
21261
+ pi: teardownPi,
21262
+ hermes: teardownHermes
21019
21263
  };
21020
21264
  var AGENT_NAMES = Object.keys(SETUP_FN);
21021
21265
  function registerAgentsCommand(program2) {
@@ -22353,10 +22597,11 @@ program.command("addto", { hidden: true }).description("Integrate Node9 with an
22353
22597
  if (target === "codex") return await setupCodex();
22354
22598
  if (target === "windsurf") return await setupWindsurf();
22355
22599
  if (target === "vscode") return await setupVSCode();
22600
+ if (target === "hermes") return setupHermes();
22356
22601
  if (target === "hud") return setupHud();
22357
22602
  console.error(
22358
22603
  import_chalk30.default.red(
22359
- `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
22604
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hermes, hud`
22360
22605
  )
22361
22606
  );
22362
22607
  process.exit(1);
@@ -22378,6 +22623,7 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
22378
22623
  console.log(" " + import_chalk30.default.green("codex") + " \u2014 OpenAI Codex CLI (MCP proxy)");
22379
22624
  console.log(" " + import_chalk30.default.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
22380
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)");
22381
22627
  process.stdout.write(
22382
22628
  " " + import_chalk30.default.green("hud") + " \u2014 Claude Code security statusline\n"
22383
22629
  );
@@ -22391,10 +22637,11 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
22391
22637
  if (t === "codex") return await setupCodex();
22392
22638
  if (t === "windsurf") return await setupWindsurf();
22393
22639
  if (t === "vscode") return await setupVSCode();
22640
+ if (t === "hermes") return setupHermes();
22394
22641
  if (t === "hud") return setupHud();
22395
22642
  console.error(
22396
22643
  import_chalk30.default.red(
22397
- `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
22644
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hermes, hud`
22398
22645
  )
22399
22646
  );
22400
22647
  process.exit(1);
@@ -22413,11 +22660,12 @@ program.command("removefrom", { hidden: true }).description("Remove Node9 hooks
22413
22660
  else if (target === "codex") fn = teardownCodex;
22414
22661
  else if (target === "windsurf") fn = teardownWindsurf;
22415
22662
  else if (target === "vscode") fn = teardownVSCode;
22663
+ else if (target === "hermes") fn = teardownHermes;
22416
22664
  else if (target === "hud") fn = teardownHud;
22417
22665
  else {
22418
22666
  console.error(
22419
22667
  import_chalk30.default.red(
22420
- `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
22668
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hermes, hud`
22421
22669
  )
22422
22670
  );
22423
22671
  process.exit(1);
@@ -22450,7 +22698,8 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
22450
22698
  ["Cursor", teardownCursor],
22451
22699
  ["Codex", teardownCodex],
22452
22700
  ["Windsurf", teardownWindsurf],
22453
- ["VSCode", teardownVSCode]
22701
+ ["VSCode", teardownVSCode],
22702
+ ["Hermes", teardownHermes]
22454
22703
  ]) {
22455
22704
  try {
22456
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,11 +19430,13 @@ function registerInitCommand(program2) {
19191
19430
  if (found.length === 0) {
19192
19431
  console.log(
19193
19432
  chalk16.gray(
19194
- "No AI agents detected. Install Claude Code, Gemini CLI, Cursor, Windsurf, VSCode, or Codex"
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
- chalk16.gray("then run: node9 agents add <claude|gemini|cursor|windsurf|vscode|codex>")
19437
+ chalk16.gray(
19438
+ "then run: node9 agents add <claude|codex|gemini|cursor|windsurf|vscode|claudeDesktop|opencode|pi|hermes>"
19439
+ )
19199
19440
  );
19200
19441
  return;
19201
19442
  }
@@ -19215,6 +19456,7 @@ function registerInitCommand(program2) {
19215
19456
  else if (agent === "claudeDesktop") await setupClaudeDesktop();
19216
19457
  else if (agent === "opencode") await setupOpencode();
19217
19458
  else if (agent === "pi") await setupPi();
19459
+ else if (agent === "hermes") setupHermes();
19218
19460
  console.log("");
19219
19461
  }
19220
19462
  if ((process.platform === "darwin" || process.platform === "linux") && process.stdout.isTTY) {
@@ -20976,7 +21218,8 @@ var SETUP_FN = {
20976
21218
  vscode: setupVSCode,
20977
21219
  claudeDesktop: setupClaudeDesktop,
20978
21220
  opencode: setupOpencode,
20979
- pi: setupPi
21221
+ pi: setupPi,
21222
+ hermes: setupHermes
20980
21223
  };
20981
21224
  var TEARDOWN_FN = {
20982
21225
  claude: teardownClaude,
@@ -20987,7 +21230,8 @@ var TEARDOWN_FN = {
20987
21230
  vscode: teardownVSCode,
20988
21231
  claudeDesktop: teardownClaudeDesktop,
20989
21232
  opencode: teardownOpencode,
20990
- pi: teardownPi
21233
+ pi: teardownPi,
21234
+ hermes: teardownHermes
20991
21235
  };
20992
21236
  var AGENT_NAMES = Object.keys(SETUP_FN);
20993
21237
  function registerAgentsCommand(program2) {
@@ -22325,10 +22569,11 @@ program.command("addto", { hidden: true }).description("Integrate Node9 with an
22325
22569
  if (target === "codex") return await setupCodex();
22326
22570
  if (target === "windsurf") return await setupWindsurf();
22327
22571
  if (target === "vscode") return await setupVSCode();
22572
+ if (target === "hermes") return setupHermes();
22328
22573
  if (target === "hud") return setupHud();
22329
22574
  console.error(
22330
22575
  chalk30.red(
22331
- `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
22576
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hermes, hud`
22332
22577
  )
22333
22578
  );
22334
22579
  process.exit(1);
@@ -22350,6 +22595,7 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
22350
22595
  console.log(" " + chalk30.green("codex") + " \u2014 OpenAI Codex CLI (MCP proxy)");
22351
22596
  console.log(" " + chalk30.green("windsurf") + " \u2014 Windsurf (MCP proxy)");
22352
22597
  console.log(" " + chalk30.green("vscode") + " \u2014 VSCode / Copilot (MCP proxy)");
22598
+ console.log(" " + chalk30.green("hermes") + " \u2014 Hermes Agent (hook mode)");
22353
22599
  process.stdout.write(
22354
22600
  " " + chalk30.green("hud") + " \u2014 Claude Code security statusline\n"
22355
22601
  );
@@ -22363,10 +22609,11 @@ program.command("setup", { hidden: true }).description('Alias for "addto" \u2014
22363
22609
  if (t === "codex") return await setupCodex();
22364
22610
  if (t === "windsurf") return await setupWindsurf();
22365
22611
  if (t === "vscode") return await setupVSCode();
22612
+ if (t === "hermes") return setupHermes();
22366
22613
  if (t === "hud") return setupHud();
22367
22614
  console.error(
22368
22615
  chalk30.red(
22369
- `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
22616
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hermes, hud`
22370
22617
  )
22371
22618
  );
22372
22619
  process.exit(1);
@@ -22385,11 +22632,12 @@ program.command("removefrom", { hidden: true }).description("Remove Node9 hooks
22385
22632
  else if (target === "codex") fn = teardownCodex;
22386
22633
  else if (target === "windsurf") fn = teardownWindsurf;
22387
22634
  else if (target === "vscode") fn = teardownVSCode;
22635
+ else if (target === "hermes") fn = teardownHermes;
22388
22636
  else if (target === "hud") fn = teardownHud;
22389
22637
  else {
22390
22638
  console.error(
22391
22639
  chalk30.red(
22392
- `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hud`
22640
+ `Unknown target: "${target}". Supported: claude, gemini, cursor, codex, windsurf, vscode, hermes, hud`
22393
22641
  )
22394
22642
  );
22395
22643
  process.exit(1);
@@ -22422,7 +22670,8 @@ program.command("uninstall").description("Remove all Node9 hooks and optionally
22422
22670
  ["Cursor", teardownCursor],
22423
22671
  ["Codex", teardownCodex],
22424
22672
  ["Windsurf", teardownWindsurf],
22425
- ["VSCode", teardownVSCode]
22673
+ ["VSCode", teardownVSCode],
22674
+ ["Hermes", teardownHermes]
22426
22675
  ]) {
22427
22676
  try {
22428
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,7 +1,7 @@
1
1
  {
2
2
  "name": "@node9/proxy",
3
- "version": "1.27.0",
4
- "description": "The Sudo Command for AI Agents. Execution Security for Claude Code & MCP.",
3
+ "version": "1.28.0",
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",
7
7
  "types": "./dist/index.d.ts",
@@ -31,13 +31,20 @@
31
31
  "homepage": "https://github.com/node9-ai/node9-proxy#readme",
32
32
  "keywords": [
33
33
  "ai-security",
34
+ "agent-security",
35
+ "agentic-ai",
34
36
  "mcp",
35
37
  "mcp-proxy",
36
38
  "claude-code",
39
+ "claude-desktop",
40
+ "codex",
37
41
  "gemini-cli",
38
42
  "cursor",
39
- "agentic-ai",
40
- "agent-security",
43
+ "windsurf",
44
+ "vscode",
45
+ "opencode",
46
+ "pi-agent",
47
+ "hermes-agent",
41
48
  "sudo",
42
49
  "security-proxy",
43
50
  "human-in-the-loop",
@@ -84,6 +91,7 @@
84
91
  "safe-regex2": "^5.1.0",
85
92
  "smol-toml": "^1.6.1",
86
93
  "string-width": "^4.2.3",
94
+ "yaml": "^2.9.0",
87
95
  "zod": "^3.25.76"
88
96
  },
89
97
  "bundleDependencies": [