agentic-bash-tool
v0.1.0
Published
Harness-neutral agentic Bash execution with streaming, background sessions, persistence, and permissions
Maintainers
Readme
agentic-bash-tool
A standalone, harness-neutral Bash tool for Node agent runtimes. It provides advanced the agent-facing behaviors for small harnesses.
Features include streamed progress, foreground and background execution, task/session control, wall-clock timeouts, output persistence, image attachments, semantic command metadata, permission analysis, approvals, and injectable execution backends.
Install
npm install agentic-bash-toolNode 22 or newer is required.
Minimal usage
Execution is fail-closed unless a security mode is chosen explicitly:
import { createBashRuntime } from "agentic-bash-tool";
const bash = createBashRuntime({
security: "full",
ask: "off",
cwd: process.cwd(),
});
const result = await bash.execute(
{ command: "git status", description: "Inspect repository status" },
{ onEvent: (event) => console.log(event.type) },
);
console.log(result.content);
await bash.dispose();security: "full" permits local host execution. Use it only when the surrounding harness has already established the appropriate trust boundary.
Generic function-tool wrapper
createBashTool returns an SDK-independent object with a plain JSON Schema and a familiar four-argument execute method:
import { createBashTool } from "agentic-bash-tool";
const tool = createBashTool({ security: "full", ask: "off" });
registerWithYourHarness({
name: tool.name,
description: tool.description,
inputSchema: tool.parameters,
execute: (args, context) =>
tool.execute(context.callId, args, context.signal, context.onUpdate),
});There is no dependency on Anthropic, OpenAI, TypeBox, Pi, or OpenClaw.
Approvals
Use allowlist mode with a callback provider when a harness can ask its operator for permission:
const bash = createBashRuntime({
security: "allowlist",
ask: "on-miss",
allowlist: ["git status", "rg *"],
permissionProvider: {
async decide(request, signal) {
// Render request.analysis.riskText however your harness prefers.
return await askOperator(request, signal);
// "allow-once" | "allow-always" | "deny"
},
},
});Allow-always creates only a narrowed command-prefix suggestion. Empty and universal wildcard rules are rejected.
The built-in semantic analyzer:
- distinguishes read-only, mutating, neutral, and sed commands;
- analyzes compound commands with quote and parenthesis awareness;
- detects dangerous wrappers, process substitution, redirects, redirect expansion, network paths, and out-of-root writes;
- caps compound-command work and fails safely when parsing is uncertain;
- produces structured reason codes, risk text, and audit records.
For higher-assurance environments, use an external policy engine through permissionProvider and an isolated ExecutionBackend.
Background commands
const result = await bash.execute({
command: "long-running-command",
runInBackground: true,
});
const sessionId = result.details.backgroundTaskId;
await bash.tasks.execute({ action: "poll", sessionId, timeout: 10_000 });
await bash.tasks.execute({ action: "log", sessionId, offset: 0, limit: 200 });
await bash.tasks.execute({ action: "write", sessionId, data: "yes\n" });
await bash.tasks.execute({ action: "send-keys", sessionId, keys: ["CTRL_C"] });
await bash.tasks.execute({ action: "stop", sessionId });Supported task actions are list, get, poll, wait, output, log, write, send-keys, submit, paste, kill, stop, clear, and remove.
Commands also auto-background after backgroundMs. A wall-clock timeoutMs is separate: yielding never kills a command, while timeout always requests process-tree termination.
Execution events
Pass onEvent globally or per invocation. Events are JSON-serializable discriminated objects:
execution.startedexecution.outputexecution.progressexecution.backgroundedpermission.requested,permission.resolved,permission.deniedexecution.completed,execution.failed,execution.timed-outsession.expired
Callbacks are isolated: a telemetry or harness callback exception cannot break the command.
Output behavior
- stdout and stderr are retained separately and in arrival-order aggregate output.
- In-memory output, pending output, and tails are bounded.
- Character, UTF-8 byte, and line counts remain accurate after truncation.
- Output over the persistence threshold is written to a mode-
0600file and replaced in model-facing content by a short preview plus path and size. - Emitted PNG, JPEG, GIF, and WebP paths are loaded best-effort as harness-neutral image blocks.
All thresholds and the OutputStore/ImageLoader implementations are configurable.
Backends and sandboxes
The bundled LocalExecutionBackend uses Node child processes and process-group termination. An execution backend implements:
interface ExecutionBackend {
id: string;
capabilities: {
sandboxed: boolean;
pty: boolean;
stdin: boolean;
remote: boolean;
};
start(request, hooks): Promise<RunningCommand>;
}Inject a container, hosted sandbox, SSH, remote-node, or PTY backend with backend. Optionally provide hostBackend and set allowDangerouslyDisableSandbox: true to let the model-facing compatibility field request that backend. The core never pretends a backend is sandboxed and contains no OpenClaw host routing.
Standalone safety helpers
The package also exports:
analyzeBashPermissionand policy/allowlist helpers;- hardened environment merging;
- root-bounded, no-follow script reads and variable-injection preflight;
ReadStateTrackerfor read-before-write integration;previewSedEditandapplySedPreview, which bind approval to original and proposed hashes and refuse stale/tampered applies;- terminal key, control-key, and hex-byte encoding.
These helpers are public so a harness can compose them with its own file tools and approval interface without importing OpenClaw.
Lifecycle
Create one runtime per desired security/session scope. Always call dispose() during shutdown. Disposal stops the TTL sweeper, terminates running processes, removes tracked persisted output, and clears session state.
The package does not use a process-global runtime singleton.
