Skip to content
Strands Agents

Strands Shell

Give your agent a shell without giving it the keys to your machine.

PythonNodeLicenseStrands Discord

DocumentationMCP ServerPythonNode.js


Agents run shell commands in tight loops: Install deps, run tests, grep for errors, iterate. Those loops need speed and isolation.

Strands Shell is a Bourne-compatible shell that runs in-process. grep, sed, jq, curl, find, 50+ commands and it does this without fork, exec, syscalls, or cold starts. You declare what the agent can reach (files, URLs, credentials) and everything else doesn't exist to the agent.

DockerCloud sandboxStrands Shell
Cold start~200ms~1s (network)<1ms
IsolationContainer namespaceMicroVMIn-process VFS
Networkiptables / sidecarPlatform policyURL allowlist + SSRF guard
SecretsEnv vars (agent can read them)Platform-specificInjected per-request, agent never sees them
SetupDocker daemonAPI key + networkpip install strands-shell
PlatformsLinuxCloud-onlymacOS, Linux, WASM

Quick Start

MCP (works with any agent framework)

Drop this into your MCP client config:

{
"mcpServers": {
"shell": {
"command": "uvx",
"args": ["strands-shell", "--mcp"]
}
}
}

That's it and your agent gets shell, read_file, write_file, list_dir. All mediated through the Kernel.

Python

pip install strands-shell
importstrands_shellshell=strands_shell.Shell(
binds=[strands_shell.Bind("/my/project", "/workspace", mode="copy")],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
allowed_urls=["https://api.example.com/"],
)
out=shell.run("grep -rn TODO /workspace")
print(out.stdout)

Node.js

npm install @strands-agents/shell
import{Shell}from'@strands-agents/shell'constshell=awaitShell.create({binds: [{source: '/my/project',destination: '/workspace',mode: 'copy'}],})constout=awaitshell.run('grep -rn TODO /workspace')console.log(out.stdout)

How It Works

flowchart TB
agent["Your agent code\n(Strands, LangGraph, Pydantic AI, etc)"]
agent -->|"MCP / Python / Node.js"| shell
subgraph shell ["Strands Shell"]
direction TB
subgraph kernel ["Kernel (mediation boundary)"]
vfs["VFS: isolated filesystem"]
net["Network: SSRF guard + allowlist"]
creds["Credentials: injected per-URL"]
limits["Limits: timeout, output, fds"]
end
engine["Shell engine: parser, 25 builtins, 33 commands, Lua 5.4"]
end
Loading

Written in Rust, with native bindings for Python (PyO3) and Node.js (napi-rs). State persists across run() calls (env vars, working directory, functions). The filesystem is shared.

Configuration

shell=strands_shell.Shell(
binds=[
strands_shell.Bind("/host/project", "/workspace", mode="copy"),
strands_shell.Bind("/tmp/output", "/output", mode="direct"),
],
credentials=[
strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN"),
],
allowed_urls=["https://api.example.com/", "https://pypi.org/"],
timeout=30.0,
env={"PROJECT": "demo"},
limits=strands_shell.Limits(
max_output=1<<20,
max_file_size=10<<20,
),
)

⚠️mode: "direct" mounts are live. The agent can read and modify host files in real time. Use only for designated output directories. Never direct-bind directories containing secrets, credentials, or configuration you don't want the agent to modify.

Inspecting configuration

A constructed shell exposes a read-only snapshot of how it was configured. This is useful when you embed Strands Shell as a sandbox in a larger framework and need to build tool descriptions, surface the network allowlist, or report the active resource caps from a shell object you were handed.

shell=strands_shell.Shell(
allowed_urls=["https://api.example.com/"],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
timeout=30.0,
)
cfg=shell.config# a frozen ShellConfig snapshotcfg.allowed_urls# ('https://api.example.com/',)cfg.timeout# 30.0cfg.credentials[0].url# 'https://api.example.com/'cfg.credentials[0].env_var# 'API_TOKEN' (the secret value is never exposed)
constshell=awaitShell.create({allowedUrls: ['https://api.example.com/'],credentials: [{url: 'https://api.example.com/',envVar: 'API_TOKEN'}],timeout: 30,})constcfg=awaitshell.config()// a deep-frozen snapshot objectcfg.allowedUrls// ['https://api.example.com/']cfg.timeout// 30cfg.credentials[0].envVar// 'API_TOKEN' (the secret value is never exposed)

The snapshot reports binds, credentials, the network allowlist, environment variables, umask, timeout, and resource limits. Credential secrets are never included — each entry reports its URL pattern, kind, and source (a literal token was supplied, or the name of the environment variable it is read from), so you can reason about credentials without the agent or your tooling ever seeing the secret itself.

TOML

You can load all of this from a config file instead:

[[bind]]
mode = "copy"source = "/host/project"destination = "/workspace"
[[cred]]
url = "https://api.openai.com/v1/"methods = ["POST"]
kind = "bearer"api_key_env = "OPENAI_API_KEY"
[[mcp]]
name = "my-tools"command = "/path/to/mcp-server"args = ["--stdio"]

MCP Server

The built-in MCP server exposes the shell over JSON-RPC on stdio, working with anything that speaks MCP.

uvx strands-shell --mcp # bare in-memory sandbox
uvx strands-shell --config sandbox.toml --mcp # with mounts + credentials

If you declare [[mcp]] servers in your TOML config, they show up as Lua modules inside the shell. Call require("my_tools") and you get a table of the server's tools.

Security Model

Strands Shell is a mediation layer, not a security sandbox. It enforces what the agent should access via Kernel-mediated deny-by-default. It does NOT protect against: memory-safety exploits in the shell engine itself, timing side-channels, or an attacker who controls the host process. For multi-tenant or adversarial workloads, run each Shell instance inside a container or microVM.

The Kernel mediates everything; it runs in the same process as your code, not in a VM. If your threat model is "untrusted tenant running arbitrary code," put Strands Shell inside a container too. For "my agent shouldn't access things I haven't explicitly allowed," the Kernel handles it.

Default-deny. You allowlist what the agent can reach:

  • Files: only bound paths exist, everything else is hidden.
  • Network: curl blocks private ranges (RFC1918, link-local, loopback, IMDS) by default while letting public URLs pass through. Use allowed_urls to permit specific internal hosts.
  • Secrets: the Kernel injects credentials per-URL at request time, ensuring the agent never holds them. The Kernel never re-injects on redirects, even back to the same host.
  • Syscalls: there are none; no fork, no exec because the shell is pure userspace.

If you bypass any of these, report it. See SECURITY.md.

Limits (best-effort): timeouts, output caps, fd limits, inode limits. These catch runaway agents but won't stop someone actively trying to break out. OS-level isolation for that.

Multi-tenant: a Shell instance is single-owner. If you're serving multiple agents, create one Shell per session. Construction is cheap (no containers, no VMs, just an in-memory VFS), so spinning up per-request is the intended pattern.

Secure Defaults

Out of the box, the shell is an empty sandbox — no files, no network, no credentials. When you grant access, follow least privilege:

  • Prefer mode: "copy" over mode: "direct" for source code. Copy-on-create isolates the agent from your live files. Use direct only for output directories where the agent needs to persist results.
  • Scope binds narrowly. Bind /my/project/src rather than /my/project or /. The agent doesn't need your .git/, .env, or node_modules/.
  • Allowlist URLs explicitly. Don't use allowed_urls: ["https://"] — this disables SSRF protection entirely. List the specific API endpoints the agent needs.
  • Set timeouts. The default has no per-command timeout. Set timeout to bound runaway commands (30s is reasonable for most agent loops).
  • Use limits. Set max_output to prevent agents from filling memory with unbounded command output (1MB is a good default).

Commands

25 builtins, 33 commands, and a Bourne-compatible shell with pipes, loops, functions, and subshells.

The commands agents use constantly: grep, find, cat, head, tail, jq for reading and searching. sed, sort, tr, cut for transforming output. cp, mv, rm, mkdir for managing files. curl for HTTP (SSRF-guarded, credentials auto-injected). lua for scripting when shell gets awkward.

The full command reference has the inventory with implementation status, supported flags, and known gaps vs GNU coreutils.

File Operations API

Read and write files without going through a shell command:

shell.write_file("/workspace/note.txt", b"hello")
data=shell.read_file("/workspace/note.txt")
entries=shell.list_files("/workspace")
shell.remove_file("/workspace/note.txt")

Contributing

See CONTRIBUTING.md. Bug reports and design questions are just as useful as PRs.

Community

Discord if you want to talk about it.

License

Apache-2.0

Security

If you find a security issue, report it privately instead of opening a public issue. Bypasses of filesystem mediation, SSRF protection, or credential injection qualify. See SECURITY.md.

About

Give your agent a shell without giving it the keys to your machine.

Resources

Code of conduct

Contributing

Security policy

Stars

235 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - strands-agents/shell: Give your agent a shell without giving it the keys to your machine. · GitHub
Skip to content
Strands Agents

