@dreb/coding-agent 2.6.3 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -24,6 +24,46 @@ const DEFAULT_FORBIDDEN_PATTERNS = [
24
24
  "^(?:export\\s+)?HUSKY=0", // bypass pre-commit hooks (anchored with optional export prefix)
25
25
  "^git\\s+commit.*--no-verify", // bypass pre-commit hooks via --no-verify flag
26
26
  "^(?:export\\s+)?SKIP_?VALIDATION=1", // bypass pre-commit hooks via SKIP_VALIDATION env var
27
+ "^rm\\s+.*--no-preserve-root", // rm with explicit safety override
28
+ "^rm\\s+.*\\s[\"']?/(\\*|[\\w.-]+/?)?[\"']?(\\s|$)", // rm targeting root or top-level dirs (/, /*, /home, /etc)
29
+ "^dd\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)", // dd writing to block devices
30
+ "^mkfs", // format filesystem (mkfs.ext4, mkfs.xfs, etc.)
31
+ "^>>?\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)", // redirect to block device (> and >>)
32
+ ];
33
+ /**
34
+ * Patterns checked against the full (quote-masked) command string before
35
+ * splitting into segments. These catch dangerous constructs that span
36
+ * shell operators and would be fragmented by the segment splitter.
37
+ *
38
+ * Matched against the masked string so quoted content doesn't trigger
39
+ * false positives (e.g., `echo ":(){ :|:& };:"` is safe).
40
+ */
41
+ const FULL_COMMAND_PATTERNS = [
42
+ ":\\(\\)\\s*\\{", // fork bomb :(){ :|:& };:
43
+ ];
44
+ /**
45
+ * Patterns also checked against content extracted from within quoted strings.
46
+ * Catches commands like `echo "rm -rf /"` where the quoted content is a
47
+ * destructive command that could be piped to execution via `| bash`.
48
+ *
49
+ * These are intentionally limited to destructive/dangerous patterns — env var
50
+ * patterns like HUSKY=0 are excluded because they appear legitimately in
51
+ * contexts like `git log --grep="HUSKY=0"`.
52
+ *
53
+ * The fork bomb pattern from FULL_COMMAND_PATTERNS is included here because
54
+ * it also needs to be caught when quoted (e.g., `echo ":(){ :|:& };:"`).
55
+ */
56
+ const QUOTED_CONTENT_PATTERNS = [
57
+ "^rm\\s+.*--no-preserve-root",
58
+ "^rm\\s+.*\\s/(\\*|[\\w.-]+/?)?(\\s|$)",
59
+ "^dd\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)",
60
+ "^mkfs",
61
+ "^>>?\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)",
62
+ "^gh pr merge.*--admin",
63
+ "^git push.*(-f\\b|--force)",
64
+ "^gh api.*bypass",
65
+ "^git\\s+commit.*--no-verify",
66
+ ":\\(\\)\\s*\\{", // fork bomb
27
67
  ];
