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