Strands Shell

Give your agent a shell without giving it the keys to your machine.

PythonNodeLicenseStrands Discord

DocumentationMCP ServerPythonNode.js


Agents run shell commands in tight loops: Install deps, run tests, grep for errors, iterate. Those loops need speed and isolation.

Strands Shell is a Bourne-compatible shell that runs in-process. grep, sed, jq, curl, find, 50+ commands and it does this without fork, exec, syscalls, or cold starts. You declare what the agent can reach (files, URLs, credentials) and everything else doesn't exist to the agent.

DockerCloud sandboxStrands Shell
Cold start~200ms~1s (network)<1ms
IsolationContainer namespaceMicroVMIn-process VFS
Networkiptables / sidecarPlatform policyURL allowlist + SSRF guard
SecretsEnv vars (agent can read them)Platform-specificInjected per-request, agent never sees them
SetupDocker daemonAPI key + networkpip install strands-shell
PlatformsLinuxCloud-onlymacOS, Linux, WASM

Quick Start

MCP (works with any agent framework)

Drop this into your MCP client config:

{
"mcpServers": {
"shell": {
"command": "uvx",
"args": ["strands-shell", "--mcp"]
}
}
}

That's it and your agent gets shell, read_file, write_file, list_dir. All mediated through the Kernel.

Python

pip install strands-shell
importstrands_shellshell=strands_shell.Shell(
binds=[strands_shell.Bind("/my/project", "/workspace", mode="copy")],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
allowed_urls=["https://api.example.com/"],
)
out=shell.run("grep -rn TODO /workspace")
print(out.stdout)

Node.js

npm install @strands-agents/shell
import{Shell}from'@strands-agents/shell'constshell=awaitShell.create({binds: [{source: '/my/project',destination: '/workspace',mode: 'copy'}],})constout=awaitshell.run('grep -rn TODO /workspace')console.log(out.stdout)

How It Works

flowchart TB
agent["Your agent code\n(Strands, LangGraph, Pydantic AI, etc)"]
agent -->|"MCP / Python / Node.js"| shell
subgraph shell ["Strands Shell"]
direction TB
subgraph kernel ["Kernel (mediation boundary)"]
vfs["VFS: isolated filesystem"]
net["Network: SSRF guard + allowlist"]
creds["Credentials: injected per-URL"]
limits["Limits: timeout, output, fds"]
end
engine["Shell engine: parser, 25 builtins, 33 commands, Lua 5.4"]
end
Loading

Written in Rust, with native bindings for Python (PyO3) and Node.js (napi-rs). State persists across run() calls (env vars, working directory, functions). The filesystem is shared.

Configuration

shell=strands_shell.Shell(
binds=[
strands_shell.Bind("/host/project", "/workspace", mode="copy"),
strands_shell.Bind("/tmp/output", "/output", mode="direct"),
],
credentials=[
strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN"),
],
allowed_urls=["https://api.example.com/", "https://pypi.org/"],
timeout=30.0,
env={"PROJECT": "demo"},
limits=strands_shell.Limits(
max_output=1<<20,
max_file_size=10<<20,
),
)

⚠️mode: "direct" mounts are live. The agent can read and modify host files in real time. Use only for designated output directories. Never direct-bind directories containing secrets, credentials, or configuration you don't want the agent to modify.

Inspecting configuration

A constructed shell exposes a read-only snapshot of how it was configured. This is useful when you embed Strands Shell as a sandbox in a larger framework and need to build tool descriptions, surface the network allowlist, or report the active resource caps from a shell object you were handed.

shell=strands_shell.Shell(
allowed_urls=["https://api.example.com/"],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
timeout=30.0,
)
cfg=shell.config# a frozen ShellConfig snapshotcfg.allowed_urls# ('https://api.example.com/',)cfg.timeout# 30.0cfg.credentials[0].url# 'https://api.example.com/'cfg.credentials[0].env_var# 'API_TOKEN' (the secret value is never exposed)
constshell=awaitShell.create({allowedUrls: ['https://api.example.com/'],credentials: [{url: 'https://api.example.com/',envVar: 'API_TOKEN'}],timeout: 30,})constcfg=awaitshell.config()// a deep-frozen snapshot objectcfg.allowedUrls// ['https://api.example.com/']cfg.timeout// 30cfg.credentials[0].envVar// 'API_TOKEN' (the secret value is never exposed)

The snapshot reports binds, credentials, the network allowlist, environment variables, umask, timeout, and resource limits. Credential secrets are never included — each entry reports its URL pattern, kind, and source (a literal token was supplied, or the name of the environment variable it is read from), so you can reason about credentials without the agent or your tooling ever seeing the secret itself.

TOML

You can load all of this from a config file instead:

[[bind]]
mode = "copy"source = "/host/project"destination = "/workspace"
[[cred]]
url = "https://api.openai.com/v1/"methods = ["POST"]
kind = "bearer"api_key_env = "OPENAI_API_KEY"
[[mcp]]
name = "my-tools"command = "/path/to/mcp-server"args = ["--stdio"]

MCP Server

The built-in MCP server exposes the shell over JSON-RPC on stdio, working with anything that speaks MCP.

uvx strands-shell --mcp # bare in-memory sandbox
uvx strands-shell --config sandbox.toml --mcp # with mounts + credentials

If you declare [[mcp]] servers in your TOML config, they show up as Lua modules inside the shell. Call require("my_tools") and you get a table of the server's tools.

Security Model

Strands Shell is a mediation layer, not a security sandbox. It enforces what the agent should access via Kernel-mediated deny-by-default. It does NOT protect against: memory-safety exploits in the shell engine itself, timing side-channels, or an attacker who controls the host process. For multi-tenant or adversarial workloads, run each Shell instance inside a container or microVM.

The Kernel mediates everything; it runs in the same process as your code, not in a VM. If your threat model is "untrusted tenant running arbitrary code," put Strands Shell inside a container too. For "my agent shouldn't access things I haven't explicitly allowed," the Kernel handles it.

Default-deny. You allowlist what the agent can reach:

  • Files: only bound paths exist, everything else is hidden.
  • Network: curl blocks private ranges (RFC1918, link-local, loopback, IMDS) by default while letting public URLs pass through. Use allowed_urls to permit specific internal hosts.
  • Secrets: the Kernel injects credentials per-URL at request time, ensuring the agent never holds them. The Kernel never re-injects on redirects, even back to the same host.
  • Syscalls: there are none; no fork, no exec because the shell is pure userspace.

If you bypass any of these, report it. See SECURITY.md.

Limits (best-effort): timeouts, output caps, fd limits, inode limits. These catch runaway agents but won't stop someone actively trying to break out. OS-level isolation for that.

Multi-tenant: a Shell instance is single-owner. If you're serving multiple agents, create one Shell per session. Construction is cheap (no containers, no VMs, just an in-memory VFS), so spinning up per-request is the intended pattern.

Secure Defaults

Out of the box, the shell is an empty sandbox — no files, no network, no credentials. When you grant access, follow least privilege:

  • Prefer mode: "copy" over mode: "direct" for source code. Copy-on-create isolates the agent from your live files. Use direct only for output directories where the agent needs to persist results.
  • Scope binds narrowly. Bind /my/project/src rather than /my/project or /. The agent doesn't need your .git/, .env, or node_modules/.
  • Allowlist URLs explicitly. Don't use allowed_urls: ["https://"] — this disables SSRF protection entirely. List the specific API endpoints the agent needs.
  • Set timeouts. The default has no per-command timeout. Set timeout to bound runaway commands (30s is reasonable for most agent loops).
  • Use limits. Set max_output to prevent agents from filling memory with unbounded command output (1MB is a good default).

Commands

25 builtins, 33 commands, and a Bourne-compatible shell with pipes, loops, functions, and subshells.

The commands agents use constantly: grep, find, cat, head, tail, jq for reading and searching. sed, sort, tr, cut for transforming output. cp, mv, rm, mkdir for managing files. curl for HTTP (SSRF-guarded, credentials auto-injected). lua for scripting when shell gets awkward.

The full command reference has the inventory with implementation status, supported flags, and known gaps vs GNU coreutils.

File Operations API

Read and write files without going through a shell command:

shell.write_file("/workspace/note.txt", b"hello")
data=shell.read_file("/workspace/note.txt")
entries=shell.list_files("/workspace")
shell.remove_file("/workspace/note.txt")

Contributing

See CONTRIBUTING.md. Bug reports and design questions are just as useful as PRs.

Community

Discord if you want to talk about it.

License

Apache-2.0

Security

If you find a security issue, report it privately instead of opening a public issue. Bypasses of filesystem mediation, SSRF protection, or credential injection qualify. See SECURITY.md.

About

Give your agent a shell without giving it the keys to your machine.

Resources

Code of conduct

Contributing

Security policy

Stars

235 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - strands-agents/shell: Give your agent a shell without giving it the keys to your machine. · GitHub
Skip to content
Strands Agents

