@blogic-cz/agent-tools 1.6.11 → 1.6.12

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.
@@ -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;AA2uFF,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,CA4VrF;AAQD,kEAAkE;AAClE,eAAO,MAAM,uBAAuB,UAjnGD,SAAS,UAAU,UAAU,KAAK,IAinGM,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,aAAa,YAnnGC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAmnGjB,CAAC;AAExD,yEAAyE;AACzE,eAAO,MAAM,aAAa,aArnGE,MAAM,KAAK,OAqnGgB,CAAC;AAExD,yDAAyD;AACzD,eAAO,MAAM,aAAa,aAvnGE,MAAM,KAAK,OAunGgB,CAAC;AAExD,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,YAznGC,MAAM,KAAK,OAynG0B,CAAC;AAE1E,iDAAiD;AACjD,eAAO,MAAM,iBAAiB,YA3nGC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IA2nGf,CAAC;AAEhE,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,YA7nGC,MAAM,KAAK,OA6nGsB,CAAC;AAElE,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,YA/nGC,MAAM,KAAK,MAAM,GAAG,IA+nGa,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;AAg4FF,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,CAoWrF;AAQD,kEAAkE;AAClE,eAAO,MAAM,uBAAuB,UA9wGD,SAAS,UAAU,UAAU,KAAK,IA8wGM,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,aAAa,YAhxGC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAgxGjB,CAAC;AAExD,yEAAyE;AACzE,eAAO,MAAM,aAAa,aAlxGE,MAAM,KAAK,OAkxGgB,CAAC;AAExD,yDAAyD;AACzD,eAAO,MAAM,aAAa,aApxGE,MAAM,KAAK,OAoxGgB,CAAC;AAExD,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,YAtxGC,MAAM,KAAK,OAsxG0B,CAAC;AAE1E,iDAAiD;AACjD,eAAO,MAAM,iBAAiB,YAxxGC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAwxGf,CAAC;AAEhE,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,YA1xGC,MAAM,KAAK,OA0xGsB,CAAC;AAElE,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,YA5xGC,MAAM,KAAK,MAAM,GAAG,IA4xGa,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "1.6.11",
3
+ "version": "1.6.12",
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",
@@ -226,11 +226,13 @@ function isSafeHerdrTabCreate(argv: string[]): boolean {
226
226
  function hasSafeLocalAssignmentCommands(
227
227
  command: string,
228
228
  assignments: Map<string, { value: string; end: number }>,
229
+ statusCaptures: Set<string>,
229
230
  ): boolean {
230
231
  if (!assignments.size) return true;
231
232
  const parsed = parseStaticShellCommands(command);
232
233
  if (!parsed || typeof parsed === "string") return false;
233
234
  return parsed.pipelines.flat().every((words) => {
235
+ if (words.length === 1 && statusCaptures.has(words[0] ?? "")) return true;
234
236
  const assignment = /^([A-Za-z_][A-Za-z0-9_]*)=([A-Za-z0-9_./-]+)$/.exec(words[0] ?? "");
235
237
  if (assignment) {
236
238
  return (
@@ -1890,6 +1892,37 @@ function isStaticNavigationCommand(argv: string[]): boolean {
1890
1892
  );
1891
1893
  }
1892
1894
 
1895
+ /** Literal wrappers and exact directory-operation roles; unknown consumers keep refusal. */
1896
+ function isProvedDirectoryCommand(words: string[]): boolean {
1897
+ const argv = unwrapStaticCommand(words);
1898
+ if (
1899
+ !words
1900
+ .slice(0, words.length - argv.length)
1901
+ .every((word) => ["rtk", "proxy", "command", "--"].includes(word))
1902
+ )
1903
+ return false;
1904
+ if (argv[0] === "cd") return isStaticNavigationCommand(argv);
1905
+ if (argv[0] !== "git") return false;
1906
+ let index = 1;
1907
+ while (argv[index] === "-C") {
1908
+ if (!isStaticNavigationCommand(["cd", argv[index + 1] ?? ""])) return false;
1909
+ index += 2;
1910
+ }
1911
+ const args = argv.slice(index);
1912
+ if (args[0] === "status")
1913
+ return args.length === 1 || (args.length === 2 && args[1] === "--short");
1914
+ if (args[0] === "fetch")
1915
+ return (
1916
+ args.length >= 2 &&
1917
+ args.length <= 3 &&
1918
+ args.slice(1).every((arg) => /^[A-Za-z0-9_][A-Za-z0-9_./-]*$/.test(arg))
1919
+ );
1920
+ return (
1921
+ (args.length === 2 && args[0] === "rev-parse" && args[1] === "HEAD") ||
1922
+ (args.length === 3 && args[0] === "rev-list" && args[1] === "--count" && args[2] === "HEAD")
1923
+ );
1924
+ }
1925
+
1893
1926
  /** Prove a single cd whose uses are gated on success, without simulating shell state. */
1894
1927
  function hasProvedNavigationSequence(command: string): boolean {
1895
1928
  const literal = literalBindingText(command, "", "");
@@ -2279,6 +2312,7 @@ function hasSensitiveFileRead(command: string, isPathBlocked: (path: string) =>
2279
2312
  if (parsed && parsed !== "brace-expansion") {
2280
2313
  let directory = "";
2281
2314
  const trackDirectory = hasProvedNavigationSequence(command);
2315
+ const safeDirectorySequence = parsed.pipelines.flat().every(isProvedDirectoryCommand);
2282
2316
  return parsed.pipelines.flat().some((words) => {
2283
2317
  const argv = unwrapStaticCommand(words);
2284
2318
  const inspectedArgv =
@@ -2291,19 +2325,36 @@ function hasSensitiveFileRead(command: string, isPathBlocked: (path: string) =>
2291
2325
  path.startsWith("/") ? posix.normalize(path) : posix.join(commandDirectory, path);
2292
2326
  const blockedOperand = (path: string) =>
2293
2327
  [path, inDirectory(path)].some((value) => isPathBlocked(value) || isSensitivePath(value));
2294
- const blockedDirectory = (path: string) => blockedOperand(path) || blockedOperand(path + "/");
2328
+ // Directory slots use path policies, not the broad credential-file name heuristic.
2329
+ const blockedDirectory = (path: string) =>
2330
+ [path, inDirectory(path)].some(
2331
+ (value) => isPathBlocked(value) || isPathBlocked(value + "/"),
2332
+ );
2295
2333
  if (name === "cd") {
2296
2334
  const path = inspected[inspected[1] === "--" ? 2 : 1];
2297
2335
  if (path === undefined) return false;
2298
2336
  if (blockedDirectory(path)) return true;
2337
+ // A successful cd alone proves neither its later readers nor pipeline consumers safe.
2338
+ if (
2339
+ (!trackDirectory || !safeDirectorySequence) &&
2340
+ (blockedOperand(path) || blockedOperand(path + "/"))
2341
+ )
2342
+ return true;
2299
2343
  if (trackDirectory && isStaticNavigationCommand(inspected)) directory = inDirectory(path);
2300
2344
  return false;
2301
2345
  }
2302
2346
  if (name === "git") {
2303
2347
  let index = 1;
2304
2348
  while (inspected[index] === "-C" && inspected[index + 1]) {
2305
- if (blockedDirectory(inspected[index + 1] ?? "")) return true;
2306
- commandDirectory = inDirectory(inspected[index + 1] ?? "");
2349
+ const path = inspected[index + 1] ?? "";
2350
+ if (blockedDirectory(path)) return true;
2351
+ // Git -C affects this command only; following consumers keep the shell directory.
2352
+ if (
2353
+ !isProvedDirectoryCommand(words) &&
2354
+ (blockedOperand(path) || blockedOperand(path + "/"))
2355
+ )
2356
+ return true;
2357
+ commandDirectory = inDirectory(path);
2307
2358
  index += 2;
2308
2359
  }
2309
2360
  // Git grep has the same pattern-versus-file roles as grep below.
@@ -2483,6 +2534,7 @@ function hasEnvironmentVariableExpansionRead(
2483
2534
  command: string,
2484
2535
  allowedNames: Set<string>,
2485
2536
  isPathBlocked: (path: string) => boolean,
2537
+ onProvedStatus?: (command: string) => void,
2486
2538
  ): boolean {
2487
2539
  let heredoc = boundedHeredoc(command);
2488
2540
  while (heredoc) {
@@ -2494,8 +2546,15 @@ function hasEnvironmentVariableExpansionRead(
2494
2546
  if (!command.includes("$") && !command.includes("`")) return false;
2495
2547
  let invalidExpansion = false;
2496
2548
  const assignments = getLeadingLiteralAssignments(command);
2497
- const localValues = new Map<string, string>();
2498
2549
  let localIndex = 0;
2550
+ const directCaptures = new Map<string, string>();
2551
+ const statusReferences = new Map<string, { name: string; fallback: string }>();
2552
+ const marker = (prefix: string): string => {
2553
+ let value: string;
2554
+ do value = `${prefix}${localIndex++}END`;
2555
+ while (command.includes(value));
2556
+ return value;
2557
+ };
2499
2558
  let expansions = 0;
2500
2559
  let normalized = "";
2501
2560
  let quote: "'" | '"' | undefined;
@@ -2534,7 +2593,18 @@ function hasEnvironmentVariableExpansionRead(
2534
2593
  }
2535
2594
  if (command[i + 1] === "?") {
2536
2595
  expansions++;
2537
- normalized += "NUMERICSTATUSVALUE";
2596
+ const value = marker("NUMERICSTATUSVALUE");
2597
+ // Only an exact standalone assignment token captures in the parent shell.
2598
+ // Quoting/escaping the name can instead produce an executable command word.
2599
+ const capture = /(?:^|[;\n])[ \t]*([A-Za-z_][A-Za-z0-9_]*)=("?)$/.exec(command.slice(0, i));
2600
+ const ending = capture?.[2] === '"' ? '"' : "";
2601
+ if (
2602
+ capture &&
2603
+ command.slice(i + 2).startsWith(ending) &&
2604
+ /^[ \t]*(?:[;\n]|$)/.test(command.slice(i + 2 + ending.length))
2605
+ )
2606
+ directCaptures.set(value, capture[1] ?? "");
2607
+ normalized += value;
2538
2608
  i++;
2539
2609
  continue;
2540
2610
  }
@@ -2558,26 +2628,101 @@ function hasEnvironmentVariableExpansionRead(
2558
2628
  }
2559
2629
  const assignment = assignments.get(name);
2560
2630
  if (assignment && assignment.end >= i) invalidExpansion = true;
2561
- if (assignment && assignment.end < i) {
2562
- const marker = `LOCALVALUE${localIndex++}`;
2563
- localValues.set(marker, assignment.value);
2564
- normalized += marker;
2565
- } else if (allowedNames.has(name)) {
2566
- normalized += quote ? "ENVVALUE" : "ENVVALUEUNQUOTED";
2567
- } else {
2568
- normalized += quote ? "UNKNOWNVALUE" : "UNKNOWNVALUEUNQUOTED";
2569
- }
2631
+ const reference = marker("STATUSREFERENCE");
2632
+ const fallback =
2633
+ assignment && assignment.end < i
2634
+ ? assignment.value
2635
+ : allowedNames.has(name)
2636
+ ? quote
2637
+ ? "ENVVALUE"
2638
+ : "ENVVALUEUNQUOTED"
2639
+ : quote
2640
+ ? "UNKNOWNVALUE"
2641
+ : "UNKNOWNVALUEUNQUOTED";
2642
+ statusReferences.set(reference, { name, fallback });
2643
+ normalized += reference;
2570
2644
  i += variable[0].length - 1;
2571
2645
  }
2572
2646
  if (!expansions) return invalidExpansion;
2573
2647
  if (invalidExpansion) return true;
2574
2648
 
2575
- const localMaterialized = [...localValues].reduce(
2576
- (text, [marker, value]) => text.replaceAll(marker, () => value),
2577
- normalized,
2578
- );
2649
+ // Resolve every reference at its command position; a later capture cannot prove
2650
+ // an earlier read, and only proved passive intermediates preserve the binding.
2651
+ const initial = parseStaticShellCommands(normalized);
2652
+ if (!initial || typeof initial === "string") return true;
2653
+ const captures = new Map<string, string>();
2654
+ for (const words of initial.pipelines.flat()) {
2655
+ const capture = /^([A-Za-z_][A-Za-z0-9_]*)=(.+)$/.exec(words[0] ?? "");
2656
+ if (capture && directCaptures.get(capture[2] ?? "") === capture[1])
2657
+ captures.set(words[0] ?? "", capture[1] ?? "");
2658
+ }
2659
+ const statusCaptures = new Set<string>();
2660
+ const localStatuses = new Set<string>();
2661
+ const resolutions = new Map<string, string>();
2662
+ const capturedNames = new Set(captures.values());
2663
+ const bound = new Set<string>();
2664
+ if (captures.size) {
2665
+ const syntax = literalBindingText(normalized, "", "")?.syntax;
2666
+ if (syntax === undefined || /&&|\|\||&/.test(syntax.replace(/[<>]&[0-9-]+/g, ""))) return true;
2667
+ }
2668
+ for (const [index, pipeline] of initial.pipelines.entries()) {
2669
+ for (const words of pipeline) {
2670
+ for (const arg of words) {
2671
+ for (const [reference, { name, fallback }] of statusReferences) {
2672
+ if (!arg.includes(reference)) continue;
2673
+ if (bound.has(name)) {
2674
+ const value = marker("NUMERICSTATUSVALUE");
2675
+ localStatuses.add(value);
2676
+ resolutions.set(reference, value);
2677
+ } else {
2678
+ if (capturedNames.has(name)) return true;
2679
+ resolutions.set(reference, fallback);
2680
+ }
2681
+ }
2682
+ }
2683
+ const capture = captures.get(words[0] ?? "");
2684
+ if (capture) {
2685
+ if (
2686
+ pipeline.length !== 1 ||
2687
+ words.length !== 1 ||
2688
+ initial.redirects.some((redirect) => redirect.pipeline === index) ||
2689
+ // Inherited export attributes can make a numeric value select code or paths.
2690
+ /^(?:LD_|DYLD_|_RLD_|LDR_|GIT_CONFIG_(?:KEY|VALUE)_)/.test(capture) ||
2691
+ /^(?:PATH|IFS|CDPATH|BASH_ENV|ENV|FPATH|ZDOTDIR|SHELLOPTS|BASHOPTS|PROMPT_COMMAND|PS[0-4]|GLOBIGNORE|RANDOM|SECONDS|PIPESTATUS|UID|EUID|BASH_XTRACEFD|HOME|XDG_CONFIG_HOME|LIBPATH|SHLIB_PATH|NODE_OPTIONS|NODE_PATH|BUN_OPTIONS|BUN_PRELOAD|PYTHONPATH|PYTHONSTARTUP|PYTHONHOME|RUBYOPT|RUBYLIB|GEM_HOME|GEM_PATH|PERL5OPT|PERL5LIB|PERLLIB|JAVA_TOOL_OPTIONS|JDK_JAVA_OPTIONS|CLASSPATH|_JAVA_OPTIONS|JAVA_HOME|GIT_SSH_COMMAND|GIT_SSH|GIT_PROXY_COMMAND|GIT_EXEC_PATH|GIT_CONFIG|GIT_CONFIG_GLOBAL|GIT_CONFIG_SYSTEM|GIT_CONFIG_PARAMETERS|GIT_CONFIG_COUNT|GIT_ASKPASS|SSH_ASKPASS|GIT_EDITOR|GIT_SEQUENCE_EDITOR|GIT_PAGER|PAGER|EDITOR|VISUAL)$/.test(
2692
+ capture,
2693
+ )
2694
+ )
2695
+ return true;
2696
+ bound.add(capture);
2697
+ statusCaptures.add(words[0] ?? "");
2698
+ continue;
2699
+ }
2700
+ const argv = unwrapStaticCommand(words);
2701
+ const name = argv[0]?.split("/").at(-1) ?? "";
2702
+ if (
2703
+ captures.size &&
2704
+ /^(?:alias|unalias|readonly|declare|typeset|local|export|eval|source|\.|trap|set|read|unset|let)$/.test(
2705
+ name,
2706
+ )
2707
+ )
2708
+ return true;
2709
+ if (!isPassiveTextCommand(argv) && name !== "cat") bound.clear();
2710
+ }
2711
+ for (const redirect of initial.redirects.filter((value) => value.pipeline === index)) {
2712
+ if (
2713
+ [...statusReferences].some(
2714
+ ([reference, { name }]) => capturedNames.has(name) && redirect.target.includes(reference),
2715
+ )
2716
+ )
2717
+ return true;
2718
+ }
2719
+ }
2720
+ for (const [reference, { fallback }] of statusReferences) {
2721
+ normalized = normalized.replaceAll(reference, () => resolutions.get(reference) ?? fallback);
2722
+ }
2723
+ const localMaterialized = normalized;
2579
2724
  // Options must be checked after local literals become actual argv, never as placeholders.
2580
- if (!hasSafeLocalAssignmentCommands(localMaterialized, assignments)) return true;
2725
+ if (!hasSafeLocalAssignmentCommands(localMaterialized, assignments, statusCaptures)) return true;
2581
2726
  const parsed = parseStaticShellCommands(localMaterialized);
2582
2727
  if (!parsed || typeof parsed === "string") return true;
2583
2728
  for (const [index, pipeline] of parsed.pipelines.entries()) {
@@ -2586,6 +2731,7 @@ function hasEnvironmentVariableExpansionRead(
2586
2731
  if (parsed.redirects.some((redirect) => redirect.pipeline === index)) return true;
2587
2732
  if (
2588
2733
  pipeline.some((words) => {
2734
+ if (words.length === 1 && statusCaptures.has(words[0] ?? "")) return false;
2589
2735
  const argv = unwrapStaticCommand(words);
2590
2736
  if (!words.some((arg) => arg.includes("NUMERICSTATUSVALUE")))
2591
2737
  return !isPassiveTextCommand(argv);
@@ -2594,6 +2740,9 @@ function hasEnvironmentVariableExpansionRead(
2594
2740
  return (
2595
2741
  !["echo", "printf"].includes(name ?? "") ||
2596
2742
  !isPassiveTextCommand(argv) ||
2743
+ argv.some(
2744
+ (arg) => arg.startsWith("-") && [...localStatuses].some((value) => arg.includes(value)),
2745
+ ) ||
2597
2746
  words
2598
2747
  .slice(0, words.length - argv.length)
2599
2748
  .some((arg) => arg.includes("NUMERICSTATUSVALUE")) ||
@@ -2617,11 +2766,12 @@ function hasEnvironmentVariableExpansionRead(
2617
2766
  return true;
2618
2767
  }
2619
2768
  if (!localMaterialized.includes("ENVVALUE")) {
2620
- return (
2769
+ const blocked =
2621
2770
  hasSensitiveFileRead(localMaterialized, isPathBlocked) ||
2622
2771
  hasSensitivePathRedirect(localMaterialized, isPathBlocked) ||
2623
- hasEnvironmentRead(localMaterialized, allowedNames)
2624
- );
2772
+ hasEnvironmentRead(localMaterialized, allowedNames);
2773
+ if (!blocked && statusCaptures.size) onProvedStatus?.(localMaterialized);
2774
+ return blocked;
2625
2775
  }
2626
2776
 
2627
2777
  for (const [index, pipeline] of parsed.pipelines.entries()) {
@@ -2657,11 +2807,10 @@ function hasEnvironmentVariableExpansionRead(
2657
2807
  return true;
2658
2808
  }
2659
2809
  }
2660
- const materialized = [...localValues].reduce(
2661
- (text, [marker, value]) => text.replaceAll(marker, () => value),
2662
- normalized.replaceAll("ENVVALUE", "SAFEENVVALUE"),
2663
- );
2664
- return hasSensitiveFileRead(materialized, isPathBlocked);
2810
+ const materialized = normalized.replaceAll("ENVVALUE", "SAFEENVVALUE");
2811
+ const blocked = hasSensitiveFileRead(materialized, isPathBlocked);
2812
+ if (!blocked && statusCaptures.size) onProvedStatus?.(materialized);
2813
+ return blocked;
2665
2814
  }
2666
2815
 
2667
2816
  /** Quoted approved metadata can be compared or checked for blankness, never evaluated. */
@@ -2999,16 +3148,24 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
2999
3148
  hasSensitivePathRedirect(command, isPathBlocked)
3000
3149
  )
3001
3150
  return "blocked by the file-access policy; a sensitive path matched or this command form could not be verified";
3151
+ let environmentCommand = command;
3002
3152
  if (
3003
3153
  !isLiteralTextWrite(command) &&
3004
- hasEnvironmentVariableExpansionRead(command, allowedEnvironmentVariables, isPathBlocked)
3154
+ hasEnvironmentVariableExpansionRead(
3155
+ command,
3156
+ allowedEnvironmentVariables,
3157
+ isPathBlocked,
3158
+ (proved) => {
3159
+ environmentCommand = proved;
3160
+ },
3161
+ )
3005
3162
  ) {
3006
3163
  return "expands an unapproved or executable environment variable value";
3007
3164
  }
3008
3165
  if (
3009
3166
  hasEnvironmentSourceAccess(command, rawEnvironmentSource, allowedEnvironmentVariables) ||
3010
3167
  hasEnvironmentRead(
3011
- command,
3168
+ environmentCommand,
3012
3169
  allowedEnvironmentVariables,
3013
3170
  allowPackageInventory,
3014
3171
  shellBodies.filter((body) => {