Bash Tool Implementation and Security Permission Design

4 min

1. Bash Tool Implementation

The Bash tool is a critical foundational Agent tool — Skill scripts need it, and as applications become increasingly CLI-based, future Agent external application calls will need it too.

While the Bash tool enables many operations and can simplify the Agent’s tool list, maintain the principle of least privilege — use dedicated tools first (Read for reading, Edit for editing, etc.).

1.1 Tool Definition

Key parameters:

  • command: Bash command string to execute
  • timeout: Execution timeout to prevent Agent hangs
  • description: Brief description rendered to users in UI
  • run_in_background: For long-running commands (builds, server starts) — run in background, poll for results
  • dangerouslyDisableSandbox: Security bypass strategy
  • _simulatedSedEdit: Pre-computed sed results — on user approval, writes the result directly instead of re-executing sed, ensuring WYSIWYG

1.2 Execution Function

Three core design principles:

  1. Generator function for real-time output streaming
  2. Production-grade exec wrapper
  3. Content length checking — large outputs written to file, returning partial results + file path

Generator mode is superior to Promise mode: Generator yields intermediate states in real-time, while Promise requires waiting until completion with a blank period.

The exec wrapper provides:

  1. Output written to disk — only ~4KB preview in memory
  2. Active interruption via AbortSignal
  3. Timeout handling — auto-stop after 120s
  4. Merged stdout/stderr for consistent UI display timing
  5. CWD auto-recovery when the working directory is accidentally deleted

Output truncation: files under 128KB are returned inline; larger outputs get the first 128KB preview plus the full file path for on-demand reading.

1.3 Return Values

Key fields:

  • returnCodeInterpretation: Semantic explanation of non-zero exit codes for better model reasoning
  • persistedOutputPath: Large output file path — the model decides whether to read the full output based on context, rather than blindly injecting everything
  • stdout: Core command output

1.4 Permission Verification Flow

The Bash tool has the broadest execution scope and highest risk. Verification includes:

  1. Command parsing
  2. Static rule checking
  3. Permission verification
  4. Model verification
  5. Container verification

Static rule checking (24 rules) and permission verification (three-tier results) are the core. Most uncertain cases output “ask” mode for user confirmation.

1. Command Parsing

Use tree-sitter to parse Bash commands into structured ASTs:

import { Language, Parser } from 'web-tree-sitter';

async function main() {
  await Parser.init();
  const lang = await Language.load('./tree-sitter-bash.wasm');
  const parser = new Parser();
  parser.setLanguage(lang);

  const command = 'grep -r "foo" . && cat file.txt | wc -l';
  const tree = parser.parse(command);
  // Walk AST, extract commands and argv...
}

2. Core 8 Static Checks

2.1 Control Character and Unicode Whitespace Rejection

Pre-parsing cleanup using regex to prevent “malicious character” injection:

const CONTROL_CHAR_RE = /[\x00-\x08\x0B-\x1F\x7F]/
const UNICODE_WHITESPACE_RE = /[\u00A0\u1680\u2000-\u200B\u2028\u2029\u202F\u205F\u3000\uFEFF]/
const BACKSLASH_WHITESPACE_RE = /\\[ \t]|[^ \t\n\\]\\\n/
tree-sitter vs bash Tokenization Divergence
tree-sitter vs bash Tokenization Divergence

Example: rm\u00A0-rf / — tree-sitter sees rm\u00A0-rf as one token (not rm), so static rules wouldn’t flag it. But bash treats \u00A0 as a separator, executing rm -rf /.

2.2 Dangerous Structure Types Trigger Ask Mode

AST node types are checked against a dangerous types set including command_substitution, process_substitution, subshell, for_statement, function_definition, etc. Matches trigger ask mode or rejection.

2.3 Wrapper Unwrapping Consistency Check

Original: timeout 5 eval "rm -rf /"

Wrappers like time, nohup, timeout, nice, env, stdbuf are stripped layer by layer to expose the actual command. Without this, checking argv[0] === 'timeout' would pass, but bash actually executes eval "rm -rf /".

Design principle: reject unknown cases.

2.4 Command Name Robustness Check

After unwrapping, validate the command name:

  1. Not empty
  2. Not a placeholder (__CMDSUB__, __VAR__)
  3. Not a fragment (starting with -, |, or &)

2.5 Eval-like Builtin Interception

Builtins that interpret arguments as code: eval, source, ., exec, command, trap, alias, let, etc. These are intercepted with specific safe-mode exceptions (e.g., command -v is allowed).

2.6 Pipe Segment Recursive Checking

Commands with pipe | are segmented, each segment getting full permission verification. Without segmentation, only the first command gets checked — echo hello | rm -rf / would pass on echo hello alone.

2.7 cd + git Combination Detection

Git reads .git/config and executes hooks from the current directory. If cd switches to an untrusted directory, any git command becomes a potential code execution entry point. Both must be detected together.

2.8 Dangerous Deletion Path Interception

For rm and rmdir, extract target paths and match against system-critical files: wildcard deletions, root directories, Windows drive roots, home directories, root direct children, and Windows system directories.

3. Complete 24 Static Verification Rules

#RuleCore Purpose
1Control chars & Unicode whitespaceParser/bash tokenization divergence
2Dangerous AST typesStatically unprovable structures
3Wrapper unwrappingExpose real inner command
4Command name robustnessEmpty/placeholder/fragment names
5Eval-like builtin interceptionSecondary code interpretation
6Zsh dangerous builtinsShell capability bypass
7Array subscript executionFlag-triggered arithmetic eval
8read/unset bare NAMEImplicit expression parsing
9[[ ]] arithmetic comparisonImplicit execution entry
10Shell keyword misparsingAST misinterpretation defense
11Newline + # comment offsetParameter hiding via comment
12jq system() interceptionCode execution bridge
13/proc/*/environ accessCredential leakage
14Complex structure operatorsHidden execution boundaries
15Pipe segmentation + cd+gitCross-segment risk splitting
16Process substitution (legacy)Fallback interception
17Redirect target safetyArbitrary file writes
18Dangerous deletion pathsSystem directory protection
19cd + write path uncertaintyCWD change write risk
20-- terminator handlingFlag parsing robustness
21Path wrapper re-verificationBypass prevention
22Legacy injection safety netRegex fallback
23Safe heredoc exceptionFalse positive reduction
24Subcommand fanout limitCPU starvation/DoS prevention

4. Permission Verification

Three permission states: allow (execute), deny (reject), ask (user confirmation).

Matching rules:

  1. Configuration file rules → corresponding permission state
  2. Static rule hits → mostly “ask” state
  3. Read-only commands → direct “allow”
Permission Strategy and Config Rule Matching
Permission Strategy and Config Rule Matching

Configuration format:

{
  "permissions": {
    "allow": ["Bash(git status:*)", "Bash(npm install:*)"],
    "deny": ["Bash(rm:*)", "Bash(rm -rf:*)"],
    "ask": ["Bash(docker:*)"]
  }
}

Read-only command criteria: ls, cat, head, tail, wc, find, grep, git status/diff/log, etc. — no cd, no output redirection or pipe write operators.