src/vs/platform/agentHost/node/commandAutoApprover.ts

595 LOC · 559 covered · 36 uncovered · 111 ranges · 1151 concepts · 39 introducers · 553 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- commandAutoApprover.ts ×14
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import type { Language, Parser, Query, QueryCapture } from '@vscode/tree-sitter-wasm';
7 > import * as fs from 'fs';
8 > import { Disposable, toDisposable } from '../../../base/common/lifecycle.js';
9 > import { FileAccess } from '../../../base/common/network.js';
10 > import { escapeRegExpCharacters, regExpLeadsToEndlessLoop } from '../../../base/common/strings.js';
11 > import { URI } from '../../../base/common/uri.js';
12 > import { getAppNodeModulesPath } from './appNodeModules.js';
13 > import { ILogService } from '../../log/common/log.js';
14 > import type { AgentHostTerminalAutoApproveRuleValue, AgentHostTerminalAutoApproveRules } from '../common/agentHostSchema.js';
15 >
16 > /**
17 > * Redirect destinations that do not result in a write to an arbitrary file
18 > * on disk: the /dev sinks that discard output (`/dev/null`) or write back to
19 > * the same terminal (`/dev/stdout`, `/dev/stderr`, `/dev/tty`).
20 > */
21 > const SAFE_REDIRECT_TARGETS: ReadonlySet<string> = new Set([
22 > '/dev/null',
23 > '/dev/stdout',
24 > '/dev/stderr',
25 > '/dev/tty',
26 > ]);
27 >
28 > /**
29 > * Returns true when the given redirection destination is known to be safe:
30 > * either a known-safe /dev sink or a file-descriptor duplication target
31 > * like `&1` (used in `2>&1`).
32 > */
33 > function isSafeRedirectDestination(dest: string): boolean { commandAutoApprover.ts ×6
34 > let cleaned = dest.trim();
35 > if (cleaned.length === 0) {
36 return false;
37 }
38 > if ((cleaned.startsWith(`'`) && cleaned.endsWith(`'`)) || commandAutoApprover.ts ×6
39 > (cleaned.startsWith('"') && cleaned.endsWith('"'))) {
40 > cleaned = cleaned.slice(1, -1); commandAutoApprover.ts ×1
41 > }
42 > // File-descriptor duplication: `&N`, optionally followed by `-` to close. commandAutoApprover.ts ×6
43 > if (/^&[0-9]+-?$/.test(cleaned)) {
44 > return true; commandAutoApprover.ts ×1
45 > }
46 > return SAFE_REDIRECT_TARGETS.has(cleaned); commandAutoApprover.ts ×6
47 > }
49 > /**
50 > * Classification of a tree-sitter `file_redirect` node.
51 > * - `read`: input-only redirect (`<`, `<&N`) — never writes.
52 > * - `safeWrite`: write to a known-safe sink (`/dev/null`, fd duplication, ...).
53 > * - `unsafeWrite`: write to an arbitrary destination. The destination string
54 > * (with surrounding quotes stripped) is included when it could be parsed,
55 > * so the caller may decide whether the target is acceptable.
56 > */
57 > type FileRedirectClassification =
58 > | { kind: 'read' }
59 > | { kind: 'safeWrite' }
60 > | { kind: 'unsafeWrite'; dest: string | undefined };
61 >
62 > function classifyFileRedirect(redirectText: string): FileRedirectClassification { commandAutoApprover.ts ×4
63 > if (!redirectText.includes('>')) {
64 > return { kind: 'read' }; commandAutoApprover.ts ×1
65 > }
66 > const destMatch = redirectText.match(/(?:[0-9]+|&)?>>?\|?\s*(.+)$/); commandAutoApprover.ts ×6
67 > if (!destMatch) {
68 return { kind: 'unsafeWrite', dest: undefined };
69 }
70 > const rawDest = destMatch[1].trim(); commandAutoApprover.ts ×6
71 > if (isSafeRedirectDestination(rawDest)) {
72 > return { kind: 'safeWrite' }; commandAutoApprover.ts ×1
73 > }
74 > let dest = rawDest; commandAutoApprover.ts ×4
75 > if ((dest.startsWith(`'`) && dest.endsWith(`'`)) ||
76 > (dest.startsWith('"') && dest.endsWith('"'))) { commandAutoApprover.ts ×4
77 > dest = dest.slice(1, -1); commandAutoApprover.ts ×1
78 > }
79 > return { kind: 'unsafeWrite', dest }; commandAutoApprover.ts ×4
80 > }
82 > /**
83 > * Result of a command auto-approval check.
84 > * - `approved`: all sub-commands match allow rules and none are denied
85 > * - `denied`: at least one sub-command matches a deny rule
86 > * - `noMatch`: no rule matched — requires user confirmation
87 > */
88 > export type CommandApprovalResult = 'approved' | 'denied' | 'noMatch';
89 >
90 > /** Options for {@link CommandAutoApprover.shouldAutoApprove}. */
91 > export interface IShouldAutoApproveOptions {
92 > /**
93 > * Predicate that decides whether a write redirection to the given
94 > * destination is acceptable. Called once per write-redirect destination
95 > * found in the command line; the destination is the raw string the user
96 > * typed (with surrounding quotes stripped). The predicate is responsible
97 > * for resolving relative paths and applying its own policy.
98 > *
99 > * When omitted, any write redirect to a destination outside the known-safe
100 > * sinks (e.g. `/dev/null`) downgrades the result to `noMatch`.
101 > */
102 > readonly isWriteDestApproved?: (dest: string) => boolean;
103 > /**
104 > * Effective VS Code `chat.tools.terminal.autoApprove` rules forwarded from
105 > * the renderer. When omitted, the agent host falls back to its bundled
106 > * default rules for compatibility with older clients.
107 > */
108 > readonly autoApproveRules?: AgentHostTerminalAutoApproveRules;
109 > }
110 >
111 > interface IAutoApproveRule {
112 > readonly regex: RegExp;
113 > }
114 >
115 > interface IAutoApproveRules {
116 > readonly allowRules: IAutoApproveRule[];
117 > readonly denyRules: IAutoApproveRule[];
118 > readonly allowCommandLineRules: IAutoApproveRule[];
119 > readonly denyCommandLineRules: IAutoApproveRule[];
120 > }
121 >
122 > const neverMatchRegex = /(?!.*)/;
123 > const transientEnvVarRegex = /^[A-Z_][A-Z0-9_]*=/i;
124 >
125 > /**
126 > * Auto-approves or denies shell commands based on terminal auto-approve rules.
127 > *
128 > * Uses tree-sitter to parse compound commands (`foo && bar`) into
129 > * sub-commands that are individually checked against allow/deny lists.
130 > * The rules are normally forwarded from VS Code's
131 > * `chat.tools.terminal.autoApprove` setting. A bundled default table is kept
132 > * as a compatibility fallback for clients that have not forwarded rules yet.
133 > *
134 > * Tree-sitter is initialized eagerly; call {@link initialize} and await the
135 > * result before using {@link shouldAutoApprove} to guarantee synchronous
136 > * parsing. If tree-sitter fails to load or parse the command,
137 > * {@link shouldAutoApprove} returns `noMatch` so the user is prompted for
138 > * confirmation rather than auto-approving based on the command name alone.
139 > */
140 > export class CommandAutoApprover extends Disposable {
141 >
142 > private _fallbackRules: IAutoApproveRules | undefined;
143 > private _cachedRuleConfig: AgentHostTerminalAutoApproveRules | undefined;
144 > private _cachedRules: IAutoApproveRules | undefined;
145 > private _parser: Parser | undefined;
146 > private _bashLanguage: Language | undefined;
147 > private _queryClass: typeof Query | undefined;
148 > private readonly _initPromise: Promise<void>;
149 >
150 > constructor(
151 > private readonly _logService: ILogService, commandAutoApprover.ts ×4
152 > ) {
153 > super();
154 > this._initPromise = this._initTreeSitter();
155 > }
157 > /**
158 > * Returns a promise that resolves once tree-sitter WASM has been loaded.
159 > * Await this before processing any events to guarantee that
160 > * {@link shouldAutoApprove} can parse commands synchronously.
161 > */
162 > initialize(): Promise<void> {
163 > return this._initPromise; commandAutoApprover.ts ×1
164 > }
166 > /**
167 > * Synchronously check whether the given command line should be auto-approved.
168 > * Uses tree-sitter (if loaded) to parse compound commands into sub-commands.
169 > *
170 > * When the command contains write redirections, `options.isWriteDestApproved`
171 > * is consulted for each destination. If every destination is approved by the
172 > * predicate, write redirections do not block auto-approval.
173 > */
174 > shouldAutoApprove(commandLine: string, options?: IShouldAutoApproveOptions): CommandApprovalResult {
175 > const trimmed = commandLine.trimStart(); commandAutoApprover.ts ×5
176 > if (trimmed.length === 0) {
177 > return 'approved'; commandAutoApprover.ts ×1
178 > }
180 > const rules = this._compileRules(options?.autoApproveRules); commandAutoApprover.ts ×5
181 >
182 > const parsed = this._extractSubCommands(trimmed);
183 > if (!parsed) {
184 this._logService.trace('[CommandAutoApprover] Tree-sitter unavailable, requiring confirmation');
185 return 'noMatch';
186 }
188 > if (this._matchesRule(trimmed, rules.denyCommandLineRules)) {
189 > return 'denied'; commandAutoApprover.ts ×5
190 > }
192 > let result = this._matchSubCommands(parsed.subCommands, rules);
193 > if (result !== 'denied' && this._matchesRule(trimmed, rules.allowCommandLineRules)) { commandAutoApprover.ts ×5
194 > result = 'approved'; commandAutoApprover.ts ×5
195 > }
196 > if (result === 'approved' && parsed.unsafeWriteDests.length > 0) { commandAutoApprover.ts ×5
197 > for (const dest of parsed.unsafeWriteDests) { commandAutoApprover.ts ×4
198 > if (dest === undefined || !options?.isWriteDestApproved?.(dest)) {
199 > this._logService.trace('[CommandAutoApprover] Write redirection to non-approved destination, requiring confirmation');
200 > return 'noMatch';
201 > }
202 > }
204 > return result; commandAutoApprover.ts ×1
207 > private _matchSubCommands(subCommands: string[], rules: IAutoApproveRules): CommandApprovalResult {
208 > let allApproved = true; commandAutoApprover.ts ×17
209 > for (const subCommand of subCommands) {
210 > // Deny transient env var assignments
211 > if (transientEnvVarRegex.test(subCommand)) {
212 > return 'denied'; commandAutoApprover.ts ×1
213 > }
215 > const result = this._matchSingleCommand(subCommand, rules);
216 > if (result === 'denied') {
217 > return 'denied'; commandAutoApprover.ts ×2
218 > }
219 > if (result !== 'approved') { commandAutoApprover.ts ×2
220 > allApproved = false; commandAutoApprover.ts ×2
221 > }
223 > return allApproved ? 'approved' : 'noMatch';
224 > }
226 > private _matchSingleCommand(command: string, rules: IAutoApproveRules): CommandApprovalResult {
227 > // Check deny rules first commandAutoApprover.ts ×3
228 > if (this._matchesRule(command, rules.denyRules)) {
229 > return 'denied'; commandAutoApprover.ts ×2
230 > }
232 > // Then check allow rules
233 > if (this._matchesRule(command, rules.allowRules)) {
234 > return 'approved'; commandAutoApprover.ts ×1
235 > }
237 > return 'noMatch';
240 > private _matchesRule(command: string, rules: readonly IAutoApproveRule[]): boolean {
241 > for (const rule of rules) { commandAutoApprover.ts ×17
242 > if (rule.regex.test(command)) { commandAutoApprover.ts ×2
243 > return true; commandAutoApprover.ts ×1
244 > }
246 > return false; commandAutoApprover.ts ×17
247 > }
249 > // ---- Tree-sitter --------------------------------------------------------
250 >
251 > private _extractSubCommands(commandLine: string): { subCommands: string[]; unsafeWriteDests: (string | undefined)[] } | undefined {
252 > if (!this._parser || !this._bashLanguage || !this._queryClass) { commandAutoApprover.ts ×17
253 return undefined;
254 }
256 > try {
257 > this._parser.setLanguage(this._bashLanguage);
258 > const tree = this._parser.parse(commandLine);
259 > if (!tree) {
260 return undefined;
261 }
263 > try {
264 > const query = new this._queryClass(this._bashLanguage, '(command) @command (file_redirect) @file_redirect (heredoc_redirect) @heredoc_redirect (herestring_redirect) @herestring_redirect');
265 > const captures: QueryCapture[] = query.captures(tree.rootNode);
266 > const subCommands: string[] = [];
267 > const unsafeWriteDests: (string | undefined)[] = [];
268 > for (const capture of captures) {
269 > if (capture.name === 'command') {
270 > subCommands.push(capture.node.text);
271 > } else if (capture.name === 'file_redirect') {
272 > // Writes to known-safe sinks (e.g. `> /dev/null`) and commandAutoApprover.ts ×4
273 > // file-descriptor duplications (e.g. `2>&1`) are allowed.
274 > const cls = classifyFileRedirect(capture.node.text);
275 > if (cls.kind === 'unsafeWrite') {
276 > unsafeWriteDests.push(cls.dest); commandAutoApprover.ts ×4
277 > }
278 > } else if (capture.name === 'heredoc_redirect' || capture.name === 'herestring_redirect') { commandAutoApprover.ts ×4
279 > // Heredoc/herestring feed data into stdin; they do not write commandAutoApprover.ts ×1
280 > // files, so they are not treated as write redirects here.
281 > }
283 > query.delete();
284 > return subCommands.length > 0 || unsafeWriteDests.length > 0 ? { subCommands, unsafeWriteDests } : undefined;
285 > } finally {
286 > tree.delete();
287 > }
288 > } catch (err) {
289 this._logService.warn('[CommandAutoApprover] Tree-sitter parsing failed', err);
290 return undefined;
291 }
294 > private async _initTreeSitter(): Promise<void> {
296 > const { default: TreeSitter } = (await import('@vscode/tree-sitter-wasm'));
297 >
298 > if (this._store.isDisposed) {
299 return;
300 }
302 > // Resolve WASM files from node_modules. In the desktop app the `.wasm`
303 > // files are unpacked next to the ASAR archive (`node_modules.asar.unpacked`),
304 > // while in dev and on the server (which has no ASAR) they live in a plain
305 > // `node_modules`.
306 > const moduleRoot = URI.joinPath(FileAccess.asFileUri(getAppNodeModulesPath()), '@vscode', 'tree-sitter-wasm', 'wasm');
307 > const wasmPath = URI.joinPath(moduleRoot, 'tree-sitter.wasm').fsPath;
308 >
309 > await TreeSitter.Parser.init({
310 > locateFile() {
311 > return wasmPath;
312 > }
313 > });
315 > if (this._store.isDisposed) {
317 > }
319 > const parser = new TreeSitter.Parser();
320 > this._register(toDisposable(() => {
321 > try {
322 > parser.delete();
323 > } catch {
324 > // WASM memory may already be freed commandAutoApprover.ts ×1
325 > }
327 >
328 > // Load bash grammar
329 > const bashWasmPath = URI.joinPath(moduleRoot, 'tree-sitter-bash.wasm').fsPath;
330 > const bashWasm = await fs.promises.readFile(bashWasmPath);
332 > if (this._store.isDisposed) {
333 > return; agentService.ts ×1
334 > }
336 > const bashLanguage = await TreeSitter.Language.load(new Uint8Array(bashWasm.buffer, bashWasm.byteOffset, bashWasm.byteLength));
337 >
338 > if (this._store.isDisposed) {
339 > return; agentService.ts ×3
340 > }
342 > this._parser = parser;
343 > this._bashLanguage = bashLanguage;
344 > this._queryClass = TreeSitter.Query;
345 > this._logService.info('[CommandAutoApprover] Tree-sitter initialized successfully');
346 > } catch (err) {
347 this._logService.warn('[CommandAutoApprover] Failed to initialize tree-sitter', err);
348 }
351 > // ---- Rules --------------------------------------------------------------
352 >
353 > private _compileRules(ruleConfig: AgentHostTerminalAutoApproveRules | undefined): IAutoApproveRules {
354 > if (!ruleConfig) { commandAutoApprover.ts ×17
355 > if (!this._fallbackRules) { commandAutoApprover.ts ×2
356 > this._fallbackRules = this._compileRuleEntries(DEFAULT_TERMINAL_AUTO_APPROVE_RULES);
357 > }
358 > return this._fallbackRules;
359 > }
361 > if (this._cachedRuleConfig === ruleConfig && this._cachedRules) { commandAutoApprover.ts ×17
362 return this._cachedRules;
363 }
365 > this._cachedRuleConfig = ruleConfig;
366 > this._cachedRules = this._compileRuleEntries(ruleConfig);
367 > return this._cachedRules;
370 > private _compileRuleEntries(ruleConfig: Readonly<Record<string, AgentHostTerminalAutoApproveRuleValue>>): IAutoApproveRules {
371 > const allowRules: IAutoApproveRule[] = []; commandAutoApprover.ts ×17
372 > const denyRules: IAutoApproveRule[] = [];
373 > const allowCommandLineRules: IAutoApproveRule[] = [];
374 > const denyCommandLineRules: IAutoApproveRule[] = [];
375 >
376 > for (const [key, value] of Object.entries(ruleConfig)) {
377 > const regex = convertAutoApproveEntryToRegex(key); commandAutoApprover.ts ×7
378 > if (value === true) {
379 > allowRules.push({ regex }); commandAutoApprover.ts ×1
380 > } else if (value === false) { commandAutoApprover.ts ×7
381 > denyRules.push({ regex }); commandAutoApprover.ts ×1
382 > } else if (value && typeof value === 'object' && typeof value.approve === 'boolean') {
383 > if (value.approve) { commandAutoApprover.ts ×5
384 > if (value.matchCommandLine === true) {
385 > allowCommandLineRules.push({ regex });
386 > } else {
387 allowRules.push({ regex });
388 }
390 > if (value.matchCommandLine === true) {
391 > denyCommandLineRules.push({ regex });
392 > } else {
393 denyRules.push({ regex });
394 }
396 > }
399 > return { allowRules, denyRules, allowCommandLineRules, denyCommandLineRules };
400 > }
402 >
403 > // ---- Regex conversion -------------------------------------------------------
404 >
405 > function convertAutoApproveEntryToRegex(value: string): RegExp { commandAutoApprover.ts ×7
406 > // If wrapped in `/`, treat as regex
407 > const regexMatch = value.match(/^\/(?<pattern>.+)\/(?<flags>[dgimsuvy]*)$/);
408 > const regexPattern = regexMatch?.groups?.pattern;
409 > if (regexPattern) {
410 > let flags = regexMatch.groups?.flags; commandAutoApprover.ts ×5
411 > if (flags) {
412 > flags = flags.replaceAll('g', ''); commandAutoApprover.ts ×2
413 > }
415 > if (regexPattern === '.*') {
416 return new RegExp(regexPattern);
417 }
419 > try {
420 > const regex = new RegExp(regexPattern, flags || undefined);
421 > if (regExpLeadsToEndlessLoop(regex)) {
422 return neverMatchRegex;
423 }
424 > return regex; commandAutoApprover.ts ×5
425 > } catch {
426 return neverMatchRegex;
427 }
430 > if (value === '') {
431 return neverMatchRegex;
432 }
434 > let sanitizedValue: string;
435 >
436 > // Match both path separators if it looks like a path
437 > if (value.includes('/') || value.includes('\\')) {
438 let pattern = value.replace(/[/\\]/g, '%%PATH_SEP%%');
439 pattern = escapeRegExpCharacters(pattern);
440 pattern = pattern.replace(/%%PATH_SEP%%*/g, '[/\\\\]');
441 sanitizedValue = `^(?:\\.[/\\\\])?${pattern}`;
443 > sanitizedValue = escapeRegExpCharacters(value);
444 > }
445 >
446 > return new RegExp(`^${sanitizedValue}\\b`);
447 > }
449 > // ---- Default rules ----------------------------------------------------------
450 > //
451 > // Compatibility fallback for clients that do not forward the VS Code
452 > // `chat.tools.terminal.autoApprove` setting.
453 > // TODO: Remove this fallback once all agent-host clients are guaranteed to
454 > // forward `chat.tools.terminal.autoApprove` before shell approvals run.
455 >
456 > const DEFAULT_TERMINAL_AUTO_APPROVE_RULES: Readonly<Record<string, AgentHostTerminalAutoApproveRuleValue>> = {
457 > // Safe readonly commands
458 > cd: true,
459 > echo: true,
460 > ls: true,
461 > dir: true,
462 > pwd: true,
463 > cat: true,
464 > head: true,
465 > tail: true,
466 > findstr: true,
467 > wc: true,
468 > tr: true,
469 > cut: true,
470 > cmp: true,
471 > which: true,
472 > basename: true,
473 > dirname: true,
474 > realpath: true,
475 > readlink: true,
476 > stat: true,
477 > file: true,
478 > od: true,
479 > du: true,
480 > df: true,
481 > sleep: true,
482 > nl: true,
483 >
484 > grep: true,
485 >
486 > // Safe git sub-commands
487 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+status\\b/': true,
488 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+log\\b/': true,
489 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+log\\b.*\\s--output(=|\\s|$)/': false,
490 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+show\\b/': true,
491 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+diff\\b/': true,
492 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+ls-files\\b/': true,
493 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+grep\\b/': true,
494 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+branch\\b/': true,
495 > '/^git(\\s+(-C\\s+\\S+|--no-pager))*\\s+branch\\b.*\\s-(d|D|m|M|-delete|-force)\\b/': false,
496 >
497 > // Docker readonly sub-commands
498 > '/^docker\\s+(ps|images|info|version|inspect|logs|top|stats|port|diff|search|events)\\b/': true,
499 > '/^docker\\s+(container|image|network|volume|context|system)\\s+(ls|ps|inspect|history|show|df|info)\\b/': true,
500 > '/^docker\\s+compose\\s+(ps|ls|top|logs|images|config|version|port|events)\\b/': true,
501 >
502 > // PowerShell
503 > 'Get-ChildItem': true,
504 > 'Get-Content': true,
505 > 'Get-Date': true,
506 > 'Get-Random': true,
507 > 'Get-Location': true,
508 > 'Set-Location': true,
509 > 'Write-Host': true,
510 > 'Write-Output': true,
511 > 'Out-String': true,
512 > 'Split-Path': true,
513 > 'Join-Path': true,
514 > 'Start-Sleep': true,
515 > 'Where-Object': true,
516 > '/^Select-[a-z0-9]/i': true,
517 > '/^Measure-[a-z0-9]/i': true,
518 > '/^Compare-[a-z0-9]/i': true,
519 > '/^Format-[a-z0-9]/i': true,
520 > '/^Sort-[a-z0-9]/i': true,
521 >
522 > // Package manager read-only commands
523 > '/^npm\\s+(ls|list|outdated|view|info|show|explain|why|root|prefix|bin|search|doctor|fund|repo|bugs|docs|home|help(-search)?)\\b/': true,
524 > '/^npm\\s+config\\s+(list|get)\\b/': true,
525 > '/^npm\\s+pkg\\s+get\\b/': true,
526 > '/^npm\\s+audit$/': true,
527 > '/^npm\\s+cache\\s+verify\\b/': true,
528 > '/^yarn\\s+(list|outdated|info|why|bin|help|versions)\\b/': true,
529 > '/^yarn\\s+licenses\\b/': true,
530 > '/^yarn\\s+audit\\b(?!.*\\bfix\\b)/': true,
531 > '/^yarn\\s+config\\s+(list|get)\\b/': true,
532 > '/^yarn\\s+cache\\s+dir\\b/': true,
533 > '/^pnpm\\s+(ls|list|outdated|why|root|bin|doctor)\\b/': true,
534 > '/^pnpm\\s+licenses\\b/': true,
535 > '/^pnpm\\s+audit\\b(?!.*\\bfix\\b)/': true,
536 > '/^pnpm\\s+config\\s+(list|get)\\b/': true,
537 >
538 > // Safe lockfile-only installs
539 > 'npm ci': true,
540 > '/^yarn\\s+install\\s+--frozen-lockfile\\b/': true,
541 > '/^pnpm\\s+install\\s+--frozen-lockfile\\b/': true,
542 >
543 > // Safe commands with dangerous arg blocking
544 > column: true,
545 > '/^column\\b.*\\s-c\\s+[0-9]{4,}/': false,
546 > date: true,
547 > '/^date\\b.*\\s(-s|--set)\\b/': false,
548 > find: true,
549 > '/^find\\b.*\\s-(delete|exec|execdir|fprint|fprintf|fls|ok|okdir)\\b/': false,
550 > rg: true,
551 > '/^rg\\b.*\\s(--pre|--hostname-bin)\\b/': false,
552 > sed: true,
553 > '/^sed\\b.*\\s(-[a-zA-Z]*(e|f)[a-zA-Z]*|--expression|--file)\\b/': false,
554 > '/^sed\\b.*s\\/.*\\/.*\\/[ew]/': false,
555 > '/^sed\\b.*;W/': false,
556 > sort: true,
557 > '/^sort\\b.*\\s-(o|S)\\b/': false,
558 > tree: true,
559 > '/^tree\\b.*\\s-o\\b/': false,
560 > '/^xxd$/': true,
561 > '/^xxd\\b(\\s+-\\S+)*\\s+[^-\\s]\\S*$/': true,
562 >
563 > // Dangerous commands
564 > rm: false,
565 > rmdir: false,
566 > del: false,
567 > 'Remove-Item': false,
568 > ri: false,
569 > rd: false,
570 > erase: false,
571 > dd: false,
572 > kill: false,
573 > ps: false,
574 > top: false,
575 > 'Stop-Process': false,
576 > spps: false,
577 > taskkill: false,
578 > 'taskkill.exe': false,
579 > curl: false,
580 > wget: false,
581 > 'Invoke-RestMethod': false,
582 > 'Invoke-WebRequest': false,
583 > irm: false,
584 > iwr: false,
585 > chmod: false,
586 > chown: false,
587 > 'Set-ItemProperty': false,
588 > sp: false,
589 > 'Set-Acl': false,
590 > jq: false,
591 > xargs: false,
592 > eval: false,
593 > 'Invoke-Expression': false,
594 > iex: false,
595 > };