Strands Shell

Give your agent a shell without giving it the keys to your machine.

PythonNodeLicenseStrands Discord

DocumentationMCP ServerPythonNode.js


Agents run shell commands in tight loops: Install deps, run tests, grep for errors, iterate. Those loops need speed and isolation.

Strands Shell is a Bourne-compatible shell that runs in-process. grep, sed, jq, curl, find, 50+ commands and it does this without fork, exec, syscalls, or cold starts. You declare what the agent can reach (files, URLs, credentials) and everything else doesn't exist to the agent.

DockerCloud sandboxStrands Shell
Cold start~200ms~1s (network)<1ms
IsolationContainer namespaceMicroVMIn-process VFS
Networkiptables / sidecarPlatform policyURL allowlist + SSRF guard
SecretsEnv vars (agent can read them)Platform-specificInjected per-request, agent never sees them
SetupDocker daemonAPI key + networkpip install strands-shell
PlatformsLinuxCloud-onlymacOS, Linux, WASM

Quick Start

MCP (works with any agent framework)

Drop this into your MCP client config:

{
"mcpServers": {
"shell": {
"command": "uvx",
"args": ["strands-shell", "--mcp"]
}
}
}

That's it and your agent gets shell, read_file, write_file, list_dir. All mediated through the Kernel.

Python

pip install strands-shell
importstrands_shellshell=strands_shell.Shell(
binds=[strands_shell.Bind("/my/project", "/workspace", mode="copy")],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
allowed_urls=["https://api.example.com/"],
)
out=shell.run("grep -rn TODO /workspace")
print(out.stdout)

Node.js

npm install @strands-agents/shell
import{Shell}from'@strands-agents/shell'constshell=awaitShell.create({binds: [{source: '/my/project',destination: '/workspace',mode: 'copy'}],})constout=awaitshell.run('grep -rn TODO /workspace')console.log(out.stdout)

How It Works

flowchart TB
agent["Your agent code\n(Strands, LangGraph, Pydantic AI, etc)"]
agent -->|"MCP / Python / Node.js"| shell
subgraph shell ["Strands Shell"]
direction TB
subgraph kernel ["Kernel (mediation boundary)"]
vfs["VFS: isolated filesystem"]
net["Network: SSRF guard + allowlist"]
creds["Credentials: injected per-URL"]
limits["Limits: timeout, output, fds"]
end
engine["Shell engine: parser, 25 builtins, 33 commands, Lua 5.4"]
end
Loading

Written in Rust, with native bindings for Python (PyO3) and Node.js (napi-rs). State persists across run() calls (env vars, working directory, functions). The filesystem is shared.

Configuration

shell=strands_shell.Shell(
binds=[
strands_shell.Bind("/host/project", "/workspace", mode="copy"),
strands_shell.Bind("/tmp/output", "/output", mode="direct"),
],
credentials=[
strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN"),
],
allowed_urls=["https://api.example.com/", "https://pypi.org/"],
timeout=30.0,
env={"PROJECT": "demo"},
limits=strands_shell.Limits(
max_output=1<<20,
max_file_size=10<<20,
),
)

⚠️mode: "direct" mounts are live. The agent can read and modify host files in real time. Use only for designated output directories. Never direct-bind directories containing secrets, credentials, or configuration you don't want the agent to modify.

Inspecting configuration

A constructed shell exposes a read-only snapshot of how it was configured. This is useful when you embed Strands Shell as a sandbox in a larger framework and need to build tool descriptions, surface the network allowlist, or report the active resource caps from a shell object you were handed.

shell=strands_shell.Shell(
allowed_urls=["https://api.example.com/"],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
timeout=30.0,
)
cfg=shell.config# a frozen ShellConfig snapshotcfg.allowed_urls# ('https://api.example.com/',)cfg.timeout# 30.0cfg.credentials[0].url# 'https://api.example.com/'cfg.credentials[0].env_var# 'API_TOKEN' (the secret value is never exposed)
constshell=awaitShell.create({allowedUrls: ['https://api.example.com/'],credentials: [{url: 'https://api.example.com/',envVar: 'API_TOKEN'}],timeout: 30,})constcfg=awaitshell.config()// a deep-frozen snapshot objectcfg.allowedUrls// ['https://api.example.com/']cfg.timeout// 30cfg.credentials[0].envVar// 'API_TOKEN' (the secret value is never exposed)

The snapshot reports binds, credentials, the network allowlist, environment variables, umask, timeout, and resource limits. Credential secrets are never included — each entry reports its URL pattern, kind, and source (a literal token was supplied, or the name of the environment variable it is read from), so you can reason about credentials without the agent or your tooling ever seeing the secret itself.

TOML

You can load all of this from a config file instead:

[[bind]]
mode = "copy"source = "/host/project"destination = "/workspace"
[[cred]]
url = "https://api.openai.com/v1/"methods = ["POST"]
kind = "bearer"api_key_env = "OPENAI_API_KEY"
[[mcp]]
name = "my-tools"command = "/path/to/mcp-server"args = ["--stdio"]

MCP Server

The built-in MCP server exposes the shell over JSON-RPC on stdio, working with anything that speaks MCP.

uvx strands-shell --mcp # bare in-memory sandbox
uvx strands-shell --config sandbox.toml --mcp # with mounts + credentials

If you declare [[mcp]] servers in your TOML config, they show up as Lua modules inside the shell. Call require("my_tools") and you get a table of the server's tools.

Security Model

Strands Shell is a mediation layer, not a security sandbox. It enforces what the agent should access via Kernel-mediated deny-by-default. It does NOT protect against: memory-safety exploits in the shell engine itself, timing side-channels, or an attacker who controls the host process. For multi-tenant or adversarial workloads, run each Shell instance inside a container or microVM.

The Kernel mediates everything; it runs in the same process as your code, not in a VM. If your threat model is "untrusted tenant running arbitrary code," put Strands Shell inside a container too. For "my agent shouldn't access things I haven't explicitly allowed," the Kernel handles it.

Default-deny. You allowlist what the agent can reach:

  • Files: only bound paths exist, everything else is hidden.
  • Network: curl blocks private ranges (RFC1918, link-local, loopback, IMDS) by default while letting public URLs pass through. Use allowed_urls to permit specific internal hosts.
  • Secrets: the Kernel injects credentials per-URL at request time, ensuring the agent never holds them. The Kernel never re-injects on redirects, even back to the same host.
  • Syscalls: there are none; no fork, no exec because the shell is pure userspace.

If you bypass any of these, report it. See SECURITY.md.

Limits (best-effort): timeouts, output caps, fd limits, inode limits. These catch runaway agents but won't stop someone actively trying to break out. OS-level isolation for that.

Multi-tenant: a Shell instance is single-owner. If you're serving multiple agents, create one Shell per session. Construction is cheap (no containers, no VMs, just an in-memory VFS), so spinning up per-request is the intended pattern.

Secure Defaults

Out of the box, the shell is an empty sandbox — no files, no network, no credentials. When you grant access, follow least privilege:

  • Prefer mode: "copy" over mode: "direct" for source code. Copy-on-create isolates the agent from your live files. Use direct only for output directories where the agent needs to persist results.
  • Scope binds narrowly. Bind /my/project/src rather than /my/project or /. The agent doesn't need your .git/, .env, or node_modules/.
  • Allowlist URLs explicitly. Don't use allowed_urls: ["https://"] — this disables SSRF protection entirely. List the specific API endpoints the agent needs.
  • Set timeouts. The default has no per-command timeout. Set timeout to bound runaway commands (30s is reasonable for most agent loops).
  • Use limits. Set max_output to prevent agents from filling memory with unbounded command output (1MB is a good default).

Commands

25 builtins, 33 commands, and a Bourne-compatible shell with pipes, loops, functions, and subshells.

The commands agents use constantly: grep, find, cat, head, tail, jq for reading and searching. sed, sort, tr, cut for transforming output. cp, mv, rm, mkdir for managing files. curl for HTTP (SSRF-guarded, credentials auto-injected). lua for scripting when shell gets awkward.

The full command reference has the inventory with implementation status, supported flags, and known gaps vs GNU coreutils.

File Operations API

Read and write files without going through a shell command:

shell.write_file("/workspace/note.txt", b"hello")
data=shell.read_file("/workspace/note.txt")
entries=shell.list_files("/workspace")
shell.remove_file("/workspace/note.txt")

Contributing

See CONTRIBUTING.md. Bug reports and design questions are just as useful as PRs.

Community

Discord if you want to talk about it.

License

Apache-2.0

Security

If you find a security issue, report it privately instead of opening a public issue. Bypasses of filesystem mediation, SSRF protection, or credential injection qualify. See SECURITY.md.

About

Give your agent a shell without giving it the keys to your machine.

Resources

Code of conduct

Contributing

Security policy

Stars

235 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - strands-agents/shell: Give your agent a shell without giving it the keys to your machine. · GitHub
Skip to content
Strands Agents

