Shell command execution for Amplifier agents.
- Python 3.11+
- UV - Fast Python package manager
# macOS/Linux/WSL
curl -LsSf https://astral.sh/uv/install.sh | sh
# Windows
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"Enables agents to execute bash commands in a controlled environment for system interactions, build operations, and automation tasks.
Platform Behavior:
- Linux/macOS/WSL: Full bash shell with all features (pipes, redirects, &&, ||, ~, $VAR, etc.)
- Windows: Limited to simple commands without shell operators (use full paths)
Module Type: Tool
Mount Point: tools
Entry Point: amplifier_module_tool_bash:mount
Execute a bash command with platform-appropriate shell.
Input:
command(string): The bash command to executetimeout(int, optional): Timeout in seconds (default: 30). Increase for builds, tests, or monitoring. Userun_in_backgroundfor truly indefinite processes.run_in_background(bool, optional): Run command in background, returning immediately with PID (default: false)
Output:
stdout: Standard output from commandstderr: Standard error from commandreturncode: Exit code (0 = success)
Platform Support:
Unix-like (Linux, macOS, WSL):
- ✅ Full bash shell (
/bin/bash) - ✅ Pipes:
ls | grep foo - ✅ Operators:
cmd1 && cmd2,cmd1 || cmd2 - ✅ Redirects:
cmd > file,cmd 2>&1,cmd &> file - ✅ Tilde expansion:
~/.amplifier - ✅ Variables:
$HOME,${VAR} - ✅ Command substitution:
$(pwd),`date` - ✅ Heredocs:
cat <<EOF
Windows (native):
- ✅ Full bash shell if Git Bash or WSL bash is found (auto-detected)
⚠️ Falls back to simple commands with no shell features if neither is found- 💡 Git Bash and WSL bash use different path/
$HOME/toolchain conventions (/c/...vs/mnt/c/...); the tool'sbashdescription names which one was resolved and its conventions. Setwindows_shellconfig (or theAMPLIFIER_BASH_WINDOWS_SHELLenv var) to"wsl"or"gitbash"to force a choice; default"auto"prefers whatever is found viaPATHfirst (WSL, if both are installed), falling back to well-known Git-for-Windows install locations ifPATHresolves nothing.
[[tools]]
module = "tool-bash"
config = {
working_dir = ".", # Working directory (defaults to session.working_dir capability)
timeout = 30, # Default timeout in seconds
require_approval = false,
safety_profile = "strict", # Safety profile: strict, standard, permissive, unrestricted
allowed_commands = [], # Allowlist patterns (supports wildcards)
denied_commands = [], # Additional custom blocked patterns
safety_overrides = { # Fine-grained overrides
allow = [], # Patterns to allow (even if normally blocked)
block = [] # Patterns to block (even if normally allowed)
}
}Note: If
working_diris not set in config, the module uses thesession.working_dircoordinator capability if available, falling back toPath.cwd(). This enables correct behavior in server/web deployments where the process cwd differs from the user's project directory.
Set managed_processes = true in the tool's host configuration to expose
additional actions on the existing bash tool. Ordinary calls (or
action = "run") and run_in_background keep their existing behavior. Managed
processes currently support POSIX hosts (Linux/macOS/WSL Python); native Windows
hosts return an explicit unsupported error for start. Pipes are the default.
Set trusted host managed_pty = true and per-start pty = true for a real
POSIX controlling terminal. Terminal output combines stdout/stderr; pty,
output_streams, and eof_semantics identify this in every status receipt.
Terminal sizing and attaching after host death are not provided.
# All actions must use the host's ordinary tool dispatcher and approval hooks.
started = await session.call_tool("bash", {
"action": "start", "command": "npm run build", "timeout": 300
})
process_id = started.output["process_id"]
page = await session.call_tool("bash", {
"action": "wait", "process_id": process_id,
"cursor": started.output["next_cursor"], "wait_ms": 1000
})| Action | Parameters and behavior |
|---|---|
start |
command required; timeout is the maximum process lifetime in seconds (1–3600, default tool timeout). Returns immediately after spawn with an opaque process_id. Optional pty requires host opt-in; question_ids binds exact required answers. |
read |
process_id, cursor (default 0), max_bytes (4096–100000, default 16384). Reads an output page without consuming it globally. |
wait |
Same as read, plus wait_ms (0–60000, default 1000). Waits for new output or completion. Expiration does not cancel the process. |
status |
process_id. Returns process state and counters without output chunks. |
write |
process_id, optional stdin (at most 65536 UTF-8 bytes) and close_stdin (EOF). Raw input requires the host policy below. |
terminate |
process_id. Requests cancellation, cleans up the owned process group and waits for observation of the result. Repeating it after completion is harmless. |
list |
Lists only processes owned by this mounted tool instance. |
Follow-up actions reject command, timeout, and run_in_background, including
an innocuous command added to match a shell auto-approval rule. Every action
retains the tool name bash, approval metadata, and normal tool:pre/tool:post
attribution; tools do not dispatch around approval hooks. Hosts must present
the action, target process, and input in approval details. A generic command
allowlist is not an authorization policy for interactive input.
Raw stdin is disabled by default. To grant it, trusted mount configuration
must set both managed_stdin = true and safety_profile = "unrestricted",
with no allowed_commands, denied_commands, or safety_overrides. These are
host options, never tool arguments. Input to an interpreter cannot be safely
validated as an independent bash command, so restricted profiles fail closed
instead of letting stdin evade their command policy. close_stdin without data
remains available for pipes. For a terminal it sends canonical EOF and disables
further input from this handle; raw-mode terminals reject EOF explicitly because
a terminal cannot be half-closed like a pipe. Applications needing a finer input policy should wait for a
host stdin-authorization adapter. Input writes are not idempotent: a cancelled
or timed-out write may already have delivered bytes; never automatically retry.
Responses contain state (running, cancel_requested, completed, failed,
cancelled, or outcome_unknown), the observed returncode (null until known),
cancellation_requested, termination_reason (cancel, timeout,
session_closed, or null), and output_complete. A successfully observed
nonzero exit has ToolResult.success = true with state = "failed": the tool
action succeeded; the command did not. Read state and returncode to judge
command success. Cancellation requested is distinct from cancellation observed;
terminating a command already observed as exited preserves its exit result.
Output is an ordered list of chunks containing stream, text, source byte
count, and cursor. Stdout/stderr retain their stream identities; their combined
order is the order the host read them, not a guarantee of inter-stream write
order. UTF-8 split across reads is preserved. Binary chunks use the ordinary
bash binary-output guard. Advance to next_cursor only after consuming the
page; rereading a cursor is repeatable while retained. has_more reports more
buffered output. Cursors are chunk sequence numbers, not byte offsets.
The buffer retains at most managed_max_output_bytes (default 100000, allowed
4096–10000000) and 1024 chunks per process. Older output is dropped with explicit
dropped_output_bytes, earliest_cursor, and cursor_expired metadata. A
stale cursor returns retained output and signals the gap; it never pretends to
have complete historical output. For durable full output, arrange a file or a
host-owned output collector. max_bytes budgets raw source bytes; rendered
replacement characters and binary notices can have different text sizes.
Process handles belong to one mounted tool instance. They cannot be read or
terminated by another session, including a fork. max_concurrent counts managed
processes throughout their lifetime together with ordinary foreground calls.
managed_max_processes bounds retained records (default 32, allowed 1–256);
once full, a new start evicts the oldest completed record or rejects if all are
still active. Evicted IDs fail explicitly.
The cleanup callback returned by mount must be run when the owning session
closes. It terminates owned groups and collects final exit/output state, including
starts racing with shutdown. A normal command exit also closes its remaining
process group. Process-tree cleanup uses the existing POSIX descendant discovery
and group teardown; deliberately escaped/reparented external processes cannot be
guaranteed to remain owned. Abrupt host death cannot run cleanup.
This is an in-memory lifecycle, not a durable process broker. A host restart does not restore handles, output, or observation, and never replays commands. Hosts persisting operation receipts should mark interrupted observations as outcome unknown after restart, and must not infer completion or retry mutations.
The bash tool uses a profile-based safety system with smart pattern matching.
| Profile | sudo |
rm -rf / |
Use Case |
|---|---|---|---|
strict (default) |
❌ Blocked | ❌ Blocked | Workstations, shared environments |
standard |
❌ (allowlist can override) | ❌ Blocked | Trusted environments with specific needs |
permissive |
✅ Allowed | ❌ Blocked | Containers, VMs, dedicated instances |
unrestricted |
✅ Allowed | ✅ Allowed | Dedicated hardware (e.g., Raspberry Pi) |
The safety system distinguishes between actual commands and text in strings/paths:
# ✅ ALLOWED - "sudo" is in a quoted string, not a command
echo "use sudo for admin tasks"
# ✅ ALLOWED - path contains /dev/ but isn't a device redirect
cd ~/dev/my-project
# ❌ BLOCKED - actual sudo command
sudo apt install vim
# ❌ BLOCKED - actual device redirect
cat file > /dev/sdaFor containers, VMs, or dedicated hardware where you want elevated access:
# Allow sudo for container/VM environments
config = { safety_profile = "permissive" }
# Allow specific sudo commands only
config = {
safety_profile = "standard",
allowed_commands = ["sudo systemctl *", "sudo apt *"]
}
# Full access for dedicated hardware
config = { safety_profile = "unrestricted" }IMPORTANT: Bash execution can be dangerous. Use with caution:
- Use
strictprofile (default) for shared/workstation environments - Set
require_approval = truefor production - Use
allowed_commandsto whitelist safe commands - Use
permissiveorunrestrictedonly in isolated environments - Never execute untrusted user input
# Agent uses bash tool
result = await session.call_tool("bash", {
"command": "ls -la",
"timeout": 10
})amplifier-core>=1.0.0
Note
This project is not currently accepting external contributions, but we're actively working toward opening this up. We value community input and look forward to collaborating in the future. For now, feel free to fork and experiment!
Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit Contributor License Agreements.
When you submit a pull request, a CLA bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA.
This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.
This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow Microsoft's Trademark & Brand Guidelines. Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies.
list also reports trusted stdin_allowed and pty availability, so hosts can
fail closed before opening an interpreter. These are observed mount policies,
not action-settable permissions.
A host can register operations.observe on the coordinator as a local callable
accepting one JSON-compatible event. The mounted tool looks it up when a managed
process starts; no app imports, URL destinations, credentials, or agent-supplied
callbacks are involved. Hosts bind their real session identity outside the event
and must not trust a tool-supplied owner ID for authorization.
Events use schemaVersion: 1, stable operationId (the process handle),
ownerId (the mounted registry), monotonic sequence, eventId, source: "tool-bash", kind: "process", and timestamp at. Phases are started,
output (one bounded chunk with its output cursor), state (for example
cancellation requested), and finished (observed terminal status). Commands
and stdin are not copied into the observation stream. Output is already subject
to the normal binary guard and can contain sensitive command results; the host
owns private storage and presentation policy. Chunks flag binary_output_withheld
and encoding_loss when rendered text cannot preserve the original bytes; a
host must treat either as incomplete original evidence, including after exit.
Each process has a bounded 32-event mailbox. Subprocess readers never await the
observer. Async observer calls have a 500 ms deadline; synchronous observers must
be cheap and nonblocking. Output can be dropped under backpressure or observer
failure: source sequence/cursor gaps and observerDroppedEvents make that loss
explicit. Failed events are never retried. Start waits for the initial bounded
observation attempt; cleanup drains remaining observations before returning.
observer_available and observer_dropped_events in normal process status
report whether an observer was present and whether delivery failed. Observer
availability is not a guarantee of durable storage.
A durable host must deduplicate events, record missing evidence, preserve actual
terminal results, and mark active receipts outcome unknown after losing their
owner. The module's local output ring can expire independently of a host's
archive. output_complete on process status means streams were drained, not
that either archive retains every byte. The observer's task and callbacks end
with the owned process; no observation or execution is restarted automatically.
Managed starts may include question_ids (at most 32 unique IDs). The module
uses the optional trusted questions.admit capability under its lifecycle fence
immediately before spawning, after the host dispatcher has completed approvals.
Absent capability, pending/cancelled/superseded or wrong-conversation answers
fail closed. Omitting the list leaves independent work independent. This is a
dependency check, never a substitute for tool permission. The capability must
return {"admitted": true, "questionIds": [the exact IDs]}; silence, false and
mismatched acknowledgment never admit work. Hosts can call the
public validate_process_owner(process_id, owner_id) immediately after approval
to verify an identity-bound control against the mounted owner.