sanook-cli 0.5.0 → 0.5.2

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 (146) hide show
  1. package/.env.example +161 -3
  2. package/CHANGELOG.md +83 -5
  3. package/README.md +240 -23
  4. package/README.th.md +87 -6
  5. package/dist/approval.js +6 -0
  6. package/dist/bin.js +3045 -210
  7. package/dist/brain-context.js +223 -0
  8. package/dist/brain-doctor.js +318 -0
  9. package/dist/brain-eval.js +186 -0
  10. package/dist/brain-final.js +371 -0
  11. package/dist/brain-review.js +382 -0
  12. package/dist/brain.js +12 -1
  13. package/dist/brand.js +1 -1
  14. package/dist/cli-args.js +152 -0
  15. package/dist/cli-option-values.js +16 -0
  16. package/dist/commands.js +172 -13
  17. package/dist/compaction.js +96 -11
  18. package/dist/config.js +118 -28
  19. package/dist/context-compression.js +191 -0
  20. package/dist/cost.js +49 -15
  21. package/dist/first-run.js +21 -0
  22. package/dist/gateway/auth.js +37 -8
  23. package/dist/gateway/bluebubbles.js +205 -0
  24. package/dist/gateway/config.js +929 -0
  25. package/dist/gateway/deliver.js +357 -0
  26. package/dist/gateway/discord.js +124 -0
  27. package/dist/gateway/email.js +472 -0
  28. package/dist/gateway/googlechat.js +207 -0
  29. package/dist/gateway/homeassistant.js +256 -0
  30. package/dist/gateway/ledger.js +18 -0
  31. package/dist/gateway/line.js +171 -0
  32. package/dist/gateway/lock.js +3 -1
  33. package/dist/gateway/matrix.js +366 -0
  34. package/dist/gateway/mattermost.js +322 -0
  35. package/dist/gateway/ntfy.js +218 -0
  36. package/dist/gateway/schedule.js +31 -4
  37. package/dist/gateway/serve.js +267 -7
  38. package/dist/gateway/server.js +253 -19
  39. package/dist/gateway/service.js +224 -0
  40. package/dist/gateway/session.js +343 -0
  41. package/dist/gateway/signal.js +351 -0
  42. package/dist/gateway/slack.js +124 -0
  43. package/dist/gateway/sms.js +169 -0
  44. package/dist/gateway/targets.js +576 -0
  45. package/dist/gateway/teams.js +106 -0
  46. package/dist/gateway/telegram.js +38 -15
  47. package/dist/gateway/webhooks.js +220 -0
  48. package/dist/gateway/whatsapp.js +230 -0
  49. package/dist/hooks.js +13 -2
  50. package/dist/insights-args.js +35 -0
  51. package/dist/insights.js +86 -0
  52. package/dist/loop.js +123 -24
  53. package/dist/lsp/index.js +23 -5
  54. package/dist/mcp-registry.js +350 -0
  55. package/dist/mcp-server.js +1 -1
  56. package/dist/mcp.js +44 -6
  57. package/dist/memory.js +100 -33
  58. package/dist/orchestrate.js +49 -19
  59. package/dist/personality.js +58 -0
  60. package/dist/providers/codex.js +86 -38
  61. package/dist/providers/keys.js +1 -1
  62. package/dist/providers/models.js +22 -6
  63. package/dist/providers/registry.js +38 -49
  64. package/dist/search/chunk.js +7 -8
  65. package/dist/search/cli.js +75 -0
  66. package/dist/search/embed-store.js +3 -0
  67. package/dist/search/indexer.js +44 -1
  68. package/dist/search/store.js +23 -1
  69. package/dist/session.js +93 -7
  70. package/dist/skill-install.js +29 -12
  71. package/dist/support-dump.js +175 -0
  72. package/dist/tools/edit.js +45 -15
  73. package/dist/tools/git.js +10 -5
  74. package/dist/tools/homeassistant.js +106 -0
  75. package/dist/tools/index.js +5 -0
  76. package/dist/tools/list.js +19 -6
  77. package/dist/tools/permission.js +923 -9
  78. package/dist/tools/read.js +16 -4
  79. package/dist/tools/schedule.js +19 -3
  80. package/dist/tools/search.js +217 -13
  81. package/dist/tools/task.js +18 -7
  82. package/dist/tools/timeout.js +21 -3
  83. package/dist/trust.js +11 -1
  84. package/dist/ui/app.js +57 -11
  85. package/dist/ui/brain-wizard.js +2 -2
  86. package/dist/ui/history.js +37 -5
  87. package/dist/ui/mentions.js +3 -2
  88. package/dist/ui/render.js +55 -15
  89. package/dist/ui/setup.js +107 -10
  90. package/dist/update.js +24 -11
  91. package/dist/worktree.js +175 -4
  92. package/package.json +4 -4
  93. package/second-brain/AGENTS.md +6 -4
  94. package/second-brain/CLAUDE.md +7 -1
  95. package/second-brain/Evals/_Index.md +10 -2
  96. package/second-brain/Evals/quality-ledger.md +9 -1
  97. package/second-brain/Evals/second-brain-benchmarks.md +62 -0
  98. package/second-brain/GEMINI.md +5 -4
  99. package/second-brain/Home.md +1 -1
  100. package/second-brain/Projects/_Index.md +3 -1
  101. package/second-brain/Projects/sanook-cli/_Index.md +26 -0
  102. package/second-brain/Projects/sanook-cli/second-brain-feature-roadmap.md +156 -0
  103. package/second-brain/README.md +1 -1
  104. package/second-brain/Research/2026-06-17-ai-second-brain-method-experiment.md +108 -0
  105. package/second-brain/Research/2026-06-18-ai-token-reduction-frameworks.md +55 -0
  106. package/second-brain/Research/2026-06-18-hermes-cli-second-brain-expansion-research.md +160 -0
  107. package/second-brain/Research/2026-06-18-sanook-mcp-ecosystem-and-ux-roadmap.md +181 -0
  108. package/second-brain/Research/_Index.md +6 -1
  109. package/second-brain/Reviews/2026-06-18-auto-improve-maintenance.md +54 -0
  110. package/second-brain/Reviews/_Index.md +1 -1
  111. package/second-brain/Runbooks/_Index.md +6 -1
  112. package/second-brain/Runbooks/ai-second-brain-operating-sequence.md +108 -0
  113. package/second-brain/SANOOK.md +45 -0
  114. package/second-brain/Sessions/2026-06-17-ai-framework-additional-zones.md +68 -0
  115. package/second-brain/Sessions/2026-06-17-ai-second-brain-sequence-experiment.md +63 -0
  116. package/second-brain/Sessions/2026-06-18-cli-args-release-readiness.md +59 -0
  117. package/second-brain/Sessions/2026-06-18-final-gate-template-final.md +192 -0
  118. package/second-brain/Sessions/2026-06-18-final-gate-template.md +71 -0
  119. package/second-brain/Sessions/2026-06-18-framework-dogfood-permission-and-memory.md +58 -0
  120. package/second-brain/Sessions/2026-06-18-hermes-second-brain-expansion-research.md +52 -0
  121. package/second-brain/Sessions/2026-06-18-mcp-ecosystem-and-sanook-ux-scan.md +81 -0
  122. package/second-brain/Sessions/2026-06-18-sanook-brain-cli-p0-implementation.md +86 -0
  123. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli-final.md +246 -0
  124. package/second-brain/Sessions/2026-06-18-sanook-brain-final-cli.md +78 -0
  125. package/second-brain/Sessions/2026-06-18-sanook-cli-second-brain-roadmap-correction.md +54 -0
  126. package/second-brain/Sessions/2026-06-18-token-reduction-framework-integration.md +69 -0
  127. package/second-brain/Sessions/_Index.md +15 -1
  128. package/second-brain/Shared/AI-Context-Index.md +22 -0
  129. package/second-brain/Shared/Context-Packs/_Index.md +9 -1
  130. package/second-brain/Shared/Context-Packs/coding-release.md +51 -0
  131. package/second-brain/Shared/Context-Packs/research-to-framework.md +51 -0
  132. package/second-brain/Shared/Context-Packs/second-brain-maintenance.md +41 -0
  133. package/second-brain/Shared/Operating-State/current-state.md +22 -3
  134. package/second-brain/Shared/Scripts/_Index.md +3 -1
  135. package/second-brain/Shared/Scripts/ai-second-brain-method-eval.mjs +198 -0
  136. package/second-brain/Shared/Tech-Standards/_Index.md +4 -1
  137. package/second-brain/Shared/Tech-Standards/mcp-integration-roadmap.md +86 -0
  138. package/second-brain/Shared/Tech-Standards/verification-standard.md +24 -0
  139. package/second-brain/Shared/User-Memory/_Index.md +4 -1
  140. package/second-brain/Shared/User-Memory/response-examples.md +98 -0
  141. package/second-brain/Shared/User-Memory/user-preferences.md +1 -0
  142. package/second-brain/Templates/_Index.md +9 -0
  143. package/second-brain/Templates/final-lite.md +111 -0
  144. package/second-brain/Templates/final.md +231 -0
  145. package/second-brain/Vault Structure Map.md +2 -1
  146. package/skills/structured-output-llm/SKILL.md +1 -1