Strands Shell

Give your agent a shell without giving it the keys to your machine.

PythonNodeLicenseStrands Discord

DocumentationMCP ServerPythonNode.js


Agents run shell commands in tight loops: Install deps, run tests, grep for errors, iterate. Those loops need speed and isolation.

Strands Shell is a Bourne-compatible shell that runs in-process. grep, sed, jq, curl, find, 50+ commands and it does this without fork, exec, syscalls, or cold starts. You declare what the agent can reach (files, URLs, credentials) and everything else doesn't exist to the agent.

DockerCloud sandboxStrands Shell
Cold start~200ms~1s (network)<1ms
IsolationContainer namespaceMicroVMIn-process VFS
Networkiptables / sidecarPlatform policyURL allowlist + SSRF guard
SecretsEnv vars (agent can read them)Platform-specificInjected per-request, agent never sees them
SetupDocker daemonAPI key + networkpip install strands-shell
PlatformsLinuxCloud-onlymacOS, Linux, WASM

Quick Start

MCP (works with any agent framework)

Drop this into your MCP client config:

{
"mcpServers": {
"shell": {
"command": "uvx",
"args": ["strands-shell", "--mcp"]
}
}
}

That's it and your agent gets shell, read_file, write_file, list_dir. All mediated through the Kernel.

Python

pip install strands-shell
importstrands_shellshell=strands_shell.Shell(
binds=[strands_shell.Bind("/my/project", "/workspace", mode="copy")],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
allowed_urls=["https://api.example.com/"],
)
out=shell.run("grep -rn TODO /workspace")
print(out.stdout)

Node.js

npm install @strands-agents/shell
import{Shell}from'@strands-agents/shell'constshell=awaitShell.create({binds: [{source: '/my/project',destination: '/workspace',mode: 'copy'}],})constout=awaitshell.run('grep -rn TODO /workspace')console.log(out.stdout)

How It Works

flowchart TB
agent["Your agent code\n(Strands, LangGraph, Pydantic AI, etc)"]
agent -->|"MCP / Python / Node.js"| shell
subgraph shell ["Strands Shell"]
direction TB
subgraph kernel ["Kernel (mediation boundary)"]
vfs["VFS: isolated filesystem"]
net["Network: SSRF guard + allowlist"]
creds["Credentials: injected per-URL"]
limits["Limits: timeout, output, fds"]
end
engine["Shell engine: parser, 25 builtins, 33 commands, Lua 5.4"]
end
Loading

Written in Rust, with native bindings for Python (PyO3) and Node.js (napi-rs). State persists across run() calls (env vars, working directory, functions). The filesystem is shared.

Configuration

shell=strands_shell.Shell(
binds=[
strands_shell.Bind("/host/project", "/workspace", mode="copy"),
strands_shell.Bind("/tmp/output", "/output", mode="direct"),
],
credentials=[
strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN"),
],
allowed_urls=["https://api.example.com/", "https://pypi.org/"],
timeout=30.0,
env={"PROJECT": "demo"},
limits=strands_shell.Limits(
max_output=1<<20,
max_file_size=10<<20,
),
)

⚠️mode: "direct" mounts are live. The agent can read and modify host files in real time. Use only for designated output directories. Never direct-bind directories containing secrets, credentials, or configuration you don't want the agent to modify.

Inspecting configuration

A constructed shell exposes a read-only snapshot of how it was configured. This is useful when you embed Strands Shell as a sandbox in a larger framework and need to build tool descriptions, surface the network allowlist, or report the active resource caps from a shell object you were handed.

shell=strands_shell.Shell(
allowed_urls=["https://api.example.com/"],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
timeout=30.0,
)
cfg=shell.config# a frozen ShellConfig snapshotcfg.allowed_urls# ('https://api.example.com/',)cfg.timeout# 30.0cfg.credentials[0].url# 'https://api.example.com/'cfg.credentials[0].env_var# 'API_TOKEN' (the secret value is never exposed)
constshell=awaitShell.create({allowedUrls: ['https://api.example.com/'],credentials: [{url: 'https://api.example.com/',envVar: 'API_TOKEN'}],timeout: 30,})constcfg=awaitshell.config()// a deep-frozen snapshot objectcfg.allowedUrls// ['https://api.example.com/']cfg.timeout// 30cfg.credentials[0].envVar// 'API_TOKEN' (the secret value is never exposed)

The snapshot reports binds, credentials, the network allowlist, environment variables, umask, timeout, and resource limits. Credential secrets are never included — each entry reports its URL pattern, kind, and source (a literal token was supplied, or the name of the environment variable it is read from), so you can reason about credentials without the agent or your tooling ever seeing the secret itself.

TOML

You can load all of this from a config file instead:

[[bind]]
mode = "copy"source = "/host/project"destination = "/workspace"
[[cred]]
url = "https://api.openai.com/v1/"methods = ["POST"]
kind = "bearer"api_key_env = "OPENAI_API_KEY"
[[mcp]]
name = "my-tools"command = "/path/to/mcp-server"args = ["--stdio"]

MCP Server

The built-in MCP server exposes the shell over JSON-RPC on stdio, working with anything that speaks MCP.

uvx strands-shell --mcp # bare in-memory sandbox
uvx strands-shell --config sandbox.toml --mcp # with mounts + credentials

If you declare [[mcp]] servers in your TOML config, they show up as Lua modules inside the shell. Call require("my_tools") and you get a table of the server's tools.

Security Model

Strands Shell is a mediation layer, not a security sandbox. It enforces what the agent should access via Kernel-mediated deny-by-default. It does NOT protect against: memory-safety exploits in the shell engine itself, timing side-channels, or an attacker who controls the host process. For multi-tenant or adversarial workloads, run each Shell instance inside a container or microVM.

The Kernel mediates everything; it runs in the same process as your code, not in a VM. If your threat model is "untrusted tenant running arbitrary code," put Strands Shell inside a container too. For "my agent shouldn't access things I haven't explicitly allowed," the Kernel handles it.

Default-deny. You allowlist what the agent can reach:

  • Files: only bound paths exist, everything else is hidden.
  • Network: curl blocks private ranges (RFC1918, link-local, loopback, IMDS) by default while letting public URLs pass through. Use allowed_urls to permit specific internal hosts.
  • Secrets: the Kernel injects credentials per-URL at request time, ensuring the agent never holds them. The Kernel never re-injects on redirects, even back to the same host.
  • Syscalls: there are none; no fork, no exec because the shell is pure userspace.

If you bypass any of these, report it. See SECURITY.md.

Limits (best-effort): timeouts, output caps, fd limits, inode limits. These catch runaway agents but won't stop someone actively trying to break out. OS-level isolation for that.

Multi-tenant: a Shell instance is single-owner. If you're serving multiple agents, create one Shell per session. Construction is cheap (no containers, no VMs, just an in-memory VFS), so spinning up per-request is the intended pattern.

Secure Defaults

Out of the box, the shell is an empty sandbox — no files, no network, no credentials. When you grant access, follow least privilege:

  • Prefer mode: "copy" over mode: "direct" for source code. Copy-on-create isolates the agent from your live files. Use direct only for output directories where the agent needs to persist results.
  • Scope binds narrowly. Bind /my/project/src rather than /my/project or /. The agent doesn't need your .git/, .env, or node_modules/.
  • Allowlist URLs explicitly. Don't use allowed_urls: ["https://"] — this disables SSRF protection entirely. List the specific API endpoints the agent needs.
  • Set timeouts. The default has no per-command timeout. Set timeout to bound runaway commands (30s is reasonable for most agent loops).
  • Use limits. Set max_output to prevent agents from filling memory with unbounded command output (1MB is a good default).

Commands

25 builtins, 33 commands, and a Bourne-compatible shell with pipes, loops, functions, and subshells.

The commands agents use constantly: grep, find, cat, head, tail, jq for reading and searching. sed, sort, tr, cut for transforming output. cp, mv, rm, mkdir for managing files. curl for HTTP (SSRF-guarded, credentials auto-injected). lua for scripting when shell gets awkward.

The full command reference has the inventory with implementation status, supported flags, and known gaps vs GNU coreutils.

File Operations API

Read and write files without going through a shell command:

shell.write_file("/workspace/note.txt", b"hello")
data=shell.read_file("/workspace/note.txt")
entries=shell.list_files("/workspace")
shell.remove_file("/workspace/note.txt")

Contributing

See CONTRIBUTING.md. Bug reports and design questions are just as useful as PRs.

Community

Discord if you want to talk about it.

License

Apache-2.0

Security

If you find a security issue, report it privately instead of opening a public issue. Bypasses of filesystem mediation, SSRF protection, or credential injection qualify. See SECURITY.md.

About

Give your agent a shell without giving it the keys to your machine.

Resources

Code of conduct

Contributing

Security policy

Stars

235 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - strands-agents/shell: Give your agent a shell without giving it the keys to your machine. · GitHub
Skip to content
Strands Agents

