@blogic-cz/agent-tools 1.6.9 → 1.6.11

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
@@ -654,9 +654,9 @@ Claude Code uses shell command hooks. The package ships a ready-made wrapper scr
654
654
 
655
655
  That's it. The hook reads tool input from stdin, runs the guard, and exits with code 2 (blocked + reason on stderr) or 0 (allowed).
656
656
 
657
- The guard permits static `printenv` reads and simple `echo`/`printf` expansions only for exact names in `credentialGuard.allowedEnvironmentVariables`. The default list is empty. Multiple names are allowed only when each name is approved. Approved environment values may be displayed, but redirecting their pipeline or passing them to an executor stays blocked.
657
+ The guard permits static `printenv` reads and simple `echo`/`printf` expansions only for exact names in `credentialGuard.allowedEnvironmentVariables`. Quoted empty defaults such as `"${NAME:-}"` follow the same display rules, including `printf` data arguments with a literal format; unquoted defaults, nonempty defaults, and other parameter operators stay blocked. The default list is empty. Multiple names are allowed only when each name is approved. Approved environment values may be displayed, but redirecting their pipeline or passing them to an executor stays blocked.
658
658
 
659
- Python inline programs and quoted stdin heredocs can also display approved values through `os.getenv("NAME")`, `os.environ.get("NAME", "default")`, or `os.environ["NAME"]`. The complete program must be `import os` followed by `print(...)` statements using these reads and plain string labels or defaults. Dynamic names, aliases, escaped or interpolated strings, additional statements, and whole-environment output remain unsupported. The same redirection and pipeline restrictions apply.
659
+ Python inline programs and quoted stdin heredocs can also display approved values through `os.getenv("NAME")`, `os.environ.get("NAME", "default")`, or `os.environ["NAME"]`. The complete program must import unaliased `os`, plus `json` for JSON display, followed by `print(...)` statements. `json.dumps(...)` supports dictionaries and lists of approved reads, including comprehensions over a nonempty literal list of approved names. Every dictionary key and environment read must use an approved name. Each projection has at most 16 entries; the program has at most 65,536 characters. Computed or unbounded names, aliases, escaped or interpolated strings, additional statements, and whole-environment output remain unsupported. The same redirection and pipeline restrictions apply.
660
660
 
661
661
  Literal assignments such as `F=README.md; cat "$F"` work in supported static commands. Bounded proofs also support a literal `for` loop over a fixed list, a single ASCII-letter path binding such as `cd PATH && A=docs/report.md && shasum -a 256 "$A"` or `F=README.md; cat "$F"`, including the existing `git add -- "$T/file"` form, and literal path braces such as `rg -n -i PATTERN src/{a,b} docs/{guide,api}.md`. Path braces work with supported passive text commands and `cat`, and support one unquoted comma list per path, at most 16 members per list and 32 generated paths per command, within 64 KiB. Nested braces, ranges, empty members, mixed quoting, command positions, and compound commands remain unsupported. Path bindings accept literal values and `;`, newline, or `&&` sequencing; a binding conditional on successful `cd` is proved only for uses in that same `&&` list. Their uses must be supported read-only file operands or explicit `git add --` paths. Rebinding, executable operands, startup-variable names, and uncertain scopes cannot establish this proof. Loop binders are a single lowercase letter or `T`, and their uses must occupy supported data operands. Every body command and expanded path is checked; unsupported control flow, mutations such as `printf -v`, and unknown command roles cannot establish the proof. Other variable assignments and brace expansions retain conservative refusal. Fully quoted literal braces remain usable in static searches.
662
662
 
