Skip to main content

Exec and plugin tools

exec (aliased as bash) is the built-in tool agents use to run shell commands. Its security policy (tools.exec.security / ask) and the human approval flow are enforced inside that tool’s implementation — they are not a general process-level sandbox. This page covers what that means when you build plugin tools: what your tools can do, what the policy gates, and how to design tools that respect the operator’s approval expectations.

The contract

runCommandWithTimeout has this shape:

What exec policy gates, and what it does not

What does gate your plugin tool is the tool policy (allow/deny lists): the tool itself can be blocked with tools.deny, limited by tools.allow, or scoped per agent. Entries match the exact tool name, your plugin id (all its tools), or group:plugins (all plugin tools). An optional tool ({ optional: true }) must be added to tools.allow before the model sees it — see Agent tools: optional tools. The practical consequence: a plugin tool that shells out can run commands the operator never approved through the exec flow. Treat that as a trust decision you are making on the operator’s behalf, and be explicit about it in your manifest description and README.

Running commands from a plugin

When your plugin genuinely needs a subprocess (a bundled CLI, a git helper), use runCommandWithTimeout and always pass a timeout:
Notes on the example:
  • ownerOnly: true restricts the tool to owner senders — the closest analogue to the exec approval flow available to plugin tools. Use it for anything with host-side effects.
  • runCommandWithTimeout never throws on non-zero exit; check result.code and surface stderr to the model.
  • The argv form (no shell string) avoids shell-injection ambiguity in model-supplied parameters.

What not to do

Designing approval-aware tools

Prefer reusing the built-in exec flow over shelling out yourself:
  1. Let the model call exec. If your plugin’s job can be expressed as commands, register prompts/skills that instruct the model to use the built-in exec tool instead of registering your own subprocess tool. The operator’s tools.exec policy, allowlist, and approval prompts then apply for free. This is the recommended default.
  2. Mark host-effecting tools ownerOnly. When you must spawn directly, ownerOnly: true ensures only the paired/allowlisted owner can trigger it.
  3. Document the trust story. State in your manifest description whether your tool executes commands, on which host, and that it does not go through exec approvals. Operators read this when deciding whether to install.
  4. Custom decision flows. If your plugin needs its own human-in-the-loop step, api.registerGatewayMethod gives you a request/respond RPC surface; broadcast events and resolve patterns can be modelled on the gateway’s own approval methods. This is advanced — reach for it only when the built-in flow genuinely cannot express the decision.

Async safety

Blocking the agent loop. register may be synchronous or async, but never perform synchronous blocking I/O inside it — execSync/readFileSync stall the event loop for the entire gateway during startup. Tool execute() functions are always async, but a slow one blocks the user’s reply until it completes.
Always pass timeoutMs (or the numeric shorthand). A hung child process otherwise blocks the tool call — and the user’s reply — indefinitely; prefer noOutputTimeoutMs for commands that stream nothing when stuck.