Strands Shell

Give your agent a shell without giving it the keys to your machine.

PythonNodeLicenseStrands Discord

DocumentationMCP ServerPythonNode.js


Agents run shell commands in tight loops: Install deps, run tests, grep for errors, iterate. Those loops need speed and isolation.

Strands Shell is a Bourne-compatible shell that runs in-process. grep, sed, jq, curl, find, 50+ commands and it does this without fork, exec, syscalls, or cold starts. You declare what the agent can reach (files, URLs, credentials) and everything else doesn't exist to the agent.

DockerCloud sandboxStrands Shell
Cold start~200ms~1s (network)<1ms
IsolationContainer namespaceMicroVMIn-process VFS
Networkiptables / sidecarPlatform policyURL allowlist + SSRF guard
SecretsEnv vars (agent can read them)Platform-specificInjected per-request, agent never sees them
SetupDocker daemonAPI key + networkpip install strands-shell
PlatformsLinuxCloud-onlymacOS, Linux, WASM

Quick Start

MCP (works with any agent framework)

Drop this into your MCP client config:

{
"mcpServers": {
"shell": {
"command": "uvx",
"args": ["strands-shell", "--mcp"]
}
}
}

That's it and your agent gets shell, read_file, write_file, list_dir. All mediated through the Kernel.

Python

pip install strands-shell
importstrands_shellshell=strands_shell.Shell(
binds=[strands_shell.Bind("/my/project", "/workspace", mode="copy")],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
allowed_urls=["https://api.example.com/"],
)
out=shell.run("grep -rn TODO /workspace")
print(out.stdout)

Node.js

npm install @strands-agents/shell
import{Shell}from'@strands-agents/shell'constshell=awaitShell.create({binds: [{source: '/my/project',destination: '/workspace',mode: 'copy'}],})constout=awaitshell.run('grep -rn TODO /workspace')console.log(out.stdout)

How It Works

flowchart TB
agent["Your agent code\n(Strands, LangGraph, Pydantic AI, etc)"]
agent -->|"MCP / Python / Node.js"| shell
subgraph shell ["Strands Shell"]
direction TB
subgraph kernel ["Kernel (mediation boundary)"]
vfs["VFS: isolated filesystem"]
net["Network: SSRF guard + allowlist"]
creds["Credentials: injected per-URL"]
limits["Limits: timeout, output, fds"]
end
engine["Shell engine: parser, 25 builtins, 33 commands, Lua 5.4"]
end
Loading

Written in Rust, with native bindings for Python (PyO3) and Node.js (napi-rs). State persists across run() calls (env vars, working directory, functions). The filesystem is shared.

Configuration

shell=strands_shell.Shell(
binds=[
strands_shell.Bind("/host/project", "/workspace", mode="copy"),
strands_shell.Bind("/tmp/output", "/output", mode="direct"),
],
credentials=[
strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN"),
],
allowed_urls=["https://api.example.com/", "https://pypi.org/"],
timeout=30.0,
env={"PROJECT": "demo"},
limits=strands_shell.Limits(
max_output=1<<20,
max_file_size=10<<20,
),
)

⚠️mode: "direct" mounts are live. The agent can read and modify host files in real time. Use only for designated output directories. Never direct-bind directories containing secrets, credentials, or configuration you don't want the agent to modify.

Inspecting configuration

A constructed shell exposes a read-only snapshot of how it was configured. This is useful when you embed Strands Shell as a sandbox in a larger framework and need to build tool descriptions, surface the network allowlist, or report the active resource caps from a shell object you were handed.

shell=strands_shell.Shell(
allowed_urls=["https://api.example.com/"],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
timeout=30.0,
)
cfg=shell.config# a frozen ShellConfig snapshotcfg.allowed_urls# ('https://api.example.com/',)cfg.timeout# 30.0cfg.credentials[0].url# 'https://api.example.com/'cfg.credentials[0].env_var# 'API_TOKEN' (the secret value is never exposed)
constshell=awaitShell.create({allowedUrls: ['https://api.example.com/'],credentials: [{url: 'https://api.example.com/',envVar: 'API_TOKEN'}],timeout: 30,})constcfg=awaitshell.config()// a deep-frozen snapshot objectcfg.allowedUrls// ['https://api.example.com/']cfg.timeout// 30cfg.credentials[0].envVar// 'API_TOKEN' (the secret value is never exposed)

The snapshot reports binds, credentials, the network allowlist, environment variables, umask, timeout, and resource limits. Credential secrets are never included — each entry reports its URL pattern, kind, and source (a literal token was supplied, or the name of the environment variable it is read from), so you can reason about credentials without the agent or your tooling ever seeing the secret itself.

TOML

You can load all of this from a config file instead:

[[bind]]
mode = "copy"source = "/host/project"destination = "/workspace"
[[cred]]
url = "https://api.openai.com/v1/"methods = ["POST"]
kind = "bearer"api_key_env = "OPENAI_API_KEY"
[[mcp]]
name = "my-tools"command = "/path/to/mcp-server"args = ["--stdio"]

MCP Server

The built-in MCP server exposes the shell over JSON-RPC on stdio, working with anything that speaks MCP.

uvx strands-shell --mcp # bare in-memory sandbox
uvx strands-shell --config sandbox.toml --mcp # with mounts + credentials

If you declare [[mcp]] servers in your TOML config, they show up as Lua modules inside the shell. Call require("my_tools") and you get a table of the server's tools.

Security Model

Strands Shell is a mediation layer, not a security sandbox. It enforces what the agent should access via Kernel-mediated deny-by-default. It does NOT protect against: memory-safety exploits in the shell engine itself, timing side-channels, or an attacker who controls the host process. For multi-tenant or adversarial workloads, run each Shell instance inside a container or microVM.

The Kernel mediates everything; it runs in the same process as your code, not in a VM. If your threat model is "untrusted tenant running arbitrary code," put Strands Shell inside a container too. For "my agent shouldn't access things I haven't explicitly allowed," the Kernel handles it.

Default-deny. You allowlist what the agent can reach:

  • Files: only bound paths exist, everything else is hidden.
  • Network: curl blocks private ranges (RFC1918, link-local, loopback, IMDS) by default while letting public URLs pass through. Use allowed_urls to permit specific internal hosts.
  • Secrets: the Kernel injects credentials per-URL at request time, ensuring the agent never holds them. The Kernel never re-injects on redirects, even back to the same host.
  • Syscalls: there are none; no fork, no exec because the shell is pure userspace.

If you bypass any of these, report it. See SECURITY.md.

Limits (best-effort): timeouts, output caps, fd limits, inode limits. These catch runaway agents but won't stop someone actively trying to break out. OS-level isolation for that.

Multi-tenant: a Shell instance is single-owner. If you're serving multiple agents, create one Shell per session. Construction is cheap (no containers, no VMs, just an in-memory VFS), so spinning up per-request is the intended pattern.

Secure Defaults

Out of the box, the shell is an empty sandbox — no files, no network, no credentials. When you grant access, follow least privilege:

  • Prefer mode: "copy" over mode: "direct" for source code. Copy-on-create isolates the agent from your live files. Use direct only for output directories where the agent needs to persist results.
  • Scope binds narrowly. Bind /my/project/src rather than /my/project or /. The agent doesn't need your .git/, .env, or node_modules/.
  • Allowlist URLs explicitly. Don't use allowed_urls: ["https://"] — this disables SSRF protection entirely. List the specific API endpoints the agent needs.
  • Set timeouts. The default has no per-command timeout. Set timeout to bound runaway commands (30s is reasonable for most agent loops).
  • Use limits. Set max_output to prevent agents from filling memory with unbounded command output (1MB is a good default).

Commands

25 builtins, 33 commands, and a Bourne-compatible shell with pipes, loops, functions, and subshells.

The commands agents use constantly: grep, find, cat, head, tail, jq for reading and searching. sed, sort, tr, cut for transforming output. cp, mv, rm, mkdir for managing files. curl for HTTP (SSRF-guarded, credentials auto-injected). lua for scripting when shell gets awkward.

The full command reference has the inventory with implementation status, supported flags, and known gaps vs GNU coreutils.

File Operations API

Read and write files without going through a shell command:

shell.write_file("/workspace/note.txt", b"hello")
data=shell.read_file("/workspace/note.txt")
entries=shell.list_files("/workspace")
shell.remove_file("/workspace/note.txt")

Contributing

See CONTRIBUTING.md. Bug reports and design questions are just as useful as PRs.

Community

Discord if you want to talk about it.

License

Apache-2.0

Security

If you find a security issue, report it privately instead of opening a public issue. Bypasses of filesystem mediation, SSRF protection, or credential injection qualify. See SECURITY.md.

About

Give your agent a shell without giving it the keys to your machine.

Resources

Code of conduct

Contributing

Security policy

Stars

235 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - strands-agents/shell: Give your agent a shell without giving it the keys to your machine. · GitHub
Skip to content
Strands Agents

