@ory/argus 1.2.3 → 1.2.4

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.
@@ -46,12 +46,75 @@ const LITERAL_LEAF_TYPES = new Set(["word", "string_content", "raw_string"]);
46
46
  */
47
47
  const WRAPPERS = new Set([
48
48
  "sudo", "doas", "env", "nohup", "setsid", "command", "exec", "nice",
49
- "ionice", "chrt", "stdbuf", "time", "timeout", "xargs",
49
+ "ionice", "chrt", "stdbuf", "time", "timeout", "xargs", "builtin",
50
50
  ]);
51
- /** Wrappers that take a numeric positional operand before the wrapped command. */
52
- const OPERAND_WRAPPERS = new Set(["timeout", "nice", "ionice", "chrt"]);
53
- /** A numeric operand (`5`, `5s`, `1.5`) consumed by an {@link OPERAND_WRAPPERS}. */
54
- const NUMERIC_OPERAND = /^\d+(\.\d+)?[smhdSMHD]?$/;
51
+ /** Wrapper options whose following token is an option value, not a command. */
52
+ const WRAPPER_OPTIONS_WITH_VALUES = {
53
+ doas: new Set(["-a", "-C", "-u"]),
54
+ sudo: new Set([
55
+ "-u", "--user", "-g", "--group", "-h", "--host", "-p", "--prompt",
56
+ "-C", "--close-from", "-T", "--command-timeout", "-R", "--chroot",
57
+ "-D", "--chdir", "-r", "--role", "-t", "--type",
58
+ ]),
59
+ env: new Set([
60
+ "-u", "--unset", "-C", "--chdir",
61
+ ]),
62
+ nice: new Set(["-n", "--adjustment"]),
63
+ exec: new Set(["-a"]),
64
+ ionice: new Set([
65
+ "-c", "--class", "-n", "--classdata", "-p", "--pid", "-P", "--pgid",
66
+ "-u", "--uid",
67
+ ]),
68
+ timeout: new Set(["-s", "--signal", "-k", "--kill-after"]),
69
+ stdbuf: new Set(["-i", "--input", "-o", "--output", "-e", "--error"]),
70
+ xargs: new Set([
71
+ "-a", "--arg-file", "-E", "--eof", "-I", "--replace", "-L",
72
+ "--max-lines", "-n", "--max-args", "-P", "--max-procs", "-s",
73
+ "--max-chars",
74
+ ]),
75
+ };
76
+ /** Common standalone options that do not consume the following command token. */
77
+ const WRAPPER_OPTIONS_WITHOUT_VALUES = {
78
+ doas: new Set(["-L", "-n", "-s"]),
79
+ sudo: new Set([
80
+ "-A", "--askpass", "-b", "--background", "-E", "--preserve-env",
81
+ "-H", "--set-home", "-K", "-k", "--reset-timestamp", "-N",
82
+ "--no-update", "-n", "--non-interactive", "-S", "--stdin", "-V",
83
+ "--version", "-v", "--validate",
84
+ ]),
85
+ env: new Set([
86
+ "-i", "--ignore-environment", "-0", "--null", "-v", "--debug",
87
+ "--ignore-signal", "--default-signal", "--block-signal",
88
+ ]),
89
+ command: new Set(["-p", "-v", "-V"]),
90
+ chrt: new Set([
91
+ "-a", "--all-tasks", "-b", "--batch", "-d", "--deadline", "-f", "--fifo",
92
+ "-i", "--idle", "-m", "--max", "-o", "--other", "-r", "--rr",
93
+ ]),
94
+ exec: new Set(["-c", "-l"]),
95
+ setsid: new Set(["-c", "--ctty", "-f", "--fork", "-w", "--wait"]),
96
+ time: new Set(["-p"]),
97
+ timeout: new Set(["--foreground", "--preserve-status", "-v", "--verbose"]),
98
+ xargs: new Set([
99
+ "-0", "--null", "-r", "--no-run-if-empty", "-t", "--verbose", "-x",
100
+ "--exit",
101
+ ]),
102
+ };
103
+ const WRAPPER_OPTIONS_WITH_ATTACHED_VALUES = {
104
+ doas: /^-[aCu].+/,
105
+ sudo: /^-[ughCpTDRrt].+/,
106
+ nice: /^-(?:n.+|\d+)$/,
107
+ exec: /^-a.+/,
108
+ ionice: /^-[cnpPu].+/,
109
+ timeout: /^-[sk].+/,
110
+ stdbuf: /^-[ioe].+/,
111
+ xargs: /^-[ELnPs].+/,
112
+ };
113
+ /** Wrappers with a required positional operand before the wrapped command. */
114
+ const WRAPPER_POSITIONAL_OPERANDS = {
115
+ timeout: 1,
116
+ chrt: 1,
117
+ };
55
118
  /** Shells whose `-c "<script>"` argument is re-parsed as a nested command. */
