Skip to content
Everruns Cloud is open in early access. Run agents without operating the platform.
IDtools_in_shell
CategoryExecution
RiskHigh, assignment requires an org Admin
Depends onBashkit Shell
RolloutFEATURE_TOOLS_IN_SHELL grade (adoption by default)

An agent with many tools spends most of its prompt on tool schemas and most of its turns on one call at a time. Tools in Shell moves most tools out of the model’s direct tool list and into one shell command, tools, inside the Bashkit Shell. One script can call several tools, filter the results with jq, and print only what the model needs:

Terminal window
tools github list-pulls '{"repo":"acme/api","state":"open"}' \
| jq -r '.[] | select(.draft | not) | .number' \
| while read n; do
tools github get-pull "repo=acme/api" "number=$n" | jq -c '{number, title, mergeable}'
done
CommandWhat it does
tools --helpLists MCP servers (with their tool counts) and the other tools
tools <server> --helpLists one MCP server’s tools
tools <tool> --help, tools <server> <tool> --helpShows the tool’s input schema
tools search <words>Finds tools by name and description
tools <server> <tool> '{...}'Calls an MCP tool
tools <tool> '{...}'Calls any other tool

MCP tools are grouped by server (mcp_github__list_pulls is tools github list-pulls); every other tool is a top-level command (web_fetch is tools web-fetch). Hyphens and underscores are interchangeable. The list is rebuilt on every shell call, so an MCP server added during a session shows up on the next one.

Input is the same JSON object the tool takes as a direct call. It can come from stdin (jq -n '{repo: "a/b"}' | tools github list-pulls, or -), from a JSON argument, or from key=value and --key value pairs, which merge over the JSON. --flag alone is true. A value is read as JSON unless the tool’s schema says the key is a string, so number=7 is a number and repo=42 stays text.

Output is the tool’s result on stdout: a text result as is, anything else as JSON. Images a tool returns are saved under tool-images/ in the working directory, and stderr names the files.

Errors are one JSON line on stderr with exit code 1, so a script can branch on them:

{"error":{"code":"needs_approval","message":"...","retryable":false}}
CodeMeaning
unknown_commandNo such server or tool
invalid_inputThe input is not a JSON object or does not match the tool’s schema
tool_errorThe tool ran and failed
deniedA guardrail or hook blocked the call
needs_approvalThe call needs a person’s approval; the script stops here (see Approvals)
stoppedAn earlier call stopped the script, so this one did not run
connection_requiredThe tool or its MCP server needs a connection the person has not made
call_limitMore than 50 tool calls in one shell call
unavailableThe command cannot run here, or a server could not load yet (retryable says whether a later step can)

A tool moves behind tools when calling it only does something and returns data: MCP tools, integration tools, search and fetch, platform and data tools. These stay direct tool calls:

  • the bash and lua tools themselves;
  • tools that pause or shape the turn: ask_user, approvals, spawn_agent, session tasks, write_todos, and client-side tools;
  • tools whose result the model must see as an image, such as screenshots, read_file, computer and browser;
  • structured file editing (write_file, edit_file);
  • tools listed in the keep_visible config.

MCP servers set to “Load tools on demand” move behind tools too. tools --help lists them as (not loaded); the first command that names one, such as tools docs --help or tools docs search-docs q=refunds, or a tools search that matches the server, loads its tools inside the same shell call. From the next step on the server is listed like any other.

Every call from a script goes through the same checks as a direct call: the turn’s guardrails and hooks run before it, the tool’s schema is checked, and the post-tool hooks see the result before the script does.

Approval-gated and destructive tools are reachable from the shell too, and Tool Approval judges each call from a script the same way it judges a direct call. Most scripts never ask: reads, searches, and tools a person already allowed “always” just run.

When a call written out in full in the script, such as tools github delete-branch branch=fix-x, needs a person’s answer, the script does not start at all: the bash result says so ("before_start": true under stopped, done is empty), the person is asked about that call, and after they answer the agent can run the same script again. Calls whose input is built while the script runs, or that read their input from stdin, are checked as they run instead.

When a call needs a person’s answer while the script runs, it is not made and the script stops there. Nothing after it runs, and the script is never resumed or run again, because part of its work is already done and may not be safe to repeat. The bash result reports what happened, and the person is asked about the stopped call in the same step:

{
"exit_code": 1,
"success": false,
"tools": {
"done": [
{"tool": "tools linear create-issue", "input": {"title": "Flaky test"},
"ok": true, "result": {"id": "EVE-1201"}}
],
"read_only_calls": 3,
"stopped": {
"reason": "needs_approval",
"approval": "requested",
"call": {"tool": "tools github delete-branch", "input": {"branch": "fix-x"}}
},
"not_reached": "the stopped call and everything after it; write a new script for the rest"
}
}

done lists every call that may have changed something; read-only calls are counted. After the person answers, the agent writes a new script for what is left. A one-off approval covers that exact call (the tool and its input) once, so the new script can make it. The same report appears when a script stops at the call limit, or exits with an error after changing something.

Tools in Shell replaces tool search. When both are enabled, the tool search capabilities (tool_search, auto_tool_search, openai_tool_search, claude_tool_search) contribute nothing, and Agent Checks report the redundant capability as a capabilities.superseded suggestion.

{
"capabilities": [
{ "ref": "tools_in_shell", "config": { "keep_visible": ["web_fetch"] } }
]
}
FieldTypeDefaultMeaning
keep_visiblestring array[]Tools the model can still call directly. They stay callable from the shell too.