Strands Shell

Give your agent a shell without giving it the keys to your machine.

PythonNodeLicenseStrands Discord

DocumentationMCP ServerPythonNode.js


Agents run shell commands in tight loops: Install deps, run tests, grep for errors, iterate. Those loops need speed and isolation.

Strands Shell is a Bourne-compatible shell that runs in-process. grep, sed, jq, curl, find, 50+ commands and it does this without fork, exec, syscalls, or cold starts. You declare what the agent can reach (files, URLs, credentials) and everything else doesn't exist to the agent.

DockerCloud sandboxStrands Shell
Cold start~200ms~1s (network)<1ms
IsolationContainer namespaceMicroVMIn-process VFS
Networkiptables / sidecarPlatform policyURL allowlist + SSRF guard
SecretsEnv vars (agent can read them)Platform-specificInjected per-request, agent never sees them
SetupDocker daemonAPI key + networkpip install strands-shell
PlatformsLinuxCloud-onlymacOS, Linux, WASM

Quick Start

MCP (works with any agent framework)

Drop this into your MCP client config:

{
"mcpServers": {
"shell": {
"command": "uvx",
"args": ["strands-shell", "--mcp"]
}
}
}

That's it and your agent gets shell, read_file, write_file, list_dir. All mediated through the Kernel.

Python

pip install strands-shell
importstrands_shellshell=strands_shell.Shell(
binds=[strands_shell.Bind("/my/project", "/workspace", mode="copy")],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
allowed_urls=["https://api.example.com/"],
)
out=shell.run("grep -rn TODO /workspace")
print(out.stdout)

Node.js

npm install @strands-agents/shell
import{Shell}from'@strands-agents/shell'constshell=awaitShell.create({binds: [{source: '/my/project',destination: '/workspace',mode: 'copy'}],})constout=awaitshell.run('grep -rn TODO /workspace')console.log(out.stdout)

How It Works

flowchart TB
agent["Your agent code\n(Strands, LangGraph, Pydantic AI, etc)"]
agent -->|"MCP / Python / Node.js"| shell
subgraph shell ["Strands Shell"]
direction TB
subgraph kernel ["Kernel (mediation boundary)"]
vfs["VFS: isolated filesystem"]
net["Network: SSRF guard + allowlist"]
creds["Credentials: injected per-URL"]
limits["Limits: timeout, output, fds"]
end
engine["Shell engine: parser, 25 builtins, 33 commands, Lua 5.4"]
end
Loading

Written in Rust, with native bindings for Python (PyO3) and Node.js (napi-rs). State persists across run() calls (env vars, working directory, functions). The filesystem is shared.

Configuration

shell=strands_shell.Shell(
binds=[
strands_shell.Bind("/host/project", "/workspace", mode="copy"),
strands_shell.Bind("/tmp/output", "/output", mode="direct"),
],
credentials=[
strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN"),
],
allowed_urls=["https://api.example.com/", "https://pypi.org/"],
timeout=30.0,
env={"PROJECT": "demo"},
limits=strands_shell.Limits(
max_output=1<<20,
max_file_size=10<<20,
),
)

⚠️mode: "direct" mounts are live. The agent can read and modify host files in real time. Use only for designated output directories. Never direct-bind directories containing secrets, credentials, or configuration you don't want the agent to modify.

Inspecting configuration

A constructed shell exposes a read-only snapshot of how it was configured. This is useful when you embed Strands Shell as a sandbox in a larger framework and need to build tool descriptions, surface the network allowlist, or report the active resource caps from a shell object you were handed.

shell=strands_shell.Shell(
allowed_urls=["https://api.example.com/"],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
timeout=30.0,
)
cfg=shell.config# a frozen ShellConfig snapshotcfg.allowed_urls# ('https://api.example.com/',)cfg.timeout# 30.0cfg.credentials[0].url# 'https://api.example.com/'cfg.credentials[0].env_var# 'API_TOKEN' (the secret value is never exposed)
constshell=awaitShell.create({allowedUrls: ['https://api.example.com/'],credentials: [{url: 'https://api.example.com/',envVar: 'API_TOKEN'}],timeout: 30,})constcfg=awaitshell.config()// a deep-frozen snapshot objectcfg.allowedUrls// ['https://api.example.com/']cfg.timeout// 30cfg.credentials[0].envVar// 'API_TOKEN' (the secret value is never exposed)

The snapshot reports binds, credentials, the network allowlist, environment variables, umask, timeout, and resource limits. Credential secrets are never included — each entry reports its URL pattern, kind, and source (a literal token was supplied, or the name of the environment variable it is read from), so you can reason about credentials without the agent or your tooling ever seeing the secret itself.

TOML

You can load all of this from a config file instead:

[[bind]]
mode = "copy"source = "/host/project"destination = "/workspace"
[[cred]]
url = "https://api.openai.com/v1/"methods = ["POST"]
kind = "bearer"api_key_env = "OPENAI_API_KEY"
[[mcp]]
name = "my-tools"command = "/path/to/mcp-server"args = ["--stdio"]

MCP Server

The built-in MCP server exposes the shell over JSON-RPC on stdio, working with anything that speaks MCP.

uvx strands-shell --mcp # bare in-memory sandbox
uvx strands-shell --config sandbox.toml --mcp # with mounts + credentials

If you declare [[mcp]] servers in your TOML config, they show up as Lua modules inside the shell. Call require("my_tools") and you get a table of the server's tools.

Security Model

Strands Shell is a mediation layer, not a security sandbox. It enforces what the agent should access via Kernel-mediated deny-by-default. It does NOT protect against: memory-safety exploits in the shell engine itself, timing side-channels, or an attacker who controls the host process. For multi-tenant or adversarial workloads, run each Shell instance inside a container or microVM.

The Kernel mediates everything; it runs in the same process as your code, not in a VM. If your threat model is "untrusted tenant running arbitrary code," put Strands Shell inside a container too. For "my agent shouldn't access things I haven't explicitly allowed," the Kernel handles it.

Default-deny. You allowlist what the agent can reach:

  • Files: only bound paths exist, everything else is hidden.
  • Network: curl blocks private ranges (RFC1918, link-local, loopback, IMDS) by default while letting public URLs pass through. Use allowed_urls to permit specific internal hosts.
  • Secrets: the Kernel injects credentials per-URL at request time, ensuring the agent never holds them. The Kernel never re-injects on redirects, even back to the same host.
  • Syscalls: there are none; no fork, no exec because the shell is pure userspace.

If you bypass any of these, report it. See SECURITY.md.

Limits (best-effort): timeouts, output caps, fd limits, inode limits. These catch runaway agents but won't stop someone actively trying to break out. OS-level isolation for that.

Multi-tenant: a Shell instance is single-owner. If you're serving multiple agents, create one Shell per session. Construction is cheap (no containers, no VMs, just an in-memory VFS), so spinning up per-request is the intended pattern.

Secure Defaults

Out of the box, the shell is an empty sandbox — no files, no network, no credentials. When you grant access, follow least privilege:

  • Prefer mode: "copy" over mode: "direct" for source code. Copy-on-create isolates the agent from your live files. Use direct only for output directories where the agent needs to persist results.
  • Scope binds narrowly. Bind /my/project/src rather than /my/project or /. The agent doesn't need your .git/, .env, or node_modules/.
  • Allowlist URLs explicitly. Don't use allowed_urls: ["https://"] — this disables SSRF protection entirely. List the specific API endpoints the agent needs.
  • Set timeouts. The default has no per-command timeout. Set timeout to bound runaway commands (30s is reasonable for most agent loops).
  • Use limits. Set max_output to prevent agents from filling memory with unbounded command output (1MB is a good default).

Commands

25 builtins, 33 commands, and a Bourne-compatible shell with pipes, loops, functions, and subshells.

The commands agents use constantly: grep, find, cat, head, tail, jq for reading and searching. sed, sort, tr, cut for transforming output. cp, mv, rm, mkdir for managing files. curl for HTTP (SSRF-guarded, credentials auto-injected). lua for scripting when shell gets awkward.

The full command reference has the inventory with implementation status, supported flags, and known gaps vs GNU coreutils.

File Operations API

Read and write files without going through a shell command:

shell.write_file("/workspace/note.txt", b"hello")
data=shell.read_file("/workspace/note.txt")
entries=shell.list_files("/workspace")
shell.remove_file("/workspace/note.txt")

Contributing

See CONTRIBUTING.md. Bug reports and design questions are just as useful as PRs.

Community

Discord if you want to talk about it.

License

Apache-2.0

Security

If you find a security issue, report it privately instead of opening a public issue. Bypasses of filesystem mediation, SSRF protection, or credential injection qualify. See SECURITY.md.

About

Give your agent a shell without giving it the keys to your machine.

Resources

Code of conduct

Contributing

Security policy

Stars

235 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - strands-agents/shell: Give your agent a shell without giving it the keys to your machine. · GitHub
Skip to content
Strands Agents

