@blogic-cz/agent-tools 1.6.3 → 1.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -668,6 +668,10 @@ Single literal `echo`/`printf` writes with output redirection and single `cat`/`
668
668
 
669
669
  Python JSON parsing, dictionary iteration, and printing are not themselves evidence of credential access. They follow the same access and execution rules as other supported inline programs. This intentionally permits ordinary JSON output that older versions refused. Unknown secrets in ordinary files can therefore reach arbitrary shell output; wrapper content checks do not protect that output. A refusal caused by unsupported syntax means the guard could not establish its policy conditions, not that it observed a secret being read.
670
670
 
671
+ The bounded Python subprocess proof supports one immutable literal argv binding followed by one `subprocess.run` call. A literal `cwd` is replayed as a quoted, success-gated directory change before the nested argv, so relative reader operands and named directories receive the ordinary path policies. Supported directories are literal relative or absolute paths, including quoted spaces. Empty, dynamic, Unicode, escaped, tilde and other unsupported directory forms refuse rather than dropping the context. Cwd proof recursively unwraps only supported command, RTK proxy and environment wrappers. Environment directory/split-string options and unproved wrapper forms refuse. Only supported passive text, reader, metadata, literal Bun script and blocked CLI roles receive nested replay; shell executors, inline interpreter programs, unproved sed programs, find execution actions and unknown command roles refuse. Nested blocked CLI, environment, sensitive-path and custom-command policies remain active. Outer shell redirects retain their original shell directory. Arbitrary multi-call Python and `check_output` programs remain unsupported; direct Git metadata commands provide index/tree inspection.
672
+
673
+ Supported runtime script/tool operands form an argument boundary: a later tool `-c` configuration flag is data. Genuine eval and loader flags, Bun exec/repl and unproved startup-option arity still refuse. Literal Herdr prompts can compose with the existing literal agent-get and pane-read forms and passive consumers. One literal `--sql` operand on the supported `bun run db-tool sql/query` shape can contain SQL braces; substitutions, duplicate SQL operands and unknown wrapper options receive no exception. SQL output pipelines require closed passive consumers, so shell, xargs and unknown executors refuse. Numeric `$?` can appear only in passive echo/printf data operands, with passive pipeline consumers. Explicitly approved environment names can appear in quoted equality or blankness tests, including the quoted empty default `${NAME:-}`. This does not add default-approved names or permit shell evaluators. One sed print program with numeric or escaped-regex addresses/ranges and known `-n`/`-E` flags is passive; execution/read/write callbacks, extra programs and script files receive no print proof. Local file bindings use that same closed print proof and never a generic sed-word fallback. Ordinary path and configured command policies still apply to all these forms.
674
+
671
675
  A separate bounded Node proof supports literal `package.json` inventory loops that print `bin` metadata or use the supported scripts filter. The complete invocation must preserve loader identity through known passive commands, closed literal writers, or proved inventories. Startup/environment mutations, unknown commands, loader changes, and other dynamic loading remain unproved and are refused.
672
676
 
673
677
  ### Setup for OpenCode
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/credential-guard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAK5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAM/D,8CAA8C;AAC9C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,+CAA+C;AAC/C,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,CAAC;AAQF,+CAA+C;AAC/C,MAAM,MAAM,eAAe,GAAG;IAC5B,uBAAuB,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IACxE,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC3E,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,sBAAsB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,iBAAiB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjF,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACjD,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CACxD,CAAC;AAgzEF,6CAA6C;AAC7C,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAErE;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEpE;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEpE;AAMD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,eAAe,CA4TrF;AAQD,kEAAkE;AAClE,eAAO,MAAM,uBAAuB,UAtpFD,SAAS,UAAU,UAAU,KAAK,IAspFM,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,aAAa,YAxpFC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAwpFjB,CAAC;AAExD,yEAAyE;AACzE,eAAO,MAAM,aAAa,aA1pFE,MAAM,KAAK,OA0pFgB,CAAC;AAExD,yDAAyD;AACzD,eAAO,MAAM,aAAa,aA5pFE,MAAM,KAAK,OA4pFgB,CAAC;AAExD,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,YA9pFC,MAAM,KAAK,OA8pF0B,CAAC;AAE1E,iDAAiD;AACjD,eAAO,MAAM,iBAAiB,YAhqFC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAgqFf,CAAC;AAEhE,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,YAlqFC,MAAM,KAAK,OAkqFsB,CAAC;AAElE,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,YApqFC,MAAM,KAAK,MAAM,GAAG,IAoqFa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/credential-guard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAK5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAM/D,8CAA8C;AAC9C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,+CAA+C;AAC/C,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,CAAC;AAQF,+CAA+C;AAC/C,MAAM,MAAM,eAAe,GAAG;IAC5B,uBAAuB,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IACxE,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC3E,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,sBAAsB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,iBAAiB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjF,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACjD,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CACxD,CAAC;AAwhFF,6CAA6C;AAC7C,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAErE;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEpE;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEpE;AAMD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,eAAe,CA4TrF;AAQD,kEAAkE;AAClE,eAAO,MAAM,uBAAuB,UA93FD,SAAS,UAAU,UAAU,KAAK,IA83FM,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,aAAa,YAh4FC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAg4FjB,CAAC;AAExD,yEAAyE;AACzE,eAAO,MAAM,aAAa,aAl4FE,MAAM,KAAK,OAk4FgB,CAAC;AAExD,yDAAyD;AACzD,eAAO,MAAM,aAAa,aAp4FE,MAAM,KAAK,OAo4FgB,CAAC;AAExD,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,YAt4FC,MAAM,KAAK,OAs4F0B,CAAC;AAE1E,iDAAiD;AACjD,eAAO,MAAM,iBAAiB,YAx4FC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAw4Ff,CAAC;AAEhE,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,YA14FC,MAAM,KAAK,OA04FsB,CAAC;AAElE,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,YA54FC,MAAM,KAAK,MAAM,GAAG,IA44Fa,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "1.6.3",
3
+ "version": "1.6.4",
4
4
  "description": "CLI tools for AI coding agent workflows — GitHub, database, Kubernetes, Azure platform, Azure DevOps, logs, sessions, and audit",
5
5
  "keywords": [
6
6
  "agent",
@@ -262,21 +262,12 @@ function isSafeLocalAssignmentCommand(words: string[]): boolean {
262
262
  return !argv.slice(1).some((arg) => /^-(?:exec|ok|delete|fprint|fprintf)/.test(arg));
263
263
  }
264
264
  if (name === "bun") return argv[1] === "run" && ["db-tool", "gh-tool"].includes(argv[2] ?? "");
265
+ if (name === "sed") return isReadonlySedPrint(argv);
265
266
  return (
266
267
  isPassiveTextCommand(argv) ||
267
268
  ["ls", "cat", "tail", "wc", "uniq", "cut", "mv"].includes(name) ||
268
269
  (name === "sort" &&
269
- !hasExecutionOption(argv.slice(1), ["compress-program", "files0-from", "output"], "o")) ||
270
- (name === "sed" &&
271
- argv
272
- .slice(1)
273
- .every(
274
- (arg) =>
275
- /^-[En]+$/.test(arg) ||
276
- /^s([/|]).*\1[^/|]*\1[gp]*$/.test(arg) ||
277
- /^[A-Za-z0-9_./-]+$/.test(arg),
278
- ) &&
279
- !argv.includes("-f"))
270
+ !hasExecutionOption(argv.slice(1), ["compress-program", "files0-from", "output"], "o"))
280
271
  );
281
272
  }
282
273
 
@@ -502,13 +493,7 @@ function isLiteralLoopCommand(words: string[], marker: string, value: string): b
502
493
  const format = passivePrintfFormatIndex(argv);
503
494
  return format !== undefined && uses.every((index) => index > format);
504
495
  }
505
- if (name === "herdr")
506
- return (
507
- ((argv.length === 4 && argv[1] === "agent" && argv[2] === "get") ||
508
- isLiteralHerdrPaneRead(argv)) &&
509
- /^[A-Za-z0-9_.:-]+$/.test(argv[3] ?? "") &&
510
- uses.every((index) => index === 3)
511
- );
496
+ if (name === "herdr") return isLiteralHerdrRead(argv) && uses.every((index) => index === 3);
512
497
  if (name === "bun")
513
498
  return (
514
499
  argv.length === 9 &&
@@ -537,6 +522,14 @@ function isLiteralHerdrPaneRead(argv: string[]): boolean {
537
522
  );
538
523
  }
539
524
 
525
+ function isLiteralHerdrRead(argv: string[]): boolean {
526
+ return (
527
+ ((argv.length === 4 && argv.slice(0, 3).join(" ") === "herdr agent get") ||
528
+ isLiteralHerdrPaneRead(argv)) &&
529
+ /^[A-Za-z0-9_.:-]+$/.test(argv[3] ?? "")
530
+ );
531
+ }
532
+
540
533
  /** A complete stdin-only JSON projection, with no suffix, imports or executable input. */
541
534
  function isLiteralStdinJsonDisplay(argv: string[]): boolean {
542
535
  if (argv.length !== 3 || !["python", "python3"].includes(argv[0] ?? "") || argv[1] !== "-c")
@@ -852,15 +845,54 @@ function inlineProgramIndex(argv: string[]): number | undefined {
852
845
  }
853
846
 
854
847
  function isUnsupportedInlineRuntime(argv: string[]): boolean {
855
- if (!inlineRuntimeKind(argv[0])) return false;
848
+ const runtime = inlineRuntimeKind(argv[0]);
849
+ if (!runtime) return false;
856
850
  if (inlineProgramIndex(argv) !== undefined) return false;
857
- return argv
858
- .slice(1)
859
- .some((arg) =>
851
+ let index = 1;
852
+ for (; index < argv.length; index++) {
853
+ const arg = argv[index] ?? "";
854
+ if (arg === "--" || !arg.startsWith("-")) break;
855
+ if (
860
856
  /^(?:-[A-Za-z]*[cep]|-[rmp]|--(?:eval|print|require|import|loader|preload|experimental-loader))(?:$|=|[^-])/.test(
861
857
  arg,
862
- ),
863
- );
858
+ )
859
+ )
860
+ return true;
861
+ if (
862
+ (runtime === "python" && arg === "--check-hash-based-pycs") ||
863
+ (runtime === "node" &&
864
+ ["--conditions", "-C", "--icu-data-dir", "--dns-result-order"].includes(arg))
865
+ ) {
866
+ if (argv[index + 1] === undefined) return true;
867
+ index++;
868
+ continue;
869
+ }
870
+ // Unknown startup-option arity cannot establish where the script/tool begins.
871
+ if (
872
+ !(
873
+ (runtime === "python" && /^-[uBISsEOqvV]+$/.test(arg)) ||
874
+ (runtime === "node" &&
875
+ /^(?:--(?:trace-warnings|no-warnings|version|help)|--(?:conditions|icu-data-dir|dns-result-order)=.+)$/.test(
876
+ arg,
877
+ )) ||
878
+ (runtime === "bun" && ["--no-env-file", "--version", "--help"].includes(arg)) ||
879
+ ["--version", "--help", "-h"].includes(arg)
880
+ )
881
+ )
882
+ return true;
883
+ }
884
+ if (runtime === "bun" && ["exec", "repl"].includes(argv[index] ?? "")) return true;
885
+ // Bun retains eval/loader controls around its tool operand; only a later -c is tool data.
886
+ return (
887
+ runtime === "bun" &&
888
+ argv
889
+ .slice(index)
890
+ .some((arg) =>
891
+ /^(?:-[A-Za-z]*[ep]|-[rmp]|--(?:eval|print|require|import|loader|preload|experimental-loader))(?:$|=|[^-])/.test(
892
+ arg,
893
+ ),
894
+ )
895
+ );
864
896
  }
865
897
 
866
898
  /** Move a complete, literal Python stdin program into the existing inline argv boundary. */
@@ -1045,7 +1077,64 @@ function staticSubprocessCommand(program: string): string | undefined {
1045
1077
  (match) => match[1] ?? match[2] ?? "",
1046
1078
  );
1047
1079
  if (!values.length) return undefined;
1048
- return values.map((value) => "'" + value.replaceAll("'", "'\"'\"'") + "'").join(" ");
1080
+ const quote = (value: string) => "'" + value.replaceAll("'", "'\"'\"'") + "'";
1081
+ const command = values.map(quote).join(" ");
1082
+ const directories = [...call[0].matchAll(/\bcwd\s*=\s*(?:"([^"\\]*)"|'([^'\\]*)')/g)];
1083
+ if (!directories.length) return command;
1084
+ const cwd = directories[0]?.[1] ?? directories[0]?.[2] ?? "";
1085
+ // Never replay a cwd-bearing call as if it ran in the original shell directory.
1086
+ if (directories.length !== 1 || !isStaticNavigationCommand(["cd", "--", cwd])) return undefined;
1087
+ const argv = unwrapCwdSubprocessCommand(values);
1088
+ if (!argv || !isProvedCwdSubprocessRole(argv)) return undefined;
1089
+ // Keep wrapper assignments/options in policy replay after proving the nested command role.
1090
+ const replay = `cd -- ${quote(cwd)} && ${command}`;
1091
+ return hasProvedNavigationSequence(replay) ? replay : undefined;
1092
+ }
1093
+
1094
+ function unwrapCwdSubprocessCommand(words: string[]): string[] | undefined {
1095
+ let argv = words;
1096
+ // Each successful wrapper removes words; the original word count bounds recursion.
1097
+ for (let depth = 0; depth < words.length; depth++) {
1098
+ const unwrapped = unwrapProofCommand(argv, "\0");
1099
+ if (!unwrapped) return undefined;
1100
+ argv = unwrapped;
1101
+ if (argv[0]?.split("/").at(-1) !== "env") return argv;
1102
+ const nested = unwrapEnvironmentCommand(argv);
1103
+ if (!nested) return undefined;
1104
+ // Inspect only env's prefix, not options belonging to its child command.
1105
+ // env directory, split-string and unknown options do not preserve the proved context.
1106
+ if (
1107
+ argv
1108
+ .slice(1, argv.length - nested.length)
1109
+ .some(
1110
+ (arg) =>
1111
+ arg.startsWith("-") && arg !== "--" && arg !== "-i" && arg !== "--ignore-environment",
1112
+ )
1113
+ )
1114
+ return undefined;
1115
+ argv = nested;
1116
+ }
1117
+ return undefined;
1118
+ }
1119
+
1120
+ function isProvedCwdSubprocessRole(argv: string[]): boolean {
1121
+ const name = argv[0]?.split("/").at(-1) ?? "";
1122
+ if (inlineProgramIndex(argv) !== undefined || isUnsupportedInlineRuntime(argv)) return false;
1123
+ if (isPassiveTextCommand(argv)) return true;
1124
+ if (name === "find") return isSafeLocalAssignmentCommand(argv);
1125
+ if (["ls", "cat", "uniq", "cut"].includes(name)) return true;
1126
+ if (name === "wc") return !hasExecutionOption(argv.slice(1), ["files0-from"]);
1127
+ if (name === "sort")
1128
+ return !hasExecutionOption(argv.slice(1), ["compress-program", "files0-from"]);
1129
+ if (name === "git") {
1130
+ let index = 1;
1131
+ while (argv[index] === "-C" && argv[index + 1]) index += 2;
1132
+ return ["ls-files", "ls-tree", "status", "rev-parse", "rev-list"].includes(argv[index] ?? "");
1133
+ }
1134
+ // A literal Bun script keeps the existing supported test-runner route.
1135
+ if (name === "bun") return /^[A-Za-z0-9_./-]+\.(?:[cm]?[jt]s)$/.test(argv[1] ?? "");
1136
+ // Retain extraction so the nested CLI policy returns its ordinary descriptor.
1137
+ return DEFAULT_BLOCKED_CLI_TOOLS.some((tool) => tool.name === name);
1049
1138
  }
1050
1139
 
1051
1140
  function inlineSubprocessCommands(command: string): string[] {
@@ -1207,6 +1296,9 @@ function hasCommandArgumentBraceExpansion(argv: string[]): boolean {
1207
1296
  argv[4] === "query"
1208
1297
  )
1209
1298
  return false;
1299
+ const sql = literalSqlArgumentIndex(argv);
1300
+ if (sql !== undefined)
1301
+ return argv.some((arg, index) => index !== sql && hasArgumentBraceExpansion(arg));
1210
1302
  return (argv[0]?.split("/").at(-1) === "awk" && !isAwkExecution(argv)) ||
1211
1303
  inlineProgramIndex(argv) !== undefined ||
1212
1304
  isJqObjectConstruction(argv)
@@ -1214,6 +1306,32 @@ function hasCommandArgumentBraceExpansion(argv: string[]): boolean {
1214
1306
  : hasArgumentBraceExpansion(argv.join(" "));
1215
1307
  }
1216
1308
 
1309
+ function literalSqlArgumentIndex(argv: string[]): number | undefined {
1310
+ if (
1311
+ argv[0]?.split("/").at(-1) !== "bun" ||
1312
+ argv[1] !== "run" ||
1313
+ argv[2] !== "db-tool" ||
1314
+ !["sql", "query"].includes(argv[3] ?? "")
1315
+ )
1316
+ return undefined;
1317
+ let sql: number | undefined;
1318
+ const seen = new Set<string>();
1319
+ for (let index = 4; index < argv.length; index++) {
1320
+ const [option = "", ...attached] = (argv[index] ?? "").split("=");
1321
+ if (
1322
+ !["--sql", "--env", "--profile", "--limit", "--format"].includes(option) ||
1323
+ seen.has(option)
1324
+ )
1325
+ return undefined;
1326
+ seen.add(option);
1327
+ const value = attached.length ? attached.join("=") : argv[++index];
1328
+ if (!value || value.startsWith("--")) return undefined;
1329
+ if (option === "--sql") sql = index;
1330
+ else if (!/^[A-Za-z0-9_./-]+$/.test(value)) return undefined;
1331
+ }
1332
+ return sql;
1333
+ }
1334
+
1217
1335
  /** echo and printf print their arguments; grep -o, rg and git grep print matched pattern text. */
1218
1336
  function isLiteralTextProducer(argv: string[]): boolean {
1219
1337
  return isPassiveTextCommand(argv) && argv[0]?.split("/").at(-1) !== "head";
@@ -1584,7 +1702,8 @@ function hasProvedNavigationSequence(command: string): boolean {
1584
1702
  return false;
1585
1703
  if (!changesDirectory.length) return true;
1586
1704
  // Only the final AND list can use this directory. A failing cd never reaches its readers.
1587
- const syntax = literal.syntax.replace(/[ \t\n]+$/g, "");
1705
+ // Trim real trailing whitespace, not the spaces masking final quoted argv words.
1706
+ const syntax = literal.syntax.slice(0, command.replace(/[ \t\n]+$/g, "").length);
1588
1707
  const boundary = Math.max(syntax.lastIndexOf(";"), syntax.lastIndexOf("\n"));
1589
1708
  const tail = parseStaticShellCommands(command.slice(boundary + 1, syntax.length));
1590
1709
  if (!tail || typeof tail === "string" || tail.pipelines.length < 2) return false;
@@ -1608,6 +1727,7 @@ function isPassiveTextCommand(argv: string[]): boolean {
1608
1727
  }
1609
1728
  if (name === "rg") return !hasExecutionOption(args, ["pre", "hostname-bin"]);
1610
1729
  if (name === "awk") return !isAwkExecution(argv);
1730
+ if (name === "sed") return isReadonlySedPrint(argv);
1611
1731
  if (name === "jq") return isJqObjectConstruction(argv);
1612
1732
  return (
1613
1733
  (name === "git" && args[0] === "status") ||
@@ -1617,6 +1737,19 @@ function isPassiveTextCommand(argv: string[]): boolean {
1617
1737
  );
1618
1738
  }
1619
1739
 
1740
+ /** One print program with optional numeric or escaped-regex addresses; no callbacks. */
1741
+ function isReadonlySedPrint(argv: string[]): boolean {
1742
+ if (argv[0]?.split("/").at(-1) !== "sed") return false;
1743
+ let index = 1;
1744
+ while (/^-[nE]+$/.test(argv[index] ?? "")) index++;
1745
+ if (argv[index] === "--") index++;
1746
+ const address = String.raw`(?:[0-9]+|\$|/(?:\\[^\r\n]|[^/\\\r\n])*/)`;
1747
+ return (
1748
+ new RegExp(`^(?:${address}(?:,${address})?)?p$`).test(argv[index] ?? "") &&
1749
+ argv.slice(index + 1).every((arg) => !arg.startsWith("-"))
1750
+ );
1751
+ }
1752
+
1620
1753
  function hasStaticEnvironmentRead(
1621
1754
  argv: string[],
1622
1755
  allowedNames: Set<string>,
@@ -1702,6 +1835,7 @@ function isStaticHerdrPrompt(command: string): boolean {
1702
1835
  argv.slice(1).every((arg) => arg === "--" || !arg.startsWith("-"))
1703
1836
  );
1704
1837
  if (isPassiveTextCommand(argv)) return true;
1838
+ if (isLiteralHerdrRead(argv)) return index === 0;
1705
1839
  if (isStaticNavigationCommand(argv))
1706
1840
  return index === 0 && (argv[0] !== "cd" || pipeline.length === 1);
1707
1841
  return false;
@@ -1732,6 +1866,26 @@ function hasEnvironmentRead(
1732
1866
  const parsed = parseStaticShellCommands(command);
1733
1867
  if (parsed === "brace-expansion") return true;
1734
1868
  if (!parsed) {
1869
+ const operands = parseStaticShellCommands(command, true);
1870
+ if (
1871
+ operands &&
1872
+ typeof operands !== "string" &&
1873
+ operands.pipelines.flat().some((words) => {
1874
+ const argv = unwrapStaticCommand(words);
1875
+ return (
1876
+ argv[0]?.split("/").at(-1) === "bun" &&
1877
+ argv[1] === "run" &&
1878
+ argv[2] === "db-tool" &&
1879
+ ["sql", "query"].includes(argv[3] ?? "") &&
1880
+ argv.some(
1881
+ (arg, index) =>
1882
+ (arg === "--sql" && /\$\(|`/.test(argv[index + 1] ?? "")) ||
1883
+ (arg.startsWith("--sql=") && /\$\(|`/.test(arg)),
1884
+ )
1885
+ );
1886
+ })
1887
+ )
1888
+ return true;
1735
1889
  const unquoted = command.replace(/'(?:\\.|[^'])*'/g, " ").replace(/"(?:\\.|[^"$`])*"/g, " ");
