Download packages/core/src/sandbox/utils/commandSafety.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 22.8 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/sandbox/utils/commandSafety.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/core/src/sandbox/utils/commandSafety.ts
-
curl -L -o commandSafety.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/core/src/sandbox/utils/commandSafety.ts
22.8 kB
| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import fs from 'node:fs'; | |
| import os from 'node:os'; | |
| import path from 'node:path'; | |
| import { parse as shellParse } from 'shell-quote'; | |
| import { | |
| extractStringFromParseEntry, | |
| initializeShellParsers, | |
| splitCommands, | |
| stripShellWrapper, | |
| } from '../../utils/shell-utils.js'; | |
| import { | |
| isSubpath, | |
| isTrustedSystemPath, | |
| resolveToRealPath, | |
| } from '../../utils/paths.js'; | |
| function isRipgrepCommand(cmd: string): boolean { | |
| const cmdBasename = path.basename(cmd); | |
| return cmdBasename === 'rg' || cmdBasename === 'rg.exe'; | |
| } | |
| function isTrustedCommandPath(cmd: string): boolean { | |
| if (!path.isAbsolute(cmd)) { | |
| return false; | |
| } | |
| try { | |
| const realPath = resolveToRealPath(cmd); | |
| return isTrustedSystemPath(realPath); | |
| } catch { | |
| return false; | |
| } | |
| } | |
| /** | |
| * Determines if a command is strictly approved for execution on macOS. | |
| * A command is approved if it's composed entirely of tools explicitly listed in `approvedTools` | |
| * OR if it's composed of known safe, read-only POSIX commands. | |
| * | |
| * @param command - The full command string to execute. | |
| * @param args - The arguments for the command. | |
| * @param approvedTools - A list of explicitly approved tool names (e.g., ['npm', 'git']). | |
| * @returns true if the command is strictly approved, false otherwise. | |
| */ | |
| export async function isStrictlyApproved( | |
| command: string, | |
| args: string[], | |
| approvedTools?: string[], | |
| cwd?: string, | |
| workspaceRoot?: string, | |
| ): Promise<boolean> { | |
| const tools = approvedTools ?? []; | |
| await initializeShellParsers(); | |
| const fullCmd = [command, ...args].join(' '); | |
| const stripped = stripShellWrapper(fullCmd); | |
| const pipelineCommands = splitCommands(stripped); | |
| // Fallback for simple commands or parsing failures | |
| if (pipelineCommands.length === 0) { | |
| // For simple commands, we check the root command. | |
| // If it's explicitly approved OR it's a known safe POSIX command, we allow it. | |
| return ( | |
| tools.includes(command) || | |
| isKnownSafeCommand([command, ...args], cwd, workspaceRoot) | |
| ); | |
| } | |
| // Check every segment of the pipeline | |
| return pipelineCommands.every((cmdString) => { | |
| const trimmed = cmdString.trim(); | |
| if (!trimmed) return true; | |
| const parsedArgs = shellParse(trimmed).map(extractStringFromParseEntry); | |
| if (parsedArgs.length === 0) return true; | |
| const root = parsedArgs[0]; | |
| // The segment is approved if the root tool is in the allowlist OR if the whole segment is safe. | |
| return ( | |
| tools.includes(root) || isKnownSafeCommand(parsedArgs, cwd, workspaceRoot) | |
| ); | |
| }); | |
| } | |
| /** | |
| * Checks whether an argument provided to a file-reading command points outside the workspace | |
| * or resolves via symbolic link to a target outside the workspace. | |
| */ | |
| function isPathEscapingWorkspace( | |
| arg: string, | |
| workspaceRoot: string, | |
| cwd: string, | |
| ): boolean { | |
| if (!arg || typeof arg !== 'string') return false; | |
| // Unresolved shell variables or backticks cannot be statically verified | |
| if (arg.includes('$') || arg.includes('`')) { | |
| return true; | |
| } | |
| let target: string; | |
| if (arg === '~' || arg.startsWith('~/') || arg.startsWith('~\\')) { | |
| const homeDir = os.homedir(); | |
| target = path.resolve(homeDir, arg.slice(2)); | |
| if (!isSubpath(workspaceRoot, target)) { | |
| return true; | |
| } | |
| } else if (arg.startsWith('~')) { | |
| return true; | |
| } else if (path.isAbsolute(arg)) { | |
| target = path.resolve(arg); | |
| if (!isSubpath(workspaceRoot, target)) { | |
| return true; | |
| } | |
| } else { | |
| target = path.resolve(cwd, arg); | |
| if (!isSubpath(workspaceRoot, target)) { | |
| return true; | |
| } | |
| } | |
| // Check if target or any parent directory resolves through a symlink to outside the workspace | |
| let curr = target; | |
| while ( | |
| curr && | |
| isSubpath(workspaceRoot, curr) && | |
| curr !== path.dirname(curr) | |
| ) { | |
| try { | |
| const stat = fs.lstatSync(curr, { throwIfNoEntry: false }); | |
| if (stat?.isSymbolicLink()) { | |
| const real = fs.realpathSync(curr); | |
| if (!isSubpath(workspaceRoot, real)) { | |
| return true; | |
| } | |
| } | |
| } catch { | |
| // ignore | |
| } | |
| curr = path.dirname(curr); | |
| } | |
| return false; | |
| } | |
| /** | |
| * Checks if a command with its arguments is known to be safe to execute | |
| * without requiring user confirmation. This is primarily used to allow | |
| * harmless, read-only commands to run silently in the macOS sandbox. | |
| * | |
| * It handles raw command execution as well as wrapped commands like `bash -c "..."` or `bash -lc "..."`. | |
| * For wrapped commands, it parses the script and ensures all individual | |
| * sub-commands are in the known-safe list and no dangerous shell operators | |
| * (like subshells or redirection) are used. | |
| * | |
| * @param args - The command and its arguments (e.g., ['ls', '-la']) | |
| * @param cwd - Optional working directory for resolving relative paths | |
| * @param workspaceRoot - Optional workspace root directory | |
| * @returns true if the command is considered safe, false otherwise. | |
| */ | |
| export function isKnownSafeCommand( | |
| args: string[], | |
| cwd?: string, | |
| workspaceRoot?: string, | |
| ): boolean { | |
| if (!args || args.length === 0) { | |
| return false; | |
| } | |
| // Normalize zsh to bash | |
| const normalizedArgs = args.map((a) => (a === 'zsh' ? 'bash' : a)); | |
| if (isSafeToCallWithExec(normalizedArgs, cwd, workspaceRoot)) { | |
| return true; | |
| } | |
| // Support `bash -lc "..."` | |
| if ( | |
| normalizedArgs.length === 3 && | |
| normalizedArgs[0] === 'bash' && | |
| (normalizedArgs[1] === '-lc' || normalizedArgs[1] === '-c') | |
| ) { | |
| try { | |
| const script = normalizedArgs[2]; | |
| // Basic check for dangerous operators that could spawn subshells or redirect output | |
| // We allow &&, ||, |, ; but explicitly block subshells () and redirection >, >>, < | |
| if (/[()<>]/g.test(script)) { | |
| return false; | |
| } | |
| const commands = splitCommands(script); | |
| if (commands.length === 0) return false; | |
| return commands.every((cmd) => { | |
| const trimmed = cmd.trim(); | |
| if (!trimmed) return true; | |
| const parsed = shellParse(trimmed).map(extractStringFromParseEntry); | |
| if (parsed.length === 0) return true; | |
| return isSafeToCallWithExec(parsed, cwd, workspaceRoot); | |
| }); | |
| } catch { | |
| return false; | |
| } | |
| } | |
| return false; | |
| } | |
| /** | |
| * Core validation logic that checks a single command and its arguments | |
| * against an allowlist of known safe operations. It performs deep validation | |
| * for specific tools like `base64`, `find`, `rg`, `git`, and `sed` to ensure | |
| * unsafe flags (like `--output`, `-exec`, or mutating options) are not used, | |
| * and ensures all path arguments remain strictly within the workspace. | |
| * | |
| * @param args - The command and its arguments. | |
| * @param cwd - Optional working directory for relative path resolution. | |
| * @param workspaceRoot - Optional workspace boundary. | |
| * @returns true if the command is strictly read-only and safe. | |
| */ | |
| function isSafeToCallWithExec( | |
| args: string[], | |
| cwd?: string, | |
| workspaceRoot?: string, | |
| ): boolean { | |
| if (!args || args.length === 0) return false; | |
| const cmd = args[0]; | |
| let effectiveWorkspace = workspaceRoot | |
| ? path.resolve(workspaceRoot) | |
| : cwd | |
| ? path.resolve(cwd) | |
| : process.cwd(); | |
| try { | |
| effectiveWorkspace = resolveToRealPath(effectiveWorkspace); | |
| } catch { | |
| // Keep resolved path on failure | |
| } | |
| let effectiveCwd = cwd ? path.resolve(cwd) : effectiveWorkspace; | |
| try { | |
| effectiveCwd = resolveToRealPath(effectiveCwd); | |
| } catch { | |
| // Keep resolved path on failure | |
| } | |
| if ( | |
| effectiveCwd !== effectiveWorkspace && | |
| !isSubpath(effectiveWorkspace, effectiveCwd) | |
| ) { | |
| return false; | |
| } | |
| const safeCommands = new Set([ | |
| '__read', | |
| '__write', | |
| 'cat', | |
| 'cd', | |
| 'cut', | |
| 'echo', | |
| 'expr', | |
| 'false', | |
| 'grep', | |
| 'head', | |
| 'id', | |
| 'ls', | |
| 'nl', | |
| 'paste', | |
| 'pwd', | |
| 'rev', | |
| 'seq', | |
| 'stat', | |
| 'tail', | |
| 'tr', | |
| 'true', | |
| 'uname', | |
| 'uniq', | |
| 'wc', | |
| 'which', | |
| 'whoami', | |
| 'numfmt', | |
| 'tac', | |
| ]); | |
| if (safeCommands.has(cmd)) { | |
| if (cmd === 'cd') { | |
| let hasPath = false; | |
| for (let i = 1; i < args.length; i++) { | |
| const arg = args[i]; | |
| if (arg.startsWith('-')) { | |
| continue; | |
| } | |
| hasPath = true; | |
| if (isPathEscapingWorkspace(arg, effectiveWorkspace, effectiveCwd)) { | |
| return false; | |
| } | |
| } | |
| if (!hasPath) { | |
| if (isPathEscapingWorkspace('~', effectiveWorkspace, effectiveCwd)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| const fileReadingCommands = new Set([ | |
| 'cat', | |
| 'head', | |
| 'tail', | |
| 'tac', | |
| 'nl', | |
| 'stat', | |
| 'wc', | |
| 'cut', | |
| 'paste', | |
| 'rev', | |
| 'uniq', | |
| 'numfmt', | |
| 'ls', | |
| '__read', | |
| ]); | |
| if (fileReadingCommands.has(cmd)) { | |
| let passedDoubleDash = false; | |
| for (let i = 1; i < args.length; i++) { | |
| const arg = args[i]; | |
| if (!passedDoubleDash) { | |
| if (arg === '--') { | |
| passedDoubleDash = true; | |
| continue; | |
| } | |
| if (arg === '-') { | |
| continue; | |
| } | |
| if (arg.startsWith('-')) { | |
| if (arg.includes('=')) { | |
| const val = arg.slice(arg.indexOf('=') + 1); | |
| if ( | |
| isPathEscapingWorkspace(val, effectiveWorkspace, effectiveCwd) | |
| ) { | |
| return false; | |
| } | |
| continue; | |
| } | |
| try { | |
| const stat = fs.lstatSync(path.resolve(effectiveCwd, arg), { | |
| throwIfNoEntry: false, | |
| }); | |
| if (!stat) { | |
| continue; | |
| } | |
| } catch { | |
| continue; | |
| } | |
| } | |
| } | |
| if (isPathEscapingWorkspace(arg, effectiveWorkspace, effectiveCwd)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| if (cmd === 'grep') { | |
| let passedDoubleDash = false; | |
| for (let i = 1; i < args.length; i++) { | |
| const arg = args[i]; | |
| if (!passedDoubleDash) { | |
| if (arg === '--') { | |
| passedDoubleDash = true; | |
| continue; | |
| } | |
| if (arg === '-') { | |
| continue; | |
| } | |
| if (arg.startsWith('-f') || arg.startsWith('--file')) { | |
| let fileArg: string | undefined; | |
| if (arg.startsWith('--file=')) { | |
| fileArg = arg.slice(7); | |
| } else if (arg === '--file') { | |
| fileArg = args[++i]; | |
| } else { | |
| fileArg = arg.length > 2 ? arg.slice(2) : args[++i]; | |
| } | |
| if ( | |
| fileArg && | |
| isPathEscapingWorkspace(fileArg, effectiveWorkspace, effectiveCwd) | |
| ) { | |
| return false; | |
| } | |
| continue; | |
| } | |
| if (arg.startsWith('-')) { | |
| if (arg.includes('=')) { | |
| const val = arg.slice(arg.indexOf('=') + 1); | |
| if ( | |
| isPathEscapingWorkspace(val, effectiveWorkspace, effectiveCwd) | |
| ) { | |
| return false; | |
| } | |
| continue; | |
| } | |
| try { | |
| const stat = fs.lstatSync(path.resolve(effectiveCwd, arg), { | |
| throwIfNoEntry: false, | |
| }); | |
| if (!stat) { | |
| continue; | |
| } | |
| } catch { | |
| continue; | |
| } | |
| } | |
| } | |
| if (isPathEscapingWorkspace(arg, effectiveWorkspace, effectiveCwd)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| return true; | |
| } | |
| if (cmd === 'base64') { | |
| const unsafeOptions = new Set(['-o', '--output']); | |
| if ( | |
| args | |
| .slice(1) | |
| .some( | |
| (arg) => | |
| unsafeOptions.has(arg) || | |
| arg.startsWith('--output=') || | |
| (arg.startsWith('-o') && arg !== '-o'), | |
| ) | |
| ) { | |
| return false; | |
| } | |
| for (let i = 1; i < args.length; i++) { | |
| const arg = args[i]; | |
| if (arg.startsWith('-')) continue; | |
| if (isPathEscapingWorkspace(arg, effectiveWorkspace, effectiveCwd)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| if (cmd === 'find') { | |
| const unsafeOptions = new Set([ | |
| '-exec', | |
| '-execdir', | |
| '-ok', | |
| '-okdir', | |
| '-delete', | |
| '-fls', | |
| '-fprint', | |
| '-fprint0', | |
| '-fprintf', | |
| ]); | |
| if (args.some((arg) => unsafeOptions.has(arg))) { | |
| return false; | |
| } | |
| for (let i = 1; i < args.length; i++) { | |
| const arg = args[i]; | |
| if (arg.startsWith('-')) continue; | |
| if (isPathEscapingWorkspace(arg, effectiveWorkspace, effectiveCwd)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| if (isRipgrepCommand(cmd)) { | |
| if (!isTrustedCommandPath(cmd)) return false; | |
| const unsafeWithArgs = new Set(['--pre', '--hostname-bin']); | |
| const unsafeWithoutArgs = new Set(['--search-zip', '-z']); | |
| for (let i = 1; i < args.length; i++) { | |
| const arg = args[i]; | |
| if (unsafeWithoutArgs.has(arg)) return false; | |
| for (const opt of unsafeWithArgs) { | |
| if (arg === opt || arg.startsWith(opt + '=')) return false; | |
| } | |
| } | |
| let passedDoubleDash = false; | |
| for (let i = 1; i < args.length; i++) { | |
| const arg = args[i]; | |
| if (!passedDoubleDash) { | |
| if (arg === '--') { | |
| passedDoubleDash = true; | |
| continue; | |
| } | |
| if (arg.startsWith('-f') || arg.startsWith('--file')) { | |
| let fileArg: string | undefined; | |
| if (arg.startsWith('--file=')) { | |
| fileArg = arg.slice(7); | |
| } else if (arg === '--file') { | |
| fileArg = args[++i]; | |
| } else { | |
| fileArg = arg.length > 2 ? arg.slice(2) : args[++i]; | |
| } | |
| if ( | |
| fileArg && | |
| isPathEscapingWorkspace(fileArg, effectiveWorkspace, effectiveCwd) | |
| ) { | |
| return false; | |
| } | |
| continue; | |
| } | |
| if (arg.startsWith('-')) { | |
| if (arg.includes('=')) { | |
| const val = arg.slice(arg.indexOf('=') + 1); | |
| if ( | |
| isPathEscapingWorkspace(val, effectiveWorkspace, effectiveCwd) | |
| ) { | |
| return false; | |
| } | |
| continue; | |
| } | |
| try { | |
| const stat = fs.lstatSync(path.resolve(effectiveCwd, arg), { | |
| throwIfNoEntry: false, | |
| }); | |
| if (!stat) { | |
| continue; | |
| } | |
| } catch { | |
| continue; | |
| } | |
| } | |
| } | |
| if (isPathEscapingWorkspace(arg, effectiveWorkspace, effectiveCwd)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| if (cmd === 'git') { | |
| if (gitHasConfigOverrideGlobalOption(args)) { | |
| return false; | |
| } | |
| const { idx, subcommand } = findGitSubcommand(args, [ | |
| 'status', | |
| 'log', | |
| 'diff', | |
| 'show', | |
| 'branch', | |
| ]); | |
| if (!subcommand) { | |
| return false; | |
| } | |
| const subcommandArgs = args.slice(idx + 1); | |
| if (['status', 'log', 'diff', 'show'].includes(subcommand)) { | |
| return gitSubcommandArgsAreReadOnly(subcommandArgs); | |
| } | |
| if (subcommand === 'branch') { | |
| return ( | |
| gitSubcommandArgsAreReadOnly(subcommandArgs) && | |
| gitBranchIsReadOnly(subcommandArgs) | |
| ); | |
| } | |
| return false; | |
| } | |
| if (cmd === 'sed') { | |
| // Special-case sed -n {N|M,N}p | |
| if (args.length <= 4 && args[1] === '-n' && isValidSedNArg(args[2])) { | |
| if (args[3]) { | |
| if ( | |
| isPathEscapingWorkspace(args[3], effectiveWorkspace, effectiveCwd) | |
| ) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| return false; | |
| } | |
| return false; | |
| } | |
| /** | |
| * Helper to identify which git subcommand is being executed, skipping over | |
| * global git options like `-c` or `--git-dir`. | |
| * | |
| * @param args - The full git command arguments. | |
| * @param subcommands - A list of subcommands to look for. | |
| * @returns An object containing the index of the subcommand and its name. | |
| */ | |
| function findGitSubcommand( | |
| args: string[], | |
| subcommands: string[], | |
| ): { idx: number; subcommand: string | null } { | |
| let skipNext = false; | |
| for (let idx = 1; idx < args.length; idx++) { | |
| if (skipNext) { | |
| skipNext = false; | |
| continue; | |
| } | |
| const arg = args[idx]; | |
| if ( | |
| arg.startsWith('--config-env=') || | |
| arg.startsWith('--exec-path=') || | |
| arg.startsWith('--git-dir=') || | |
| arg.startsWith('--namespace=') || | |
| arg.startsWith('--super-prefix=') || | |
| arg.startsWith('--work-tree=') || | |
| ((arg.startsWith('-C') || arg.startsWith('-c')) && arg.length > 2) | |
| ) { | |
| continue; | |
| } | |
| if ( | |
| arg === '-C' || | |
| arg === '-c' || | |
| arg === '--config-env' || | |
| arg === '--exec-path' || | |
| arg === '--git-dir' || | |
| arg === '--namespace' || | |
| arg === '--super-prefix' || | |
| arg === '--work-tree' | |
| ) { | |
| skipNext = true; | |
| continue; | |
| } | |
| if (arg === '--' || arg.startsWith('-')) { | |
| continue; | |
| } | |
| if (subcommands.includes(arg)) { | |
| return { idx, subcommand: arg }; | |
| } | |
| return { idx: -1, subcommand: null }; | |
| } | |
| return { idx: -1, subcommand: null }; | |
| } | |
| /** | |
| * Checks if a git command contains global configuration override flags | |
| * (e.g., `-c` or `--config-env`) which could be used maliciously to | |
| * execute arbitrary code via git config. | |
| * | |
| * @param args - The git command arguments. | |
| * @returns true if config overrides are present. | |
| */ | |
| function gitHasConfigOverrideGlobalOption(args: string[]): boolean { | |
| return args.some( | |
| (arg) => | |
| arg === '-c' || | |
| arg === '--config-env' || | |
| (arg.startsWith('-c') && arg.length > 2) || | |
| arg.startsWith('--config-env='), | |
| ); | |
| } | |
| /** | |
| * Validates that the arguments for safe git subcommands (like `status`, `log`, | |
| * `diff`, `show`) do not contain flags that could cause mutations or execute | |
| * arbitrary commands (e.g., `--output`, `--exec`). | |
| * | |
| * @param args - Arguments passed to the git subcommand. | |
| * @returns true if the arguments only represent read-only operations. | |
| */ | |
| function gitSubcommandArgsAreReadOnly(args: string[]): boolean { | |
| const unsafeFlags = new Set([ | |
| '--output', | |
| '--ext-diff', | |
| '--textconv', | |
| '--exec', | |
| '--paginate', | |
| ]); | |
| return !args.some( | |
| (arg) => | |
| unsafeFlags.has(arg) || | |
| arg.startsWith('--output=') || | |
| arg.startsWith('--exec='), | |
| ); | |
| } | |
| /** | |
| * Validates that `git branch` is only used for read operations | |
| * (e.g., listing branches) rather than creating, deleting, or renaming branches. | |
| * | |
| * @param args - Arguments passed to `git branch`. | |
| * @returns true if it's purely a listing/read-only branch command. | |
| */ | |
| function gitBranchIsReadOnly(args: string[]): boolean { | |
| if (args.length === 0) return true; | |
| let sawReadOnlyFlag = false; | |
| for (const arg of args) { | |
| if ( | |
| [ | |
| '--list', | |
| '-l', | |
| '--show-current', | |
| '-a', | |
| '--all', | |
| '-r', | |
| '--remotes', | |
| '-v', | |
| '-vv', | |
| '--verbose', | |
| ].includes(arg) | |
| ) { | |
| sawReadOnlyFlag = true; | |
| } else if (arg.startsWith('--format=')) { | |
| sawReadOnlyFlag = true; | |
| } else { | |
| return false; | |
| } | |
| } | |
| return sawReadOnlyFlag; | |
| } | |
| /** | |
| * Ensures that a `sed` command argument is a valid line-printing instruction | |
| * (e.g., `10p` or `5,10p`), preventing unsafe script execution in `sed`. | |
| * | |
| * @param arg - The script argument passed to `sed -n`. | |
| * @returns true if it's a valid, safe print command. | |
| */ | |
| function isValidSedNArg(arg: string | undefined): boolean { | |
| if (!arg) return false; | |
| if (!arg.endsWith('p')) return false; | |
| const core = arg.slice(0, -1); | |
| const parts = core.split(','); | |
| if (parts.length === 1) { | |
| const num = parts[0]; | |
| return num.length > 0 && /^\d+$/.test(num); | |
| } else if (parts.length === 2) { | |
| const a = parts[0]; | |
| const b = parts[1]; | |
| return a.length > 0 && b.length > 0 && /^\d+$/.test(a) && /^\d+$/.test(b); | |
| } | |
| return false; | |
| } | |
| /** | |
| * Checks if a command with its arguments is explicitly known to be dangerous | |
| * and should be blocked or require strict user confirmation. This catches | |
| * destructive commands like `rm -rf`, `sudo`, and commands with execution | |
| * flags like `find -exec`. | |
| * | |
| * @param args - The command and its arguments. | |
| * @returns true if the command is identified as dangerous, false otherwise. | |
| */ | |
| export function isDangerousCommand( | |
| args: string[], | |
| _cwd?: string, | |
| _workspaceRoot?: string, | |
| ): boolean { | |
| if (!args || args.length === 0) { | |
| return false; | |
| } | |
| const cmd = args[0]; | |
| if (cmd === 'ln') { | |
| const isSymbolic = args.some( | |
| (arg) => | |
| arg === '-s' || | |
| arg === '--symbolic' || | |
| (arg.startsWith('-') && !arg.startsWith('--') && arg.includes('s')), | |
| ); | |
| if (isSymbolic) { | |
| return true; | |
| } | |
| } | |
| if (cmd === 'rm') { | |
| return args[1] === '-f' || args[1] === '-rf' || args[1] === '-fr'; | |
| } | |
| if (cmd === 'sudo') { | |
| return isDangerousCommand(args.slice(1)); | |
| } | |
| if (cmd === 'find') { | |
| const unsafeOptions = new Set([ | |
| '-exec', | |
| '-execdir', | |
| '-ok', | |
| '-okdir', | |
| '-delete', | |
| '-fls', | |
| '-fprint', | |
| '-fprint0', | |
| '-fprintf', | |
| ]); | |
| return args.some((arg) => unsafeOptions.has(arg)); | |
| } | |
| if (isRipgrepCommand(cmd)) { | |
| const unsafeWithArgs = new Set(['--pre', '--hostname-bin']); | |
| const unsafeWithoutArgs = new Set(['--search-zip', '-z']); | |
| return args.some((arg) => { | |
| if (unsafeWithoutArgs.has(arg)) return true; | |
| for (const opt of unsafeWithArgs) { | |
| if (arg === opt || arg.startsWith(opt + '=')) return true; | |
| } | |
| return false; | |
| }); | |
| } | |
| if (cmd === 'git') { | |
| if (gitHasConfigOverrideGlobalOption(args)) { | |
| return true; | |
| } | |
| const { idx, subcommand } = findGitSubcommand(args, [ | |
| 'status', | |
| 'log', | |
| 'diff', | |
| 'show', | |
| 'branch', | |
| ]); | |
| if (!subcommand) { | |
| // It's a git command we don't recognize as explicitly safe. | |
| return false; | |
| } | |
| const subcommandArgs = args.slice(idx + 1); | |
| if (['status', 'log', 'diff', 'show'].includes(subcommand)) { | |
| return !gitSubcommandArgsAreReadOnly(subcommandArgs); | |
| } | |
| if (subcommand === 'branch') { | |
| return !( | |
| gitSubcommandArgsAreReadOnly(subcommandArgs) && | |
| gitBranchIsReadOnly(subcommandArgs) | |
| ); | |
| } | |
| return false; | |
| } | |
| if (cmd === 'base64') { | |
| const unsafeOptions = new Set(['-o', '--output']); | |
| return args | |
| .slice(1) | |
| .some( | |
| (arg) => | |
| unsafeOptions.has(arg) || | |
| arg.startsWith('--output=') || | |
| (arg.startsWith('-o') && arg !== '-o'), | |
| ); | |
| } | |
| return false; | |
| } | |