Strands Shell

Give your agent a shell without giving it the keys to your machine.

PythonNodeLicenseStrands Discord

DocumentationMCP ServerPythonNode.js


Agents run shell commands in tight loops: Install deps, run tests, grep for errors, iterate. Those loops need speed and isolation.

Strands Shell is a Bourne-compatible shell that runs in-process. grep, sed, jq, curl, find, 50+ commands and it does this without fork, exec, syscalls, or cold starts. You declare what the agent can reach (files, URLs, credentials) and everything else doesn't exist to the agent.

DockerCloud sandboxStrands Shell
Cold start~200ms~1s (network)<1ms
IsolationContainer namespaceMicroVMIn-process VFS
Networkiptables / sidecarPlatform policyURL allowlist + SSRF guard
SecretsEnv vars (agent can read them)Platform-specificInjected per-request, agent never sees them
SetupDocker daemonAPI key + networkpip install strands-shell
PlatformsLinuxCloud-onlymacOS, Linux, WASM

Quick Start

MCP (works with any agent framework)

Drop this into your MCP client config:

{
"mcpServers": {
"shell": {
"command": "uvx",
"args": ["strands-shell", "--mcp"]
}
}
}

That's it and your agent gets shell, read_file, write_file, list_dir. All mediated through the Kernel.

Python

pip install strands-shell
importstrands_shellshell=strands_shell.Shell(
binds=[strands_shell.Bind("/my/project", "/workspace", mode="copy")],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
allowed_urls=["https://api.example.com/"],
)
out=shell.run("grep -rn TODO /workspace")
print(out.stdout)

Node.js

npm install @strands-agents/shell
import{Shell}from'@strands-agents/shell'constshell=awaitShell.create({binds: [{source: '/my/project',destination: '/workspace',mode: 'copy'}],})constout=awaitshell.run('grep -rn TODO /workspace')console.log(out.stdout)

How It Works

flowchart TB
agent["Your agent code\n(Strands, LangGraph, Pydantic AI, etc)"]
agent -->|"MCP / Python / Node.js"| shell
subgraph shell ["Strands Shell"]
direction TB
subgraph kernel ["Kernel (mediation boundary)"]
vfs["VFS: isolated filesystem"]
net["Network: SSRF guard + allowlist"]
creds["Credentials: injected per-URL"]
limits["Limits: timeout, output, fds"]
end
engine["Shell engine: parser, 25 builtins, 33 commands, Lua 5.4"]
end
Loading

Written in Rust, with native bindings for Python (PyO3) and Node.js (napi-rs). State persists across run() calls (env vars, working directory, functions). The filesystem is shared.

Configuration

shell=strands_shell.Shell(
binds=[
strands_shell.Bind("/host/project", "/workspace", mode="copy"),
strands_shell.Bind("/tmp/output", "/output", mode="direct"),
],
credentials=[
strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN"),
],
allowed_urls=["https://api.example.com/", "https://pypi.org/"],
timeout=30.0,
env={"PROJECT": "demo"},
limits=strands_shell.Limits(
max_output=1<<20,
max_file_size=10<<20,
),
)

⚠️mode: "direct" mounts are live. The agent can read and modify host files in real time. Use only for designated output directories. Never direct-bind directories containing secrets, credentials, or configuration you don't want the agent to modify.

Inspecting configuration

A constructed shell exposes a read-only snapshot of how it was configured. This is useful when you embed Strands Shell as a sandbox in a larger framework and need to build tool descriptions, surface the network allowlist, or report the active resource caps from a shell object you were handed.

shell=strands_shell.Shell(
allowed_urls=["https://api.example.com/"],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
timeout=30.0,
)
cfg=shell.config# a frozen ShellConfig snapshotcfg.allowed_urls# ('https://api.example.com/',)cfg.timeout# 30.0cfg.credentials[0].url# 'https://api.example.com/'cfg.credentials[0].env_var# 'API_TOKEN' (the secret value is never exposed)
constshell=awaitShell.create({allowedUrls: ['https://api.example.com/'],credentials: [{url: 'https://api.example.com/',envVar: 'API_TOKEN'}],timeout: 30,})constcfg=awaitshell.config()// a deep-frozen snapshot objectcfg.allowedUrls// ['https://api.example.com/']cfg.timeout// 30cfg.credentials[0].envVar// 'API_TOKEN' (the secret value is never exposed)

The snapshot reports binds, credentials, the network allowlist, environment variables, umask, timeout, and resource limits. Credential secrets are never included — each entry reports its URL pattern, kind, and source (a literal token was supplied, or the name of the environment variable it is read from), so you can reason about credentials without the agent or your tooling ever seeing the secret itself.

TOML

You can load all of this from a config file instead:

[[bind]]
mode = "copy"source = "/host/project"destination = "/workspace"
[[cred]]
url = "https://api.openai.com/v1/"methods = ["POST"]
kind = "bearer"api_key_env = "OPENAI_API_KEY"
[[mcp]]
name = "my-tools"command = "/path/to/mcp-server"args = ["--stdio"]

MCP Server

The built-in MCP server exposes the shell over JSON-RPC on stdio, working with anything that speaks MCP.

uvx strands-shell --mcp # bare in-memory sandbox
uvx strands-shell --config sandbox.toml --mcp # with mounts + credentials

If you declare [[mcp]] servers in your TOML config, they show up as Lua modules inside the shell. Call require("my_tools") and you get a table of the server's tools.

Security Model

Strands Shell is a mediation layer, not a security sandbox. It enforces what the agent should access via Kernel-mediated deny-by-default. It does NOT protect against: memory-safety exploits in the shell engine itself, timing side-channels, or an attacker who controls the host process. For multi-tenant or adversarial workloads, run each Shell instance inside a container or microVM.

The Kernel mediates everything; it runs in the same process as your code, not in a VM. If your threat model is "untrusted tenant running arbitrary code," put Strands Shell inside a container too. For "my agent shouldn't access things I haven't explicitly allowed," the Kernel handles it.

Default-deny. You allowlist what the agent can reach:

  • Files: only bound paths exist, everything else is hidden.
  • Network: curl blocks private ranges (RFC1918, link-local, loopback, IMDS) by default while letting public URLs pass through. Use allowed_urls to permit specific internal hosts.
  • Secrets: the Kernel injects credentials per-URL at request time, ensuring the agent never holds them. The Kernel never re-injects on redirects, even back to the same host.
  • Syscalls: there are none; no fork, no exec because the shell is pure userspace.

If you bypass any of these, report it. See SECURITY.md.

Limits (best-effort): timeouts, output caps, fd limits, inode limits. These catch runaway agents but won't stop someone actively trying to break out. OS-level isolation for that.

Multi-tenant: a Shell instance is single-owner. If you're serving multiple agents, create one Shell per session. Construction is cheap (no containers, no VMs, just an in-memory VFS), so spinning up per-request is the intended pattern.

Secure Defaults

Out of the box, the shell is an empty sandbox — no files, no network, no credentials. When you grant access, follow least privilege:

  • Prefer mode: "copy" over mode: "direct" for source code. Copy-on-create isolates the agent from your live files. Use direct only for output directories where the agent needs to persist results.
  • Scope binds narrowly. Bind /my/project/src rather than /my/project or /. The agent doesn't need your .git/, .env, or node_modules/.
  • Allowlist URLs explicitly. Don't use allowed_urls: ["https://"] — this disables SSRF protection entirely. List the specific API endpoints the agent needs.
  • Set timeouts. The default has no per-command timeout. Set timeout to bound runaway commands (30s is reasonable for most agent loops).
  • Use limits. Set max_output to prevent agents from filling memory with unbounded command output (1MB is a good default).

Commands

25 builtins, 33 commands, and a Bourne-compatible shell with pipes, loops, functions, and subshells.

The commands agents use constantly: grep, find, cat, head, tail, jq for reading and searching. sed, sort, tr, cut for transforming output. cp, mv, rm, mkdir for managing files. curl for HTTP (SSRF-guarded, credentials auto-injected). lua for scripting when shell gets awkward.

The full command reference has the inventory with implementation status, supported flags, and known gaps vs GNU coreutils.

File Operations API

Read and write files without going through a shell command:

shell.write_file("/workspace/note.txt", b"hello")
data=shell.read_file("/workspace/note.txt")
entries=shell.list_files("/workspace")
shell.remove_file("/workspace/note.txt")

Contributing

See CONTRIBUTING.md. Bug reports and design questions are just as useful as PRs.

Community

Discord if you want to talk about it.

License

Apache-2.0

Security

If you find a security issue, report it privately instead of opening a public issue. Bypasses of filesystem mediation, SSRF protection, or credential injection qualify. See SECURITY.md.

About

Give your agent a shell without giving it the keys to your machine.

Resources

Code of conduct

Contributing

Security policy

Stars

235 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - strands-agents/shell: Give your agent a shell without giving it the keys to your machine. · GitHub
Skip to content
Strands Agents

