cc-safety-net 0.8.2 → 0.9.0

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.
@@ -871,6 +871,11 @@ var ENV_VARS = [
871
871
  name: "SAFETY_NET_PARANOID_INTERPRETERS",
872
872
  description: "Block interpreter one-liners",
873
873
  defaultBehavior: "off"
874
+ },
875
+ {
876
+ name: "SAFETY_NET_WORKTREE",
877
+ description: "Allow local git discards in linked worktrees",
878
+ defaultBehavior: "off"
874
879
  }
875
880
  ];
876
881
  function getEnvironmentInfo() {
@@ -969,7 +974,8 @@ var PLATFORM_NAMES = {
969
974
  "claude-code": "Claude Code",
970
975
  opencode: "OpenCode",
971
976
  "gemini-cli": "Gemini CLI",
972
- "copilot-cli": "Copilot CLI"
977
+ "copilot-cli": "Copilot CLI",
978
+ codex: "Codex"
973
979
  };
974
980
  function formatHooksSection(hooks) {
975
981
  const lines = [];
@@ -1009,7 +1015,7 @@ function formatHooksSection(hooks) {
1009
1015
  lines.push(` Warning (${w.platform}): ${w.message}`);
1010
1016
  }
1011
1017
  for (const e of errors) {
1012
- lines.push(` Error (${e.platform}): ${e.message}`);
1018
+ lines.push(colors.red(` Error (${e.platform}): ${e.message}`));
1013
1019
  }
1014
1020
  return lines.join(`
1015
1021
  `);
@@ -1375,9 +1381,9 @@ All checks passed.`);
1375
1381
  }
1376
1382
 
1377
1383
  // src/bin/doctor/hooks.ts
1378
- import { existsSync as existsSync4, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
1384
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "node:fs";
1379
1385
  import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
1380
- import { join as join3 } from "node:path";
1386
+ import { join as join5 } from "node:path";
1381
1387
 
1382
1388
  // src/core/analyze/dangerous-text.ts
1383
1389
  function dangerousInText(text) {
@@ -1563,16 +1569,336 @@ function hasRecursiveForceFlags(tokens) {
1563
1569
  return hasRecursive && hasForce;
1564
1570
  }
1565
1571
 
1572
+ // src/core/shell.ts
1573
+ import { realpathSync as realpathSync3 } from "node:fs";
1574
+ import { isAbsolute as isAbsolute3, parse as parsePath2 } from "node:path";
1575
+
1566
1576
  // node_modules/shell-quote/index.js
1567
1577
  var $quote = require_quote();
1568
1578
  var $parse = require_parse();
1569
1579
 
1580
+ // src/core/path.ts
1581
+ import { lstatSync, realpathSync } from "node:fs";
1582
+ import { dirname, isAbsolute, parse as parsePath, sep } from "node:path";
1583
+ function resolveChdirTarget(baseCwd, target) {
1584
+ const root = isAbsolute(target) ? getPathRoot(target) : "";
1585
+ let current = root || baseCwd;
1586
+ for (const component of getPathComponents(root ? target.slice(root.length) : target)) {
1587
+ if (component === "" || component === ".") {
1588
+ continue;
1589
+ }
1590
+ if (component === "..") {
1591
+ current = dirname(current);
1592
+ continue;
1593
+ }
1594
+ const candidate = appendPathWithoutNormalizing(current, component);
1595
+ current = lstatSync(candidate).isSymbolicLink() ? realpathSync(candidate) : candidate;
1596
+ }
1597
+ return current;
1598
+ }
1599
+ function appendPathWithoutNormalizing(base, target) {
1600
+ return base.endsWith("/") || base.endsWith("\\") ? `${base}${target}` : `${base}${sep}${target}`;
1601
+ }
1602
+ function getPathRoot(target) {
1603
+ return parsePath(target).root;
1604
+ }
1605
+ function getPathComponents(target) {
1606
+ const separator = process.platform === "win32" ? /[\\/]+/ : /\/+/;
1607
+ return target.split(separator);
1608
+ }
1609
+
1610
+ // src/core/worktree.ts
1611
+ import { existsSync as existsSync4, lstatSync as lstatSync2, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync } from "node:fs";
1612
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "node:path";
1613
+ var GIT_GLOBAL_OPTS_WITH_VALUE = new Set([
1614
+ "-c",
1615
+ "-C",
1616
+ "--git-dir",
1617
+ "--work-tree",
1618
+ "--namespace",
1619
+ "--super-prefix",
1620
+ "--config-env"
1621
+ ]);
1622
+ var GIT_CONTEXT_ENV_OVERRIDES = [
1623
+ "GIT_DIR",
1624
+ "GIT_WORK_TREE",
1625
+ "GIT_COMMON_DIR",
1626
+ "GIT_INDEX_FILE"
1627
+ ];
1628
+ var GIT_CONFIG_AFFECTING_ENV_NAMES = new Set([
1629
+ "GIT_CONFIG_GLOBAL",
1630
+ "GIT_CONFIG_NOSYSTEM",
1631
+ "GIT_CONFIG_SYSTEM",
1632
+ "HOME",
1633
+ "XDG_CONFIG_HOME"
1634
+ ]);
1635
+ function hasGitContextEnvOverride(envAssignments) {
1636
+ for (const name of GIT_CONTEXT_ENV_OVERRIDES) {
1637
+ if (envAssignments?.has(name) || Object.hasOwn(process.env, name)) {
1638
+ return true;
1639
+ }
1640
+ }
1641
+ return false;
1642
+ }
1643
+ function getGitExecutionContext(tokens, cwd) {
1644
+ if (!cwd) {
1645
+ return { gitCwd: null, hasExplicitGitContext: false };
1646
+ }
1647
+ let gitCwd;
1648
+ try {
1649
+ gitCwd = realpathSync2(resolve2(cwd));
1650
+ } catch {
1651
+ return { gitCwd: null, hasExplicitGitContext: false };
1652
+ }
1653
+ if (!isDirectory(gitCwd)) {
1654
+ return { gitCwd: null, hasExplicitGitContext: false };
1655
+ }
1656
+ let hasExplicitGitContext = false;
1657
+ let i = 1;
1658
+ while (i < tokens.length) {
1659
+ const token = tokens[i];
1660
+ if (!token)
1661
+ break;
1662
+ if (token === "--") {
1663
+ break;
1664
+ }
1665
+ if (!token.startsWith("-")) {
1666
+ break;
1667
+ }
1668
+ if (token === "-C") {
1669
+ const target = tokens[i + 1];
1670
+ if (!target) {
1671
+ return { gitCwd: null, hasExplicitGitContext };
1672
+ }
1673
+ const resolvedCwd = resolveGitCwd(gitCwd, target);
1674
+ if (!resolvedCwd) {
1675
+ return { gitCwd: null, hasExplicitGitContext };
1676
+ }
1677
+ gitCwd = resolvedCwd;
1678
+ i += 2;
1679
+ continue;
1680
+ }
1681
+ if (token.startsWith("-C") && token.length > 2) {
1682
+ const resolvedCwd = resolveGitCwd(gitCwd, token.slice(2));
1683
+ if (!resolvedCwd) {
1684
+ return { gitCwd: null, hasExplicitGitContext };
1685
+ }
1686
+ gitCwd = resolvedCwd;
1687
+ i++;
1688
+ continue;
1689
+ }
1690
+ if (token === "--git-dir" || token === "--work-tree") {
1691
+ hasExplicitGitContext = true;
1692
+ i += 2;
1693
+ continue;
1694
+ }
1695
+ if (token.startsWith("--git-dir=") || token.startsWith("--work-tree=")) {
1696
+ hasExplicitGitContext = true;
1697
+ i++;
1698
+ continue;
1699
+ }
1700
+ if (GIT_GLOBAL_OPTS_WITH_VALUE.has(token)) {
1701
+ i += 2;
1702
+ } else if (token.startsWith("-c") && token.length > 2) {
1703
+ i++;
1704
+ } else {
1705
+ i++;
1706
+ }
1707
+ }
1708
+ return { gitCwd, hasExplicitGitContext };
1709
+ }
1710
+ function isLinkedWorktree(cwd) {
1711
+ const dotGitPath = findDotGit(cwd);
1712
+ if (!dotGitPath) {
1713
+ return false;
1714
+ }
1715
+ try {
1716
+ const stat = lstatSync2(dotGitPath);
1717
+ if (stat.isSymbolicLink() || !stat.isFile()) {
1718
+ return false;
1719
+ }
1720
+ const content = readFileSync4(dotGitPath, "utf-8");
1721
+ const firstLine = content.split(/\r?\n/, 1)[0]?.trim() ?? "";
1722
+ if (!firstLine.startsWith("gitdir:")) {
1723
+ return false;
1724
+ }
1725
+ const rawGitDir = firstLine.slice("gitdir:".length).trim();
1726
+ if (rawGitDir === "") {
1727
+ return false;
1728
+ }
1729
+ const gitDir = isAbsolute2(rawGitDir) ? rawGitDir : resolve2(dirname2(dotGitPath), rawGitDir);
1730
+ if (!existsSync4(join3(gitDir, "commondir"))) {
1731
+ return false;
1732
+ }
1733
+ if (!worktreeGitdirBacklinkMatches(gitDir, dotGitPath)) {
1734
+ return false;
1735
+ }
1736
+ return worktreeConfigMatchesRoot(gitDir, dirname2(dotGitPath));
1737
+ } catch {
1738
+ return false;
1739
+ }
1740
+ }
1741
+ function worktreeGitdirBacklinkMatches(gitDir, dotGitPath) {
1742
+ const backlinkPath = join3(gitDir, "gitdir");
1743
+ if (!existsSync4(backlinkPath)) {
1744
+ return false;
1745
+ }
1746
+ const rawBacklink = readFileSync4(backlinkPath, "utf-8").split(/\r?\n/, 1)[0]?.trim() ?? "";
1747
+ if (rawBacklink === "") {
1748
+ return false;
1749
+ }
1750
+ const linkedDotGitPath = isAbsolute2(rawBacklink) ? rawBacklink : resolve2(gitDir, rawBacklink);
1751
+ try {
1752
+ return sameFilesystemPath(linkedDotGitPath, dotGitPath);
1753
+ } catch {
1754
+ return false;
1755
+ }
1756
+ }
1757
+ function worktreeConfigMatchesRoot(gitDir, worktreeRoot) {
1758
+ const configWorktreePath = join3(gitDir, "config.worktree");
1759
+ if (!existsSync4(configWorktreePath)) {
1760
+ return true;
1761
+ }
1762
+ const configuredWorktree = readCoreWorktree(configWorktreePath);
1763
+ if (configuredWorktree === null) {
1764
+ return true;
1765
+ }
1766
+ const resolvedConfiguredWorktree = isAbsolute2(configuredWorktree) ? configuredWorktree : resolve2(gitDir, configuredWorktree);
1767
+ try {
1768
+ return sameFilesystemPath(resolvedConfiguredWorktree, worktreeRoot);
1769
+ } catch {
1770
+ return false;
1771
+ }
1772
+ }
1773
+ function sameFilesystemPath(left, right) {
1774
+ try {
1775
+ const leftStat = statSync(left);
1776
+ const rightStat = statSync(right);
1777
+ if (leftStat.ino !== 0 && rightStat.ino !== 0 && leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino) {
1778
+ return true;
1779
+ }
1780
+ } catch {}
1781
+ return getCanonicalPathForComparison(left) === getCanonicalPathForComparison(right);
1782
+ }
1783
+ function getCanonicalPathForComparison(path) {
1784
+ return normalizePathForComparison(realpathSync2.native(path));
1785
+ }
1786
+ function normalizePathForComparison(path) {
1787
+ let normalized = path.replace(/^\\\\\?\\UNC\\/i, "//").replace(/^\\\\\?\\/i, "");
1788
+ normalized = normalized.replace(/\\/g, "/");
1789
+ if (normalized.length > 1 && normalized.endsWith("/")) {
1790
+ normalized = normalized.slice(0, -1);
1791
+ }
1792
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
1793
+ }
1794
+ function readCoreWorktree(configPath) {
1795
+ const content = readFileSync4(configPath, "utf-8");
1796
+ let inCore = false;
1797
+ let configuredWorktree = null;
1798
+ for (const line of content.split(/\r?\n/)) {
1799
+ const trimmed = line.trim();
1800
+ if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith(";")) {
1801
+ continue;
1802
+ }
1803
+ if (trimmed.startsWith("[")) {
1804
+ inCore = /^\[core\]$/i.test(trimmed);
1805
+ continue;
1806
+ }
1807
+ if (!inCore) {
1808
+ continue;
1809
+ }
1810
+ const match = trimmed.match(/^worktree\s*=\s*(.*)$/i);
1811
+ if (match) {
1812
+ configuredWorktree = parseGitConfigValue(match[1] ?? "");
1813
+ }
1814
+ }
1815
+ return configuredWorktree;
1816
+ }
1817
+ function parseGitConfigValue(value) {
1818
+ const trimmed = value.trim();
1819
+ if (!trimmed.startsWith('"') || !trimmed.endsWith('"')) {
1820
+ return trimmed;
1821
+ }
1822
+ return unescapeDoubleQuotedGitConfigValue(trimmed.slice(1, -1));
1823
+ }
1824
+ function unescapeDoubleQuotedGitConfigValue(value) {
1825
+ let result = "";
1826
+ for (let i = 0;i < value.length; i++) {
1827
+ const char = value[i];
1828
+ if (char !== "\\") {
1829
+ result += char;
1830
+ continue;
1831
+ }
1832
+ const next = value[i + 1];
1833
+ if (next === undefined) {
1834
+ result += char;
1835
+ continue;
1836
+ }
1837
+ switch (next) {
1838
+ case "\\":
1839
+ case '"':
1840
+ result += next;
1841
+ break;
1842
+ case "n":
1843
+ result += `
1844
+ `;
1845
+ break;
1846
+ case "t":
1847
+ result += "\t";
1848
+ break;
1849
+ case "b":
1850
+ result += "\b";
1851
+ break;
1852
+ default:
1853
+ result += `\\${next}`;
1854
+ break;
1855
+ }
1856
+ i++;
1857
+ }
1858
+ return result;
1859
+ }
1860
+ function resolveGitCwd(baseCwd, target) {
1861
+ try {
1862
+ const resolved = resolveChdirTarget(baseCwd, target);
1863
+ return isDirectory(resolved) ? resolved : null;
1864
+ } catch {
1865
+ return null;
1866
+ }
1867
+ }
1868
+ function isDirectory(path) {
1869
+ try {
1870
+ return statSync(path).isDirectory();
1871
+ } catch {
1872
+ return false;
1873
+ }
1874
+ }
1875
+ function findDotGit(cwd) {
1876
+ let current;
1877
+ try {
1878
+ current = realpathSync2(cwd);
1879
+ } catch {
1880
+ return null;
1881
+ }
1882
+ while (true) {
1883
+ const dotGitPath = join3(current, ".git");
1884
+ if (existsSync4(dotGitPath)) {
1885
+ return dotGitPath;
1886
+ }
1887
+ const parent = dirname2(current);
1888
+ if (parent === current) {
1889
+ return null;
1890
+ }
1891
+ current = parent;
1892
+ }
1893
+ }
1894
+
1570
1895
  // src/core/shell.ts
1571
1896
  var ENV_PROXY = new Proxy({}, {
1572
1897
  get: (_, name) => `$${String(name)}`
1573
1898
  });
1574
1899
  var ARITHMETIC_SENTINEL = "__CC_SAFETY_NET_ARITH_SENTINEL__";
1575
1900
  var BACKTICK_ATTACHED_SUFFIX_SENTINEL = "__CC_SAFETY_NET_BACKTICK_SUFFIX__";
1901
+ var DYNAMIC_SUBSTITUTION_TOKEN = "$__CC_SAFETY_NET_DYNAMIC_SUBSTITUTION__";
1576
1902
  function splitShellCommands(command) {
1577
1903
  if (hasUnclosedQuotes(command)) {
1578
1904
  return [[command]];
@@ -1616,9 +1942,14 @@ function splitShellCommands(command) {
1616
1942
  const { innerSegments, endIndex } = extractCommandSubstitution(tokens, i + 2);
1617
1943
  const attachedSuffix = _getBacktickAttachedSuffix(tokens[endIndex + 1]);
1618
1944
  const shouldKeepCurrent = attachedSuffix !== null && !_isRedirectOp(tokens[i - 1]) && !isOperatorToken(tokens[i - 1]);
1619
- if (!shouldKeepCurrent && current.length > 0) {
1620
- segments.push(current);
1621
- current = [];
1945
+ if (current.length > 0) {
1946
+ if (_containsGitCommandToken(current)) {
1947
+ current.push(DYNAMIC_SUBSTITUTION_TOKEN);
1948
+ }
1949
+ if (!shouldKeepCurrent) {
1950
+ segments.push(current);
1951
+ current = [];
1952
+ }
1622
1953
  }
1623
1954
  for (const seg of innerSegments) {
1624
1955
  segments.push(seg);
@@ -1637,6 +1968,9 @@ function splitShellCommands(command) {
1637
1968
  current.push(prefix);
1638
1969
  }
1639
1970
  }
1971
+ if (_containsGitCommandToken(current)) {
1972
+ current.push(DYNAMIC_SUBSTITUTION_TOKEN);
1973
+ }
1640
1974
  const { innerSegments, endIndex } = extractCommandSubstitution(tokens, i + 2);
1641
1975
  for (const seg of innerSegments) {
1642
1976
  segments.push(seg);
@@ -1723,6 +2057,9 @@ function _getCommandTokenText(token) {
1723
2057
  }
1724
2058
  return null;
1725
2059
  }
2060
+ function _containsGitCommandToken(tokens) {
2061
+ return tokens.some((token) => (token.split("/").pop() ?? token).toLowerCase() === "git");
2062
+ }
1726
2063
  function extractCommandSubstitution(tokens, startIndex) {
1727
2064
  if (tokens[startIndex] === ARITHMETIC_SENTINEL) {
1728
2065
  return _extractArithmeticSubstitution(tokens, startIndex);
@@ -2032,6 +2369,8 @@ function _stripAttachedIoNumbers(command) {
2032
2369
  return result;
2033
2370
  }
2034
2371
  var ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
2372
+ var ENV_APPEND_ASSIGNMENT_RE = /^([A-Za-z_][A-Za-z0-9_]*)\+=/;
2373
+ var GIT_CONTEXT_ENV_OVERRIDE_NAMES = new Set(GIT_CONTEXT_ENV_OVERRIDES);
2035
2374
  function parseEnvAssignment(token) {
2036
2375
  if (!ENV_ASSIGNMENT_RE.test(token)) {
2037
2376
  return null;
@@ -2039,6 +2378,21 @@ function parseEnvAssignment(token) {
2039
2378
  const eqIdx = token.indexOf("=");
2040
2379
  return { name: token.slice(0, eqIdx), value: token.slice(eqIdx + 1) };
2041
2380
  }
2381
+ function parseGitContextAppendEnvAssignment(token) {
2382
+ const match = token.match(ENV_APPEND_ASSIGNMENT_RE);
2383
+ const name = match?.[1];
2384
+ if (!name || !isTrackedGitEnvName(name)) {
2385
+ return null;
2386
+ }
2387
+ const eqIdx = token.indexOf("=");
2388
+ return { name, value: token.slice(eqIdx + 1) };
2389
+ }
2390
+ function isTrackedGitEnvName(name) {
2391
+ return GIT_CONTEXT_ENV_OVERRIDE_NAMES.has(name) || GIT_CONFIG_AFFECTING_ENV_NAMES.has(name) || isGitConfigEnvName(name);
2392
+ }
2393
+ function isGitConfigEnvName(name) {
2394
+ return name === "GIT_CONFIG_COUNT" || name === "GIT_CONFIG_PARAMETERS" || /^GIT_CONFIG_(KEY|VALUE)_\d+$/.test(name);
2395
+ }
2042
2396
  function stripEnvAssignmentsWithInfo(tokens) {
2043
2397
  const envAssignments = new Map;
2044
2398
  let i = 0;
@@ -2056,12 +2410,13 @@ function stripEnvAssignmentsWithInfo(tokens) {
2056
2410
  }
2057
2411
  return { tokens: tokens.slice(i), envAssignments };
2058
2412
  }
2059
- function stripWrappers(tokens) {
2060
- return stripWrappersWithInfo(tokens).tokens;
2413
+ function stripWrappers(tokens, cwd) {
2414
+ return stripWrappersWithInfo(tokens, cwd).tokens;
2061
2415
  }
2062
- function stripWrappersWithInfo(tokens) {
2416
+ function stripWrappersWithInfo(tokens, cwd) {
2063
2417
  let result = [...tokens];
2064
2418
  const allEnvAssignments = new Map;
2419
+ let currentCwd = cwd;
2065
2420
  for (let iteration = 0;iteration < MAX_STRIP_ITERATIONS; iteration++) {
2066
2421
  const before = result.join(" ");
2067
2422
  const { tokens: strippedTokens, envAssignments } = stripEnvAssignmentsWithInfo(result);
@@ -2072,6 +2427,10 @@ function stripWrappersWithInfo(tokens) {
2072
2427
  if (result.length === 0)
2073
2428
  break;
2074
2429
  while (result.length > 0 && result[0]?.includes("=") && !ENV_ASSIGNMENT_RE.test(result[0] ?? "")) {
2430
+ const appendAssignment = parseGitContextAppendEnvAssignment(result[0] ?? "");
2431
+ if (appendAssignment) {
2432
+ allEnvAssignments.set(appendAssignment.name, appendAssignment.value);
2433
+ }
2075
2434
  result = result.slice(1);
2076
2435
  }
2077
2436
  if (result.length === 0)
@@ -2081,11 +2440,18 @@ function stripWrappersWithInfo(tokens) {
2081
2440
  break;
2082
2441
  }
2083
2442
  if (head === "sudo") {
2084
- result = stripSudo(result);
2443
+ const sudoResult = stripSudoWithInfo(result, currentCwd);
2444
+ result = sudoResult.tokens;
2445
+ if (sudoResult.cwd !== undefined) {
2446
+ currentCwd = sudoResult.cwd;
2447
+ }
2085
2448
  }
2086
2449
  if (head === "env") {
2087
- const envResult = stripEnvWithInfo(result);
2450
+ const envResult = stripEnvWithInfo(result, currentCwd);
2088
2451
  result = envResult.tokens;
2452
+ if (envResult.cwd !== undefined) {
2453
+ currentCwd = envResult.cwd;
2454
+ }
2089
2455
  for (const [k, v] of envResult.envAssignments) {
2090
2456
  allEnvAssignments.set(k, v);
2091
2457
  }
@@ -2100,28 +2466,50 @@ function stripWrappersWithInfo(tokens) {
2100
2466
  for (const [k, v] of finalAssignments) {
2101
2467
  allEnvAssignments.set(k, v);
2102
2468
  }
2103
- return { tokens: finalTokens, envAssignments: allEnvAssignments };
2469
+ return { tokens: finalTokens, envAssignments: allEnvAssignments, cwd: currentCwd };
2104
2470
  }
2105
2471
  var SUDO_OPTS_WITH_VALUE = new Set(["-u", "-g", "-C", "-D", "-h", "-p", "-r", "-t", "-T", "-U"]);
2106
- function stripSudo(tokens) {
2472
+ function stripSudoWithInfo(tokens, cwd) {
2107
2473
  let i = 1;
2474
+ let currentCwd = cwd;
2108
2475
  while (i < tokens.length) {
2109
2476
  const token = tokens[i];
2110
2477
  if (!token)
2111
2478
  break;
2112
2479
  if (token === "--") {
2113
- return tokens.slice(i + 1);
2480
+ return { tokens: tokens.slice(i + 1), cwd: currentCwd };
2114
2481
  }
2115
2482
  if (!token.startsWith("-")) {
2116
2483
  break;
2117
2484
  }
2485
+ if (token === "-D" || token === "--chdir") {
2486
+ const target = tokens[i + 1];
2487
+ currentCwd = target ? resolveWrapperCwd(currentCwd, target) : null;
2488
+ i += 2;
2489
+ continue;
2490
+ }
2491
+ if (token.startsWith("--chdir=")) {
2492
+ currentCwd = resolveWrapperCwd(currentCwd, token.slice("--chdir=".length));
2493
+ i++;
2494
+ continue;
2495
+ }
2496
+ if (token.startsWith("-D") && token.length > 2) {
2497
+ currentCwd = resolveWrapperCwd(currentCwd, token.slice(2));
2498
+ i++;
2499
+ continue;
2500
+ }
2501
+ if (token === "-i" || token === "--login") {
2502
+ currentCwd = null;
2503
+ i++;
2504
+ continue;
2505
+ }
2118
2506
  if (SUDO_OPTS_WITH_VALUE.has(token)) {
2119
2507
  i += 2;
2120
2508
  continue;
2121
2509
  }
2122
2510
  i++;
2123
2511
  }
2124
- return tokens.slice(i);
2512
+ return { tokens: tokens.slice(i), cwd: currentCwd };
2125
2513
  }
2126
2514
  var ENV_OPTS_NO_VALUE = new Set(["-i", "-0", "--null"]);
2127
2515
  var ENV_OPTS_WITH_VALUE = new Set([
@@ -2133,21 +2521,70 @@ var ENV_OPTS_WITH_VALUE = new Set([
2133
2521
  "--split-string",
2134
2522
  "-P"
2135
2523
  ]);
2136
- function stripEnvWithInfo(tokens) {
2524
+ function stripEnvWithInfo(tokens, cwd) {
2137
2525
  const envAssignments = new Map;
2526
+ let currentCwd = cwd;
2527
+ let expandedTokens = tokens;
2138
2528
  let i = 1;
2139
- while (i < tokens.length) {
2140
- const token = tokens[i];
2529
+ while (i < expandedTokens.length) {
2530
+ const token = expandedTokens[i];
2141
2531
  if (!token)
2142
2532
  break;
2143
2533
  if (token === "--") {
2144
- return { tokens: tokens.slice(i + 1), envAssignments };
2534
+ return { tokens: expandedTokens.slice(i + 1), envAssignments, cwd: currentCwd };
2145
2535
  }
2146
2536
  if (ENV_OPTS_NO_VALUE.has(token)) {
2147
2537
  i++;
2148
2538
  continue;
2149
2539
  }
2540
+ if (token === "-S" || token === "--split-string") {
2541
+ const splitValue = expandedTokens[i + 1];
2542
+ const splitTokens = splitValue !== undefined ? parseEnvSplitString(splitValue) : null;
2543
+ if (!splitTokens) {
2544
+ currentCwd = null;
2545
+ i += 2;
2546
+ continue;
2547
+ }
2548
+ expandedTokens = [
2549
+ ...expandedTokens.slice(0, i),
2550
+ ...splitTokens,
2551
+ ...expandedTokens.slice(i + 2)
2552
+ ];
2553
+ continue;
2554
+ }
2555
+ if (token.startsWith("-S") && token.length > 2) {
2556
+ const splitTokens = parseEnvSplitString(token.slice("-S".length));
2557
+ if (!splitTokens) {
2558
+ currentCwd = null;
2559
+ i++;
2560
+ continue;
2561
+ }
2562
+ expandedTokens = [
2563
+ ...expandedTokens.slice(0, i),
2564
+ ...splitTokens,
2565
+ ...expandedTokens.slice(i + 1)
2566
+ ];
2567
+ continue;
2568
+ }
2569
+ if (token.startsWith("--split-string=")) {
2570
+ const splitTokens = parseEnvSplitString(token.slice("--split-string=".length));
2571
+ if (!splitTokens) {
2572
+ currentCwd = null;
2573
+ i++;
2574
+ continue;
2575
+ }
2576
+ expandedTokens = [
2577
+ ...expandedTokens.slice(0, i),
2578
+ ...splitTokens,
2579
+ ...expandedTokens.slice(i + 1)
2580
+ ];
2581
+ continue;
2582
+ }
2150
2583
  if (ENV_OPTS_WITH_VALUE.has(token)) {
2584
+ if (token === "-C" || token === "--chdir") {
2585
+ const target = expandedTokens[i + 1];
2586
+ currentCwd = target ? resolveWrapperCwd(currentCwd, target) : null;
2587
+ }
2151
2588
  i += 2;
2152
2589
  continue;
2153
2590
  }
@@ -2155,7 +2592,9 @@ function stripEnvWithInfo(tokens) {
2155
2592
  i++;
2156
2593
  continue;
2157
2594
  }
2158
- if (token.startsWith("-C=") || token.startsWith("--chdir=")) {
2595
+ if (token.startsWith("-C") && token.length > 2 || token.startsWith("--chdir=")) {
2596
+ const target = token.startsWith("--chdir=") ? token.slice("--chdir=".length) : token.startsWith("-C=") ? token.slice("-C=".length) : token.slice("-C".length);
2597
+ currentCwd = resolveWrapperCwd(currentCwd, target);
2159
2598
  i++;
2160
2599
  continue;
2161
2600
  }
@@ -2174,7 +2613,39 @@ function stripEnvWithInfo(tokens) {
2174
2613
  envAssignments.set(assignment.name, assignment.value);
2175
2614
  i++;
2176
2615
  }
2177
- return { tokens: tokens.slice(i), envAssignments };
2616
+ return { tokens: expandedTokens.slice(i), envAssignments, cwd: currentCwd };
2617
+ }
2618
+ function parseEnvSplitString(value) {
2619
+ if (hasUnclosedQuotes(value)) {
2620
+ return null;
2621
+ }
2622
+ const parsed = $parse(value, ENV_PROXY);
2623
+ const result = [];
2624
+ for (const entry of parsed) {
2625
+ const token = _getCommandTokenText(entry);
2626
+ if (token === null) {
2627
+ return null;
2628
+ }
2629
+ result.push(token);
2630
+ }
2631
+ return result;
2632
+ }
2633
+ function resolveWrapperCwd(cwd, target) {
2634
+ if (target === "") {
2635
+ return null;
2636
+ }
2637
+ try {
2638
+ if (!cwd && !isAbsolute3(target)) {
2639
+ return null;
2640
+ }
2641
+ const baseCwd = isAbsolute3(target) ? getPathRoot2(target) : realpathSync3(cwd ?? "/");
2642
+ return resolveChdirTarget(baseCwd, target);
2643
+ } catch {
2644
+ return null;
2645
+ }
2646
+ }
2647
+ function getPathRoot2(target) {
2648
+ return parsePath2(target).root;
2178
2649
  }
2179
2650
  function stripCommand(tokens) {
2180
2651
  let i = 1;
@@ -2527,6 +2998,9 @@ function extractDashCArg(tokens) {
2527
2998
  }
2528
2999
 
2529
3000
  // src/core/rules-git.ts
3001
+ import { execFileSync } from "node:child_process";
3002
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
3003
+ import { dirname as dirname3, isAbsolute as isAbsolute4, join as join4, resolve as resolve3 } from "node:path";
2530
3004
  var REASON_CHECKOUT_DOUBLE_DASH = "git checkout -- discards uncommitted changes permanently. Use 'git stash' first.";
2531
3005
  var REASON_CHECKOUT_FORCE = "git checkout --force discards uncommitted changes. Use 'git stash' first.";
2532
3006
  var REASON_CHECKOUT_REF_PATH = "git checkout <ref> -- <path> overwrites working tree with ref version. Use 'git stash' first.";
@@ -2544,15 +3018,6 @@ var REASON_BRANCH_DELETE = "git branch -D force-deletes without merge check. Use
2544
3018
  var REASON_STASH_DROP = "git stash drop permanently deletes stashed changes. Consider 'git stash list' first.";
2545
3019
  var REASON_STASH_CLEAR = "git stash clear deletes ALL stashed changes permanently.";
2546
3020
  var REASON_WORKTREE_REMOVE_FORCE = "git worktree remove --force can delete uncommitted changes. Remove --force flag.";
2547
- var GIT_GLOBAL_OPTS_WITH_VALUE = new Set([
2548
- "-c",
2549
- "-C",
2550
- "--git-dir",
2551
- "--work-tree",
2552
- "--namespace",
2553
- "--super-prefix",
2554
- "--config-env"
2555
- ]);
2556
3021
  var CHECKOUT_OPTS_WITH_VALUE = new Set([
2557
3022
  "-b",
2558
3023
  "-B",
@@ -2565,6 +3030,13 @@ var CHECKOUT_OPTS_WITH_VALUE = new Set([
2565
3030
  var CHECKOUT_OPTS_WITH_OPTIONAL_VALUE = new Set(["--recurse-submodules", "--track", "-t"]);
2566
3031
  var CHECKOUT_SHORT_OPTS_WITH_VALUE = new Set(["-b", "-B", "-U"]);
2567
3032
  var SWITCH_SHORT_OPTS_WITH_VALUE = new Set(["-c", "-C"]);
3033
+ var TRUSTED_GIT_BINARIES = [
3034
+ "/usr/bin/git",
3035
+ "/usr/local/bin/git",
3036
+ "/opt/homebrew/bin/git",
3037
+ "C:\\Program Files\\Git\\cmd\\git.exe",
3038
+ "C:\\Program Files\\Git\\bin\\git.exe"
3039
+ ];
2568
3040
  var CHECKOUT_KNOWN_OPTS_NO_VALUE = new Set([
2569
3041
  "-q",
2570
3042
  "--quiet",
@@ -2611,34 +3083,76 @@ function splitAtDoubleDash(tokens) {
2611
3083
  after: tokens.slice(index + 1)
2612
3084
  };
2613
3085
  }
2614
- function analyzeGit(tokens) {
3086
+ function analyzeGit(tokens, options = {}) {
3087
+ const match = analyzeGitRule(tokens);
3088
+ if (!match) {
3089
+ return null;
3090
+ }
3091
+ if (getGitWorktreeRelaxationForMatch(tokens, match, options)) {
3092
+ return null;
3093
+ }
3094
+ return match.reason;
3095
+ }
3096
+ function getGitWorktreeRelaxation(tokens, options = {}) {
3097
+ const match = analyzeGitRule(tokens);
3098
+ if (!match) {
3099
+ return null;
3100
+ }
3101
+ return getGitWorktreeRelaxationForMatch(tokens, match, options);
3102
+ }
3103
+ function analyzeGitRule(tokens) {
2615
3104
  const { subcommand, rest } = extractGitSubcommandAndRest(tokens);
2616
3105
  if (!subcommand) {
2617
3106
  return null;
2618
3107
  }
2619
3108
  switch (subcommand.toLowerCase()) {
2620
3109
  case "checkout":
2621
- return analyzeGitCheckout(rest);
3110
+ return localDiscard(analyzeGitCheckout(rest));
2622
3111
  case "switch":
2623
- return analyzeGitSwitch(rest);
3112
+ return localDiscard(analyzeGitSwitch(rest));
2624
3113
  case "restore":
2625
- return analyzeGitRestore(rest);
3114
+ return localDiscard(analyzeGitRestore(rest));
2626
3115
  case "reset":
2627
3116
  return analyzeGitReset(rest);
2628
3117
  case "clean":
2629
- return analyzeGitClean(rest);
3118
+ return localDiscard(analyzeGitClean(rest));
2630
3119
  case "push":
2631
- return analyzeGitPush(rest);
3120
+ return sharedState(analyzeGitPush(rest));
2632
3121
  case "branch":
2633
- return analyzeGitBranch(rest);
3122
+ return sharedState(analyzeGitBranch(rest));
2634
3123
  case "stash":
2635
- return analyzeGitStash(rest);
3124
+ return sharedState(analyzeGitStash(rest));
2636
3125
  case "worktree":
2637
- return analyzeGitWorktree(rest);
3126
+ return sharedState(analyzeGitWorktree(rest));
2638
3127
  default:
2639
3128
  return null;
2640
3129
  }
2641
3130
  }
3131
+ function localDiscard(reason) {
3132
+ return reason ? { reason, localDiscard: true } : null;
3133
+ }
3134
+ function sharedState(reason) {
3135
+ return reason ? { reason, localDiscard: false } : null;
3136
+ }
3137
+ function getGitWorktreeRelaxationForMatch(tokens, match, options) {
3138
+ if (!match.localDiscard || !options.worktreeMode || hasGitContextEnvOverride(options.envAssignments)) {
3139
+ return null;
3140
+ }
3141
+ const context = getGitExecutionContext(tokens, options.cwd);
3142
+ if (!context.gitCwd || context.hasExplicitGitContext) {
3143
+ return null;
3144
+ }
3145
+ if (!isLinkedWorktree(context.gitCwd)) {
3146
+ return null;
3147
+ }
3148
+ if (isNonRelaxableLocalDiscard(tokens, options, context.gitCwd)) {
3149
+ return null;
3150
+ }
3151
+ return {
3152
+ originalReason: match.reason,
3153
+ gitCwd: context.gitCwd
3154
+ };
3155
+ }
2642
3156
  function extractGitSubcommandAndRest(tokens) {
2643
3157
  if (tokens.length === 0) {
2644
3158
  return { subcommand: null, rest: [] };
@@ -2776,15 +3290,32 @@ function analyzeGitRestore(tokens) {
2776
3290
  return hasStaged ? null : REASON_RESTORE;
2777
3291
  }
2778
3292
  function analyzeGitReset(tokens) {
3293
+ let reason = null;
2779
3294
  for (const token of tokens) {
2780
3295
  if (token === "--hard") {
2781
- return REASON_RESET_HARD;
3296
+ reason = REASON_RESET_HARD;
3297
+ break;
2782
3298
  }
2783
3299
  if (token === "--merge") {
2784
- return REASON_RESET_MERGE;
3300
+ reason = REASON_RESET_MERGE;
3301
+ break;
2785
3302
  }
2786
3303
  }
2787
- return null;
3304
+ if (!reason) {
3305
+ return null;
3306
+ }
3307
+ return resetHasRef(tokens) ? sharedState(reason) : localDiscard(reason);
3308
+ }
3309
+ function resetHasRef(tokens) {
3310
+ for (const token of tokens) {
3311
+ if (token === "--") {
3312
+ return false;
3313
+ }
3314
+ if (!token.startsWith("-")) {
3315
+ return true;
3316
+ }
3317
+ }
3318
+ return false;
2788
3319
  }
2789
3320
  function analyzeGitClean(tokens) {
2790
3321
  for (const token of tokens) {
@@ -2798,6 +3329,351 @@ function analyzeGitClean(tokens) {
2798
3329
  }
2799
3330
  return null;
2800
3331
  }
3332
+ function isNonRelaxableLocalDiscard(tokens, options, gitCwd) {
3333
+ const { subcommand, rest } = extractGitSubcommandAndRest(tokens);
3334
+ const normalizedSubcommand = subcommand?.toLowerCase();
3335
+ if (hasDynamicGitArgument(rest) || hasRecursiveSubmoduleConfig(tokens, options, gitCwd) || hasRecurseSubmodulesOption(rest) || isForcedBranchReset(normalizedSubcommand, rest)) {
3336
+ return true;
3337
+ }
3338
+ return normalizedSubcommand === "clean" && countCleanForceFlags(rest) > 1;
3339
+ }
3340
+ function hasDynamicGitArgument(tokens) {
3341
+ return tokens.some((token) => /[$*?[]/.test(token));
3342
+ }
3343
+ function hasRecursiveSubmoduleConfig(tokens, options, gitCwd) {
3344
+ const commandLineConfig = commandLineRecursiveSubmoduleConfig(tokens, options.envAssignments);
3345
+ if (commandLineConfig !== null) {
3346
+ return commandLineConfig;
3347
+ }
3348
+ const envConfig = envRecursiveSubmoduleConfig(options.envAssignments);
3349
+ if (envConfig !== null) {
3350
+ return envConfig;
3351
+ }
3352
+ if (hasConfigAffectingEnvAssignment(options.envAssignments)) {
3353
+ return true;
3354
+ }
3355
+ return effectiveGitConfigEnablesRecursiveSubmodules(gitCwd);
3356
+ }
3357
+ function commandLineRecursiveSubmoduleConfig(tokens, envAssignments) {
3358
+ let recursiveSubmoduleConfig = null;
3359
+ let i = 1;
3360
+ while (i < tokens.length) {
3361
+ const token = tokens[i];
3362
+ if (!token || token === "--") {
3363
+ return recursiveSubmoduleConfig;
3364
+ }
3365
+ if (!token.startsWith("-")) {
3366
+ return recursiveSubmoduleConfig;
3367
+ }
3368
+ if (token === "-c") {
3369
+ const configValue = recursiveSubmoduleConfigValue(tokens[i + 1]);
3370
+ if (configValue !== null) {
3371
+ recursiveSubmoduleConfig = configValue;
3372
+ }
3373
+ i += 2;
3374
+ continue;
3375
+ }
3376
+ if (token.startsWith("-c") && token.length > 2) {
3377
+ const configValue = recursiveSubmoduleConfigValue(token.slice(2));
3378
+ if (configValue !== null) {
3379
+ recursiveSubmoduleConfig = configValue;
3380
+ }
3381
+ i++;
3382
+ continue;
3383
+ }
3384
+ if (token === "--config-env") {
3385
+ const configValue = recursiveSubmoduleConfigEnvValue(tokens[i + 1], envAssignments);
3386
+ if (configValue !== null) {
3387
+ recursiveSubmoduleConfig = configValue;
3388
+ }
3389
+ i += 2;
3390
+ continue;
3391
+ }
3392
+ if (token.startsWith("--config-env=")) {
3393
+ const configValue = recursiveSubmoduleConfigEnvValue(token.slice("--config-env=".length), envAssignments);
3394
+ if (configValue !== null) {
3395
+ recursiveSubmoduleConfig = configValue;
3396
+ }
3397
+ i++;
3398
+ continue;
3399
+ }
3400
+ if (GIT_GLOBAL_OPTS_WITH_VALUE.has(token)) {
3401
+ i += 2;
3402
+ } else {
3403
+ i++;
3404
+ }
3405
+ }
3406
+ return recursiveSubmoduleConfig;
3407
+ }
3408
+ function envRecursiveSubmoduleConfig(envAssignments) {
3409
+ if (getEnvConfigValue("GIT_CONFIG_PARAMETERS", envAssignments) !== undefined) {
3410
+ return true;
3411
+ }
3412
+ const countValue = getEnvConfigValue("GIT_CONFIG_COUNT", envAssignments);
3413
+ if (countValue === undefined) {
3414
+ return null;
3415
+ }
3416
+ const count = Number.parseInt(countValue, 10);
3417
+ if (!Number.isInteger(count) || count < 0) {
3418
+ return true;
3419
+ }
3420
+ let recursiveSubmoduleConfig = null;
3421
+ for (let i = 0;i < count; i++) {
3422
+ const key = getEnvConfigValue(`GIT_CONFIG_KEY_${i}`, envAssignments);
3423
+ if (key?.toLowerCase() !== "submodule.recurse") {
3424
+ continue;
3425
+ }
3426
+ const value = getEnvConfigValue(`GIT_CONFIG_VALUE_${i}`, envAssignments);
3427
+ recursiveSubmoduleConfig = value === undefined || gitConfigValueEnablesRecursiveSubmodules(value);
3428
+ }
3429
+ return recursiveSubmoduleConfig;
3430
+ }
3431
+ function hasConfigAffectingEnvAssignment(envAssignments) {
3432
+ if (!envAssignments) {
3433
+ return false;
3434
+ }
3435
+ for (const key of envAssignments.keys()) {
3436
+ if (GIT_CONFIG_AFFECTING_ENV_NAMES.has(key)) {
3437
+ return true;
3438
+ }
3439
+ }
3440
+ return false;
3441
+ }
3442
+ function getEnvConfigValue(name, envAssignments) {
3443
+ return envAssignments?.get(name) ?? process.env[name];
3444
+ }
3445
+ function effectiveGitConfigEnablesRecursiveSubmodules(cwd, gitBinary = getTrustedGitBinary()) {
3446
+ const localConfigResult = localGitConfigEnablesRecursiveSubmodules(cwd);
3447
+ if (localConfigResult === null || localConfigResult) {
3448
+ return true;
3449
+ }
3450
+ if (gitBinary === null) {
3451
+ return true;
3452
+ }
3453
+ try {
3454
+ const value = execFileSync(gitBinary, ["config", "--get", "submodule.recurse"], {
3455
+ cwd,
3456
+ encoding: "utf8",
3457
+ env: withoutGitConfigEnv(process.env),
3458
+ stdio: ["ignore", "pipe", "ignore"]
3459
+ }).trim();
3460
+ return gitConfigValueEnablesRecursiveSubmodules(value);
3461
+ } catch (error) {
3462
+ return !isGitConfigUnsetError(error);
3463
+ }
3464
+ }
3465
+ function localGitConfigEnablesRecursiveSubmodules(cwd) {
3466
+ const configPaths = getLocalGitConfigPaths(cwd);
3467
+ if (configPaths === null) {
3468
+ return null;
3469
+ }
3470
+ for (const configPath of configPaths) {
3471
+ if (!existsSync5(configPath)) {
3472
+ continue;
3473
+ }
3474
+ const result = gitConfigFileEnablesRecursiveSubmodules(configPath);
3475
+ if (result) {
3476
+ return true;
3477
+ }
3478
+ }
3479
+ return false;
3480
+ }
3481
+ function getTrustedGitBinary() {
3482
+ for (const gitBinary of TRUSTED_GIT_BINARIES) {
3483
+ if (existsSync5(gitBinary)) {
3484
+ return gitBinary;
3485
+ }
3486
+ }
3487
+ return null;
3488
+ }
3489
+ function withoutGitConfigEnv(env) {
3490
+ const nextEnv = { ...env };
3491
+ for (const key of Object.keys(nextEnv)) {
3492
+ if (key === "GIT_CONFIG_COUNT" || key === "GIT_CONFIG_PARAMETERS" || /^GIT_CONFIG_(KEY|VALUE)_\d+$/.test(key)) {
3493
+ delete nextEnv[key];
3494
+ }
3495
+ }
3496
+ return nextEnv;
3497
+ }
3498
+ function isGitConfigUnsetError(error) {
3499
+ return typeof error === "object" && error !== null && "status" in error && error.status === 1;
3500
+ }
3501
+ function getLocalGitConfigPaths(cwd) {
3502
+ const dotGitPath = findDotGitPath(cwd);
3503
+ if (dotGitPath === null) {
3504
+ return null;
3505
+ }
3506
+ const gitDir = resolveGitDirFromDotGit(dotGitPath);
3507
+ if (gitDir === null) {
3508
+ return null;
3509
+ }
3510
+ const commonDir = resolveCommonGitDir(gitDir);
3511
+ if (commonDir === null) {
3512
+ return null;
3513
+ }
3514
+ return [join4(commonDir, "config"), join4(gitDir, "config.worktree")];
3515
+ }
3516
+ function findDotGitPath(cwd) {
3517
+ let current = cwd;
3518
+ while (true) {
3519
+ const dotGitPath = join4(current, ".git");
3520
+ if (existsSync5(dotGitPath)) {
3521
+ return dotGitPath;
3522
+ }
3523
+ const parent = dirname3(current);
3524
+ if (parent === current) {
3525
+ return null;
3526
+ }
3527
+ current = parent;
3528
+ }
3529
+ }
3530
+ function resolveGitDirFromDotGit(dotGitPath) {
3531
+ try {
3532
+ const content = readFileSync5(dotGitPath, "utf-8");
3533
+ const firstLine = content.split(/\r?\n/, 1)[0]?.trim() ?? "";
3534
+ if (!firstLine.startsWith("gitdir:")) {
3535
+ return dotGitPath;
3536
+ }
3537
+ const rawGitDir = firstLine.slice("gitdir:".length).trim();
3538
+ if (rawGitDir === "") {
3539
+ return null;
3540
+ }
3541
+ return isAbsolute4(rawGitDir) ? rawGitDir : resolve3(dirname3(dotGitPath), rawGitDir);
3542
+ } catch {
3543
+ return null;
3544
+ }
3545
+ }
3546
+ function resolveCommonGitDir(gitDir) {
3547
+ const commonDirPath = join4(gitDir, "commondir");
3548
+ if (!existsSync5(commonDirPath)) {
3549
+ return gitDir;
3550
+ }
3551
+ try {
3552
+ const rawCommonDir = readFileSync5(commonDirPath, "utf-8").split(/\r?\n/, 1)[0]?.trim() ?? "";
3553
+ if (rawCommonDir === "") {
3554
+ return null;
3555
+ }
3556
+ return isAbsolute4(rawCommonDir) ? rawCommonDir : resolve3(gitDir, rawCommonDir);
3557
+ } catch {
3558
+ return null;
3559
+ }
3560
+ }
3561
+ function gitConfigFileEnablesRecursiveSubmodules(configPath) {
3562
+ let content;
3563
+ try {
3564
+ content = readFileSync5(configPath, "utf-8");
3565
+ } catch {
3566
+ return true;
3567
+ }
3568
+ let section = "";
3569
+ let recursiveSubmoduleConfig = false;
3570
+ for (const line of content.split(/\r?\n/)) {
3571
+ const trimmed = line.trim();
3572
+ if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith(";")) {
3573
+ continue;
3574
+ }
3575
+ const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
3576
+ if (sectionMatch) {
3577
+ section = sectionMatch[1]?.trim().toLowerCase() ?? "";
3578
+ continue;
3579
+ }
3580
+ const eqIdx = trimmed.indexOf("=");
3581
+ const key = (eqIdx === -1 ? trimmed : trimmed.slice(0, eqIdx)).trim().toLowerCase();
3582
+ const value = eqIdx === -1 ? "true" : trimmed.slice(eqIdx + 1).trim();
3583
+ if (isIncludeConfigSection(section) && key === "path") {
3584
+ return true;
3585
+ }
3586
+ if (section === "submodule" && key === "recurse") {
3587
+ recursiveSubmoduleConfig = gitConfigValueEnablesRecursiveSubmodules(value);
3588
+ }
3589
+ }
3590
+ return recursiveSubmoduleConfig;
3591
+ }
3592
+ function isIncludeConfigSection(section) {
3593
+ return section === "include" || section.startsWith("includeif ");
3594
+ }
3595
+ function recursiveSubmoduleConfigValue(config) {
3596
+ if (!config) {
3597
+ return null;
3598
+ }
3599
+ const eqIdx = config.indexOf("=");
3600
+ const key = (eqIdx === -1 ? config : config.slice(0, eqIdx)).toLowerCase();
3601
+ if (isIncludeConfigKey(key)) {
3602
+ return true;
3603
+ }
3604
+ if (key !== "submodule.recurse") {
3605
+ return null;
3606
+ }
3607
+ const value = eqIdx === -1 ? "true" : config.slice(eqIdx + 1).toLowerCase();
3608
+ return gitConfigValueEnablesRecursiveSubmodules(value);
3609
+ }
3610
+ function gitConfigValueEnablesRecursiveSubmodules(value) {
3611
+ const normalizedValue = value.toLowerCase();
3612
+ return normalizedValue !== "false" && normalizedValue !== "no" && normalizedValue !== "off" && normalizedValue !== "0";
3613
+ }
3614
+ function recursiveSubmoduleConfigEnvValue(configEnv, envAssignments) {
3615
+ const eqIdx = configEnv?.indexOf("=") ?? -1;
3616
+ if (!configEnv || eqIdx === -1) {
3617
+ return null;
3618
+ }
3619
+ const key = configEnv.slice(0, eqIdx).toLowerCase();
3620
+ if (isIncludeConfigKey(key)) {
3621
+ return true;
3622
+ }
3623
+ if (key !== "submodule.recurse") {
3624
+ return null;
3625
+ }
3626
+ const value = getEnvConfigValue(configEnv.slice(eqIdx + 1), envAssignments);
3627
+ return value === undefined || gitConfigValueEnablesRecursiveSubmodules(value);
3628
+ }
3629
+ function isIncludeConfigKey(key) {
3630
+ return key === "include.path" || key.startsWith("includeif.") && key.endsWith(".path");
3631
+ }
3632
+ function isForcedBranchReset(subcommand, rest) {
3633
+ if (subcommand === "checkout") {
3634
+ const { before } = splitAtDoubleDash(rest);
3635
+ const shortOpts = extractShortOpts(before, {
3636
+ shortOptsWithValue: CHECKOUT_SHORT_OPTS_WITH_VALUE
3637
+ });
3638
+ const hasForce = before.includes("--force") || shortOpts.has("-f");
3639
+ const hasBranchReset = shortOpts.has("-B") || before.some((token) => token === "-B" || token.startsWith("-B"));
3640
+ return hasForce && hasBranchReset;
3641
+ }
3642
+ if (subcommand === "switch") {
3643
+ const { before } = splitAtDoubleDash(rest);
3644
+ const shortOpts = extractShortOpts(before, {
3645
+ shortOptsWithValue: SWITCH_SHORT_OPTS_WITH_VALUE
3646
+ });
3647
+ const hasForce = before.includes("--force") || before.includes("--discard-changes") || shortOpts.has("-f");
3648
+ const hasForceCreate = before.some((token) => token === "-C" || token.startsWith("-C") || isForceCreateOption(token)) || shortOpts.has("-C");
3649
+ return hasForce && hasForceCreate;
3650
+ }
3651
+ return false;
3652
+ }
3653
+ function isForceCreateOption(token) {
3654
+ const optionName = token.split("=", 1)[0] ?? token;
3655
+ return optionName === "--force-create" || optionName.length >= "--force-c".length && "--force-create".startsWith(optionName);
3656
+ }
3657
+ function hasRecurseSubmodulesOption(tokens) {
3658
+ return tokens.some((token) => token.startsWith("--recurse-sub"));
3659
+ }
3660
+ function countCleanForceFlags(tokens) {
3661
+ let count = 0;
3662
+ for (const token of tokens) {
3663
+ if (token === "--force") {
3664
+ count++;
3665
+ continue;
3666
+ }
3667
+ if (token.startsWith("-") && !token.startsWith("--")) {
3668
+ for (const opt of token.slice(1)) {
3669
+ if (opt === "f") {
3670
+ count++;
3671
+ }
3672
+ }
3673
+ }
3674
+ }
3675
+ return count;
3676
+ }
2801
3677
  function analyzeGitPush(tokens) {
2802
3678
  let hasForceWithLease = false;
2803
3679
  const shortOpts = extractShortOpts(tokens.filter((t) => t !== "--"));
@@ -2844,11 +3720,11 @@ function analyzeGitWorktree(tokens) {
2844
3720
  }
2845
3721
 
2846
3722
  // src/core/rules-rm.ts
2847
- import { realpathSync } from "node:fs";
3723
+ import { realpathSync as realpathSync4 } from "node:fs";
2848
3724
  import { homedir as homedir3, tmpdir } from "node:os";
2849
- import { normalize, resolve as resolve2, sep } from "node:path";
3725
+ import { normalize, resolve as resolve4, sep as sep2 } from "node:path";
2850
3726
  var IS_WINDOWS = process.platform === "win32";
2851
- function normalizePathForComparison(p) {
3727
+ function normalizePathForComparison2(p) {
2852
3728
  let normalized = normalize(p);
2853
3729
  if (IS_WINDOWS) {
2854
3730
  normalized = normalized.replace(/\//g, "\\");
@@ -2988,9 +3864,9 @@ function isTempTarget(path, allowTmpdirVar) {
2988
3864
  return true;
2989
3865
  }
2990
3866
  const systemTmpdir = tmpdir();
2991
- const normalizedTmpdir = normalizePathForComparison(systemTmpdir);
2992
- const pathToCompare = normalizePathForComparison(normalized);
2993
- if (pathToCompare.startsWith(`${normalizedTmpdir}${sep}`) || pathToCompare === normalizedTmpdir) {
3867
+ const normalizedTmpdir = normalizePathForComparison2(systemTmpdir);
3868
+ const pathToCompare = normalizePathForComparison2(normalized);
3869
+ if (pathToCompare.startsWith(`${normalizedTmpdir}${sep2}`) || pathToCompare === normalizedTmpdir) {
2994
3870
  return true;
2995
3871
  }
2996
3872
  if (allowTmpdirVar) {
@@ -3008,7 +3884,7 @@ function getHomeDirForRmPolicy() {
3008
3884
  }
3009
3885
  function isCwdHomeForRmPolicy(cwd, homeDir) {
3010
3886
  try {
3011
- return normalizePathForComparison(cwd) === normalizePathForComparison(homeDir);
3887
+ return normalizePathForComparison2(cwd) === normalizePathForComparison2(homeDir);
3012
3888
  } catch {
3013
3889
  return false;
3014
3890
  }
@@ -3018,14 +3894,14 @@ function isCwdSelfTarget(target, cwd) {
3018
3894
  return true;
3019
3895
  }
3020
3896
  try {
3021
- const resolved = resolve2(cwd, target);
3022
- const realCwd = realpathSync(cwd);
3023
- const realResolved = realpathSync(resolved);
3024
- return normalizePathForComparison(realResolved) === normalizePathForComparison(realCwd);
3897
+ const resolved = resolve4(cwd, target);
3898
+ const realCwd = realpathSync4(cwd);
3899
+ const realResolved = realpathSync4(resolved);
3900
+ return normalizePathForComparison2(realResolved) === normalizePathForComparison2(realCwd);
3025
3901
  } catch {
3026
3902
  try {
3027
- const resolved = resolve2(cwd, target);
3028
- return normalizePathForComparison(resolved) === normalizePathForComparison(cwd);
3903
+ const resolved = resolve4(cwd, target);
3904
+ return normalizePathForComparison2(resolved) === normalizePathForComparison2(cwd);
3029
3905
  } catch {
3030
3906
  return false;
3031
3907
  }
@@ -3041,8 +3917,8 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
3041
3917
  }
3042
3918
  if (target.startsWith("/") || /^[A-Za-z]:[\\/]/.test(target)) {
3043
3919
  try {
3044
- const normalizedTarget = normalizePathForComparison(target);
3045
- const normalizedCwd = `${normalizePathForComparison(originalCwd)}${sep}`;
3920
+ const normalizedTarget = normalizePathForComparison2(target);
3921
+ const normalizedCwd = `${normalizePathForComparison2(originalCwd)}${sep2}`;
3046
3922
  return normalizedTarget.startsWith(normalizedCwd);
3047
3923
  } catch {
3048
3924
  return false;
@@ -3050,10 +3926,10 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
3050
3926
  }
3051
3927
  if (target.startsWith("./") || target.startsWith(".\\") || !target.includes("/") && !target.includes("\\")) {
3052
3928
  try {
3053
- const resolved = resolve2(resolveCwd, target);
3054
- const normalizedResolved = normalizePathForComparison(resolved);
3055
- const normalizedOriginalCwd = normalizePathForComparison(originalCwd);
3056
- return normalizedResolved.startsWith(`${normalizedOriginalCwd}${sep}`) || normalizedResolved === normalizedOriginalCwd;
3929
+ const resolved = resolve4(resolveCwd, target);
3930
+ const normalizedResolved = normalizePathForComparison2(resolved);
3931
+ const normalizedOriginalCwd = normalizePathForComparison2(originalCwd);
3932
+ return normalizedResolved.startsWith(`${normalizedOriginalCwd}${sep2}`) || normalizedResolved === normalizedOriginalCwd;
3057
3933
  } catch {
3058
3934
  return false;
3059
3935
  }
@@ -3062,10 +3938,10 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
3062
3938
  return false;
3063
3939
  }
3064
3940
  try {
3065
- const resolved = resolve2(resolveCwd, target);
3066
- const normalizedResolved = normalizePathForComparison(resolved);
3067
- const normalizedCwd = normalizePathForComparison(originalCwd);
3068
- return normalizedResolved.startsWith(`${normalizedCwd}${sep}`) || normalizedResolved === normalizedCwd;
3941
+ const resolved = resolve4(resolveCwd, target);
3942
+ const normalizedResolved = normalizePathForComparison2(resolved);
3943
+ const normalizedCwd = normalizePathForComparison2(originalCwd);
3944
+ return normalizedResolved.startsWith(`${normalizedCwd}${sep2}`) || normalizedResolved === normalizedCwd;
3069
3945
  } catch {
3070
3946
  return false;
3071
3947
  }
@@ -3074,22 +3950,32 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
3074
3950
  // src/core/analyze/parallel.ts
3075
3951
  var REASON_PARALLEL_RM = "parallel rm -rf with dynamic input is dangerous. Use explicit file list instead.";
3076
3952
  var REASON_PARALLEL_SHELL = "parallel with shell -c can execute arbitrary commands from dynamic input.";
3953
+ var PARALLEL_PLACEHOLDER_RE = /\{[^{}\s]*\}/;
3077
3954
  function analyzeParallel(tokens, context) {
3078
3955
  const parseResult = parseParallelCommand(tokens);
3079
3956
  if (!parseResult) {
3080
3957
  return null;
3081
3958
  }
3082
- const { template, args, hasPlaceholder } = parseResult;
3959
+ const { template, args, hasPlaceholder, runsRemotely, usesStdin } = parseResult;
3960
+ const hasDynamicStdinPlaceholder = usesStdin && hasPlaceholder;
3083
3961
  if (template.length === 0) {
3962
+ const nestedOverrides2 = buildCommandsModeOverrides(context, runsRemotely);
3084
3963
  for (const arg of args) {
3085
- const reason = context.analyzeNested(arg);
3964
+ const reason = context.analyzeNested(arg, nestedOverrides2);
3086
3965
  if (reason) {
3087
3966
  return reason;
3088
3967
  }
3089
3968
  }
3090
3969
  return null;
3091
3970
  }
3092
- let childTokens = stripWrappers([...template]);
3971
+ const childWrapperInfo = stripWrappersWithInfo([...template], context.cwd);
3972
+ let childTokens = childWrapperInfo.tokens;
3973
+ const childEnvAssignments = new Map(context.envAssignments ?? []);
3974
+ for (const [k, v] of childWrapperInfo.envAssignments) {
3975
+ childEnvAssignments.set(k, v);
3976
+ }
3977
+ const childCwd = childWrapperInfo.cwd === null ? undefined : childWrapperInfo.cwd ?? context.cwd;
3978
+ const nestedOverrides = buildNestedOverrides(childEnvAssignments, childWrapperInfo.cwd, runsRemotely || hasDynamicStdinPlaceholder);
3093
3979
  let head = getBasename(childTokens[0] ?? "").toLowerCase();
3094
3980
  if (head === "busybox" && childTokens.length > 1) {
3095
3981
  childTokens = childTokens.slice(1);
@@ -3098,27 +3984,27 @@ function analyzeParallel(tokens, context) {
3098
3984
  if (SHELL_WRAPPERS.has(head)) {
3099
3985
  const dashCArg = extractDashCArg(childTokens);
3100
3986
  if (dashCArg) {
3101
- if (dashCArg === "{}" || dashCArg === "{1}") {
3987
+ if (isOnlyParallelPlaceholder(dashCArg)) {
3102
3988
  return REASON_PARALLEL_SHELL;
3103
3989
  }
3104
- if (dashCArg.includes("{}")) {
3990
+ if (hasParallelPlaceholder(dashCArg)) {
3105
3991
  if (args.length > 0) {
3106
3992
  for (const arg of args) {
3107
- const expandedScript = dashCArg.replace(/{}/g, arg);
3108
- const reason3 = context.analyzeNested(expandedScript);
3993
+ const expandedScript = replaceParallelPlaceholder(dashCArg, arg);
3994
+ const reason3 = context.analyzeNested(expandedScript, nestedOverrides);
3109
3995
  if (reason3) {
3110
3996
  return reason3;
3111
3997
  }
3112
3998
  }
3113
3999
  return null;
3114
4000
  }
3115
- const reason2 = context.analyzeNested(dashCArg);
4001
+ const reason2 = context.analyzeNested(dashCArg, nestedOverrides);
3116
4002
  if (reason2) {
3117
4003
  return reason2;
3118
4004
  }
3119
4005
  return null;
3120
4006
  }
3121
- const reason = context.analyzeNested(dashCArg);
4007
+ const reason = context.analyzeNested(dashCArg, nestedOverrides);
3122
4008
  if (reason) {
3123
4009
  return reason;
3124
4010
  }
@@ -3140,7 +4026,7 @@ function analyzeParallel(tokens, context) {
3140
4026
  for (const arg of args) {
3141
4027
  const expandedTokens = childTokens.map((t) => t.replace(/{}/g, arg));
3142
4028
  const rmResult = analyzeRm(expandedTokens, {
3143
- cwd: context.cwd,
4029
+ cwd: childCwd,
3144
4030
  originalCwd: context.originalCwd,
3145
4031
  paranoid: context.paranoidRm,
3146
4032
  allowTmpdirVar: context.allowTmpdirVar
@@ -3154,7 +4040,7 @@ function analyzeParallel(tokens, context) {
3154
4040
  if (args.length > 0) {
3155
4041
  const expandedTokens = [...childTokens, args[0] ?? ""];
3156
4042
  const rmResult = analyzeRm(expandedTokens, {
3157
- cwd: context.cwd,
4043
+ cwd: childCwd,
3158
4044
  originalCwd: context.originalCwd,
3159
4045
  paranoid: context.paranoidRm,
3160
4046
  allowTmpdirVar: context.allowTmpdirVar
@@ -3173,13 +4059,53 @@ function analyzeParallel(tokens, context) {
3173
4059
  }
3174
4060
  }
3175
4061
  if (head === "git") {
3176
- const gitResult = analyzeGit(childTokens);
3177
- if (gitResult) {
3178
- return gitResult;
4062
+ const gitTokenSets = hasPlaceholder && args.length > 0 ? args.map((arg) => childTokens.map((token) => replaceParallelPlaceholder(token, arg))) : !hasPlaceholder && args.length > 0 ? args.map((arg) => [...childTokens, arg]) : [childTokens];
4063
+ const dynamicGitArgs = usesStdin || hasPlaceholder;
4064
+ for (const gitTokens of gitTokenSets) {
4065
+ const gitResult = analyzeGit(gitTokens, {
4066
+ cwd: childCwd,
4067
+ envAssignments: childEnvAssignments,
4068
+ worktreeMode: runsRemotely || dynamicGitArgs ? false : context.worktreeMode
4069
+ });
4070
+ if (gitResult) {
4071
+ return gitResult;
4072
+ }
3179
4073
  }
3180
4074
  }
3181
4075
  return null;
3182
4076
  }
4077
+ function buildNestedOverrides(envAssignments, cwd, runsRemotely) {
4078
+ const overrides = { envAssignments };
4079
+ if (cwd !== undefined) {
4080
+ overrides.effectiveCwd = cwd;
4081
+ }
4082
+ if (runsRemotely) {
4083
+ overrides.worktreeMode = false;
4084
+ }
4085
+ return overrides;
4086
+ }
4087
+ function buildCommandsModeOverrides(context, runsRemotely) {
4088
+ const overrides = {};
4089
+ if (context.envAssignments) {
4090
+ overrides.envAssignments = context.envAssignments;
4091
+ }
4092
+ if (context.cwd !== undefined) {
4093
+ overrides.effectiveCwd = context.cwd;
4094
+ }
4095
+ if (runsRemotely) {
4096
+ overrides.worktreeMode = false;
4097
+ }
4098
+ return Object.keys(overrides).length > 0 ? overrides : undefined;
4099
+ }
4100
+ function replaceParallelPlaceholder(token, arg) {
4101
+ return token.replace(/\{[^{}\s]*\}/g, arg);
4102
+ }
4103
+ function hasParallelPlaceholder(token) {
4104
+ return PARALLEL_PLACEHOLDER_RE.test(token);
4105
+ }
4106
+ function isOnlyParallelPlaceholder(token) {
4107
+ return /^\{[^{}\s]*\}$/.test(token);
4108
+ }
3183
4109
  function parseParallelCommand(tokens) {
3184
4110
  const parallelOptsWithValue = new Set([
3185
4111
  "-S",
@@ -3198,6 +4124,7 @@ function parseParallelCommand(tokens) {
3198
4124
  let i = 1;
3199
4125
  const templateTokens = [];
3200
4126
  let markerIndex = -1;
4127
+ let runsRemotely = false;
3201
4128
  while (i < tokens.length) {
3202
4129
  const token = tokens[i];
3203
4130
  if (!token)
@@ -3221,6 +4148,21 @@ function parseParallelCommand(tokens) {
3221
4148
  break;
3222
4149
  }
3223
4150
  if (token.startsWith("-")) {
4151
+ if (token === "-S" || token === "--sshlogin" || token === "--slf" || token === "--sshloginfile") {
4152
+ runsRemotely = true;
4153
+ i += 2;
4154
+ continue;
4155
+ }
4156
+ if (token.startsWith("-S") && token.length > 2) {
4157
+ runsRemotely = true;
4158
+ i++;
4159
+ continue;
4160
+ }
4161
+ if (token.startsWith("--sshlogin=") || token.startsWith("--slf=") || token.startsWith("--sshloginfile=")) {
4162
+ runsRemotely = true;
4163
+ i++;
4164
+ continue;
4165
+ }
3224
4166
  if (token.startsWith("-j") && token.length > 2 && /^\d+$/.test(token.slice(2))) {
3225
4167
  i++;
3226
4168
  continue;
@@ -3261,16 +4203,22 @@ function parseParallelCommand(tokens) {
3261
4203
  }
3262
4204
  }
3263
4205
  }
3264
- const hasPlaceholder = templateTokens.some((t) => t.includes("{}") || t.includes("{1}") || t.includes("{.}"));
4206
+ const hasPlaceholder = templateTokens.some(hasParallelPlaceholder);
3265
4207
  if (templateTokens.length === 0 && markerIndex === -1) {
3266
4208
  return null;
3267
4209
  }
3268
- return { template: templateTokens, args, hasPlaceholder };
4210
+ return {
4211
+ template: templateTokens,
4212
+ args,
4213
+ hasPlaceholder,
4214
+ runsRemotely,
4215
+ usesStdin: markerIndex === -1
4216
+ };
3269
4217
  }
3270
4218
 
3271
4219
  // src/core/analyze/tmpdir.ts
3272
4220
  import { tmpdir as tmpdir2 } from "node:os";
3273
- import { normalize as normalize2, sep as sep2 } from "node:path";
4221
+ import { normalize as normalize2, sep as sep3 } from "node:path";
3274
4222
  function isTmpdirOverriddenToNonTemp(envAssignments) {
3275
4223
  if (!envAssignments.has("TMPDIR")) {
3276
4224
  return false;
@@ -3290,16 +4238,23 @@ function isPathOrSubpath(path, basePath) {
3290
4238
  if (path === basePath) {
3291
4239
  return true;
3292
4240
  }
3293
- const baseWithSlash = basePath.endsWith(sep2) ? basePath : `${basePath}${sep2}`;
4241
+ const baseWithSlash = basePath.endsWith(sep3) ? basePath : `${basePath}${sep3}`;
3294
4242
  return path.startsWith(baseWithSlash);
3295
4243
  }
3296
4244
 
3297
4245
  // src/core/analyze/xargs.ts
3298
4246
  var REASON_XARGS_RM = "xargs rm -rf with dynamic input is dangerous. Use explicit file list instead.";
3299
4247
  var REASON_XARGS_SHELL = "xargs with shell -c can execute arbitrary commands from dynamic input.";
4248
+ var XARGS_APPENDED_INPUT = "__CC_SAFETY_NET_XARGS_INPUT__";
3300
4249
  function analyzeXargs(tokens, context) {
3301
- const { childTokens: rawChildTokens } = extractXargsChildCommandWithInfo(tokens);
3302
- let childTokens = stripWrappers(rawChildTokens);
4250
+ const { childTokens: rawChildTokens, replacementToken } = extractXargsChildCommandWithInfo(tokens);
4251
+ const childWrapperInfo = stripWrappersWithInfo(rawChildTokens, context.cwd);
4252
+ let childTokens = childWrapperInfo.tokens;
4253
+ const childEnvAssignments = new Map(context.envAssignments ?? []);
4254
+ for (const [k, v] of childWrapperInfo.envAssignments) {
4255
+ childEnvAssignments.set(k, v);
4256
+ }
4257
+ const childCwd = childWrapperInfo.cwd === null ? undefined : childWrapperInfo.cwd ?? context.cwd;
3303
4258
  if (childTokens.length === 0) {
3304
4259
  return null;
3305
4260
  }
@@ -3313,7 +4268,7 @@ function analyzeXargs(tokens, context) {
3313
4268
  }
3314
4269
  if (head === "rm" && hasRecursiveForceFlags(childTokens)) {
3315
4270
  const rmResult = analyzeRm(childTokens, {
3316
- cwd: context.cwd,
4271
+ cwd: childCwd,
3317
4272
  originalCwd: context.originalCwd,
3318
4273
  paranoid: context.paranoidRm,
3319
4274
  allowTmpdirVar: context.allowTmpdirVar
@@ -3330,7 +4285,13 @@ function analyzeXargs(tokens, context) {
3330
4285
  }
3331
4286
  }
3332
4287
  if (head === "git") {
3333
- const gitResult = analyzeGit(childTokens);
4288
+ const gitTokens = replacementToken === null ? [...childTokens, XARGS_APPENDED_INPUT] : childTokens;
4289
+ const hasDynamicReplacement = replacementToken !== null && childTokens.some((token) => token.includes(replacementToken));
4290
+ const gitResult = analyzeGit(gitTokens, {
4291
+ cwd: childCwd,
4292
+ envAssignments: childEnvAssignments,
4293
+ worktreeMode: replacementToken === null || hasDynamicReplacement ? false : context.worktreeMode
4294
+ });
3334
4295
  if (gitResult) {
3335
4296
  return gitResult;
3336
4297
  }
@@ -3500,9 +4461,17 @@ function analyzeSegment(tokens, depth, options) {
3500
4461
  if (tokens.length === 0) {
3501
4462
  return null;
3502
4463
  }
4464
+ const { cwdForRm: baseCwdForRm, originalCwd } = deriveCwdContext(options);
3503
4465
  const { tokens: strippedEnv, envAssignments: leadingEnvAssignments } = stripEnvAssignmentsWithInfo(tokens);
3504
- const { tokens: stripped, envAssignments: wrapperEnvAssignments } = stripWrappersWithInfo(strippedEnv);
3505
- const envAssignments = new Map(leadingEnvAssignments);
4466
+ const {
4467
+ tokens: stripped,
4468
+ envAssignments: wrapperEnvAssignments,
4469
+ cwd: wrapperCwd
4470
+ } = stripWrappersWithInfo(strippedEnv, baseCwdForRm);
4471
+ const envAssignments = new Map(options.envAssignments ?? []);
4472
+ for (const [k, v] of leadingEnvAssignments) {
4473
+ envAssignments.set(k, v);
4474
+ }
3506
4475
  for (const [k, v] of wrapperEnvAssignments) {
3507
4476
  envAssignments.set(k, v);
3508
4477
  }
@@ -3515,12 +4484,16 @@ function analyzeSegment(tokens, depth, options) {
3515
4484
  }
3516
4485
  const normalizedHead = normalizeCommandToken(head);
3517
4486
  const basename = getBasename(head);
3518
- const { cwdForRm, originalCwd } = deriveCwdContext(options);
4487
+ const cwdForRm = wrapperCwd === null ? undefined : wrapperCwd ?? baseCwdForRm;
4488
+ const nestedEffectiveCwd = wrapperCwd === undefined ? options.effectiveCwd : wrapperCwd;
3519
4489
  const allowTmpdirVar = !isTmpdirOverriddenToNonTemp(envAssignments);
3520
4490
  if (SHELL_WRAPPERS.has(normalizedHead)) {
3521
4491
  const dashCArg = extractDashCArg(stripped);
3522
4492
  if (dashCArg) {
3523
- return options.analyzeNested(dashCArg);
4493
+ return options.analyzeNested(dashCArg, {
4494
+ effectiveCwd: nestedEffectiveCwd,
4495
+ envAssignments
4496
+ });
3524
4497
  }
3525
4498
  }
3526
4499
  if (INTERPRETERS.has(normalizedHead)) {
@@ -3529,7 +4502,10 @@ function analyzeSegment(tokens, depth, options) {
3529
4502
  if (options.paranoidInterpreters) {
3530
4503
  return REASON_INTERPRETER_BLOCKED + PARANOID_INTERPRETERS_SUFFIX;
3531
4504
  }
3532
- const innerReason = options.analyzeNested(codeArg);
4505
+ const innerReason = options.analyzeNested(codeArg, {
4506
+ effectiveCwd: nestedEffectiveCwd,
4507
+ envAssignments
4508
+ });
3533
4509
  if (innerReason) {
3534
4510
  return innerReason;
3535
4511
  }
@@ -3539,7 +4515,11 @@ function analyzeSegment(tokens, depth, options) {
3539
4515
  }
3540
4516
  }
3541
4517
  if (normalizedHead === "busybox" && stripped.length > 1) {
3542
- return analyzeSegment(stripped.slice(1), depth, options);
4518
+ return analyzeSegment(stripped.slice(1), depth, {
4519
+ ...options,
4520
+ effectiveCwd: nestedEffectiveCwd,
4521
+ envAssignments
4522
+ });
3543
4523
  }
3544
4524
  const isGit = basename.toLowerCase() === "git";
3545
4525
  const isRm = basename === "rm";
@@ -3547,7 +4527,11 @@ function analyzeSegment(tokens, depth, options) {
3547
4527
  const isXargs = basename === "xargs";
3548
4528
  const isParallel = basename === "parallel";
3549
4529
  if (isGit) {
3550
- const gitResult = analyzeGit(stripped);
4530
+ const gitResult = analyzeGit(stripped, {
4531
+ cwd: cwdForRm,
4532
+ envAssignments,
4533
+ worktreeMode: options.worktreeMode
4534
+ });
3551
4535
  if (gitResult) {
3552
4536
  return gitResult;
3553
4537
  }
@@ -3574,7 +4558,9 @@ function analyzeSegment(tokens, depth, options) {
3574
4558
  cwd: cwdForRm,
3575
4559
  originalCwd,
3576
4560
  paranoidRm: options.paranoidRm,
3577
- allowTmpdirVar
4561
+ allowTmpdirVar,
4562
+ envAssignments,
4563
+ worktreeMode: options.worktreeMode
3578
4564
  });
3579
4565
  if (xargsResult) {
3580
4566
  return xargsResult;
@@ -3586,6 +4572,8 @@ function analyzeSegment(tokens, depth, options) {
3586
4572
  originalCwd,
3587
4573
  paranoidRm: options.paranoidRm,
3588
4574
  allowTmpdirVar,
4575
+ envAssignments,
4576
+ worktreeMode: options.worktreeMode,
3589
4577
  analyzeNested: options.analyzeNested
3590
4578
  });
3591
4579
  if (parallelResult) {
@@ -3614,7 +4602,11 @@ function analyzeSegment(tokens, depth, options) {
3614
4602
  }
3615
4603
  if (cmd === "git") {
3616
4604
  const gitTokens = ["git", ...stripped.slice(i + 1)];
3617
- const reason = analyzeGit(gitTokens);
4605
+ const reason = analyzeGit(gitTokens, {
4606
+ cwd: cwdForRm,
4607
+ envAssignments,
4608
+ worktreeMode: false
4609
+ });
3618
4610
  if (reason) {
3619
4611
  return reason;
3620
4612
  }
@@ -3671,6 +4663,8 @@ function stripLeadingGrouping(tokens) {
3671
4663
  // src/core/analyze/analyze-command.ts
3672
4664
  var REASON_STRICT_UNPARSEABLE = "Command could not be safely analyzed (strict mode). Verify manually.";
3673
4665
  var REASON_RECURSION_LIMIT = "Command exceeds maximum recursion depth and cannot be safely analyzed.";
4666
+ var GIT_CONTEXT_ENV_OVERRIDE_NAMES2 = new Set(GIT_CONTEXT_ENV_OVERRIDES);
4667
+ var GIT_CONTEXT_APPEND_ASSIGNMENT_RE = /^([A-Za-z_][A-Za-z0-9_]*)\+=/;
3674
4668
  function analyzeCommandInternal(command, depth, options) {
3675
4669
  if (depth >= MAX_RECURSION_DEPTH) {
3676
4670
  return { reason: REASON_RECURSION_LIMIT, segment: command };
@@ -3681,8 +4675,10 @@ function analyzeCommandInternal(command, depth, options) {
3681
4675
  }
3682
4676
  const originalCwd = options.cwd;
3683
4677
  let effectiveCwd = options.effectiveCwd !== undefined ? options.effectiveCwd : options.cwd;
4678
+ const shellGitContextState = createShellGitContextEnvState(options.envAssignments);
3684
4679
  for (const segment of segments) {
3685
4680
  const segmentStr = segment.join(" ");
4681
+ const segmentEnvAssignments = getSegmentGitContextEnvAssignments(segment, shellGitContextState);
3686
4682
  if (segment.length === 1 && segment[0]?.includes(" ")) {
3687
4683
  const textReason = dangerousInText(segment[0]);
3688
4684
  if (textReason) {
@@ -3697,8 +4693,15 @@ function analyzeCommandInternal(command, depth, options) {
3697
4693
  ...options,
3698
4694
  cwd: originalCwd,
3699
4695
  effectiveCwd,
3700
- analyzeNested: (nestedCommand) => {
3701
- return analyzeCommandInternal(nestedCommand, depth + 1, { ...options, effectiveCwd })?.reason ?? null;
4696
+ envAssignments: segmentEnvAssignments,
4697
+ analyzeNested: (nestedCommand, overrides) => {
4698
+ const nestedEffectiveCwd = overrides && Object.hasOwn(overrides, "effectiveCwd") ? overrides.effectiveCwd : effectiveCwd;
4699
+ return analyzeCommandInternal(nestedCommand, depth + 1, {
4700
+ ...options,
4701
+ effectiveCwd: nestedEffectiveCwd,
4702
+ envAssignments: overrides?.envAssignments ?? segmentEnvAssignments,
4703
+ worktreeMode: overrides?.worktreeMode ?? options.worktreeMode
4704
+ })?.reason ?? null;
3702
4705
  }
3703
4706
  });
3704
4707
  if (reason) {
@@ -3707,9 +4710,338 @@ function analyzeCommandInternal(command, depth, options) {
3707
4710
  if (segmentChangesCwd(segment)) {
3708
4711
  effectiveCwd = null;
3709
4712
  }
4713
+ applyShellGitContextEnvSegment(segment, shellGitContextState);
3710
4714
  }
3711
4715
  return null;
3712
4716
  }
4717
+ function createShellGitContextEnvState(effectiveEnvAssignments) {
4718
+ return {
4719
+ effectiveEnvAssignments,
4720
+ shellAssignments: new Map,
4721
+ exportedNames: getInitiallyExportedGitContextNames(effectiveEnvAssignments),
4722
+ allexport: false,
4723
+ keywordExport: false
4724
+ };
4725
+ }
4726
+ function applyShellGitContextEnvSegment(tokens, state) {
4727
+ const commandInfo = getShellCommandInfo(tokens);
4728
+ if (!commandInfo) {
4729
+ return;
4730
+ }
4731
+ const { command, commandIndex, leadingAssignments } = commandInfo;
4732
+ if (command === null) {
4733
+ for (const assignment of leadingAssignments.values()) {
4734
+ setShellGitContextAssignment(state, assignment);
4735
+ }
4736
+ return;
4737
+ }
4738
+ if (command === "set") {
4739
+ const changes = getSetOptionChanges(tokens, commandIndex);
4740
+ if (changes.allexport !== null) {
4741
+ state.allexport = changes.allexport;
4742
+ }
4743
+ if (changes.keywordExport !== null) {
4744
+ state.keywordExport = changes.keywordExport;
4745
+ }
4746
+ return;
4747
+ }
4748
+ if (command !== "export" && command !== "typeset" && command !== "declare" && command !== "readonly") {
4749
+ return;
4750
+ }
4751
+ for (const assignment of leadingAssignments.values()) {
4752
+ setShellGitContextAssignment(state, assignment);
4753
+ }
4754
+ if (command === "export") {
4755
+ const operandsStart = getExportOperandsStart(tokens, commandIndex);
4756
+ if (operandsStart === null) {
4757
+ return;
4758
+ }
4759
+ for (const token of tokens.slice(operandsStart)) {
4760
+ addExportedGitContextEnvAssignment(state, token);
4761
+ }
4762
+ return;
4763
+ }
4764
+ const operandsInfo = getTypesetOperandsInfo(tokens, commandIndex);
4765
+ if (operandsInfo === null) {
4766
+ return;
4767
+ }
4768
+ for (const token of tokens.slice(operandsInfo.operandsStart)) {
4769
+ addTypesetGitContextEnvAssignment(state, token, operandsInfo.exports, command === "readonly" ? leadingAssignments : undefined);
4770
+ }
4771
+ }
4772
+ function getSegmentGitContextEnvAssignments(tokens, state) {
4773
+ if (!state.keywordExport) {
4774
+ return state.effectiveEnvAssignments;
4775
+ }
4776
+ let nextEnvAssignments = null;
4777
+ for (const token of tokens) {
4778
+ const assignment = parseGitContextEnvAssignment(token);
4779
+ if (!assignment) {
4780
+ continue;
4781
+ }
4782
+ nextEnvAssignments ??= new Map(state.effectiveEnvAssignments ?? []);
4783
+ nextEnvAssignments.set(assignment.name, assignment.value);
4784
+ }
4785
+ return nextEnvAssignments ?? state.effectiveEnvAssignments;
4786
+ }
4787
+ function getShellCommandInfo(tokens) {
4788
+ const leadingAssignments = new Map;
4789
+ let i = 0;
4790
+ while (i < tokens.length) {
4791
+ const token = tokens[i];
4792
+ if (!token) {
4793
+ return null;
4794
+ }
4795
+ const assignment = parseShellAssignment(token);
4796
+ if (!assignment) {
4797
+ break;
4798
+ }
4799
+ if (isTrackedGitEnvName2(assignment.name)) {
4800
+ leadingAssignments.set(assignment.name, assignment);
4801
+ }
4802
+ i++;
4803
+ }
4804
+ if (i >= tokens.length) {
4805
+ return { command: null, commandIndex: i, leadingAssignments };
4806
+ }
4807
+ let commandIndex = i;
4808
+ let command = tokens[commandIndex] ?? null;
4809
+ if (command === "builtin") {
4810
+ commandIndex++;
4811
+ if (tokens[commandIndex] === "--") {
4812
+ commandIndex++;
4813
+ }
4814
+ command = tokens[commandIndex] ?? null;
4815
+ }
4816
+ if (command === "command") {
4817
+ const commandBuiltinInfo = getCommandBuiltinTarget(tokens, commandIndex);
4818
+ if (!commandBuiltinInfo) {
4819
+ return null;
4820
+ }
4821
+ commandIndex = commandBuiltinInfo.commandIndex;
4822
+ command = commandBuiltinInfo.command;
4823
+ }
4824
+ if (command === null) {
4825
+ return null;
4826
+ }
4827
+ return { command, commandIndex, leadingAssignments };
4828
+ }
4829
+ function getCommandBuiltinTarget(tokens, commandIndex) {
4830
+ let i = commandIndex + 1;
4831
+ while (i < tokens.length) {
4832
+ const token = tokens[i];
4833
+ if (!token) {
4834
+ return null;
4835
+ }
4836
+ if (token === "--") {
4837
+ i++;
4838
+ break;
4839
+ }
4840
+ if (token === "-p") {
4841
+ i++;
4842
+ continue;
4843
+ }
4844
+ if (token === "-v" || token === "-V") {
4845
+ return null;
4846
+ }
4847
+ break;
4848
+ }
4849
+ const command = tokens[i];
4850
+ return command ? { command, commandIndex: i } : null;
4851
+ }
4852
+ function parseShellAssignment(token) {
4853
+ return parseEnvAssignment(token) ?? parseGitContextAppendEnvAssignment2(token);
4854
+ }
4855
+ function parseGitContextEnvAssignment(token) {
4856
+ const assignment = parseEnvAssignment(token) ?? parseGitContextAppendEnvAssignment2(token);
4857
+ if (!assignment || !isTrackedGitEnvName2(assignment.name)) {
4858
+ return null;
4859
+ }
4860
+ return assignment;
4861
+ }
4862
+ function parseGitContextAppendEnvAssignment2(token) {
4863
+ const match = token.match(GIT_CONTEXT_APPEND_ASSIGNMENT_RE);
4864
+ const name = match?.[1];
4865
+ if (!name || !isTrackedGitEnvName2(name)) {
4866
+ return null;
4867
+ }
4868
+ const eqIdx = token.indexOf("=");
4869
+ return { name, value: token.slice(eqIdx + 1) };
4870
+ }
4871
+ function isTrackedGitEnvName2(name) {
4872
+ return GIT_CONTEXT_ENV_OVERRIDE_NAMES2.has(name) || GIT_CONFIG_AFFECTING_ENV_NAMES.has(name) || isGitConfigEnvName2(name);
4873
+ }
4874
+ function isGitConfigEnvName2(name) {
4875
+ return name === "GIT_CONFIG_COUNT" || name === "GIT_CONFIG_PARAMETERS" || /^GIT_CONFIG_(KEY|VALUE)_\d+$/.test(name);
4876
+ }
4877
+ function getInitiallyExportedGitContextNames(effectiveEnvAssignments) {
4878
+ const exportedNames = new Set;
4879
+ for (const name of Object.keys(process.env)) {
4880
+ if (isTrackedGitEnvName2(name)) {
4881
+ exportedNames.add(name);
4882
+ }
4883
+ }
4884
+ for (const name of effectiveEnvAssignments?.keys() ?? []) {
4885
+ if (isTrackedGitEnvName2(name)) {
4886
+ exportedNames.add(name);
4887
+ }
4888
+ }
4889
+ return exportedNames;
4890
+ }
4891
+ function setShellGitContextAssignment(state, assignment) {
4892
+ state.shellAssignments.set(assignment.name, assignment.value);
4893
+ if (state.allexport || state.exportedNames.has(assignment.name)) {
4894
+ setEffectiveGitContextAssignment(state, assignment);
4895
+ }
4896
+ }
4897
+ function setEffectiveGitContextAssignment(state, assignment) {
4898
+ const nextEnvAssignments = new Map(state.effectiveEnvAssignments ?? []);
4899
+ nextEnvAssignments.set(assignment.name, assignment.value);
4900
+ state.effectiveEnvAssignments = nextEnvAssignments;
4901
+ }
4902
+ function addExportedGitContextEnvAssignment(state, token) {
4903
+ const assignment = parseGitContextEnvAssignment(token);
4904
+ if (assignment) {
4905
+ state.shellAssignments.set(assignment.name, assignment.value);
4906
+ state.exportedNames.add(assignment.name);
4907
+ setEffectiveGitContextAssignment(state, assignment);
4908
+ return;
4909
+ }
4910
+ if (isTrackedGitEnvName2(token)) {
4911
+ state.exportedNames.add(token);
4912
+ const value = state.shellAssignments.get(token);
4913
+ if (value !== undefined) {
4914
+ setEffectiveGitContextAssignment(state, { name: token, value });
4915
+ } else {
4916
+ setEffectiveGitContextAssignment(state, { name: token, value: "" });
4917
+ }
4918
+ }
4919
+ }
4920
+ function addTypesetGitContextEnvAssignment(state, token, exports, readonlyLeadingAssignments) {
4921
+ const assignment = parseGitContextEnvAssignment(token);
4922
+ if (assignment) {
4923
+ state.shellAssignments.set(assignment.name, assignment.value);
4924
+ if (exports) {
4925
+ state.exportedNames.add(assignment.name);
4926
+ setEffectiveGitContextAssignment(state, assignment);
4927
+ } else if (state.allexport || state.exportedNames.has(assignment.name)) {
4928
+ setEffectiveGitContextAssignment(state, assignment);
4929
+ }
4930
+ return;
4931
+ }
4932
+ const readonlyAssignment = readonlyLeadingAssignments?.get(token);
4933
+ if (readonlyAssignment) {
4934
+ state.exportedNames.add(token);
4935
+ setEffectiveGitContextAssignment(state, readonlyAssignment);
4936
+ return;
4937
+ }
4938
+ if (exports && isTrackedGitEnvName2(token)) {
4939
+ state.exportedNames.add(token);
4940
+ const value = state.shellAssignments.get(token);
4941
+ if (value !== undefined) {
4942
+ setEffectiveGitContextAssignment(state, { name: token, value });
4943
+ } else {
4944
+ setEffectiveGitContextAssignment(state, { name: token, value: "" });
4945
+ }
4946
+ }
4947
+ }
4948
+ function getExportOperandsStart(tokens, commandIndex) {
4949
+ let i = commandIndex + 1;
4950
+ while (i < tokens.length) {
4951
+ const token = tokens[i];
4952
+ if (!token) {
4953
+ return null;
4954
+ }
4955
+ if (token === "--") {
4956
+ return i + 1;
4957
+ }
4958
+ if (token === "-p") {
4959
+ i++;
4960
+ continue;
4961
+ }
4962
+ if (token.startsWith("-")) {
4963
+ return null;
4964
+ }
4965
+ return i;
4966
+ }
4967
+ return i;
4968
+ }
4969
+ function getTypesetOperandsInfo(tokens, commandIndex) {
4970
+ let i = commandIndex + 1;
4971
+ let hasExportFlag = false;
4972
+ while (i < tokens.length) {
4973
+ const token = tokens[i];
4974
+ if (!token) {
4975
+ return null;
4976
+ }
4977
+ if (token === "--") {
4978
+ return { operandsStart: i + 1, exports: hasExportFlag };
4979
+ }
4980
+ if (token.startsWith("-")) {
4981
+ if (token.slice(1).includes("x")) {
4982
+ hasExportFlag = true;
4983
+ }
4984
+ i++;
4985
+ continue;
4986
+ }
4987
+ if (token.startsWith("+")) {
4988
+ if (token.slice(1).includes("x")) {
4989
+ hasExportFlag = false;
4990
+ }
4991
+ i++;
4992
+ continue;
4993
+ }
4994
+ return { operandsStart: i, exports: hasExportFlag };
4995
+ }
4996
+ return { operandsStart: i, exports: hasExportFlag };
4997
+ }
4998
+ function getSetOptionChanges(tokens, commandIndex) {
4999
+ const changes = { allexport: null, keywordExport: null };
5000
+ let i = commandIndex + 1;
5001
+ while (i < tokens.length) {
5002
+ const token = tokens[i];
5003
+ if (!token) {
5004
+ return changes;
5005
+ }
5006
+ if (token === "--") {
5007
+ return changes;
5008
+ }
5009
+ if (token === "-o" || token === "+o") {
5010
+ if (tokens[i + 1] === "allexport") {
5011
+ changes.allexport = token === "-o";
5012
+ }
5013
+ if (tokens[i + 1] === "keyword") {
5014
+ changes.keywordExport = token === "-o";
5015
+ }
5016
+ i += 2;
5017
+ continue;
5018
+ }
5019
+ if (token.startsWith("-") && token.length > 1) {
5020
+ const flags = token.slice(1);
5021
+ if (flags.includes("a")) {
5022
+ changes.allexport = true;
5023
+ }
5024
+ if (flags.includes("k")) {
5025
+ changes.keywordExport = true;
5026
+ }
5027
+ i++;
5028
+ continue;
5029
+ }
5030
+ if (token.startsWith("+") && token.length > 1) {
5031
+ const flags = token.slice(1);
5032
+ if (flags.includes("a")) {
5033
+ changes.allexport = false;
5034
+ }
5035
+ if (flags.includes("k")) {
5036
+ changes.keywordExport = false;
5037
+ }
5038
+ i++;
5039
+ continue;
5040
+ }
5041
+ return changes;
5042
+ }
5043
+ return changes;
5044
+ }
3713
5045
 
3714
5046
  // src/core/analyze.ts
3715
5047
  function analyzeCommand(command, options = {}) {
@@ -3719,6 +5051,12 @@ function analyzeCommand(command, options = {}) {
3719
5051
 
3720
5052
  // src/bin/doctor/hooks.ts
3721
5053
  var COPILOT_PLUGIN_CONFIG_PATH = "copilot-plugin";
5054
+ var CLAUDE_PLUGIN_LIST_CONFIG_PATH = "claude plugin list";
5055
+ var CLAUDE_SAFETY_NET_PLUGIN_ID = "safety-net@cc-marketplace";
5056
+ var GEMINI_EXTENSIONS_LIST_CONFIG_PATH = "gemini extensions list";
5057
+ var GEMINI_SAFETY_NET_SOURCE = "https://github.com/kenryu42/gemini-safety-net";
5058
+ var CODEX_PLUGIN_HOOKS_WARNING = "Codex plugin hooks are behind a feature flag. Add `plugin_hooks = true` under [features] in $CODEX_HOME/config.toml.";
5059
+ var CODEX_SAFETY_NET_PLUGIN_ID = "safety-net@cc-marketplace";
3722
5060
  var SELF_TEST_CASES = [
3723
5061
  { command: "git reset --hard", description: "git reset --hard", expectBlocked: true },
3724
5062
  { command: "rm -rf /", description: "rm -rf /", expectBlocked: true },
@@ -3726,7 +5064,7 @@ var SELF_TEST_CASES = [
3726
5064
  ];
3727
5065
  var SELF_TEST_CONFIG = { version: 1, rules: [] };
3728
5066
  function runSelfTest() {
3729
- const selfTestCwd = join3(tmpdir3(), "cc-safety-net-self-test");
5067
+ const selfTestCwd = join5(tmpdir3(), "cc-safety-net-self-test");
3730
5068
  const results = SELF_TEST_CASES.map((tc) => {
3731
5069
  const result = analyzeCommand(tc.command, {
3732
5070
  cwd: selfTestCwd,
@@ -3834,50 +5172,63 @@ function stripJsonComments(content) {
3834
5172
  }
3835
5173
  return result;
3836
5174
  }
3837
- function detectClaudeCode(homeDir) {
3838
- const errors = [];
3839
- const settingsPath = join3(homeDir, ".claude", "settings.json");
3840
- const pluginKey = "safety-net@cc-marketplace";
3841
- if (existsSync4(settingsPath)) {
3842
- try {
3843
- const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
3844
- const pluginValue = settings.enabledPlugins?.[pluginKey];
3845
- if (pluginValue === true) {
3846
- return {
3847
- platform: "claude-code",
3848
- status: "configured",
3849
- method: "marketplace plugin",
3850
- configPath: settingsPath,
3851
- selfTest: runSelfTest()
3852
- };
3853
- }
3854
- if (pluginValue === false) {
3855
- return {
3856
- platform: "claude-code",
3857
- status: "disabled",
3858
- method: "marketplace plugin",
3859
- configPath: settingsPath
3860
- };
3861
- }
3862
- } catch (e) {
3863
- errors.push(`Failed to parse settings.json: ${e instanceof Error ? e.message : String(e)}`);
3864
- }
5175
+ function detectClaudeCode(pluginListOutput) {
5176
+ if (!pluginListOutput) {
5177
+ return { platform: "claude-code", status: "n/a" };
5178
+ }
5179
+ const pluginBlock = _findClaudeSafetyNetPluginBlock(pluginListOutput);
5180
+ if (!pluginBlock) {
5181
+ return { platform: "claude-code", status: "n/a" };
5182
+ }
5183
+ if (/^\s*Status:\s*.*\bdisabled\b\s*$/im.test(pluginBlock)) {
5184
+ return {
5185
+ platform: "claude-code",
5186
+ status: "disabled",
5187
+ method: "plugin list",
5188
+ configPath: CLAUDE_PLUGIN_LIST_CONFIG_PATH
5189
+ };
5190
+ }
5191
+ if (/^\s*Status:\s*.*\benabled\b\s*$/im.test(pluginBlock)) {
5192
+ return {
5193
+ platform: "claude-code",
5194
+ status: "configured",
5195
+ method: "plugin list",
5196
+ configPath: CLAUDE_PLUGIN_LIST_CONFIG_PATH,
5197
+ selfTest: runSelfTest()
5198
+ };
3865
5199
  }
3866
5200
  return {
3867
5201
  platform: "claude-code",
3868
- status: "n/a",
3869
- errors: errors.length > 0 ? errors : undefined
5202
+ status: "disabled",
5203
+ method: "plugin list",
5204
+ configPath: CLAUDE_PLUGIN_LIST_CONFIG_PATH,
5205
+ errors: ["Status is not enabled"]
3870
5206
  };
3871
5207
  }
5208
+ function _findClaudeSafetyNetPluginBlock(output) {
5209
+ const pluginLinePattern = new RegExp(`^\\s*(?:[^\\w\\s@]+\\s+)?${_escapeRegExp(CLAUDE_SAFETY_NET_PLUGIN_ID)}\\s*$`);
5210
+ const pluginStartPattern = /^\s*(?:[^\w\s@]+\s+)?\S+@\S+\s*$/;
5211
+ const lines = output.split(`
5212
+ `);
5213
+ const startIndex = lines.findIndex((line) => pluginLinePattern.test(line));
5214
+ if (startIndex === -1)
5215
+ return;
5216
+ const endIndex = lines.findIndex((line, index) => index > startIndex && pluginStartPattern.test(line));
5217
+ return lines.slice(startIndex, endIndex === -1 ? undefined : endIndex).join(`
5218
+ `);
5219
+ }
5220
+ function _escapeRegExp(value) {
5221
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5222
+ }
3872
5223
  function detectOpenCode(homeDir) {
3873
5224
  const errors = [];
3874
- const configDir = join3(homeDir, ".config", "opencode");
5225
+ const configDir = join5(homeDir, ".config", "opencode");
3875
5226
  const candidates = ["opencode.json", "opencode.jsonc"];
3876
5227
  for (const filename of candidates) {
3877
- const configPath = join3(configDir, filename);
3878
- if (existsSync4(configPath)) {
5228
+ const configPath = join5(configDir, filename);
5229
+ if (existsSync6(configPath)) {
3879
5230
  try {
3880
- const content = readFileSync4(configPath, "utf-8");
5231
+ const content = readFileSync6(configPath, "utf-8");
3881
5232
  const json = stripJsonComments(content);
3882
5233
  const config = JSON.parse(json);
3883
5234
  const plugins = config.plugin ?? [];
@@ -3903,79 +5254,143 @@ function detectOpenCode(homeDir) {
3903
5254
  errors: errors.length > 0 ? errors : undefined
3904
5255
  };
3905
5256
  }
3906
- function checkGeminiHooksEnabled(homeDir, cwd, errors) {
3907
- const candidates = [
3908
- join3(homeDir, ".gemini", "settings.json"),
3909
- join3(cwd, ".gemini", "settings.json")
5257
+ function detectGeminiCLI(extensionsListOutput) {
5258
+ if (!extensionsListOutput) {
5259
+ return { platform: "gemini-cli", status: "n/a" };
5260
+ }
5261
+ const extension = _parseGeminiExtensionsList(extensionsListOutput).find((item) => item.source?.includes(GEMINI_SAFETY_NET_SOURCE));
5262
+ if (!extension) {
5263
+ return { platform: "gemini-cli", status: "n/a" };
5264
+ }
5265
+ const effectiveEnabled = extension.enabledWorkspace ?? extension.enabledUser ?? true;
5266
+ const errors = effectiveEnabled ? [] : [
5267
+ extension.enabledWorkspace === false ? "Enabled (Workspace) is false" : "Enabled (User) is false"
3910
5268
  ];
3911
- for (const settingsPath of candidates) {
3912
- if (existsSync4(settingsPath)) {
3913
- try {
3914
- const settings = JSON.parse(readFileSync4(settingsPath, "utf-8"));
3915
- if (settings.tools?.enableHooks === true) {
3916
- return { enabled: true, configPath: settingsPath };
3917
- }
3918
- } catch (e) {
3919
- errors.push(`Failed to parse ${settingsPath}: ${e instanceof Error ? e.message : String(e)}`);
3920
- }
5269
+ if (errors.length > 0) {
5270
+ return {
5271
+ platform: "gemini-cli",
5272
+ status: "disabled",
5273
+ method: "extension list",
5274
+ configPath: GEMINI_EXTENSIONS_LIST_CONFIG_PATH,
5275
+ errors
5276
+ };
5277
+ }
5278
+ return {
5279
+ platform: "gemini-cli",
5280
+ status: "configured",
5281
+ method: "extension list",
5282
+ configPath: GEMINI_EXTENSIONS_LIST_CONFIG_PATH,
5283
+ selfTest: runSelfTest()
5284
+ };
5285
+ }
5286
+ function _parseGeminiExtensionsList(output) {
5287
+ const blocks = output.split(`
5288
+ `).reduce((result, line) => {
5289
+ if (/^\S/.test(line) || result.length === 0) {
5290
+ result.push(line);
5291
+ return result;
3921
5292
  }
5293
+ const index = result.length - 1;
5294
+ result[index] = `${result[index]}
5295
+ ${line}`;
5296
+ return result;
5297
+ }, []);
5298
+ return blocks.map((block) => ({
5299
+ source: /^\s*Source:\s*(.+)$/m.exec(block)?.[1],
5300
+ enabledUser: _parseGeminiEnabledValue(block, "User"),
5301
+ enabledWorkspace: _parseGeminiEnabledValue(block, "Workspace")
5302
+ }));
5303
+ }
5304
+ function _parseGeminiEnabledValue(block, scope) {
5305
+ const match = new RegExp(`^\\s*Enabled \\(${scope}\\):\\s*(true|false)\\s*$`, "im").exec(block);
5306
+ if (!match)
5307
+ return;
5308
+ return match[1] === "true";
5309
+ }
5310
+ function _getCodexHome(homeDir) {
5311
+ return process.env.CODEX_HOME || join5(homeDir, ".codex");
5312
+ }
5313
+ function _parseCodexConfig(content) {
5314
+ const result = {};
5315
+ content.split(`
5316
+ `).reduce((activeSection, line) => {
5317
+ const trimmed = line.trim();
5318
+ if (trimmed === "" || trimmed.startsWith("#"))
5319
+ return activeSection;
5320
+ const sectionMatch = /^\[([^\]]+)]\s*(?:#.*)?$/.exec(trimmed);
5321
+ if (sectionMatch)
5322
+ return sectionMatch[1];
5323
+ if (activeSection === "features") {
5324
+ const pluginHooksMatch = /^plugin_hooks\s*=\s*(true|false)\s*(?:#.*)?$/.exec(trimmed);
5325
+ if (pluginHooksMatch)
5326
+ result.pluginHooks = pluginHooksMatch[1] === "true";
5327
+ }
5328
+ if (activeSection === `plugins."${CODEX_SAFETY_NET_PLUGIN_ID}"`) {
5329
+ const enabledMatch = /^enabled\s*=\s*(true|false)\s*(?:#.*)?$/.exec(trimmed);
5330
+ if (enabledMatch)
5331
+ result.safetyNetEnabled = enabledMatch[1] === "true";
5332
+ }
5333
+ return activeSection;
5334
+ }, undefined);
5335
+ return result;
5336
+ }
5337
+ function _readCodexConfig(configPath, errors) {
5338
+ try {
5339
+ return _parseCodexConfig(readFileSync6(configPath, "utf-8"));
5340
+ } catch (e) {
5341
+ errors.push(`Failed to read ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
5342
+ return {};
3922
5343
  }
3923
- return { enabled: false };
3924
5344
  }