28
68
  /**
29
69
  * Mask content inside single and double-quoted strings by replacing
@@ -41,9 +81,9 @@ function maskQuotedContent(command) {
41
81
  for (let i = 0; i < command.length; i++) {
42
82
  const ch = command[i];
43
83
  if (ch === "'" && !inDouble) {
44
- if (!isEscaped(command, i)) {
45
- inSingle = !inSingle;
46
- }
84
+ // In bash, single-quoted strings are completely literal — backslashes
85
+ // have no escape function inside single quotes. Always toggle.
86
+ inSingle = !inSingle;
47
87
  result += ch;
48
88
  }
49
89
  else if (ch === '"' && !inSingle) {
@@ -80,6 +120,36 @@ function isEscaped(str, i) {
80
120
  }
81
121
  return count % 2 === 1;
82
122
  }
123
+ /**
124
+ * Extract text content from within quoted strings in a segment.
125
+ * Used to catch commands like `echo "rm -rf /" | bash` where dangerous
126
+ * content is hidden inside quotes. The normal segment check won't catch
127
+ * this because `echo` (not `rm`) starts the segment. By extracting the
128
+ * quoted content and checking it separately, we block segments that
129
+ * contain forbidden commands in their quoted arguments.
130
+ */
131
+ function extractQuotedContent(text) {
132
+ const results = [];
133
+ let inQuote = null;
134
+ let start = -1;
135
+ for (let i = 0; i < text.length; i++) {
136
+ const ch = text[i];
137
+ if ((ch === '"' || ch === "'") && (ch === "'" || !isEscaped(text, i))) {
138
+ if (inQuote === null) {
139
+ inQuote = ch;
140
+ start = i + 1;
141
+ }
142
+ else if (ch === inQuote) {
143
+ const content = text.substring(start, i).trim();
144
+ if (content.length > 0) {
145
+ results.push(content);
146
+ }
147
+ inQuote = null;
148
+ }
149
+ }
150
+ }
151
+ return results;
152
+ }
83
153
  /**
84
154
  * Split a command string into individual segments on shell operators.
85
155
  *
@@ -161,7 +231,26 @@ export function isForbiddenCommand(command, extraPatterns) {
161
231
  const allPatterns = validatedExtras
162
232
  ? [...DEFAULT_FORBIDDEN_PATTERNS, ...validatedExtras]
163
233
  : DEFAULT_FORBIDDEN_PATTERNS;
234
+ // Pre-split check: match full-command patterns against the quote-masked
235
+ // string to catch constructs that span shell operators (e.g., fork bombs).
236
+ // Using the masked string prevents false positives from quoted content.
237
+ const masked = maskQuotedContent(command);
238
+ for (const pattern of FULL_COMMAND_PATTERNS) {
239
+ try {
240
+ const re = new RegExp(pattern);
241
+ if (re.test(masked)) {
242
+ return pattern;
243
+ }
244
+ }
245
+ catch {
246
+ // Invalid regex — skip
247
+ }
248
+ }
164
249
  const segments = splitCommandSegments(command);
250
+ // Combine quoted-content patterns with any user extras for quoted checking
251
+ const allQuotedPatterns = validatedExtras
252
+ ? [...QUOTED_CONTENT_PATTERNS, ...validatedExtras]
253
+ : QUOTED_CONTENT_PATTERNS;
165
254
  for (const segment of segments) {
166
255
  // Check both the raw segment and the subshell-unwrapped version
167
256
  const toCheck = [segment, stripSubshellWrapper(segment)];
@@ -178,6 +267,89 @@ export function isForbiddenCommand(command, extraPatterns) {
178
267
  }
179
268
  }
180
269
  }
270
+ // Check content within quotes for embedded dangerous commands.
271
+ // There is no legitimate reason for an agent to output/echo forbidden
272
+ // commands, and quoted content could be piped to execution via | bash.
273
+ const quotedContent = extractQuotedContent(segment);
274
+ for (const content of quotedContent) {
275
+ for (const pattern of allQuotedPatterns) {
276
+ try {
277
+ const re = new RegExp(pattern);
278
+ if (re.test(content)) {
279
+ return pattern;
280
+ }
281
+ }
282
+ catch {
283
+ // Invalid regex — skip
284
+ }
285
+ }
286
+ }
287
+ }
288
+ return undefined;
289
+ }
290
+ /**
291
+ * Extract file paths from a command that executes a script file.
292
+ * Detects: `bash file`, `sh file`, `source file`, `. file`, and input
293
+ * redirects like `bash < file`.
294
+ *
295
+ * Returns an array of file paths (usually 0 or 1). Does not check whether
296
+ * the files exist — the caller handles that.
297
+ *
298
+ * @returns Array of script file paths referenced by the command.
299
+ */
300
+ export function extractScriptPaths(command) {
301
+ const paths = [];
302
+ const segments = splitCommandSegments(command);
303
+ for (const segment of segments) {
304
+ const trimmed = segment.trim();
305
+ // bash < file.sh (input redirect) — check before shell exec to avoid
306
+ // the shell exec regex matching "<" as a filename
307
+ const redirectMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)(?:\s+-\S+)*\s+<\s*(\S+)/);
308
+ if (redirectMatch?.[1]) {
309
+ paths.push(redirectMatch[1]);
310
+ continue;
311
+ }
312
+ // bash [flags] file.sh, sh [flags] file.sh
313
+ // Flags are short options like -x, -e, -ex, etc.
314
+ // Exclude -c (inline command — handled by quoted content check)
315
+ if (/^(?:bash|sh|zsh|ksh)\s+-c\b/.test(trimmed)) {
316
+ continue;
317
+ }
318
+ const shellExecMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)\s+(?:-\S+\s+)*(\S+)/);
319
+ if (shellExecMatch) {
320
+ const filePath = shellExecMatch[1];
321
+ if (filePath && !filePath.startsWith("-")) {
322
+ paths.push(filePath);
323
+ }
324
+ }
325
+ // source file.sh, . file.sh
326
+ const sourceMatch = trimmed.match(/^(?:source|\.)\s+(\S+)/);
327
+ if (sourceMatch?.[1]) {
328
+ paths.push(sourceMatch[1]);
329
+ }
330
+ }
331
+ return [...new Set(paths)]; // deduplicate
332
+ }
333
+ /**
334
+ * Check file content line-by-line for forbidden commands.
335
+ * Each non-empty, non-comment line is passed through `isForbiddenCommand`.
336
+ *
337
+ * This is a pure function — the caller is responsible for reading the file
338
+ * and passing the content string.
339
+ *
340
+ * @returns The first match with pattern, line number, and line text, or undefined.
341
+ */
342
+ export function checkScriptContent(content, extraPatterns) {
343
+ const lines = content.split("\n");
344
+ for (let i = 0; i < lines.length; i++) {
345
+ const line = lines[i].trim();
346
+ // Skip empty lines and comments
347
+ if (!line || line.startsWith("#"))
348
+ continue;
349
+ const pattern = isForbiddenCommand(line, extraPatterns);
350
+ if (pattern) {
351
+ return { pattern, line: i + 1, text: line };
352
+ }
181
353
  }
182
354
  return undefined;
183
355
  }