Strands Shell

Give your agent a shell without giving it the keys to your machine.

PythonNodeLicenseStrands Discord

DocumentationMCP ServerPythonNode.js


Agents run shell commands in tight loops: Install deps, run tests, grep for errors, iterate. Those loops need speed and isolation.

Strands Shell is a Bourne-compatible shell that runs in-process. grep, sed, jq, curl, find, 50+ commands and it does this without fork, exec, syscalls, or cold starts. You declare what the agent can reach (files, URLs, credentials) and everything else doesn't exist to the agent.

DockerCloud sandboxStrands Shell
Cold start~200ms~1s (network)<1ms
IsolationContainer namespaceMicroVMIn-process VFS
Networkiptables / sidecarPlatform policyURL allowlist + SSRF guard
SecretsEnv vars (agent can read them)Platform-specificInjected per-request, agent never sees them
SetupDocker daemonAPI key + networkpip install strands-shell
PlatformsLinuxCloud-onlymacOS, Linux, WASM

Quick Start

MCP (works with any agent framework)

Drop this into your MCP client config:

{
"mcpServers": {
"shell": {
"command": "uvx",
"args": ["strands-shell", "--mcp"]
}
}
}

That's it and your agent gets shell, read_file, write_file, list_dir. All mediated through the Kernel.

Python

pip install strands-shell
importstrands_shellshell=strands_shell.Shell(
binds=[strands_shell.Bind("/my/project", "/workspace", mode="copy")],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
allowed_urls=["https://api.example.com/"],
)
out=shell.run("grep -rn TODO /workspace")
print(out.stdout)

Node.js

npm install @strands-agents/shell
import{Shell}from'@strands-agents/shell'constshell=awaitShell.create({binds: [{source: '/my/project',destination: '/workspace',mode: 'copy'}],})constout=awaitshell.run('grep -rn TODO /workspace')console.log(out.stdout)

How It Works

flowchart TB
agent["Your agent code\n(Strands, LangGraph, Pydantic AI, etc)"]
agent -->|"MCP / Python / Node.js"| shell
subgraph shell ["Strands Shell"]
direction TB
subgraph kernel ["Kernel (mediation boundary)"]
vfs["VFS: isolated filesystem"]
net["Network: SSRF guard + allowlist"]
creds["Credentials: injected per-URL"]
limits["Limits: timeout, output, fds"]
end
engine["Shell engine: parser, 25 builtins, 33 commands, Lua 5.4"]
end
Loading

Written in Rust, with native bindings for Python (PyO3) and Node.js (napi-rs). State persists across run() calls (env vars, working directory, functions). The filesystem is shared.

Configuration

shell=strands_shell.Shell(
binds=[
strands_shell.Bind("/host/project", "/workspace", mode="copy"),
strands_shell.Bind("/tmp/output", "/output", mode="direct"),
],
credentials=[
strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN"),
],
allowed_urls=["https://api.example.com/", "https://pypi.org/"],
timeout=30.0,
env={"PROJECT": "demo"},
limits=strands_shell.Limits(
max_output=1<<20,
max_file_size=10<<20,
),
)

⚠️mode: "direct" mounts are live. The agent can read and modify host files in real time. Use only for designated output directories. Never direct-bind directories containing secrets, credentials, or configuration you don't want the agent to modify.

Inspecting configuration

A constructed shell exposes a read-only snapshot of how it was configured. This is useful when you embed Strands Shell as a sandbox in a larger framework and need to build tool descriptions, surface the network allowlist, or report the active resource caps from a shell object you were handed.

shell=strands_shell.Shell(
allowed_urls=["https://api.example.com/"],
credentials=[strands_shell.Cred("https://api.example.com/", env_var="API_TOKEN")],
timeout=30.0,
)
cfg=shell.config# a frozen ShellConfig snapshotcfg.allowed_urls# ('https://api.example.com/',)cfg.timeout# 30.0cfg.credentials[0].url# 'https://api.example.com/'cfg.credentials[0].env_var# 'API_TOKEN' (the secret value is never exposed)
constshell=awaitShell.create({allowedUrls: ['https://api.example.com/'],credentials: [{url: 'https://api.example.com/',envVar: 'API_TOKEN'}],timeout: 30,})constcfg=awaitshell.config()// a deep-frozen snapshot objectcfg.allowedUrls// ['https://api.example.com/']cfg.timeout// 30cfg.credentials[0].envVar// 'API_TOKEN' (the secret value is never exposed)

The snapshot reports binds, credentials, the network allowlist, environment variables, umask, timeout, and resource limits. Credential secrets are never included — each entry reports its URL pattern, kind, and source (a literal token was supplied, or the name of the environment variable it is read from), so you can reason about credentials without the agent or your tooling ever seeing the secret itself.

TOML

You can load all of this from a config file instead:

[[bind]]
mode = "copy"source = "/host/project"destination = "/workspace"
[[cred]]
url = "https://api.openai.com/v1/"methods = ["POST"]
kind = "bearer"api_key_env = "OPENAI_API_KEY"
[[mcp]]
name = "my-tools"command = "/path/to/mcp-server"args = ["--stdio"]

MCP Server

The built-in MCP server exposes the shell over JSON-RPC on stdio, working with anything that speaks MCP.

uvx strands-shell --mcp # bare in-memory sandbox
uvx strands-shell --config sandbox.toml --mcp # with mounts + credentials

If you declare [[mcp]] servers in your TOML config, they show up as Lua modules inside the shell. Call require("my_tools") and you get a table of the server's tools.

Security Model

Strands Shell is a mediation layer, not a security sandbox. It enforces what the agent should access via Kernel-mediated deny-by-default. It does NOT protect against: memory-safety exploits in the shell engine itself, timing side-channels, or an attacker who controls the host process. For multi-tenant or adversarial workloads, run each Shell instance inside a container or microVM.

The Kernel mediates everything; it runs in the same process as your code, not in a VM. If your threat model is "untrusted tenant running arbitrary code," put Strands Shell inside a container too. For "my agent shouldn't access things I haven't explicitly allowed," the Kernel handles it.

Default-deny. You allowlist what the agent can reach:

  • Files: only bound paths exist, everything else is hidden.
  • Network: curl blocks private ranges (RFC1918, link-local, loopback, IMDS) by default while letting public URLs pass through. Use allowed_urls to permit specific internal hosts.
  • Secrets: the Kernel injects credentials per-URL at request time, ensuring the agent never holds them. The Kernel never re-injects on redirects, even back to the same host.
  • Syscalls: there are none; no fork, no exec because the shell is pure userspace.

If you bypass any of these, report it. See SECURITY.md.

Limits (best-effort): timeouts, output caps, fd limits, inode limits. These catch runaway agents but won't stop someone actively trying to break out. OS-level isolation for that.

Multi-tenant: a Shell instance is single-owner. If you're serving multiple agents, create one Shell per session. Construction is cheap (no containers, no VMs, just an in-memory VFS), so spinning up per-request is the intended pattern.

Secure Defaults

Out of the box, the shell is an empty sandbox — no files, no network, no credentials. When you grant access, follow least privilege:

  • Prefer mode: "copy" over mode: "direct" for source code. Copy-on-create isolates the agent from your live files. Use direct only for output directories where the agent needs to persist results.
  • Scope binds narrowly. Bind /my/project/src rather than /my/project or /. The agent doesn't need your .git/, .env, or node_modules/.
  • Allowlist URLs explicitly. Don't use allowed_urls: ["https://"] — this disables SSRF protection entirely. List the specific API endpoints the agent needs.
  • Set timeouts. The default has no per-command timeout. Set timeout to bound runaway commands (30s is reasonable for most agent loops).
  • Use limits. Set max_output to prevent agents from filling memory with unbounded command output (1MB is a good default).

Commands

25 builtins, 33 commands, and a Bourne-compatible shell with pipes, loops, functions, and subshells.

The commands agents use constantly: grep, find, cat, head, tail, jq for reading and searching. sed, sort, tr, cut for transforming output. cp, mv, rm, mkdir for managing files. curl for HTTP (SSRF-guarded, credentials auto-injected). lua for scripting when shell gets awkward.

The full command reference has the inventory with implementation status, supported flags, and known gaps vs GNU coreutils.

File Operations API

Read and write files without going through a shell command:

shell.write_file("/workspace/note.txt", b"hello")
data=shell.read_file("/workspace/note.txt")
entries=shell.list_files("/workspace")
shell.remove_file("/workspace/note.txt")

Contributing

See CONTRIBUTING.md. Bug reports and design questions are just as useful as PRs.

Community

Discord if you want to talk about it.

License

Apache-2.0

Security

If you find a security issue, report it privately instead of opening a public issue. Bypasses of filesystem mediation, SSRF protection, or credential injection qualify. See SECURITY.md.

About

Give your agent a shell without giving it the keys to your machine.

Resources

Code of conduct

Contributing

Security policy

Stars

235 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages