Intelligent Bash Tool Execution
Runs agent shell commands through a multi-mode executor that uses a PTY when needed, falls back to direct exec, and manages approvals, background jobs, and signals
Add to Pack
Saved locally in this browser for now.
Cite This Pattern
OpenClaw Contributors (2026). Intelligent Bash Tool Execution. In *Awesome Agentic Patterns*. Retrieved October 14, 2026, from https://agentic-patterns.com/patterns/intelligent-bash-tool-execution
@misc{agentic_patterns_intelligent-bash-tool-execution,
title = {Intelligent Bash Tool Execution},
author = {OpenClaw Contributors},
year = {2026},
howpublished = {\url{https://agentic-patterns.com/patterns/intelligent-bash-tool-execution}},
note = {Awesome Agentic Patterns}
} Use when
- Agents run TTY-required CLIs or interactive tools
- Agents start long-running background processes that need tracking and cleanup
- Command execution needs approval modes such as deny, allowlist, or full
Avoid when
- Agents only run simple non-interactive commands
- The environment cannot install or compile a native PTY module and needs no TTY support
Problem
Secure, reliable command execution from agents is complex and error-prone:
- PTY requirements: TTY-required CLIs (coding agents, terminal UIs) fail with direct exec
- Platform differences: Linux and macOS behave differently for detached processes, signal handling
- Security concerns: Arbitrary command execution needs approval workflows, elevated mode detection
- Background management: Long-running processes need tracking, output aggregation, and cleanup
Agents need a multi-mode execution strategy that adapts to the command's requirements while maintaining security and reliability.
Solution
Multi-mode execution with adaptive fallback: direct exec → PTY, with automatic selection based on command requirements and runtime capabilities. The system handles PTY spawn failures gracefully, manages background processes, and provides security-aware approval workflows.
Core concepts:
- Tool schema definition: Bash commands invoked through structured tool interfaces (MCP, OpenAI Function Calling) with validated parameters.
- PTY-first for TTY-required commands: Detects when commands need a pseudo-terminal (coding agents, interactive CLIs) and spawns via
node-pty. - Graceful PTY fallback: If PTY spawn fails (module missing, platform unsupported), falls back to direct exec with a warning.
- Platform-specific handling: macOS requires detached processes for proper signal propagation; Linux handles both modes.
- Security-aware modes: Elevated mode detection with approval workflows (deny, allowlist, full).
- Background process registry: Long-running processes tracked with session IDs, output tailing, and exit notifications.
- Output truncation: Enforce
maxOutputlimits to prevent memory exhaustion for verbose processes. - Proper signal propagation: SIGTERM/SIGKILL delivered correctly to child processes on timeout or abort.
Implementation sketch:
async function runExecProcess(opts: {
command: string;
workdir: string;
env: Record<string, string>;
usePty: boolean;
timeoutSec: number;
runInBackground?: boolean;
maxOutput?: number;
}): Promise<ExecProcessHandle> {
let child: ChildProcess | null = null;
let pty: PtyHandle | null = null;
const warnings: string[] = [];
if (opts.usePty) {
try {
const { spawn } = await import("@lydell/node-pty");
pty = spawn(shell, [opts.command], {
cwd: opts.workdir,
env: opts.env,
cols: 120,
rows: 30,
});
} catch (err) {
// PTY unavailable; fallback to direct exec
warnings.push(`PTY spawn failed (${err}); retrying without PTY.`);
const { child: spawned } = await spawnWithFallback({
argv: [shell, opts.command],
options: { cwd: opts.workdir, env: opts.env },
fallbacks: [{ label: "no-detach", options: { detached: false } }],
});
child = spawned;
}
} else {
// Direct exec without PTY
const { child: spawned } = await spawnWithFallback({
argv: [shell, opts.command],
options: { cwd: opts.workdir, env: opts.env, detached: platform !== "win32" },
fallbacks: [{ label: "no-detach", options: { detached: false } }],
});
child = spawned;
}
// Register session for tracking
const session = {
id: createSessionSlug(),
command: opts.command,
pid: child?.pid ?? pty?.pid,
aggregated: "",
tail: "",
exited: false,
};
addSession(session);
// Handle timeout with SIGKILL
if (opts.timeoutSec > 0) {
setTimeout(() => {
if (!session.exited) {
killSession(session); // SIGTERM then SIGKILL
}
}, opts.timeoutSec * 1000);
}
return { session, promise /* resolves on exit */ };
}
Spawn fallback for platform differences:
async function spawnWithFallback(params: {
argv: string[];
options: ChildProcess.SpawnOptions;
fallbacks: Array<{ label: string; options: Partial<ChildProcess.SpawnOptions> }>;
}): Promise<{ child: ChildProcess }> {
try {
return { child: spawn(...params.argv, params.options) };
} catch (err) {
for (const fallback of params.fallbacks) {
try {
const mergedOptions = { ...params.options, ...fallback.options };
const child = spawn(...params.argv, mergedOptions);
// Warn about fallback
logWarn(`spawn failed (${err}); retrying with ${fallback.label}.`);
return { child };
} catch {
continue;
}
}
throw err;
}
}
Security-aware execution with approval:
async function executeWithApproval(params: {
command: string;
security: "deny" | "allowlist" | "full";
ask: "off" | "on-miss" | "always";
agentId: string;
}): Promise<ExecResult> {
const approvals = resolveExecApprovals(params.agentId, {
security: params.security,
ask: params.ask,
});
const allowlistEval = evaluateShellAllowlist({
command: params.command,
allowlist: approvals.allowlist,
safeBins: approvals.safeBins,
});
const requiresAsk = requiresExecApproval({
ask: params.ask,
security: params.security,
analysisOk: allowlistEval.analysisOk,
allowlistSatisfied: allowlistEval.allowlistSatisfied,
});
if (requiresAsk) {
const approvalId = crypto.randomUUID();
// Request approval via gateway; wait for decision
const decision = await requestApproval(approvalId, params.command);
if (decision === "deny") {
throw new Error("exec denied: user rejected");
}
}
// Execute command
return runExecProcess(params);
}
Background process management:
type ProcessSession = {
id: string;
command: string;
pid?: number;
aggregated: string;
tail: string;
exited: boolean;
exitCode?: number | null;
exitSignal?: NodeJS.Signals | null;
backgrounded: boolean;
};
const sessions = new Map<string, ProcessSession>();
function addSession(session: ProcessSession) {
sessions.set(session.id, session);
}
function markBackgrounded(session: ProcessSession) {
session.backgrounded = true;
}
function killSession(session: ProcessSession) {
if (session.child) {
session.child.kill("SIGTERM");
// Fallback to SIGKILL after grace period
setTimeout(() => {
if (!session.exited) {
session.child?.kill("SIGKILL");
markExited(session, null, "SIGKILL", "failed");
}
}, 1000);
}
}
How to use it
- Detect TTY requirements: Check if the command is a TTY-required CLI (coding agent, interactive tool) and set
usePty: true. - Handle PTY failures: Wrap PTY spawn in try-catch and fall back to direct exec with appropriate warnings.
- Configure security modes: Set default security level (
deny,allowlist,full) and approval behavior (off,on-miss,always). - Register background processes: Add sessions to a registry for tracking, polling, and cleanup.
- Propagate signals correctly: Use SIGTERM then SIGKILL for graceful shutdown, and handle platform-specific detached process behavior.
- Aggregate output: Collect stdout/stderr into
aggregatedand maintain atailfor user notifications. - Enforce output limits: Set
maxOutputthresholds to prevent memory exhaustion on verbose processes.
Pitfalls to avoid:
- Missing PTY module:
node-ptymay not be available in all environments; always provide fallback. - Signal handling differences: macOS detached processes don't receive signals; use process groups or alternative signaling.
- Zombie processes: Always handle the
"close"event and clean up session registry entries. - Output truncation: Large outputs can overwhelm memory; enforce
maxOutputlimits and truncate middle sections.
Trade-offs
Pros:
- TTY support: PTY mode enables TTY-required tools that would otherwise fail.
- Graceful degradation: Falls back to direct exec when PTY is unavailable.
- Security layers: Multiple modes (deny, allowlist, full) provide flexible security policies.
- Background tracking: Process registry enables long-running task management and cleanup.
- Platform awareness: Handles macOS/Linux differences for signal propagation.
Cons/Considerations:
- PTY dependency: Requires
node-ptynative module, which may fail to compile in some environments. - Complexity: Multi-mode execution increases code complexity and testing surface.
- Output buffering: Aggregating all output in memory can exhaust RAM for long-running, verbose processes.
- Signal limitations: Detached processes on macOS don't receive signals; requires workarounds.
References
- OpenClaw bash-tools.exec-run.ts - Command execution; the original
bash-tools.exec.tswas split into the currentbash-tools.exec-*modules after the Clawdbot-to-OpenClaw rename - OpenClaw bash-tools.exec-host-spawn.ts - PTY versus child spawn modes
- OpenClaw bash-process-registry.ts - Background registry
- OpenClaw bash-tools.exec-approval-request.ts - Approval workflow
- Claude Code - Tool-mediated bash execution with
run_in_backgroundsupport - Model Context Protocol - Structured tool definition standard
- Related: Virtual Machine Operator Agent for remote execution patterns