3925
- function detectGeminiCLI(homeDir, cwd) {
5345
+ function detectCodex(homeDir) {
5346
+ const codexHome = _getCodexHome(homeDir);
5347
+ const pluginCachePath = join5(codexHome, "plugins", "cache", "cc-marketplace", "safety-net");
3926
5348
  const errors = [];
3927
- const extensionPath = join3(homeDir, ".gemini", "extensions", "extension-enablement.json");
3928
- if (!existsSync4(extensionPath)) {
3929
- return { platform: "gemini-cli", status: "n/a" };
5349
+ if (!existsSync6(pluginCachePath)) {
5350
+ return { platform: "codex", status: "n/a", configPath: pluginCachePath };
3930
5351
  }
3931
- let isInstalled = false;
3932
- let isEnabled = false;
3933
5352
  try {
3934
- const extensionConfig = JSON.parse(readFileSync4(extensionPath, "utf-8"));
3935
- const pluginConfig = extensionConfig["gemini-safety-net"];
3936
- if (pluginConfig) {
3937
- isInstalled = true;
3938
- const overrides = pluginConfig.overrides ?? [];
3939
- isEnabled = overrides.some((o) => !o.startsWith("!"));
5353
+ if (readdirSync2(pluginCachePath).length === 0) {
5354
+ return { platform: "codex", status: "n/a", configPath: pluginCachePath };
3940
5355
  }
3941
5356
  } catch (e) {
3942
- errors.push(`Failed to parse extension-enablement.json: ${e instanceof Error ? e.message : String(e)}`);
3943
- }
3944
- if (!isInstalled) {
3945
5357
  return {
3946
- platform: "gemini-cli",
5358
+ platform: "codex",
3947
5359
  status: "n/a",
3948
- errors: errors.length > 0 ? errors : undefined
5360
+ configPath: pluginCachePath,
5361
+ errors: [`Failed to read ${pluginCachePath}: ${e instanceof Error ? e.message : String(e)}`]
3949
5362
  };
3950
5363
  }
3951
- if (!isEnabled) {
3952
- errors.push("Plugin is installed but disabled (no enabled workspace overrides)");
5364
+ const configPath = join5(codexHome, "config.toml");
5365
+ const config = _readCodexConfig(configPath, errors);
5366
+ if (config.safetyNetEnabled !== true) {
3953
5367
  return {
3954
- platform: "gemini-cli",
5368
+ platform: "codex",
3955
5369
  status: "disabled",
3956
- method: "extension plugin",
3957
- configPath: extensionPath,
3958
- errors
5370
+ method: "plugin cache",
5371
+ configPath,
5372
+ errors: [
5373
+ ...errors,
5374
+ `Codex plugin ${CODEX_SAFETY_NET_PLUGIN_ID} is not enabled. Add enabled = true under [plugins."${CODEX_SAFETY_NET_PLUGIN_ID}"] in $CODEX_HOME/config.toml.`
5375
+ ]
3959
5376
  };
3960
5377
  }
3961
- const hooksCheck = checkGeminiHooksEnabled(homeDir, cwd, errors);
3962
- if (hooksCheck.enabled) {
5378
+ if (config.pluginHooks !== true) {
3963
5379
  return {
3964
- platform: "gemini-cli",
3965
- status: "configured",
3966
- method: "extension plugin",
3967
- configPath: extensionPath,
3968
- selfTest: runSelfTest(),
3969
- errors: errors.length > 0 ? errors : undefined
5380
+ platform: "codex",
5381
+ status: "disabled",
5382
+ method: "plugin cache",
5383
+ configPath,
5384
+ errors: [...errors, CODEX_PLUGIN_HOOKS_WARNING]
3970
5385
  };
3971
5386
  }
3972
- errors.push("Hooks are not enabled (set tools.enableHooks: true in settings.json)");
3973
5387
  return {
3974
- platform: "gemini-cli",
3975
- status: "n/a",
3976
- method: "extension plugin",
3977
- configPath: extensionPath,
3978
- errors
5388
+ platform: "codex",
5389
+ status: "configured",
5390
+ method: "plugin cache",
5391
+ configPath,
5392
+ selfTest: runSelfTest(),
5393
+ errors: errors.length > 0 ? errors : undefined
3979
5394
  };
3980
5395
  }
3981
5396
  function _isSafetyNetCopilotCommand(command) {
@@ -4018,7 +5433,7 @@ function _supportsCopilotInlineHooks(version) {
4018
5433
  return comparison >= 0;
4019
5434
  }
4020
5435
  function _getCopilotConfigHome(homeDir) {
4021
- return process.env.COPILOT_HOME || join3(homeDir, ".copilot");
5436
+ return process.env.COPILOT_HOME || join5(homeDir, ".copilot");
4022
5437
  }
4023
5438
  function _hasSafetyNetCopilotHook(config) {
4024
5439
  const preToolUseHooks = config.hooks?.preToolUse ?? [];
@@ -4030,7 +5445,7 @@ function _hasSafetyNetCopilotHook(config) {
4030
5445
  }
4031
5446
  function _readCopilotConfigFile(configPath, errors) {
4032
5447
  try {
4033
- return JSON.parse(readFileSync4(configPath, "utf-8"));
5448
+ return JSON.parse(stripJsonComments(readFileSync6(configPath, "utf-8")));
4034
5449
  } catch (e) {
4035
5450
  errors?.push(`Failed to parse ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
4036
5451
  return;
@@ -4045,11 +5460,11 @@ function _listJsonFiles(dirPath, errors) {
4045
5460
  }
4046
5461
  }
4047
5462
  function _collectSafetyNetCopilotHookFiles(dirPath, errors) {
4048
- if (!existsSync4(dirPath))
5463
+ if (!existsSync6(dirPath))
4049
5464
  return [];
4050
5465
  const matches = [];
4051
5466
  for (const filename of _listJsonFiles(dirPath, errors)) {
4052
- const configPath = join3(dirPath, filename);
5467
+ const configPath = join5(dirPath, filename);
4053
5468
  const config = _readCopilotConfigFile(configPath, errors);
4054
5469
  if (config && _hasSafetyNetCopilotHook(config)) {
4055
5470
  matches.push(configPath);
@@ -4058,7 +5473,7 @@ function _collectSafetyNetCopilotHookFiles(dirPath, errors) {
4058
5473
  return matches;
4059
5474
  }
4060
5475
  function _collectCopilotInlineConfig(configPath, errors) {
4061
- if (!existsSync4(configPath))
5476
+ if (!existsSync6(configPath))
4062
5477
  return;
4063
5478
  const config = _readCopilotConfigFile(configPath, errors);
4064
5479
  if (!config)
@@ -4088,15 +5503,15 @@ function _resolveCopilotInlineDisableSource(inlineSources) {
4088
5503
  }
4089
5504
  function _checkCopilotEnabled(homeDir, cwd, copilotCliVersion, errors) {
4090
5505
  const configHome = _getCopilotConfigHome(homeDir);
4091
- const repoHookDir = join3(cwd, ".github", "hooks");
4092
- const userHookDir = join3(configHome, "hooks");
4093
- const repoConfigDir = join3(cwd, ".github", "copilot");
5506
+ const repoHookDir = join5(cwd, ".github", "hooks");
5507
+ const userHookDir = join5(configHome, "hooks");
5508
+ const repoConfigDir = join5(cwd, ".github", "copilot");
4094
5509
  const inlineSupport = _supportsCopilotInlineHooks(copilotCliVersion);
4095
5510
  const inlineErrors = inlineSupport === true ? errors : undefined;
4096
5511
  const inlineSources = {
4097
- userConfig: _collectCopilotInlineConfig(join3(configHome, "config.json"), inlineErrors),
4098
- repoSettings: _collectCopilotInlineConfig(join3(repoConfigDir, "settings.json"), inlineErrors),
4099
- localSettings: _collectCopilotInlineConfig(join3(repoConfigDir, "settings.local.json"), inlineErrors)
5512
+ userConfig: _collectCopilotInlineConfig(join5(configHome, "config.json"), inlineErrors),
5513
+ repoSettings: _collectCopilotInlineConfig(join5(repoConfigDir, "settings.json"), inlineErrors),
5514
+ localSettings: _collectCopilotInlineConfig(join5(repoConfigDir, "settings.local.json"), inlineErrors)
4100
5515
  };
4101
5516
  if (inlineSupport !== false) {
4102
5517
  const disableSource = _resolveCopilotInlineDisableSource(inlineSources);
@@ -4110,10 +5525,10 @@ function _checkCopilotEnabled(homeDir, cwd, copilotCliVersion, errors) {
4110
5525
  const repoHookPaths = _collectSafetyNetCopilotHookFiles(repoHookDir, errors);
4111
5526
  const userHookSupport = _supportsCopilotUserHookFiles(copilotCliVersion);
4112
5527
  const userHookErrors = userHookSupport === true ? errors : undefined;
4113
- const userHookFiles = existsSync4(userHookDir) ? _listJsonFiles(userHookDir, userHookErrors) : [];
5528
+ const userHookFiles = existsSync6(userHookDir) ? _listJsonFiles(userHookDir, userHookErrors) : [];
4114
5529
  const userHookPaths = [];
4115
5530
  for (const filename of userHookFiles) {
4116
- const configPath = join3(userHookDir, filename);
5531
+ const configPath = join5(userHookDir, filename);
4117
5532
  const config = _readCopilotConfigFile(configPath, userHookErrors);
4118
5533
  if (config && _hasSafetyNetCopilotHook(config)) {
4119
5534
  userHookPaths.push(configPath);
@@ -4153,7 +5568,7 @@ function _checkCopilotEnabled(homeDir, cwd, copilotCliVersion, errors) {
4153
5568
  }
4154
5569
  function detectAllHooks(cwd, options) {
4155
5570
  const homeDir = options?.homeDir ?? homedir4();
4156
- const detectCopilot = () => {
5571
+ const detectCopilotCLI = () => {
4157
5572
  const errors = [];
4158
5573
  const hooksCheck = _checkCopilotEnabled(homeDir, cwd, options?.copilotCliVersion, errors);
4159
5574
  if (hooksCheck.disabledBy) {
@@ -4186,16 +5601,17 @@ function detectAllHooks(cwd, options) {
4186
5601
  };
4187
5602
  };
4188
5603
  return [
4189
- detectClaudeCode(homeDir),
5604
+ detectClaudeCode(options?.claudePluginListOutput),
4190
5605
  detectOpenCode(homeDir),
4191
- detectGeminiCLI(homeDir, cwd),
4192
- detectCopilot()
5606
+ detectGeminiCLI(options?.geminiExtensionsListOutput),
5607
+ detectCopilotCLI(),
5608
+ detectCodex(homeDir)
4193
5609
  ];
4194
5610
  }
4195
5611
 
4196
5612
  // src/bin/doctor/system-info.ts
4197
5613
  import { spawn } from "node:child_process";
4198
- var CURRENT_VERSION = "0.8.2";
5614
+ var CURRENT_VERSION = "0.9.0";
4199
5615
  var VERSION_FETCH_TIMEOUT_MS = 2000;
4200
5616
  function getPackageVersion() {
4201
5617
  return CURRENT_VERSION;
@@ -4205,36 +5621,39 @@ var defaultVersionFetcher = async (args) => {
4205
5621
  const [cmd, ...rest] = args;
4206
5622
  if (!cmd)
4207
5623
  return null;
4208
- return new Promise((resolve3) => {
5624
+ return new Promise((resolve5) => {
4209
5625
  try {
4210
5626
  const proc = spawn(cmd, rest, {
4211
5627
  stdio: ["ignore", "pipe", "pipe"]
4212
5628
  });
4213
5629
  let isSettled = false;
4214
5630
  let output = "";
5631
+ let errorOutput = "";
4215
5632
  proc.stdout.on("data", (data) => {
4216
5633
  output += data.toString();
4217
5634
  });
4218
- proc.stderr.on("data", () => {});
5635
+ proc.stderr.on("data", (data) => {
5636
+ errorOutput += data.toString();
5637
+ });
4219
5638
  const finish = (value) => {
4220
5639
  if (isSettled)
4221
5640
  return;
4222
5641
  isSettled = true;
4223
5642
  clearTimeout(timeoutId);
4224
- resolve3(value);
5643
+ resolve5(value);
4225
5644
  };
4226
5645
  const timeoutId = setTimeout(() => {
4227
5646
  proc.kill();
4228
5647
  finish(null);
4229
5648
  }, VERSION_FETCH_TIMEOUT_MS);
4230
5649
  proc.on("close", (code) => {
4231
- finish(code === 0 ? output.trim() || null : null);
5650
+ finish(code === 0 ? output.trim() || errorOutput.trim() || null : null);
4232
5651
  });
4233
5652
  proc.on("error", () => {
4234
5653
  finish(null);
4235
5654
  });
4236
5655
  } catch {
4237
- resolve3(null);
5656
+ resolve5(null);
4238
5657
  }
4239
5658
  });
4240
5659
  };
@@ -4267,10 +5686,23 @@ async function getSystemInfo(fetcher = defaultVersionFetcher) {
4267
5686
  }
4268
5687
  return fallbackVersionPromise;
4269
5688
  };
4270
- const [claudeRaw, openCodeRaw, geminiRaw, copilotRaw, nodeRaw, npmRaw, bunRaw, pluginListRaw] = await Promise.all([
5689
+ const [
5690
+ claudeRaw,
5691
+ claudePluginListOutput,
5692
+ openCodeRaw,
5693
+ geminiRaw,
5694
+ geminiExtensionsListOutput,
5695
+ copilotRaw,
5696
+ nodeRaw,
5697
+ npmRaw,
5698
+ bunRaw,
5699
+ pluginListRaw
5700
+ ] = await Promise.all([
4271
5701
  fetcher(["claude", "--version"]),
5702
+ fetcher(["claude", "plugin", "list"]),
4272
5703
  fetcher(["opencode", "--version"]),
4273
5704
  fetcher(["gemini", "--version"]),
5705
+ fetcher(["gemini", "extensions", "list"]),
4274
5706
  fetchCopilotVersion(),
4275
5707
  fetcher(["node", "--version"]),
4276
5708
  fetcher(["npm", "--version"]),
@@ -4280,8 +5712,10 @@ async function getSystemInfo(fetcher = defaultVersionFetcher) {
4280
5712
  return {
4281
5713
  version: CURRENT_VERSION,
4282
5714
  claudeCodeVersion: parseVersion(claudeRaw),
5715
+ claudePluginListOutput,
4283
5716
  openCodeVersion: parseVersion(openCodeRaw),
4284
5717
  geminiCliVersion: parseVersion(geminiRaw),
5718
+ geminiExtensionsListOutput,
4285
5719
  copilotCliVersion: parseVersion(copilotRaw),
4286
5720
  nodeVersion: parseVersion(nodeRaw),
4287
5721
  npmVersion: parseVersion(npmRaw),
@@ -4353,6 +5787,8 @@ async function runDoctor(options = {}) {
4353
5787
  const cwd = options.cwd ?? process.cwd();
4354
5788
  const system = await getSystemInfo();
4355
5789
  const hooks = detectAllHooks(cwd, {
5790
+ claudePluginListOutput: system.claudePluginListOutput,
5791
+ geminiExtensionsListOutput: system.geminiExtensionsListOutput,
4356
5792
  copilotCliVersion: system.copilotCliVersion,
4357
5793
  copilotPluginInstalled: system.copilotPluginInstalled
4358
5794
  });
@@ -4400,8 +5836,8 @@ function printReport(report) {
4400
5836
  }
4401
5837
 
4402
5838
  // src/bin/explain/config.ts
4403
- import { existsSync as existsSync5 } from "node:fs";
4404
- import { resolve as resolve3 } from "node:path";
5839
+ import { existsSync as existsSync7 } from "node:fs";
5840
+ import { resolve as resolve5 } from "node:path";
4405
5841
 
4406
5842
  // src/core/env.ts
4407
5843
  function envTruthy(name) {
@@ -4413,7 +5849,7 @@ function envTruthy(name) {
4413
5849
  function getConfigSource(options) {
4414
5850
  const projectPath = getProjectConfigPath(options?.cwd);
4415
5851
  let invalidProjectPath = null;
4416
- if (existsSync5(projectPath)) {
5852
+ if (existsSync7(projectPath)) {
4417
5853
  const validation = validateConfigFile(projectPath);
4418
5854
  if (validation.errors.length === 0) {
4419
5855
  return { configSource: projectPath, configValid: true };
@@ -4421,7 +5857,7 @@ function getConfigSource(options) {
4421
5857
  invalidProjectPath = projectPath;
4422
5858
  }
4423
5859
  const userPath = options?.userConfigPath ?? getUserConfigPath();
4424
- if (existsSync5(userPath)) {
5860
+ if (existsSync7(userPath)) {
4425
5861
  const validation = validateConfigFile(userPath);
4426
5862
  return { configSource: userPath, configValid: validation.errors.length === 0 };
4427
5863
  }
@@ -4431,7 +5867,7 @@ function getConfigSource(options) {
4431
5867
  return { configSource: null, configValid: true };
4432
5868
  }
4433
5869
  function buildAnalyzeOptions(explainOptions) {
4434
- const cwd = resolve3(explainOptions?.cwd ?? process.cwd());
5870
+ const cwd = resolve5(explainOptions?.cwd ?? process.cwd());
4435
5871
  const paranoidAll = envTruthy("SAFETY_NET_PARANOID");
4436
5872
  return {
4437
5873
  cwd,
@@ -4439,7 +5875,8 @@ function buildAnalyzeOptions(explainOptions) {
4439
5875
  config: explainOptions?.config ?? loadConfig(cwd),
4440
5876
  strict: explainOptions?.strict ?? envTruthy("SAFETY_NET_STRICT"),
4441
5877
  paranoidRm: paranoidAll || envTruthy("SAFETY_NET_PARANOID_RM"),
4442
- paranoidInterpreters: paranoidAll || envTruthy("SAFETY_NET_PARANOID_INTERPRETERS")
5878
+ paranoidInterpreters: paranoidAll || envTruthy("SAFETY_NET_PARANOID_INTERPRETERS"),
5879
+ worktreeMode: envTruthy("SAFETY_NET_WORKTREE")
4443
5880
  };
4444
5881
  }
4445
5882
 
@@ -4488,6 +5925,7 @@ function explainInnerSegments(innerCmd, depth, options, steps) {
4488
5925
  return { reason: REASON_STRICT_UNPARSEABLE2 };
4489
5926
  }
4490
5927
  let effectiveCwd = options.effectiveCwd === undefined ? options.cwd : options.effectiveCwd;
5928
+ const shellGitContextState = createShellGitContextEnvState(options.envAssignments);
4491
5929
  for (const segment of innerSegments) {
4492
5930
  if (segment.length === 1 && segment[0]?.includes(" ")) {
4493
5931
  const textReason = dangerousInText(segment[0]);
@@ -4515,7 +5953,11 @@ function explainInnerSegments(innerCmd, depth, options, steps) {
4515
5953
  }
4516
5954
  continue;
4517
5955
  }
4518
- const result = explainSegment(segment, depth + 1, { ...options, effectiveCwd }, steps);
5956
+ const result = explainSegment(segment, depth + 1, {
5957
+ ...options,
5958
+ effectiveCwd,
5959
+ envAssignments: getSegmentGitContextEnvAssignments(segment, shellGitContextState)
5960
+ }, steps);
4519
5961
  if (result)
4520
5962
  return result;
4521
5963
  if (segmentChangesCwd(segment)) {
@@ -4526,6 +5968,7 @@ function explainInnerSegments(innerCmd, depth, options, steps) {
4526
5968
  });
4527
5969
  effectiveCwd = null;
4528
5970
  }
5971
+ applyShellGitContextEnvSegment(segment, shellGitContextState);
4529
5972
  }
4530
5973
  return null;
4531
5974
  }
@@ -4546,7 +5989,11 @@ function explainSegment(tokens, depth, options, steps) {
4546
5989
  output: envResult.tokens
4547
5990
  });
4548
5991
  }
4549
- const wrapperResult = stripWrappersWithInfo(envResult.tokens);
5992
+ const effectiveCwd = options.effectiveCwd === undefined ? options.cwd : options.effectiveCwd;
5993
+ const cwdUnknown = effectiveCwd === null;
5994
+ const baseCwdForRm = cwdUnknown ? undefined : effectiveCwd ?? options.cwd;
5995
+ const originalCwd = cwdUnknown ? undefined : options.cwd;
5996
+ const wrapperResult = stripWrappersWithInfo(envResult.tokens, baseCwdForRm);
4550
5997
  const removed = envResult.tokens.slice(0, envResult.tokens.length - wrapperResult.tokens.length);
4551
5998
  if (removed.length > 0) {
4552
5999
  steps.push({
@@ -4557,6 +6004,20 @@ function explainSegment(tokens, depth, options, steps) {
4557
6004
  });
4558
6005
  }
4559
6006
  const strippedTokens = wrapperResult.tokens;
6007
+ const envAssignments = new Map(options.envAssignments ?? []);
6008
+ for (const [k, v] of envResult.envAssignments) {
6009
+ envAssignments.set(k, v);
6010
+ }
6011
+ for (const [k, v] of wrapperResult.envAssignments) {
6012
+ envAssignments.set(k, v);
6013
+ }
6014
+ const cwdForRm = wrapperResult.cwd === null ? undefined : wrapperResult.cwd ?? baseCwdForRm;
6015
+ const nestedEffectiveCwd = wrapperResult.cwd === undefined ? options.effectiveCwd : wrapperResult.cwd;
6016
+ const nestedOptions = {
6017
+ ...options,
6018
+ effectiveCwd: nestedEffectiveCwd,
6019
+ envAssignments
6020
+ };
4560
6021
  if (strippedTokens.length === 0) {
4561
6022
  return null;
4562
6023
  }
@@ -4580,7 +6041,7 @@ function explainSegment(tokens, depth, options, steps) {
4580
6041
  innerCommand: redactedInnerCmd,
4581
6042
  depth: depth + 1
4582
6043
  });
4583
- return explainInnerSegments(innerCmd, depth, options, steps);
6044
+ return explainInnerSegments(innerCmd, depth, nestedOptions, steps);
4584
6045
  }
4585
6046
  }
4586
6047
  if (INTERPRETERS.has(baseNameLower)) {
@@ -4603,7 +6064,7 @@ function explainSegment(tokens, depth, options, steps) {
4603
6064
  innerCommand: redactedCodeArg,
4604
6065
  depth: depth + 1
4605
6066
  });
4606
- const nestedResult = explainInnerSegments(codeArg, depth, options, steps);
6067
+ const nestedResult = explainInnerSegments(codeArg, depth, nestedOptions, steps);
4607
6068
  if (nestedResult)
4608
6069
  return nestedResult;
4609
6070
  if (containsDangerousCode(codeArg)) {
@@ -4631,18 +6092,10 @@ function explainSegment(tokens, depth, options, steps) {
4631
6092
  innerCommand: redactEnvAssignmentsInString(busyboxInnerCmd),
4632
6093
  depth: depth + 1
4633
6094
  });
4634
- return explainSegment(strippedTokens.slice(1), depth + 1, options, steps);
4635
- }
4636
- const envAssignments = new Map(envResult.envAssignments);
4637
- for (const [k, v] of wrapperResult.envAssignments) {
4638
- envAssignments.set(k, v);
6095
+ return explainSegment(strippedTokens.slice(1), depth + 1, nestedOptions, steps);
4639
6096
  }
4640
6097
  const allowTmpdirVar = !isTmpdirOverriddenToNonTemp(envAssignments);
4641
6098
  const tmpdirValue = envAssignments.get("TMPDIR") ?? process.env.TMPDIR ?? null;
4642
- const effectiveCwd = options.effectiveCwd === undefined ? options.cwd : options.effectiveCwd;
4643
- const cwdUnknown = effectiveCwd === null;
4644
- const cwdForRm = cwdUnknown ? undefined : effectiveCwd ?? options.cwd;
4645
- const originalCwd = cwdUnknown ? undefined : options.cwd;
4646
6099
  const isGit = baseNameLower === "git";
4647
6100
  const isRm = baseName === "rm";
4648
6101
  const isFind = baseName === "find";
@@ -4657,14 +6110,27 @@ function explainSegment(tokens, depth, options, steps) {
4657
6110
  });
4658
6111
  }
4659
6112
  if (isGit) {
4660
- const reason = analyzeGit(strippedTokens);
6113
+ const gitOptions = {
6114
+ cwd: cwdForRm,
6115
+ envAssignments,
6116
+ worktreeMode: options.worktreeMode
6117
+ };
6118
+ const relaxation = getGitWorktreeRelaxation(strippedTokens, gitOptions);
6119
+ const reason = analyzeGit(strippedTokens, gitOptions);
4661
6120
  steps.push({
4662
6121
  type: "rule-check",
4663
6122
  ruleModule: "rules-git.ts",
4664
6123
  ruleFunction: "analyzeGit",
4665
- matched: !!reason,
4666
- reason: reason ?? undefined
6124
+ matched: !!reason || !!relaxation,
6125
+ reason: reason ?? relaxation?.originalReason
4667
6126
  });
6127
+ if (relaxation) {
6128
+ steps.push({
6129
+ type: "worktree-relaxation",
6130
+ originalReason: relaxation.originalReason,
6131
+ gitCwd: relaxation.gitCwd
6132
+ });
6133
+ }
4668
6134
  if (reason)
4669
6135
  return { reason };
4670
6136
  }
@@ -4702,7 +6168,9 @@ function explainSegment(tokens, depth, options, steps) {
4702
6168
  cwd: cwdForRm,
4703
6169
  originalCwd,
4704
6170
  paranoidRm: options.paranoidRm,
4705
- allowTmpdirVar
6171
+ allowTmpdirVar,
6172
+ envAssignments,
6173
+ worktreeMode: options.worktreeMode
4706
6174
  });
4707
6175
  steps.push({
4708
6176
  type: "rule-check",
@@ -4715,8 +6183,14 @@ function explainSegment(tokens, depth, options, steps) {
4715
6183
  return { reason };
4716
6184
  }
4717
6185
  if (isParallel) {
4718
- const analyzeNested = (cmd) => {
4719
- const result = explainInnerSegments(cmd, depth, options, steps);
6186
+ const analyzeNested = (cmd, overrides) => {
6187
+ const overriddenOptions = {
6188
+ ...nestedOptions,
6189
+ effectiveCwd: overrides && Object.hasOwn(overrides, "effectiveCwd") ? overrides.effectiveCwd : nestedOptions.effectiveCwd,
6190
+ envAssignments: overrides?.envAssignments ?? nestedOptions.envAssignments,
6191
+ worktreeMode: overrides?.worktreeMode ?? nestedOptions.worktreeMode
6192
+ };
6193
+ const result = explainInnerSegments(cmd, depth, overriddenOptions, steps);
4720
6194
  return result?.reason ?? null;
4721
6195
  };
4722
6196
  const reason = analyzeParallel(strippedTokens, {
@@ -4724,6 +6198,8 @@ function explainSegment(tokens, depth, options, steps) {
4724
6198
  originalCwd,
4725
6199
  paranoidRm: options.paranoidRm,
4726
6200
  allowTmpdirVar,
6201
+ envAssignments,
6202
+ worktreeMode: options.worktreeMode,
4727
6203
  analyzeNested
4728
6204
  });
4729
6205
  steps.push({
@@ -4739,6 +6215,7 @@ function explainSegment(tokens, depth, options, steps) {
4739
6215
  const matchedKnown = isGit || isRm || isFind || isXargs || isParallel;
4740
6216
  const tokensScanned = [];
4741
6217
  let fallbackReason = null;
6218
+ let fallbackRelaxation = null;
4742
6219
  let embeddedCommandFound;
4743
6220
  if (!matchedKnown && !DISPLAY_COMMANDS.has(normalizeCommandToken(head))) {
4744
6221
  for (let i = 1;i < strippedTokens.length && !fallbackReason; i++) {
@@ -4760,7 +6237,13 @@ function explainSegment(tokens, depth, options, steps) {
4760
6237
  if (!fallbackReason && cmd === "git") {
4761
6238
  embeddedCommandFound = "git";
4762
6239
  const gitTokens = ["git", ...strippedTokens.slice(i + 1)];
4763
- fallbackReason = analyzeGit(gitTokens);
6240
+ const gitOptions = {
6241
+ cwd: cwdForRm,
6242
+ envAssignments,
6243
+ worktreeMode: false
6244
+ };
6245
+ fallbackRelaxation = getGitWorktreeRelaxation(gitTokens, gitOptions);
6246
+ fallbackReason = analyzeGit(gitTokens, gitOptions);
4764
6247
  }
4765
6248
  if (!fallbackReason && cmd === "find") {
4766
6249
  embeddedCommandFound = "find";
@@ -4774,6 +6257,13 @@ function explainSegment(tokens, depth, options, steps) {
4774
6257
  tokensScanned,
4775
6258
  embeddedCommandFound
4776
6259
  });
6260
+ if (fallbackRelaxation) {
6261
+ steps.push({
6262
+ type: "worktree-relaxation",
6263
+ originalReason: fallbackRelaxation.originalReason,
6264
+ gitCwd: fallbackRelaxation.gitCwd
6265
+ });
6266
+ }
4777
6267
  if (fallbackReason)
4778
6268
  return { reason: fallbackReason };
4779
6269
  const shouldCheckCustomRules = depth === 0 || !matchedKnown;
@@ -4839,6 +6329,7 @@ function explainCommand2(command, options) {
4839
6329
  let blockReason;
4840
6330
  let blockSegment;
4841
6331
  let effectiveCwd = analyzeOpts.effectiveCwd;
6332
+ const shellGitContextState = createShellGitContextEnvState(analyzeOpts.envAssignments);
4842
6333
  for (let i = 0;i < segments.length; i++) {
4843
6334
  const segment = segments[i];
4844
6335
  if (!segment)
@@ -4884,7 +6375,11 @@ function explainCommand2(command, options) {
4884
6375
  trace.segments.push({ index: i, steps: segmentSteps });
4885
6376
  continue;
4886
6377
  }
4887
- const result = explainSegment(segment, 0, { ...analyzeOpts, effectiveCwd }, segmentSteps);
6378
+ const result = explainSegment(segment, 0, {
6379
+ ...analyzeOpts,
6380
+ effectiveCwd,
6381
+ envAssignments: getSegmentGitContextEnvAssignments(segment, shellGitContextState)
6382
+ }, segmentSteps);
4888
6383
  if (result) {
4889
6384
  blocked = true;
4890
6385
  blockReason = result.reason;
@@ -4898,6 +6393,7 @@ function explainCommand2(command, options) {
4898
6393
  });
4899
6394
  effectiveCwd = null;
4900
6395
  }
6396
+ applyShellGitContextEnvSegment(segment, shellGitContextState);
4901
6397
  trace.segments.push({ index: i, steps: segmentSteps });
4902
6398
  }
4903
6399
  return {
@@ -5077,6 +6573,14 @@ function formatStepStyleD(step, stepNum, box) {
5077
6573
  }
5078
6574
  return { lines, incrementStep: true };
5079
6575
  }
6576
+ case "worktree-relaxation": {
6577
+ lines.push("");
6578
+ lines.push(`STEP ${stepNum} ${box.h} Worktree relaxation`);
6579
+ lines.push(` Mode: SAFETY_NET_WORKTREE`);
6580
+ lines.push(` Git cwd: ${step.gitCwd}`);
6581
+ lines.push(` Result: Allowed local discard in linked worktree`);
6582
+ return { lines, incrementStep: true };
6583
+ }
5080
6584
  case "tmpdir-check":
5081
6585
  return null;
5082
6586
  case "fallback-scan": {
@@ -5268,7 +6772,7 @@ function formatTraceJson(result) {
5268
6772
  return JSON.stringify(result, null, 2);
5269
6773
  }
5270
6774
  // src/bin/help.ts
5271
- var version = "0.8.2";
6775
+ var version = "0.9.0";
5272
6776
  var INDENT = " ";
5273
6777
  var PROGRAM_NAME = "cc-safety-net";
5274
6778
  function formatOptionFlags(option) {
@@ -5334,6 +6838,7 @@ function printHelp() {
5334
6838
  lines.push(`${INDENT}SAFETY_NET_PARANOID=1 Enable all paranoid checks`);
5335
6839
  lines.push(`${INDENT}SAFETY_NET_PARANOID_RM=1 Block non-temp rm -rf within cwd`);
5336
6840
  lines.push(`${INDENT}SAFETY_NET_PARANOID_INTERPRETERS=1 Block interpreter one-liners`);
6841
+ lines.push(`${INDENT}SAFETY_NET_WORKTREE=1 Allow local git discards in linked worktrees`);
5337
6842
  lines.push("");
5338
6843
  lines.push("CONFIG FILES:");
5339
6844
  lines.push(`${INDENT}~/.cc-safety-net/config.json User-scope config`);
@@ -5354,9 +6859,9 @@ function showCommandHelp(commandName) {
5354
6859
  }
5355
6860
 
5356
6861
  // src/core/audit.ts
5357
- import { appendFileSync, existsSync as existsSync6, mkdirSync } from "node:fs";
6862
+ import { appendFileSync, existsSync as existsSync8, mkdirSync } from "node:fs";
5358
6863
  import { homedir as homedir5 } from "node:os";
5359
- import { join as join4 } from "node:path";
6864
+ import { join as join6 } from "node:path";
5360
6865
  function sanitizeSessionIdForFilename(sessionId) {
5361
6866
  const raw = sessionId.trim();
5362
6867
  if (!raw) {
@@ -5375,12 +6880,12 @@ function writeAuditLog(sessionId, command, segment, reason, cwd, options = {}) {
5375
6880
  return;
5376
6881
  }
5377
6882
  const home = options.homeDir ?? homedir5();
5378
- const logsDir = join4(home, ".cc-safety-net", "logs");
6883
+ const logsDir = join6(home, ".cc-safety-net", "logs");
5379
6884
  try {
5380
- if (!existsSync6(logsDir)) {
6885
+ if (!existsSync8(logsDir)) {
5381
6886
  mkdirSync(logsDir, { recursive: true });
5382
6887
  }
5383
- const logFile = join4(logsDir, `${safeSessionId}.jsonl`);
6888
+ const logFile = join6(logsDir, `${safeSessionId}.jsonl`);
5384
6889
  const entry = {
5385
6890
  ts: new Date().toISOString(),
5386
6891
  command: redactSecrets(command).slice(0, 300),
@@ -5478,13 +6983,15 @@ async function runClaudeCodeHook() {
5478
6983
  const paranoidAll = envTruthy("SAFETY_NET_PARANOID");
5479
6984
  const paranoidRm = paranoidAll || envTruthy("SAFETY_NET_PARANOID_RM");
5480
6985
  const paranoidInterpreters = paranoidAll || envTruthy("SAFETY_NET_PARANOID_INTERPRETERS");
6986
+ const worktreeMode = envTruthy("SAFETY_NET_WORKTREE");
5481
6987
  const config = loadConfig(cwd);
5482
6988
  const result = analyzeCommand(command, {
5483
6989
  cwd,
5484
6990
  config,
5485
6991
  strict,
5486
6992
  paranoidRm,
5487
- paranoidInterpreters
6993
+ paranoidInterpreters,
6994
+ worktreeMode
5488
6995
  });
5489
6996
  if (result) {
5490
6997
  const sessionId = input.session_id;
@@ -5548,13 +7055,15 @@ async function runCopilotCliHook() {
5548
7055
  const paranoidAll = envTruthy("SAFETY_NET_PARANOID");
5549
7056
  const paranoidRm = paranoidAll || envTruthy("SAFETY_NET_PARANOID_RM");
5550
7057
  const paranoidInterpreters = paranoidAll || envTruthy("SAFETY_NET_PARANOID_INTERPRETERS");
7058
+ const worktreeMode = envTruthy("SAFETY_NET_WORKTREE");
5551
7059
  const config = loadConfig(cwd);
5552
7060
  const result = analyzeCommand(command, {
5553
7061
  cwd,
5554
7062
  config,
5555
7063
  strict,
5556
7064
  paranoidRm,
5557
- paranoidInterpreters
7065
+ paranoidInterpreters,
7066
+ worktreeMode
5558
7067
  });
5559
7068
  if (result) {
5560
7069
  const sessionId = `copilot-${input.timestamp ?? Date.now()}`;
@@ -5611,13 +7120,15 @@ async function runGeminiCLIHook() {
5611
7120
  const paranoidAll = envTruthy("SAFETY_NET_PARANOID");
5612
7121
  const paranoidRm = paranoidAll || envTruthy("SAFETY_NET_PARANOID_RM");
5613
7122
  const paranoidInterpreters = paranoidAll || envTruthy("SAFETY_NET_PARANOID_INTERPRETERS");
7123
+ const worktreeMode = envTruthy("SAFETY_NET_WORKTREE");
5614
7124
  const config = loadConfig(cwd);
5615
7125
  const result = analyzeCommand(command, {
5616
7126
  cwd,
5617
7127
  config,
5618
7128
  strict,
5619
7129
  paranoidRm,
5620
- paranoidInterpreters
7130
+ paranoidInterpreters,
7131
+ worktreeMode
5621
7132
  });
5622
7133
  if (result) {
5623
7134
  const sessionId = input.session_id;
@@ -5629,14 +7140,14 @@ async function runGeminiCLIHook() {
5629
7140
  }
5630
7141
 
5631
7142
  // src/bin/statusline.ts
5632
- import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:fs";
7143
+ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
5633
7144
  import { homedir as homedir6 } from "node:os";
5634
- import { join as join5 } from "node:path";
7145
+ import { join as join7 } from "node:path";
5635
7146
  async function readStdinAsync() {
5636
7147
  if (process.stdin.isTTY) {
5637
7148
  return null;
5638
7149
  }
5639
- return new Promise((resolve4) => {
7150
+ return new Promise((resolve6) => {
5640
7151
  let data = "";
5641
7152
  process.stdin.setEncoding("utf-8");
5642
7153
  process.stdin.on("data", (chunk) => {
@@ -5644,10 +7155,10 @@ async function readStdinAsync() {
5644
7155
  });
5645
7156
  process.stdin.on("end", () => {
5646
7157
  const trimmed = data.trim();
5647
- resolve4(trimmed || null);
7158
+ resolve6(trimmed || null);
5648
7159
  });
5649
7160
  process.stdin.on("error", () => {
5650
- resolve4(null);
7161
+ resolve6(null);
5651
7162
  });
5652
7163
  });
5653
7164
  }
@@ -5655,15 +7166,15 @@ function getSettingsPath() {
5655
7166
  if (process.env.CLAUDE_SETTINGS_PATH) {
5656
7167
  return process.env.CLAUDE_SETTINGS_PATH;
5657
7168
  }
5658
- return join5(homedir6(), ".claude", "settings.json");
7169
+ return join7(homedir6(), ".claude", "settings.json");
5659
7170
  }
5660
7171
  function isPluginEnabled() {
5661
7172
  const settingsPath = getSettingsPath();
5662
- if (!existsSync7(settingsPath)) {
7173
+ if (!existsSync9(settingsPath)) {
5663
7174
  return false;
5664
7175
  }
5665
7176
  try {
5666
- const content = readFileSync5(settingsPath, "utf-8");
7177
+ const content = readFileSync7(settingsPath, "utf-8");
5667
7178
  const settings = JSON.parse(content);
5668
7179
  if (!settings.enabledPlugins) {
5669
7180
  return false;
@@ -5687,6 +7198,7 @@ async function printStatusline() {
5687
7198
  const paranoidAll = envTruthy("SAFETY_NET_PARANOID");
5688
7199
  const paranoidRm = paranoidAll || envTruthy("SAFETY_NET_PARANOID_RM");
5689
7200
  const paranoidInterpreters = paranoidAll || envTruthy("SAFETY_NET_PARANOID_INTERPRETERS");
7201
+ const worktreeMode = envTruthy("SAFETY_NET_WORKTREE");
5690
7202
  let modeEmojis = "";
5691
7203
  if (strict) {
5692
7204
  modeEmojis += "\uD83D\uDD12";
@@ -5698,6 +7210,9 @@ async function printStatusline() {
5698
7210
  } else if (paranoidInterpreters) {
5699
7211
  modeEmojis += "\uD83D\uDC1A";
5700
7212
  }
7213
+ if (worktreeMode) {
7214
+ modeEmojis += "\uD83C\uDF33";
7215
+ }
5701
7216
  const statusEmoji = modeEmojis || "✅";
5702
7217
  status = `\uD83D\uDEE1️ Safety Net ${statusEmoji}`;
5703
7218
  }
@@ -5710,8 +7225,8 @@ async function printStatusline() {
5710
7225
  }
5711
7226
 
5712
7227
  // src/bin/verify-config.ts
5713
- import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync } from "node:fs";
5714
- import { resolve as resolve4 } from "node:path";
7228
+ import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync } from "node:fs";
7229
+ import { resolve as resolve6 } from "node:path";
5715
7230
  var HEADER = "Safety Net Config";
5716
7231
  var SEPARATOR = "═".repeat(HEADER.length);
5717
7232
  var SCHEMA_URL = "https://raw.githubusercontent.com/kenryu42/claude-code-safety-net/main/assets/cc-safety-net.schema.json";
@@ -5747,7 +7262,7 @@ function printInvalidConfig(scope, path, errors) {
5747
7262
  }
5748
7263
  function addSchemaIfMissing(path) {
5749
7264
  try {
5750
- const content = readFileSync6(path, "utf-8");
7265
+ const content = readFileSync8(path, "utf-8");
5751
7266
  const parsed = JSON.parse(content);
5752
7267
  if (parsed.$schema) {
5753
7268
  return false;
@@ -5765,18 +7280,18 @@ function verifyConfig(options = {}) {
5765
7280
  let hasErrors = false;
5766
7281
  const configsChecked = [];
5767
7282
  printHeader();
5768
- if (existsSync8(userConfig)) {
7283
+ if (existsSync10(userConfig)) {
5769
7284
  const result = validateConfigFile(userConfig);
5770
7285
  configsChecked.push({ scope: "User", path: userConfig, result });
5771
7286
  if (result.errors.length > 0) {
5772
7287
  hasErrors = true;
5773
7288
  }
5774
7289
  }
5775
- if (existsSync8(projectConfig)) {
7290
+ if (existsSync10(projectConfig)) {
5776
7291
  const result = validateConfigFile(projectConfig);
5777
7292
  configsChecked.push({
5778
7293
  scope: "Project",
5779
- path: resolve4(projectConfig),
7294
+ path: resolve6(projectConfig),
5780
7295
  result
5781
7296
  });
5782
7297
  if (result.errors.length > 0) {