Shell tools
When to use
Two distinct tools, both named "shell" — pick deliberately:
| Need | Use | Why |
|---|---|---|
| Works with any model provider; full control over what runs and where | LocalShellTool |
Client-side subprocess. You own the sandbox. |
| Provider-managed sandbox (container, network policy) on Anthropic / OpenAI | ShellTool |
Server-side execution. No local subprocess. |
LocalShellTool is the workhorse. Reach for it unless you specifically need provider-managed isolation and you're on Anthropic or OpenAI.
60-second recipe — LocalShellTool
from autogen.beta import Agent
from autogen.beta.config import AnthropicConfig
from autogen.beta.tools import LocalShellTool
agent = Agent(
"coder",
"You write and run Python code.",
config=AnthropicConfig(model="claude-sonnet-4-6"),
tools=[LocalShellTool()],
)
reply = await agent.ask("Write a hello world script and run it.")
print(await reply.content())
LocalShellTool is provider-agnostic — swap AnthropicConfig for OpenAIConfig(model="gpt-4.1"), GeminiConfig(model="gemini-2.5-pro"), etc. Make sure you've installed the matching ag2[<provider>] extra and set the matching env var (see ag2-quickstart → Prerequisites).
With no arguments, LocalShellTool creates a temporary working directory (prefixed ag2_shell_) and cleans it up when the process exits. Pass a path to use a specific directory:
from pathlib import Path
LocalShellTool("/tmp/my_project")
LocalShellTool(Path("/tmp/my_project"))
When a path is given, the directory is created if it does not exist and is not deleted on exit. Inspect the resolved working directory via tool.workdir.
Sandboxing (LocalShellEnvironment)
For anything beyond a throwaway demo, use LocalShellEnvironment and lock down what the agent can do. Filtering is applied in this order on every call:
allowed— if set, the command must match at least one prefix.blocked— if set, the command must not match any prefix.ignore— literal path tokens in the command are checked against gitignore-style patterns; matches return"Access denied: <path>".- Execute via
subprocess.run.
from autogen.beta.tools import LocalShellTool
from autogen.beta.tools.shell import LocalShellEnvironment
sh = LocalShellTool(
LocalShellEnvironment(
path="/tmp/my_project",
allowed=["python", "uv run", "git"],
blocked=["rm -rf", "curl", "wget"],
ignore=["**/.env", "*.key", "secrets/**"],
timeout=30,
max_output=50_000,
)
)
Read-only mode
For inspection-only access (cat, head, tail, ls, grep, find, git log, git diff, git status, …):
from autogen.beta.tools import LocalShellTool
from autogen.beta.tools.shell import LocalShellEnvironment
sh = LocalShellTool(LocalShellEnvironment(path="/my/codebase", readonly=True))
Pass an explicit allowed=[...] to override the built-in read-only allowlist.
LocalShellEnvironment parameter reference
| Parameter | Default | Description |
|---|---|---|
path |
None |
Working dir. None → temp dir, deleted on exit |
cleanup |
None |
None → auto (True when path=None, False otherwise). Deletes path on process exit |
allowed |
None |
Whitelist of command prefixes. None → all commands allowed |
blocked |
None |
Blacklist of command prefixes |
ignore |
None |
Gitignore-style path patterns; matches block the command |
readonly |
False |
When True and allowed unset, restricts to a built-in read-only list |
env |
None |
Extra env vars merged into each command |
timeout |
60 |
Per-command timeout in seconds (returns "Command timed out after Ns [exit code: 124]") |
max_output |
100_000 |
Max characters returned (truncated output is suffixed [truncated: …]) |
Stateful multi-turn workspaces
Files persist in workdir across ask() calls, so the agent can build on prior work:
from autogen.beta.tools import LocalShellTool
from autogen.beta.tools.shell import LocalShellEnvironment
sh = LocalShellTool(LocalShellEnvironment(path="/tmp/counter_demo"))
agent = Agent("coder", "You manage files.", config=config, tools=[sh])
reply1 = await agent.ask("Create counter.txt with value 0")
reply2 = await reply1.ask("Increment the counter by 1")
reply3 = await reply2.ask("Read the counter and tell me the value")
Provider-native ShellTool (Anthropic / OpenAI)
from autogen.beta.tools import ShellTool
agent = Agent("devops", config=AnthropicConfig(model="claude-sonnet-4-6"), tools=[ShellTool()])
OpenAI lets you configure the execution environment:
from autogen.beta.config import OpenAIResponsesConfig
from autogen.beta.tools import ShellTool
from autogen.beta.tools.builtin.shell import ContainerAutoEnvironment, NetworkPolicy
agent = Agent(
"devops",
config=OpenAIResponsesConfig(model="gpt-4.1"),
tools=[
ShellTool(
environment=ContainerAutoEnvironment(
network_policy=NetworkPolicy(allowed_domains=["pypi.org"]),
),
),
],
)
Environment options:
| Environment | Description |
|---|---|
ContainerAutoEnvironment |
Provider-managed container with optional network policy |
ContainerReferenceEnvironment |
Reference an existing container by ID |
ShellTool is not supported on Gemini — the request will raise UnsupportedToolError.
LocalShellTool vs ShellTool
LocalShellTool |
ShellTool |
|
|---|---|---|
| Execution | Client-side subprocess |
Provider-side container |
| Provider support | Any provider | Anthropic, OpenAI |
| Environment control | Full (allowed, blocked, ignore, readonly, …) |
Limited (provider-dependent) |
| Local FS access | Yes (you choose what's exposed) | No |
| Network control | Via blocked / allowed patterns |
OpenAI: NetworkPolicy |
| Import | from autogen.beta.tools import LocalShellTool (env: from autogen.beta.tools.shell import LocalShellEnvironment) |
from autogen.beta.tools import ShellTool |
Going deeper
website/docs/beta/tools/local_shell.mdx— fullLocalShellToolreference, command-filtering semantics.website/docs/beta/tools/builtin_tools.mdx#shell— provider-nativeShellToolsetup and environment configs.- For human-approval gating before each shell call, layer
approval_required()middleware (seeag2-hitl).
Common pitfalls
- Forgetting sandboxing in production —
LocalShellTool()with no environment runs anything anywhere with a 60s timeout. Setallowed,blocked, orreadonlyfor any non-trivial use. ignoreonly checks literal paths in the command string — variable substitution, command substitution (`cat secrets.key`), and dynamic glob expansion are not inspected. Layer inblocked=["cat", "less"]if you also want to block readers.- Trying to use
ShellToolon Gemini — unsupported, will raise. UseLocalShellToolinstead. - Using a hardcoded path that another process is also touching — multiple agents sharing
/tmp/my_projectwill race. Usetempfile.mkdtemp(prefix="...")for parallel runs. - Expecting
ShellToolto access local files — it doesn't; it runs in the provider's container. UseLocalShellToolfor anything on your filesystem. - Trusting the LLM with shell access — even sandboxed, write
prompts that scope what's allowed and consider pairing withapproval_required()for destructive operations.