@getmarrow/mcp 3.9.108 → 3.9.110

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.
Files changed (109) hide show
  1. package/README.md +26 -1
  2. package/dist/cli.js +44 -3
  3. package/dist/cli.js.map +1 -1
  4. package/dist/control-notice.d.ts +13 -0
  5. package/dist/control-notice.d.ts.map +1 -0
  6. package/dist/control-notice.js +76 -0
  7. package/dist/control-notice.js.map +1 -0
  8. package/dist/control-routes.d.ts +10 -0
  9. package/dist/control-routes.d.ts.map +1 -0
  10. package/dist/control-routes.js +81 -0
  11. package/dist/control-routes.js.map +1 -0
  12. package/dist/env.d.ts +15 -0
  13. package/dist/env.d.ts.map +1 -1
  14. package/dist/env.js +29 -2
  15. package/dist/env.js.map +1 -1
  16. package/dist/hook-context.d.ts.map +1 -1
  17. package/dist/hook-context.js +34 -11
  18. package/dist/hook-context.js.map +1 -1
  19. package/dist/hook-contract.d.ts +2 -2
  20. package/dist/hook-contract.js +1 -1
  21. package/dist/hook-pre-action.d.ts +10 -2
  22. package/dist/hook-pre-action.d.ts.map +1 -1
  23. package/dist/hook-pre-action.js +105 -33
  24. package/dist/hook-pre-action.js.map +1 -1
  25. package/dist/hook-session.d.ts.map +1 -1
  26. package/dist/hook-session.js +12 -7
  27. package/dist/hook-session.js.map +1 -1
  28. package/dist/hook-tool-policy.d.ts +1 -1
  29. package/dist/hook-tool-policy.d.ts.map +1 -1
  30. package/dist/hook.d.ts.map +1 -1
  31. package/dist/hook.js +19 -14
  32. package/dist/hook.js.map +1 -1
  33. package/dist/host-approval.d.ts +9 -0
  34. package/dist/host-approval.d.ts.map +1 -1
  35. package/dist/host-approval.js +22 -0
  36. package/dist/host-approval.js.map +1 -1
  37. package/dist/identity-explain.d.ts +35 -0
  38. package/dist/identity-explain.d.ts.map +1 -0
  39. package/dist/identity-explain.js +60 -0
  40. package/dist/identity-explain.js.map +1 -0
  41. package/dist/ipv4-first.d.ts +7 -0
  42. package/dist/ipv4-first.d.ts.map +1 -0
  43. package/dist/ipv4-first.js +60 -0
  44. package/dist/ipv4-first.js.map +1 -0
  45. package/dist/key-class.d.ts +16 -0
  46. package/dist/key-class.d.ts.map +1 -0
  47. package/dist/key-class.js +37 -0
  48. package/dist/key-class.js.map +1 -0
  49. package/dist/keyless-notice.d.ts +10 -0
  50. package/dist/keyless-notice.d.ts.map +1 -0
  51. package/dist/keyless-notice.js +47 -0
  52. package/dist/keyless-notice.js.map +1 -0
  53. package/dist/normalized-action.d.ts +4 -0
  54. package/dist/normalized-action.d.ts.map +1 -1
  55. package/dist/normalized-action.js +60 -27
  56. package/dist/normalized-action.js.map +1 -1
  57. package/dist/own-server-identity.d.ts +20 -0
  58. package/dist/own-server-identity.d.ts.map +1 -1
  59. package/dist/own-server-identity.js +130 -5
  60. package/dist/own-server-identity.js.map +1 -1
  61. package/dist/owner-bypass.d.ts +9 -0
  62. package/dist/owner-bypass.d.ts.map +1 -0
  63. package/dist/owner-bypass.js +79 -0
  64. package/dist/owner-bypass.js.map +1 -0
  65. package/dist/redact.d.ts +5 -0
  66. package/dist/redact.d.ts.map +1 -1
  67. package/dist/redact.js +61 -9
  68. package/dist/redact.js.map +1 -1
  69. package/dist/seatbelt/hook.d.ts +3 -1
  70. package/dist/seatbelt/hook.d.ts.map +1 -1
  71. package/dist/seatbelt/hook.js +7 -3
  72. package/dist/seatbelt/hook.js.map +1 -1
  73. package/dist/seatbelt/mapper.d.ts.map +1 -1
  74. package/dist/seatbelt/mapper.js +849 -12
  75. package/dist/seatbelt/mapper.js.map +1 -1
  76. package/dist/seatbelt/patch-text.d.ts +49 -0
  77. package/dist/seatbelt/patch-text.d.ts.map +1 -0
  78. package/dist/seatbelt/patch-text.js +376 -0
  79. package/dist/seatbelt/patch-text.js.map +1 -0
  80. package/dist/seatbelt/tables.d.ts +66 -0
  81. package/dist/seatbelt/tables.d.ts.map +1 -1
  82. package/dist/seatbelt/tables.js +467 -3
  83. package/dist/seatbelt/tables.js.map +1 -1
  84. package/dist/seatbelt/win-protected.d.ts +52 -0
  85. package/dist/seatbelt/win-protected.d.ts.map +1 -0
  86. package/dist/seatbelt/win-protected.js +1279 -0
  87. package/dist/seatbelt/win-protected.js.map +1 -0
  88. package/dist/self-protect.d.ts +18 -0
  89. package/dist/self-protect.d.ts.map +1 -0
  90. package/dist/self-protect.js +185 -0
  91. package/dist/self-protect.js.map +1 -0
  92. package/dist/session-id.d.ts +2 -0
  93. package/dist/session-id.d.ts.map +1 -0
  94. package/dist/session-id.js +32 -0
  95. package/dist/session-id.js.map +1 -0
  96. package/dist/session-loop-guard.d.ts +40 -0
  97. package/dist/session-loop-guard.d.ts.map +1 -1
  98. package/dist/session-loop-guard.js +236 -9
  99. package/dist/session-loop-guard.js.map +1 -1
  100. package/dist/session-marker.d.ts +7 -0
  101. package/dist/session-marker.d.ts.map +1 -0
  102. package/dist/session-marker.js +42 -0
  103. package/dist/session-marker.js.map +1 -0
  104. package/dist/win-private-state.d.ts +117 -0
  105. package/dist/win-private-state.d.ts.map +1 -0
  106. package/dist/win-private-state.js +544 -0
  107. package/dist/win-private-state.js.map +1 -0
  108. package/package.json +1 -1
  109. package/server.json +2 -2