56
119
  const SHELL_DASH_C = new Set(["sh", "bash", "dash", "zsh", "ksh", "ash"]);
57
120
  /** Cap re-parse recursion (`sh -c "sh -c …"`) so a pathological input can't loop. */
@@ -122,6 +185,8 @@ function lastLeaf(node) {
122
185
  function literalText(node) {
123
186
  switch (node.type) {
124
187
  case "word":
188
+ // Unquoted globs depend on the runtime filesystem and are not literal.
189
+ return /[*?[]/.test(node.text) ? null : node.text;
125
190
  case "number":
126
191
  return node.text;
127
192
  case "raw_string":
@@ -150,10 +215,10 @@ function literalText(node) {
150
215
  function resolveCommandName(nameNode) {
151
216
  const child = nameNode.namedChild(0);
152
217
  if (!child)
153
- return basename(nameNode.text);
218
+ return /[*?[]/.test(nameNode.text) ? null : basename(nameNode.text);
154
219
  switch (child.type) {
155
220
  case "word":
156
- return basename(child.text);
221
+ return /[*?[]/.test(child.text) ? null : basename(child.text);
157
222
  case "raw_string":
158
223
  return basename(stripQuotes(child.text));
159
224
  case "simple_expansion":
@@ -227,20 +292,55 @@ function collectRedirects(cmd) {
227
292
  * {@link ShellCommand} for that command (and recursing through nested wrappers,
228
293
  * e.g. `sudo env A=1 curl`).
229
294
  */
230
- function descendWrapper(wrapper, args, pipeline, state) {
231
- const skipsNumeric = OPERAND_WRAPPERS.has(wrapper);
295
+ function descendWrapper(wrapper, args, pipeline, parser, depth, state) {
296
+ let positionalOperands = WRAPPER_POSITIONAL_OPERANDS[wrapper] ?? 0;
297
+ let optionsEnded = false;
232
298
  for (let i = 0; i < args.length; i++) {
233
299
  const txt = literalText(args[i]);
234
300
  if (txt === null) {
235
301
  state.tooComplex = true;
236
302
  return;
237
303
  }
238
- if (txt.startsWith("-"))
304
+ if (wrapper === "env" && /^--s[^=]*=/.test(txt)) {
305
+ state.tooComplex = true;
306
+ return;
307
+ }
308
+ if (wrapper === "env" && (txt === "-S" || txt === "--split-string")) {
309
+ // env's split-string language is not shell syntax. Parsing it with the
310
+ // Bash grammar can reinterpret comments and quoting, so fail closed.
311
+ state.tooComplex = true;
312
+ return;
313
+ }
314
+ if (!optionsEnded && txt.startsWith("-")) {
315
+ if (txt === "--") {
316
+ optionsEnded = true;
317
+ continue;
318
+ }
319
+ if (txt.includes("="))
320
+ continue;
321
+ if (WRAPPER_OPTIONS_WITH_ATTACHED_VALUES[wrapper]?.test(txt))
322
+ continue;
323
+ if (WRAPPER_OPTIONS_WITH_VALUES[wrapper]?.has(txt)) {
324
+ if (!args[i + 1] || literalText(args[i + 1]) === null) {
325
+ state.tooComplex = true;
326
+ return;
327
+ }
328
+ i++;
329
+ continue;
330
+ }
331
+ if (!WRAPPER_OPTIONS_WITHOUT_VALUES[wrapper]?.has(txt)) {
332
+ // An unknown option may consume the next token. Do not guess which
333
+ // token is the command because a wrong guess would silently bypass it.
334
+ state.tooComplex = true;
335
+ }
239
336
  continue;
337
+ }
240
338
  if (/^[A-Za-z_]\w*=/.test(txt))
241
339
  continue;
242
- if (skipsNumeric && NUMERIC_OPERAND.test(txt))
340
+ if (positionalOperands > 0) {
341
+ positionalOperands--;
243
342
  continue;
343
+ }
244
344
  const wrapped = basename(txt);
245
345
  if (wrapped.length === 0)
246
346
  return;
@@ -252,11 +352,92 @@ function descendWrapper(wrapper, args, pipeline, state) {
252
352
  redirects: [],
253
353
  pipeline,
254
354
  });
255
- if (WRAPPERS.has(wrapped))
256
- descendWrapper(wrapped, rest, pipeline, state);
355
+ if (SHELL_DASH_C.has(wrapped) && state.pipelineIds.has(pipeline)) {
356
+ if (shellReadsPipelineInput(rest))
357
+ state.tooComplex = true;
358
+ }
359
+ if (WRAPPERS.has(wrapped)) {
360
+ descendWrapper(wrapped, rest, pipeline, parser, depth, state);
361
+ }
362
+ inspectLiteralScriptInvocation(wrapped, rest, parser, state, depth);
257
363
  return;
258
364
  }
259
365
  }
366
+ function shellReadsPipelineInput(args) {
367
+ for (let i = 0; i < args.length; i++) {
368
+ const arg = literalText(args[i]);
369
+ if (arg === null)
370
+ return true;
371
+ if (arg === "--")
372
+ return i + 1 >= args.length;
373
+ if (arg === "-O" || arg === "-o" || arg === "--init-file" || arg === "--rcfile") {
374
+ i++;
375
+ continue;
376
+ }
377
+ if (arg.startsWith("-")) {
378
+ if (/^-[^-]*c/.test(arg))
379
+ return false;
380
+ if (/^-[^-]*s/.test(arg))
381
+ return true;
382
+ continue;
383
+ }
384
+ const normalized = arg.startsWith("/")
385
+ ? `/${arg.split("/").reduce((parts, part) => {
386
+ if (!part || part === ".")
387
+ return parts;
388
+ if (part === "..")
389
+ parts.pop();
390
+ else
391
+ parts.push(part);
392
+ return parts;
393
+ }, []).join("/")}`
394
+ : arg;
395
+ if (arg === "-"
396
+ || normalized === "/dev/stdin"
397
+ || /^\/dev\/fd\/0+$/.test(normalized)
398
+ || /^\/proc\/self\/fd\/0+$/.test(normalized))
399
+ return true;
400
+ // A positional operand is a script file, so pipeline stdin is data rather
401
+ // than shell source and does not hide additional command words.
402
+ return false;
403
+ }
404
+ return true;
405
+ }
406
+ function inspectLiteralScriptInvocation(name, args, parser, state, depth) {
407
+ if (SHELL_DASH_C.has(name)) {
408
+ for (let i = 0; i < args.length; i++) {
409
+ const text = literalText(args[i]);
410
+ if (text === null) {
411
+ state.tooComplex = true;
412
+ break;
413
+ }
414
+ if (text === "--" || !text.startsWith("-"))
415
+ break;
416
+ if (text === "-O" || text === "-o" || text === "--init-file" || text === "--rcfile") {
417
+ i++;
418
+ continue;
419
+ }
420
+ if (/^-[^-]*c/.test(text)) {
421
+ const target = args[i + 1];
422
+ const script = target ? literalText(target) : null;
423
+ if (script === null)
424
+ state.tooComplex = true;
425
+ else
426
+ reparse(script, parser, state, depth + 1);
427
+ break;
428
+ }
429
+ }
430
+ }
431
+ if (name === "eval") {
432
+ const parts = args.map(literalText);
433
+ if (parts.length === 0 || parts.some((part) => part === null)) {
434
+ state.tooComplex = true;
435
+ }
436
+ else {
437
+ reparse(parts.join(" "), parser, state, depth + 1);
438
+ }
439
+ }
440
+ }
260
441
  /** Recursively walk the tree, building the command model. */
261
442
  function walk(node, pipeline, parser, state, depth, treeDepth) {
262
443
  // Bound recursion so a maliciously deep AST can't overflow the stack (which
@@ -267,6 +448,7 @@ function walk(node, pipeline, parser, state, depth, treeDepth) {
267
448
  }
268
449
  if (node.type === "pipeline") {
269
450
  const id = state.pipelineCounter++;
451
+ state.pipelineIds.add(id);
270
452
  for (const child of node.children)
271
453
  walk(child, id, parser, state, depth, treeDepth + 1);
272
454
  return;
@@ -291,41 +473,15 @@ function walk(node, pipeline, parser, state, depth, treeDepth) {
291
473
  redirects: collectRedirects(node),
292
474
  pipeline: pid,
293
475
  });
294
- if (name && WRAPPERS.has(name))
295
- descendWrapper(name, args, pid, state);
296
- if (name && SHELL_DASH_C.has(name)) {
297
- const flagIdx = args.findIndex((a) => {
298
- const t = literalText(a);
299
- return t !== null && t.startsWith("-") && t.includes("c");
300
- });
301
- if (flagIdx >= 0) {
302
- const target = args[flagIdx + 1];
303
- if (!target)
304
- state.tooComplex = true;
305
- else {
306
- const script = literalText(target);
307
- if (script === null)
308
- state.tooComplex = true;
309
- else
310
- reparse(script, parser, state, depth + 1);
311
- }
312
- }
476
+ if (name && WRAPPERS.has(name)) {
477
+ descendWrapper(name, args, pid, parser, depth, state);
313
478
  }
314
- if (name === "eval") {
315
- const parts = [];
316
- let dynamic = false;
317
- for (const a of args) {
318
- const t = literalText(a);
319
- if (t === null)
320
- dynamic = true;
321
- else
322
- parts.push(t);
323
- }
324
- if (dynamic || parts.length === 0)
479
+ if (name && SHELL_DASH_C.has(name) && state.pipelineIds.has(pid)) {
480
+ if (shellReadsPipelineInput(args))
325
481
  state.tooComplex = true;
326
- else
327
- reparse(parts.join(" "), parser, state, depth + 1);
328
482
  }
483
+ if (name)
484
+ inspectLiteralScriptInvocation(name, args, parser, state, depth);
329
485
  // Descend into arguments for nested command substitutions ($(curl …)).
330
486
  for (const child of node.children)
331
487
  walk(child, -1, parser, state, depth, treeDepth + 1);
@@ -337,7 +493,11 @@ function walk(node, pipeline, parser, state, depth, treeDepth) {
337
493
  }
338
494
  /** Parse a nested script string and fold its commands into `state`. */
339
495
  function reparse(script, parser, state, depth) {
340
- if (depth > MAX_REPARSE_DEPTH || script.trim().length === 0)
496
+ if (depth > MAX_REPARSE_DEPTH) {
497
+ state.tooComplex = true;
498
+ return;
499
+ }
500
+ if (script.trim().length === 0)
341
501
  return;
342
502
  const tree = parser.parse(script);
343
503
  if (!tree) {
@@ -370,7 +530,12 @@ async function extractShellCommands(command) {
370
530
  }
371
531
  if (!tree)
372
532
  return { commands: [], tooComplex: true };
373
- const state = { commands: [], tooComplex: false, pipelineCounter: 0 };
533
+ const state = {
534
+ commands: [],
535
+ tooComplex: false,
536
+ pipelineCounter: 0,
537
+ pipelineIds: new Set(),
538
+ };
374
539
  if (command.includes("\\\n"))
375
540
  state.tooComplex = true;
376
541
  if (tree.rootNode.hasError)
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "repo": "ory-agent-plugins",
3
- "commit": "bce419a80533136b9877d3cd33e0a03e1e585f52",
4
- "commitShort": "bce419a",
3
+ "commit": "8e70fe38343bc8599c65c5e1bd4af54861f633be",
4
+ "commitShort": "8e70fe3",
5
5
  "branch": "main",
6
- "commitDate": "2026-09-11T13:08:35-07:00",
6
+ "commitDate": "2026-09-14T08:17:01-07:00",
7
7
  "dirty": false,
8
- "builtAt": "2026-09-11T20:11:58.706Z"
8
+ "builtAt": "2026-09-14T15:20:26.275Z"
9
9
  }
@@ -231,13 +231,13 @@ export interface GateToolCallArgs {
231
231
  /** Override the resolved {@link PermissionMode}. Tests use this. */
232
232
  modeOverride?: PermissionMode;
233
233
  /**
234
- * Raw shell command string when {@link toolName} is the harness's shell tool
234
+ * Raw shell command string(s) when {@link toolName} is the harness's shell tool
235
235
  * (issue #76). When present and the tool is a shell tool, the gate decomposes
236
- * the command into `ShellTool:<word>#use` sub-checks in addition to the
236
+ * each command into `ShellTool:<word>#use` sub-checks in addition to the
237
237
  * top-level tool check. Absent / undefined ⇒ decomposition is skipped and the
238
238
  * gate behaves exactly as before.
239
239
  */
240
- shellCommand?: string;
240
+ shellCommand?: string | readonly string[];
241
241
  /**
242
242
  * The acting machine principals, so an explicit block on the agent install,
243
243
  * this session, the sub-agent kind, or this spawn can deny the call. Omitted ⇒
@@ -407,11 +407,36 @@ async function decomposeAndCheck(client, args) {
407
407
  // letting the exception bubble to the harness catch, which fails open.
408
408
  let parsed;
409
409
  try {
410
- parsed = await (0, bash_parser_js_1.extractBashCommandWords)(args.shellCommand ?? "");
410
+ const commands = typeof args.shellCommand === "string"
411
+ ? [args.shellCommand]
412
+ : args.shellCommand ?? [];
413
+ const words = [];
414
+ const seen = new Set();
415
+ let tooComplex = false;
416
+ let parserUnavailable = false;
417
+ for (const command of commands) {
418
+ const result = await (0, bash_parser_js_1.extractBashCommandWords)(command);
419
+ tooComplex ||= result.tooComplex;
420
+ parserUnavailable ||= result.parserUnavailable === true;
421
+ for (const word of result.words) {
422
+ if (!seen.has(word)) {
423
+ seen.add(word);
424
+ words.push(word);
425
+ }
426
+ }
427
+ }
428
+ parsed = { words, tooComplex, parserUnavailable };
411
429
  }
412
430
  catch {
413
431
  parsed = { words: [], tooComplex: true };
414
432
  }
433
+ if (args.toolName.toLowerCase() === "powershell"
434
+ || (args.harness === "cline" && args.toolName === "run_commands" && process.platform === "win32")) {
435
+ // The bundled grammar models POSIX shell syntax, not PowerShell. Retain its
436
+ // Never retain words parsed with the incompatible grammar: PowerShell
437
+ // literals could otherwise be persisted as if they were command names.
438
+ parsed = { words: [], tooComplex: true, parserUnavailable: false };
439
+ }
415
440
  if (parsed.parserUnavailable) {
416
441
  client.logger.warn("bash.parser_unavailable", {
417
442
  harness: args.harness,
@@ -14,17 +14,17 @@
14
14
  * Built-in tool names known to ship with each supported harness.
15
15
  */
16
16
  export declare const HARNESS_TOOL_CATALOG: {
17
- readonly "claude-code": readonly ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "WebFetch", "WebSearch", "NotebookEdit", "ToolSearch", "Skill", "TodoWrite", "Agent", "Task"];
18
- readonly "claude-agent-sdk": readonly ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "WebFetch", "WebSearch", "NotebookEdit", "ToolSearch", "Skill", "TodoWrite", "Agent", "Task"];
17
+ readonly "claude-code": readonly ["Read", "Write", "Edit", "Bash", "Monitor", "PowerShell", "Workflow", "Glob", "Grep", "WebFetch", "WebSearch", "NotebookEdit", "ToolSearch", "Skill", "TodoWrite", "Agent", "Task"];
18
+ readonly "claude-agent-sdk": readonly ["Read", "Write", "Edit", "Bash", "Monitor", "PowerShell", "Workflow", "Glob", "Grep", "WebFetch", "WebSearch", "NotebookEdit", "ToolSearch", "Skill", "TodoWrite", "Agent", "Task"];
19
19
  readonly codex: readonly ["Bash", "apply_patch"];
20
20
  readonly "gemini-cli": readonly ["read_file", "write_file", "edit_file", "list_files", "search_files", "shell", "web_search"];
21
21
  readonly openclaw: readonly ["exec", "read", "write", "edit", "apply_patch", "web_search", "web_fetch", "browser"];
22
22
  readonly opencode: readonly ["read", "write", "edit", "bash", "glob", "grep", "webfetch"];
23
23
  readonly continue: readonly ["Bash", "Read", "Edit", "Write", "Grep", "Glob", "WebSearch"];
24
24
  readonly goose: readonly ["shell", "text_editor", "read_file", "write_file", "list_windows", "screen_capture"];
25
- readonly cline: readonly ["execute_command", "read_file", "write_to_file", "replace_in_file", "search_files", "list_files", "use_mcp_tool"];
25
+ readonly cline: readonly ["execute_command", "run_commands", "read_file", "write_to_file", "replace_in_file", "search_files", "list_files", "use_mcp_tool"];
26
26
  readonly amp: readonly ["Bash", "Read", "create_file", "edit_file", "undo_edit", "glob", "Grep", "finder", "read_web_page", "web_search", "todo_read", "todo_write", "oracle", "Task"];
27
- readonly pi: readonly ["read", "write", "edit", "bash", "grep", "find", "ls"];
27
+ readonly pi: readonly ["read", "write", "edit", "bash", "powershell", "grep", "find", "ls"];
28
28
  readonly antigravity: readonly ["run_command", "view_file", "write_to_file", "replace_file_content", "multi_replace_file_content", "list_directory", "grep_search", "find", "read_url_content", "search_web", "call_mcp_tool"];
29
29
  readonly cursor: readonly ["Read", "Write", "Edit", "Delete", "Shell", "List", "Search", "MCP", "Task"];
30
30
  };
@@ -109,10 +109,12 @@ export declare function getShellCommandCatalog(): readonly string[];
109
109
  */
110
110
  export declare function resolveShellCommandNamespace(): string;
111
111
  /**
112
- * Extract the shell command string from a harness's tool-args object — the same
112
+ * Extract shell command strings from a harness's tool-args object — the same
113
113
  * object the handler already passes to `summarizeToolInput`. Returns `undefined`
114
114
  * when the tool isn't a shell tool for this harness or no command field is
115
- * present (the gate then skips decomposition and degrades to the top-level
116
- * `Bash` check — it never breaks).
115
+ * present. Cline's `run_commands` is the exception: malformed command data is
116
+ * represented as an unresolvable command so enforce mode fails closed. Cline's
117
+ * command array remains an array so independently executed entries cannot
118
+ * change how neighboring entries parse.
117
119
  */
118
- export declare function extractShellCommand(harness: string, toolName: string, args: unknown): string | undefined;
120
+ export declare function extractShellCommand(harness: string, toolName: string, args: unknown): string | readonly string[] | undefined;
@@ -35,6 +35,9 @@ exports.HARNESS_TOOL_CATALOG = {
35
35
  "Write",
36
36
  "Edit",
37
37
  "Bash",
38
+ "Monitor",
39
+ "PowerShell",
40
+ "Workflow",
38
41
  "Glob",
39
42
  "Grep",
40
43
  "WebFetch",
@@ -54,6 +57,9 @@ exports.HARNESS_TOOL_CATALOG = {
54
57
  "Write",
55
58
  "Edit",
56
59
  "Bash",
60
+ "Monitor",
61
+ "PowerShell",
62
+ "Workflow",
57
63
  "Glob",
58
64
  "Grep",
59
65
  "WebFetch",
@@ -103,6 +109,7 @@ exports.HARNESS_TOOL_CATALOG = {
103
109
  ],
104
110
  cline: [
105
111
  "execute_command",
112
+ "run_commands",
106
113
  "read_file",
107
114
  "write_to_file",
108
115
  "replace_in_file",
@@ -126,7 +133,7 @@ exports.HARNESS_TOOL_CATALOG = {
126
133
  "oracle",
127
134
  "Task",
128
135
  ],
129
- pi: ["read", "write", "edit", "bash", "grep", "find", "ls"],
136
+ pi: ["read", "write", "edit", "bash", "powershell", "grep", "find", "ls"],
130
137
  antigravity: [
131
138
  "run_command",
132
139
  "view_file",
@@ -245,8 +252,8 @@ function isInteractiveTool(harness, toolName) {
245
252
  * precise map is used rather than the {@link isBashLikeTool} heuristic.
246
253
  */
247
254
  exports.SHELL_TOOL_CATALOG = {
248
- "claude-code": ["Bash"],
249
- "claude-agent-sdk": ["Bash"],
255
+ "claude-code": ["Bash", "Monitor", "PowerShell"],
256
+ "claude-agent-sdk": ["Bash", "Monitor", "PowerShell"],
250
257
  codex: ["Bash"],
251
258
  // Gemini CLI's hook payload has been observed with both the short `shell`
252
259
  // name and the fuller `run_shell_command` id across versions; accept both so
@@ -256,10 +263,11 @@ exports.SHELL_TOOL_CATALOG = {
256
263
  opencode: ["bash"],
257
264
  continue: ["Bash"],
258
265
  goose: ["shell"],
259
- cline: ["execute_command"],
266
+ cline: ["execute_command", "run_commands"],
260
267
  amp: ["Bash"],
261
- pi: ["bash"],
268
+ pi: ["bash", "powershell"],
262
269
  antigravity: ["run_command"],
270
+ cursor: ["Shell"],
263
271
  };
264
272
  function parseEnvList(raw) {
265
273
  if (!raw)
@@ -346,16 +354,39 @@ const SHELL_COMMAND_FIELDS = [
346
354
  "commandLine",
347
355
  "CommandLine",
348
356
  ];
357
+ const UNRESOLVED_SHELL_COMMAND = "$ORY_UNRESOLVED_SHELL_COMMAND";
358
+ function extractClineRunCommands(args) {
359
+ if (!args || typeof args !== "object")
360
+ return [UNRESOLVED_SHELL_COMMAND];
361
+ const value = args.commands;
362
+ if (typeof value !== "string")
363
+ return [UNRESOLVED_SHELL_COMMAND];
364
+ try {
365
+ const commands = JSON.parse(value);
366
+ if (!Array.isArray(commands) || !commands.every((command) => typeof command === "string")) {
367
+ return [UNRESOLVED_SHELL_COMMAND];
368
+ }
369
+ return commands.length > 0 ? commands : undefined;
370
+ }
371
+ catch {
372
+ return [UNRESOLVED_SHELL_COMMAND];
373
+ }
374
+ }
349
375
  /**
350
- * Extract the shell command string from a harness's tool-args object — the same
376
+ * Extract shell command strings from a harness's tool-args object — the same
351
377
  * object the handler already passes to `summarizeToolInput`. Returns `undefined`
352
378
  * when the tool isn't a shell tool for this harness or no command field is
353
- * present (the gate then skips decomposition and degrades to the top-level
354
- * `Bash` check — it never breaks).
379
+ * present. Cline's `run_commands` is the exception: malformed command data is
380
+ * represented as an unresolvable command so enforce mode fails closed. Cline's
381
+ * command array remains an array so independently executed entries cannot
382
+ * change how neighboring entries parse.
355
383
  */
356
384
  function extractShellCommand(harness, toolName, args) {
357
385
  if (!isShellTool(harness, toolName))
358
386
  return undefined;
387
+ if (harness === "cline" && toolName === "run_commands") {
388
+ return extractClineRunCommands(args);
389
+ }
359
390
  if (!args || typeof args !== "object")
360
391
  return undefined;
361
392
  const obj = args;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/argus",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "Ory Argus: the core API for building authentication, authorization, and audit into AI agent harness plugins, extensions, and custom integrations",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://ory.com",