@@ -1 +1 @@
1
- {"version":3,"file":"forbidden-commands.js","sourceRoot":"","sources":["../../src/core/forbidden-commands.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Forbidden-commands guard — blocks bash commands matching dangerous patterns\n * before they reach the shell.\n *\n * Hardcoded default patterns are ALWAYS active regardless of settings.\n * Users can add additional patterns via settings.forbiddenCommands.\n *\n * Commands are split on shell operators (&&, ||, ;, |, &) and each segment\n * is checked independently. Default patterns are anchored to the start of\n * each segment (^) so they only match commands that *begin with* the dangerous\n * command, not commands that merely *mention* the pattern in string literals\n * or arguments.\n *\n * To avoid false positives from operators inside quoted strings, content\n * within single/double quotes is masked before splitting. To catch subshell\n * wrappers like $(cmd) and (cmd), leading wrapper characters are stripped\n * from each segment before pattern matching.\n */\n\n/** Hardcoded patterns that are always active. Always anchored with ^. */\nconst DEFAULT_FORBIDDEN_PATTERNS: string[] = [\n\t\"^gh pr merge.*--admin\", // bypass branch protection\n\t\"^git push.*(-f\\\\b|--force)\", // force push (includes --force-with-lease)\n\t\"^gh api.*bypass\", // API calls with bypass flag\n\t\"^(?:export\\\\s+)?HUSKY=0\", // bypass pre-commit hooks (anchored with optional export prefix)\n\t\"^git\\\\s+commit.*--no-verify\", // bypass pre-commit hooks via --no-verify flag\n\t\"^(?:export\\\\s+)?SKIP_?VALIDATION=1\", // bypass pre-commit hooks via SKIP_VALIDATION env var\n];\n\n/**\n * Mask content inside single and double-quoted strings by replacing\n * characters within quotes with underscores. This prevents shell operators\n * inside quoted strings from causing false splits.\n *\n * Handles escaped quotes (\\\", \\') within strings. Correctly counts\n * consecutive backslashes before a quote — an even count means the quote\n * is real (e.g. `\\\\\"` is escaped-backslash + closing quote).\n */\nfunction maskQuotedContent(command: string): string {\n\tlet result = \"\";\n\tlet inSingle = false;\n\tlet inDouble = false;\n\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst ch = command[i];\n\n\t\tif (ch === \"'\" && !inDouble) {\n\t\t\tif (!isEscaped(command, i)) {\n\t\t\t\tinSingle = !inSingle;\n\t\t\t}\n\t\t\tresult += ch;\n\t\t} else if (ch === '\"' && !inSingle) {\n\t\t\tif (!isEscaped(command, i)) {\n\t\t\t\tinDouble = !inDouble;\n\t\t\t}\n\t\t\tresult += ch;\n\t\t} else if (inSingle || inDouble) {\n\t\t\t// Replace content inside quotes with a safe character\n\t\t\t// that won't match shell operators\n\t\t\tresult += ch === \"\\n\" ? \"\\n\" : \"_\";\n\t\t} else {\n\t\t\tresult += ch;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Check if the character at position `i` is escaped by counting consecutive\n * trailing backslashes. If the count is odd, the character is escaped.\n * If even (including zero), it is not escaped.\n *\n * e.g. `\\\\\"` → 2 backslashes → even → `\"` is NOT escaped (real quote)\n * `\\\\\\\"` → 3 backslashes → odd → `\"` IS escaped (literal quote)\n */\nfunction isEscaped(str: string, i: number): boolean {\n\tlet count = 0;\n\tlet j = i - 1;\n\twhile (j >= 0 && str[j] === \"\\\\\") {\n\t\tcount++;\n\t\tj--;\n\t}\n\treturn count % 2 === 1;\n}\n\n/**\n * Split a command string into individual segments on shell operators.\n *\n * Handles: &&, ||, ;, |, & (background), and newlines.\n * Content inside single/double quotes is masked before splitting so that\n * operators inside quoted strings don't cause false splits.\n * Each segment is trimmed of leading whitespace.\n */\nfunction splitCommandSegments(command: string): string[] {\n\t// Mask quoted content to avoid splitting on operators inside strings\n\tconst masked = maskQuotedContent(command);\n\n\t// Split on shell operators: &&, ||, ;, |, &, and newlines\n\tconst splits = masked.split(/\\s*(?:&&|\\|\\||[;&|]|\\n)\\s*/);\n\n\t// Map split positions back to original command segments.\n\t// We split the masked string to find operator positions, but return\n\t// the original (unmasked) segments so pattern matching sees real text.\n\tconst originalSegments: string[] = [];\n\tlet maskedIdx = 0;\n\n\tfor (const part of splits) {\n\t\t// Find the start of this part in the masked string\n\t\tconst startInMasked = masked.indexOf(part, maskedIdx);\n\t\tif (startInMasked === -1) {\n\t\t\t// Fallback: use the part as-is (shouldn't happen)\n\t\t\toriginalSegments.push(command.substring(maskedIdx, maskedIdx + part.length).trim());\n\t\t} else {\n\t\t\toriginalSegments.push(command.substring(startInMasked, startInMasked + part.length).trim());\n\t\t}\n\t\tmaskedIdx = startInMasked + part.length;\n\t}\n\n\treturn originalSegments.filter((s) => s.length > 0);\n}\n\n/**\n * Strip leading subshell/command-substitution wrappers from a segment\n * so that $(cmd), (cmd), and `cmd` are checked against patterns too.\n *\n * Handles both full-segment wrappers ($(cmd)) and inline substitutions\n * (result=$(cmd)) by extracting inner commands.\n */\nfunction stripSubshellWrapper(segment: string): string {\n\t// Strip $(...) wrapper when it's the whole segment\n\tif (/^\\$\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(2, -1).trim();\n\t}\n\t// Strip (...) wrapper (subshell) when it's the whole segment\n\tif (/^\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Strip backtick wrapper when it's the whole segment\n\tif (/^`/.test(segment) && segment.endsWith(\"`\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Extract inner command from inline $() or backtick substitutions\n\t// e.g., \"result=$(git push --force)\" → \"git push --force\"\n\tconst inlineMatch = segment.match(/\\$\\(([^)]+)\\)/);\n\tif (inlineMatch) {\n\t\treturn inlineMatch[1].trim();\n\t}\n\tconst backtickMatch = segment.match(/`([^`]+)`/);\n\tif (backtickMatch) {\n\t\treturn backtickMatch[1].trim();\n\t}\n\treturn segment;\n}\n\n/**\n * Check whether a command matches any forbidden pattern.\n *\n * The command is split on shell operators (&&, ||, ;, |) with quoted content\n * masked to avoid false splits. Each segment is then stripped of subshell\n * wrappers ($(...), (...), `...`) and checked against patterns. Default\n * patterns are ^-anchored so they only match commands that start with the\n * dangerous command prefix.\n *\n * @returns The first matching pattern, or `undefined` if the command is allowed.\n */\nexport function isForbiddenCommand(command: string, extraPatterns?: string[]): string | undefined {\n\t// Guard against misconfigured settings (string instead of array)\n\tconst validatedExtras = Array.isArray(extraPatterns) ? extraPatterns : undefined;\n\tconst allPatterns = validatedExtras\n\t\t? [...DEFAULT_FORBIDDEN_PATTERNS, ...validatedExtras]\n\t\t: DEFAULT_FORBIDDEN_PATTERNS;\n\tconst segments = splitCommandSegments(command);\n\n\tfor (const segment of segments) {\n\t\t// Check both the raw segment and the subshell-unwrapped version\n\t\tconst toCheck = [segment, stripSubshellWrapper(segment)];\n\t\tfor (const text of toCheck) {\n\t\t\tfor (const pattern of allPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(text)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex in user settings — skip it\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn undefined;\n}\n"]}
1
+ {"version":3,"file":"forbidden-commands.js","sourceRoot":"","sources":["../../src/core/forbidden-commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,yEAAyE;AACzE,MAAM,0BAA0B,GAAa;IAC5C,uBAAuB,EAAE,2BAA2B;IACpD,4BAA4B,EAAE,2CAA2C;IACzE,iBAAiB,EAAE,6BAA6B;IAChD,yBAAyB,EAAE,iEAAiE;IAC5F,6BAA6B,EAAE,+CAA+C;IAC9E,oCAAoC,EAAE,sDAAsD;IAC5F,6BAA6B,EAAE,mCAAmC;IAClE,mDAAmD,EAAE,2DAA2D;IAChH,uDAAuD,EAAE,8BAA8B;IACvF,OAAO,EAAE,gDAAgD;IACzD,mDAAmD,EAAE,sCAAsC;CAC3F,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,qBAAqB,GAAa;IACvC,gBAAgB,EAAE,0BAA0B;CAC5C,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,uBAAuB,GAAa;IACzC,6BAA6B;IAC7B,uCAAuC;IACvC,uDAAuD;IACvD,OAAO;IACP,mDAAmD;IACnD,uBAAuB;IACvB,4BAA4B;IAC5B,iBAAiB;IACjB,6BAA6B;IAC7B,gBAAgB,EAAE,YAAY;CAC9B,CAAC;AAEF;;;;;;;;GAQG;AACH,SAAS,iBAAiB,CAAC,OAAe,EAAU;IACnD,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACzC,MAAM,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QAEtB,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC7B,wEAAsE;YACtE,+DAA+D;YAC/D,QAAQ,GAAG,CAAC,QAAQ,CAAC;YACrB,MAAM,IAAI,EAAE,CAAC;QACd,CAAC;aAAM,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YACpC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,CAAC;gBAC5B,QAAQ,GAAG,CAAC,QAAQ,CAAC;YACtB,CAAC;YACD,MAAM,IAAI,EAAE,CAAC;QACd,CAAC;aAAM,IAAI,QAAQ,IAAI,QAAQ,EAAE,CAAC;YACjC,sDAAsD;YACtD,mCAAmC;YACnC,MAAM,IAAI,EAAE,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC;QACpC,CAAC;aAAM,CAAC;YACP,MAAM,IAAI,EAAE,CAAC;QACd,CAAC;IACF,CAAC;IAED,OAAO,MAAM,CAAC;AAAA,CACd;AAED;;;;;;;GAOG;AACH,SAAS,SAAS,CAAC,GAAW,EAAE,CAAS,EAAW;IACnD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACd,OAAO,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAClC,KAAK,EAAE,CAAC;QACR,CAAC,EAAE,CAAC;IACL,CAAC;IACD,OAAO,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC;AAAA,CACvB;AAED;;;;;;;GAOG;AACH,SAAS,oBAAoB,CAAC,IAAY,EAAY;IACrD,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,OAAO,GAAkB,IAAI,CAAC;IAClC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC;IAEf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACnB,IAAI,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACvE,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACtB,OAAO,GAAG,EAAE,CAAC;gBACb,KAAK,GAAG,CAAC,GAAG,CAAC,CAAC;YACf,CAAC;iBAAM,IAAI,EAAE,KAAK,OAAO,EAAE,CAAC;gBAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;gBAChD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBACxB,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACvB,CAAC;gBACD,OAAO,GAAG,IAAI,CAAC;YAChB,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,OAAO,CAAC;AAAA,CACf;AAED;;;;;;;GAOG;AACH,SAAS,oBAAoB,CAAC,OAAe,EAAY;IACxD,qEAAqE;IACrE,MAAM,MAAM,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAE1C,0DAA0D;IAC1D,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;IAE1D,yDAAyD;IACzD,oEAAoE;IACpE,uEAAuE;IACvE,MAAM,gBAAgB,GAAa,EAAE,CAAC;IACtC,IAAI,SAAS,GAAG,CAAC,CAAC;IAElB,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC3B,mDAAmD;QACnD,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;QACtD,IAAI,aAAa,KAAK,CAAC,CAAC,EAAE,CAAC;YAC1B,kDAAkD;YAClD,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACrF,CAAC;aAAM,CAAC;YACP,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,aAAa,EAAE,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC7F,CAAC;QACD,SAAS,GAAG,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC;IACzC,CAAC;IAED,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAAA,CACpD;AAED;;;;;;GAMG;AACH,SAAS,oBAAoB,CAAC,OAAe,EAAU;IACtD,mDAAmD;IACnD,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACpD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACpC,CAAC;IACD,6DAA6D;IAC7D,IAAI,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAClD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACpC,CAAC;IACD,qDAAqD;IACrD,IAAI,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACjD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACpC,CAAC;IACD,kEAAkE;IAClE,4DAA0D;IAC1D,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;IACnD,IAAI,WAAW,EAAE,CAAC;QACjB,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAC9B,CAAC;IACD,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IACjD,IAAI,aAAa,EAAE,CAAC;QACnB,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IAChC,CAAC;IACD,OAAO,OAAO,CAAC;AAAA,CACf;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe,EAAE,aAAwB,EAAsB;IACjG,iEAAiE;IACjE,MAAM,eAAe,GAAG,KAAK,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC;IACjF,MAAM,WAAW,GAAG,eAAe;QAClC,CAAC,CAAC,CAAC,GAAG,0BAA0B,EAAE,GAAG,eAAe,CAAC;QACrD,CAAC,CAAC,0BAA0B,CAAC;IAE9B,wEAAwE;IACxE,2EAA2E;IAC3E,wEAAwE;IACxE,MAAM,MAAM,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC1C,KAAK,MAAM,OAAO,IAAI,qBAAqB,EAAE,CAAC;QAC7C,IAAI,CAAC;YACJ,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC;YAC/B,IAAI,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBACrB,OAAO,OAAO,CAAC;YAChB,CAAC;QACF,CAAC;QAAC,MAAM,CAAC;YACR,yBAAuB;QACxB,CAAC;IACF,CAAC;IAED,MAAM,QAAQ,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAE/C,2EAA2E;IAC3E,MAAM,iBAAiB,GAAG,eAAe;QACxC,CAAC,CAAC,CAAC,GAAG,uBAAuB,EAAE,GAAG,eAAe,CAAC;QAClD,CAAC,CAAC,uBAAuB,CAAC;IAE3B,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAChC,gEAAgE;QAChE,MAAM,OAAO,GAAG,CAAC,OAAO,EAAE,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC;QACzD,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC5B,KAAK,MAAM,OAAO,IAAI,WAAW,EAAE,CAAC;gBACnC,IAAI,CAAC;oBACJ,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC;oBAC/B,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;wBACnB,OAAO,OAAO,CAAC;oBAChB,CAAC;gBACF,CAAC;gBAAC,MAAM,CAAC;oBACR,6CAA2C;gBAC5C,CAAC;YACF,CAAC;QACF,CAAC;QAED,+DAA+D;QAC/D,sEAAsE;QACtE,uEAAuE;QACvE,MAAM,aAAa,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;QACpD,KAAK,MAAM,OAAO,IAAI,aAAa,EAAE,CAAC;YACrC,KAAK,MAAM,OAAO,IAAI,iBAAiB,EAAE,CAAC;gBACzC,IAAI,CAAC;oBACJ,MAAM,EAAE,GAAG,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC;oBAC/B,IAAI,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;wBACtB,OAAO,OAAO,CAAC;oBAChB,CAAC;gBACF,CAAC;gBAAC,MAAM,CAAC;oBACR,yBAAuB;gBACxB,CAAC;YACF,CAAC;QACF,CAAC;IACF,CAAC;IAED,OAAO,SAAS,CAAC;AAAA,CACjB;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe,EAAY;IAC7D,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,QAAQ,GAAG,oBAAoB,CAAC,OAAO,CAAC,CAAC;IAE/C,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAChC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAE/B,uEAAqE;QACrE,kDAAkD;QAClD,MAAM,aAAa,GAAG,OAAO,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;QACpF,IAAI,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACxB,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;YAC7B,SAAS;QACV,CAAC;QAED,2CAA2C;QAC3C,iDAAiD;QACjD,kEAAgE;QAChE,IAAI,6BAA6B,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACjD,SAAS;QACV,CAAC;QACD,MAAM,cAAc,GAAG,OAAO,CAAC,KAAK,CAAC,0CAA0C,CAAC,CAAC;QACjF,IAAI,cAAc,EAAE,CAAC;YACpB,MAAM,QAAQ,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;YACnC,IAAI,QAAQ,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC3C,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACtB,CAAC;QACF,CAAC;QAED,4BAA4B;QAC5B,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,CAAC,wBAAwB,CAAC,CAAC;QAC5D,IAAI,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACtB,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5B,CAAC;IACF,CAAC;IAED,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,cAAc;AAAf,CAC3B;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CACjC,OAAe,EACf,aAAwB,EACsC;IAC9D,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAElC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAE7B,gCAAgC;QAChC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,SAAS;QAE5C,MAAM,OAAO,GAAG,kBAAkB,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;QACxD,IAAI,OAAO,EAAE,CAAC;YACb,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QAC7C,CAAC;IACF,CAAC;IAED,OAAO,SAAS,CAAC;AAAA,CACjB","sourcesContent":["/**\n * Forbidden-commands guard — blocks bash commands matching dangerous patterns\n * before they reach the shell.\n *\n * Hardcoded default patterns are ALWAYS active regardless of settings.\n * Users can add additional patterns via settings.forbiddenCommands.\n *\n * Commands are split on shell operators (&&, ||, ;, |, &) and each segment\n * is checked independently. Default patterns are anchored to the start of\n * each segment (^) so they only match commands that *begin with* the dangerous\n * command, not commands that merely *mention* the pattern in string literals\n * or arguments.\n *\n * To avoid false positives from operators inside quoted strings, content\n * within single/double quotes is masked before splitting. To catch subshell\n * wrappers like $(cmd) and (cmd), leading wrapper characters are stripped\n * from each segment before pattern matching.\n */\n\n/** Hardcoded patterns that are always active. Always anchored with ^. */\nconst DEFAULT_FORBIDDEN_PATTERNS: string[] = [\n\t\"^gh pr merge.*--admin\", // bypass branch protection\n\t\"^git push.*(-f\\\\b|--force)\", // force push (includes --force-with-lease)\n\t\"^gh api.*bypass\", // API calls with bypass flag\n\t\"^(?:export\\\\s+)?HUSKY=0\", // bypass pre-commit hooks (anchored with optional export prefix)\n\t\"^git\\\\s+commit.*--no-verify\", // bypass pre-commit hooks via --no-verify flag\n\t\"^(?:export\\\\s+)?SKIP_?VALIDATION=1\", // bypass pre-commit hooks via SKIP_VALIDATION env var\n\t\"^rm\\\\s+.*--no-preserve-root\", // rm with explicit safety override\n\t\"^rm\\\\s+.*\\\\s[\\\"']?/(\\\\*|[\\\\w.-]+/?)?[\\\"']?(\\\\s|$)\", // rm targeting root or top-level dirs (/, /*, /home, /etc)\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // dd writing to block devices\n\t\"^mkfs\", // format filesystem (mkfs.ext4, mkfs.xfs, etc.)\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\", // redirect to block device (> and >>)\n];\n\n/**\n * Patterns checked against the full (quote-masked) command string before\n * splitting into segments. These catch dangerous constructs that span\n * shell operators and would be fragmented by the segment splitter.\n *\n * Matched against the masked string so quoted content doesn't trigger\n * false positives (e.g., `echo \":(){ :|:& };:\"` is safe).\n */\nconst FULL_COMMAND_PATTERNS: string[] = [\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb :(){ :|:& };:\n];\n\n/**\n * Patterns also checked against content extracted from within quoted strings.\n * Catches commands like `echo \"rm -rf /\"` where the quoted content is a\n * destructive command that could be piped to execution via `| bash`.\n *\n * These are intentionally limited to destructive/dangerous patterns — env var\n * patterns like HUSKY=0 are excluded because they appear legitimately in\n * contexts like `git log --grep=\"HUSKY=0\"`.\n *\n * The fork bomb pattern from FULL_COMMAND_PATTERNS is included here because\n * it also needs to be caught when quoted (e.g., `echo \":(){ :|:& };:\"`).\n */\nconst QUOTED_CONTENT_PATTERNS: string[] = [\n\t\"^rm\\\\s+.*--no-preserve-root\",\n\t\"^rm\\\\s+.*\\\\s/(\\\\*|[\\\\w.-]+/?)?(\\\\s|$)\",\n\t\"^dd\\\\s+.*of=/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^mkfs\",\n\t\"^>>?\\\\s*/dev/(sd|hd|vd|nvme|xvd|loop|mmcblk|disk)\",\n\t\"^gh pr merge.*--admin\",\n\t\"^git push.*(-f\\\\b|--force)\",\n\t\"^gh api.*bypass\",\n\t\"^git\\\\s+commit.*--no-verify\",\n\t\":\\\\(\\\\)\\\\s*\\\\{\", // fork bomb\n];\n\n/**\n * Mask content inside single and double-quoted strings by replacing\n * characters within quotes with underscores. This prevents shell operators\n * inside quoted strings from causing false splits.\n *\n * Handles escaped quotes (\\\", \\') within strings. Correctly counts\n * consecutive backslashes before a quote — an even count means the quote\n * is real (e.g. `\\\\\"` is escaped-backslash + closing quote).\n */\nfunction maskQuotedContent(command: string): string {\n\tlet result = \"\";\n\tlet inSingle = false;\n\tlet inDouble = false;\n\n\tfor (let i = 0; i < command.length; i++) {\n\t\tconst ch = command[i];\n\n\t\tif (ch === \"'\" && !inDouble) {\n\t\t\t// In bash, single-quoted strings are completely literal — backslashes\n\t\t\t// have no escape function inside single quotes. Always toggle.\n\t\t\tinSingle = !inSingle;\n\t\t\tresult += ch;\n\t\t} else if (ch === '\"' && !inSingle) {\n\t\t\tif (!isEscaped(command, i)) {\n\t\t\t\tinDouble = !inDouble;\n\t\t\t}\n\t\t\tresult += ch;\n\t\t} else if (inSingle || inDouble) {\n\t\t\t// Replace content inside quotes with a safe character\n\t\t\t// that won't match shell operators\n\t\t\tresult += ch === \"\\n\" ? \"\\n\" : \"_\";\n\t\t} else {\n\t\t\tresult += ch;\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/**\n * Check if the character at position `i` is escaped by counting consecutive\n * trailing backslashes. If the count is odd, the character is escaped.\n * If even (including zero), it is not escaped.\n *\n * e.g. `\\\\\"` → 2 backslashes → even → `\"` is NOT escaped (real quote)\n * `\\\\\\\"` → 3 backslashes → odd → `\"` IS escaped (literal quote)\n */\nfunction isEscaped(str: string, i: number): boolean {\n\tlet count = 0;\n\tlet j = i - 1;\n\twhile (j >= 0 && str[j] === \"\\\\\") {\n\t\tcount++;\n\t\tj--;\n\t}\n\treturn count % 2 === 1;\n}\n\n/**\n * Extract text content from within quoted strings in a segment.\n * Used to catch commands like `echo \"rm -rf /\" | bash` where dangerous\n * content is hidden inside quotes. The normal segment check won't catch\n * this because `echo` (not `rm`) starts the segment. By extracting the\n * quoted content and checking it separately, we block segments that\n * contain forbidden commands in their quoted arguments.\n */\nfunction extractQuotedContent(text: string): string[] {\n\tconst results: string[] = [];\n\tlet inQuote: string | null = null;\n\tlet start = -1;\n\n\tfor (let i = 0; i < text.length; i++) {\n\t\tconst ch = text[i];\n\t\tif ((ch === '\"' || ch === \"'\") && (ch === \"'\" || !isEscaped(text, i))) {\n\t\t\tif (inQuote === null) {\n\t\t\t\tinQuote = ch;\n\t\t\t\tstart = i + 1;\n\t\t\t} else if (ch === inQuote) {\n\t\t\t\tconst content = text.substring(start, i).trim();\n\t\t\t\tif (content.length > 0) {\n\t\t\t\t\tresults.push(content);\n\t\t\t\t}\n\t\t\t\tinQuote = null;\n\t\t\t}\n\t\t}\n\t}\n\treturn results;\n}\n\n/**\n * Split a command string into individual segments on shell operators.\n *\n * Handles: &&, ||, ;, |, & (background), and newlines.\n * Content inside single/double quotes is masked before splitting so that\n * operators inside quoted strings don't cause false splits.\n * Each segment is trimmed of leading whitespace.\n */\nfunction splitCommandSegments(command: string): string[] {\n\t// Mask quoted content to avoid splitting on operators inside strings\n\tconst masked = maskQuotedContent(command);\n\n\t// Split on shell operators: &&, ||, ;, |, &, and newlines\n\tconst splits = masked.split(/\\s*(?:&&|\\|\\||[;&|]|\\n)\\s*/);\n\n\t// Map split positions back to original command segments.\n\t// We split the masked string to find operator positions, but return\n\t// the original (unmasked) segments so pattern matching sees real text.\n\tconst originalSegments: string[] = [];\n\tlet maskedIdx = 0;\n\n\tfor (const part of splits) {\n\t\t// Find the start of this part in the masked string\n\t\tconst startInMasked = masked.indexOf(part, maskedIdx);\n\t\tif (startInMasked === -1) {\n\t\t\t// Fallback: use the part as-is (shouldn't happen)\n\t\t\toriginalSegments.push(command.substring(maskedIdx, maskedIdx + part.length).trim());\n\t\t} else {\n\t\t\toriginalSegments.push(command.substring(startInMasked, startInMasked + part.length).trim());\n\t\t}\n\t\tmaskedIdx = startInMasked + part.length;\n\t}\n\n\treturn originalSegments.filter((s) => s.length > 0);\n}\n\n/**\n * Strip leading subshell/command-substitution wrappers from a segment\n * so that $(cmd), (cmd), and `cmd` are checked against patterns too.\n *\n * Handles both full-segment wrappers ($(cmd)) and inline substitutions\n * (result=$(cmd)) by extracting inner commands.\n */\nfunction stripSubshellWrapper(segment: string): string {\n\t// Strip $(...) wrapper when it's the whole segment\n\tif (/^\\$\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(2, -1).trim();\n\t}\n\t// Strip (...) wrapper (subshell) when it's the whole segment\n\tif (/^\\(/.test(segment) && segment.endsWith(\")\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Strip backtick wrapper when it's the whole segment\n\tif (/^`/.test(segment) && segment.endsWith(\"`\")) {\n\t\treturn segment.slice(1, -1).trim();\n\t}\n\t// Extract inner command from inline $() or backtick substitutions\n\t// e.g., \"result=$(git push --force)\" → \"git push --force\"\n\tconst inlineMatch = segment.match(/\\$\\(([^)]+)\\)/);\n\tif (inlineMatch) {\n\t\treturn inlineMatch[1].trim();\n\t}\n\tconst backtickMatch = segment.match(/`([^`]+)`/);\n\tif (backtickMatch) {\n\t\treturn backtickMatch[1].trim();\n\t}\n\treturn segment;\n}\n\n/**\n * Check whether a command matches any forbidden pattern.\n *\n * The command is split on shell operators (&&, ||, ;, |) with quoted content\n * masked to avoid false splits. Each segment is then stripped of subshell\n * wrappers ($(...), (...), `...`) and checked against patterns. Default\n * patterns are ^-anchored so they only match commands that start with the\n * dangerous command prefix.\n *\n * @returns The first matching pattern, or `undefined` if the command is allowed.\n */\nexport function isForbiddenCommand(command: string, extraPatterns?: string[]): string | undefined {\n\t// Guard against misconfigured settings (string instead of array)\n\tconst validatedExtras = Array.isArray(extraPatterns) ? extraPatterns : undefined;\n\tconst allPatterns = validatedExtras\n\t\t? [...DEFAULT_FORBIDDEN_PATTERNS, ...validatedExtras]\n\t\t: DEFAULT_FORBIDDEN_PATTERNS;\n\n\t// Pre-split check: match full-command patterns against the quote-masked\n\t// string to catch constructs that span shell operators (e.g., fork bombs).\n\t// Using the masked string prevents false positives from quoted content.\n\tconst masked = maskQuotedContent(command);\n\tfor (const pattern of FULL_COMMAND_PATTERNS) {\n\t\ttry {\n\t\t\tconst re = new RegExp(pattern);\n\t\t\tif (re.test(masked)) {\n\t\t\t\treturn pattern;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Invalid regex — skip\n\t\t}\n\t}\n\n\tconst segments = splitCommandSegments(command);\n\n\t// Combine quoted-content patterns with any user extras for quoted checking\n\tconst allQuotedPatterns = validatedExtras\n\t\t? [...QUOTED_CONTENT_PATTERNS, ...validatedExtras]\n\t\t: QUOTED_CONTENT_PATTERNS;\n\n\tfor (const segment of segments) {\n\t\t// Check both the raw segment and the subshell-unwrapped version\n\t\tconst toCheck = [segment, stripSubshellWrapper(segment)];\n\t\tfor (const text of toCheck) {\n\t\t\tfor (const pattern of allPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(text)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex in user settings — skip it\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check content within quotes for embedded dangerous commands.\n\t\t// There is no legitimate reason for an agent to output/echo forbidden\n\t\t// commands, and quoted content could be piped to execution via | bash.\n\t\tconst quotedContent = extractQuotedContent(segment);\n\t\tfor (const content of quotedContent) {\n\t\t\tfor (const pattern of allQuotedPatterns) {\n\t\t\t\ttry {\n\t\t\t\t\tconst re = new RegExp(pattern);\n\t\t\t\t\tif (re.test(content)) {\n\t\t\t\t\t\treturn pattern;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Invalid regex — skip\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn undefined;\n}\n\n/**\n * Extract file paths from a command that executes a script file.\n * Detects: `bash file`, `sh file`, `source file`, `. file`, and input\n * redirects like `bash < file`.\n *\n * Returns an array of file paths (usually 0 or 1). Does not check whether\n * the files exist — the caller handles that.\n *\n * @returns Array of script file paths referenced by the command.\n */\nexport function extractScriptPaths(command: string): string[] {\n\tconst paths: string[] = [];\n\tconst segments = splitCommandSegments(command);\n\n\tfor (const segment of segments) {\n\t\tconst trimmed = segment.trim();\n\n\t\t// bash < file.sh (input redirect) — check before shell exec to avoid\n\t\t// the shell exec regex matching \"<\" as a filename\n\t\tconst redirectMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)(?:\\s+-\\S+)*\\s+<\\s*(\\S+)/);\n\t\tif (redirectMatch?.[1]) {\n\t\t\tpaths.push(redirectMatch[1]);\n\t\t\tcontinue;\n\t\t}\n\n\t\t// bash [flags] file.sh, sh [flags] file.sh\n\t\t// Flags are short options like -x, -e, -ex, etc.\n\t\t// Exclude -c (inline command — handled by quoted content check)\n\t\tif (/^(?:bash|sh|zsh|ksh)\\s+-c\\b/.test(trimmed)) {\n\t\t\tcontinue;\n\t\t}\n\t\tconst shellExecMatch = trimmed.match(/^(?:bash|sh|zsh|ksh)\\s+(?:-\\S+\\s+)*(\\S+)/);\n\t\tif (shellExecMatch) {\n\t\t\tconst filePath = shellExecMatch[1];\n\t\t\tif (filePath && !filePath.startsWith(\"-\")) {\n\t\t\t\tpaths.push(filePath);\n\t\t\t}\n\t\t}\n\n\t\t// source file.sh, . file.sh\n\t\tconst sourceMatch = trimmed.match(/^(?:source|\\.)\\s+(\\S+)/);\n\t\tif (sourceMatch?.[1]) {\n\t\t\tpaths.push(sourceMatch[1]);\n\t\t}\n\t}\n\n\treturn [...new Set(paths)]; // deduplicate\n}\n\n/**\n * Check file content line-by-line for forbidden commands.\n * Each non-empty, non-comment line is passed through `isForbiddenCommand`.\n *\n * This is a pure function — the caller is responsible for reading the file\n * and passing the content string.\n *\n * @returns The first match with pattern, line number, and line text, or undefined.\n */\nexport function checkScriptContent(\n\tcontent: string,\n\textraPatterns?: string[],\n): { pattern: string; line: number; text: string } | undefined {\n\tconst lines = content.split(\"\\n\");\n\n\tfor (let i = 0; i < lines.length; i++) {\n\t\tconst line = lines[i].trim();\n\n\t\t// Skip empty lines and comments\n\t\tif (!line || line.startsWith(\"#\")) continue;\n\n\t\tconst pattern = isForbiddenCommand(line, extraPatterns);\n\t\tif (pattern) {\n\t\t\treturn { pattern, line: i + 1, text: line };\n\t\t}\n\t}\n\n\treturn undefined;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dreb/coding-agent",
3
- "version": "2.6.3",
3
+ "version": "2.8.0",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "drebConfig": {