@@ -1,21 +1,103 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { realpath, stat } from 'node:fs/promises';
3
- import { basename, dirname, resolve, join, sep } from 'node:path';
3
+ import { dirname, resolve, join, sep } from 'node:path';
4
4
  import { getBrainPath } from '../memory.js';
5
5
  import { BRAND_ENV, envFlag } from '../brand.js';
6
6
  import { agentCwd } from '../agentContext.js';
7
7
  // Permission gate (M1): ก่อนมี interactive ask (M4) — hard-deny อันตราย, allow ที่เหลือ
8
8
  // คำสั่ง shell ที่ทำลายล้าง irreversible
9
9
  const DESTRUCTIVE_CMD = /(\bgit\s+reset\s+--hard\b|\bgit\s+push\b.*--force|\bmkfs\b|\bdd\s+if=|:\(\)\s*\{|\bchmod\s+-R\s+777\b|>\s*\/dev\/sd|\bsudo\b|\bcrontab\b)/i;
10
- const PROTECTED_CMD_PATH = /(\$HOME|~)?\/?(\.ssh|\.aws|\.gnupg|\.sanook)(\/|\b)|(^|\s)(cat|less|more|sed|awk|tail|head)\s+[^|;&]*\.env(\.|\b)/i;
10
+ const PROTECTED_CMD_PATH = /(\$HOME|~)?\/?(\.ssh|\.aws|\.gnupg|\.sanook)(\/|\b)/i;
11
+ const ENV_READ_CMD = /(?:^|[\r\n;&|]\s*|\$\(\s*|[<>]\(\s*|`\s*)(?:(?:if|then|elif|while|until|do|time|command|builtin|exec)\s+)*(?:env\s+(?:-\S+\s+)*(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*)?(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(cat|less|more|sed|awk|tail|head|grep|rg)\b/gi;
12
+ const ENV_SOURCE_CMD = /(?:^|[\r\n;&|]\s*|\$\(\s*|[<>]\(\s*|`\s*)(?:(?:if|then|elif|while|until|do|time|command|builtin|exec)\s+)*(source\b|\.)/gi;
13
+ const ENV_WRITE_CMD = /(?:^|[\r\n;&|]\s*|\$\(\s*|[<>]\(\s*|`\s*)(?:(?:if|then|elif|while|until|do|time|command|builtin|exec)\s+)*(?:env\s+(?:-\S+\s+)*(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*)?(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(tee)\b/gi;
14
+ const NESTED_SHELL_CMD = /(?:^|[\r\n;&|]\s*|\$\(\s*|[<>]\(\s*|`\s*)(?:(?:if|then|elif|while|until|do|time|command|builtin|exec)\s+)*(?:env\s+(?:-\S+\s+)*(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*)?(?:[A-Za-z_][A-Za-z0-9_]*=\S+\s+)*(sh|bash|zsh|dash|ksh|fish|csh|tcsh)\b/gi;
15
+ const ENV_CMD = /(?:^|[\r\n;&|]\s*|\$\(\s*|[<>]\(\s*|`\s*)(?:(?:if|then|elif|while|until|do|time|command|builtin|exec)\s+)*(env)\b/gi;
11
16
  const HOME = homedir();
12
17
  // ไฟล์ที่ห้ามเขียน (persistence backdoor): shell rc, git/npm config, ~/.sanook (token/mcp/hooks)
13
18
  const PROTECTED_EXACT = new Set(['.gitconfig', '.zshrc', '.bashrc', '.bash_profile', '.profile', '.zprofile', '.npmrc'].map((f) => join(HOME, f)));
14
19
  // โฟลเดอร์ที่ห้ามเขียนเข้าไป (credentials + sanook internal)
15
20
  const PROTECTED_DIRS = ['.ssh', '.aws', '.gnupg', '.sanook'].map((d) => join(HOME, d));
16
21
  const PROTECTED_SEGMENTS = new Set(['.git', 'node_modules', '.ssh', '.aws', '.gnupg', '.sanook']);
22
+ const ENV_OPTIONS_WITH_VALUE = new Set(['-C', '--chdir', '-S', '--split-string', '-u', '--unset']);
23
+ const GIT_OPTIONS_WITH_VALUE = new Set([
24
+ '-C',
25
+ '-c',
26
+ '--config-env',
27
+ '--exec-path',
28
+ '--git-dir',
29
+ '--namespace',
30
+ '--super-prefix',
31
+ '--work-tree',
32
+ ]);
33
+ const SHELL_OPTIONS_WITH_VALUE = new Set(['--init-file', '--rcfile']);
34
+ const SEARCH_COMMANDS_WITH_LITERAL_PATTERN = new Set(['grep', 'rg']);
35
+ const SEARCH_SHORT_OPTIONS_WITHOUT_VALUE = new Map([
36
+ ['grep', new Set(['E', 'F', 'G', 'P', 'R', 'r', 'I', 'i', 'v', 'w', 'x', 'c', 'l', 'L', 'n', 'H', 'h', 'o', 'q', 's', 'a', 'b', 'U', 'Z', 'z'])],
37
+ ['rg', new Set(['F', 'H', 'I', 'L', 'N', 'P', 'S', 'T', 'U', 'V', 'a', 'b', 'c', 'h', 'i', 'l', 'n', 'p', 'q', 's', 'u', 'v', 'w', 'x', 'z'])],
38
+ ]);
39
+ const SEARCH_OPTIONS_WITH_VALUE = new Map([
40
+ [
41
+ 'grep',
42
+ new Set([
43
+ '-A',
44
+ '-B',
45
+ '-C',
46
+ '-D',
47
+ '-d',
48
+ '-m',
49
+ '--after-context',
50
+ '--before-context',
51
+ '--binary-files',
52
+ '--context',
53
+ '--devices',
54
+ '--directories',
55
+ '--exclude',
56
+ '--exclude-dir',
57
+ '--include',
58
+ '--label',
59
+ '--max-count',
60
+ ]),
61
+ ],
62
+ [
63
+ 'rg',
64
+ new Set([
65
+ '-A',
66
+ '-B',
67
+ '-C',
68
+ '-E',
69
+ '-M',
70
+ '-g',
71
+ '-m',
72
+ '-t',
73
+ '-T',
74
+ '--after-context',
75
+ '--before-context',
76
+ '--context',
77
+ '--encoding',
78
+ '--engine',
79
+ '--glob',
80
+ '--iglob',
81
+ '--max-columns',
82
+ '--max-count',
83
+ '--max-depth',
84
+ '--path-separator',
85
+ '--pre',
86
+ '--pre-glob',
87
+ '--regex-size-limit',
88
+ '--sort',
89
+ '--sortr',
90
+ '--type',
91
+ '--type-not',
92
+ ]),
93
+ ],
94
+ ]);
95
+ const SHELL_COMMAND_PREFIXES = new Set(['if', 'then', 'elif', 'while', 'until', 'do', 'time', 'command', 'builtin', 'exec']);
17
96
  function hasRmRecursiveForce(cmd) {
97
+ const literalPatternRanges = literalSearchPatternRanges(cmd);
18
98
  for (const match of cmd.matchAll(/\brm\b([^;&|]*)/gi)) {
99
+ if (match.index !== undefined && inRanges(match.index, literalPatternRanges))
100
+ continue;
19
101
  const parts = match[1].split(/\s+/).filter(Boolean);
20
102
  const shortFlags = parts.filter((part) => /^-[^-]/.test(part)).join('');
21
103
  const recursive = /r/i.test(shortFlags) || parts.includes('--recursive') || parts.includes('--dir');
@@ -25,13 +107,840 @@ function hasRmRecursiveForce(cmd) {
25
107
  }
26
108
  return false;
27
109
  }
28
- export function checkBash(cmd) {
29
- if (hasRmRecursiveForce(cmd) || DESTRUCTIVE_CMD.test(cmd)) {
110
+ function hasDestructiveCommand(cmd) {
111
+ const literalPatternRanges = literalSearchPatternRanges(cmd);
112
+ const destructive = new RegExp(DESTRUCTIVE_CMD.source, 'gi');
113
+ for (const match of cmd.matchAll(destructive)) {
114
+ if (match.index !== undefined && inRanges(match.index, literalPatternRanges))
115
+ continue;
116
+ return true;
117
+ }
118
+ return false;
119
+ }
120
+ function inRanges(index, ranges) {
121
+ return ranges.some(({ start, end }) => index >= start && index < end);
122
+ }
123
+ function isLiteralSingleQuotedToken(raw) {
124
+ const token = raw.trim();
125
+ return /^'(?:[^']*)'$/.test(token) || /^\$'(?:\\.|[^'])*'$/.test(token);
126
+ }
127
+ function literalSearchPatternRanges(cmd) {
128
+ const ranges = [];
129
+ const bySegment = new Map();
130
+ for (const entry of shellishTokenEntries(cmd)) {
131
+ const segment = bySegment.get(entry.segment);
132
+ if (segment)
133
+ segment.push(entry);
134
+ else
135
+ bySegment.set(entry.segment, [entry]);
136
+ }
137
+ for (const entries of bySegment.values()) {
138
+ const commandIndex = shellSegmentCommandIndex(entries);
139
+ if (commandIndex < 0)
140
+ continue;
141
+ const command = cleanShellToken(entries[commandIndex].raw).toLowerCase();
142
+ if (!SEARCH_COMMANDS_WITH_LITERAL_PATTERN.has(command))
143
+ continue;
144
+ let patternSourceSeen = false;
145
+ let optionsDone = false;
146
+ for (let j = commandIndex + 1; j < entries.length; j += 1) {
147
+ const clean = cleanShellToken(entries[j].raw);
148
+ if (!optionsDone) {
149
+ if (clean === '--') {
150
+ optionsDone = true;
151
+ continue;
152
+ }
153
+ if (searchPatternOptionConsumesNext(clean)) {
154
+ const value = entries[j + 1];
155
+ if (value && isLiteralSingleQuotedToken(value.raw))
156
+ ranges.push({ start: value.start, end: value.end });
157
+ patternSourceSeen = true;
158
+ j += 1;
159
+ continue;
160
+ }
161
+ const attachedPattern = attachedLiteralSearchPatternOption(command, entries[j].raw);
162
+ if (attachedPattern !== undefined) {
163
+ if (attachedPattern)
164
+ ranges.push({ start: entries[j].start, end: entries[j].end });
165
+ patternSourceSeen = true;
166
+ continue;
167
+ }
168
+ if (searchPatternFileOptionConsumesNext(clean)) {
169
+ patternSourceSeen = true;
170
+ j += 1;
171
+ continue;
172
+ }
173
+ if (attachedSearchPatternFileOption(clean)) {
174
+ patternSourceSeen = true;
175
+ continue;
176
+ }
177
+ if (searchOptionConsumesNext(command, clean)) {
178
+ j += 1;
179
+ continue;
180
+ }
181
+ if (clean.startsWith('-'))
182
+ continue;
183
+ }
184
+ if (patternSourceSeen)
185
+ break;
186
+ if (isLiteralSingleQuotedToken(entries[j].raw))
187
+ ranges.push({ start: entries[j].start, end: entries[j].end });
188
+ break;
189
+ }
190
+ }
191
+ return ranges;
192
+ }
193
+ function searchPatternOptionConsumesNext(clean) {
194
+ return clean === '-e' || clean === '--regexp';
195
+ }
196
+ function searchPatternFileOptionConsumesNext(clean) {
197
+ return clean === '-f' || clean === '--file';
198
+ }
199
+ function attachedLiteralSearchPatternOption(command, raw) {
200
+ const token = raw.trim();
201
+ if (token.startsWith('-e') && token.length > 2)
202
+ return isLiteralSingleQuotedToken(token.slice(2));
203
+ const longRegexp = token.match(/^--regexp=(.*)$/);
204
+ if (longRegexp)
205
+ return isLiteralSingleQuotedToken(longRegexp[1]);
206
+ return attachedShortLiteralSearchPatternOption(command, token);
207
+ }
208
+ function attachedShortLiteralSearchPatternOption(command, token) {
209
+ if (!token.startsWith('-') || token.startsWith('--'))
210
+ return undefined;
211
+ const patternOptionIndex = token.indexOf('e', 1);
212
+ if (patternOptionIndex <= 1 || patternOptionIndex === token.length - 1)
213
+ return undefined;
214
+ const prefix = token.slice(1, patternOptionIndex);
215
+ const optionsWithoutValue = SEARCH_SHORT_OPTIONS_WITHOUT_VALUE.get(command);
216
+ if (!optionsWithoutValue || [...prefix].some((option) => !optionsWithoutValue.has(option)))
217
+ return undefined;
218
+ return isLiteralSingleQuotedToken(token.slice(patternOptionIndex + 1));
219
+ }
220
+ function attachedSearchPatternFileOption(clean) {
221
+ return /^-f.+/.test(clean) || clean.startsWith('--file=');
222
+ }
223
+ function searchOptionConsumesNext(command, clean) {
224
+ return SEARCH_OPTIONS_WITH_VALUE.get(command)?.has(clean) ?? false;
225
+ }
226
+ function shellSegmentCommandIndex(entries) {
227
+ let envMode = false;
228
+ let envOptionsDone = false;
229
+ for (let i = 0; i < entries.length; i += 1) {
230
+ const clean = cleanShellToken(entries[i].raw);
231
+ if (!clean)
232
+ continue;
233
+ if (!envMode && shellEnvAssignment(clean))
234
+ continue;
235
+ if (!envMode && SHELL_COMMAND_PREFIXES.has(clean))
236
+ continue;
237
+ if (!envMode && clean === 'env') {
238
+ envMode = true;
239
+ envOptionsDone = false;
240
+ continue;
241
+ }
242
+ if (envMode) {
243
+ if (!envOptionsDone && clean === '--') {
244
+ envOptionsDone = true;
245
+ continue;
246
+ }
247
+ if (!envOptionsDone && envOptionConsumesNext(clean)) {
248
+ i += 1;
249
+ continue;
250
+ }
251
+ if (!envOptionsDone && clean.startsWith('-'))
252
+ continue;
253
+ if (shellEnvAssignment(clean))
254
+ continue;
255
+ return i;
256
+ }
257
+ return i;
258
+ }
259
+ return -1;
260
+ }
261
+ function hasDangerousGitOperation(cmd) {
262
+ const literalPatternRanges = literalSearchPatternRanges(cmd);
263
+ for (const match of cmd.matchAll(/\bgit\b/gi)) {
264
+ if (match.index !== undefined && inRanges(match.index, literalPatternRanges))
265
+ continue;
266
+ const args = shellishArgsAfter(cmd, match.index + match[0].length).map(cleanShellToken);
267
+ const envAssignments = shellEnvAssignmentsBeforeCommand(cmd, match.index);
268
+ const { aliases, unknownAliases } = gitAliasesFromEnvConfig(envAssignments);
269
+ for (let i = 0; i < args.length; i += 1) {
270
+ const arg = args[i];
271
+ if (arg === '--')
272
+ break;
273
+ const configValue = gitInlineConfigValue(arg, args[i + 1]);
274
+ if (configValue) {
275
+ const alias = gitAliasFromConfig(configValue.value);
276
+ if (alias)
277
+ aliases.set(alias.name, alias.command);
278
+ if (configValue.consumesNext)
279
+ i += 1;
280
+ continue;
281
+ }
282
+ const configEnvValue = gitConfigEnvValue(arg, args[i + 1]);
283
+ if (configEnvValue) {
284
+ const alias = gitAliasFromConfigEnv(configEnvValue.value, envAssignments);
285
+ if (alias) {
286
+ if (alias.command === undefined) {
287
+ aliases.delete(alias.name);
288
+ unknownAliases.add(alias.name);
289
+ }
290
+ else {
291
+ unknownAliases.delete(alias.name);
292
+ aliases.set(alias.name, alias.command);
293
+ }
294
+ }
295
+ if (configEnvValue.consumesNext)
296
+ i += 1;
297
+ continue;
298
+ }
299
+ if (arg === 'push') {
300
+ if (gitPushHasDeniedOperation(args.slice(i + 1)))
301
+ return true;
302
+ break;
303
+ }
304
+ if (arg === 'reset') {
305
+ if (gitResetHasHardFlag(args.slice(i + 1)))
306
+ return true;
307
+ break;
308
+ }
309
+ if (arg === 'clean') {
310
+ if (gitCleanHasForceFlag(args.slice(i + 1)))
311
+ return true;
312
+ break;
313
+ }
314
+ const aliasCommand = aliases.get(arg);
315
+ if (aliasCommand) {
316
+ if (gitAliasHasDeniedOperation(aliasCommand, args.slice(i + 1)))
317
+ return true;
318
+ break;
319
+ }
320
+ if (unknownAliases.has(arg))
321
+ return true;
322
+ if (gitOptionConsumesNext(arg)) {
323
+ i += 1;
324
+ continue;
325
+ }
326
+ if (arg.startsWith('-'))
327
+ continue;
328
+ break;
329
+ }
330
+ }
331
+ return false;
332
+ }
333
+ function gitInlineConfigValue(arg, next) {
334
+ if (arg === '-c')
335
+ return next ? { value: next, consumesNext: true } : undefined;
336
+ if (arg.startsWith('-c') && arg.length > 2)
337
+ return { value: arg.slice(2), consumesNext: false };
338
+ return undefined;
339
+ }
340
+ function gitConfigEnvValue(arg, next) {
341
+ if (arg === '--config-env')
342
+ return next ? { value: next, consumesNext: true } : undefined;
343
+ if (arg.startsWith('--config-env='))
344
+ return { value: arg.slice('--config-env='.length), consumesNext: false };
345
+ return undefined;
346
+ }
347
+ function gitAliasFromConfig(config) {
348
+ const match = config.match(/^alias\.([^=\s]+)=(.+)$/i);
349
+ return match ? { name: match[1], command: match[2] } : undefined;
350
+ }
351
+ function gitAliasFromConfigEnv(configEnv, envAssignments) {
352
+ const match = configEnv.match(/^alias\.([^=\s]+)=([A-Za-z_][A-Za-z0-9_]*)$/i);
353
+ if (!match)
354
+ return undefined;
355
+ return { name: match[1], command: envAssignments.get(match[2]) };
356
+ }
357
+ function gitAliasesFromEnvConfig(envAssignments) {
358
+ const aliases = new Map();
359
+ const unknownAliases = new Set();
360
+ const countValue = envAssignments.get('GIT_CONFIG_COUNT');
361
+ if (!countValue || !/^\d+$/.test(countValue))
362
+ return { aliases, unknownAliases };
363
+ const count = Number.parseInt(countValue, 10);
364
+ const entries = [];
365
+ for (const [name, key] of envAssignments) {
366
+ const match = name.match(/^GIT_CONFIG_KEY_(\d+)$/);
367
+ if (!match)
368
+ continue;
369
+ const index = Number.parseInt(match[1], 10);
370
+ if (index >= count)
371
+ continue;
372
+ entries.push({ index, key });
373
+ }
374
+ entries.sort((a, b) => a.index - b.index);
375
+ for (const { index, key } of entries) {
376
+ const alias = key.match(/^alias\.([^=\s]+)$/i);
377
+ if (!alias)
378
+ continue;
379
+ const command = envAssignments.get(`GIT_CONFIG_VALUE_${index}`);
380
+ if (command === undefined) {
381
+ aliases.delete(alias[1]);
382
+ unknownAliases.add(alias[1]);
383
+ }
384
+ else {
385
+ unknownAliases.delete(alias[1]);
386
+ aliases.set(alias[1], command);
387
+ }
388
+ }
389
+ return { aliases, unknownAliases };
390
+ }
391
+ function gitOptionConsumesNext(arg) {
392
+ return GIT_OPTIONS_WITH_VALUE.has(arg);
393
+ }
394
+ function gitAliasHasDeniedOperation(command, remainingArgs) {
395
+ const aliasArgs = shellishArgsAfter(command, 0).map(cleanShellToken);
396
+ if (aliasArgs.length === 0)
397
+ return false;
398
+ if (aliasArgs[0].startsWith('!')) {
399
+ const shellCommand = [aliasArgs[0].slice(1), ...aliasArgs.slice(1), ...remainingArgs].join(' ');
400
+ return hasRmRecursiveForce(shellCommand) || hasDangerousGitOperation(shellCommand) || hasDestructiveCommand(shellCommand);
401
+ }
402
+ return gitExpandedArgsHaveDeniedOperation([...aliasArgs, ...remainingArgs]);
403
+ }
404
+ function gitExpandedArgsHaveDeniedOperation(args) {
405
+ for (let i = 0; i < args.length; i += 1) {
406
+ const arg = args[i];
407
+ if (arg === '--')
408
+ break;
409
+ if (arg === 'push')
410
+ return gitPushHasDeniedOperation(args.slice(i + 1));
411
+ if (arg === 'reset')
412
+ return gitResetHasHardFlag(args.slice(i + 1));
413
+ if (arg === 'clean')
414
+ return gitCleanHasForceFlag(args.slice(i + 1));
415
+ if (gitOptionConsumesNext(arg)) {
416
+ i += 1;
417
+ continue;
418
+ }
419
+ if (arg.startsWith('-'))
420
+ continue;
421
+ break;
422
+ }
423
+ return false;
424
+ }
425
+ function gitResetHasHardFlag(args) {
426
+ for (const arg of args) {
427
+ if (arg === '--')
428
+ break;
429
+ if (arg === '--hard')
430
+ return true;
431
+ }
432
+ return false;
433
+ }
434
+ function gitCleanHasForceFlag(args) {
435
+ let force = false;
436
+ let dryRun = false;
437
+ for (const arg of args) {
438
+ if (arg === '--')
439
+ break;
440
+ if (arg === '--dry-run')
441
+ dryRun = true;
442
+ if (arg === '--force' || /^-[^-]*f/.test(arg))
443
+ force = true;
444
+ if (/^-[^-]*n/.test(arg))
445
+ dryRun = true;
446
+ }
447
+ return force && !dryRun;
448
+ }
449
+ function gitPushHasDeniedOperation(args) {
450
+ let optionsDone = false;
451
+ for (const arg of args) {
452
+ if (arg === '--') {
453
+ optionsDone = true;
454
+ continue;
455
+ }
456
+ if (/^\+.+/.test(arg))
457
+ return true;
458
+ if (/^\+?:[^:]/.test(arg))
459
+ return true;
460
+ if (optionsDone)
461
+ continue;
462
+ if (/^--force(?:$|[=-])/.test(arg) || /^-[^-]*f/.test(arg))
463
+ return true;
464
+ if (arg === '--delete' || /^-[^-]*d/.test(arg) || arg === '--mirror' || arg === '--prune')
465
+ return true;
466
+ }
467
+ return false;
468
+ }
469
+ function protectedEnvToken(token) {
470
+ const clean = cleanShellToken(token);
471
+ if (!clean || clean.startsWith('-'))
472
+ return false;
473
+ return hasProtectedEnvSegment(clean);
474
+ }
475
+ function decodeEscapedCodePoint(match, value, radix) {
476
+ const codePoint = Number.parseInt(value, radix);
477
+ return codePoint >= 0 && codePoint <= 0x10ffff ? String.fromCodePoint(codePoint) : match;
478
+ }
479
+ function decodeAnsiCQuoteBody(body) {
480
+ return body
481
+ .replace(/\\x([0-9a-fA-F]{1,2})/g, (match, hex) => decodeEscapedCodePoint(match, hex, 16))
482
+ .replace(/\\u([0-9a-fA-F]{1,4})/g, (match, hex) => decodeEscapedCodePoint(match, hex, 16))
483
+ .replace(/\\U([0-9a-fA-F]{1,8})/g, (match, hex) => decodeEscapedCodePoint(match, hex, 16))
484
+ .replace(/\\([0-7]{1,3})/g, (match, octal) => decodeEscapedCodePoint(match, octal, 8))
485
+ .replace(/\\([abefnrtv\\'"])/g, (_match, escaped) => {
486
+ const mapped = {
487
+ a: '\x07',
488
+ b: '\b',
489
+ e: '\x1b',
490
+ f: '\f',
491
+ n: '\n',
492
+ r: '\r',
493
+ t: '\t',
494
+ v: '\v',
495
+ '\\': '\\',
496
+ "'": "'",
497
+ '"': '"',
498
+ };
499
+ return mapped[escaped] ?? escaped;
500
+ });
501
+ }
502
+ function expandAnsiCQuotes(token) {
503
+ return token.replace(/\$'((?:\\.|[^'])*)'/g, (_match, body) => decodeAnsiCQuoteBody(body));
504
+ }
505
+ function cleanShellToken(token) {
506
+ return expandAnsiCQuotes(token)
507
+ .trim()
508
+ .replace(/\$(['"])/g, '$1')
509
+ .replace(/^['"`]+|['"`]+$/g, '')
510
+ .replace(/['"`]/g, '')
511
+ .replace(/\\(.)/g, '$1')
512
+ .replace(/^\d*(?:<|>)+/, '')
513
+ .replace(/[),\]}]+$/g, '');
514
+ }
515
+ function cleanRedirectionToken(token) {
516
+ return token
517
+ .trim()
518
+ .replace(/^['"`]+|['"`]+$/g, '')
519
+ .replace(/\\(.)/g, '$1')
520
+ .replace(/[),\]}]+$/g, '');
521
+ }
522
+ function cleanCommandPayloadToken(token) {
523
+ return expandAnsiCQuotes(token)
524
+ .trim()
525
+ .replace(/[),\]}]+$/g, '')
526
+ .replace(/\$(['"])/g, '$1')
527
+ .replace(/^['"`]+|['"`]+$/g, '')
528
+ .replace(/\\(.)/g, '$1');
529
+ }
530
+ function readerOptionReadsProtectedEnv(command, token) {
531
+ if (!['awk', 'grep', 'rg', 'sed'].includes(command))
532
+ return false;
533
+ const clean = cleanShellToken(token);
534
+ const longFile = clean.match(/^--file=(.+)$/);
535
+ if (longFile)
536
+ return optionValueHasProtectedEnvSegment(longFile[1]);
537
+ const shortFile = clean.match(/^-f(.+)$/);
538
+ if (shortFile)
539
+ return optionValueHasProtectedEnvSegment(shortFile[1]);
540
+ if (command === 'grep') {
541
+ const include = clean.match(/^--include=(.+)$/);
542
+ return include ? optionValueHasProtectedEnvSegment(include[1]) : false;
543
+ }
544
+ if (command === 'rg') {
545
+ const glob = clean.match(/^--i?glob=(.+)$/);
546
+ if (glob)
547
+ return rgGlobReadsProtectedEnv(glob[1]);
548
+ const shortGlob = clean.match(/^-g(.+)$/);
549
+ return shortGlob ? rgGlobReadsProtectedEnv(shortGlob[1]) : false;
550
+ }
551
+ return false;
552
+ }
553
+ function optionValueHasProtectedEnvSegment(value) {
554
+ return hasProtectedEnvSegment(cleanShellToken(value));
555
+ }
556
+ function rgGlobReadsProtectedEnv(value) {
557
+ const clean = cleanShellToken(value);
558
+ return !clean.startsWith('!') && hasProtectedEnvSegment(clean);
559
+ }
560
+ function hasProtectedEnvSegment(path) {
561
+ return path.split(/[\\/]+/).some((part) => part.startsWith('.env') && part !== '.env.example');
562
+ }
563
+ function shellTokenExcludesProtectedEnvPath(token) {
564
+ const clean = cleanShellToken(token);
565
+ return /^(?:--exclude|--exclude-dir)=/.test(clean) && clean.split('=').slice(1).every(optionValueHasProtectedEnvSegment);
566
+ }
567
+ function mentionsProtectedEnvPath(cmd) {
568
+ return shellishTokens(cmd).some((token) => {
569
+ if (shellTokenExcludesProtectedEnvPath(token))
570
+ return false;
571
+ return cleanShellToken(token)
572
+ .split(/[=$(){}\[\],;<>|&]+/)
573
+ .some((part) => hasProtectedEnvSegment(cleanShellToken(part)));
574
+ });
575
+ }
576
+ function shellishArgsAfter(cmd, start) {
577
+ const args = [];
578
+ let token = '';
579
+ let quote = '';
580
+ let escaping = false;
581
+ for (let i = start; i < cmd.length; i += 1) {
582
+ const ch = cmd[i];
583
+ if (escaping) {
584
+ token += ch;
585
+ escaping = false;
586
+ continue;
587
+ }
588
+ if (ch === '\\' && quote !== "'") {
589
+ escaping = true;
590
+ token += ch;
591
+ continue;
592
+ }
593
+ if (quote) {
594
+ token += ch;
595
+ if (ch === quote)
596
+ quote = '';
597
+ continue;
598
+ }
599
+ if (ch === "'" || ch === '"') {
600
+ quote = ch;
601
+ token += ch;
602
+ continue;
603
+ }
604
+ if (ch === '`' || ch === ';' || ch === '&' || ch === '|')
605
+ break;
606
+ if (/\s/.test(ch)) {
607
+ if (token) {
608
+ args.push(token);
609
+ token = '';
610
+ }
611
+ continue;
612
+ }
613
+ token += ch;
614
+ }
615
+ if (token)
616
+ args.push(token);
617
+ return args;
618
+ }
619
+ function shellishTokens(cmd) {
620
+ const args = [];
621
+ let token = '';
622
+ let quote = '';
623
+ let escaping = false;
624
+ for (let i = 0; i < cmd.length; i += 1) {
625
+ const ch = cmd[i];
626
+ if (escaping) {
627
+ token += ch;
628
+ escaping = false;
629
+ continue;
630
+ }
631
+ if (ch === '\\' && quote !== "'") {
632
+ escaping = true;
633
+ token += ch;
634
+ continue;
635
+ }
636
+ if (quote) {
637
+ token += ch;
638
+ if (ch === quote)
639
+ quote = '';
640
+ continue;
641
+ }
642
+ if (ch === "'" || ch === '"') {
643
+ quote = ch;
644
+ token += ch;
645
+ continue;
646
+ }
647
+ if (/[\s;|&]/.test(ch)) {
648
+ if (token) {
649
+ args.push(token);
650
+ token = '';
651
+ }
652
+ continue;
653
+ }
654
+ token += ch;
655
+ }
656
+ if (token)
657
+ args.push(token);
658
+ return args;
659
+ }
660
+ function shellishTokenEntries(cmd) {
661
+ const entries = [];
662
+ let token = '';
663
+ let tokenStart = 0;
664
+ let segment = 0;
665
+ let quote = '';
666
+ let escaping = false;
667
+ const flush = (end) => {
668
+ if (!token)
669
+ return;
670
+ entries.push({ raw: token, start: tokenStart, end, segment });
671
+ token = '';
672
+ };
673
+ const append = (ch, index) => {
674
+ if (!token)
675
+ tokenStart = index;
676
+ token += ch;
677
+ };
678
+ for (let i = 0; i < cmd.length; i += 1) {
679
+ const ch = cmd[i];
680
+ if (escaping) {
681
+ append(ch, i);
682
+ escaping = false;
683
+ continue;
684
+ }
685
+ if (ch === '\\' && quote !== "'") {
686
+ escaping = true;
687
+ append(ch, i);
688
+ continue;
689
+ }
690
+ if (quote) {
691
+ append(ch, i);
692
+ if (ch === quote)
693
+ quote = '';
694
+ continue;
695
+ }
696
+ if (ch === "'" || ch === '"') {
697
+ quote = ch;
698
+ append(ch, i);
699
+ continue;
700
+ }
701
+ if (/[;|&\n\r]/.test(ch)) {
702
+ flush(i);
703
+ segment += 1;
704
+ continue;
705
+ }
706
+ if (/\s/.test(ch)) {
707
+ flush(i);
708
+ continue;
709
+ }
710
+ append(ch, i);
711
+ }
712
+ flush(cmd.length);
713
+ return entries;
714
+ }
715
+ function shellCommandSegmentStart(cmd, end) {
716
+ let start = 0;
717
+ let quote = '';
718
+ let escaping = false;
719
+ for (let i = 0; i < end; i += 1) {
720
+ const ch = cmd[i];
721
+ if (escaping) {
722
+ escaping = false;
723
+ continue;
724
+ }
725
+ if (ch === '\\' && quote !== "'") {
726
+ escaping = true;
727
+ continue;
728
+ }
729
+ if (quote) {
730
+ if (ch === quote)
731
+ quote = '';
732
+ continue;
733
+ }
734
+ if (ch === "'" || ch === '"') {
735
+ quote = ch;
736
+ continue;
737
+ }
738
+ if (ch === '`' || ch === ';' || ch === '&' || ch === '|' || ch === '\n' || ch === '\r')
739
+ start = i + 1;
740
+ }
741
+ return start;
742
+ }
743
+ function shellEnvAssignment(clean) {
744
+ const match = clean.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
745
+ return match ? { name: match[1], value: match[2] } : undefined;
746
+ }
747
+ function shellEnvAssignmentsBeforeCommand(cmd, commandStart) {
748
+ const segmentStart = shellCommandSegmentStart(cmd, commandStart);
749
+ const tokens = shellishTokens(cmd.slice(segmentStart, commandStart));
750
+ const assignments = new Map();
751
+ let envMode = false;
752
+ let envOptionsDone = false;
753
+ let commandSeen = false;
754
+ for (let i = 0; i < tokens.length; i += 1) {
755
+ const clean = cleanShellToken(tokens[i]);
756
+ const assignment = shellEnvAssignment(clean);
757
+ if (assignment && (!commandSeen || envMode)) {
758
+ assignments.set(assignment.name, assignment.value);
759
+ continue;
760
+ }
761
+ if (!commandSeen && clean === 'env') {
762
+ envMode = true;
763
+ envOptionsDone = false;
764
+ commandSeen = true;
765
+ continue;
766
+ }
767
+ if (envMode) {
768
+ if (!envOptionsDone && clean === '--') {
769
+ envOptionsDone = true;
770
+ continue;
771
+ }
772
+ if (!envOptionsDone && envOptionConsumesNext(clean)) {
773
+ i += 1;
774
+ continue;
775
+ }
776
+ if (!envOptionsDone && clean.startsWith('-'))
777
+ continue;
778
+ }
779
+ commandSeen = true;
780
+ envMode = false;
781
+ }
782
+ return assignments;
783
+ }
784
+ function inlineRedirectionTarget(token) {
785
+ const clean = cleanRedirectionToken(token);
786
+ const match = clean.match(/^(?:\d*|&)(?:<>|>>|>|<)(?![<(])(.+)$/);
787
+ return match?.[1];
788
+ }
789
+ function standaloneRedirection(token) {
790
+ return /^(?:\d*|&)(?:<>|>>|>|<)$/.test(cleanRedirectionToken(token));
791
+ }
792
+ function commandSubstitutionTouchesProtectedEnv(cmd) {
793
+ for (const match of cmd.matchAll(/\$\(\s*(?:\d*|&)(?:<>|>>|>|<)\s*([^\s)]+)/g)) {
794
+ if (optionValueHasProtectedEnvSegment(match[1]))
795
+ return true;
796
+ }
797
+ return false;
798
+ }
799
+ function redirectionTouchesProtectedEnv(cmd) {
800
+ if (commandSubstitutionTouchesProtectedEnv(cmd))
801
+ return true;
802
+ const tokens = shellishTokens(cmd);
803
+ for (let i = 0; i < tokens.length; i += 1) {
804
+ const inlineTarget = inlineRedirectionTarget(tokens[i]);
805
+ if (inlineTarget && optionValueHasProtectedEnvSegment(inlineTarget))
806
+ return true;
807
+ if (standaloneRedirection(tokens[i]) && protectedEnvToken(tokens[i + 1] ?? ''))
808
+ return true;
809
+ }
810
+ return false;
811
+ }
812
+ function writerArgsTouchProtectedEnv(args) {
813
+ let optionsDone = false;
814
+ for (const arg of args) {
815
+ const clean = cleanShellToken(arg);
816
+ if (!optionsDone) {
817
+ if (clean === '--') {
818
+ optionsDone = true;
819
+ continue;
820
+ }
821
+ if (clean.startsWith('-'))
822
+ continue;
823
+ }
824
+ if (protectedEnvToken(arg))
825
+ return true;
826
+ }
827
+ return false;
828
+ }
829
+ function shellOptionConsumesNext(clean) {
830
+ return SHELL_OPTIONS_WITH_VALUE.has(clean) || /^[-+]o$/i.test(clean);
831
+ }
832
+ function envOptionConsumesNext(clean) {
833
+ return ENV_OPTIONS_WITH_VALUE.has(clean);
834
+ }
835
+ function inlineEnvSplitStringPayload(clean) {
836
+ return clean.match(/^-S(.+)$/)?.[1] ?? clean.match(/^--split-string=(.+)$/)?.[1];
837
+ }
838
+ function envWrappedCommandDenied(cmd, depth) {
839
+ if (depth >= 4)
840
+ return false;
841
+ for (const match of cmd.matchAll(ENV_CMD)) {
842
+ const args = shellishArgsAfter(cmd, match.index + match[0].length);
843
+ let commandIndex = -1;
844
+ for (let i = 0; i < args.length; i += 1) {
845
+ const clean = cleanShellToken(args[i]);
846
+ if (clean === '--') {
847
+ commandIndex = i + 1;
848
+ break;
849
+ }
850
+ const inlineSplitPayload = inlineEnvSplitStringPayload(clean);
851
+ if (inlineSplitPayload !== undefined) {
852
+ const payload = [inlineSplitPayload, ...args.slice(i + 1)].join(' ');
853
+ if (payload && !checkBash(cleanCommandPayloadToken(payload), depth + 1).ok)
854
+ return true;
855
+ break;
856
+ }
857
+ if (clean === '-S' || clean === '--split-string') {
858
+ const payload = args.slice(i + 1).join(' ');
859
+ if (payload && !checkBash(cleanCommandPayloadToken(payload), depth + 1).ok)
860
+ return true;
861
+ break;
862
+ }
863
+ if (envOptionConsumesNext(clean)) {
864
+ i += 1;
865
+ continue;
866
+ }
867
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(clean))
868
+ continue;
869
+ if (clean.startsWith('-'))
870
+ continue;
871
+ commandIndex = i;
872
+ break;
873
+ }
874
+ if (commandIndex >= 0) {
875
+ const payload = args.slice(commandIndex).join(' ');
876
+ if (payload && !checkBash(payload, depth + 1).ok)
877
+ return true;
878
+ }
879
+ }
880
+ return false;
881
+ }
882
+ function nestedShellCommandDenied(cmd, depth) {
883
+ if (depth >= 4)
884
+ return false;
885
+ for (const match of cmd.matchAll(NESTED_SHELL_CMD)) {
886
+ const args = shellishArgsAfter(cmd, match.index + match[0].length);
887
+ for (let i = 0; i < args.length; i += 1) {
888
+ const clean = cleanShellToken(args[i]);
889
+ if (clean === '--')
890
+ break;
891
+ if (shellOptionConsumesNext(clean)) {
892
+ i += 1;
893
+ continue;
894
+ }
895
+ if (clean.startsWith('--'))
896
+ continue;
897
+ if (!clean.startsWith('-'))
898
+ break;
899
+ const inlineCommand = clean.match(/^-[^-]*?c(.+)$/);
900
+ if (inlineCommand || /^-[^-]*?c$/.test(clean)) {
901
+ const payload = inlineCommand?.[1] || args[i + 1];
902
+ if (payload && !checkBash(cleanCommandPayloadToken(payload), depth + 1).ok)
903
+ return true;
904
+ break;
905
+ }
906
+ }
907
+ }
908
+ return false;
909
+ }
910
+ function readsProtectedEnvFile(cmd) {
911
+ for (const match of cmd.matchAll(ENV_READ_CMD)) {
912
+ const args = shellishArgsAfter(cmd, match.index + match[0].length);
913
+ const command = match[1].toLowerCase();
914
+ if (args.some((arg) => protectedEnvToken(arg) || readerOptionReadsProtectedEnv(command, arg)))
915
+ return true;
916
+ }
917
+ for (const match of cmd.matchAll(ENV_SOURCE_CMD)) {
918
+ const args = shellishArgsAfter(cmd, match.index + match[0].length);
919
+ if (protectedEnvToken(args[0] ?? ''))
920
+ return true;
921
+ }
922
+ for (const match of cmd.matchAll(ENV_WRITE_CMD)) {
923
+ const args = shellishArgsAfter(cmd, match.index + match[0].length);
924
+ if (writerArgsTouchProtectedEnv(args))
925
+ return true;
926
+ }
927
+ if (redirectionTouchesProtectedEnv(cmd))
928
+ return true;
929
+ return false;
930
+ }
931
+ export function checkBash(cmd, depth = 0) {
932
+ if (hasRmRecursiveForce(cmd) || hasDangerousGitOperation(cmd) || hasDestructiveCommand(cmd)) {
30
933
  return { ok: false, reason: `คำสั่งทำลายล้าง/irreversible ถูกปฏิเสธ: "${cmd}"` };
31
934
  }
32
- if (PROTECTED_CMD_PATH.test(cmd)) {
935
+ if (PROTECTED_CMD_PATH.test(cmd) || mentionsProtectedEnvPath(cmd) || readsProtectedEnvFile(cmd)) {
33
936
  return { ok: false, reason: `คำสั่งที่อ่าน/แตะ path ลับถูกปฏิเสธ: "${cmd}"` };
34
937
  }
938
+ if (nestedShellCommandDenied(cmd, depth)) {
939
+ return { ok: false, reason: `คำสั่ง nested shell ที่อันตรายถูกปฏิเสธ: "${cmd}"` };
940
+ }
941
+ if (envWrappedCommandDenied(cmd, depth)) {
942
+ return { ok: false, reason: `คำสั่ง env wrapper ที่อันตรายถูกปฏิเสธ: "${cmd}"` };
943
+ }
35
944
  return { ok: true };
36
945
  }
37
946
  async function canonicalExisting(path) {
@@ -75,8 +984,7 @@ function protectedSegment(abs) {
75
984
  const parts = abs.split(/[\\/]+/);
76
985
  if (parts.some((p) => PROTECTED_SEGMENTS.has(p)))
77
986
  return true;
78
- const base = basename(abs);
79
- return base.startsWith('.env') && base !== '.env.example';
987
+ return hasProtectedEnvSegment(abs);
80
988
  }
81
989
  async function checkPathScope(path, intent) {
82
990
  const abs = intent === 'write' ? await existingAncestor(path) : await canonicalExisting(path);
@@ -100,8 +1008,14 @@ export async function checkReadPath(path) {
100
1008
  /** กันเขียนทับ secrets/shell-rc/.sanook + กันเขียนนอก workspace/brain */
101
1009
  export async function checkWritePath(path) {
102
1010
  const abs = resolve(path);
103
- const inProtectedDir = PROTECTED_DIRS.some((d) => abs === d || abs.startsWith(d + sep));
104
- if (PROTECTED_EXACT.has(abs) || inProtectedDir || protectedSegment(abs)) {
1011
+ const canonical = await existingAncestor(path);
1012
+ const inProtectedDir = (p) => PROTECTED_DIRS.some((d) => p === d || p.startsWith(d + sep));
1013
+ if (PROTECTED_EXACT.has(abs) ||
1014
+ PROTECTED_EXACT.has(canonical) ||
1015
+ inProtectedDir(abs) ||
1016
+ inProtectedDir(canonical) ||
1017
+ protectedSegment(abs) ||
1018
+ protectedSegment(canonical)) {
105
1019
  return {
106
1020
  ok: false,
107
1021
  reason: `path ที่ป้องกันถูกปฏิเสธ: "${path}" (secrets / shell-rc / .sanook / .git / .env / node_modules)`,