@odingard/guardian-hook 0.2.0 → 0.3.1

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/dist/agent-sdk.js CHANGED
@@ -3,6 +3,456 @@ import { createGuardianClient } from "@odingard/guardian-sdk";
3
3
 
4
4
  // src/classify.ts
5
5
  import { createHash } from "node:crypto";
6
+
7
+ // src/shell.ts
8
+ var MAX_NESTING = 16;
9
+ var isBlank = (c) => c === " " || c === " ";
10
+ var OPERATOR_START = /* @__PURE__ */ new Set([";", "&", "|", "\n", "(", ")", "<", ">"]);
11
+ function parseShell(src) {
12
+ const st = {
13
+ src,
14
+ pos: 0,
15
+ ambiguous: false,
16
+ nesting: 0,
17
+ heredocs: [],
18
+ bodySubs: []
19
+ };
20
+ const parsed = parseList(st, false);
21
+ if (st.pos < src.length) st.ambiguous = true;
22
+ if (st.bodySubs.length > 0) {
23
+ parsed.commands.push(bodyCommand(st.bodySubs));
24
+ }
25
+ return {
26
+ commands: parsed.commands,
27
+ ambiguous: st.ambiguous || parsed.ambiguous
28
+ };
29
+ }
30
+ function bodyCommand(subs) {
31
+ return { words: [], writes: [], pipeline: -1, stage: 0, subs };
32
+ }
33
+ function parseList(st, inSub) {
34
+ const commands = [];
35
+ let pipeline = 0;
36
+ let stage = 0;
37
+ let words = [];
38
+ let writes = [];
39
+ let depth = 0;
40
+ let pendingWrite = false;
41
+ let pendingHeredoc = null;
42
+ let pendingHereString = false;
43
+ const endCommand = (nextIsPipe) => {
44
+ if (words.length > 0 || writes.length > 0) {
45
+ commands.push({ words, writes, pipeline, stage, subs: [] });
46
+ }
47
+ words = [];
48
+ writes = [];
49
+ if (nextIsPipe) stage++;
50
+ else {
51
+ pipeline++;
52
+ stage = 0;
53
+ }
54
+ };
55
+ const { src } = st;
56
+ while (st.pos < src.length) {
57
+ const c = src[st.pos];
58
+ if (isBlank(c)) {
59
+ st.pos++;
60
+ continue;
61
+ }
62
+ if (c === "\\" && src[st.pos + 1] === "\n") {
63
+ st.pos += 2;
64
+ continue;
65
+ }
66
+ if (c === "#") {
67
+ while (st.pos < src.length && src[st.pos] !== "\n") st.pos++;
68
+ continue;
69
+ }
70
+ if (c === "\n") {
71
+ st.pos++;
72
+ endCommand(false);
73
+ readHeredocBodies(st);
74
+ continue;
75
+ }
76
+ if (c === ";" || c === "&" || c === "|") {
77
+ const two = src.slice(st.pos, st.pos + 2);
78
+ if (c === "&" && src[st.pos + 1] === ">") {
79
+ st.pos += src[st.pos + 2] === ">" ? 3 : 2;
80
+ pendingWrite = true;
81
+ continue;
82
+ }
83
+ if (two === "&&" || two === "||" || two === ";;" || two === "|&") {
84
+ st.pos += 2;
85
+ endCommand(two === "|&");
86
+ continue;
87
+ }
88
+ st.pos++;
89
+ endCommand(c === "|");
90
+ continue;
91
+ }
92
+ if (c === "(") {
93
+ st.pos++;
94
+ depth++;
95
+ endCommand(false);
96
+ continue;
97
+ }
98
+ if (c === ")") {
99
+ if (depth > 0) {
100
+ st.pos++;
101
+ depth--;
102
+ endCommand(false);
103
+ continue;
104
+ }
105
+ if (inSub) {
106
+ endCommand(false);
107
+ return { commands, ambiguous: false };
108
+ }
109
+ st.pos++;
110
+ st.ambiguous = true;
111
+ endCommand(false);
112
+ continue;
113
+ }
114
+ if (c === "<" || c === ">") {
115
+ if (src[st.pos + 1] === "(") {
116
+ const w2 = readWord(st);
117
+ if (w2) addWord(w2);
118
+ continue;
119
+ }
120
+ const op = /^(<<<|<<-|<<|<>|<&|>>|>&|>\||<|>)/.exec(src.slice(st.pos))[0];
121
+ st.pos += op.length;
122
+ if (op === "<<" || op === "<<-") pendingHeredoc = { stripTabs: op === "<<-" };
123
+ else if (op === "<<<") pendingHereString = true;
124
+ else if (op === ">&" || op === "<&") {
125
+ const m = /^[ \t]*([0-9]+|-)(?=[\s;&|)<>]|$)/.exec(src.slice(st.pos));
126
+ if (m) st.pos += m[0].length;
127
+ else if (op === ">&") pendingWrite = true;
128
+ } else if (op !== "<") pendingWrite = true;
129
+ continue;
130
+ }
131
+ const w = readWord(st);
132
+ if (!w) continue;
133
+ addWord(w);
134
+ }
135
+ if (inSub) st.ambiguous = true;
136
+ if (depth > 0) st.ambiguous = true;
137
+ endCommand(false);
138
+ return { commands, ambiguous: false };
139
+ function addWord(w) {
140
+ if (pendingHeredoc) {
141
+ st.heredocs.push({
142
+ delimiter: w.value,
143
+ stripTabs: pendingHeredoc.stripTabs,
144
+ quoted: w.quoted
145
+ });
146
+ pendingHeredoc = null;
147
+ return;
148
+ }
149
+ if (pendingHereString) {
150
+ pendingHereString = false;
151
+ collectSubs(w);
152
+ return;
153
+ }
154
+ if (pendingWrite) {
155
+ pendingWrite = false;
156
+ writes.push(w);
157
+ return;
158
+ }
159
+ const next = src[st.pos];
160
+ if (/^[0-9]+$/.test(w.value) && !w.quoted && (next === ">" || next === "<")) {
161
+ return;
162
+ }
163
+ words.push(w);
164
+ }
165
+ function collectSubs(w) {
166
+ if (w.subs.length > 0) {
167
+ commands.push({ words: [], writes: [], pipeline: -1, stage: 0, subs: w.subs });
168
+ }
169
+ }
170
+ }
171
+ function readHeredocBodies(st) {
172
+ while (st.heredocs.length > 0) {
173
+ const doc = st.heredocs.shift();
174
+ const lines = [];
175
+ let found = false;
176
+ while (st.pos < st.src.length) {
177
+ let end = st.src.indexOf("\n", st.pos);
178
+ if (end < 0) end = st.src.length;
179
+ const raw = st.src.slice(st.pos, end);
180
+ st.pos = Math.min(end + 1, st.src.length);
181
+ const line = doc.stripTabs ? raw.replace(/^\t+/, "") : raw;
182
+ if (line === doc.delimiter) {
183
+ found = true;
184
+ break;
185
+ }
186
+ lines.push(raw);
187
+ }
188
+ void found;
189
+ if (!doc.quoted) {
190
+ const body = lines.join("\n");
191
+ if (/\$\(|`/.test(body)) {
192
+ const inner = { ...st, src: body, pos: 0, heredocs: [], bodySubs: [] };
193
+ const w = readDoubleQuotedBody(inner);
194
+ st.ambiguous ||= inner.ambiguous;
195
+ if (w.subs.length > 0) st.bodySubs.push(...w.subs);
196
+ }
197
+ }
198
+ }
199
+ }
200
+ function readDoubleQuotedBody(st) {
201
+ const word = { value: "", dynamic: false, subs: [] };
202
+ while (st.pos < st.src.length) {
203
+ const c = st.src[st.pos];
204
+ if (c === "\\") {
205
+ st.pos += 2;
206
+ continue;
207
+ }
208
+ if (c === "$" || c === "`") {
209
+ readExpansion(st, word);
210
+ continue;
211
+ }
212
+ st.pos++;
213
+ }
214
+ return word;
215
+ }
216
+ function readWord(st) {
217
+ const { src } = st;
218
+ const word = {
219
+ value: "",
220
+ dynamic: false,
221
+ subs: [],
222
+ quoted: false
223
+ };
224
+ const start = st.pos;
225
+ while (st.pos < src.length) {
226
+ const c = src[st.pos];
227
+ if (isBlank(c) || c === "\n") break;
228
+ if ((c === "<" || c === ">") && src[st.pos + 1] === "(") {
229
+ st.pos += 2;
230
+ word.dynamic = true;
231
+ word.subs.push(parseNested(st));
232
+ continue;
233
+ }
234
+ if (OPERATOR_START.has(c)) break;
235
+ if (c === "\\") {
236
+ if (st.pos + 1 < src.length) {
237
+ if (src[st.pos + 1] !== "\n") word.value += src[st.pos + 1];
238
+ st.pos += 2;
239
+ } else {
240
+ st.pos++;
241
+ }
242
+ word.quoted = true;
243
+ continue;
244
+ }
245
+ if (c === "'") {
246
+ const end = src.indexOf("'", st.pos + 1);
247
+ if (end < 0) {
248
+ st.ambiguous = true;
249
+ word.value += src.slice(st.pos + 1);
250
+ st.pos = src.length;
251
+ break;
252
+ }
253
+ word.value += src.slice(st.pos + 1, end);
254
+ st.pos = end + 1;
255
+ word.quoted = true;
256
+ continue;
257
+ }
258
+ if (c === '"') {
259
+ st.pos++;
260
+ word.quoted = true;
261
+ let closed = false;
262
+ while (st.pos < src.length) {
263
+ const d = src[st.pos];
264
+ if (d === '"') {
265
+ st.pos++;
266
+ closed = true;
267
+ break;
268
+ }
269
+ if (d === "\\") {
270
+ const n = src[st.pos + 1];
271
+ if (n === void 0) {
272
+ st.pos++;
273
+ continue;
274
+ }
275
+ if ('$`"\\\n'.includes(n)) {
276
+ if (n !== "\n") word.value += n;
277
+ } else {
278
+ word.value += d + n;
279
+ }
280
+ st.pos += 2;
281
+ continue;
282
+ }
283
+ if (d === "$" || d === "`") {
284
+ readExpansion(st, word);
285
+ continue;
286
+ }
287
+ word.value += d;
288
+ st.pos++;
289
+ }
290
+ if (!closed) st.ambiguous = true;
291
+ continue;
292
+ }
293
+ if (c === "$" && src[st.pos + 1] === "'") {
294
+ let i = st.pos + 2;
295
+ let out = "";
296
+ let closed = false;
297
+ while (i < src.length) {
298
+ const d = src[i];
299
+ if (d === "'") {
300
+ closed = true;
301
+ i++;
302
+ break;
303
+ }
304
+ if (d === "\\" && i + 1 < src.length) {
305
+ const n = src[i + 1];
306
+ const simple = { n: "\n", t: " ", "\\": "\\", "'": "'", '"': '"' };
307
+ if (simple[n] !== void 0) {
308
+ out += simple[n];
309
+ i += 2;
310
+ continue;
311
+ }
312
+ const hex = /^x([0-9a-fA-F]{1,2})/.exec(src.slice(i + 1));
313
+ if (hex) {
314
+ out += String.fromCharCode(parseInt(hex[1], 16));
315
+ i += 1 + hex[0].length;
316
+ continue;
317
+ }
318
+ const oct = /^([0-7]{1,3})/.exec(src.slice(i + 1));
319
+ if (oct) {
320
+ out += String.fromCharCode(parseInt(oct[1], 8));
321
+ i += 1 + oct[0].length;
322
+ continue;
323
+ }
324
+ st.ambiguous = true;
325
+ out += n;
326
+ i += 2;
327
+ continue;
328
+ }
329
+ out += d;
330
+ i++;
331
+ }
332
+ if (!closed) st.ambiguous = true;
333
+ word.value += out;
334
+ word.quoted = true;
335
+ st.pos = i;
336
+ continue;
337
+ }
338
+ if (c === "$" || c === "`") {
339
+ readExpansion(st, word);
340
+ continue;
341
+ }
342
+ word.value += c;
343
+ st.pos++;
344
+ }
345
+ return st.pos > start ? word : null;
346
+ }
347
+ function readExpansion(st, word) {
348
+ const { src } = st;
349
+ const c = src[st.pos];
350
+ if (c === "`") {
351
+ let i = st.pos + 1;
352
+ let inner = "";
353
+ while (i < src.length && src[i] !== "`") {
354
+ if (src[i] === "\\" && i + 1 < src.length) {
355
+ inner += src[i + 1];
356
+ i += 2;
357
+ continue;
358
+ }
359
+ inner += src[i];
360
+ i++;
361
+ }
362
+ if (i >= src.length) st.ambiguous = true;
363
+ word.value += "`" + inner + "`";
364
+ word.dynamic = true;
365
+ word.subs.push(parseString(st, inner));
366
+ st.pos = Math.min(i + 1, src.length);
367
+ return;
368
+ }
369
+ const n = src[st.pos + 1];
370
+ if (n === "(" && src[st.pos + 2] === "(") {
371
+ let i = st.pos + 3;
372
+ let level = 2;
373
+ while (i < src.length && level > 0) {
374
+ if (src[i] === "(") level++;
375
+ else if (src[i] === ")") level--;
376
+ i++;
377
+ }
378
+ if (level > 0) st.ambiguous = true;
379
+ word.value += src.slice(st.pos, i);
380
+ word.dynamic = true;
381
+ st.pos = i;
382
+ return;
383
+ }
384
+ if (n === "(") {
385
+ const start = st.pos;
386
+ st.pos += 2;
387
+ word.subs.push(parseNested(st));
388
+ word.value += src.slice(start, st.pos);
389
+ word.dynamic = true;
390
+ return;
391
+ }
392
+ if (n === "{") {
393
+ let i = st.pos + 2;
394
+ let level = 1;
395
+ while (i < src.length && level > 0) {
396
+ if (src[i] === "{") level++;
397
+ else if (src[i] === "}") level--;
398
+ i++;
399
+ }
400
+ if (level > 0) st.ambiguous = true;
401
+ const text = src.slice(st.pos, i);
402
+ word.value += text;
403
+ word.dynamic = true;
404
+ if (/\$\(|`/.test(text)) {
405
+ word.subs.push(parseString(st, text.slice(2, -1)));
406
+ }
407
+ st.pos = i;
408
+ return;
409
+ }
410
+ const name = /^\$([A-Za-z_][A-Za-z0-9_]*|[0-9@*#?$!-])/.exec(src.slice(st.pos));
411
+ if (name) {
412
+ word.value += name[0];
413
+ word.dynamic = true;
414
+ st.pos += name[0].length;
415
+ return;
416
+ }
417
+ word.value += "$";
418
+ st.pos++;
419
+ }
420
+ function parseNested(st) {
421
+ if (st.nesting >= MAX_NESTING) {
422
+ st.ambiguous = true;
423
+ st.pos = st.src.length;
424
+ return { commands: [], ambiguous: true };
425
+ }
426
+ st.nesting++;
427
+ const savedHeredocs = st.heredocs;
428
+ st.heredocs = [];
429
+ const inner = parseList(st, true);
430
+ st.heredocs = savedHeredocs;
431
+ st.nesting--;
432
+ if (st.src[st.pos] === ")") st.pos++;
433
+ else st.ambiguous = true;
434
+ return inner;
435
+ }
436
+ function parseString(st, text) {
437
+ if (st.nesting >= MAX_NESTING) {
438
+ st.ambiguous = true;
439
+ return { commands: [], ambiguous: true };
440
+ }
441
+ const inner = {
442
+ src: text,
443
+ pos: 0,
444
+ ambiguous: false,
445
+ nesting: st.nesting + 1,
446
+ heredocs: [],
447
+ bodySubs: []
448
+ };
449
+ const parsed = parseList(inner, false);
450
+ if (inner.bodySubs.length) parsed.commands.push(bodyCommand(inner.bodySubs));
451
+ st.ambiguous ||= inner.ambiguous;
452
+ return { commands: parsed.commands, ambiguous: inner.ambiguous };
453
+ }
454
+
455
+ // src/classify.ts
6
456
  var HOOK_TOOLS = [
7
457
  {
8
458
  name: "shell",
@@ -17,13 +467,18 @@ var HOOK_TOOLS = [
17
467
  {
18
468
  name: "shell_destructive",
19
469
  suggestedMode: "ask",
20
- description: "Destructive commands: rm -rf, git reset --hard, git clean, force pushes, dropping databases, disk formatting."
470
+ description: "Destructive commands: rm -rf (also through xargs or find -exec), git reset --hard, git clean, force pushes and remote branch deletes, dropping databases, disk formatting, piping a download into a shell. Also changes to the agent's own MCP servers, hooks or permissions (claude mcp add, writes to .mcp.json or .claude/settings.json), and commands the hook cannot parse."
21
471
  },
22
472
  {
23
473
  name: "git_push",
24
474
  suggestedMode: "ask",
25
475
  description: "Pushing commits to a remote."
26
476
  },
477
+ {
478
+ name: "shell_session",
479
+ suggestedMode: "ask",
480
+ description: "Starting an interactive shell, interpreter or remote session (bash, python, ssh, docker exec -it). What is typed into it later is not seen by Guardian."
481
+ },
27
482
  {
28
483
  name: "file_write",
29
484
  suggestedMode: "allow",
@@ -63,15 +518,697 @@ var DESTRUCTIVE = [
63
518
  /\b(drop|truncate)\s+(table|database|schema)\b/i,
64
519
  /\bdelete\s+from\s+\w+\s*(;|$)/i,
65
520
  /\b(mkfs|dd\s+if=|shred|wipefs)\b/,
521
+ /\bdd\b.*\bof=/,
522
+ /\brsync\b.*\s--(delete\S*|remove-source-files)\b/,
523
+ /\btruncate\b.*(\s-s\s*0\b|\s--size[= ]0\b)/,
66
524
  /\bchmod\s+-R\s+777\b/,
67
525
  /\bfind\b.*\s-delete\b/
68
526
  ];
69
527
  var GIT_PUSH = /\bgit\s+push\b/;
528
+ var INTERPRETERS = new Set(
529
+ "sh bash zsh dash ksh mksh csh tcsh fish ash pwsh powershell python python2 python3 ipython node nodejs deno bun ruby irb perl php lua luajit r julia ghci scala groovysh jshell tclsh wish expect su login tmux screen script".split(" ")
530
+ );
531
+ var DB_CLIENTS = new Set(
532
+ "psql mysql mariadb mongo mongosh cqlsh sqlplus clickhouse-client".split(" ")
533
+ );
534
+ var DB_INLINE = /^(-c|-e|-f|--command|--execute|--file|--eval|--query)(=|$)/;
535
+ var NET_PIPES = new Set("telnet nc ncat netcat socat openssl".split(" "));
536
+ var SSH_VALUE_OPTS = new Set(
537
+ "-B -b -c -D -E -e -F -I -i -J -L -l -m -O -o -p -Q -R -S -W -w".split(" ")
538
+ );
539
+ var SUDO_VALUE_OPTS = new Set("-u -g -U -C -D -h -p -r -t -T".split(" "));
540
+ var REDIS_VALUE_OPTS = new Set("-h -p -a -n -u --user --pass".split(" "));
541
+ var WRAPPERS = new Set(
542
+ "exec command builtin nohup time env stdbuf nice ionice".split(" ")
543
+ );
544
+ function positionals(args, valueOpts) {
545
+ const out = [];
546
+ for (let i = 0; i < args.length; i++) {
547
+ const a = args[i];
548
+ if (valueOpts.has(a)) i++;
549
+ else if (!a.startsWith("-")) out.push(a);
550
+ }
551
+ return out;
552
+ }
553
+ function startsSession(segment) {
554
+ const words = segment.trim().split(/\s+/).filter(Boolean);
555
+ let wrapped = false;
556
+ while (words.length > 0) {
557
+ const w = words[0];
558
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(w) || WRAPPERS.has(w) || wrapped && w.startsWith("-")) {
559
+ wrapped ||= WRAPPERS.has(w);
560
+ words.shift();
561
+ continue;
562
+ }
563
+ break;
564
+ }
565
+ if (words.length === 0) return false;
566
+ const base = (words[0].split(/[\\/]/).pop() ?? "").replace(/^['"]+|['"]+$/g, "").toLowerCase().replace(/\.exe$/, "");
567
+ const args = words.slice(1);
568
+ if (args.some((a) => /^(--version|-V|--help)$/.test(a))) {
569
+ return false;
570
+ }
571
+ if (base === "sudo" || base === "doas" || base === "busybox") {
572
+ if (base !== "busybox" && args.some((a) => /^-[a-zA-Z]*[is]$/.test(a))) {
573
+ return true;
574
+ }
575
+ const rest = positionals(args, SUDO_VALUE_OPTS);
576
+ if (rest.length === 0) return false;
577
+ return startsSession(args.slice(args.indexOf(rest[0])).join(" "));
578
+ }
579
+ if (base === "ssh" || base === "mosh") {
580
+ if (args.some((a) => /^-[a-zA-Z]*t/.test(a))) return true;
581
+ return positionals(args, SSH_VALUE_OPTS).length <= 1;
582
+ }
583
+ if (["docker", "podman", "nerdctl", "kubectl", "oc"].includes(base)) {
584
+ if (!args.some((a) => ["exec", "run", "attach", "debug"].includes(a))) {
585
+ return false;
586
+ }
587
+ return args.includes("attach") || args.some(
588
+ (a) => /^(-[a-zA-Z]*i[a-zA-Z]*|--interactive|--stdin)(=true)?$/.test(a)
589
+ );
590
+ }
591
+ if (DB_CLIENTS.has(base)) {
592
+ return !args.some((a) => DB_INLINE.test(a)) && !segment.includes("<");
593
+ }
594
+ if (base === "sqlite3") {
595
+ return positionals(args, /* @__PURE__ */ new Set(["-cmd"])).length <= 1;
596
+ }
597
+ if (base === "redis-cli") {
598
+ return positionals(args, REDIS_VALUE_OPTS).length === 0;
599
+ }
600
+ if (NET_PIPES.has(base)) {
601
+ return base === "openssl" ? args[0] === "s_client" : !args.some((a) => /^-[a-zA-Z]*z/.test(a));
602
+ }
603
+ if (!INTERPRETERS.has(base) && !/^python[0-9.]+$/.test(base)) return false;
604
+ if ((base === "deno" || base === "bun") && args[0] === "repl") return true;
605
+ const code = args.findIndex((a) => /^-[a-zA-Z]*c[a-zA-Z]*$/.test(a));
606
+ if (code >= 0 && /sh$/.test(base)) {
607
+ const inner = args.slice(code + 1).join(" ").replace(/['"]/g, " ");
608
+ if (startsInteractiveSession(inner)) return true;
609
+ }
610
+ if (args.some((a) => /^(-[a-zA-Z]*[is][a-zA-Z]*|--interactive|-)$/.test(a))) {
611
+ return true;
612
+ }
613
+ return args.every((a) => a.startsWith("-") && !/^-[a-zA-Z]*[ce]/.test(a));
614
+ }
615
+ function startsInteractiveSession(command) {
616
+ if (/\bwhile\s+(?:IFS=\S*\s+)?read\b/.test(command)) return true;
617
+ return command.split(/&&|\|\||[;&|\n()]|\$\(|`/).some((segment) => startsSession(segment));
618
+ }
619
+ var RANK = {
620
+ shell: 0,
621
+ shell_session: 1,
622
+ git_push: 2,
623
+ shell_deploy: 3,
624
+ shell_destructive: 4
625
+ };
626
+ var BY_RANK = [
627
+ "shell",
628
+ "shell_session",
629
+ "git_push",
630
+ "shell_deploy",
631
+ "shell_destructive"
632
+ ];
633
+ var ASK = RANK.shell_destructive;
634
+ var MAX_DEPTH = 12;
635
+ var EDGE = String.raw`(?:^|[\s/\\'"=:,(~])`;
636
+ var AGENT_CONFIG_PATH = new RegExp(
637
+ [
638
+ EDGE + String.raw`(?:\.?mcp\.json|\.claude\.json|settings\.local\.json|managed-settings\.json|managed-mcp\.json|claude_desktop_config\.json|guardian-hooks\.json|cordis\.patch\.ya?ml)(?![\w-])`,
639
+ EDGE + String.raw`\.claude(?:[/\\](?:settings[^/\\\s'"]*\.json|hooks|agents|commands|skills|plugins|output-styles)(?![\w-])|[/\\]?(?=$|[\s'"]))`,
640
+ EDGE + String.raw`\.(?:codex|cursor|gemini|dsh)(?=$|[/\\\s'"])`
641
+ ].join("|"),
642
+ "i"
643
+ );
644
+ var READERS = new Set(
645
+ "cat tac less more head tail grep egrep fgrep zgrep rg ag ack ls dir stat file wc diff cmp comm jq bat batcat md5sum sha1sum sha256sum sha512sum shasum b2sum cksum echo printf test [ [[ find fd realpath readlink dirname basename du tree type which".split(" ")
646
+ );
647
+ var GIT_READS = new Set(
648
+ "status log show diff blame ls-files grep add commit check-ignore cat-file rev-parse ls-tree shortlog describe".split(" ")
649
+ );
650
+ var RESERVED = new Set(
651
+ "if then else elif fi do done while until for in case esac ! { } select function".split(
652
+ " "
653
+ )
654
+ );
655
+ var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*(\[[^\]]*\])?\+?=/;
656
+ var SHELLS = new Set(
657
+ "sh bash zsh dash ksh mksh ash yash posh fish busybox".split(" ")
658
+ );
659
+ var FETCHERS = new Set(
660
+ "curl wget fetch http https xh aria2c iwr irm invoke-webrequest invoke-restmethod".split(
661
+ " "
662
+ )
663
+ );
664
+ var BULK_DELETE = /* @__PURE__ */ new Set(["rm", "unlink", "shred"]);
665
+ var EVALUATORS = /* @__PURE__ */ new Set(["eval", "source", ".", "iex", "invoke-expression"]);
666
+ var WRAPPER_SPECS = {
667
+ nohup: {},
668
+ command: {},
669
+ builtin: {},
670
+ chronic: {},
671
+ unbuffer: {},
672
+ busybox: {},
673
+ time: { value: ["-f", "-o", "--format", "--output"] },
674
+ nice: { value: ["-n", "--adjustment"] },
675
+ ionice: { value: ["-c", "-n", "-p", "-P", "-u", "--class", "--classdata"] },
676
+ stdbuf: { value: ["-i", "-o", "-e", "--input", "--output", "--error"] },
677
+ timeout: { value: ["-s", "-k", "--signal", "--kill-after"], positional: 1 },
678
+ exec: { value: ["-a"] },
679
+ sudo: {
680
+ value: [
681
+ ...SUDO_VALUE_OPTS,
682
+ "--user",
683
+ "--group",
684
+ "--host",
685
+ "--prompt",
686
+ "--chdir",
687
+ "--role",
688
+ "--type",
689
+ "--close-from",
690
+ "--other-user",
691
+ "--command-timeout"
692
+ ]
693
+ },
694
+ doas: { value: ["-u", "-C"] },
695
+ xargs: {
696
+ value: [
697
+ "-a",
698
+ "--arg-file",
699
+ "-d",
700
+ "--delimiter",
701
+ "-E",
702
+ "-I",
703
+ "-L",
704
+ "-n",
705
+ "--max-args",
706
+ "-P",
707
+ "--max-procs",
708
+ "-s",
709
+ "--max-chars",
710
+ "--process-slot-var",
711
+ "--max-lines"
712
+ ]
713
+ },
714
+ chroot: { value: ["--userspec", "--groups"], positional: 1 },
715
+ taskset: { positional: 1 },
716
+ flock: {
717
+ value: ["-w", "-E", "--timeout", "--conflict-exit-code"],
718
+ positional: 1
719
+ },
720
+ setsid: {},
721
+ caffeinate: { value: ["-t", "-w"] }
722
+ };
723
+ var leaf = (rank, base = "", args = []) => ({
724
+ rank,
725
+ base,
726
+ args
727
+ });
728
+ function baseName(value) {
729
+ return (value.split(/[\\/]/).pop() ?? "").toLowerCase().replace(/\.exe$/, "");
730
+ }
731
+ var literal = (value) => ({ value, dynamic: false, subs: [] });
732
+ function skipOptions(args, spec) {
733
+ const value = new Set(spec.value ?? []);
734
+ let i = 0;
735
+ let unknown = false;
736
+ while (i < args.length) {
737
+ const a = args[i].value;
738
+ if (a === "--") {
739
+ i++;
740
+ break;
741
+ }
742
+ if (a.startsWith("--")) {
743
+ if (a.includes("=")) i++;
744
+ else if (value.has(a)) i += 2;
745
+ else {
746
+ unknown = true;
747
+ i++;
748
+ }
749
+ continue;
750
+ }
751
+ if (a.startsWith("-") && a.length > 1) {
752
+ let takes = false;
753
+ for (let k = 1; k < a.length; k++) {
754
+ if (value.has(`-${a[k]}`)) {
755
+ takes = k === a.length - 1;
756
+ break;
757
+ }
758
+ }
759
+ i += takes ? 2 : 1;
760
+ continue;
761
+ }
762
+ break;
763
+ }
764
+ let positional = spec.positional ?? 0;
765
+ while (positional-- > 0 && i < args.length) i++;
766
+ return { start: i, unknown };
767
+ }
768
+ function rankText(text, depth) {
769
+ if (depth > MAX_DEPTH) return ASK;
770
+ return rankParsed(parseShell(text), depth + 1);
771
+ }
772
+ function rankParsed(parsed, depth) {
773
+ if (depth > MAX_DEPTH) return ASK;
774
+ let rank = parsed.ambiguous ? ASK : 0;
775
+ const stages = /* @__PURE__ */ new Map();
776
+ for (const cmd of parsed.commands) {
777
+ const l = rankArgv(cmd.words, depth);
778
+ rank = Math.max(rank, l.rank);
779
+ if (cmd.writes.some((w) => AGENT_CONFIG_PATH.test(w.value))) rank = ASK;
780
+ const subs = [
781
+ ...cmd.subs,
782
+ ...cmd.words.flatMap((w) => w.subs),
783
+ ...cmd.writes.flatMap((w) => w.subs)
784
+ ];
785
+ for (const sub of subs) rank = Math.max(rank, rankParsed(sub, depth + 1));
786
+ if (runsCode(l) && cmd.words.some((w) => w.subs.some((s) => fetches(s, depth + 1)))) {
787
+ rank = ASK;
788
+ }
789
+ if (cmd.pipeline >= 0) {
790
+ const list = stages.get(cmd.pipeline) ?? [];
791
+ list.push(l);
792
+ stages.set(cmd.pipeline, list);
793
+ }
794
+ }
795
+ for (const list of stages.values()) {
796
+ const first = list.findIndex((l) => FETCHERS.has(l.base));
797
+ if (first >= 0 && list.slice(first + 1).some((l) => readsCodeFromStdin(l))) {
798
+ rank = ASK;
799
+ }
800
+ }
801
+ return rank;
802
+ }
803
+ function fetches(parsed, depth) {
804
+ return parsed.commands.some(
805
+ (c) => FETCHERS.has(rankArgv(c.words, depth).base) || [...c.subs, ...c.words.flatMap((w) => w.subs)].some(
806
+ (s) => fetches(s, depth + 1)
807
+ )
808
+ );
809
+ }
810
+ var isInterpreter = (base) => INTERPRETERS.has(base) || /^python[0-9.]*$/.test(base);
811
+ function runsCode(l) {
812
+ return EVALUATORS.has(l.base) || isInterpreter(l.base);
813
+ }
814
+ function readsCodeFromStdin(l) {
815
+ if (EVALUATORS.has(l.base)) return true;
816
+ if (!isInterpreter(l.base)) return false;
817
+ const options = [];
818
+ let script = false;
819
+ for (const a of l.args) {
820
+ if (a === "--") break;
821
+ if (a === "-") return true;
822
+ if (a.startsWith("-") || a.startsWith("+")) options.push(a);
823
+ else {
824
+ script = true;
825
+ break;
826
+ }
827
+ }
828
+ if (options.some((a) => /^-[a-zA-Z]*s[a-zA-Z]*$/.test(a))) return true;
829
+ if (options.some((a) => /^-[a-zA-Z]*[cem][a-zA-Z]*$/.test(a))) return false;
830
+ return !script;
831
+ }
832
+ function rankArgv(words, depth) {
833
+ if (depth > MAX_DEPTH) return leaf(ASK);
834
+ let i = 0;
835
+ while (i < words.length && (ASSIGNMENT.test(words[i].value) || RESERVED.has(words[i].value))) {
836
+ i++;
837
+ }
838
+ if (i >= words.length) return leaf(0);
839
+ const head = words[i];
840
+ const argWords = words.slice(i + 1);
841
+ if (head.dynamic) return leaf(ASK);
842
+ const base = baseName(head.value);
843
+ const args = argWords.map((w) => w.value);
844
+ const joined = [base, ...args].join(" ");
845
+ let rank = 0;
846
+ if (DESTRUCTIVE.some((re) => re.test(joined))) rank = ASK;
847
+ else if (DEPLOY.some((re) => re.test(joined))) rank = RANK.shell_deploy;
848
+ const done = (r, b = base, a = args) => leaf(Math.max(rank, r), b, a);
849
+ const inner = (from) => rankArgv(from, depth + 1);
850
+ const pathCheck = (values) => values.some((v) => AGENT_CONFIG_PATH.test(v)) ? ASK : 0;
851
+ if (base === "env") return done(rankEnv(argWords, depth));
852
+ if (base === "sudoedit") return done(pathCheck(args));
853
+ if (Object.hasOwn(WRAPPER_SPECS, base)) {
854
+ if (base === "sudo" && args.some((a) => /^-[a-zA-Z]*e[a-zA-Z]*$/.test(a))) {
855
+ return done(pathCheck(args));
856
+ }
857
+ if (base === "command" && args.some((a) => /^-[a-zA-Z]*[vV]/.test(a))) {
858
+ return done(0);
859
+ }
860
+ if (base === "flock") {
861
+ const c = args.findIndex((a) => a === "-c" || a === "--command");
862
+ if (c >= 0) return done(rankText(args[c + 1] ?? "", depth));
863
+ }
864
+ const { start, unknown } = skipOptions(argWords, WRAPPER_SPECS[base]);
865
+ let rest = argWords.slice(start);
866
+ if (base === "xargs" && rest.length === 0) rest = [literal("echo")];
867
+ const l = inner(rest);
868
+ if (base === "xargs" && BULK_DELETE.has(l.base)) l.rank = ASK;
869
+ const alt = unknown ? inner(argWords.slice(start + 1)).rank : 0;
870
+ return leaf(Math.max(rank, l.rank, alt), l.base, l.args);
871
+ }
872
+ if (base === "watch") {
873
+ const { start } = skipOptions(argWords, {
874
+ value: ["-n", "--interval", "-d", "-q", "--equexit"]
875
+ });
876
+ return done(rankText(args.slice(start).join(" "), depth));
877
+ }
878
+ if (SHELLS.has(base)) {
879
+ return done(rankShell(base, argWords, depth));
880
+ }
881
+ if (base === "su") {
882
+ const c = args.findIndex((a) => /^(-c|--command)$/.test(a));
883
+ const eq = args.find((a) => a.startsWith("--command="));
884
+ if (c >= 0) return done(rankText(args[c + 1] ?? "", depth));
885
+ if (eq) return done(rankText(eq.slice("--command=".length), depth));
886
+ return done(0);
887
+ }
888
+ if (base === "eval" || base === "iex" || base === "invoke-expression") {
889
+ return done(rankText(args.join(" "), depth));
890
+ }
891
+ if (base === "find") return done(rankFind(argWords, depth));
892
+ if (["npx", "bunx", "pnpx", "npm", "pnpm", "yarn"].includes(base)) {
893
+ const l = rankRunner(base, argWords, depth);
894
+ if (l) return leaf(Math.max(rank, l.rank), l.base, l.args);
895
+ return done(0);
896
+ }
897
+ if (base === "rm") {
898
+ const recursive = argsBeforeDashDash(args).some(
899
+ (a) => a === "--recursive" || /^-[a-zA-Z]*[rR]/.test(a)
900
+ );
901
+ return done(Math.max(recursive ? ASK : 0, pathCheck(args)));
902
+ }
903
+ if (base === "chmod") return done(Math.max(rankChmod(args), pathCheck(args)));
904
+ if (base === "git") return done(rankGit(argWords, depth));
905
+ if (base === "claude" || base === "codex" || base === "gemini") {
906
+ return done(rankAgentCli(base, args));
907
+ }
908
+ if (READERS.has(base)) return done(0);
909
+ return done(pathCheck(args));
910
+ }
911
+ function argsBeforeDashDash(args) {
912
+ const end = args.indexOf("--");
913
+ return end < 0 ? args : args.slice(0, end);
914
+ }
915
+ function rankEnv(argWords, depth) {
916
+ let i = 0;
917
+ while (i < argWords.length) {
918
+ const a = argWords[i].value;
919
+ if (a === "--") {
920
+ i++;
921
+ break;
922
+ }
923
+ if (a === "-S" || a === "--split-string") {
924
+ const text = [a, ...argWords.slice(i + 1).map((w) => w.value)].slice(1).join(" ");
925
+ return rankText(text, depth);
926
+ }
927
+ if (a.startsWith("--split-string=") || /^-S./.test(a)) {
928
+ const first = a.startsWith("--") ? a.slice(15) : a.slice(2);
929
+ return rankText(
930
+ [first, ...argWords.slice(i + 1).map((w) => w.value)].join(" "),
931
+ depth
932
+ );
933
+ }
934
+ if (/^(-u|-C|-P|--unset|--chdir)$/.test(a)) {
935
+ i += 2;
936
+ continue;
937
+ }
938
+ if (a.startsWith("-") && a.length > 1) {
939
+ i++;
940
+ continue;
941
+ }
942
+ break;
943
+ }
944
+ return rankArgv(argWords.slice(i), depth + 1).rank;
945
+ }
946
+ function rankShell(base, argWords, depth) {
947
+ let code = false;
948
+ for (let i = 0; i < argWords.length; i++) {
949
+ const a = argWords[i].value;
950
+ if (a === "--") break;
951
+ if (/^(--rcfile|--init-file|-o|\+o|-O|\+O)$/.test(a)) {
952
+ i++;
953
+ continue;
954
+ }
955
+ if (a === "--command" || base === "fish" && a === "-c") {
956
+ return rankText(argWords[i + 1]?.value ?? "", depth);
957
+ }
958
+ if (/^[-+][a-zA-Z]+$/.test(a)) {
959
+ if (/^-[a-zA-Z]*c/.test(a)) code = true;
960
+ continue;
961
+ }
962
+ if (a.startsWith("-")) continue;
963
+ if (code) {
964
+ if (base === "busybox")
965
+ return rankArgv(argWords.slice(i), depth + 1).rank;
966
+ return rankText(a, depth);
967
+ }
968
+ if (base === "busybox") return rankArgv(argWords.slice(i), depth + 1).rank;
969
+ return 0;
970
+ }
971
+ return 0;
972
+ }
973
+ function rankFind(argWords, depth) {
974
+ let rank = 0;
975
+ for (let i = 0; i < argWords.length; i++) {
976
+ const a = argWords[i].value;
977
+ if (a === "-delete") rank = ASK;
978
+ if (/^-(exec|execdir|ok|okdir)$/.test(a)) {
979
+ let end = i + 1;
980
+ while (end < argWords.length && argWords[end].value !== ";" && argWords[end].value !== "+") {
981
+ end++;
982
+ }
983
+ const l = rankArgv(argWords.slice(i + 1, end), depth + 1);
984
+ rank = Math.max(rank, BULK_DELETE.has(l.base) ? ASK : l.rank);
985
+ i = end;
986
+ }
987
+ if (/^-f(print|print0|printf|ls)$/.test(a)) {
988
+ if (AGENT_CONFIG_PATH.test(argWords[i + 1]?.value ?? "")) rank = ASK;
989
+ }
990
+ }
991
+ return rank;
992
+ }
993
+ function rankRunner(base, argWords, depth) {
994
+ let rest = argWords;
995
+ if (base === "npm" || base === "pnpm" || base === "yarn") {
996
+ const sub = rest[0]?.value;
997
+ if (!["exec", "x", "dlx"].includes(sub ?? "")) return null;
998
+ rest = rest.slice(1);
999
+ }
1000
+ let i = 0;
1001
+ while (i < rest.length) {
1002
+ const a = rest[i].value;
1003
+ if (a === "--") {
1004
+ i++;
1005
+ break;
1006
+ }
1007
+ if (a === "-c" || a === "--call")
1008
+ return leaf(rankText(rest[i + 1]?.value ?? "", depth));
1009
+ if (a.startsWith("--call=")) return leaf(rankText(a.slice(7), depth));
1010
+ if (a === "-p" || a === "--package") {
1011
+ i += 2;
1012
+ continue;
1013
+ }
1014
+ if (a.startsWith("-")) {
1015
+ i++;
1016
+ continue;
1017
+ }
1018
+ break;
1019
+ }
1020
+ const pkg = rest[i];
1021
+ if (!pkg) return null;
1022
+ if (pkg.dynamic) return leaf(ASK);
1023
+ const name = /^(?:@[^/]+\/)?([^@]+)(?:@.*)?$/.exec(pkg.value)?.[1] ?? pkg.value;
1024
+ const bin = name === "claude-code" ? "claude" : name;
1025
+ return rankArgv([literal(bin), ...rest.slice(i + 1)], depth + 1);
1026
+ }
1027
+ function rankChmod(args) {
1028
+ const opts = argsBeforeDashDash(args);
1029
+ const recursive = opts.some(
1030
+ (a) => a === "--recursive" || /^-[a-zA-Z]*R/.test(a)
1031
+ );
1032
+ if (!recursive) return 0;
1033
+ const modes = args.filter(
1034
+ (a) => !a.startsWith("-") || /^-[rwxXst]+$/.test(a)
1035
+ );
1036
+ const worldWritable = modes.some(
1037
+ (m) => /^0*[0-7]?[0-7]?[0-7]?[2367]$/.test(m) && m.length >= 3 || /(^|,)[ugo]*[ao][ugo]*[+=][rwxXst]*w/.test(m)
1038
+ );
1039
+ return worldWritable ? ASK : 0;
1040
+ }
1041
+ var GIT_VALUE_OPTS = /* @__PURE__ */ new Set([
1042
+ "-C",
1043
+ "-c",
1044
+ "--git-dir",
1045
+ "--work-tree",
1046
+ "--namespace",
1047
+ "--super-prefix",
1048
+ "--config-env",
1049
+ "--exec-path",
1050
+ "--list-cmds",
1051
+ "--attr-source"
1052
+ ]);
1053
+ var GIT_EXEC_KEY = /^(core\.(pager|editor|sshcommand|askpass|gitproxy)|pager\..+|sequence\.editor|.+\.(textconv|command|cmd|helper|program|clean|smudge|process|driver|tool)|credential\..*|uploadpack\.packobjectshook|gpg\..*program|.*\.(receivepack|uploadpack))$/i;
1054
+ function rankGit(argWords, depth) {
1055
+ const args = argWords.map((w) => w.value);
1056
+ let rank = 0;
1057
+ const aliases = /* @__PURE__ */ new Map();
1058
+ let i = 0;
1059
+ while (i < args.length) {
1060
+ const a = args[i];
1061
+ if (!a.startsWith("-")) break;
1062
+ let opt = a;
1063
+ let val;
1064
+ if (a.startsWith("--") && a.includes("=")) {
1065
+ opt = a.slice(0, a.indexOf("="));
1066
+ val = a.slice(a.indexOf("=") + 1);
1067
+ i++;
1068
+ } else if (GIT_VALUE_OPTS.has(a)) {
1069
+ val = args[i + 1] ?? "";
1070
+ if (argWords[i + 1]?.dynamic && (a === "-c" || a === "--config-env")) {
1071
+ rank = ASK;
1072
+ }
1073
+ i += 2;
1074
+ } else {
1075
+ i++;
1076
+ }
1077
+ if (opt === "-c" && val !== void 0) {
1078
+ const eq = val.indexOf("=");
1079
+ const key = (eq < 0 ? val : val.slice(0, eq)).toLowerCase();
1080
+ const value = eq < 0 ? "" : val.slice(eq + 1);
1081
+ if (key.startsWith("alias.")) aliases.set(key.slice(6), value);
1082
+ else if (key === "core.hookspath" || key === "core.fsmonitor") {
1083
+ if (!/^(false|0|no|off|)$/i.test(value)) rank = ASK;
1084
+ } else if (GIT_EXEC_KEY.test(key)) {
1085
+ rank = Math.max(rank, rankText(value, depth));
1086
+ }
1087
+ }
1088
+ if (opt === "--config-env" && val !== void 0) {
1089
+ const key = val.split("=")[0].toLowerCase();
1090
+ if (key.startsWith("alias.") || GIT_EXEC_KEY.test(key) || /hookspath|fsmonitor/.test(key)) {
1091
+ rank = ASK;
1092
+ }
1093
+ }
1094
+ }
1095
+ if (i >= args.length) return rank;
1096
+ if (argWords[i].dynamic) return ASK;
1097
+ const sub = args[i];
1098
+ const rest = args.slice(i + 1);
1099
+ const alias = aliases.get(sub.toLowerCase());
1100
+ if (alias !== void 0) {
1101
+ if (alias.startsWith("!")) {
1102
+ return Math.max(
1103
+ rank,
1104
+ rankText(`${alias.slice(1)} ${rest.join(" ")}`, depth)
1105
+ );
1106
+ }
1107
+ const expanded = parseShell(alias);
1108
+ const aliasWords = expanded.commands[0]?.words ?? [];
1109
+ return Math.max(
1110
+ rank,
1111
+ expanded.ambiguous ? ASK : 0,
1112
+ rankGit([...aliasWords, ...argWords.slice(i + 1)], depth + 1)
1113
+ );
1114
+ }
1115
+ const opts = argsBeforeDashDash(rest);
1116
+ const has = (...names) => opts.some((a) => names.includes(a.split("=")[0]));
1117
+ const short = (letter) => opts.some((a) => new RegExp(`^-[a-zA-Z]*${letter}`).test(a));
1118
+ let sr = 0;
1119
+ switch (sub) {
1120
+ case "push": {
1121
+ const forced = has(
1122
+ "--force",
1123
+ "--force-with-lease",
1124
+ "--force-if-includes",
1125
+ "--mirror",
1126
+ "--delete",
1127
+ "--prune"
1128
+ ) || opts.some(
1129
+ (a) => /^-[a-zA-Z]+$/.test(a) && /[fd]/.test(a.replace(/o.*$/, ""))
1130
+ ) || rest.some((a) => /^[+:]/.test(a));
1131
+ sr = forced ? ASK : RANK.git_push;
1132
+ break;
1133
+ }
1134
+ case "reset":
1135
+ sr = has("--hard", "--merge") ? ASK : 0;
1136
+ break;
1137
+ case "clean":
1138
+ sr = has("--force") || short("f") ? ASK : 0;
1139
+ break;
1140
+ case "branch":
1141
+ sr = short("D") || (has("--delete") || short("d")) && (has("--force") || short("f")) ? ASK : 0;
1142
+ break;
1143
+ case "checkout":
1144
+ sr = rest.includes("--") || has("--force") || short("f") ? ASK : 0;
1145
+ break;
1146
+ case "switch":
1147
+ sr = has("--discard-changes", "--force") || short("f") ? ASK : 0;
1148
+ break;
1149
+ case "restore":
1150
+ sr = has("--source") || short("s") ? ASK : 0;
1151
+ break;
1152
+ case "stash":
1153
+ sr = ["drop", "clear"].includes(rest[0] ?? "") ? ASK : 0;
1154
+ break;
1155
+ case "update-ref":
1156
+ sr = short("d") ? ASK : 0;
1157
+ break;
1158
+ case "reflog":
1159
+ sr = ["expire", "delete"].includes(rest[0] ?? "") ? ASK : 0;
1160
+ break;
1161
+ case "filter-branch":
1162
+ case "filter-repo":
1163
+ sr = ASK;
1164
+ break;
1165
+ }
1166
+ if (!GIT_READS.has(sub) && rest.some((a) => AGENT_CONFIG_PATH.test(a)))
1167
+ sr = ASK;
1168
+ return Math.max(rank, sr);
1169
+ }
1170
+ var AGENT_CONFIG_SUBCOMMANDS = /* @__PURE__ */ new Set([
1171
+ "mcp",
1172
+ "plugin",
1173
+ "plugins",
1174
+ "config"
1175
+ ]);
1176
+ var AGENT_READ_ACTIONS = /* @__PURE__ */ new Set([
1177
+ "list",
1178
+ "ls",
1179
+ "get",
1180
+ "show",
1181
+ "serve",
1182
+ "help",
1183
+ "--help",
1184
+ "-h"
1185
+ ]);
1186
+ var AGENT_WIDENING_OPTS = /^(--mcp-config|--settings|--dangerously-skip-permissions|--allow-dangerously-skip-permissions|--allowed-tools|--allowedtools|--add-dir|--plugin-dir|--dangerously-bypass-approvals-and-sandbox|--yolo|--config)(=|$)/i;
1187
+ function rankAgentCli(base, args) {
1188
+ if (args.some((a) => AGENT_WIDENING_OPTS.test(a))) return ASK;
1189
+ if (base === "codex" && args.includes("-c")) return ASK;
1190
+ const mode = args.findIndex((a) => a === "--permission-mode");
1191
+ const modeValue = mode >= 0 ? args[mode + 1] : args.find((a) => a.startsWith("--permission-mode="))?.slice(18);
1192
+ if (modeValue !== void 0 && !/^(default|plan)$/i.test(modeValue))
1193
+ return ASK;
1194
+ const positionals2 = args.filter((a) => !a.startsWith("-"));
1195
+ const sub = positionals2.findIndex((p) => AGENT_CONFIG_SUBCOMMANDS.has(p));
1196
+ if (sub < 0) return 0;
1197
+ const action = positionals2[sub + 1];
1198
+ if (action === void 0 || AGENT_READ_ACTIONS.has(action)) return 0;
1199
+ return ASK;
1200
+ }
70
1201
  function classifyCommand(command) {
71
- if (DESTRUCTIVE.some((re) => re.test(command))) return "shell_destructive";
72
- if (DEPLOY.some((re) => re.test(command))) return "shell_deploy";
73
- if (GIT_PUSH.test(command)) return "git_push";
74
- return "shell";
1202
+ let rank = 0;
1203
+ if (DESTRUCTIVE.some((re) => re.test(command))) rank = ASK;
1204
+ else if (DEPLOY.some((re) => re.test(command))) rank = RANK.shell_deploy;
1205
+ else if (GIT_PUSH.test(command)) rank = RANK.git_push;
1206
+ else if (startsInteractiveSession(command)) rank = RANK.shell_session;
1207
+ if (rank < ASK) rank = Math.max(rank, rankText(command, 0));
1208
+ return BY_RANK[rank];
1209
+ }
1210
+ function isAgentConfigPath(path) {
1211
+ return AGENT_CONFIG_PATH.test(path);
75
1212
  }
76
1213
  function mcpToolName(server, tool) {
77
1214
  const part = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").replace(/^(\d)/, "x$1") || "x";
@@ -86,34 +1223,124 @@ function digest(text) {
86
1223
  function str(value) {
87
1224
  return typeof value === "string" ? value : "";
88
1225
  }
1226
+ var SHELL_TOOLS = {
1227
+ Bash: "command",
1228
+ // Claude Code; Codex shell and unified exec
1229
+ shell: "command",
1230
+ bash: "command",
1231
+ // DeepSeek Harness (plain and persistent)
1232
+ pwsh: "command",
1233
+ // DeepSeek Harness on Windows
1234
+ terminal_send: "text"
1235
+ // DeepSeek Harness PTY: typed input runs as a command
1236
+ };
1237
+ var FILE_TOOLS = /* @__PURE__ */ new Set([
1238
+ "Write",
1239
+ "Edit",
1240
+ "MultiEdit",
1241
+ "NotebookEdit",
1242
+ "write",
1243
+ // DeepSeek Harness
1244
+ "edit"
1245
+ // DeepSeek Harness
1246
+ ]);
1247
+ function commandText(value) {
1248
+ if (typeof value === "string") return value;
1249
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
1250
+ return value.join(" ");
1251
+ }
1252
+ return "";
1253
+ }
1254
+ var MAX_PATHS = 50;
1255
+ function patchTargets(patch) {
1256
+ const targets = [];
1257
+ const re = /^[ \t]*\*\*\* (Add File|Update File|Delete File|Move to): (.+?)[ \t\r]*$/gm;
1258
+ for (const m of patch.matchAll(re)) {
1259
+ const kind = m[1];
1260
+ const op = kind === "Add File" ? "add" : kind === "Update File" ? "update" : kind === "Delete File" ? "delete" : "move";
1261
+ targets.push({ op, path: m[2] });
1262
+ }
1263
+ return targets;
1264
+ }
1265
+ var AGENT_CONFIG_NOTE = "(agent MCP, hook, permission or plugin config)";
1266
+ function fileWrite(operation, path, content, cwd, summaryVerb) {
1267
+ const config = isAgentConfigPath(path);
1268
+ return {
1269
+ toolName: config ? "shell_destructive" : "file_write",
1270
+ arguments: {
1271
+ operation,
1272
+ path,
1273
+ contentBytes: Buffer.byteLength(content),
1274
+ contentDigest: digest(content),
1275
+ ...config ? { touches: "agent-config" } : {},
1276
+ ...cwd
1277
+ },
1278
+ summary: `${summaryVerb} ${path}${config ? ` ${AGENT_CONFIG_NOTE}` : ""}`
1279
+ };
1280
+ }
89
1281
  function governedCall(call) {
90
1282
  const { tool, input } = call;
91
1283
  const cwd = call.cwd ? { cwd: call.cwd } : {};
92
- if (tool === "Bash" || tool === "shell") {
93
- const command = str(input.command);
1284
+ const shellField = Object.hasOwn(SHELL_TOOLS, tool) ? SHELL_TOOLS[tool] : void 0;
1285
+ if (shellField) {
1286
+ const command = commandText(input[shellField]);
94
1287
  const toolName = classifyCommand(command);
95
1288
  return {
96
1289
  toolName,
97
- arguments: { command: bounded(command), ...cwd },
1290
+ arguments: {
1291
+ command: bounded(command),
1292
+ ...tool === "Bash" || tool === "shell" ? {} : { agentTool: tool },
1293
+ ...cwd
1294
+ },
98
1295
  summary: `Run: ${bounded(command, 160)}`
99
1296
  };
100
1297
  }
101
- if (["Write", "Edit", "MultiEdit", "NotebookEdit"].includes(tool)) {
1298
+ if (FILE_TOOLS.has(tool)) {
102
1299
  const path = str(input.file_path) || str(input.notebook_path);
103
1300
  const content = str(input.content) || str(input.new_string) || str(input.new_source) || (Array.isArray(input.edits) ? JSON.stringify(input.edits) : "");
1301
+ return fileWrite(
1302
+ tool,
1303
+ path,
1304
+ content,
1305
+ cwd,
1306
+ tool === "Write" || tool === "write" ? "Write" : "Edit"
1307
+ );
1308
+ }
1309
+ if (tool === "str_replace_editor") {
1310
+ if (input.command === "view") return null;
1311
+ const sub = str(input.command) || "unknown";
1312
+ const content = str(input.file_text) || str(input.new_str);
1313
+ return fileWrite(
1314
+ `str_replace_editor.${sub}`,
1315
+ str(input.path),
1316
+ content,
1317
+ cwd,
1318
+ sub === "create" ? "Write" : "Edit"
1319
+ );
1320
+ }
1321
+ if (tool === "apply_patch") {
1322
+ const patch = commandText(input.command) || str(input.patch) || str(input.input);
1323
+ const targets = patchTargets(patch);
1324
+ const paths = [...new Set(targets.map((t) => t.path))];
1325
+ const shown = paths.slice(0, MAX_PATHS).map((p) => bounded(p, 500));
1326
+ const config = paths.some((p) => isAgentConfigPath(p));
104
1327
  return {
105
- toolName: "file_write",
1328
+ toolName: config ? "shell_destructive" : "file_write",
106
1329
  arguments: {
107
- operation: tool,
108
- path,
109
- contentBytes: Buffer.byteLength(content),
110
- contentDigest: digest(content),
1330
+ operation: "apply_patch",
1331
+ ...config ? { touches: "agent-config" } : {},
1332
+ path: shown[0] ?? "",
1333
+ paths: shown,
1334
+ ...paths.length > MAX_PATHS ? { morePaths: paths.length - MAX_PATHS } : {},
1335
+ operations: targets.slice(0, MAX_PATHS).map((t) => `${t.op} ${bounded(t.path, 500)}`),
1336
+ contentBytes: Buffer.byteLength(patch),
1337
+ contentDigest: digest(patch),
111
1338
  ...cwd
112
1339
  },
113
- summary: `${tool === "Write" ? "Write" : "Edit"} ${path}`
1340
+ summary: (paths.length === 0 ? "Apply a patch (no file headers found)" : paths.length === 1 ? `Patch ${bounded(paths[0], 160)}` : `Patch ${paths.length} files: ${bounded(paths.join(", "), 160)}`) + (config ? ` ${AGENT_CONFIG_NOTE}` : "")
114
1341
  };
115
1342
  }
116
- if (tool === "WebFetch") {
1343
+ if (tool === "WebFetch" || tool === "web_fetch") {
117
1344
  const url = str(input.url);
118
1345
  return {
119
1346
  toolName: "web_fetch",
@@ -164,6 +1391,7 @@ function denied(call, code, extra = {}) {
164
1391
  ...extra
165
1392
  };
166
1393
  }
1394
+ var FINAL_CHECK_LEAD_MS = 1500;
167
1395
  var isPending = (r) => !r.ok && (r.reasonCode === "APPROVAL_REQUIRED" || r.reasonCode === "APPROVAL_PENDING") || r.ok && r.decision === "ASK";
168
1396
  async function decide(call, options) {
169
1397
  const wait = options.wait ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -183,6 +1411,7 @@ async function decide(call, options) {
183
1411
  } catch {
184
1412
  return denied(call, "SERVICE_UNAVAILABLE");
185
1413
  }
1414
+ const answeredAt = now();
186
1415
  const requestId = result.requestId;
187
1416
  let approvedByHuman = false;
188
1417
  if (isPending(result)) {
@@ -190,14 +1419,19 @@ async function decide(call, options) {
190
1419
  log(
191
1420
  `Guardian: "${call.summary}" is held for approval (request ${requestId}). Approve it on your phone or in the console.`
192
1421
  );
193
- const deadline = now() + options.approvalTimeoutMs;
1422
+ const window = Math.min(
1423
+ options.approvalTimeoutMs,
1424
+ options.requestTtlMs ?? options.approvalTimeoutMs
1425
+ );
1426
+ const lead = Math.min(FINAL_CHECK_LEAD_MS, Math.floor(window / 4));
1427
+ const lastCheckAt = answeredAt + window - lead;
194
1428
  const maxInterval = Math.max(
195
1429
  options.maxPollIntervalMs ?? 1e4,
196
1430
  options.pollIntervalMs
197
1431
  );
198
1432
  let interval = options.pollIntervalMs;
199
1433
  for (; ; ) {
200
- await wait(interval);
1434
+ await wait(Math.max(0, Math.min(interval, lastCheckAt - now())));
201
1435
  interval = Math.min(Math.ceil(interval * 1.5), maxInterval);
202
1436
  let claimed;
203
1437
  try {
@@ -210,7 +1444,7 @@ async function decide(call, options) {
210
1444
  approvedByHuman = claimed.ok && claimed.decision === "ALLOW";
211
1445
  break;
212
1446
  }
213
- if (now() >= deadline) {
1447
+ if (now() >= lastCheckAt) {
214
1448
  return {
215
1449
  decision: "deny",
216
1450
  reason: `Guardian is still waiting for a human to approve ${call.toolName} (request ${requestId}). Nothing ran. Tell the developer it needs approval in the Guardian app, then try again.`,
@@ -256,6 +1490,39 @@ async function decide(call, options) {
256
1490
  var DEFAULT_BASE_URL = "https://guardian-api.odingard.com";
257
1491
  var REQUEST_TTL_MS = 3e5;
258
1492
  var MAX_WAIT_MS = REQUEST_TTL_MS;
1493
+ var DEFAULT_DEADLINE_MS = 54e4;
1494
+ var AGENT_NAMES = {
1495
+ "claude-code": "Claude Code",
1496
+ codex: "Codex",
1497
+ dsh: "DeepSeek Harness"
1498
+ };
1499
+ function formatVerdict(dialect, verdict) {
1500
+ if (verdict === null) {
1501
+ return { stdout: "", stderr: "", exitCode: 0, verdict };
1502
+ }
1503
+ if (dialect === "claude-code") {
1504
+ return {
1505
+ stdout: claudeCodeOutput(verdict),
1506
+ stderr: "",
1507
+ exitCode: 0,
1508
+ verdict
1509
+ };
1510
+ }
1511
+ if (verdict.decision === "allow") {
1512
+ return {
1513
+ stdout: dialect === "codex" ? "" : claudeCodeOutput(verdict),
1514
+ stderr: "",
1515
+ exitCode: 0,
1516
+ verdict
1517
+ };
1518
+ }
1519
+ return {
1520
+ stdout: claudeCodeOutput(verdict),
1521
+ stderr: verdict.reason,
1522
+ exitCode: 2,
1523
+ verdict
1524
+ };
1525
+ }
259
1526
  function claudeCodeOutput(verdict) {
260
1527
  return JSON.stringify({
261
1528
  hookSpecificOutput: {
@@ -271,16 +1538,53 @@ function connectionFor(toolName, env) {
271
1538
  return env.GUARDIAN_HOOK_ASK_CONNECTION_ID?.trim() || allow;
272
1539
  }
273
1540
  async function runHook(stdin, env, deps = {}) {
274
- const failClosed = (reason) => claudeCodeOutput({ decision: "deny", reason: `Guardian hook: ${reason}` });
1541
+ return (await runHookAs("claude-code", stdin, env, deps)).stdout;
1542
+ }
1543
+ async function runHookAs(dialect, stdin, env, deps = {}) {
1544
+ const requested = Number(
1545
+ env.GUARDIAN_HOOK_DEADLINE_MS ?? DEFAULT_DEADLINE_MS
1546
+ );
1547
+ const deadlineMs = Number.isFinite(requested) && requested > 0 ? Math.min(requested, DEFAULT_DEADLINE_MS) : DEFAULT_DEADLINE_MS;
1548
+ let timer;
1549
+ const timedOut = new Promise((resolve) => {
1550
+ timer = setTimeout(
1551
+ () => resolve({
1552
+ decision: "deny",
1553
+ reason: "Guardian hook: no decision before the hook's deadline. Nothing ran."
1554
+ }),
1555
+ deadlineMs
1556
+ );
1557
+ timer.unref?.();
1558
+ });
1559
+ try {
1560
+ const verdict = await Promise.race([
1561
+ evaluate(dialect, stdin, env, deps, deadlineMs).catch(
1562
+ () => ({
1563
+ decision: "deny",
1564
+ reason: "Guardian hook: crashed. Nothing ran."
1565
+ })
1566
+ ),
1567
+ timedOut
1568
+ ]);
1569
+ return formatVerdict(dialect, verdict);
1570
+ } finally {
1571
+ clearTimeout(timer);
1572
+ }
1573
+ }
1574
+ async function evaluate(dialect, stdinText, env, deps, deadlineMs) {
1575
+ const failClosed = (reason) => ({
1576
+ decision: "deny",
1577
+ reason: `Guardian hook: ${reason}`
1578
+ });
275
1579
  let input;
276
1580
  try {
277
- input = JSON.parse(stdin);
1581
+ input = JSON.parse(await stdinText);
278
1582
  } catch {
279
1583
  return failClosed(
280
1584
  "could not read the tool call from the agent. Nothing ran."
281
1585
  );
282
1586
  }
283
- if (typeof input.tool_name !== "string") {
1587
+ if (!input || typeof input.tool_name !== "string") {
284
1588
  return failClosed("the tool call had no tool name. Nothing ran.");
285
1589
  }
286
1590
  const call = governedCall({
@@ -288,7 +1592,7 @@ async function runHook(stdin, env, deps = {}) {
288
1592
  input: input.tool_input && typeof input.tool_input === "object" ? input.tool_input : {},
289
1593
  ...typeof input.cwd === "string" ? { cwd: input.cwd } : {}
290
1594
  });
291
- if (call === null) return "";
1595
+ if (call === null) return null;
292
1596
  const connectionId = connectionFor(call.toolName, env);
293
1597
  const credential = env.GUARDIAN_AGENT_CREDENTIAL?.trim();
294
1598
  const baseUrl = (env.GUARDIAN_BASE_URL?.trim() || DEFAULT_BASE_URL).replace(
@@ -297,7 +1601,7 @@ async function runHook(stdin, env, deps = {}) {
297
1601
  );
298
1602
  if (!deps.client && (!credential || !connectionId)) {
299
1603
  return failClosed(
300
- "GUARDIAN_AGENT_CREDENTIAL and GUARDIAN_CONNECTION_ID must be set (run `npx @odingard/guardian-hook setup`). Nothing ran."
1604
+ `GUARDIAN_AGENT_CREDENTIAL and GUARDIAN_CONNECTION_ID must be set (run \`${dialect === "claude-code" ? "npx @odingard/guardian-hook" : `guardian-hook ${dialect}`} setup\`). Nothing ran.`
301
1605
  );
302
1606
  }
303
1607
  if (!deps.client && !baseUrl.startsWith("https://")) {
@@ -306,15 +1610,16 @@ async function runHook(stdin, env, deps = {}) {
306
1610
  const requested = Number(
307
1611
  env.GUARDIAN_HOOK_APPROVAL_TIMEOUT_MS ?? MAX_WAIT_MS
308
1612
  );
309
- const approvalTimeoutMs = Number.isFinite(requested) ? Math.min(Math.max(requested, 0), MAX_WAIT_MS) : MAX_WAIT_MS;
310
- const verdict = await decide(call, {
1613
+ const ceiling = Math.max(0, Math.min(MAX_WAIT_MS, deadlineMs - 3e4));
1614
+ const approvalTimeoutMs = Number.isFinite(requested) ? Math.min(Math.max(requested, 0), ceiling) : ceiling;
1615
+ return decide(call, {
311
1616
  client: deps.client ?? createGuardianClient({
312
1617
  baseUrl,
313
1618
  credential,
314
1619
  ...env.GUARDIAN_ADAPTER_ID ? { adapterId: env.GUARDIAN_ADAPTER_ID } : {}
315
1620
  }),
316
1621
  connectionId: connectionId ?? "",
317
- agentName: env.GUARDIAN_HOOK_AGENT_NAME?.trim() || "Claude Code",
1622
+ agentName: env.GUARDIAN_HOOK_AGENT_NAME?.trim() || AGENT_NAMES[dialect],
318
1623
  approvalTimeoutMs,
319
1624
  requestTtlMs: REQUEST_TTL_MS,
320
1625
  pollIntervalMs: 2e3,
@@ -322,7 +1627,6 @@ async function runHook(stdin, env, deps = {}) {
322
1627
  ...deps.now ? { now: deps.now } : {},
323
1628
  ...deps.log ? { log: deps.log } : {}
324
1629
  });
325
- return claudeCodeOutput(verdict);
326
1630
  }
327
1631
 
328
1632
  // src/agent-sdk.ts