packages/opencode/src/tool/shell.ts implements the ShellTool, the built-in shell command execution tool, identified by ShellID.ToolID.[1] The tree-sitter parser in shell.ts — loaded lazily via the lazy utility on first use — initializes both bash and ps (PowerShell) grammars (tree-sitter-bash and tree-sitter-powershell) using web-tree-sitter, enabling static analysis of arguments and paths before execution.[1] shell.ts resolves WASM assets for tree-sitter grammars using fileURLToPath for file:// URLs or direct path detection for absolute paths, falling back to resolving relative URLs against import.meta.url.[1]
In shell.ts, the default shell command timeout is 2 * 60 * 1000 ms (2 minutes), overridable via the bashDefaultTimeoutMs runtime flag.[1] On Windows, shell.ts spawns PowerShell with -NoLogo -NoProfile -NonInteractive -Command flags; on other platforms it passes the command string to the shell via the shell option.[1] Child processes in shell.ts are spawned with detached: true on non-Windows platforms and detached: false on Windows.[1] On Windows, shell.ts calls ChildProcessSpawner to run cygpath -w (via the POSIX shell) to resolve POSIX-style paths like /usr/… to Windows absolute paths, catching any error and returning an empty array rather than propagating the failure.[1]
shell.ts recognizes a fixed set of directory-changing commands — cd, chdir, popd, pushd, push-location, set-location — for permission-check purposes.[1] shell.ts also recognizes a broader set of file-manipulating POSIX commands and PowerShell cmdlets for path-permission scanning; CMD.exe equivalents are held in a separate CMD_FILES set.[1] Before scanning for permission-relevant paths, shell.ts expands $env:VAR, ${env:VAR}, and $(HOME|PWD|PSHOME) variable references in shell arguments.[1] Tilde (~) in path arguments is expanded to the OS home directory by shell.ts, handling ~, ~/, and ~' prefixes.[1] On Windows, shell.ts performs case-insensitive environment variable lookup via envValue, scanning process.env keys case-insensitively to match Windows semantics.[1] The dynamic function in shell.ts detects dynamic argument expressions — subshells, variable expansions, and backticks — and skips static path scanning for those arguments to avoid false positives.[1]
The tail function in shell.ts truncates output from the end by iterating lines in reverse and stopping when either maxLines or maxBytes is exceeded; when a single line exceeds the byte budget, tail truncates that line at a UTF-8 character boundary.[1]
The shell tool requests a single "bash" permission whose patterns array contains each individual sub-command parsed from the full command string — for example, "echo foo" and "echo bar" for the input "echo foo && echo bar".[2] For PowerShell commands, the shell tool parses conditional chains (; and if ($?)) into individual permission patterns and populates always with wildcard cmdlet glob entries (e.g., "Write-Host *").[2] PowerShell always-allow patterns use the bare cmdlet name with a wildcard (e.g., "Remove-Item *"); flags such as -Recurse are stripped so the always-allow entry is not flag-specific.[2]
When the configured shell is terminal-only (e.g., fish), Shell.acceptable falls back to a supported shell rather than using fish, and the fallback shell name is reflected in the ShellTool's description.[2] Shell.acceptable and Shell.preferred maintain resettable caches — calling .reset() on either clears the cached selection so the next call re-evaluates the environment.[2]
The shell tool test layer in packages/opencode/test/tool/shell.test.ts is assembled from CrossSpawnSpawner.node, FSUtil.node, Plugin.node, Truncate.node, Config.node, Agent.node, and RuntimeFlags.node via Layer.mergeAll and LayerNode.compile.[2] On non-Windows platforms the shell test uses the single system Shell.acceptable() shell; on Windows it builds a deduped list covering bash (git bash fallback), pwsh, powershell, and cmd.exe.[2]
Sources