1736
1890
  const hasExecutor =
1737
1891
  /\|\s*(?:sh|bash|zsh|xargs)|\b(?:sh|bash|zsh)\b|\$\(|`|\b(?:eval|source)\b|\bfind\b.*\b-exec\b/i.test(
@@ -1745,6 +1899,16 @@ function hasEnvironmentRead(
1745
1899
  }
1746
1900
  const pipelines = parsed.pipelines.map((commands) => commands.map(unwrapStaticCommand));
1747
1901
  const unwrapped = pipelines.flat();
1902
+ if (
1903
+ pipelines.some((pipeline) =>
1904
+ pipeline.some(
1905
+ (argv, index) =>
1906
+ literalSqlArgumentIndex(argv) !== undefined &&
1907
+ pipeline.slice(index + 1).some((consumer) => !isPassiveTextCommand(consumer)),
1908
+ ),
1909
+ )
1910
+ )
1911
+ return true;
1748
1912
  if (
1749
1913
  parsed.redirects.length > 0 &&
1750
1914
  unwrapped.some((argv) => isAllowedEnvironmentRead(argv, allowedNames))
@@ -2146,7 +2310,13 @@ function hasEnvironmentVariableExpansionRead(
2146
2310
  normalized += char;
2147
2311
  continue;
2148
2312
  }
2149
- const variable = /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}|^\$([A-Za-z_][A-Za-z0-9_]*)/.exec(
2313
+ if (command[i + 1] === "?") {
2314
+ expansions++;
2315
+ normalized += "NUMERICSTATUSVALUE";
2316
+ i++;
2317
+ continue;
2318
+ }
2319
+ const variable = /^\$\{([A-Za-z_][A-Za-z0-9_]*)(:-)?\}|^\$([A-Za-z_][A-Za-z0-9_]*)/.exec(
2150
2320
  command.slice(i),
2151
2321
  );
2152
2322
  if (!variable) {
@@ -2157,7 +2327,13 @@ function hasEnvironmentVariableExpansionRead(
2157
2327
  continue;
2158
2328
  }
2159
2329
  expansions++;
2160
- const name = variable[1] ?? variable[2] ?? "";
2330
+ const name = variable[1] ?? variable[3] ?? "";
2331
+ if (variable[2]) {
2332
+ if (quote !== '"' || !allowedNames.has(name)) invalidExpansion = true;
2333
+ normalized += "ENVTESTVALUE";
2334
+ i += variable[0].length - 1;
2335
+ continue;
2336
+ }
2161
2337
  const assignment = assignments.get(name);
2162
2338
  if (assignment && assignment.end >= i) invalidExpansion = true;
2163
2339
  if (assignment && assignment.end < i) {
@@ -2182,6 +2358,39 @@ function hasEnvironmentVariableExpansionRead(
2182
2358
  if (!hasSafeLocalAssignmentCommands(localMaterialized, assignments)) return true;
2183
2359
  const parsed = parseStaticShellCommands(localMaterialized);
2184
2360
  if (!parsed || typeof parsed === "string") return true;
2361
+ for (const [index, pipeline] of parsed.pipelines.entries()) {
2362
+ if (!pipeline.some((words) => words.some((arg) => arg.includes("NUMERICSTATUSVALUE"))))
2363
+ continue;
2364
+ if (parsed.redirects.some((redirect) => redirect.pipeline === index)) return true;
2365
+ if (
2366
+ pipeline.some((words) => {
2367
+ const argv = unwrapStaticCommand(words);
2368
+ if (!words.some((arg) => arg.includes("NUMERICSTATUSVALUE")))
2369
+ return !isPassiveTextCommand(argv);
2370
+ const name = argv[0]?.split("/").at(-1);
2371
+ const format = name === "printf" ? passivePrintfFormatIndex(argv) : undefined;
2372
+ return (
2373
+ !["echo", "printf"].includes(name ?? "") ||
2374
+ !isPassiveTextCommand(argv) ||
2375
+ words
2376
+ .slice(0, words.length - argv.length)
2377
+ .some((arg) => arg.includes("NUMERICSTATUSVALUE")) ||
2378
+ argv[0]?.includes("NUMERICSTATUSVALUE") === true ||
2379
+ (name === "printf" &&
2380
+ (format === undefined ||
2381
+ argv.slice(0, format + 1).some((arg) => arg.includes("NUMERICSTATUSVALUE"))))
2382
+ );
2383
+ })
2384
+ )
2385
+ return true;
2386
+ }
2387
+ for (const words of parsed.pipelines.flat()) {
2388
+ if (
2389
+ words.some((arg) => arg.includes("ENVTESTVALUE")) &&
2390
+ !isApprovedEnvironmentPredicate(unwrapStaticCommand(words))
2391
+ )
2392
+ return true;
2393
+ }
2185
2394
  for (const words of parsed.pipelines.flat()) {
2186
2395
  if (!words.some((arg) => arg.includes("UNKNOWNVALUE"))) continue;
2187
2396
  if (words.some((arg) => arg.includes("UNKNOWNVALUEUNQUOTED"))) return true;
@@ -2192,7 +2401,7 @@ function hasEnvironmentVariableExpansionRead(
2192
2401
  )
2193
2402
  return true;
2194
2403
  }
2195
- if (!localMaterialized.includes("ENVVALUE")) {
2404
+ if (!localMaterialized.includes("ENVVALUE") && !localMaterialized.includes("ENVTESTVALUE")) {
2196
2405
  return (
2197
2406
  hasSensitiveFileRead(localMaterialized, isPathBlocked) ||
2198
2407
  hasSensitivePathRedirect(localMaterialized, isPathBlocked) ||
@@ -2202,7 +2411,7 @@ function hasEnvironmentVariableExpansionRead(
2202
2411
 
2203
2412
  for (const [index, pipeline] of parsed.pipelines.entries()) {
2204
2413
  const hasEnvironmentValue = pipeline.some((argv) =>
2205
- argv.some((arg) => arg.includes("ENVVALUE")),
2414
+ argv.some((arg) => arg.includes("ENVVALUE") || arg.includes("ENVTESTVALUE")),
2206
2415
  );
2207
2416
  if (!hasEnvironmentValue) continue;
2208
2417
  if (parsed.redirects.some((redirect) => redirect.pipeline === index)) return true;
@@ -2210,9 +2419,14 @@ function hasEnvironmentVariableExpansionRead(
2210
2419
  pipeline.some((argv) => {
2211
2420
  const unwrapped = unwrapStaticCommand(argv);
2212
2421
  const name = unwrapped[0]?.split("/").at(-1) ?? "";
2213
- if (argv.some((arg) => arg.includes("ENVVALUE"))) {
2214
- if (argv.slice(0, argv.length - unwrapped.length).some((arg) => arg.includes("ENVVALUE")))
2422
+ if (argv.some((arg) => arg.includes("ENVVALUE") || arg.includes("ENVTESTVALUE"))) {
2423
+ if (
2424
+ argv
2425
+ .slice(0, argv.length - unwrapped.length)
2426
+ .some((arg) => arg.includes("ENVVALUE") || arg.includes("ENVTESTVALUE"))
2427
+ )
2215
2428
  return true;
2429
+ if (isApprovedEnvironmentPredicate(unwrapped)) return false;
2216
2430
  if (isNavigationOperand(unwrapped, "ENVVALUE")) {
2217
2431
  return unwrapped.some((arg) => arg.includes("ENVVALUEUNQUOTED"));
2218
2432
  }
@@ -2239,6 +2453,24 @@ function hasEnvironmentVariableExpansionRead(
2239
2453
  return hasSensitiveFileRead(materialized, isPathBlocked);
2240
2454
  }
2241
2455
 
2456
+ /** Quoted approved metadata can be compared or checked for blankness, never evaluated. */
2457
+ function isApprovedEnvironmentPredicate(argv: string[]): boolean {
2458
+ let args = argv.slice(1);
2459
+ if (argv[0] === "[") {
2460
+ if (args.at(-1) !== "]") return false;
2461
+ args = args.slice(0, -1);
2462
+ } else if (argv[0] !== "test") return false;
2463
+ const value = (arg: string | undefined) => arg === "ENVVALUE" || arg === "ENVTESTVALUE";
2464
+ const literal = (arg: string | undefined) =>
2465
+ /^[A-Za-z0-9_.:-]*$/.test(arg ?? "") && arg !== undefined;
2466
+ return (
2467
+ (args.length === 2 && ["-n", "-z"].includes(args[0] ?? "") && value(args[1])) ||
2468
+ (args.length === 3 &&
2469
+ args[1] === "=" &&
2470
+ ((value(args[0]) && literal(args[2])) || (literal(args[0]) && value(args[2]))))
2471
+ );
2472
+ }
2473
+
2242
2474
  function mentionsEnvironmentSource(command: string): boolean {
2243
2475
  return /\b(?:os\s*\.\s*(?:environ|getenv)|process\s*\.\s*env|Environment\s*\.\s*GetEnvironmentVariable)\b/.test(
2244
2476
  command,