@@ -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;AA+oFF,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,UArhGD,SAAS,UAAU,UAAU,KAAK,IAqhGM,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,aAAa,YAvhGC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAuhGjB,CAAC;AAExD,yEAAyE;AACzE,eAAO,MAAM,aAAa,aAzhGE,MAAM,KAAK,OAyhGgB,CAAC;AAExD,yDAAyD;AACzD,eAAO,MAAM,aAAa,aA3hGE,MAAM,KAAK,OA2hGgB,CAAC;AAExD,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,YA7hGC,MAAM,KAAK,OA6hG0B,CAAC;AAE1E,iDAAiD;AACjD,eAAO,MAAM,iBAAiB,YA/hGC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IA+hGf,CAAC;AAEhE,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,YAjiGC,MAAM,KAAK,OAiiGsB,CAAC;AAElE,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,YAniGC,MAAM,KAAK,MAAM,GAAG,IAmiGa,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;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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blogic-cz/agent-tools",
3
- "version": "1.6.9",
3
+ "version": "1.6.11",
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",
@@ -1608,8 +1608,10 @@ function isAllowedPythonEnvironmentRead(argv: string[], allowedNames: Set<string
1608
1608
  return false;
1609
1609
 
1610
1610
  const program = argv[index] ?? "";
1611
+ if (program.length > 65_536) return false;
1611
1612
  let position = 0;
1612
1613
  let foundRead = false;
1614
+ let importedJson = false;
1613
1615
  const skipInlineWhitespace = () => {
1614
1616
  while (/[\t ]/.test(program[position] ?? "")) position++;
1615
1617
  };
@@ -1619,7 +1621,12 @@ function isAllowedPythonEnvironmentRead(argv: string[], allowedNames: Set<string
1619
1621
  return true;
1620
1622
  };
1621
1623
  const takeIdentifier = (value: string): boolean => {
1622
- if (!take(value) || /[A-Za-z0-9_]/.test(program[position] ?? "")) return false;
1624
+ if (
1625
+ !program.startsWith(value, position) ||
1626
+ /[A-Za-z0-9_]/.test(program[position + value.length] ?? "")
1627
+ )
1628
+ return false;
1629
+ position += value.length;
1623
1630
  return true;
1624
1631
  };
1625
1632
  const takeString = (): string | undefined => {
@@ -1638,8 +1645,14 @@ function isAllowedPythonEnvironmentRead(argv: string[], allowedNames: Set<string
1638
1645
  position++;
1639
1646
  return value;
1640
1647
  };
1641
- const takeEnvironmentRead = (): boolean => {
1648
+ const takeEnvironmentRead = (variable?: string): boolean => {
1642
1649
  skipInlineWhitespace();
1650
+ const takeName = (): boolean => {
1651
+ skipInlineWhitespace();
1652
+ if (variable !== undefined) return takeIdentifier(variable);
1653
+ const name = takeString();
1654
+ return name !== undefined && allowedNames.has(name);
1655
+ };
1643
1656
  const start = position;
1644
1657
  if (!takeIdentifier("os")) return false;
1645
1658
  if (!take(".")) {
@@ -1648,8 +1661,7 @@ function isAllowedPythonEnvironmentRead(argv: string[], allowedNames: Set<string
1648
1661
  }
1649
1662
  if (takeIdentifier("getenv")) {
1650
1663
  if (!take("(")) return false;
1651
- const name = takeString();
1652
- if (name === undefined || !allowedNames.has(name)) return false;
1664
+ if (!takeName()) return false;
1653
1665
  skipInlineWhitespace();
1654
1666
  if (take(",")) {
1655
1667
  if (takeString() === undefined) return false;
@@ -1662,8 +1674,7 @@ function isAllowedPythonEnvironmentRead(argv: string[], allowedNames: Set<string
1662
1674
  if (!takeIdentifier("environ")) return false;
1663
1675
  if (take(".")) {
1664
1676
  if (!takeIdentifier("get") || !take("(")) return false;
1665
- const name = takeString();
1666
- if (name === undefined || !allowedNames.has(name)) return false;
1677
+ if (!takeName()) return false;
1667
1678
  skipInlineWhitespace();
1668
1679
  if (take(",")) {
1669
1680
  if (takeString() === undefined) return false;
@@ -1674,15 +1685,91 @@ function isAllowedPythonEnvironmentRead(argv: string[], allowedNames: Set<string
1674
1685
  return true;
1675
1686
  }
1676
1687
  if (!take("[")) return false;
1677
- const name = takeString();
1678
- if (name === undefined || !allowedNames.has(name)) return false;
1688
+ if (!takeName()) return false;
1679
1689
  skipInlineWhitespace();
1680
1690
  if (!take("]")) return false;
1681
1691
  foundRead = true;
1682
1692
  return true;
1683
1693
  };
1694
+ const takeApprovedNames = (): boolean => {
1695
+ if (!take("[")) return false;
1696
+ for (let count = 0; count < 16; count++) {
1697
+ const name = takeString();
1698
+ if (name === undefined || !allowedNames.has(name)) return false;
1699
+ skipInlineWhitespace();
1700
+ if (take("]")) return true;
1701
+ if (!take(",")) return false;
1702
+ skipInlineWhitespace();
1703
+ if (take("]")) return true;
1704
+ }
1705
+ return false;
1706
+ };
1707
+ const takeComprehension = (): boolean => {
1708
+ const dictionary = take("{");
1709
+ if (!dictionary && !take("[")) return false;
1710
+ skipInlineWhitespace();
1711
+ const variable = dictionary
1712
+ ? /^[A-Za-z_][A-Za-z0-9_]*/.exec(program.slice(position))?.[0]
1713
+ : /^os\.(?:getenv\(|environ(?:\.get\(|\[))[\t ]*([A-Za-z_][A-Za-z0-9_]*)/.exec(
1714
+ program.slice(position),
1715
+ )?.[1];
1716
+ if (!variable || ["os", "json", "print"].includes(variable)) return false;
1717
+ if (dictionary) {
1718
+ if (!takeIdentifier(variable)) return false;
1719
+ skipInlineWhitespace();
1720
+ if (!take(":")) return false;
1721
+ }
1722
+ if (!takeEnvironmentRead(variable)) return false;
1723
+ const loop = new RegExp(`[\\t ]+for[\\t ]+${variable}[\\t ]+in[\\t ]+`, "y");
1724
+ loop.lastIndex = position;
1725
+ const match = loop.exec(program);
1726
+ if (!match) return false;
1727
+ position = loop.lastIndex;
1728
+ if (!takeApprovedNames()) return false;
1729
+ skipInlineWhitespace();
1730
+ return take(dictionary ? "}" : "]");
1731
+ };
1732
+ const takeProjection = (): boolean => {
1733
+ const start = position;
1734
+ const previousRead = foundRead;
1735
+ if (takeComprehension()) return true;
1736
+ position = start;
1737
+ foundRead = previousRead;
1738
+ const dictionary = take("{");
1739
+ if (!dictionary && !take("[")) return false;
1740
+ const close = dictionary ? "}" : "]";
1741
+ for (let count = 0; count < 16; count++) {
1742
+ if (dictionary) {
1743
+ const key = takeString();
1744
+ if (key === undefined || !allowedNames.has(key)) return false;
1745
+ skipInlineWhitespace();
1746
+ if (!take(":")) return false;
1747
+ }
1748
+ if (!takeEnvironmentRead()) return false;
1749
+ skipInlineWhitespace();
1750
+ if (take(close)) return true;
1751
+ if (!take(",")) return false;
1752
+ skipInlineWhitespace();
1753
+ if (take(close)) return true;
1754
+ }
1755
+ return false;
1756
+ };
1757
+ const takeJsonDisplay = (): boolean => {
1758
+ if (!importedJson || !takeIdentifier("json") || !take(".dumps")) return false;
1759
+ skipInlineWhitespace();
1760
+ if (!take("(")) return false;
1761
+ skipInlineWhitespace();
1762
+ if (!takeProjection()) return false;
1763
+ skipInlineWhitespace();
1764
+ return take(")");
1765
+ };
1684
1766
  const takeExpression = (): boolean => {
1685
- const takeTerm = (): boolean => takeString() !== undefined || takeEnvironmentRead();
1767
+ const takeTerm = (): boolean => {
1768
+ skipInlineWhitespace();
1769
+ return program.startsWith("json", position)
1770
+ ? takeJsonDisplay()
1771
+ : takeString() !== undefined || takeEnvironmentRead();
1772
+ };
1686
1773
  if (!takeTerm()) return false;
1687
1774
  skipInlineWhitespace();
1688
1775
  while (take("+")) {
@@ -1718,10 +1805,26 @@ function isAllowedPythonEnvironmentRead(argv: string[], allowedNames: Set<string
1718
1805
  skipInlineWhitespace();
1719
1806
  if (!takeIdentifier("import")) return false;
1720
1807
  skipInlineWhitespace();
1721
- if (!takeIdentifier("os")) return false;
1808
+ const jsonFirst = takeIdentifier("json");
1809
+ if (!jsonFirst && !takeIdentifier("os")) return false;
1810
+ importedJson = jsonFirst;
1811
+ skipInlineWhitespace();
1812
+ if (take(",")) {
1813
+ skipInlineWhitespace();
1814
+ if (!takeIdentifier(jsonFirst ? "os" : "json")) return false;
1815
+ importedJson = true;
1816
+ } else if (jsonFirst) return false;
1722
1817
  skipInlineWhitespace();
1723
1818
  if (!(take(";") || take("\n") || take("\r\n"))) return false;
1724
1819
  skipSeparators();
1820
+ if (!importedJson && takeIdentifier("import")) {
1821
+ skipInlineWhitespace();
1822
+ if (!takeIdentifier("json")) return false;
1823
+ importedJson = true;
1824
+ skipInlineWhitespace();
1825
+ if (!(take(";") || take("\n") || take("\r\n"))) return false;
1826
+ skipSeparators();
1827
+ }
1725
1828
  while (position < program.length) {
1726
1829
  if (!takePrint()) return false;
1727
1830
  skipInlineWhitespace();
@@ -2449,7 +2552,7 @@ function hasEnvironmentVariableExpansionRead(
2449
2552
  const name = variable[1] ?? variable[3] ?? "";
2450
2553
  if (variable[2]) {
2451
2554
  if (quote !== '"' || !allowedNames.has(name)) invalidExpansion = true;
2452
- normalized += "ENVTESTVALUE";
2555
+ normalized += "ENVVALUE";
2453
2556
  i += variable[0].length - 1;
2454
2557
  continue;
2455
2558
  }
@@ -2503,13 +2606,6 @@ function hasEnvironmentVariableExpansionRead(
2503
2606
  )
2504
2607
  return true;
2505
2608
  }
2506
- for (const words of parsed.pipelines.flat()) {
2507
- if (
2508
- words.some((arg) => arg.includes("ENVTESTVALUE")) &&
2509
- !isApprovedEnvironmentPredicate(unwrapStaticCommand(words))
2510
- )
2511
- return true;
2512
- }
2513
2609
  for (const words of parsed.pipelines.flat()) {
2514
2610
  if (!words.some((arg) => arg.includes("UNKNOWNVALUE"))) continue;
2515
2611
  if (words.some((arg) => arg.includes("UNKNOWNVALUEUNQUOTED"))) return true;
@@ -2520,7 +2616,7 @@ function hasEnvironmentVariableExpansionRead(
2520
2616
  )
2521
2617
  return true;
2522
2618
  }
2523
- if (!localMaterialized.includes("ENVVALUE") && !localMaterialized.includes("ENVTESTVALUE")) {
2619
+ if (!localMaterialized.includes("ENVVALUE")) {
2524
2620
  return (
2525
2621
  hasSensitiveFileRead(localMaterialized, isPathBlocked) ||
2526
2622
  hasSensitivePathRedirect(localMaterialized, isPathBlocked) ||
@@ -2530,7 +2626,7 @@ function hasEnvironmentVariableExpansionRead(
2530
2626
 
2531
2627
  for (const [index, pipeline] of parsed.pipelines.entries()) {
2532
2628
  const hasEnvironmentValue = pipeline.some((argv) =>
2533
- argv.some((arg) => arg.includes("ENVVALUE") || arg.includes("ENVTESTVALUE")),
2629
+ argv.some((arg) => arg.includes("ENVVALUE")),
2534
2630
  );
2535
2631
  if (!hasEnvironmentValue) continue;
2536
2632
  if (parsed.redirects.some((redirect) => redirect.pipeline === index)) return true;
@@ -2538,12 +2634,8 @@ function hasEnvironmentVariableExpansionRead(
2538
2634
  pipeline.some((argv) => {
2539
2635
  const unwrapped = unwrapStaticCommand(argv);
2540
2636
  const name = unwrapped[0]?.split("/").at(-1) ?? "";
2541
- if (argv.some((arg) => arg.includes("ENVVALUE") || arg.includes("ENVTESTVALUE"))) {
2542
- if (
2543
- argv
2544
- .slice(0, argv.length - unwrapped.length)
2545
- .some((arg) => arg.includes("ENVVALUE") || arg.includes("ENVTESTVALUE"))
2546
- )
2637
+ if (argv.some((arg) => arg.includes("ENVVALUE"))) {
2638
+ if (argv.slice(0, argv.length - unwrapped.length).some((arg) => arg.includes("ENVVALUE")))
2547
2639
  return true;
2548
2640
  if (isApprovedEnvironmentPredicate(unwrapped)) return false;
2549
2641
  if (isNavigationOperand(unwrapped, "ENVVALUE")) {
@@ -2579,7 +2671,7 @@ function isApprovedEnvironmentPredicate(argv: string[]): boolean {
2579
2671
  if (args.at(-1) !== "]") return false;
2580
2672
  args = args.slice(0, -1);
2581
2673
  } else if (argv[0] !== "test") return false;
2582
- const value = (arg: string | undefined) => arg === "ENVVALUE" || arg === "ENVTESTVALUE";
2674
+ const value = (arg: string | undefined) => arg === "ENVVALUE";
2583
2675
  const literal = (arg: string | undefined) =>
2584
2676
  /^[A-Za-z0-9_.:-]*$/.test(arg ?? "") && arg !== undefined;
2585
2677
  return (