@@ -0,0 +1,1279 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WINDOWS_REASONS = void 0;
4
+ exports.matcherCells = matcherCells;
5
+ exports.windowsProtectedHits = windowsProtectedHits;
6
+ exports.mergeWindowsHits = mergeWindowsHits;
7
+ exports.withWindowsProtected = withWindowsProtected;
8
+ const tables_1 = require("./tables");
9
+ const baseline = require('../../vendor/classifier/policy-baseline.js');
10
+ /** The reason codes this module adds. The decision's rule is `<category>.<reason>` when one of them leads it. */
11
+ exports.WINDOWS_REASONS = Object.freeze({
12
+ protectedPath: 'windows_protected_path',
13
+ persistence: 'windows_persistence',
14
+ shim: 'windows_shim_write',
15
+ secretPath: 'windows_secret_path',
16
+ envDump: 'windows_env_dump',
17
+ secretStore: 'windows_secret_store',
18
+ oddPath: 'windows_odd_path',
19
+ });
20
+ // ---------------------------------------------------------------------------------------------
21
+ // patterns
22
+ // A home the hook does not know: `~/X` is matched in any folder (`**/X`), except where that would catch
23
+ // ordinary project folders (`bin`, `.cursor`, `.gemini`, `Library`); those are matched in any profile.
24
+ const HOME = 'c:/users/__user__';
25
+ const GENERIC_HOME_DIRS = new Set(['bin', '.cursor', '.gemini', 'library']);
26
+ // Windows places with no POSIX twin. `op`: like a POSIX protected path, any program that is not a reader stops
27
+ // on it. `wo`: like the POSIX program folders and system trees (OPERAND_RULE_OMITS), only a write stops.
28
+ const WINDOWS_PROTECTED = [
29
+ // PowerShell profiles and modules (the POSIX .bashrc / .profile).
30
+ ['**/documents/windowspowershell/**', 'op'], ['**/documents/powershell/**', 'op'], ['**/profile.ps1', 'op'], ['**/*_profile.ps1', 'op'],
31
+ // Marrow's code copies and the hook configs.
32
+ ['**/npm-cache/_npx/*/node_modules/@getmarrow/**', 'op'], ['**/.claude/plugins/**', 'op'], ['**/.claude/settings*.json', 'op'],
33
+ // The git and Git Bash configuration Git for Windows reads (the POSIX /etc/gitconfig, /etc/profile).
34
+ ['**/.config/git/config', 'op'], ['**/git/etc/gitconfig', 'op'], ['**/git/mingw64/etc/gitconfig', 'op'], ['?:/programdata/git/config', 'op'],
35
+ ['**/git/etc/profile', 'op'], ['**/git/etc/profile.d/**', 'op'], ['**/git/etc/bash.bashrc', 'op'], ['**/git/etc/bash_profile', 'op'],
36
+ // Claude Code managed settings on Windows (the POSIX /etc/claude-code) and the hosts file.
37
+ ['?:/programdata/claudecode/**', 'op'], ['?:/program_files/claudecode/**', 'op'], ['?:/windows/system32/drivers/etc/hosts', 'op'],
38
+ // Start-up and PATH folders (the POSIX LaunchAgents and ~/.local/bin).
39
+ ['**/start_menu/programs/startup/**', 'wo'], ['**/appdata/roaming/npm/**', 'wo'], ['**/appdata/local/microsoft/windowsapps/**', 'wo'],
40
+ // System trees (the POSIX /usr, /etc).
41
+ ['?:/windows/**', 'wo'], ['?:/program_files/**', 'wo'], ['?:/program_files_x86/**', 'wo'], ['?:/programdata/**', 'wo'],
42
+ ];
43
+ // Windows secret stores with no POSIX twin, plus two agent credential files (stricter than POSIX, Windows only).
44
+ const WINDOWS_SECRETS = [
45
+ '**/appdata/roaming/microsoft/credentials/**', '**/appdata/local/microsoft/credentials/**', '**/appdata/roaming/microsoft/protect/**',
46
+ '**/appdata/roaming/microsoft/crypto/**', '**/appdata/roaming/github_cli/hosts.yml', '**/appdata/roaming/gcloud/**/*credentials*',
47
+ '**/appdata/roaming/gcloud/**/access_tokens.db', '**/appdata/roaming/npm/etc/npmrc', '?:/programdata/ssh/ssh_host_*',
48
+ '**/system32/config/sam', '**/system32/config/security', '**/.claude/.credentials.json', '**/.codex/auth.json',
49
+ ];
50
+ const KEY_EXT = /\.(?:pem|key|p12|pfx|ppk)$/;
51
+ const KEY_EXT_SAMPLES = ['server.pem', 'tls.key', 'cert.p12', 'cert.pfx', 'key.ppk'];
52
+ // Folders whose removal or move takes a protected file with it (the POSIX protected anchors).
53
+ const HOME_ANCHORS = ['.marrow', '.claude', '.cursor', '.codex', '.gemini', '.grok', '.codeium', '.windsurf', '.hermes', '.openclaw', '.ssh', '.aws',
54
+ '.kube', '.docker', '.azure', '.gnupg', '.config/git', '.config/gh', '.config/gcloud', '.npm/_npx', '.local/bin', 'bin',
55
+ 'appdata/local/npm-cache/_npx', 'appdata/roaming/npm', 'appdata/roaming/microsoft/windows/start_menu/programs/startup',
56
+ 'appdata/roaming/microsoft/credentials', 'appdata/roaming/microsoft/protect', 'appdata/local/microsoft/windowsapps',
57
+ 'documents/windowspowershell', 'documents/powershell'];
58
+ const SYSTEM_ANCHORS = ['?:/windows', '?:/program_files', '?:/program_files_x86', '?:/programdata'];
59
+ // Concrete names for the glob parts of the patterns: a wildcard operand or an 8.3 short name is compared with these.
60
+ const GLOB_COMP_SAMPLES = {
61
+ 'id_*': ['id_rsa', 'id_ed25519', 'id_ecdsa', 'id_dsa'],
62
+ '*_key': ['deploy_key'],
63
+ '*credentials*': ['credentials', 'application_default_credentials.json', 'credentials.db'],
64
+ 'private-keys*': ['private-keys-v1.d'],
65
+ 'env*': ['env', 'env.local'],
66
+ '.env.*': ['.env.local', '.env.production', '.env.development', '.env.test'],
67
+ '*.env': ['app.env', 'prod.env'],
68
+ '*.json': ['installed_plugins.json', 'known_marketplaces.json'],
69
+ '*marrow*': ['marrow', 'getmarrow'],
70
+ 'settings*.json': ['settings.json', 'settings.local.json'],
71
+ '*_profile.ps1': ['microsoft.powershell_profile.ps1', 'microsoft.vscode_profile.ps1'],
72
+ 'ssh_host_*': ['ssh_host_rsa_key', 'ssh_host_ed25519_key'],
73
+ };
74
+ // Program names a host may run a hook with. cmd.exe looks in the CURRENT folder before PATH (PATHEXT order), so
75
+ // one of these written anywhere can replace the hook (audit RC-1d, consumer C19).
76
+ const SHIM = /^(?:(?:node|nodejs|npx|npm|pnpm|yarn|git|bash|sh|cmd|powershell|pwsh|whoami|python3?|py|marrow[\w.-]*)\.(?:com|exe|bat|cmd|ps1)|(?:node|npx)\.(?:vbs|vbe|js|jse|wsf|wsh|msc|cpl))$/;
77
+ const SHIM_SAMPLES = ['node.cmd', 'node.exe', 'node.bat', 'node.com', 'node.ps1', 'npx.cmd', 'npm.cmd', 'git.exe', 'pwsh.exe', 'powershell.exe',
78
+ 'cmd.exe', 'bash.exe', 'marrow-mcp.cmd'];
79
+ // Patterns are ours (never agent text): a RegExp per glob part is safe here.
80
+ function patternCompRe(g) {
81
+ return new RegExp(`^${g.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*').replace(/\?/g, '[^/]')}$`);
82
+ }
83
+ function compilePattern(raw) {
84
+ const lower = raw.toLowerCase();
85
+ let text = lower;
86
+ if (lower.startsWith('~/')) {
87
+ const rest = lower.slice(2);
88
+ text = GENERIC_HOME_DIRS.has(rest.split('/')[0]) ? `?:/users/*/${rest}` : `**/${rest}`;
89
+ }
90
+ text = multiword(text);
91
+ const comps = text.split('/').map((c, i) => {
92
+ if (c === '**')
93
+ return { k: 'gs' };
94
+ if (i === 0 && c === '')
95
+ return { k: 'lit', v: '' };
96
+ if (c === '?:')
97
+ return { k: 'glob', v: c, re: /^[a-z]:$/ };
98
+ return /[*?]/.test(c) ? { k: 'glob', v: c, re: patternCompRe(c) } : { k: 'lit', v: c };
99
+ });
100
+ // The last literal part: a path without it (and without a wildcard part) cannot match.
101
+ let key = null;
102
+ for (let i = comps.length - 1; i >= 0; i -= 1) {
103
+ const c = comps[i];
104
+ if (c.k === 'lit' && c.v !== '') {
105
+ key = c.v;
106
+ break;
107
+ }
108
+ }
109
+ return { comps, key };
110
+ }
111
+ let PATTERNS = null;
112
+ let PATTERN_SOURCE = null;
113
+ function patterns(parts) {
114
+ if (PATTERNS && PATTERN_SOURCE === parts.protectedExtra)
115
+ return PATTERNS;
116
+ const op = [];
117
+ const wo = [];
118
+ for (const p of [...baseline.BASELINE_POLICY.protected_paths, ...parts.protectedExtra])
119
+ (parts.operandOmits.has(p) ? wo : op).push(compilePattern(p));
120
+ for (const [p, kind] of WINDOWS_PROTECTED)
121
+ (kind === 'op' ? op : wo).push(compilePattern(p));
122
+ const secret = [...tables_1.SECRET_PATTERNS, ...tables_1.PRESENT_SECRET_PATTERNS, ...WINDOWS_SECRETS].map(compilePattern);
123
+ PATTERNS = { op, wo, secret };
124
+ PATTERN_SOURCE = parts.protectedExtra;
125
+ return PATTERNS;
126
+ }
127
+ // ---------------------------------------------------------------------------------------------
128
+ // wildcards in agent text: a linear walk, never a RegExp
129
+ /**
130
+ * The cells the two matchers below fill (wildMatch: glob x text; matches: pattern parts x path parts), added up since the last reset.
131
+ * A test reads it to pin the cost of a decision by a count and not by a clock (M-4); no decision reads it.
132
+ */
133
+ const matcherWork = { cells: 0 };
134
+ function matcherCells(reset = false) {
135
+ const n = matcherWork.cells;
136
+ if (reset)
137
+ matcherWork.cells = 0;
138
+ return n;
139
+ }
140
+ /** `*` is any run of characters, `?` one character. O(glob x text). */
141
+ function wildMatch(glob, text) {
142
+ matcherWork.cells += glob.length * text.length;
143
+ let prev = new Uint8Array(text.length + 1);
144
+ let cur = new Uint8Array(text.length + 1);
145
+ prev[0] = 1;
146
+ for (let i = 1; i <= glob.length; i += 1) {
147
+ const g = glob[i - 1];
148
+ const star = g === '*';
149
+ cur[0] = star ? prev[0] : 0;
150
+ for (let j = 1; j <= text.length; j += 1) {
151
+ if (star)
152
+ cur[j] = prev[j] || cur[j - 1] ? 1 : 0;
153
+ else
154
+ cur[j] = prev[j - 1] && (g === '?' || g === text[j - 1]) ? 1 : 0;
155
+ }
156
+ [prev, cur] = [cur, prev];
157
+ }
158
+ return prev[text.length] === 1;
159
+ }
160
+ const DYN = '\u0001';
161
+ const SHORT_NAME = /^([^~./*?\u0001]{1,6})~\d{1,6}(?:\.([^.*?\u0001]{1,3}))?$/;
162
+ function fuzzyOf(c) {
163
+ if (/[*?[\u0001]/.test(c)) {
164
+ // A character class is one character; a dynamic part is any run.
165
+ const glob = c.replace(/\[[^\]]*\]?/g, '?').split(DYN).join('*').replace(/\*+/g, '*');
166
+ return (name) => wildMatch(glob, name);
167
+ }
168
+ const m = SHORT_NAME.exec(c);
169
+ if (!m)
170
+ return null;
171
+ const heads = [m[1]];
172
+ if (/^.{1,2}[0-9a-f]{4}$/.test(m[1]))
173
+ heads.push(m[1].slice(0, m[1].length - 4));
174
+ const ext = m[2];
175
+ return (name) => {
176
+ // A short name keeps an underscore of the long name (`ID_RSA~1`); spaces are dropped. Both readings are tried.
177
+ const kept = name.replace(/^\.+/, '').replace(/ /g, '');
178
+ return [kept, kept.replace(/_/g, '')].some((n) => {
179
+ if (!heads.some((h) => n.startsWith(h)))
180
+ return false;
181
+ if (!ext)
182
+ return true;
183
+ const dot = n.lastIndexOf('.');
184
+ return dot > 0 && n.slice(dot + 1).startsWith(ext);
185
+ });
186
+ };
187
+ }
188
+ // Multi-word folder names the patterns name as one word.
189
+ function multiword(s) {
190
+ return s.replace(/start\s+menu/g, 'start_menu').replace(/program\s+files\s*\(x86\)/g, 'program_files_x86').replace(/program\s+files/g, 'program_files')
191
+ .replace(/github\s+cli/g, 'github_cli');
192
+ }
193
+ // Environment variables and PowerShell names that stand for a known folder.
194
+ const FOLDER_VARS = [
195
+ [/\$\{env:(?:userprofile|home)\}|\$env:(?:userprofile|home)\b|%userprofile%|%home%|%homedrive%%homepath%|\$env:homedrive\$env:homepath|\$\{(?:home|userprofile)\}|\$(?:home|userprofile)\b/g, HOME],
196
+ [/\$\{env:appdata\}|\$env:appdata\b|%appdata%/g, `${HOME}/appdata/roaming`],
197
+ [/\$\{env:localappdata\}|\$env:localappdata\b|%localappdata%/g, `${HOME}/appdata/local`],
198
+ [/\$\{env:(?:temp|tmp)\}|\$env:(?:temp|tmp)\b|%(?:temp|tmp)%/g, `${HOME}/appdata/local/temp`],
199
+ [/\$\{env:(?:programdata|allusersprofile)\}|\$env:(?:programdata|allusersprofile)\b|%(?:programdata|allusersprofile)%/g, 'c:/programdata'],
200
+ [/\$\{env:programfiles\(x86\)\}|%programfiles\(x86\)%/g, 'c:/program_files_x86'],
201
+ [/\$\{env:(?:programfiles|programw6432)\}|\$env:(?:programfiles|programw6432)\b|%(?:programfiles|programw6432)%/g, 'c:/program_files'],
202
+ [/\$\{env:(?:systemroot|windir)\}|\$env:(?:systemroot|windir)\b|%(?:systemroot|windir)%/g, 'c:/windows'],
203
+ [/\$\{env:onedrive\}|\$env:onedrive\b|%onedrive%/g, `${HOME}/onedrive`],
204
+ [/\$profile(?:\.(?:currentusercurrenthost|currentuserallhosts|allusersallhosts|alluserscurrenthost))?\b/g, `${HOME}/documents/windowspowershell/microsoft.powershell_profile.ps1`],
205
+ [/\$pshome\b/g, 'c:/windows/system32/windowspowershell/v1.0'],
206
+ [/shell:(?:common\s*)?startup|\[(?:system\.)?environment\]::getfolderpath\(\s*['"]?(?:common)?startup['"]?\s*\)|\[(?:system\.)?environment\+specialfolder\]::(?:common)?startup/g, `${HOME}/appdata/roaming/microsoft/windows/start_menu/programs/startup`],
207
+ [/\[(?:system\.)?environment\]::getfolderpath\(\s*['"]?userprofile['"]?\s*\)/g, HOME],
208
+ [/\[(?:system\.)?environment\]::getfolderpath\(\s*['"]?(?:mydocuments|personal)['"]?\s*\)/g, `${HOME}/documents`],
209
+ [/\[(?:system\.)?environment\]::getfolderpath\(\s*['"]?applicationdata['"]?\s*\)/g, `${HOME}/appdata/roaming`],
210
+ [/\[(?:system\.)?environment\]::getfolderpath\(\s*['"]?localapplicationdata['"]?\s*\)/g, `${HOME}/appdata/local`],
211
+ [/\$pwd\b|%cd%/g, '.'],
212
+ ];
213
+ function expandFolders(s) {
214
+ let out = s;
215
+ for (const [re, to] of FOLDER_VARS)
216
+ out = out.replace(re, to);
217
+ // `~` at the start of a word is the home folder (PowerShell, Git Bash).
218
+ return out.replace(/(^|[\s"'=(,;|&<>{}])~(?=\/|$|[\s"';|&)<>,])/g, `$1${HOME}`);
219
+ }
220
+ /** Device, UNC, provider, URL and MSYS forms of one path, rewritten to `x:/...` where they name a drive. */
221
+ function pathPrefixes(p) {
222
+ let s = p;
223
+ s = s.replace(/^file:\/\/(?:localhost)?\/?(?=[a-z]:)/, '').replace(/^file:\/\/\//, '/');
224
+ s = s.replace(/^(?:microsoft\.powershell\.core\/)?filesystem::/, '');
225
+ s = s.replace(/^\/\/[?.]\/unc\//, '//').replace(/^\/\/[?.]\/(?=[a-z]:)/, '').replace(/^\/\?\?\/(?=[a-z]:)/, '');
226
+ s = s.replace(/^\/\/[^/]+\/([a-z])\$(?=\/|$)/, '$1:');
227
+ s = s.replace(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?=\/|$)/, '$1:');
228
+ // `C:Users\x` (relative to the drive's current folder): read from the drive root (the strict reading).
229
+ s = s.replace(/^([a-z]):(?=[^/])/, '$1:/');
230
+ return s;
231
+ }
232
+ /**
233
+ * The parts of a path as Windows opens it, lower case. Trailing dots and spaces of a part are dropped and an
234
+ * alternate data stream (`name:stream`, `name::$DATA`) is cut off, as Win32 does; `.` and `..` are resolved.
235
+ * A UNC path starts with the part `//`; a path from the drive root starts with ''. A profile folder name is
236
+ * `__user__`. A part that is only a variable is DYN (any number of parts). Null: longer than any path.
237
+ */
238
+ function comps(raw) {
239
+ if (raw.length > tables_1.LIMITS.maxPathChars)
240
+ return null;
241
+ const s = pathPrefixes(raw);
242
+ const unc = s.startsWith('//');
243
+ const parts = (unc ? s.slice(2) : s).split('/');
244
+ const out = [];
245
+ if (unc)
246
+ out.push('//');
247
+ const drive = /^[a-z]:$/.test(parts[0]);
248
+ parts.forEach((part, i) => {
249
+ let c = part;
250
+ if (!(i === 0 && drive)) {
251
+ const colon = c.indexOf(':');
252
+ if (colon >= 0)
253
+ c = c.slice(0, colon);
254
+ if (c !== '.' && c !== '..')
255
+ c = c.replace(/[. ]+$/, '');
256
+ c = c.replace(/^ +/, '');
257
+ }
258
+ if (i === 0 && c === '' && !unc && parts.length > 1) {
259
+ out.push('');
260
+ return;
261
+ }
262
+ if (c === '' || c === '.')
263
+ return;
264
+ if (c === '..') {
265
+ const prev = out[out.length - 1];
266
+ if (prev !== undefined && prev !== '..' && prev !== '' && prev !== '//' && !/^[a-z]:$/.test(prev) && prev !== DYN)
267
+ out.pop();
268
+ else if (prev === undefined || prev === '..')
269
+ out.push('..');
270
+ return;
271
+ }
272
+ out.push(c);
273
+ });
274
+ if ((/^[a-z]:$/.test(out[0] || '') || out[0] === '') && out[1] === 'users' && out.length >= 3)
275
+ out[2] = '__user__';
276
+ return out;
277
+ }
278
+ function view(c) {
279
+ const gs = c.map((x) => x === DYN);
280
+ const fz = c.map((x) => (x === DYN ? () => true : fuzzyOf(x)));
281
+ return { comps: c, fz, gs, hasFuzzy: fz.some(Boolean), set: new Set(c) };
282
+ }
283
+ function compMatch(p, c, fuzzy) {
284
+ if (p.k === 'gs')
285
+ return false;
286
+ if (fuzzy) {
287
+ if (p.k === 'lit')
288
+ return p.v !== '' && (fuzzy(p.v) || (p.v.includes('_') && fuzzy(p.v.replace(/_/g, ''))));
289
+ if (p.v === '?:' || p.v === '*')
290
+ return true;
291
+ return (GLOB_COMP_SAMPLES[p.v] || [p.v.replace(/[*?]/g, 'x')]).some((v) => fuzzy(v));
292
+ }
293
+ if (c === '__user__' && p.k === 'glob')
294
+ return true;
295
+ return p.k === 'lit' ? p.v === c : p.re.test(c);
296
+ }
297
+ /** The pattern matches the path. Both sides may hold "any number of parts" (`**`, a dynamic part). */
298
+ function matches(pat, v) {
299
+ if (pat.key !== null && !v.hasFuzzy && !v.set.has(pat.key))
300
+ return false;
301
+ const n = v.comps.length;
302
+ const m = pat.comps.length;
303
+ const memo = new Uint8Array((m + 1) * (n + 1));
304
+ matcherWork.cells += (m + 1) * (n + 1);
305
+ const go = (i, j) => {
306
+ const k = i * (n + 1) + j;
307
+ if (memo[k])
308
+ return memo[k] === 1;
309
+ let r;
310
+ if (i === m && j === n)
311
+ r = true;
312
+ else if (j < n && v.gs[j])
313
+ r = go(i, j + 1) || (i < m && go(i + 1, j));
314
+ else if (i === m)
315
+ r = false;
316
+ else {
317
+ const pc = pat.comps[i];
318
+ if (pc.k === 'gs')
319
+ r = go(i + 1, j) || (j < n && go(i, j + 1));
320
+ else
321
+ r = j < n && compMatch(pc, v.comps[j], v.fz[j]) && go(i + 1, j + 1);
322
+ }
323
+ memo[k] = r ? 1 : 2;
324
+ return r;
325
+ };
326
+ return go(0, 0);
327
+ }
328
+ function anyMatch(list, v) {
329
+ return list.some((p) => matches(p, v));
330
+ }
331
+ function isSecret(v, parts) {
332
+ if (!v.comps.length)
333
+ return false;
334
+ const base = v.comps[v.comps.length - 1];
335
+ const baseFz = v.fz[v.fz.length - 1];
336
+ if (!baseFz && /^\.env(?:\.|$)/.test(base) && tables_1.ENV_TEMPLATE_SUFFIX.test(base))
337
+ return false;
338
+ const keyFile = baseFz ? KEY_EXT_SAMPLES.some((s) => baseFz(s)) : KEY_EXT.test(base);
339
+ if (keyFile && !v.comps.some((c) => tables_1.KEY_FIXTURE_SEGMENTS.has(c)))
340
+ return true;
341
+ if (!anyMatch(patterns(parts).secret, v))
342
+ return false;
343
+ // ssh public keys are never secret.
344
+ if (!baseFz && v.comps.includes('.ssh') && /\.pub$/.test(base))
345
+ return false;
346
+ return true;
347
+ }
348
+ function protectedKind(v, parts) {
349
+ if (!v.comps.length)
350
+ return null;
351
+ const pats = patterns(parts);
352
+ if (anyMatch(pats.op, v))
353
+ return 'op';
354
+ if (anyMatch(pats.wo, v))
355
+ return 'wo';
356
+ return null;
357
+ }
358
+ /** The last part is a program name a hook may be run with. A wildcard counts only for the classic extensions. */
359
+ function isShim(v) {
360
+ if (!v.comps.length)
361
+ return false;
362
+ const base = v.comps[v.comps.length - 1];
363
+ const fz = v.fz[v.fz.length - 1];
364
+ return fz ? SHIM_SAMPLES.some((s) => fz(s)) : SHIM.test(base);
365
+ }
366
+ let ANCHORS = null;
367
+ function anchors() {
368
+ if (ANCHORS)
369
+ return ANCHORS;
370
+ const home = HOME_ANCHORS.map((a) => compilePattern(`?:/users/*/${a}`).comps);
371
+ const system = SYSTEM_ANCHORS.map((a) => compilePattern(a).comps);
372
+ const tails = HOME_ANCHORS.filter((a) => !GENERIC_HOME_DIRS.has(a.split('/')[0])).map((a) => a.split('/'));
373
+ ANCHORS = { home, system, tails };
374
+ return ANCHORS;
375
+ }
376
+ /**
377
+ * The path is an anchor or a folder above one (its removal or move takes a protected file with it).
378
+ * `minDepth`: the fewest parts an absolute path needs (4 = only folders inside a profile).
379
+ */
380
+ function ancestorOfAnchor(v, minDepth) {
381
+ const a = anchors();
382
+ const rooted = v.comps[0] === '';
383
+ const c = rooted ? ['c:', ...v.comps.slice(1)] : v.comps;
384
+ const fz = rooted ? [null, ...v.fz.slice(1)] : v.fz;
385
+ const prefixOf = (anchor) => c.length <= anchor.length && c.every((x, i) => compMatch(anchor[i], x, fz[i]));
386
+ if (/^[a-z]:$/.test(c[0] || '')) {
387
+ if (c.length < minDepth)
388
+ return false;
389
+ return a.home.some(prefixOf) || a.system.some(prefixOf);
390
+ }
391
+ if (c[0] === '//')
392
+ return false;
393
+ if (c[0] === '..' || c[0] === DYN)
394
+ return minDepth <= 1;
395
+ // A relative path under an unknown folder: the start of a profile anchor (`.claude`, `appdata\roaming`).
396
+ return a.tails.some((t) => c.length <= t.length && c.every((x, i) => (fz[i] ? fz[i](t[i]) : x === t[i])));
397
+ }
398
+ /**
399
+ * The folder is an anchor itself or the profile folder: a protected folder (`.ssh`, `.claude`, `appdata\roaming\npm` ...), a system
400
+ * tree, or `C:\Users\<name>`. `del <folder>\*` is then a hit; a folder only ABOVE an anchor (`Documents`, `AppData`) is not, since
401
+ * `*` names the files in the folder and not the folders under it. The folder has no wildcard (bareStarFolder).
402
+ */
403
+ function isAnchorFolder(v) {
404
+ const a = anchors();
405
+ const rooted = v.comps[0] === '';
406
+ const c = rooted ? ['c:', ...v.comps.slice(1)] : v.comps;
407
+ if (/^[a-z]:$/.test(c[0] || '')) {
408
+ if (c.length === 3 && c[1] === 'users')
409
+ return true;
410
+ return [...a.home, ...a.system].some((anchor) => c.length === anchor.length && c.every((x, i) => compMatch(anchor[i], x, null)));
411
+ }
412
+ if (c[0] === '//' || c[0] === '..' || c[0] === DYN)
413
+ return false;
414
+ return a.tails.some((t) => c.length === t.length && c.every((x, i) => x === t[i]));
415
+ }
416
+ // ---------------------------------------------------------------------------------------------
417
+ // shell text
418
+ const READERS = new Set(['type', 'gc', 'get-content', 'cat', 'more', 'less', 'head', 'tail', 'findstr', 'find', 'select-string', 'sls', 'fc', 'comp',
419
+ 'compare-object', 'diff', 'get-filehash', 'format-hex', 'fhx', 'get-item', 'gi', 'get-itemproperty', 'gp', 'get-acl', 'test-path', 'resolve-path',
420
+ 'rvpa', 'split-path', 'join-path', 'convert-path', 'dir', 'ls', 'gci', 'get-childitem', 'tree', 'where', 'get-command', 'gcm', 'which', 'echo',
421
+ 'write-output', 'write', 'write-host', 'cd', 'chdir', 'set-location', 'sl', 'pushd', 'push-location', 'popd', 'pop-location', 'wc', 'grep', 'rg',
422
+ 'measure-object']);
423
+ // Programs that take a secret path without printing or copying its content (POSIX S4_EXEMPT_PROGRAMS).
424
+ const S4_EXEMPT = new Set(['dir', 'ls', 'gci', 'get-childitem', 'tree', 'test-path', 'resolve-path', 'rvpa', 'split-path', 'join-path', 'convert-path',
425
+ 'get-acl', 'icacls', 'attrib', 'takeown', 'cd', 'chdir', 'set-location', 'sl', 'pushd', 'push-location', 'popd', 'echo', 'write-output', 'write',
426
+ 'write-host', 'where', 'get-command', 'gcm', 'which', 'del', 'erase', 'rd', 'rmdir', 'rm', 'ri', 'remove-item', 'mkdir', 'md', 'ssh', 'ssh-add',
427
+ 'sftp', 'kubectl', 'helm']);
428
+ const GIT_READ_SUBS = new Set(['status', 'diff', 'log', 'show', 'blame', 'annotate', 'ls-files', 'ls-tree', 'grep', 'add', 'rev-parse', 'cat-file',
429
+ 'check-ignore', 'check-attr', 'shortlog', 'whatchanged', 'describe', 'name-rev', 'rev-list', 'diff-tree', 'diff-files', 'diff-index',
430
+ 'count-objects', 'help', 'version', 'var', 'commit']);
431
+ const GIT_S4_EXEMPT_SUBS = new Set(['rm', 'check-ignore', 'check-attr', 'ls-files', 'status', 'log', 'restore']);
432
+ // Programs that remove, move, rename, link or re-permission what they name: a folder above an anchor stops.
433
+ const DESTRUCTIVE = new Set(['del', 'erase', 'rd', 'rmdir', 'rm', 'ri', 'remove-item', 'move', 'mv', 'mi', 'move-item', 'ren', 'rename', 'rni',
434
+ 'rename-item', 'robocopy', 'mklink', 'new-item', 'ni', 'set-acl', 'icacls', 'takeown', 'attrib', 'fsutil', 'junction', 'ln']);
435
+ // Modelled writers: with DESTRUCTIVE, the only programs that stop on a `wo` place (POSIX: the system trees and program folders).
436
+ const WRITERS = new Set([...DESTRUCTIVE, 'set-content', 'sc', 'add-content', 'ac', 'out-file', 'tee', 'tee-object', 'md', 'mkdir', 'copy', 'cp',
437
+ 'cpi', 'copy-item', 'xcopy', 'clear-content', 'clc', 'expand', 'expand-archive', 'compress-archive', 'tar', '7z', '7za', 'unzip',
438
+ 'invoke-webrequest', 'iwr', 'invoke-restmethod', 'irm', 'wget', 'curl', 'start-bitstransfer', 'bitsadmin', 'certutil', 'install', 'replace',
439
+ 'export-csv', 'export-clixml', 'set-itemproperty', 'sp', 'new-itemproperty', 'dd', 'sed', 'scp', 'rsync', 'touch', 'set-item', 'si']);
440
+ const DOTNET_WRITE = /\[(?:system\.)?io\.(?:file|directory)\]::(?:write|append|copy|move|create|replace|open(?!read)|delete|setattributes|encrypt)|\.(?:copyto|moveto|writealltext|writeallbytes|writealllines|appendalltext|createtext|appendtext|openwrite|downloadfile|savetofile|delete)\s*\(|system\.net\.webclient|::extracttodirectory|scripting\.filesystemobject|io\.streamwriter/;
441
+ const INTERPRETERS = new Set(['node', 'nodejs', 'python', 'python3', 'py', 'ruby', 'perl', 'php', 'deno', 'bun', 'tsx', 'ts-node']);
442
+ const START_LIKE = new Set(['start', 'start-process', 'saps']);
443
+ // Text whose meaning the lexical reading cannot follow (built strings, evaluated text): the mapper's own
444
+ // S0/S4 tripwire is applied to it, as the mapper does for text it cannot parse.
445
+ const DYNAMIC_TEXT = /(?:^|[^a-z0-9_-])(?:iex|invoke-expression|invoke-command|icm)(?![a-z0-9_-])|\[scriptblock\]::create|\[char\]|-join(?![a-z])|-f(?=\s)|\[string\]::(?:concat|join|format)|\.replace\s*\(|-replace(?![a-z])|\.invoke\s*\(|frombase64string|-split(?![a-z])/;
446
+ function progBase(w) {
447
+ if (!w)
448
+ return '';
449
+ const b = w.slice(w.lastIndexOf('/') + 1);
450
+ return b.replace(/\.(?:exe|cmd|bat|com|ps1)$/, '');
451
+ }
452
+ /**
453
+ * The text cut into commands and words with every quote removed: separators split even inside a quoted
454
+ * string (the strict reading; a quoted command passed to `cmd /c` or `powershell -c` is read as commands).
455
+ */
456
+ function segments(t) {
457
+ const out = [];
458
+ const fresh = (opener) => ({ words: [], redirect: new Set(), input: new Set(), here: new Set(), opener, hasEq: false, assign: false });
459
+ let cur = fresh('');
460
+ let word = '';
461
+ let pending = false;
462
+ let pendingIn = false;
463
+ let pendingHere = false;
464
+ const endWord = () => {
465
+ if (!word)
466
+ return;
467
+ if (pending) {
468
+ cur.redirect.add(cur.words.length);
469
+ pending = false;
470
+ }
471
+ if (pendingIn) {
472
+ cur.input.add(cur.words.length);
473
+ pendingIn = false;
474
+ }
475
+ if (pendingHere) {
476
+ cur.here.add(cur.words.length);
477
+ pendingHere = false;
478
+ }
479
+ cur.words.push(word);
480
+ word = '';
481
+ };
482
+ const endSeg = (opener) => {
483
+ endWord();
484
+ if (cur.words.length)
485
+ out.push(cur);
486
+ cur = fresh(opener);
487
+ pending = false;
488
+ pendingIn = false;
489
+ pendingHere = false;
490
+ };
491
+ for (let i = 0; i < t.length; i += 1) {
492
+ const ch = t[i];
493
+ if (ch === '"' || ch === "'")
494
+ continue;
495
+ if (ch === '$' && t[i + 1] === '(') {
496
+ endSeg('$(');
497
+ i += 1;
498
+ continue;
499
+ }
500
+ if (ch === '(') {
501
+ endSeg('(');
502
+ continue;
503
+ }
504
+ if (ch === '{') {
505
+ endSeg('{');
506
+ continue;
507
+ }
508
+ if (';|&\n\r)}'.includes(ch)) {
509
+ endSeg(';');
510
+ continue;
511
+ }
512
+ if (ch === '>') {
513
+ // `2>`, `*>`: the descriptor is not a word.
514
+ if (/^(?:\d|\*)$/.test(word))
515
+ word = '';
516
+ endWord();
517
+ while (t[i + 1] === '>')
518
+ i += 1;
519
+ pending = true;
520
+ continue;
521
+ }
522
+ if (ch === '<') {
523
+ // `< name` reads the file; `<<` and `<<<` (here-documents and here-strings) and `<(` take text or a command, not a file.
524
+ endWord();
525
+ let n = 1;
526
+ while (t[i + 1] === '<') {
527
+ i += 1;
528
+ n += 1;
529
+ }
530
+ pendingIn = n === 1 && t[i + 1] !== '(';
531
+ pendingHere = n > 1;
532
+ continue;
533
+ }
534
+ if (ch === '=') {
535
+ // `$x = …`, `$x += …`: a PowerShell assignment.
536
+ if (/^[-+*/?]$/.test(word))
537
+ word = '';
538
+ endWord();
539
+ if (cur.words.length === 1 && cur.words[0].startsWith('$') && !cur.hasEq)
540
+ cur.assign = true;
541
+ cur.hasEq = true;
542
+ continue;
543
+ }
544
+ if (ch === ',' || /\s/.test(ch)) {
545
+ endWord();
546
+ continue;
547
+ }
548
+ word += ch;
549
+ }
550
+ endSeg(';');
551
+ return out;
552
+ }
553
+ /**
554
+ * The program of a command and the index of its word; `cmd /c`, `powershell -c`, call operators and `$x =` are looked through.
555
+ * `launchers`: the indexes of the words that only start the command (`cmd.exe` of `cmd.exe /c ...`, `powershell.exe`, `pwsh.exe`,
556
+ * and the program word after `env`). Each is the program position of its own level and is read like the program word is; the
557
+ * words of the command they start are never launchers, so those are read in full.
558
+ */
559
+ function programOf(seg) {
560
+ const w = seg.words;
561
+ if (seg.opener === '(')
562
+ return { prog: '', at: -1, sub: null, launchers: [] };
563
+ let i = 0;
564
+ if (seg.assign) {
565
+ // `$x = <command>` runs the command; `$x = <value>` stores a value (no program: every word is an operand).
566
+ if (w.length < 2 || !/^[a-z][a-z0-9-]*$/.test(w[1]))
567
+ return { prog: '', at: -1, sub: null, launchers: [] };
568
+ i = 1;
569
+ }
570
+ // A redirect target is never the program (`> x.json echo`, or `echo '{}' > x.json`, whose `{}` cut the text).
571
+ while (i < w.length && (seg.redirect.has(i) || w[i] === '.' || w[i] === '&' || w[i] === 'call' || w[i] === '@'))
572
+ i += 1;
573
+ let prog = progBase(w[i]);
574
+ const launchers = [];
575
+ for (let depth = 0; depth < 4 && i < w.length; depth += 1) {
576
+ let j = -1;
577
+ if (prog === 'cmd')
578
+ j = w.findIndex((x, k) => k > i && /^\/[ckr]$/.test(x));
579
+ else if (prog === 'powershell' || prog === 'pwsh')
580
+ j = w.findIndex((x, k) => k > i && /^[-/](?:c|co|com|comm|comma|comman|command|commandwithargs|cwa)$/.test(x));
581
+ if (j < 0 || j + 1 >= w.length)
582
+ break;
583
+ launchers.push(i);
584
+ i = j + 1;
585
+ // A redirect target is never the program here either (`cmd.exe /c > node.cmd echo x` writes node.cmd).
586
+ while (i < w.length && (seg.redirect.has(i) || w[i] === '.' || w[i] === '&' || w[i] === 'call'))
587
+ i += 1;
588
+ prog = progBase(w[i]);
589
+ }
590
+ // `env` runs the next word as a program (after `-i`, `-0`, `-v`). Its program word is a launcher; what `env` itself does with
591
+ // the rest is not changed. With an `=` in the text the assignments cannot be told from the program, so nothing is looked through.
592
+ if (prog === 'env' && !seg.hasEq) {
593
+ let k = i + 1;
594
+ while (k < w.length && /^-(?:i|0|v)$/.test(w[k]))
595
+ k += 1;
596
+ if (k < w.length && !w[k].startsWith('-') && !seg.redirect.has(k))
597
+ launchers.push(k);
598
+ }
599
+ let sub = null;
600
+ if (prog === 'git') {
601
+ for (let k = i + 1; k < w.length; k += 1) {
602
+ if (/^-[cC]$/.test(w[k]) || /^--(?:git-dir|work-tree|namespace|exec-path)$/.test(w[k])) {
603
+ k += 1;
604
+ continue;
605
+ }
606
+ if (w[k].startsWith('-'))
607
+ continue;
608
+ sub = w[k];
609
+ break;
610
+ }
611
+ }
612
+ return { prog, at: i, sub, launchers };
613
+ }
614
+ function isOption(w) {
615
+ return /^-[a-z?][\w?-]*$/.test(w) || /^--[a-z][\w-]*$/.test(w) || /^\/[a-z?]{1,12}$/.test(w);
616
+ }
617
+ // Verbs that persist something or poison the environment (S0) and that read secrets (S4), matched on the
618
+ // normalised text. Each test is linear: two facts that must both hold are tested on their own.
619
+ const WS = '(?:^|[^a-z0-9_.$-])';
620
+ const PERSIST = [
621
+ new RegExp(`${WS}setx(?:\\.exe)?(?![a-z0-9_.-])`),
622
+ /::\s*setenvironmentvariable\b|\.setenvironmentvariable\s*\(/,
623
+ new RegExp(`${WS}reg(?:\\.exe)?\\s+(?:add|import|load|restore|copy|delete|unload)(?![a-z0-9_-])`),
624
+ new RegExp(`${WS}(?:regedit|regedt32|regini)(?:\\.exe)?(?![a-z0-9_-])`),
625
+ /(?:register|set)-scheduled(?:task|job)\b|schedule\.service|new-jobtrigger/,
626
+ /__eventfilter|commandlineeventconsumer|activescripteventconsumer|__filtertoconsumerbinding|stdregprov/,
627
+ new RegExp(`${WS}sc(?:\\.exe)?\\s+(?:create|config|failure)\\s`),
628
+ /(?:new|set)-service\b/,
629
+ ];
630
+ const SCHTASKS = new RegExp(`${WS}schtasks(?:\\.exe)?(?![a-z0-9_-])`);
631
+ const SCHTASKS_WRITE = /\/(?:create|change)\b/;
632
+ const AT_JOB = new RegExp(`${WS}at(?:\\.exe)?\\s+(?:\\/\\/\\S+\\s+)?\\d{1,2}:\\d{2}`);
633
+ const WMIC = new RegExp(`${WS}wmic(?:\\.exe)?(?![a-z0-9_-])`);
634
+ const WMIC_ENV = /\benvironment\b/;
635
+ const REG_PATH = /(?:^|[^a-z0-9_])(?:hk(?:cu|lm|cr|u|cc)|hkey_(?:current_user|local_machine|classes_root|users|current_config))(?=[:/\s"';]|$)|registry::/;
636
+ const REG_WRITE = /(?:new|set|remove|rename|copy|move|clear)-item(?:property)?\b|(?:^|[\s;|&({])(?:ni|si|sp|rp|ri|rni|rnp|cpi|cpp|mi|mp|cli|clp|del|erase|rm|rd|rmdir|move|copy|mv|cp)(?=$|[\s;|&)}])|(?:\.|::)\s*(?:setvalue|createsubkey|deletevalue|deletesubkey(?:tree)?|setaccesscontrol)\s*\(/;
637
+ const STORE = new RegExp(`${WS}(?:cmdkey|vaultcmd)(?:\\.exe)?(?![a-z0-9_-])|passwordvault|get-storedcredential|${WS}cred(?:read|enumerate)\\b|protecteddata\\]?::\\s*unprotect|mimikatz|sekurlsa|lsadump|credential-manager(?:-core)?\\s+(?:get|erase|store)|${WS}git(?:\\.exe)?\\s+credential(?:-manager(?:-core)?)?\\s+(?:fill|get)`);
638
+ const STORE_PAIRS = [[/convertfrom-securestring/, /-asplaintext/], [/comsvcs/, /minidump/], [/lsass/, /dump|procdump|minidump/],
639
+ [/get-credential|pscredential/, /getnetworkcredential\(\)\s*\.\s*password/]];
640
+ const PROVIDER_ENV = /(?:^|[^a-z0-9_${:])(env|variable):\/?([a-z0-9_*?().-]*)/g;
641
+ const ENV_READ = /\$\{?env:([a-z0-9_]+)\}?(\s*=(?!=))?/g;
642
+ const GETENV = /getenvironmentvariable\s*\(\s*(['"]?)([^'"),\s]*)/g;
643
+ const CMD_VAR = /%([a-z_][a-z0-9_]*)%|!([a-z_][a-z0-9_]*)!/g;
644
+ function verbHits(s, hit) {
645
+ if (PERSIST.some((re) => re.test(s)) || (SCHTASKS.test(s) && SCHTASKS_WRITE.test(s)) || (WMIC.test(s) && WMIC_ENV.test(s))
646
+ || (REG_PATH.test(s) && REG_WRITE.test(s)) || AT_JOB.test(s))
647
+ hit('S0', exports.WINDOWS_REASONS.persistence);
648
+ if (STORE.test(s) || STORE_PAIRS.some(([a, b]) => a.test(s) && b.test(s)))
649
+ hit('S4', exports.WINDOWS_REASONS.secretStore);
650
+ for (const m of s.matchAll(PROVIDER_ENV)) {
651
+ const name = m[2];
652
+ if (name === '' || /[*?]/.test(name) || tables_1.SECRET_VAR.test(name)) {
653
+ hit('S4', exports.WINDOWS_REASONS.envDump);
654
+ break;
655
+ }
656
+ }
657
+ for (const m of s.matchAll(ENV_READ))
658
+ if (!m[2] && tables_1.SECRET_VAR.test(m[1])) {
659
+ hit('S4', exports.WINDOWS_REASONS.envDump);
660
+ break;
661
+ }
662
+ if (/getenvironmentvariables\b/.test(s))
663
+ hit('S4', exports.WINDOWS_REASONS.envDump);
664
+ for (const m of s.matchAll(GETENV))
665
+ if (!m[1] || tables_1.SECRET_VAR.test(m[2])) {
666
+ hit('S4', exports.WINDOWS_REASONS.envDump);
667
+ break;
668
+ }
669
+ for (const m of s.matchAll(CMD_VAR))
670
+ if (tables_1.SECRET_VAR.test(m[1] || m[2])) {
671
+ hit('S4', exports.WINDOWS_REASONS.envDump);
672
+ break;
673
+ }
674
+ }
675
+ /**
676
+ * Git commit and tag messages (`-m "..."`) are data. A message is taken out of the text only when it is a plain
677
+ * literal in every shell that may read it: a double-quoted string at the top level (an even count of `"` and `'`
678
+ * before it), in a git command, with no `$`, backtick, `%`, `!`, `^` or `\` inside, and no `^`, backtick or `\"`
679
+ * anywhere in the text.
680
+ */
681
+ // One forward pass (linear): the quote counts and the command start are carried from match to match, and the
682
+ // git test is made once per command start. A git head that would end after the message counts as not git.
683
+ const GIT_HEAD = /\s*(?:\S*[\\/])?git(?:\.exe)?\s/iy;
684
+ function stripGitMessages(raw) {
685
+ if (/[\^`]|\\"/.test(raw))
686
+ return raw;
687
+ let pos = 0;
688
+ let dq = 0;
689
+ let sq = 0;
690
+ let segStart = -1;
691
+ const heads = new Map();
692
+ return raw.replace(/(\s(?:-m|--message)(?:\s+|=))"([^"$`%!^\\\r\n]*)"(?=\s|$)/gi, (all, head, _msg, offset) => {
693
+ for (; pos < offset; pos += 1) {
694
+ const ch = raw[pos];
695
+ if (ch === '"')
696
+ dq += 1;
697
+ else if (ch === "'")
698
+ sq += 1;
699
+ else if (ch === ';' || ch === '&' || ch === '|' || ch === '\n' || ch === '(' || ch === '{')
700
+ segStart = pos;
701
+ }
702
+ if (dq % 2 !== 0 || sq % 2 !== 0)
703
+ return all;
704
+ let end = heads.get(segStart);
705
+ if (end === undefined) {
706
+ GIT_HEAD.lastIndex = segStart + 1;
707
+ end = GIT_HEAD.test(raw) ? GIT_HEAD.lastIndex : -1;
708
+ heads.set(segStart, end);
709
+ }
710
+ if (end < 0 || end > offset)
711
+ return all;
712
+ return `${head}""`;
713
+ });
714
+ }
715
+ function normText(raw) {
716
+ let s = raw;
717
+ s = s.replace(/[\u2010-\u2015\u2212]/g, '-').replace(/[\u2018-\u201b\u2032]/g, "'").replace(/[\u201c-\u201f\u2033]/g, '"');
718
+ s = s.replace(/[\u00a0\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]/g, ' ').replace(/[\u200b-\u200d\u2060\ufeff]/g, '');
719
+ s = s.toLowerCase().replace(/[`^]/g, '').replace(/\\/g, '/');
720
+ // String joints (`'C:\Users\x\.aws\' + 'credentials'`).
721
+ s = s.replace(/(["'])\s*\+\s*(["'])/g, '');
722
+ s = multiword(expandFolders(s));
723
+ // The profile folder name may hold spaces: one word for matching.
724
+ s = s.replace(/([a-z]:\/+users\/+)([^/;|&<>"'\n\r(){},=]+)(?=\/)/g, '$1__user__');
725
+ return s;
726
+ }
727
+ /** A word with its variables as dynamic parts (`$x`, `${x}`, `$env:x`, `%x%`, `!x!`); known folders were expanded before. */
728
+ function dynWord(w) {
729
+ return w.replace(/\$\{[^}]*\}|\$(?:[a-z_][a-z0-9_]*:)?[a-z_][a-z0-9_]*|\$_|%[a-z_][a-z0-9_()]*%|![a-z_][a-z0-9_]*!/g, DYN);
730
+ }
731
+ /** `-EncodedCommand` values (any accepted spelling) that decode to UTF-16LE text, from the text as written. */
732
+ function encodedTexts(raw) {
733
+ const out = [];
734
+ const re = /(?:^|[\s"'])[-/\u2013\u2014](?:e|ec|en|enc|enco|encod|encode|encoded|encodedc|encodedco|encodedcom|encodedcomm|encodedcomma|encodedcomman|encodedcommand)(?:\s+|:)["']?([A-Za-z0-9+/=]{8,})/gi;
735
+ for (const m of raw.matchAll(re)) {
736
+ if (out.length >= 16)
737
+ break;
738
+ const buf = Buffer.from(m[1], 'base64');
739
+ if (buf.length < 2 || buf.length % 2 !== 0)
740
+ continue;
741
+ const text = buf.toString('utf16le');
742
+ if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/.test(text))
743
+ continue;
744
+ out.push(text);
745
+ }
746
+ return out;
747
+ }
748
+ // ---------------------------------------------------------------------------------------------
749
+ // unusual path spellings (owner route "hold all odd paths", 22 Oct 2026)
750
+ //
751
+ // On win32 a path written in an unusual way is HELD for the owner (Sx.windows_odd_path), whatever it points at. Nothing here
752
+ // resolves a path or asks where a form leads: the spelling alone is the reason. The forms: 8.3 short names (a `NAME~N` part),
753
+ // the `\\?\` and `\\.\` prefixes and the NT `\??\` prefix, UNC paths, three or more leading separators, a mix of `/` and `\` at the
754
+ // start of a path, an alternate data stream (`name:stream`, `::$DATA`), and a path that starts like one and cannot be taken apart
755
+ // (longer than any path, a control character, a look-alike of a separator). Plain paths, URLs, host:port, `git@host:repo`, git
756
+ // revisions (`HEAD~1`, `HEAD:src/a.ts`) and `a:/b` pairs (container volumes, PATH lists) are not held. The check only adds a hit,
757
+ // so it can never turn a stop into a pass, and it runs from the same win32-only entry as the rest.
758
+ //
759
+ // A stream on a bare name (no separator) is read from the words around it, since a bare word alone says little (`app.main:app`,
760
+ // `localhost:3000`, `echo a:b` are not paths): the typed spellings (`a.txt::$DATA`, `a.txt:s:$DATA`, `:Zone.Identifier`) in any
761
+ // word; any colon in a word that a file program or a redirect names as a path (`type fxfile:s`, `echo x > fxfile:s`,
762
+ // `Set-Content fxfile:s x`), except a PowerShell drive or variable scope (`Env:PATH`, `$env:TEMP`) or a DOS device (`nul:`); and, in any other word, a colon after
763
+ // a file-like name with a stream name behind it (`node a.txt:s`), except host:port and file:line:col.
764
+ const SEP = '[\\\\/]';
765
+ const ODD_DEVICE = new RegExp(`^${SEP}{2}[?.]${SEP}|^${SEP}\\?\\?${SEP}`);
766
+ const ODD_UNC = new RegExp(`^${SEP}{2}[^\\\\/\\s?.][^\\\\/]*${SEP}[^\\\\/]`);
767
+ const ODD_LEADING = new RegExp(`^${SEP}{3,}[^\\\\/]`);
768
+ const URL_SCHEME = /^[a-z][a-z0-9+.-]+:\/\//i;
769
+ const SCP_LIKE = /^[^\\/:\s@]+@[^\\/:\s]+:/;
770
+ const HOST_PORT = /^[a-z0-9.-]+:\d{1,5}(?:[\\/]|$)/i;
771
+ const STREAM_TYPE = /:\$(?:data|index_allocation|bitmap|attribute_list|ea|reparse_point)\b|:zone\.identifier\b/i;
772
+ const SHORT_PART = /^[^\s~\\/:*?"<>|]{1,8}~\d{1,7}(?:\.[^\s~\\/:*?"<>|.]{0,3})?$/;
773
+ const SEP_LOOKALIKE = /[\uff0f\uff3c\u2215\u2216\u29f5\u29f8\u2044]/;
774
+ // eslint-disable-next-line no-control-regex
775
+ const CONTROL = /[\u0000-\u001f\u007f]/;
776
+ const PS_DRIVE = /^(?:\$(?:env|global|script|local|private|using):[^:]*$|(?:env|variable|function|alias|cert|wsman|hklm|hkcu|temp|certificate):|(?:registry|variable|alias|function|environment|certificate|wsman)::|(?:nul|con|prn|aux|com\d|lpt\d):$)/i;
777
+ const STREAM_TYPED = /:[^:\s]*:\$(?:data|index_allocation|bitmap|attribute_list|ea|reparse_point)\b|:zone\.identifier\b/i;
778
+ const LINE_COL = /^[^\s:\\/]+:\d{1,5}(?::\d{1,5})?$/;
779
+ const FILE_LIKE = /\.(?:txt|log|json|jsonc|ini|cfg|conf|config|env|yml|yaml|toml|xml|md|csv|tsv|dat|bak|tmp|db|sqlite|kdbx|pem|key|crt|cer|pfx|p12|pub|ps1|psm1|bat|cmd|vbs|exe|dll|lnk|zip|7z|tar|gz|pdf|docx?|xlsx?|html?|css)$|^\.[\w-]+$/i;
780
+ // Programs whose every operand is a path, and cmdlets whose first operand is the path.
781
+ const PATH_OPERANDS = new Set(['dir', 'ls', 'gci', 'get-childitem', 'cd', 'chdir', 'set-location', 'sl', 'pushd', 'push-location', 'type', 'cat', 'gc',
782
+ 'get-content', 'more', 'copy', 'cp', 'copy-item', 'cpi', 'move', 'mv', 'move-item', 'mi', 'del', 'erase', 'rd', 'rmdir', 'rm', 'ri', 'remove-item',
783
+ 'ren', 'rename', 'rni', 'rename-item', 'get-item', 'gi', 'test-path', 'notepad', 'mkdir', 'md', 'tree', 'xcopy', 'attrib', 'tee', 'tee-object', 'sort', 'fc',
784
+ 'comp', 'get-filehash', 'get-acl', 'set-acl', 'import-csv', 'export-csv', 'resolve-path', 'invoke-item', 'ii', 'get-itemproperty', 'gp']);
785
+ const PATH_FIRST_OPERAND = new Set(['set-content', 'sc', 'add-content', 'ac', 'out-file', 'new-item', 'ni', 'clear-content', 'clc']);
786
+ // For those cmdlets the path is the first positional word or the value of a path-naming parameter, wherever it stands. PowerShell takes
787
+ // any unique prefix of a parameter name, so every prefix counts; a prefix that fits a path name is read as one (fail closed). The value
788
+ // of a data parameter (`-Value x`, `-Encoding utf8`) is no path and must not use up the first-operand slot.
789
+ const PATH_PARAMS = ['path', 'literalpath', 'pspath', 'filepath', 'name', 'destination', 'lp'];
790
+ const DATA_PARAMS = ['value', 'encoding', 'itemtype', 'type', 'delimiter', 'inputobject', 'width', 'filter', 'include', 'exclude', 'credential', 'readcount', 'totalcount', 'tail', 'head'];
791
+ function paramOf(w) {
792
+ const m = /^-([a-z]+)(:?)/.exec(w);
793
+ if (!m)
794
+ return null;
795
+ const n = m[1];
796
+ const path = PATH_PARAMS.some((p) => p.startsWith(n));
797
+ return { path, data: !path && DATA_PARAMS.some((p) => p.startsWith(n)), attached: m[2] === ':' };
798
+ }
799
+ // ls with sort flags (`ls -St`, `ls -Str`) is no PowerShell -Stream: ls is Get-ChildItem there, which has no such parameter.
800
+ const LS_SORT = /^-Str?$/;
801
+ // Programs that open a file or folder by the name given, for a bare `NAME~N` word (no separator): a short name there.
802
+ const FILE_PROGRAMS = new Set(['dir', 'ls', 'gci', 'get-childitem', 'cd', 'chdir', 'set-location', 'sl', 'pushd', 'push-location', 'type', 'cat', 'gc',
803
+ 'get-content', 'more', 'copy', 'cp', 'copy-item', 'cpi', 'move', 'mv', 'move-item', 'mi', 'del', 'erase', 'rd', 'rmdir', 'rm', 'ri', 'remove-item',
804
+ 'xcopy', 'robocopy', 'mkdir', 'md', 'ren', 'rename', 'rni', 'rename-item', 'tree', 'attrib', 'icacls', 'takeown', 'get-item', 'gi', 'test-path',
805
+ 'start', 'invoke-item', 'ii', 'explorer', 'notepad']);
806
+ function baseName(w) {
807
+ return w.slice(Math.max(w.lastIndexOf('/'), w.lastIndexOf('\\')) + 1).toLowerCase().replace(/\.(?:exe|cmd|bat|com|ps1)$/, '');
808
+ }
809
+ /**
810
+ * One word (or, for a file tool, one whole path) read for an unusual spelling. `path`: the value is a path by definition (a file
811
+ * tool's argument); otherwise it must look like one. `git`: a git command is earlier in the line, so `name~N` and `rev:path`
812
+ * are revisions. `fileProgram`: a program that opens files by name is earlier in the line, so a bare `NAME~N` is a short name.
813
+ * `operand`: a file program or a redirect names this word as a path, so a colon in a bare word is a stream.
814
+ */
815
+ // A `file:` URL whose path is a drive (`file:///C:/x`, `file:/C:/x`, `file:C:/x`, `file:///C%3A/x`, `file:///C|/x`; the shell text is cut at
816
+ // the `|`, which leaves `file:///C`) or a UNC share
817
+ // (`file://srv/share/x`, `file:////srv/share/x`). `file://localhost/C:/x` is a drive path; a `file:` URL with a plain rooted path is not held.
818
+ const FILE_URL = /^file:([\\/]*)([\s\S]*)$/i;
819
+ const DRIVE_START = /^[a-z](?::|\||%3a|%7c|$)/i;
820
+ function fileUrlOdd(t) {
821
+ const m = FILE_URL.exec(t);
822
+ if (!m)
823
+ return false;
824
+ const seps = m[1].length;
825
+ const rest = m[2];
826
+ if (seps >= 4)
827
+ return true;
828
+ if (seps === 2) {
829
+ const host = /^[^\\/]*/.exec(rest)[0];
830
+ if (host !== '' && host.toLowerCase() !== 'localhost')
831
+ return true;
832
+ return DRIVE_START.test(rest.slice(host.length).replace(/^[\\/]+/, ''));
833
+ }
834
+ return DRIVE_START.test(rest);
835
+ }
836
+ // PowerShell's `-Stream` (and its shortest unambiguous forms `-St`, `-Str`, `-Stre`, `-Strea`), with a value after a space or a colon:
837
+ // it reaches an alternate data stream without any odd spelling in the path. `-S` alone is far too common (`git status -s`) to hold.
838
+ const STREAM_PARAM = /^-st(?:r(?:e(?:a(?:m)?)?)?)?(?::|$)/i;
839
+ function bareStream(t, git, operand) {
840
+ const colon = t.indexOf(':');
841
+ if (colon < 0)
842
+ return false;
843
+ if (STREAM_TYPED.test(t))
844
+ return true;
845
+ if (git || PS_DRIVE.test(t))
846
+ return false;
847
+ if (operand)
848
+ return colon > 0;
849
+ return t.length > colon + 1 && t[colon + 1] !== ':' && FILE_LIKE.test(t.slice(0, colon)) && !HOST_PORT.test(t) && !LINE_COL.test(t);
850
+ }
851
+ function oddPathWord(raw, path, git, fileProgram, operand = false) {
852
+ const t = raw.replace(/^[-/][a-z][\w-]*:/i, '');
853
+ if (t === '')
854
+ return false;
855
+ if (oddPathCore(t, path, git, fileProgram, operand))
856
+ return true;
857
+ for (const rest of behindPrefix(t))
858
+ if (oddPathCore(rest, path, git, fileProgram, operand))
859
+ return true;
860
+ return false;
861
+ }
862
+ // What a word may carry in front of a path without being a different kind of word: a PowerShell provider (`FileSystem::`,
863
+ // `Microsoft.PowerShell.Core\FileSystem::`), a response file mark (`@list`), one attached option letter (`-o\\srv\x`, `/o\\srv\x`), and
864
+ // the string prefix of interpreter code (`r'..'`, `b'..'`, `f'..'`; the quote itself is already gone from the word). The word
865
+ // after the prefix is read like a word of its own, once per prefix.
866
+ const PROVIDER_PREFIX = /^(?:[\w.-]+\\)?[a-z][\w.-]*::/i;
867
+ const STRING_PREFIX = /^(?:rb|br|fr|rf|[rbfu])(?=[\\/]{2})/i;
868
+ function behindPrefix(t) {
869
+ const out = [];
870
+ const provider = PROVIDER_PREFIX.exec(t);
871
+ if (provider)
872
+ out.push(t.slice(provider[0].length));
873
+ if (t[0] === '@')
874
+ out.push(t.slice(1));
875
+ if (/^[-/][a-z]./i.test(t))
876
+ out.push(t.slice(2));
877
+ const prefix = STRING_PREFIX.exec(t);
878
+ if (prefix)
879
+ out.push(t.slice(prefix[0].length));
880
+ return out.filter((rest) => rest !== '');
881
+ }
882
+ function oddPathCore(t, path, git, fileProgram, operand) {
883
+ if (fileUrlOdd(t))
884
+ return true;
885
+ if (URL_SCHEME.test(t) || SCP_LIKE.test(t))
886
+ return path;
887
+ const drive = /^[a-z]:/i.test(t);
888
+ const rooted = drive || /^[\\/]|^\.{1,2}[\\/]/.test(t);
889
+ const hasSep = /[\\/]/.test(t);
890
+ if (!path && !rooted && !hasSep)
891
+ return (fileProgram && !git && SHORT_PART.test(t)) || bareStream(t, git, operand);
892
+ // A path that starts like one and cannot be taken apart.
893
+ if ((path || rooted) && (t.length > tables_1.LIMITS.maxPathChars || CONTROL.test(t) || SEP_LOOKALIKE.test(t)))
894
+ return true;
895
+ if (ODD_DEVICE.test(t) || ODD_UNC.test(t) || ODD_LEADING.test(t))
896
+ return true;
897
+ // The first two separators of a path that starts at a drive, a root or a dot differ in kind: `C:/fx\a`, `C:\/x`, `./a\b`.
898
+ if (rooted) {
899
+ const first = t.search(/[\\/]/);
900
+ const second = first < 0 ? -1 : t.slice(first + 1).search(/[\\/]/);
901
+ if (second >= 0 && t[first] !== t[first + 1 + second])
902
+ return true;
903
+ }
904
+ // An alternate data stream: a stream type anywhere; a colon after the drive letter's own in a path that starts like one; or a
905
+ // colon in the last part of any other path when a dot comes before it (`a.txt:s`). A colon followed by a separator makes a pair
906
+ // of paths (`a:/b`, `HKLM:\x`), `host:80` is a port, and a git revision is `rev:path`.
907
+ const body = drive ? t.slice(2) : t;
908
+ if (body.includes(':') && STREAM_TYPE.test(body))
909
+ return true;
910
+ if (body.includes(':') && !/:[\\/]/.test(body) && !HOST_PORT.test(body) && !(git && !drive)) {
911
+ const last = body.split(/[\\/]/).pop() || '';
912
+ const colon = last.indexOf(':');
913
+ if (path || rooted ? true : colon > 0 && last.slice(0, colon).includes('.'))
914
+ return true;
915
+ }
916
+ // 8.3 short names: any part between separators. A git revision (`origin/main~1`) is no path.
917
+ if (t.includes('~') && !(git && !t.includes('\\') && !drive)) {
918
+ for (const part of t.split(/[\\/]/))
919
+ if (part.includes('~') && SHORT_PART.test(part))
920
+ return true;
921
+ }
922
+ return false;
923
+ }
924
+ /** Shell text (or interpreter code) with its words read for an unusual spelling; the separators of the text are kept as written. */
925
+ function oddPathText(raw, st) {
926
+ const s = raw.replace(/[\u2010-\u2015\u2212]/g, '-').replace(/[\u2018-\u201b\u2032]/g, "'").replace(/[\u201c-\u201f\u2033]/g, '"')
927
+ .replace(/[\u00a0\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]/g, ' ').replace(/[\u200b-\u200d\u2060\ufeff]/g, '').replace(/[`^]/g, '')
928
+ .replace(/(["'])\s*\+\s*(["'])/g, '')
929
+ // `${x}` and `${env:x}` are the variables `$x` and `$env:x` here: the braces would cut the word, and `${x}:s` is held like `$x:s`.
930
+ // Only a plain name: `${x:-a.txt}` (a default) stays as it was.
931
+ .replace(/\$\{((?:env:)?\w{1,128})\}/gi, '$$$1');
932
+ for (const seg of segments(s)) {
933
+ if (!tick(st))
934
+ return false;
935
+ let git = false;
936
+ let fileProgram = false;
937
+ let operands = false;
938
+ let first = false;
939
+ let pathCmd = false;
940
+ let pathNext = false;
941
+ let dataNext = false;
942
+ const ls = baseName(seg.words[0] || '') === 'ls';
943
+ for (let k = 0; k < seg.words.length; k += 1) {
944
+ const word = seg.words[k];
945
+ const redirect = seg.redirect.has(k) || seg.input.has(k);
946
+ // The word after `<<` or `<<<` is text (a here-string) or a delimiter, not a file, unless a redirect names it (`cat <<< > x`).
947
+ const here = seg.here.has(k) && !redirect;
948
+ if (STREAM_PARAM.test(word) && !(ls && k > 0 && LS_SORT.test(word)))
949
+ return true;
950
+ let operand = redirect || operands;
951
+ if (pathCmd && !redirect && !here) {
952
+ const param = paramOf(word.toLowerCase());
953
+ if (param && (param.path || param.data)) {
954
+ if (param.attached)
955
+ operand = operand || param.path;
956
+ else if (param.path)
957
+ pathNext = true;
958
+ else
959
+ dataNext = true;
960
+ }
961
+ else if (!isOption(word.toLowerCase())) {
962
+ if (dataNext)
963
+ dataNext = false;
964
+ else if (first || pathNext) {
965
+ operand = true;
966
+ first = false;
967
+ pathNext = false;
968
+ }
969
+ }
970
+ }
971
+ if (oddPathWord(word, false, git, fileProgram, operand && !here))
972
+ return true;
973
+ const name = baseName(word);
974
+ if (name === 'git')
975
+ git = true;
976
+ else if (FILE_PROGRAMS.has(name))
977
+ fileProgram = true;
978
+ if (PATH_OPERANDS.has(name))
979
+ operands = true;
980
+ else if (PATH_FIRST_OPERAND.has(name)) {
981
+ first = true;
982
+ pathCmd = true;
983
+ }
984
+ }
985
+ }
986
+ return false;
987
+ }
988
+ function addHit(st, category, reason) {
989
+ if (!st.hits.some((h) => h.category === category && h.reason === reason))
990
+ st.hits.push({ category, reason });
991
+ }
992
+ function tick(st) {
993
+ if (st.over)
994
+ return false;
995
+ st.work += 1;
996
+ if (Date.now() >= st.deadline) {
997
+ st.over = true;
998
+ addHit(st, 'Sx', 'budget_time');
999
+ return false;
1000
+ }
1001
+ if (st.work > tables_1.LIMITS.maxToolNodes) {
1002
+ st.over = true;
1003
+ addHit(st, 'Sx', 'budget_work');
1004
+ return false;
1005
+ }
1006
+ return true;
1007
+ }
1008
+ const INTERESTING = /[/~*?.\u0001]|^[a-z]:/;
1009
+ // The mapper's S0/S4 tripwire, on text the lexical reading cannot follow.
1010
+ const TRIPWIRE_REASON = {
1011
+ tripwire_secret_path: exports.WINDOWS_REASONS.secretPath, tripwire_env_file: exports.WINDOWS_REASONS.secretPath, tripwire_secret_dumper: exports.WINDOWS_REASONS.envDump,
1012
+ tripwire_self_protection: exports.WINDOWS_REASONS.protectedPath, tripwire_plugin_protection: exports.WINDOWS_REASONS.protectedPath,
1013
+ };
1014
+ /** The name a bare `*` stands for when its folder is known: one part that no protected or shim pattern lists. */
1015
+ const BARE_STAR_NAME = '\u0002';
1016
+ /**
1017
+ * The folder whose files a path names when its last part is exactly `*` or `*.*` (cmd's "every file", the same set) and every part
1018
+ * before it is a plain name (no wildcard, no variable, no 8.3 short name); null for any other path. The folder may be empty (`*`
1019
+ * alone: the working folder, not known).
1020
+ */
1021
+ function bareStarFolder(c) {
1022
+ const last = c[c.length - 1];
1023
+ if (last !== '*' && last !== '*.*')
1024
+ return null;
1025
+ const folder = view(c.slice(0, -1));
1026
+ return folder.hasFuzzy ? null : folder;
1027
+ }
1028
+ /** One shell text, or interpreter code (`code`: no program or reader is trusted). `depth` bounds decoded text. */
1029
+ function scanText(raw, st, depth, code) {
1030
+ if (depth > 4 || st.over || typeof raw !== 'string' || raw.trim() === '')
1031
+ return;
1032
+ const head = raw.length > tables_1.LIMITS.maxCommandBytes ? raw.slice(0, tables_1.LIMITS.maxCommandBytes) : raw;
1033
+ const s = normText(code ? head : stripGitMessages(head));
1034
+ verbHits(s, (c, r) => addHit(st, c, r));
1035
+ if (oddPathText(code ? head : stripGitMessages(head), st))
1036
+ addHit(st, 'Sx', exports.WINDOWS_REASONS.oddPath);
1037
+ if (code || DYNAMIC_TEXT.test(s)) {
1038
+ const flat = s.replace(/["']/g, '');
1039
+ for (const t of tables_1.TRIPWIRE) {
1040
+ if ((t.category === 'S0' || t.category === 'S4') && t.re.test(flat)) {
1041
+ addHit(st, t.category, TRIPWIRE_REASON[t.reason] || (t.category === 'S0' ? exports.WINDOWS_REASONS.protectedPath : exports.WINDOWS_REASONS.secretPath));
1042
+ }
1043
+ }
1044
+ }
1045
+ const dotnetWrite = DOTNET_WRITE.test(s);
1046
+ for (const seg of segments(s)) {
1047
+ if (st.over)
1048
+ return;
1049
+ const { prog, at, sub, launchers } = code ? { prog: '', at: -1, sub: null, launchers: [] } : programOf(seg);
1050
+ // cmd `set` with no value lists variables (`set`, `set AWS`); PowerShell `Get-Variable` lists them.
1051
+ if (prog === 'set' && !seg.hasEq && (seg.words.length === at + 1 || (seg.words.length === at + 2 && /^[a-z_][a-z0-9_]*$/.test(seg.words[at + 1]))))
1052
+ addHit(st, 'S4', exports.WINDOWS_REASONS.envDump);
1053
+ if ((prog === 'get-variable' || prog === 'gv') && (seg.words.length === at + 1 || seg.words.slice(at + 1).some((x) => /[*?]/.test(x))))
1054
+ addHit(st, 'S4', exports.WINDOWS_REASONS.envDump);
1055
+ const reader = prog === 'git' ? sub !== null && GIT_READ_SUBS.has(sub) : READERS.has(prog);
1056
+ const s4Exempt = prog === 'git' ? sub !== null && GIT_S4_EXEMPT_SUBS.has(sub) : S4_EXEMPT.has(prog);
1057
+ const writer = WRITERS.has(prog) || (dotnetWrite && !reader);
1058
+ let firstOperand = -1;
1059
+ for (let k = 0; k < seg.words.length; k += 1) {
1060
+ if (!tick(st))
1061
+ return;
1062
+ const w = seg.words[k];
1063
+ if (isOption(w))
1064
+ continue;
1065
+ const isProg = k === at;
1066
+ if (!isProg && k > at && firstOperand < 0)
1067
+ firstOperand = k;
1068
+ const p = dynWord(w.replace(/^-[a-z][\w-]*:(?=.)/, ''));
1069
+ if (!INTERESTING.test(p) || !/[a-z0-9._~]/.test(p.split(DYN).join('')))
1070
+ continue;
1071
+ const c = comps(p);
1072
+ if (!c || !c.length)
1073
+ continue;
1074
+ const v = view(c);
1075
+ const redirect = seg.redirect.has(k);
1076
+ if (isSecret(v, st.parts) && !(s4Exempt && !redirect))
1077
+ addHit(st, 'S4', exports.WINDOWS_REASONS.secretPath);
1078
+ if (isProg)
1079
+ continue;
1080
+ // A last part of exactly `*` (or `*.*`) under a folder that is known (no wildcard, variable or 8.3 name in it) names the files IN that
1081
+ // folder, as POSIX expands it. It is read as one plain name that no protected pattern lists: a protected folder still holds
1082
+ // it, and the folder itself is checked as an anchor (a protected folder, the profile, a system tree, or a folder above one).
1083
+ // Any other wildcard keeps the reading that it may stand for any name.
1084
+ const folder = bareStarFolder(c);
1085
+ const kind = protectedKind(folder ? view([...folder.comps, BARE_STAR_NAME]) : v, st.parts);
1086
+ const script = INTERPRETERS.has(prog) && k === firstOperand;
1087
+ const started = START_LIKE.has(prog) && k === firstOperand;
1088
+ const readOnly = reader && !redirect;
1089
+ if (kind === 'op' && !readOnly && !script && !started)
1090
+ addHit(st, 'S0', exports.WINDOWS_REASONS.protectedPath);
1091
+ // A word that only starts the command (`cmd.exe` of `cmd.exe /c copy a b`) is no write target, no folder the command
1092
+ // removes and no file it creates: the write-only, ancestor and shim checks skip it. The secret check above and the
1093
+ // protected-place check just above still ran on it, and every word after it is read in full.
1094
+ if (launchers.includes(k))
1095
+ continue;
1096
+ if (kind === 'wo' && (redirect || writer))
1097
+ addHit(st, 'S0', exports.WINDOWS_REASONS.protectedPath);
1098
+ // The folder of a bare `*` is checked, not the wildcard word: it must be an anchor itself (the folder of a relative `*` alone,
1099
+ // `del *`, is not known).
1100
+ const above = folder ? isAnchorFolder(folder) : ancestorOfAnchor(v, 3);
1101
+ const aboveWrite = folder ? above : ancestorOfAnchor(v, 4);
1102
+ if (!readOnly && (DESTRUCTIVE.has(prog) || (dotnetWrite && prog === '')) && above)
1103
+ addHit(st, 'S0', exports.WINDOWS_REASONS.protectedPath);
1104
+ else if (!readOnly && (redirect || writer) && prog !== 'md' && prog !== 'mkdir' && aboveWrite)
1105
+ addHit(st, 'S0', exports.WINDOWS_REASONS.protectedPath);
1106
+ const literalShim = !v.fz[v.fz.length - 1];
1107
+ if (!folder && isShim(v) && !readOnly && !script && !started && (literalShim || redirect || writer))
1108
+ addHit(st, 'S0', exports.WINDOWS_REASONS.shim);
1109
+ }
1110
+ }
1111
+ for (const text of encodedTexts(head))
1112
+ scanText(text, st, depth + 1, false);
1113
+ }
1114
+ function toolPath(raw, st, mode, text = false) {
1115
+ if (!tick(st) || typeof raw !== 'string' || !raw)
1116
+ return;
1117
+ // An unusual spelling is held first and whole; the readings below only add to it. A value that is not a path field (a string
1118
+ // of an MCP call) is read word by word.
1119
+ if (text ? oddPathText(raw, st) : oddPathWord(raw, true, false, false))
1120
+ addHit(st, 'Sx', exports.WINDOWS_REASONS.oddPath);
1121
+ const c = comps(multiword(expandFolders(raw.toLowerCase().replace(/\\/g, '/'))));
1122
+ if (!c || !c.length)
1123
+ return;
1124
+ const v = view(c);
1125
+ // A write of a secret that is also a protected file (an 8.3 name may stand for either) gets both hits.
1126
+ if (isSecret(v, st.parts))
1127
+ addHit(st, 'S4', exports.WINDOWS_REASONS.secretPath);
1128
+ if (mode === 'read')
1129
+ return;
1130
+ // Phase A: no JSON-compare relaxation for hook configs on Windows; any write to a protected path stops.
1131
+ if (protectedKind(v, st.parts) !== null) {
1132
+ addHit(st, 'S0', exports.WINDOWS_REASONS.protectedPath);
1133
+ return;
1134
+ }
1135
+ if (mode === 'delete' && ancestorOfAnchor(v, 1)) {
1136
+ addHit(st, 'S0', exports.WINDOWS_REASONS.protectedPath);
1137
+ return;
1138
+ }
1139
+ if (isShim(v))
1140
+ addHit(st, 'S0', exports.WINDOWS_REASONS.shim);
1141
+ }
1142
+ const MCP_WRITE_VERBS = /write|edit|create|move|delete|remove|rename|put|update|append|save|mkdir|copy|patch|upload/;
1143
+ // As mapper.ts mcpPathChecks: every string value of an MCP call (or the path fields of a tool the mapper
1144
+ // does not recognise); a secret is S4, and a protected path is S0 when the tool's name says it writes.
1145
+ function mcpPaths(label, input, st, allStrings) {
1146
+ const writes = MCP_WRITE_VERBS.test(label);
1147
+ const deletes = /delete|remove|move|rename/.test(label);
1148
+ const walked = st.parts.walkPathInputs(input, st.deadline, allStrings);
1149
+ const mode = deletes ? 'delete' : writes ? 'write' : 'read';
1150
+ for (const p of walked.paths)
1151
+ toolPath(p, st, mode);
1152
+ for (const p of walked.strings.filter((t) => t.length <= tables_1.LIMITS.maxPathChars && !/[\r\n]/.test(t)))
1153
+ toolPath(p, st, mode, !walked.paths.includes(p));
1154
+ }
1155
+ function tryJson(text) {
1156
+ try {
1157
+ return JSON.parse(text);
1158
+ }
1159
+ catch {
1160
+ return null;
1161
+ }
1162
+ }
1163
+ const URL_FIELD = /path|file|dir|src|dest|uri|target|source|^from$|^to$|include/i;
1164
+ /**
1165
+ * The mapper takes `file://` off a path field before it is read here, which hides the drive or share behind it (`file:///C:/x` reads
1166
+ * as `/C:/x`, `file://srv/share/x` as `srv/share/x`). The field as written is read once more for a `file:` URL with a drive or a share.
1167
+ */
1168
+ function fileUrlFields(input, st) {
1169
+ const check = (v) => {
1170
+ if (typeof v === 'string') {
1171
+ if (fileUrlOdd(v.trim()))
1172
+ addHit(st, 'Sx', exports.WINDOWS_REASONS.oddPath);
1173
+ }
1174
+ else if (Array.isArray(v))
1175
+ v.slice(0, 64).forEach(check);
1176
+ };
1177
+ for (const [key, value] of Object.entries(input))
1178
+ if (URL_FIELD.test(key))
1179
+ check(value);
1180
+ if (Array.isArray(input.edits))
1181
+ for (const e of input.edits.slice(0, 64))
1182
+ if (e && typeof e === 'object')
1183
+ check(e.file_path);
1184
+ }
1185
+ /** The extra hits for one event on win32 (the same tool reading as mapper.ts evaluateEvent). */
1186
+ function windowsProtectedHits(event, env, parts, startedAt = Date.now()) {
1187
+ const st = {
1188
+ hits: [], work: 0, parts, over: false,
1189
+ deadline: startedAt + (typeof env.budgetMs === 'number' && env.budgetMs >= 0 ? env.budgetMs : tables_1.LIMITS.timeBudgetMs),
1190
+ };
1191
+ const tool = String((event && event.tool_name) || '');
1192
+ const name = tool.toLowerCase().replace(/^(?:functions\.\s*)+/, '');
1193
+ let rawInput = event && event.tool_input;
1194
+ let rawText = null;
1195
+ if (typeof rawInput === 'string') {
1196
+ // Over the size cap the mapper stops (budget_tool_input); the head is still read here.
1197
+ const body = rawInput.slice(0, tables_1.LIMITS.maxToolJsonChars).replace(/^\uFEFF/, '');
1198
+ const parsed = /^\s*[{[]/.test(body) ? tryJson(body) : null;
1199
+ if (parsed && typeof parsed === 'object')
1200
+ rawInput = parsed;
1201
+ else {
1202
+ rawText = body;
1203
+ const kv = /^\s*(?:command|cmd|script|shell_command|commandline)\s*[=:]\s*([\s\S]+)$/i.exec(body);
1204
+ if (kv)
1205
+ rawText = `${body}\n${kv[1]}`;
1206
+ rawInput = {};
1207
+ }
1208
+ }
1209
+ if (Array.isArray(rawInput))
1210
+ rawInput = { commands: rawInput.filter((v) => typeof v === 'string'), items: rawInput.filter((v) => v && typeof v === 'object') };
1211
+ const input = (rawInput && typeof rawInput === 'object' ? rawInput : {});
1212
+ const nativeShell = parts.tools.nativeShell.get(tool);
1213
+ if (nativeShell !== undefined)
1214
+ rawText = rawText === null ? nativeShell : `${rawText}\n${nativeShell}`;
1215
+ const mcp = tables_1.SHELL_TOOL_NAMES.has(name) ? null : parts.mcpCall(name, input);
1216
+ // A call whose arguments cannot be read: the mapper does not recognise it, and the hook gives master's answer.
1217
+ if (mcp && mcp.malformed)
1218
+ return st.hits;
1219
+ const evName = mcp ? mcp.name : name;
1220
+ const evInput = mcp ? mcp.args : input;
1221
+ const shellInput = !mcp && tool === 'Workflow' && typeof evInput.script === 'string'
1222
+ ? Object.fromEntries(Object.entries(evInput).filter(([key]) => key !== 'script')) : evInput;
1223
+ const invocation = parts.shellInvocations(evName, shellInput, st.deadline, rawText);
1224
+ if (invocation) {
1225
+ for (const text of invocation.shell) {
1226
+ if (st.over)
1227
+ break;
1228
+ scanText(text, st, 0, false);
1229
+ }
1230
+ for (const code of invocation.code) {
1231
+ if (st.over)
1232
+ break;
1233
+ scanText(code, st, 0, true);
1234
+ }
1235
+ if (mcp)
1236
+ mcpPaths(mcp.label, evInput, st, true);
1237
+ return st.hits;
1238
+ }
1239
+ const t = parts.tools;
1240
+ fileUrlFields(evInput, st);
1241
+ if (mcp)
1242
+ mcpPaths(mcp.label, evInput, st, true);
1243
+ else if (t.read.has(name) || t.grep.has(name)) {
1244
+ for (const p of parts.pathInputs(input))
1245
+ toolPath(p, st, 'read');
1246
+ }
1247
+ else if (t.write.has(name)) {
1248
+ const targets = name === 'apply_patch' ? parts.patchPaths(String(input.patch || input.input || input.content || '')) : parts.pathInputs(input);
1249
+ const viewOnly = (name === 'str_replace_editor' || name === 'str_replace_based_edit_tool') && String(input.command || '') === 'view';
1250
+ for (const p of targets)
1251
+ toolPath(p, st, viewOnly ? 'read' : 'write');
1252
+ }
1253
+ else if (t.delete.has(name)) {
1254
+ for (const p of parts.pathInputs(input))
1255
+ toolPath(p, st, 'delete');
1256
+ }
1257
+ else if (!t.list.has(name) && !t.inert.has(name) && !t.nonShell.has(name)) {
1258
+ mcpPaths(name, input, st, false);
1259
+ }
1260
+ return st.hits;
1261
+ }
1262
+ const ORDER = ['S0', 'S1', 'S2', 'S3', 'S4', 'S5', 'Sx'];
1263
+ /** The mapper's decision with the extra hits added. Hits are only added; a stop is never removed. */
1264
+ function mergeWindowsHits(decision, extra) {
1265
+ const fresh = extra.filter((h) => !decision.hits.some((d) => d.category === h.category && d.reason === h.reason));
1266
+ if (!fresh.length)
1267
+ return decision;
1268
+ const hits = [...decision.hits, ...fresh].sort((a, b) => ORDER.indexOf(a.category) - ORDER.indexOf(b.category));
1269
+ const category = hits[0].category;
1270
+ const rule = decision.decision === 'stop' && decision.category === category ? decision.rule : `${category}.${hits[0].reason}`;
1271
+ return { ...decision, decision: 'stop', category, rule, reasons: hits.map((h) => h.reason), hits, outageHelpOnly: false };
1272
+ }
1273
+ /** The one entry point (mapper.ts evaluateSeatbelt): win32 only; any other platform gets the decision unchanged. */
1274
+ function withWindowsProtected(decision, event, env, parts, startedAt) {
1275
+ if (env.platform !== 'win32')
1276
+ return decision;
1277
+ return mergeWindowsHits(decision, windowsProtectedHits(event, env, parts, startedAt));
1278
+ }
1279
+ //# sourceMappingURL=win-protected.js.map