fix: rename bash tool to exec (#748) (thanks @myfunc)

This commit is contained in:
Peter Steinberger 2026-01-12 02:49:55 +00:00
parent b33bd6aaeb
commit 98337a14b3
51 changed files with 294 additions and 252 deletions

View file

@ -10,6 +10,7 @@
- Docs: add beginner-friendly plugin quick start + expand Voice Call plugin docs. - Docs: add beginner-friendly plugin quick start + expand Voice Call plugin docs.
- Tests: add Docker plugin loader + tgz-install smoke test. - Tests: add Docker plugin loader + tgz-install smoke test.
- Tests: extend Docker plugin E2E to cover installing from local folders (`plugins.load.paths`) and `file:` npm specs. - Tests: extend Docker plugin E2E to cover installing from local folders (`plugins.load.paths`) and `file:` npm specs.
- Agents/Tools: rename the bash tool to exec (config alias maintained). (#748) — thanks @myfunc.
- Config: add `$include` directive for modular config files. (#731) — thanks @pasogott. - Config: add `$include` directive for modular config files. (#731) — thanks @pasogott.
- Build: set pnpm minimum release age to 2880 minutes (2 days). (#718) — thanks @dan-dr. - Build: set pnpm minimum release age to 2880 minutes (2 days). (#718) — thanks @dan-dr.
- macOS: prompt to install the global `clawdbot` CLI when missing in local mode; install via `clawd.bot/install-cli.sh` (no onboarding) and use external launchd/CLI instead of the embedded gateway runtime. - macOS: prompt to install the global `clawdbot` CLI when missing in local mode; install via `clawd.bot/install-cli.sh` (no onboarding) and use external launchd/CLI instead of the embedded gateway runtime.

View file

@ -200,9 +200,9 @@ Details: [Tailscale guide](https://docs.clawd.bot/tailscale) · [Web surfaces](h
Its perfectly fine to run the Gateway on a small Linux instance. Clients (macOS app, CLI, WebChat) can connect over **Tailscale Serve/Funnel** or **SSH tunnels**, and you can still pair device nodes (macOS/iOS/Android) to execute devicelocal actions when needed. Its perfectly fine to run the Gateway on a small Linux instance. Clients (macOS app, CLI, WebChat) can connect over **Tailscale Serve/Funnel** or **SSH tunnels**, and you can still pair device nodes (macOS/iOS/Android) to execute devicelocal actions when needed.
- **Gateway host** runs the bash tool and provider connections by default. - **Gateway host** runs the exec tool and provider connections by default.
- **Device nodes** run devicelocal actions (`system.run`, camera, screen recording, notifications) via `node.invoke`. - **Device nodes** run devicelocal actions (`system.run`, camera, screen recording, notifications) via `node.invoke`.
In short: bash runs where the Gateway lives; device actions run where the device lives. In short: exec runs where the Gateway lives; device actions run where the device lives.
Details: [Remote access](https://docs.clawd.bot/remote) · [Nodes](https://docs.clawd.bot/nodes) · [Security](https://docs.clawd.bot/security) Details: [Remote access](https://docs.clawd.bot/remote) · [Nodes](https://docs.clawd.bot/nodes) · [Security](https://docs.clawd.bot/security)

View file

@ -180,7 +180,7 @@ In group `120363403215116621@g.us` with agents `["alfred", "baerbel"]`:
Session: agent:alfred:whatsapp:group:120363403215116621@g.us Session: agent:alfred:whatsapp:group:120363403215116621@g.us
History: [user message, alfred's previous responses] History: [user message, alfred's previous responses]
Workspace: /Users/pascal/clawd-alfred/ Workspace: /Users/pascal/clawd-alfred/
Tools: read, write, bash Tools: read, write, exec
``` ```
**Bärbel's context:** **Bärbel's context:**
@ -230,10 +230,10 @@ Give agents only the tools they need:
{ {
"agents": { "agents": {
"reviewer": { "reviewer": {
"tools": { "allow": ["read", "bash"] } // Read-only "tools": { "allow": ["read", "exec"] } // Read-only
}, },
"fixer": { "fixer": {
"tools": { "allow": ["read", "write", "edit", "bash"] } // Read-write "tools": { "allow": ["read", "write", "edit", "exec"] } // Read-write
} }
} }
} }
@ -330,8 +330,8 @@ tail -f ~/.clawdbot/logs/gateway.log | grep broadcast
"agents": { "agents": {
"list": [ "list": [
{ "id": "code-formatter", "workspace": "~/agents/formatter", "tools": { "allow": ["read", "write"] } }, { "id": "code-formatter", "workspace": "~/agents/formatter", "tools": { "allow": ["read", "write"] } },
{ "id": "security-scanner", "workspace": "~/agents/security", "tools": { "allow": ["read", "bash"] } }, { "id": "security-scanner", "workspace": "~/agents/security", "tools": { "allow": ["read", "exec"] } },
{ "id": "test-coverage", "workspace": "~/agents/testing", "tools": { "allow": ["read", "bash"] } }, { "id": "test-coverage", "workspace": "~/agents/testing", "tools": { "allow": ["read", "exec"] } },
{ "id": "docs-checker", "workspace": "~/agents/docs", "tools": { "allow": ["read"] } } { "id": "docs-checker", "workspace": "~/agents/docs", "tools": { "allow": ["read"] } }
] ]
} }

View file

@ -45,7 +45,7 @@ To disable bootstrap file creation entirely (for pre-seeded workspaces), set:
## Built-in tools ## Built-in tools
Core tools (read/bash/edit/write and related system tools) are always available. `TOOLS.md` does **not** control which tools exist; its guidance for how *you* want them used. Core tools (read/exec/edit/write and related system tools) are always available. `TOOLS.md` does **not** control which tools exist; its guidance for how *you* want them used.
## Skills ## Skills

View file

@ -217,7 +217,7 @@ Starting with v2026.1.6, each agent can have its own sandbox and tool restrictio
}, },
tools: { tools: {
allow: ["read"], // Only read tool allow: ["read"], // Only read tool
deny: ["bash", "write", "edit"], // Deny others deny: ["exec", "write", "edit"], // Deny others
}, },
}, },
], ],
@ -231,7 +231,7 @@ Starting with v2026.1.6, each agent can have its own sandbox and tool restrictio
- **Flexible policies**: Different permissions per agent - **Flexible policies**: Different permissions per agent
Note: `tools.elevated` is **global** and sender-based; it is not configurable per agent. Note: `tools.elevated` is **global** and sender-based; it is not configurable per agent.
If you need per-agent boundaries, use `agents.list[].tools` to deny `bash`. If you need per-agent boundaries, use `agents.list[].tools` to deny `exec`.
For group targeting, use `agents.list[].groupChat.mentionPatterns` so @mentions map cleanly to the intended agent. For group targeting, use `agents.list[].groupChat.mentionPatterns` so @mentions map cleanly to the intended agent.
See [Multi-Agent Sandbox & Tools](/multi-agent-sandbox-tools) for detailed examples. See [Multi-Agent Sandbox & Tools](/multi-agent-sandbox-tools) for detailed examples.

View file

@ -93,7 +93,7 @@ Restrict pruning to specific tools:
agent: { agent: {
contextPruning: { contextPruning: {
mode: "adaptive", mode: "adaptive",
tools: { allow: ["bash", "read"], deny: ["*image*"] } tools: { allow: ["exec", "read"], deny: ["*image*"] }
} }
} }
} }

View file

@ -19,7 +19,7 @@ The prompt is intentionally compact and uses fixed sections:
- **Clawdbot Self-Update**: how to run `config.apply` and `update.run`. - **Clawdbot Self-Update**: how to run `config.apply` and `update.run`.
- **Workspace**: working directory (`agents.defaults.workspace`). - **Workspace**: working directory (`agents.defaults.workspace`).
- **Workspace Files (injected)**: indicates bootstrap files are included below. - **Workspace Files (injected)**: indicates bootstrap files are included below.
- **Sandbox** (when enabled): indicates sandboxed runtime, sandbox paths, and whether elevated bash is available. - **Sandbox** (when enabled): indicates sandboxed runtime, sandbox paths, and whether elevated exec is available.
- **Time**: UTC default + the users local time (already converted). - **Time**: UTC default + the users local time (already converted).
- **Reply Tags**: optional reply tag syntax for supported providers. - **Reply Tags**: optional reply tag syntax for supported providers.
- **Heartbeats**: heartbeat prompt and ack behavior. - **Heartbeats**: heartbeat prompt and ack behavior.

View file

@ -103,7 +103,11 @@
}, },
{ {
"source": "/bash", "source": "/bash",
"destination": "/tools/bash" "destination": "/tools/exec"
},
{
"source": "/tools/bash",
"destination": "/tools/exec"
}, },
{ {
"source": "/bonjour", "source": "/bonjour",
@ -696,7 +700,7 @@
"pages": [ "pages": [
"tools", "tools",
"plugin", "plugin",
"tools/bash", "tools/exec",
"tools/elevated", "tools/elevated",
"tools/browser", "tools/browser",
"tools/browser-linux-troubleshooting", "tools/browser-linux-troubleshooting",

View file

@ -1,15 +1,15 @@
--- ---
summary: "Background bash execution and process management" summary: "Background exec execution and process management"
read_when: read_when:
- Adding or modifying background bash behavior - Adding or modifying background exec behavior
- Debugging long-running bash tasks - Debugging long-running exec tasks
--- ---
# Background Bash + Process Tool # Background Exec + Process Tool
Clawdbot runs shell commands through the `bash` tool and keeps longrunning tasks in memory. The `process` tool manages those background sessions. Clawdbot runs shell commands through the `exec` tool and keeps longrunning tasks in memory. The `process` tool manages those background sessions.
## bash tool ## exec tool
Key parameters: Key parameters:
- `command` (required) - `command` (required)
@ -24,7 +24,7 @@ Behavior:
- Foreground runs return output directly. - Foreground runs return output directly.
- When backgrounded (explicit or timeout), the tool returns `status: "running"` + `sessionId` and a short tail. - When backgrounded (explicit or timeout), the tool returns `status: "running"` + `sessionId` and a short tail.
- Output is kept in memory until the session is polled or cleared. - Output is kept in memory until the session is polled or cleared.
- If the `process` tool is disallowed, `bash` runs synchronously and ignores `yieldMs`/`background`. - If the `process` tool is disallowed, `exec` runs synchronously and ignores `yieldMs`/`background`.
Environment overrides: Environment overrides:
- `PI_BASH_YIELD_MS`: default yield (ms) - `PI_BASH_YIELD_MS`: default yield (ms)
@ -32,9 +32,9 @@ Environment overrides:
- `PI_BASH_JOB_TTL_MS`: TTL for finished sessions (ms, bounded to 1m3h) - `PI_BASH_JOB_TTL_MS`: TTL for finished sessions (ms, bounded to 1m3h)
Config (preferred): Config (preferred):
- `tools.bash.backgroundMs` (default 10000) - `tools.exec.backgroundMs` (default 10000)
- `tools.bash.timeoutSec` (default 1800) - `tools.exec.timeoutSec` (default 1800)
- `tools.bash.cleanupMs` (default 1800000) - `tools.exec.cleanupMs` (default 1800000)
## process tool ## process tool
@ -59,7 +59,7 @@ Notes:
Run a long task and poll later: Run a long task and poll later:
```json ```json
{"tool": "bash", "command": "sleep 5 && echo done", "yieldMs": 1000} {"tool": "exec", "command": "sleep 5 && echo done", "yieldMs": 1000}
``` ```
```json ```json
{"tool": "process", "action": "poll", "sessionId": "<id>"} {"tool": "process", "action": "poll", "sessionId": "<id>"}
@ -67,7 +67,7 @@ Run a long task and poll later:
Start immediately in background: Start immediately in background:
```json ```json
{"tool": "bash", "command": "npm run build", "background": true} {"tool": "exec", "command": "npm run build", "background": true}
``` ```
Send stdin: Send stdin:

View file

@ -259,9 +259,9 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
}, },
tools: { tools: {
allow: ["bash", "process", "read", "write", "edit"], allow: ["exec", "process", "read", "write", "edit"],
deny: ["browser", "canvas"], deny: ["browser", "canvas"],
bash: { exec: {
backgroundMs: 10000, backgroundMs: 10000,
timeoutSec: 1800, timeoutSec: 1800,
cleanupMs: 1800000 cleanupMs: 1800000

View file

@ -638,7 +638,7 @@ Read-only tools + read-only workspace:
}, },
tools: { tools: {
allow: ["read", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"], allow: ["read", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"],
deny: ["write", "edit", "bash", "process", "browser"] deny: ["write", "edit", "exec", "process", "browser"]
} }
} }
] ]
@ -661,7 +661,7 @@ No filesystem access (messaging/session tools enabled):
}, },
tools: { tools: {
allow: ["sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status", "whatsapp", "telegram", "slack", "discord", "gateway"], allow: ["sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status", "whatsapp", "telegram", "slack", "discord", "gateway"],
deny: ["read", "write", "edit", "bash", "process", "browser", "canvas", "nodes", "cron", "gateway", "image"] deny: ["read", "write", "edit", "exec", "process", "browser", "canvas", "nodes", "cron", "gateway", "image"]
} }
} }
] ]
@ -1274,7 +1274,7 @@ Example:
maxConcurrent: 1, maxConcurrent: 1,
archiveAfterMinutes: 60 archiveAfterMinutes: 60
}, },
bash: { exec: {
backgroundMs: 10000, backgroundMs: 10000,
timeoutSec: 1800, timeoutSec: 1800,
cleanupMs: 1800000 cleanupMs: 1800000
@ -1427,10 +1427,11 @@ Z.AI models are available as `zai/<model>` (e.g. `zai/glm-4.7`) and require
Heartbeats run full agent turns. Shorter intervals burn more tokens; be mindful Heartbeats run full agent turns. Shorter intervals burn more tokens; be mindful
of `every`, keep `HEARTBEAT.md` tiny, and/or choose a cheaper `model`. of `every`, keep `HEARTBEAT.md` tiny, and/or choose a cheaper `model`.
`tools.bash` configures background bash defaults: `tools.exec` configures background exec defaults:
- `backgroundMs`: time before auto-background (ms, default 10000) - `backgroundMs`: time before auto-background (ms, default 10000)
- `timeoutSec`: auto-kill after this runtime (seconds, default 1800) - `timeoutSec`: auto-kill after this runtime (seconds, default 1800)
- `cleanupMs`: how long to keep finished sessions in memory (ms, default 1800000) - `cleanupMs`: how long to keep finished sessions in memory (ms, default 1800000)
Legacy: `tools.bash` is still accepted as an alias.
`agents.defaults.subagents` configures sub-agent defaults: `agents.defaults.subagents` configures sub-agent defaults:
- `maxConcurrent`: max concurrent sub-agent runs (default 1) - `maxConcurrent`: max concurrent sub-agent runs (default 1)
@ -1447,7 +1448,7 @@ Example (disable browser/canvas everywhere):
} }
``` ```
`tools.elevated` controls elevated (host) bash access: `tools.elevated` controls elevated (host) exec access:
- `enabled`: allow elevated mode (default true) - `enabled`: allow elevated mode (default true)
- `allowFrom`: per-provider allowlists (empty = disabled) - `allowFrom`: per-provider allowlists (empty = disabled)
- `whatsapp`: E.164 numbers - `whatsapp`: E.164 numbers
@ -1491,8 +1492,8 @@ Per-agent override (further restrict):
Notes: Notes:
- `tools.elevated` is the global baseline. `agents.list[].tools.elevated` can only further restrict (both must allow). - `tools.elevated` is the global baseline. `agents.list[].tools.elevated` can only further restrict (both must allow).
- `/elevated on|off` stores state per session key; inline directives apply to a single message. - `/elevated on|off` stores state per session key; inline directives apply to a single message.
- Elevated `bash` runs on the host and bypasses sandboxing. - Elevated `exec` runs on the host and bypasses sandboxing.
- Tool policy still applies; if `bash` is denied, elevated cannot be used. - Tool policy still applies; if `exec` is denied, elevated cannot be used.
`agents.defaults.maxConcurrent` sets the maximum number of embedded agent runs that can `agents.defaults.maxConcurrent` sets the maximum number of embedded agent runs that can
execute in parallel across sessions. Each session is still serialized (one run execute in parallel across sessions. Each session is still serialized (one run
@ -1513,7 +1514,7 @@ Defaults (if enabled):
- `"ro"`: keep the sandbox workspace at `/workspace`, and mount the agent workspace read-only at `/agent` (disables `write`/`edit`) - `"ro"`: keep the sandbox workspace at `/workspace`, and mount the agent workspace read-only at `/agent` (disables `write`/`edit`)
- `"rw"`: mount the agent workspace read/write at `/workspace` - `"rw"`: mount the agent workspace read/write at `/workspace`
- auto-prune: idle > 24h OR age > 7d - auto-prune: idle > 24h OR age > 7d
- tool policy: allow only `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` (deny wins) - tool policy: allow only `exec`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` (deny wins)
- configure via `tools.sandbox.tools`, override per-agent via `agents.list[].tools.sandbox.tools` - configure via `tools.sandbox.tools`, override per-agent via `agents.list[].tools.sandbox.tools`
- optional sandboxed browser (Chromium + CDP, noVNC observer) - optional sandboxed browser (Chromium + CDP, noVNC observer)
- hardening knobs: `network`, `user`, `pidsLimit`, `memory`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile` - hardening knobs: `network`, `user`, `pidsLimit`, `memory`, `cpus`, `ulimits`, `seccompProfile`, `apparmorProfile`
@ -1584,7 +1585,7 @@ Legacy: `perSession` is still supported (`true` → `scope: "session"`,
tools: { tools: {
sandbox: { sandbox: {
tools: { tools: {
allow: ["bash", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"], allow: ["exec", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"],
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"] deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"]
} }
} }

View file

@ -111,7 +111,7 @@ Current migrations:
- `routing.agentToAgent``tools.agentToAgent` - `routing.agentToAgent``tools.agentToAgent`
- `routing.transcribeAudio``tools.audio.transcription` - `routing.transcribeAudio``tools.audio.transcription`
- `identity``agents.list[].identity` - `identity``agents.list[].identity`
- `agent.*``agents.defaults` + `tools.*` (tools/elevated/bash/sandbox/subagents) - `agent.*``agents.defaults` + `tools.*` (tools/elevated/exec/sandbox/subagents)
- `agent.model`/`allowedModels`/`modelAliases`/`modelFallbacks`/`imageModelFallbacks` - `agent.model`/`allowedModels`/`modelAliases`/`modelFallbacks`/`imageModelFallbacks`
`agents.defaults.models` + `agents.defaults.model.primary/fallbacks` + `agents.defaults.imageModel.primary/fallbacks` `agents.defaults.models` + `agents.defaults.model.primary/fallbacks` + `agents.defaults.imageModel.primary/fallbacks`

View file

@ -50,7 +50,7 @@ You can tune console verbosity independently via:
## Tool summary redaction ## Tool summary redaction
Verbose tool summaries (e.g. `🛠️ bash: ...`) can mask sensitive tokens before they hit the Verbose tool summaries (e.g. `🛠️ exec: ...`) can mask sensitive tokens before they hit the
console stream. This is **tools-only** and does not alter file logs. console stream. This is **tools-only** and does not alter file logs.
- `logging.redactSensitive`: `off` | `tools` (default: `tools`) - `logging.redactSensitive`: `off` | `tools` (default: `tools`)

View file

@ -1,6 +1,6 @@
--- ---
title: Sandbox vs Tool Policy vs Elevated title: Sandbox vs Tool Policy vs Elevated
summary: "Why a tool is blocked: sandbox runtime, tool allow/deny policy, and elevated bash gates" summary: "Why a tool is blocked: sandbox runtime, tool allow/deny policy, and elevated exec gates"
read_when: "You hit 'sandbox jail' or see a tool/elevated refusal and want the exact config key to change." read_when: "You hit 'sandbox jail' or see a tool/elevated refusal and want the exact config key to change."
status: active status: active
--- ---
@ -11,7 +11,7 @@ Clawdbot has three related (but different) controls:
1. **Sandbox** (`agents.defaults.sandbox.*` / `agents.list[].sandbox.*`) decides **where tools run** (Docker vs host). 1. **Sandbox** (`agents.defaults.sandbox.*` / `agents.list[].sandbox.*`) decides **where tools run** (Docker vs host).
2. **Tool policy** (`tools.*`, `tools.sandbox.tools.*`, `agents.list[].tools.*`) decides **which tools are available/allowed**. 2. **Tool policy** (`tools.*`, `tools.sandbox.tools.*`, `agents.list[].tools.*`) decides **which tools are available/allowed**.
3. **Elevated** (`tools.elevated.*`, `agents.list[].tools.elevated.*`) is a **bash-only escape hatch** to run on the host when youre sandboxed. 3. **Elevated** (`tools.elevated.*`, `agents.list[].tools.elevated.*`) is an **exec-only escape hatch** to run on the host when youre sandboxed.
## Quick debug ## Quick debug
@ -49,10 +49,10 @@ Rules of thumb:
- `deny` always wins. - `deny` always wins.
- If `allow` is non-empty, everything else is treated as blocked. - If `allow` is non-empty, everything else is treated as blocked.
## Elevated: bash-only “run on host” ## Elevated: exec-only “run on host”
Elevated does **not** grant extra tools; it only affects `bash`. Elevated does **not** grant extra tools; it only affects `exec`.
- If youre sandboxed, `/elevated on` (or `bash` with `elevated: true`) runs on the host. - If youre sandboxed, `/elevated on` (or `exec` with `elevated: true`) runs on the host.
- If youre already running direct, elevated is effectively a no-op (still gated). - If youre already running direct, elevated is effectively a no-op (still gated).
Gates: Gates:
@ -74,4 +74,3 @@ Fix-it keys (pick one):
### “I thought this was main, why is it sandboxed?” ### “I thought this was main, why is it sandboxed?”
In `"non-main"` mode, group/channel keys are *not* main. Use the main session key (shown by `sandbox explain`) or switch mode to `"off"`. In `"non-main"` mode, group/channel keys are *not* main. Use the main session key (shown by `sandbox explain`) or switch mode to `"off"`.

View file

@ -17,7 +17,7 @@ This is not a perfect security boundary, but it materially limits filesystem
and process access when the model does something dumb. and process access when the model does something dumb.
## What gets sandboxed ## What gets sandboxed
- Tool execution (`bash`, `read`, `write`, `edit`, `process`, etc.). - Tool execution (`exec`, `read`, `write`, `edit`, `process`, etc.).
- Optional sandboxed browser (`agents.defaults.sandbox.browser`). - Optional sandboxed browser (`agents.defaults.sandbox.browser`).
- By default, the sandbox browser auto-starts (ensures CDP is reachable) when the browser tool needs it. - By default, the sandbox browser auto-starts (ensures CDP is reachable) when the browser tool needs it.
Configure via `agents.defaults.sandbox.browser.autoStart` and `agents.defaults.sandbox.browser.autoStartTimeoutMs`. Configure via `agents.defaults.sandbox.browser.autoStart` and `agents.defaults.sandbox.browser.autoStartTimeoutMs`.
@ -27,7 +27,7 @@ and process access when the model does something dumb.
Not sandboxed: Not sandboxed:
- The Gateway process itself. - The Gateway process itself.
- Any tool explicitly allowed to run on the host (e.g. `tools.elevated`). - Any tool explicitly allowed to run on the host (e.g. `tools.elevated`).
- **Elevated bash runs on the host and bypasses sandboxing.** - **Elevated exec runs on the host and bypasses sandboxing.**
- If sandboxing is off, `tools.elevated` does not change execution (already on host). See [Elevated Mode](/tools/elevated). - If sandboxing is off, `tools.elevated` does not change execution (already on host). See [Elevated Mode](/tools/elevated).
## Modes ## Modes
@ -79,7 +79,7 @@ Docker installs and the containerized gateway live here:
Tool allow/deny policies still apply before sandbox rules. If a tool is denied Tool allow/deny policies still apply before sandbox rules. If a tool is denied
globally or per-agent, sandboxing doesnt bring it back. globally or per-agent, sandboxing doesnt bring it back.
`tools.elevated` is an explicit escape hatch that runs `bash` on the host. `tools.elevated` is an explicit escape hatch that runs `exec` on the host.
Debugging: Debugging:
- Use `clawdbot sandbox explain` to inspect effective sandbox mode, tool policy, and fix-it config keys. - Use `clawdbot sandbox explain` to inspect effective sandbox mode, tool policy, and fix-it config keys.

View file

@ -184,7 +184,7 @@ Consider running your AI on a separate phone number from your personal one:
You can already build a read-only profile by combining: You can already build a read-only profile by combining:
- `agents.defaults.sandbox.workspaceAccess: "ro"` (or `"none"` for no workspace access) - `agents.defaults.sandbox.workspaceAccess: "ro"` (or `"none"` for no workspace access)
- tool allow/deny lists that block `write`, `edit`, `bash`, `process`, etc. - tool allow/deny lists that block `write`, `edit`, `exec`, `process`, etc.
We may add a single `readOnlyMode` flag later to simplify this configuration. We may add a single `readOnlyMode` flag later to simplify this configuration.
@ -206,7 +206,7 @@ Also consider agent workspace access inside the sandbox:
- `agents.defaults.sandbox.workspaceAccess: "ro"` mounts the agent workspace read-only at `/agent` (disables `write`/`edit`) - `agents.defaults.sandbox.workspaceAccess: "ro"` mounts the agent workspace read-only at `/agent` (disables `write`/`edit`)
- `agents.defaults.sandbox.workspaceAccess: "rw"` mounts the agent workspace read/write at `/workspace` - `agents.defaults.sandbox.workspaceAccess: "rw"` mounts the agent workspace read/write at `/workspace`
Important: `tools.elevated` is the global baseline escape hatch that runs bash on the host. Keep `tools.elevated.allowFrom` tight and dont enable it for strangers. You can further restrict elevated per agent via `agents.list[].tools.elevated`. See [Elevated Mode](/tools/elevated). Important: `tools.elevated` is the global baseline escape hatch that runs exec on the host. Keep `tools.elevated.allowFrom` tight and dont enable it for strangers. You can further restrict elevated per agent via `agents.list[].tools.elevated`. See [Elevated Mode](/tools/elevated).
## Browser control risks ## Browser control risks
@ -261,7 +261,7 @@ Common use cases:
}, },
tools: { tools: {
allow: ["read"], allow: ["read"],
deny: ["write", "edit", "bash", "process", "browser"] deny: ["write", "edit", "exec", "process", "browser"]
} }
} }
] ]
@ -285,7 +285,7 @@ Common use cases:
}, },
tools: { tools: {
allow: ["sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status", "whatsapp", "telegram", "slack", "discord", "gateway"], allow: ["sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status", "whatsapp", "telegram", "slack", "discord", "gateway"],
deny: ["read", "write", "edit", "bash", "process", "browser", "canvas", "nodes", "cron", "gateway", "image"] deny: ["read", "write", "edit", "exec", "process", "browser", "canvas", "nodes", "cron", "gateway", "image"]
} }
} }
] ]

View file

@ -250,7 +250,7 @@ precedence, and troubleshooting.
- `"rw"` mounts the agent workspace read/write at `/workspace` - `"rw"` mounts the agent workspace read/write at `/workspace`
- Auto-prune: idle > 24h OR age > 7d - Auto-prune: idle > 24h OR age > 7d
- Network: `none` by default (explicitly opt-in if you need egress) - Network: `none` by default (explicitly opt-in if you need egress)
- Default allow: `bash`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status` - Default allow: `exec`, `process`, `read`, `write`, `edit`, `sessions_list`, `sessions_history`, `sessions_send`, `sessions_spawn`, `session_status`
- Default deny: `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway` - Default deny: `browser`, `canvas`, `nodes`, `cron`, `discord`, `gateway`
### Enable sandboxing ### Enable sandboxing
@ -297,7 +297,7 @@ precedence, and troubleshooting.
tools: { tools: {
sandbox: { sandbox: {
tools: { tools: {
allow: ["bash", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"], allow: ["exec", "process", "read", "write", "edit", "sessions_list", "sessions_history", "sessions_send", "sessions_spawn", "session_status"],
deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"] deny: ["browser", "canvas", "nodes", "cron", "discord", "gateway"]
} }
} }
@ -424,7 +424,7 @@ Example:
### Security notes ### Security notes
- Hard wall only applies to **tools** (bash/read/write/edit). - Hard wall only applies to **tools** (exec/read/write/edit).
- Host-only tools like browser/camera/canvas are blocked by default. - Host-only tools like browser/camera/canvas are blocked by default.
- Allowing `browser` in sandbox **breaks isolation** (browser runs on host). - Allowing `browser` in sandbox **breaks isolation** (browser runs on host).

View file

@ -48,7 +48,7 @@ For debugging “why is this blocked?”, see [Sandbox vs Tool Policy vs Elevate
}, },
"tools": { "tools": {
"allow": ["read"], "allow": ["read"],
"deny": ["bash", "write", "edit", "process", "browser"] "deny": ["exec", "write", "edit", "process", "browser"]
} }
} }
] ]
@ -95,7 +95,7 @@ For debugging “why is this blocked?”, see [Sandbox vs Tool Policy vs Elevate
"workspaceRoot": "/tmp/work-sandboxes" "workspaceRoot": "/tmp/work-sandboxes"
}, },
"tools": { "tools": {
"allow": ["read", "write", "bash"], "allow": ["read", "write", "exec"],
"deny": ["browser", "gateway", "discord"] "deny": ["browser", "gateway", "discord"]
} }
} }
@ -134,7 +134,7 @@ For debugging “why is this blocked?”, see [Sandbox vs Tool Policy vs Elevate
}, },
"tools": { "tools": {
"allow": ["read"], "allow": ["read"],
"deny": ["bash", "write", "edit"] "deny": ["exec", "write", "edit"]
} }
} }
] ]
@ -177,7 +177,7 @@ If `agents.list[].tools.sandbox.tools` is set, it replaces `tools.sandbox.tools`
`tools.elevated` is the global baseline (sender-based allowlist). `agents.list[].tools.elevated` can further restrict elevated for specific agents (both must allow). `tools.elevated` is the global baseline (sender-based allowlist). `agents.list[].tools.elevated` can further restrict elevated for specific agents (both must allow).
Mitigation patterns: Mitigation patterns:
- Deny `bash` for untrusted agents (`agents.list[].tools.deny: ["bash"]`) - Deny `exec` for untrusted agents (`agents.list[].tools.deny: ["exec"]`)
- Avoid allowlisting senders that route to restricted agents - Avoid allowlisting senders that route to restricted agents
- Disable elevated globally (`tools.elevated.enabled: false`) if you only want sandboxed execution - Disable elevated globally (`tools.elevated.enabled: false`) if you only want sandboxed execution
- Disable elevated per agent (`agents.list[].tools.elevated.enabled: false`) for sensitive profiles - Disable elevated per agent (`agents.list[].tools.elevated.enabled: false`) for sensitive profiles
@ -200,7 +200,7 @@ Mitigation patterns:
"tools": { "tools": {
"sandbox": { "sandbox": {
"tools": { "tools": {
"allow": ["read", "write", "bash"], "allow": ["read", "write", "exec"],
"deny": [] "deny": []
} }
} }
@ -235,7 +235,7 @@ Legacy `agent.*` configs are migrated by `clawdbot doctor`; prefer `agents.defau
{ {
"tools": { "tools": {
"allow": ["read"], "allow": ["read"],
"deny": ["bash", "write", "edit", "process"] "deny": ["exec", "write", "edit", "process"]
} }
} }
``` ```
@ -244,7 +244,7 @@ Legacy `agent.*` configs are migrated by `clawdbot doctor`; prefer `agents.defau
```json ```json
{ {
"tools": { "tools": {
"allow": ["read", "bash", "process"], "allow": ["read", "exec", "process"],
"deny": ["write", "edit", "browser", "gateway"] "deny": ["write", "edit", "browser", "gateway"]
} }
} }
@ -255,7 +255,7 @@ Legacy `agent.*` configs are migrated by `clawdbot doctor`; prefer `agents.defau
{ {
"tools": { "tools": {
"allow": ["sessions_list", "sessions_send", "sessions_history", "session_status"], "allow": ["sessions_list", "sessions_send", "sessions_history", "session_status"],
"deny": ["bash", "write", "edit", "read", "browser"] "deny": ["exec", "write", "edit", "read", "browser"]
} }
} }
``` ```
@ -276,12 +276,12 @@ sandbox, set `agents.list[].sandbox.mode: "off"`.
After configuring multi-agent sandbox and tools: After configuring multi-agent sandbox and tools:
1. **Check agent resolution:** 1. **Check agent resolution:**
```bash ```exec
clawdbot agents list --bindings clawdbot agents list --bindings
``` ```
2. **Verify sandbox containers:** 2. **Verify sandbox containers:**
```bash ```exec
docker ps --filter "label=clawdbot.sandbox=1" docker ps --filter "label=clawdbot.sandbox=1"
``` ```
@ -290,7 +290,7 @@ After configuring multi-agent sandbox and tools:
- Verify the agent cannot use denied tools - Verify the agent cannot use denied tools
4. **Monitor logs:** 4. **Monitor logs:**
```bash ```exec
tail -f "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}/logs/gateway.log" | grep -E "routing|sandbox|tools" tail -f "${CLAWDBOT_STATE_DIR:-$HOME/.clawdbot}/logs/gateway.log" | grep -E "routing|sandbox|tools"
``` ```

View file

@ -25,7 +25,7 @@ read_when:
- `overridden(ActivityKind)` (debug override) - `overridden(ActivityKind)` (debug override)
### ActivityKind → glyph ### ActivityKind → glyph
- `bash` → 💻 - `exec` → 💻
- `read` → 📄 - `read` → 📄
- `write` → ✍️ - `write` → ✍️
- `edit` → 📝 - `edit` → 📝
@ -40,7 +40,7 @@ read_when:
## Status row text (menu) ## Status row text (menu)
- While work is active: `<Session role> · <activity label>` - While work is active: `<Session role> · <activity label>`
- Examples: `Main · bash: pnpm test`, `Other · read: apps/macos/Sources/Clawdbot/AppState.swift`. - Examples: `Main · exec: pnpm test`, `Other · read: apps/macos/Sources/Clawdbot/AppState.swift`.
- When idle: falls back to the health summary. - When idle: falls back to the health summary.
## Event ingestion ## Event ingestion
@ -49,7 +49,7 @@ read_when:
- `stream: "job"` with `data.state` for start/stop. - `stream: "job"` with `data.state` for start/stop.
- `stream: "tool"` with `data.phase`, `name`, optional `meta`/`args`. - `stream: "tool"` with `data.phase`, `name`, optional `meta`/`args`.
- Labels: - Labels:
- `bash`: first line of `args.command`. - `exec`: first line of `args.command`.
- `read`/`write`: shortened path. - `read`/`write`: shortened path.
- `edit`: path plus inferred change kind from `meta`/diff counts. - `edit`: path plus inferred change kind from `meta`/diff counts.
- fallback: tool name. - fallback: tool name.

View file

@ -836,7 +836,7 @@ exit
These are abort triggers (not slash commands). These are abort triggers (not slash commands).
For background processes (from the bash tool), you can ask the agent to run: For background processes (from the exec tool), you can ask the agent to run:
``` ```
process action:kill sessionId:XXX process action:kill sessionId:XXX

View file

@ -95,7 +95,7 @@ Use these hubs to discover every page, including deep dives and reference docs t
- [Tools surface](/tools) - [Tools surface](/tools)
- [CLI reference](/cli) - [CLI reference](/cli)
- [Bash tool](/tools/bash) - [Exec tool](/tools/exec)
- [Elevated mode](/tools/elevated) - [Elevated mode](/tools/elevated)
- [Cron jobs](/automation/cron-jobs) - [Cron jobs](/automation/cron-jobs)
- [Thinking + verbose](/tools/thinking) - [Thinking + verbose](/tools/thinking)

View file

@ -120,11 +120,11 @@ Live tests are split into two layers so we can isolate failures:
- Iterate models-with-keys and assert: - Iterate models-with-keys and assert:
- “meaningful” response (no tools) - “meaningful” response (no tools)
- a real tool invocation works (read probe) - a real tool invocation works (read probe)
- optional extra tool probes (bash+read probe) - optional extra tool probes (exec+read probe)
- OpenAI regression paths (tool-call-only → follow-up) keep working - OpenAI regression paths (tool-call-only → follow-up) keep working
- Probe details (so you can explain failures quickly): - Probe details (so you can explain failures quickly):
- `read` probe: the test writes a nonce file in the workspace and asks the agent to `read` it and echo the nonce back. - `read` probe: the test writes a nonce file in the workspace and asks the agent to `read` it and echo the nonce back.
- `bash+read` probe: the test asks the agent to `bash`-write a nonce into a temp file, then `read` it back. - `exec+read` probe: the test asks the agent to `exec`-write a nonce into a temp file, then `read` it back.
- image probe: the test attaches a generated PNG (cat + randomized code) and expects the model to return `cat <CODE>`. - image probe: the test attaches a generated PNG (cat + randomized code) and expects the model to return `cat <CODE>`.
- Implementation reference: `src/gateway/gateway-models.profiles.live.test.ts` and `src/gateway/live-image-probe.ts`. - Implementation reference: `src/gateway/gateway-models.profiles.live.test.ts` and `src/gateway/live-image-probe.ts`.
- How to enable: - How to enable:
@ -136,7 +136,7 @@ Live tests are split into two layers so we can isolate failures:
- How to select providers (avoid “OpenRouter everything”): - How to select providers (avoid “OpenRouter everything”):
- `CLAWDBOT_LIVE_GATEWAY_PROVIDERS="google,google-antigravity,google-gemini-cli,openai,anthropic,zai,minimax"` (comma allowlist) - `CLAWDBOT_LIVE_GATEWAY_PROVIDERS="google,google-antigravity,google-gemini-cli,openai,anthropic,zai,minimax"` (comma allowlist)
- Optional tool-calling stress: - Optional tool-calling stress:
- `CLAWDBOT_LIVE_GATEWAY_TOOL_PROBE=1` enables an extra “bash writes file → read reads it back → echo nonce” check. - `CLAWDBOT_LIVE_GATEWAY_TOOL_PROBE=1` enables an extra “exec writes file → read reads it back → echo nonce” check.
- This is specifically meant to catch tool-calling compatibility issues across providers (formatting, history replay, tool_result pairing, etc.). - This is specifically meant to catch tool-calling compatibility issues across providers (formatting, history replay, tool_result pairing, etc.).
- Optional image send smoke: - Optional image send smoke:
- `CLAWDBOT_LIVE_GATEWAY_IMAGE_PROBE=1` sends a real image attachment through the gateway agent pipeline (multimodal message) and asserts the model can read back a per-run code from the image. - `CLAWDBOT_LIVE_GATEWAY_IMAGE_PROBE=1` sends a real image attachment through the gateway agent pipeline (multimodal message) and asserts the model can read back a per-run code from the image.
@ -215,7 +215,7 @@ Narrow, explicit allowlists are fastest and least flaky:
- Single model, gateway smoke: - Single model, gateway smoke:
- `LIVE=1 CLAWDBOT_LIVE_GATEWAY=1 CLAWDBOT_LIVE_GATEWAY_ALL_MODELS=1 CLAWDBOT_LIVE_GATEWAY_MODELS="openai/gpt-5.2" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` - `LIVE=1 CLAWDBOT_LIVE_GATEWAY=1 CLAWDBOT_LIVE_GATEWAY_ALL_MODELS=1 CLAWDBOT_LIVE_GATEWAY_MODELS="openai/gpt-5.2" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
- Tool calling across several providers (bash + read probe): - Tool calling across several providers (exec + read probe):
- `LIVE=1 CLAWDBOT_LIVE_GATEWAY=1 CLAWDBOT_LIVE_GATEWAY_ALL_MODELS=1 CLAWDBOT_LIVE_GATEWAY_TOOL_PROBE=1 CLAWDBOT_LIVE_GATEWAY_MODELS="openai/gpt-5.2,anthropic/claude-opus-4-5,google/gemini-3-flash,zai/glm-4.7,minimax/minimax-m2.1" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` - `LIVE=1 CLAWDBOT_LIVE_GATEWAY=1 CLAWDBOT_LIVE_GATEWAY_ALL_MODELS=1 CLAWDBOT_LIVE_GATEWAY_TOOL_PROBE=1 CLAWDBOT_LIVE_GATEWAY_MODELS="openai/gpt-5.2,anthropic/claude-opus-4-5,google/gemini-3-flash,zai/glm-4.7,minimax/minimax-m2.1" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
- Google focus (Gemini API key + Antigravity): - Google focus (Gemini API key + Antigravity):
@ -248,7 +248,7 @@ This is the “common models” run we expect to keep working:
Run gateway smoke with tools + image: Run gateway smoke with tools + image:
`LIVE=1 CLAWDBOT_LIVE_GATEWAY=1 CLAWDBOT_LIVE_GATEWAY_TOOL_PROBE=1 CLAWDBOT_LIVE_GATEWAY_IMAGE_PROBE=1 CLAWDBOT_LIVE_GATEWAY_MODELS="openai/gpt-5.2,openai-codex/gpt-5.2,anthropic/claude-opus-4-5,google/gemini-3-pro,google/gemini-3-flash,google-antigravity/claude-opus-4-5-thinking,google-antigravity/gemini-3-flash,zai/glm-4.7,minimax/minimax-m2.1" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts` `LIVE=1 CLAWDBOT_LIVE_GATEWAY=1 CLAWDBOT_LIVE_GATEWAY_TOOL_PROBE=1 CLAWDBOT_LIVE_GATEWAY_IMAGE_PROBE=1 CLAWDBOT_LIVE_GATEWAY_MODELS="openai/gpt-5.2,openai-codex/gpt-5.2,anthropic/claude-opus-4-5,google/gemini-3-pro,google/gemini-3-flash,google-antigravity/claude-opus-4-5-thinking,google-antigravity/gemini-3-flash,zai/glm-4.7,minimax/minimax-m2.1" pnpm test:live src/gateway/gateway-models.profiles.live.test.ts`
### Baseline: tool calling (Read + optional Bash) ### Baseline: tool calling (Read + optional Exec)
Pick at least one per provider family: Pick at least one per provider family:
- OpenAI: `openai/gpt-5.2` (or `openai/gpt-5-mini`) - OpenAI: `openai/gpt-5.2` (or `openai/gpt-5-mini`)

View file

@ -1,12 +1,12 @@
--- ---
summary: "Elevated bash mode and /elevated directives" summary: "Elevated exec mode and /elevated directives"
read_when: read_when:
- Adjusting elevated mode defaults, allowlists, or slash command behavior - Adjusting elevated mode defaults, allowlists, or slash command behavior
--- ---
# Elevated Mode (/elevated directives) # Elevated Mode (/elevated directives)
## What it does ## What it does
- Elevated mode allows the bash tool to run with elevated privileges when the feature is available and the sender is approved. - Elevated mode allows the exec tool to run with elevated privileges when the feature is available and the sender is approved.
- **Optional for sandboxed agents**: elevated only changes behavior when the agent is running in a sandbox. If the agent already runs unsandboxed, elevated is effectively a no-op. - **Optional for sandboxed agents**: elevated only changes behavior when the agent is running in a sandbox. If the agent already runs unsandboxed, elevated is effectively a no-op.
- Directive forms: `/elevated on`, `/elevated off`, `/elev on`, `/elev off`. - Directive forms: `/elevated on`, `/elevated off`, `/elev on`, `/elev off`.
- Only `on|off` are accepted; anything else returns a hint and does not change state. - Only `on|off` are accepted; anything else returns a hint and does not change state.
@ -16,16 +16,16 @@ read_when:
- **Per-session state**: `/elevated on|off` sets the elevated level for the current session key. - **Per-session state**: `/elevated on|off` sets the elevated level for the current session key.
- **Inline directive**: `/elevated on` inside a message applies to that message only. - **Inline directive**: `/elevated on` inside a message applies to that message only.
- **Groups**: In group chats, elevated directives are only honored when the agent is mentioned. Command-only messages that bypass mention requirements are treated as mentioned. - **Groups**: In group chats, elevated directives are only honored when the agent is mentioned. Command-only messages that bypass mention requirements are treated as mentioned.
- **Host execution**: elevated runs `bash` on the host (bypasses sandbox). - **Host execution**: elevated runs `exec` on the host (bypasses sandbox).
- **Unsandboxed agents**: when there is no sandbox to bypass, elevated does not change where `bash` runs. - **Unsandboxed agents**: when there is no sandbox to bypass, elevated does not change where `exec` runs.
- **Tool policy still applies**: if `bash` is denied by tool policy, elevated cannot be used. - **Tool policy still applies**: if `exec` is denied by tool policy, elevated cannot be used.
Note: Note:
- Sandbox on: `/elevated on` runs that `bash` command on the host. - Sandbox on: `/elevated on` runs that `exec` command on the host.
- Sandbox off: `/elevated on` does not change execution (already on host). - Sandbox off: `/elevated on` does not change execution (already on host).
## When elevated matters ## When elevated matters
- Only impacts `bash` when the agent is running sandboxed (it drops the sandbox for that command). - Only impacts `exec` when the agent is running sandboxed (it drops the sandbox for that command).
- For unsandboxed agents, elevated does not change execution; it only affects gating, logging, and status. - For unsandboxed agents, elevated does not change execution; it only affects gating, logging, and status.
## Resolution order ## Resolution order
@ -48,5 +48,5 @@ Note:
- All gates must pass; otherwise elevated is treated as unavailable. - All gates must pass; otherwise elevated is treated as unavailable.
## Logging + status ## Logging + status
- Elevated bash calls are logged at info level. - Elevated exec calls are logged at info level.
- Session status includes elevated mode (e.g. `elevated=on`). - Session status includes elevated mode (e.g. `elevated=on`).

View file

@ -1,14 +1,14 @@
--- ---
summary: "Bash tool usage, stdin modes, and TTY support" summary: "Exec tool usage, stdin modes, and TTY support"
read_when: read_when:
- Using or modifying the bash tool - Using or modifying the exec tool
- Debugging stdin or TTY behavior - Debugging stdin or TTY behavior
--- ---
# Bash tool # Exec tool
Run shell commands in the workspace. Supports foreground + background execution via `process`. Run shell commands in the workspace. Supports foreground + background execution via `process`.
If `process` is disallowed, `bash` runs synchronously and ignores `yieldMs`/`background`. If `process` is disallowed, `exec` runs synchronously and ignores `yieldMs`/`background`.
Background sessions are scoped per agent; `process` only sees sessions from the same agent. Background sessions are scoped per agent; `process` only sees sessions from the same agent.
## Parameters ## Parameters
@ -19,17 +19,17 @@ Background sessions are scoped per agent; `process` only sees sessions from the
- `timeout` (seconds, default 1800): kill on expiry - `timeout` (seconds, default 1800): kill on expiry
- `elevated` (bool): run on host if elevated mode is enabled/allowed (only changes behavior when the agent is sandboxed) - `elevated` (bool): run on host if elevated mode is enabled/allowed (only changes behavior when the agent is sandboxed)
- Need a real TTY? Use the tmux skill. - Need a real TTY? Use the tmux skill.
Note: `elevated` is ignored when sandboxing is off (bash already runs on the host). Note: `elevated` is ignored when sandboxing is off (exec already runs on the host).
## Examples ## Examples
Foreground: Foreground:
```json ```json
{"tool":"bash","command":"ls -la"} {"tool":"exec","command":"ls -la"}
``` ```
Background + poll: Background + poll:
```json ```json
{"tool":"bash","command":"npm run build","yieldMs":1000} {"tool":"exec","command":"npm run build","yieldMs":1000}
{"tool":"process","action":"poll","sessionId":"<id>"} {"tool":"process","action":"poll","sessionId":"<id>"}
``` ```

View file

@ -31,7 +31,7 @@ alongside tools (for example, the voice-call plugin).
## Tool inventory ## Tool inventory
### `bash` ### `exec`
Run shell commands in the workspace. Run shell commands in the workspace.
Core parameters: Core parameters:
@ -45,12 +45,12 @@ Core parameters:
Notes: Notes:
- Returns `status: "running"` with a `sessionId` when backgrounded. - Returns `status: "running"` with a `sessionId` when backgrounded.
- Use `process` to poll/log/write/kill/clear background sessions. - Use `process` to poll/log/write/kill/clear background sessions.
- If `process` is disallowed, `bash` runs synchronously and ignores `yieldMs`/`background`. - If `process` is disallowed, `exec` runs synchronously and ignores `yieldMs`/`background`.
- `elevated` is gated by `tools.elevated` plus any `agents.list[].tools.elevated` override (both must allow) and runs on the host. - `elevated` is gated by `tools.elevated` plus any `agents.list[].tools.elevated` override (both must allow) and runs on the host.
- `elevated` only changes behavior when the agent is sandboxed (otherwise its a no-op). - `elevated` only changes behavior when the agent is sandboxed (otherwise its a no-op).
### `process` ### `process`
Manage background bash sessions. Manage background exec sessions.
Core actions: Core actions:
- `list`, `poll`, `log`, `write`, `kill`, `clear`, `remove` - `list`, `poll`, `log`, `write`, `kill`, `clear`, `remove`

View file

@ -80,7 +80,7 @@ Override via config:
// deny wins // deny wins
deny: ["gateway", "cron"], deny: ["gateway", "cron"],
// if allow is set, it becomes allow-only (deny still wins) // if allow is set, it becomes allow-only (deny still wins)
// allow: ["read", "bash", "process"] // allow: ["read", "exec", "process"]
} }
} }
} }

View file

@ -265,7 +265,7 @@ function walk(node, parent) {
if (name) seen.add(name); if (name) seen.add(name);
} }
if (typeof obj.name === "string" && typeof obj.input === "object" && obj.input) { if (typeof obj.name === "string" && typeof obj.input === "object" && obj.input) {
// Many tool-use blocks look like { type: "...", name: "bash", input: {...} } // Many tool-use blocks look like { type: "...", name: "exec", input: {...} }
// but some transcripts omit/rename type. // but some transcripts omit/rename type.
seen.add(obj.name); seen.add(obj.name);
} }
@ -405,7 +405,7 @@ run_profile() {
TURN4_JSON="/tmp/agent-${profile}-4.json" TURN4_JSON="/tmp/agent-${profile}-4.json"
run_agent_turn "$profile" "$SESSION_ID" \ run_agent_turn "$profile" "$SESSION_ID" \
"Use the read tool (not bash) to read proof.txt. Reply with the exact contents only (no extra whitespace)." \ "Use the read tool (not exec) to read proof.txt. Reply with the exact contents only (no extra whitespace)." \
"$TURN1_JSON" "$TURN1_JSON"
assert_agent_json_has_text "$TURN1_JSON" assert_agent_json_has_text "$TURN1_JSON"
assert_agent_json_ok "$TURN1_JSON" "$agent_model_provider" assert_agent_json_ok "$TURN1_JSON" "$agent_model_provider"
@ -417,7 +417,7 @@ run_profile() {
fi fi
local prompt2 local prompt2
prompt2=$'Use the write tool (not bash) to write exactly this string into copy.txt:\n'"${reply1}"$'\nThen use the read tool (not bash) to read copy.txt and reply with the exact contents only (no extra whitespace).' prompt2=$'Use the write tool (not exec) to write exactly this string into copy.txt:\n'"${reply1}"$'\nThen use the read tool (not exec) to read copy.txt and reply with the exact contents only (no extra whitespace).'
run_agent_turn "$profile" "$SESSION_ID" "$prompt2" "$TURN2_JSON" run_agent_turn "$profile" "$SESSION_ID" "$prompt2" "$TURN2_JSON"
assert_agent_json_has_text "$TURN2_JSON" assert_agent_json_has_text "$TURN2_JSON"
assert_agent_json_ok "$TURN2_JSON" "$agent_model_provider" assert_agent_json_ok "$TURN2_JSON" "$agent_model_provider"
@ -435,7 +435,7 @@ run_profile() {
fi fi
local prompt3 local prompt3
prompt3=$'Use the bash tool to run: cat /etc/hostname\nThen use the write tool to write the exact stdout (trim trailing newline) into hostname.txt. Reply with the hostname only.' prompt3=$'Use the exec tool to run: cat /etc/hostname\nThen use the write tool to write the exact stdout (trim trailing newline) into hostname.txt. Reply with the hostname only.'
run_agent_turn "$profile" "$SESSION_ID" "$prompt3" "$TURN3_JSON" run_agent_turn "$profile" "$SESSION_ID" "$prompt3" "$TURN3_JSON"
assert_agent_json_has_text "$TURN3_JSON" assert_agent_json_has_text "$TURN3_JSON"
assert_agent_json_ok "$TURN3_JSON" "$agent_model_provider" assert_agent_json_ok "$TURN3_JSON" "$agent_model_provider"
@ -468,7 +468,7 @@ run_profile() {
ls -la "/root/.clawdbot-${profile}/agents/main/sessions" >&2 || true ls -la "/root/.clawdbot-${profile}/agents/main/sessions" >&2 || true
exit 1 exit 1
fi fi
assert_session_used_tools "$SESSION_JSONL" read write bash image assert_session_used_tools "$SESSION_JSONL" read write exec image
cleanup_profile cleanup_profile
trap - EXIT trap - EXIT

View file

@ -122,7 +122,7 @@ async function main() {
console.log("== Run 1: create tool history (primary only)"); console.log("== Run 1: create tool history (primary only)");
const toolPrompt = const toolPrompt =
"Use the bash tool to create a file named zai-fallback-tool.txt with the content tool-ok. " + "Use the exec tool to create a file named zai-fallback-tool.txt with the content tool-ok. " +
"Then use the read tool to display the file contents. Reply with just the file contents."; "Then use the read tool to display the file contents. Reply with just the file contents.";
const run1 = await runCommand( const run1 = await runCommand(
"run1", "run1",

View file

@ -6,9 +6,9 @@ metadata: {"clawdbot":{"emoji":"🧵","os":["darwin","linux"],"requires":{"bins"
# tmux Skill (Clawdbot) # tmux Skill (Clawdbot)
Use tmux only when you need an interactive TTY. Prefer bash background mode for long-running, non-interactive tasks. Use tmux only when you need an interactive TTY. Prefer exec background mode for long-running, non-interactive tasks.
## Quickstart (isolated socket, bash tool) ## Quickstart (isolated socket, exec tool)
```bash ```bash
SOCKET_DIR="${CLAWDBOT_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/clawdbot-tmux-sockets}" SOCKET_DIR="${CLAWDBOT_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/clawdbot-tmux-sockets}"

View file

@ -84,7 +84,7 @@ describe("resolveAgentConfig", () => {
workspace: "~/clawd-restricted", workspace: "~/clawd-restricted",
tools: { tools: {
allow: ["read"], allow: ["read"],
deny: ["bash", "write", "edit"], deny: ["exec", "write", "edit"],
elevated: { elevated: {
enabled: false, enabled: false,
allowFrom: { whatsapp: ["+15555550123"] }, allowFrom: { whatsapp: ["+15555550123"] },
@ -97,7 +97,7 @@ describe("resolveAgentConfig", () => {
const result = resolveAgentConfig(cfg, "restricted"); const result = resolveAgentConfig(cfg, "restricted");
expect(result?.tools).toEqual({ expect(result?.tools).toEqual({
allow: ["read"], allow: ["read"],
deny: ["bash", "write", "edit"], deny: ["exec", "write", "edit"],
elevated: { elevated: {
enabled: false, enabled: false,
allowFrom: { whatsapp: ["+15555550123"] }, allowFrom: { whatsapp: ["+15555550123"] },
@ -118,7 +118,7 @@ describe("resolveAgentConfig", () => {
}, },
tools: { tools: {
allow: ["read"], allow: ["read"],
deny: ["bash"], deny: ["exec"],
}, },
}, },
], ],

View file

@ -1,9 +1,9 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { resetProcessRegistryForTests } from "./bash-process-registry.js"; import { resetProcessRegistryForTests } from "./bash-process-registry.js";
import { import {
bashTool, createExecTool,
createBashTool,
createProcessTool, createProcessTool,
execTool,
processTool, processTool,
} from "./bash-tools.js"; } from "./bash-tools.js";
import { sanitizeBinaryOutput } from "./shell-utils.js"; import { sanitizeBinaryOutput } from "./shell-utils.js";
@ -50,7 +50,7 @@ beforeEach(() => {
resetProcessRegistryForTests(); resetProcessRegistryForTests();
}); });
describe("bash tool backgrounding", () => { describe("exec tool backgrounding", () => {
const originalShell = process.env.SHELL; const originalShell = process.env.SHELL;
beforeEach(() => { beforeEach(() => {
@ -64,7 +64,7 @@ describe("bash tool backgrounding", () => {
it( it(
"backgrounds after yield and can be polled", "backgrounds after yield and can be polled",
async () => { async () => {
const result = await bashTool.execute("call1", { const result = await execTool.execute("call1", {
command: joinCommands([yieldDelayCmd, "echo done"]), command: joinCommands([yieldDelayCmd, "echo done"]),
yieldMs: 10, yieldMs: 10,
}); });
@ -97,7 +97,7 @@ describe("bash tool backgrounding", () => {
); );
it("supports explicit background", async () => { it("supports explicit background", async () => {
const result = await bashTool.execute("call1", { const result = await execTool.execute("call1", {
command: echoAfterDelay("later"), command: echoAfterDelay("later"),
background: true, background: true,
}); });
@ -113,7 +113,7 @@ describe("bash tool backgrounding", () => {
}); });
it("derives a session name from the command", async () => { it("derives a session name from the command", async () => {
const result = await bashTool.execute("call1", { const result = await execTool.execute("call1", {
command: "echo hello", command: "echo hello",
background: true, background: true,
}); });
@ -129,7 +129,7 @@ describe("bash tool backgrounding", () => {
}); });
it("uses default timeout when timeout is omitted", async () => { it("uses default timeout when timeout is omitted", async () => {
const customBash = createBashTool({ timeoutSec: 1, backgroundMs: 10 }); const customBash = createExecTool({ timeoutSec: 1, backgroundMs: 10 });
const customProcess = createProcessTool(); const customProcess = createProcessTool();
const result = await customBash.execute("call1", { const result = await customBash.execute("call1", {
@ -156,7 +156,7 @@ describe("bash tool backgrounding", () => {
}); });
it("rejects elevated requests when not allowed", async () => { it("rejects elevated requests when not allowed", async () => {
const customBash = createBashTool({ const customBash = createExecTool({
elevated: { enabled: true, allowed: false, defaultLevel: "off" }, elevated: { enabled: true, allowed: false, defaultLevel: "off" },
}); });
@ -169,7 +169,7 @@ describe("bash tool backgrounding", () => {
}); });
it("does not default to elevated when not allowed", async () => { it("does not default to elevated when not allowed", async () => {
const customBash = createBashTool({ const customBash = createExecTool({
elevated: { enabled: true, allowed: false, defaultLevel: "on" }, elevated: { enabled: true, allowed: false, defaultLevel: "on" },
backgroundMs: 1000, backgroundMs: 1000,
timeoutSec: 5, timeoutSec: 5,
@ -183,7 +183,7 @@ describe("bash tool backgrounding", () => {
}); });
it("logs line-based slices and defaults to last lines", async () => { it("logs line-based slices and defaults to last lines", async () => {
const result = await bashTool.execute("call1", { const result = await execTool.execute("call1", {
command: echoLines(["one", "two", "three"]), command: echoLines(["one", "two", "three"]),
background: true, background: true,
}); });
@ -203,7 +203,7 @@ describe("bash tool backgrounding", () => {
}); });
it("supports line offsets for log slices", async () => { it("supports line offsets for log slices", async () => {
const result = await bashTool.execute("call1", { const result = await execTool.execute("call1", {
command: echoLines(["alpha", "beta", "gamma"]), command: echoLines(["alpha", "beta", "gamma"]),
background: true, background: true,
}); });
@ -221,9 +221,9 @@ describe("bash tool backgrounding", () => {
}); });
it("scopes process sessions by scopeKey", async () => { it("scopes process sessions by scopeKey", async () => {
const bashA = createBashTool({ backgroundMs: 10, scopeKey: "agent:alpha" }); const bashA = createExecTool({ backgroundMs: 10, scopeKey: "agent:alpha" });
const processA = createProcessTool({ scopeKey: "agent:alpha" }); const processA = createProcessTool({ scopeKey: "agent:alpha" });
const bashB = createBashTool({ backgroundMs: 10, scopeKey: "agent:beta" }); const bashB = createExecTool({ backgroundMs: 10, scopeKey: "agent:beta" });
const processB = createProcessTool({ scopeKey: "agent:beta" }); const processB = createProcessTool({ scopeKey: "agent:beta" });
const resultA = await bashA.execute("call1", { const resultA = await bashA.execute("call1", {

View file

@ -54,11 +54,11 @@ const _stringEnum = <T extends readonly string[]>(
...options, ...options,
}); });
export type BashToolDefaults = { export type ExecToolDefaults = {
backgroundMs?: number; backgroundMs?: number;
timeoutSec?: number; timeoutSec?: number;
sandbox?: BashSandboxConfig; sandbox?: BashSandboxConfig;
elevated?: BashElevatedDefaults; elevated?: ExecElevatedDefaults;
allowBackground?: boolean; allowBackground?: boolean;
scopeKey?: string; scopeKey?: string;
cwd?: string; cwd?: string;
@ -76,14 +76,14 @@ export type BashSandboxConfig = {
env?: Record<string, string>; env?: Record<string, string>;
}; };
export type BashElevatedDefaults = { export type ExecElevatedDefaults = {
enabled: boolean; enabled: boolean;
allowed: boolean; allowed: boolean;
defaultLevel: "on" | "off"; defaultLevel: "on" | "off";
}; };
const bashSchema = Type.Object({ const execSchema = Type.Object({
command: Type.String({ description: "Bash command to execute" }), command: Type.String({ description: "Shell command to execute" }),
workdir: Type.Optional( workdir: Type.Optional(
Type.String({ description: "Working directory (defaults to cwd)" }), Type.String({ description: "Working directory (defaults to cwd)" }),
), ),
@ -108,7 +108,7 @@ const bashSchema = Type.Object({
), ),
}); });
export type BashToolDetails = export type ExecToolDetails =
| { | {
status: "running"; status: "running";
sessionId: string; sessionId: string;
@ -125,10 +125,10 @@ export type BashToolDetails =
cwd?: string; cwd?: string;
}; };
export function createBashTool( export function createExecTool(
defaults?: BashToolDefaults, defaults?: ExecToolDefaults,
// biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance. // biome-ignore lint/suspicious/noExplicitAny: TypeBox schema type from pi-agent-core uses a different module instance.
): AgentTool<any, BashToolDetails> { ): AgentTool<any, ExecToolDetails> {
const defaultBackgroundMs = clampNumber( const defaultBackgroundMs = clampNumber(
defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"), defaults?.backgroundMs ?? readEnvInt("PI_BASH_YIELD_MS"),
10_000, 10_000,
@ -142,11 +142,11 @@ export function createBashTool(
: 1800; : 1800;
return { return {
name: "bash", name: "exec",
label: "bash", label: "exec",
description: description:
"Execute bash with background continuation. Use yieldMs/background to continue later via process tool. For real TTY mode, use the tmux skill.", "Execute shell commands with background continuation. Use yieldMs/background to continue later via process tool. For real TTY mode, use the tmux skill.",
parameters: bashSchema, parameters: execSchema,
execute: async (_toolCallId, args, signal, onUpdate) => { execute: async (_toolCallId, args, signal, onUpdate) => {
const params = args as { const params = args as {
command: string; command: string;
@ -218,7 +218,7 @@ export function createBashTool(
); );
} }
logInfo( logInfo(
`bash: elevated command (${sessionId.slice(0, 8)}) ${truncateMiddle( `exec: elevated command (${sessionId.slice(0, 8)}) ${truncateMiddle(
params.command, params.command,
120, 120,
)}`, )}`,
@ -363,7 +363,7 @@ export function createBashTool(
} }
}); });
return new Promise<AgentToolResult<BashToolDetails>>( return new Promise<AgentToolResult<ExecToolDetails>>(
(resolve, reject) => { (resolve, reject) => {
const resolveRunning = () => { const resolveRunning = () => {
settle(() => settle(() =>
@ -482,7 +482,7 @@ export function createBashTool(
}; };
} }
export const bashTool = createBashTool(); export const execTool = createExecTool();
const processSchema = Type.Object({ const processSchema = Type.Object({
action: Type.String({ description: "Process action" }), action: Type.String({ description: "Process action" }),
@ -509,7 +509,7 @@ export function createProcessTool(
return { return {
name: "process", name: "process",
label: "process", label: "process",
description: "Manage running bash sessions: list, poll, log, write, kill.", description: "Manage running exec sessions: list, poll, log, write, kill.",
parameters: processSchema, parameters: processSchema,
execute: async (_toolCallId, args) => { execute: async (_toolCallId, args) => {
const params = args as { const params = args as {

View file

@ -356,7 +356,7 @@ describe("sanitizeGoogleTurnOrdering", () => {
{ {
role: "assistant", role: "assistant",
content: [ content: [
{ type: "toolCall", id: "call_1", name: "bash", arguments: {} }, { type: "toolCall", id: "call_1", name: "exec", arguments: {} },
], ],
}, },
] satisfies AgentMessage[]; ] satisfies AgentMessage[];
@ -403,7 +403,7 @@ describe("sanitizeSessionMessagesImages", () => {
{ {
type: "toolCall", type: "toolCall",
id: "call_abc|item:456", id: "call_abc|item:456",
name: "bash", name: "exec",
arguments: {}, arguments: {},
}, },
], ],

View file

@ -44,7 +44,7 @@ describe("buildEmbeddedSandboxInfo", () => {
env: { LANG: "C.UTF-8" }, env: { LANG: "C.UTF-8" },
}, },
tools: { tools: {
allow: ["bash"], allow: ["exec"],
deny: ["browser"], deny: ["browser"],
}, },
browserAllowHostControl: true, browserAllowHostControl: true,
@ -87,7 +87,7 @@ describe("buildEmbeddedSandboxInfo", () => {
env: { LANG: "C.UTF-8" }, env: { LANG: "C.UTF-8" },
}, },
tools: { tools: {
allow: ["bash"], allow: ["exec"],
deny: ["browser"], deny: ["browser"],
}, },
browserAllowHostControl: false, browserAllowHostControl: false,
@ -171,7 +171,7 @@ function createStubTool(name: string): AgentTool {
describe("splitSdkTools", () => { describe("splitSdkTools", () => {
const tools = [ const tools = [
createStubTool("read"), createStubTool("read"),
createStubTool("bash"), createStubTool("exec"),
createStubTool("edit"), createStubTool("edit"),
createStubTool("write"), createStubTool("write"),
createStubTool("browser"), createStubTool("browser"),
@ -185,7 +185,7 @@ describe("splitSdkTools", () => {
expect(builtInTools).toEqual([]); expect(builtInTools).toEqual([]);
expect(customTools.map((tool) => tool.name)).toEqual([ expect(customTools.map((tool) => tool.name)).toEqual([
"read", "read",
"bash", "exec",
"edit", "edit",
"write", "write",
"browser", "browser",
@ -200,7 +200,7 @@ describe("splitSdkTools", () => {
expect(builtInTools).toEqual([]); expect(builtInTools).toEqual([]);
expect(customTools.map((tool) => tool.name)).toEqual([ expect(customTools.map((tool) => tool.name)).toEqual([
"read", "read",
"bash", "exec",
"edit", "edit",
"write", "write",
"browser", "browser",
@ -226,7 +226,7 @@ describe("applyGoogleTurnOrderingFix", () => {
{ {
role: "assistant", role: "assistant",
content: [ content: [
{ type: "toolCall", id: "call_1", name: "bash", arguments: {} }, { type: "toolCall", id: "call_1", name: "exec", arguments: {} },
], ],
}, },
] satisfies AgentMessage[]; ] satisfies AgentMessage[];
@ -360,7 +360,7 @@ describe("limitHistoryTurns", () => {
{ role: "user", content: [{ type: "text", text: "first" }] }, { role: "user", content: [{ type: "text", text: "first" }] },
{ {
role: "assistant", role: "assistant",
content: [{ type: "toolCall", id: "1", name: "bash", arguments: {} }], content: [{ type: "toolCall", id: "1", name: "exec", arguments: {} }],
}, },
{ role: "user", content: [{ type: "text", text: "second" }] }, { role: "user", content: [{ type: "text", text: "second" }] },
{ role: "assistant", content: [{ type: "text", text: "response" }] }, { role: "assistant", content: [{ type: "text", text: "response" }] },

View file

@ -51,7 +51,7 @@ import {
markAuthProfileGood, markAuthProfileGood,
markAuthProfileUsed, markAuthProfileUsed,
} from "./auth-profiles.js"; } from "./auth-profiles.js";
import type { BashElevatedDefaults } from "./bash-tools.js"; import type { ExecElevatedDefaults, ExecToolDefaults } from "./bash-tools.js";
import { import {
CONTEXT_WINDOW_HARD_MIN_TOKENS, CONTEXT_WINDOW_HARD_MIN_TOKENS,
CONTEXT_WINDOW_WARN_BELOW_TOKENS, CONTEXT_WINDOW_WARN_BELOW_TOKENS,
@ -768,11 +768,11 @@ function describeUnknownError(error: unknown): string {
export function buildEmbeddedSandboxInfo( export function buildEmbeddedSandboxInfo(
sandbox?: Awaited<ReturnType<typeof resolveSandboxContext>>, sandbox?: Awaited<ReturnType<typeof resolveSandboxContext>>,
bashElevated?: BashElevatedDefaults, execElevated?: ExecElevatedDefaults,
): EmbeddedSandboxInfo | undefined { ): EmbeddedSandboxInfo | undefined {
if (!sandbox?.enabled) return undefined; if (!sandbox?.enabled) return undefined;
const elevatedAllowed = Boolean( const elevatedAllowed = Boolean(
bashElevated?.enabled && bashElevated.allowed, execElevated?.enabled && execElevated.allowed,
); );
return { return {
enabled: true, enabled: true,
@ -790,7 +790,7 @@ export function buildEmbeddedSandboxInfo(
? { ? {
elevated: { elevated: {
allowed: true, allowed: true,
defaultLevel: bashElevated?.defaultLevel ?? "off", defaultLevel: execElevated?.defaultLevel ?? "off",
}, },
} }
: {}), : {}),
@ -949,6 +949,16 @@ function mapThinkingLevel(level?: ThinkLevel): ThinkingLevel {
return level; return level;
} }
function resolveExecToolDefaults(
config?: ClawdbotConfig,
): ExecToolDefaults | undefined {
const tools = config?.tools;
if (!tools) return undefined;
if (!tools.exec) return tools.bash;
if (!tools.bash) return tools.exec;
return { ...tools.bash, ...tools.exec };
}
function resolveModel( function resolveModel(
provider: string, provider: string,
modelId: string, modelId: string,
@ -987,7 +997,7 @@ export async function compactEmbeddedPiSession(params: {
model?: string; model?: string;
thinkLevel?: ThinkLevel; thinkLevel?: ThinkLevel;
reasoningLevel?: ReasoningLevel; reasoningLevel?: ReasoningLevel;
bashElevated?: BashElevatedDefaults; bashElevated?: ExecElevatedDefaults;
customInstructions?: string; customInstructions?: string;
lane?: string; lane?: string;
enqueue?: typeof enqueueCommand; enqueue?: typeof enqueueCommand;
@ -1087,8 +1097,8 @@ export async function compactEmbeddedPiSession(params: {
const contextFiles = buildBootstrapContextFiles(bootstrapFiles); const contextFiles = buildBootstrapContextFiles(bootstrapFiles);
const runAbortController = new AbortController(); const runAbortController = new AbortController();
const tools = createClawdbotCodingTools({ const tools = createClawdbotCodingTools({
bash: { exec: {
...params.config?.tools?.bash, ...resolveExecToolDefaults(params.config),
elevated: params.bashElevated, elevated: params.bashElevated,
}, },
sandbox, sandbox,
@ -1289,7 +1299,7 @@ export async function runEmbeddedPiAgent(params: {
thinkLevel?: ThinkLevel; thinkLevel?: ThinkLevel;
verboseLevel?: VerboseLevel; verboseLevel?: VerboseLevel;
reasoningLevel?: ReasoningLevel; reasoningLevel?: ReasoningLevel;
bashElevated?: BashElevatedDefaults; bashElevated?: ExecElevatedDefaults;
timeoutMs: number; timeoutMs: number;
runId: string; runId: string;
abortSignal?: AbortSignal; abortSignal?: AbortSignal;
@ -1499,8 +1509,8 @@ export async function runEmbeddedPiAgent(params: {
// Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`). // Tool schemas must be provider-compatible (OpenAI requires top-level `type: "object"`).
// `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged. // `createClawdbotCodingTools()` normalizes schemas so the session can pass them through unchanged.
const tools = createClawdbotCodingTools({ const tools = createClawdbotCodingTools({
bash: { exec: {
...params.config?.tools?.bash, ...resolveExecToolDefaults(params.config),
elevated: params.bashElevated, elevated: params.bashElevated,
}, },
sandbox, sandbox,

View file

@ -94,28 +94,28 @@ describe("context-pruning", () => {
makeAssistant("a1"), makeAssistant("a1"),
makeToolResult({ makeToolResult({
toolCallId: "t1", toolCallId: "t1",
toolName: "bash", toolName: "exec",
text: "x".repeat(20_000), text: "x".repeat(20_000),
}), }),
makeUser("u2"), makeUser("u2"),
makeAssistant("a2"), makeAssistant("a2"),
makeToolResult({ makeToolResult({
toolCallId: "t2", toolCallId: "t2",
toolName: "bash", toolName: "exec",
text: "y".repeat(20_000), text: "y".repeat(20_000),
}), }),
makeUser("u3"), makeUser("u3"),
makeAssistant("a3"), makeAssistant("a3"),
makeToolResult({ makeToolResult({
toolCallId: "t3", toolCallId: "t3",
toolName: "bash", toolName: "exec",
text: "z".repeat(20_000), text: "z".repeat(20_000),
}), }),
makeUser("u4"), makeUser("u4"),
makeAssistant("a4"), makeAssistant("a4"),
makeToolResult({ makeToolResult({
toolCallId: "t4", toolCallId: "t4",
toolName: "bash", toolName: "exec",
text: "w".repeat(20_000), text: "w".repeat(20_000),
}), }),
]; ];
@ -161,7 +161,7 @@ describe("context-pruning", () => {
makeUser("u1"), makeUser("u1"),
makeToolResult({ makeToolResult({
toolCallId: "t1", toolCallId: "t1",
toolName: "bash", toolName: "exec",
text: "y".repeat(20_000), text: "y".repeat(20_000),
}), }),
]; ];
@ -184,19 +184,19 @@ describe("context-pruning", () => {
makeAssistant("a1"), makeAssistant("a1"),
makeToolResult({ makeToolResult({
toolCallId: "t1", toolCallId: "t1",
toolName: "bash", toolName: "exec",
text: "x".repeat(20_000), text: "x".repeat(20_000),
}), }),
makeToolResult({ makeToolResult({
toolCallId: "t2", toolCallId: "t2",
toolName: "bash", toolName: "exec",
text: "y".repeat(20_000), text: "y".repeat(20_000),
}), }),
makeUser("u2"), makeUser("u2"),
makeAssistant("a2"), makeAssistant("a2"),
makeToolResult({ makeToolResult({
toolCallId: "t3", toolCallId: "t3",
toolName: "bash", toolName: "exec",
text: "z".repeat(20_000), text: "z".repeat(20_000),
}), }),
]; ];
@ -225,7 +225,7 @@ describe("context-pruning", () => {
makeAssistant("a1"), makeAssistant("a1"),
makeToolResult({ makeToolResult({
toolCallId: "t1", toolCallId: "t1",
toolName: "bash", toolName: "exec",
text: "x".repeat(20_000), text: "x".repeat(20_000),
}), }),
makeAssistant("a2"), makeAssistant("a2"),
@ -273,7 +273,7 @@ describe("context-pruning", () => {
makeAssistant("a1"), makeAssistant("a1"),
makeToolResult({ makeToolResult({
toolCallId: "t1", toolCallId: "t1",
toolName: "bash", toolName: "exec",
text: "x".repeat(20_000), text: "x".repeat(20_000),
}), }),
makeAssistant("a2"), makeAssistant("a2"),
@ -313,7 +313,7 @@ describe("context-pruning", () => {
makeUser("u1"), makeUser("u1"),
makeToolResult({ makeToolResult({
toolCallId: "t1", toolCallId: "t1",
toolName: "Bash", toolName: "Exec",
text: "x".repeat(20_000), text: "x".repeat(20_000),
}), }),
makeToolResult({ makeToolResult({
@ -329,7 +329,7 @@ describe("context-pruning", () => {
softTrimRatio: 0.0, softTrimRatio: 0.0,
hardClearRatio: 0.0, hardClearRatio: 0.0,
minPrunableToolChars: 0, minPrunableToolChars: 0,
tools: { allow: ["ba*"], deny: ["bash"] }, tools: { allow: ["ex*"], deny: ["exec"] },
hardClear: { enabled: true, placeholder: "[cleared]" }, hardClear: { enabled: true, placeholder: "[cleared]" },
softTrim: { maxChars: 10, headChars: 3, tailChars: 3 }, softTrim: { maxChars: 10, headChars: 3, tailChars: 3 },
}; };
@ -339,7 +339,7 @@ describe("context-pruning", () => {
} as unknown as ExtensionContext; } as unknown as ExtensionContext;
const next = pruneContextMessages({ messages, settings, ctx }); const next = pruneContextMessages({ messages, settings, ctx });
// Deny wins => bash is not pruned, even though allow matches. // Deny wins => exec is not pruned, even though allow matches.
expect(toolText(findToolResult(next, "t1"))).toContain("x".repeat(20_000)); expect(toolText(findToolResult(next, "t1"))).toContain("x".repeat(20_000));
// allow is non-empty and browser is not allowed => never pruned. // allow is non-empty and browser is not allowed => never pruned.
expect(toolText(findToolResult(next, "t2"))).toContain("y".repeat(20_000)); expect(toolText(findToolResult(next, "t2"))).toContain("y".repeat(20_000));
@ -350,7 +350,7 @@ describe("context-pruning", () => {
makeUser("u1"), makeUser("u1"),
makeImageToolResult({ makeImageToolResult({
toolCallId: "t1", toolCallId: "t1",
toolName: "bash", toolName: "exec",
text: "x".repeat(20_000), text: "x".repeat(20_000),
}), }),
]; ];
@ -384,7 +384,7 @@ describe("context-pruning", () => {
{ {
role: "toolResult", role: "toolResult",
toolCallId: "t1", toolCallId: "t1",
toolName: "bash", toolName: "exec",
content: [ content: [
{ type: "text", text: "AAAAA" }, { type: "text", text: "AAAAA" },
{ type: "text", text: "BBBBB" }, { type: "text", text: "BBBBB" },
@ -418,7 +418,7 @@ describe("context-pruning", () => {
makeUser("u1"), makeUser("u1"),
makeToolResult({ makeToolResult({
toolCallId: "t1", toolCallId: "t1",
toolName: "bash", toolName: "exec",
text: "abcdefghij".repeat(1000), text: "abcdefghij".repeat(1000),
}), }),
]; ];

View file

@ -30,7 +30,7 @@ describe("Agent-specific tool filtering", () => {
const toolNames = tools.map((t) => t.name); const toolNames = tools.map((t) => t.name);
expect(toolNames).toContain("read"); expect(toolNames).toContain("read");
expect(toolNames).toContain("write"); expect(toolNames).toContain("write");
expect(toolNames).not.toContain("bash"); expect(toolNames).not.toContain("exec");
}); });
it("should keep global tool policy when agent only sets tools.elevated", () => { it("should keep global tool policy when agent only sets tools.elevated", () => {
@ -62,7 +62,7 @@ describe("Agent-specific tool filtering", () => {
}); });
const toolNames = tools.map((t) => t.name); const toolNames = tools.map((t) => t.name);
expect(toolNames).toContain("bash"); expect(toolNames).toContain("exec");
expect(toolNames).toContain("read"); expect(toolNames).toContain("read");
expect(toolNames).not.toContain("write"); expect(toolNames).not.toContain("write");
}); });
@ -70,7 +70,7 @@ describe("Agent-specific tool filtering", () => {
it("should apply agent-specific tool policy", () => { it("should apply agent-specific tool policy", () => {
const cfg: ClawdbotConfig = { const cfg: ClawdbotConfig = {
tools: { tools: {
allow: ["read", "write", "bash"], allow: ["read", "write", "exec"],
deny: [], deny: [],
}, },
agents: { agents: {
@ -80,7 +80,7 @@ describe("Agent-specific tool filtering", () => {
workspace: "~/clawd-restricted", workspace: "~/clawd-restricted",
tools: { tools: {
allow: ["read"], // Agent override: only read allow: ["read"], // Agent override: only read
deny: ["bash", "write", "edit"], deny: ["exec", "write", "edit"],
}, },
}, },
], ],
@ -96,7 +96,7 @@ describe("Agent-specific tool filtering", () => {
const toolNames = tools.map((t) => t.name); const toolNames = tools.map((t) => t.name);
expect(toolNames).toContain("read"); expect(toolNames).toContain("read");
expect(toolNames).not.toContain("bash"); expect(toolNames).not.toContain("exec");
expect(toolNames).not.toContain("write"); expect(toolNames).not.toContain("write");
expect(toolNames).not.toContain("edit"); expect(toolNames).not.toContain("edit");
}); });
@ -115,7 +115,7 @@ describe("Agent-specific tool filtering", () => {
workspace: "~/clawd-family", workspace: "~/clawd-family",
tools: { tools: {
allow: ["read"], allow: ["read"],
deny: ["bash", "write", "edit", "process"], deny: ["exec", "write", "edit", "process"],
}, },
}, },
], ],
@ -130,7 +130,7 @@ describe("Agent-specific tool filtering", () => {
agentDir: "/tmp/agent-main", agentDir: "/tmp/agent-main",
}); });
const mainToolNames = mainTools.map((t) => t.name); const mainToolNames = mainTools.map((t) => t.name);
expect(mainToolNames).toContain("bash"); expect(mainToolNames).toContain("exec");
expect(mainToolNames).toContain("write"); expect(mainToolNames).toContain("write");
expect(mainToolNames).toContain("edit"); expect(mainToolNames).toContain("edit");
@ -143,7 +143,7 @@ describe("Agent-specific tool filtering", () => {
}); });
const familyToolNames = familyTools.map((t) => t.name); const familyToolNames = familyTools.map((t) => t.name);
expect(familyToolNames).toContain("read"); expect(familyToolNames).toContain("read");
expect(familyToolNames).not.toContain("bash"); expect(familyToolNames).not.toContain("exec");
expect(familyToolNames).not.toContain("write"); expect(familyToolNames).not.toContain("write");
expect(familyToolNames).not.toContain("edit"); expect(familyToolNames).not.toContain("edit");
}); });
@ -159,7 +159,7 @@ describe("Agent-specific tool filtering", () => {
id: "work", id: "work",
workspace: "~/clawd-work", workspace: "~/clawd-work",
tools: { tools: {
deny: ["bash", "process"], // Agent deny (override) deny: ["exec", "process"], // Agent deny (override)
}, },
}, },
], ],
@ -176,7 +176,7 @@ describe("Agent-specific tool filtering", () => {
const toolNames = tools.map((t) => t.name); const toolNames = tools.map((t) => t.name);
// Agent policy overrides global: browser is allowed again // Agent policy overrides global: browser is allowed again
expect(toolNames).toContain("browser"); expect(toolNames).toContain("browser");
expect(toolNames).not.toContain("bash"); expect(toolNames).not.toContain("exec");
expect(toolNames).not.toContain("process"); expect(toolNames).not.toContain("process");
}); });
@ -199,7 +199,7 @@ describe("Agent-specific tool filtering", () => {
}, },
tools: { tools: {
allow: ["read"], // Agent further restricts to only read allow: ["read"], // Agent further restricts to only read
deny: ["bash", "write"], deny: ["exec", "write"],
}, },
}, },
], ],
@ -207,7 +207,7 @@ describe("Agent-specific tool filtering", () => {
tools: { tools: {
sandbox: { sandbox: {
tools: { tools: {
allow: ["read", "write", "bash"], // Sandbox allows these allow: ["read", "write", "exec"], // Sandbox allows these
deny: [], deny: [],
}, },
}, },
@ -237,7 +237,7 @@ describe("Agent-specific tool filtering", () => {
capDrop: [], capDrop: [],
} satisfies SandboxDockerConfig, } satisfies SandboxDockerConfig,
tools: { tools: {
allow: ["read", "write", "bash"], allow: ["read", "write", "exec"],
deny: [], deny: [],
}, },
browserAllowHostControl: false, browserAllowHostControl: false,
@ -246,14 +246,14 @@ describe("Agent-specific tool filtering", () => {
const toolNames = tools.map((t) => t.name); const toolNames = tools.map((t) => t.name);
// Agent policy should be applied first, then sandbox // Agent policy should be applied first, then sandbox
// Agent allows only "read", sandbox allows ["read", "write", "bash"] // Agent allows only "read", sandbox allows ["read", "write", "exec"]
// Result: only "read" (most restrictive wins) // Result: only "read" (most restrictive wins)
expect(toolNames).toContain("read"); expect(toolNames).toContain("read");
expect(toolNames).not.toContain("bash"); expect(toolNames).not.toContain("exec");
expect(toolNames).not.toContain("write"); expect(toolNames).not.toContain("write");
}); });
it("should run bash synchronously when process is denied", async () => { it("should run exec synchronously when process is denied", async () => {
const cfg: ClawdbotConfig = { const cfg: ClawdbotConfig = {
tools: { tools: {
deny: ["process"], deny: ["process"],
@ -266,10 +266,10 @@ describe("Agent-specific tool filtering", () => {
workspaceDir: "/tmp/test-main", workspaceDir: "/tmp/test-main",
agentDir: "/tmp/agent-main", agentDir: "/tmp/agent-main",
}); });
const bash = tools.find((tool) => tool.name === "bash"); const execTool = tools.find((tool) => tool.name === "exec");
expect(bash).toBeDefined(); expect(execTool).toBeDefined();
const result = await bash?.execute("call1", { const result = await execTool?.execute("call1", {
command: "echo done", command: "echo done",
yieldMs: 10, yieldMs: 10,
}); });

View file

@ -153,9 +153,9 @@ describe("createClawdbotCodingTools", () => {
} }
}); });
it("includes bash and process tools", () => { it("includes exec and process tools", () => {
const tools = createClawdbotCodingTools(); const tools = createClawdbotCodingTools();
expect(tools.some((tool) => tool.name === "bash")).toBe(true); expect(tools.some((tool) => tool.name === "exec")).toBe(true);
expect(tools.some((tool) => tool.name === "process")).toBe(true); expect(tools.some((tool) => tool.name === "process")).toBe(true);
}); });
@ -165,7 +165,7 @@ describe("createClawdbotCodingTools", () => {
modelAuthMode: "oauth", modelAuthMode: "oauth",
}); });
const names = new Set(tools.map((tool) => tool.name)); const names = new Set(tools.map((tool) => tool.name));
expect(names.has("bash")).toBe(true); expect(names.has("exec")).toBe(true);
expect(names.has("read")).toBe(true); expect(names.has("read")).toBe(true);
expect(names.has("write")).toBe(true); expect(names.has("write")).toBe(true);
expect(names.has("edit")).toBe(true); expect(names.has("edit")).toBe(true);
@ -210,7 +210,7 @@ describe("createClawdbotCodingTools", () => {
expect(names.has("sessions_spawn")).toBe(false); expect(names.has("sessions_spawn")).toBe(false);
expect(names.has("read")).toBe(true); expect(names.has("read")).toBe(true);
expect(names.has("bash")).toBe(true); expect(names.has("exec")).toBe(true);
expect(names.has("process")).toBe(true); expect(names.has("process")).toBe(true);
}); });
@ -330,7 +330,7 @@ describe("createClawdbotCodingTools", () => {
browserAllowHostControl: false, browserAllowHostControl: false,
}; };
const tools = createClawdbotCodingTools({ sandbox }); const tools = createClawdbotCodingTools({ sandbox });
expect(tools.some((tool) => tool.name === "bash")).toBe(true); expect(tools.some((tool) => tool.name === "exec")).toBe(true);
expect(tools.some((tool) => tool.name === "read")).toBe(false); expect(tools.some((tool) => tool.name === "read")).toBe(false);
expect(tools.some((tool) => tool.name === "browser")).toBe(false); expect(tools.some((tool) => tool.name === "browser")).toBe(false);
}); });
@ -371,7 +371,7 @@ describe("createClawdbotCodingTools", () => {
const tools = createClawdbotCodingTools({ const tools = createClawdbotCodingTools({
config: { tools: { deny: ["browser"] } }, config: { tools: { deny: ["browser"] } },
}); });
expect(tools.some((tool) => tool.name === "bash")).toBe(true); expect(tools.some((tool) => tool.name === "exec")).toBe(true);
expect(tools.some((tool) => tool.name === "browser")).toBe(false); expect(tools.some((tool) => tool.name === "browser")).toBe(false);
}); });

View file

@ -15,9 +15,9 @@ import {
resolveAgentIdFromSessionKey, resolveAgentIdFromSessionKey,
} from "./agent-scope.js"; } from "./agent-scope.js";
import { import {
type BashToolDefaults, createExecTool,
createBashTool,
createProcessTool, createProcessTool,
type ExecToolDefaults,
type ProcessToolDefaults, type ProcessToolDefaults,
} from "./bash-tools.js"; } from "./bash-tools.js";
import { createClawdbotTools } from "./clawdbot-tools.js"; import { createClawdbotTools } from "./clawdbot-tools.js";
@ -290,9 +290,18 @@ function cleanToolSchemaForGemini(schema: Record<string, unknown>): unknown {
return cleanSchemaForGemini(schema); return cleanSchemaForGemini(schema);
} }
const TOOL_NAME_ALIASES: Record<string, string> = {
bash: "exec",
};
function normalizeToolName(name: string) {
const normalized = name.trim().toLowerCase();
return TOOL_NAME_ALIASES[normalized] ?? normalized;
}
function normalizeToolNames(list?: string[]) { function normalizeToolNames(list?: string[]) {
if (!list) return []; if (!list) return [];
return list.map((entry) => entry.trim().toLowerCase()).filter(Boolean); return list.map(normalizeToolName).filter(Boolean);
} }
const DEFAULT_SUBAGENT_TOOL_DENY = [ const DEFAULT_SUBAGENT_TOOL_DENY = [
@ -354,7 +363,7 @@ function isToolAllowedByPolicy(name: string, policy?: SandboxToolPolicy) {
const deny = new Set(normalizeToolNames(policy.deny)); const deny = new Set(normalizeToolNames(policy.deny));
const allowRaw = normalizeToolNames(policy.allow); const allowRaw = normalizeToolNames(policy.allow);
const allow = allowRaw.length > 0 ? new Set(allowRaw) : null; const allow = allowRaw.length > 0 ? new Set(allowRaw) : null;
const normalized = name.trim().toLowerCase(); const normalized = normalizeToolName(name);
if (deny.has(normalized)) return false; if (deny.has(normalized)) return false;
if (allow) return allow.has(normalized); if (allow) return allow.has(normalized);
return true; return true;
@ -467,7 +476,7 @@ function wrapToolWithAbortSignal(
} }
export function createClawdbotCodingTools(options?: { export function createClawdbotCodingTools(options?: {
bash?: BashToolDefaults & ProcessToolDefaults; exec?: ExecToolDefaults & ProcessToolDefaults;
messageProvider?: string; messageProvider?: string;
agentAccountId?: string; agentAccountId?: string;
sandbox?: SandboxContext | null; sandbox?: SandboxContext | null;
@ -495,14 +504,14 @@ export function createClawdbotCodingTools(options?: {
/** Mutable ref to track if a reply was sent (for "first" mode). */ /** Mutable ref to track if a reply was sent (for "first" mode). */
hasRepliedRef?: { value: boolean }; hasRepliedRef?: { value: boolean };
}): AnyAgentTool[] { }): AnyAgentTool[] {
const bashToolName = "bash"; const execToolName = "exec";
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined; const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
const { agentId, policy: effectiveToolsPolicy } = resolveEffectiveToolPolicy({ const { agentId, policy: effectiveToolsPolicy } = resolveEffectiveToolPolicy({
config: options?.config, config: options?.config,
sessionKey: options?.sessionKey, sessionKey: options?.sessionKey,
}); });
const scopeKey = const scopeKey =
options?.bash?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined); options?.exec?.scopeKey ?? (agentId ? `agent:${agentId}` : undefined);
const subagentPolicy = const subagentPolicy =
isSubagentSessionKey(options?.sessionKey) && options?.sessionKey isSubagentSessionKey(options?.sessionKey) && options?.sessionKey
? resolveSubagentToolPolicy(options.config) ? resolveSubagentToolPolicy(options.config)
@ -524,7 +533,7 @@ export function createClawdbotCodingTools(options?: {
const freshReadTool = createReadTool(workspaceRoot); const freshReadTool = createReadTool(workspaceRoot);
return [createClawdbotReadTool(freshReadTool)]; return [createClawdbotReadTool(freshReadTool)];
} }
if (tool.name === bashToolName) return []; if (tool.name === "bash" || tool.name === execToolName) return [];
if (tool.name === "write") { if (tool.name === "write") {
if (sandboxRoot) return []; if (sandboxRoot) return [];
return [createWriteTool(workspaceRoot)]; return [createWriteTool(workspaceRoot)];
@ -535,8 +544,8 @@ export function createClawdbotCodingTools(options?: {
} }
return [tool as AnyAgentTool]; return [tool as AnyAgentTool];
}); });
const bashTool = createBashTool({ const execTool = createExecTool({
...options?.bash, ...options?.exec,
cwd: options?.workspaceDir, cwd: options?.workspaceDir,
allowBackground, allowBackground,
scopeKey, scopeKey,
@ -550,7 +559,7 @@ export function createClawdbotCodingTools(options?: {
: undefined, : undefined,
}); });
const processTool = createProcessTool({ const processTool = createProcessTool({
cleanupMs: options?.bash?.cleanupMs, cleanupMs: options?.exec?.cleanupMs,
scopeKey, scopeKey,
}); });
const tools: AnyAgentTool[] = [ const tools: AnyAgentTool[] = [
@ -563,7 +572,7 @@ export function createClawdbotCodingTools(options?: {
] ]
: [] : []
: []), : []),
bashTool as unknown as AnyAgentTool, execTool as unknown as AnyAgentTool,
processTool as unknown as AnyAgentTool, processTool as unknown as AnyAgentTool,
// Provider docking: include provider-defined agent tools (login, etc.). // Provider docking: include provider-defined agent tools (login, etc.).
...listProviderAgentTools({ cfg: options?.config }), ...listProviderAgentTools({ cfg: options?.config }),

View file

@ -110,13 +110,13 @@ describe("workspace path resolution", () => {
}); });
}); });
it("defaults bash cwd to workspaceDir when workdir is omitted", async () => { it("defaults exec cwd to workspaceDir when workdir is omitted", async () => {
await withTempDir("clawdbot-ws-", async (workspaceDir) => { await withTempDir("clawdbot-ws-", async (workspaceDir) => {
const tools = createClawdbotCodingTools({ workspaceDir }); const tools = createClawdbotCodingTools({ workspaceDir });
const bashTool = tools.find((tool) => tool.name === "bash"); const execTool = tools.find((tool) => tool.name === "exec");
expect(bashTool).toBeDefined(); expect(execTool).toBeDefined();
const result = await bashTool?.execute("ws-bash", { const result = await execTool?.execute("ws-exec", {
command: "echo ok", command: "echo ok",
}); });
const cwd = const cwd =
@ -134,14 +134,14 @@ describe("workspace path resolution", () => {
}); });
}); });
it("lets bash workdir override the workspace default", async () => { it("lets exec workdir override the workspace default", async () => {
await withTempDir("clawdbot-ws-", async (workspaceDir) => { await withTempDir("clawdbot-ws-", async (workspaceDir) => {
await withTempDir("clawdbot-override-", async (overrideDir) => { await withTempDir("clawdbot-override-", async (overrideDir) => {
const tools = createClawdbotCodingTools({ workspaceDir }); const tools = createClawdbotCodingTools({ workspaceDir });
const bashTool = tools.find((tool) => tool.name === "bash"); const execTool = tools.find((tool) => tool.name === "exec");
expect(bashTool).toBeDefined(); expect(execTool).toBeDefined();
const result = await bashTool?.execute("ws-bash-override", { const result = await execTool?.execute("ws-exec-override", {
command: "echo ok", command: "echo ok",
workdir: overrideDir, workdir: overrideDir,
}); });

View file

@ -450,7 +450,7 @@ describe("Agent-specific sandbox config", () => {
sandbox: { sandbox: {
tools: { tools: {
allow: ["read"], allow: ["read"],
deny: ["bash"], deny: ["exec"],
}, },
}, },
}, },

View file

@ -163,7 +163,7 @@ const DEFAULT_SANDBOX_WORKDIR = "/workspace";
const DEFAULT_SANDBOX_IDLE_HOURS = 24; const DEFAULT_SANDBOX_IDLE_HOURS = 24;
const DEFAULT_SANDBOX_MAX_AGE_DAYS = 7; const DEFAULT_SANDBOX_MAX_AGE_DAYS = 7;
const DEFAULT_TOOL_ALLOW = [ const DEFAULT_TOOL_ALLOW = [
"bash", "exec",
"process", "process",
"read", "read",
"write", "write",

View file

@ -9,14 +9,14 @@ describe("sanitizeToolUseResultPairing", () => {
role: "assistant", role: "assistant",
content: [ content: [
{ type: "toolCall", id: "call_1", name: "read", arguments: {} }, { type: "toolCall", id: "call_1", name: "read", arguments: {} },
{ type: "toolCall", id: "call_2", name: "bash", arguments: {} }, { type: "toolCall", id: "call_2", name: "exec", arguments: {} },
], ],
}, },
{ role: "user", content: "user message that should come after tool use" }, { role: "user", content: "user message that should come after tool use" },
{ {
role: "toolResult", role: "toolResult",
toolCallId: "call_2", toolCallId: "call_2",
toolName: "bash", toolName: "exec",
content: [{ type: "text", text: "ok" }], content: [{ type: "text", text: "ok" }],
isError: false, isError: false,
}, },

View file

@ -37,7 +37,7 @@ describe("buildAgentSystemPrompt", () => {
it("lists available tools when provided", () => { it("lists available tools when provided", () => {
const prompt = buildAgentSystemPrompt({ const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd", workspaceDir: "/tmp/clawd",
toolNames: ["bash", "sessions_list", "sessions_history", "sessions_send"], toolNames: ["exec", "sessions_list", "sessions_history", "sessions_send"],
}); });
expect(prompt).toContain("Tool availability (filtered by policy):"); expect(prompt).toContain("Tool availability (filtered by policy):");
@ -49,13 +49,13 @@ describe("buildAgentSystemPrompt", () => {
it("preserves tool casing in the prompt", () => { it("preserves tool casing in the prompt", () => {
const prompt = buildAgentSystemPrompt({ const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd", workspaceDir: "/tmp/clawd",
toolNames: ["Read", "Bash", "process"], toolNames: ["Read", "Exec", "process"],
skillsPrompt: skillsPrompt:
"<available_skills>\n <skill>\n <name>demo</name>\n </skill>\n</available_skills>", "<available_skills>\n <skill>\n <name>demo</name>\n </skill>\n</available_skills>",
}); });
expect(prompt).toContain("- Read: Read file contents"); expect(prompt).toContain("- Read: Read file contents");
expect(prompt).toContain("- Bash: Run shell commands"); expect(prompt).toContain("- Exec: Run shell commands");
expect(prompt).toContain( expect(prompt).toContain(
"Use `Read` to load the SKILL.md at the location listed for that skill.", "Use `Read` to load the SKILL.md at the location listed for that skill.",
); );
@ -90,7 +90,7 @@ describe("buildAgentSystemPrompt", () => {
it("adds ClaudeBot self-update guidance when gateway tool is available", () => { it("adds ClaudeBot self-update guidance when gateway tool is available", () => {
const prompt = buildAgentSystemPrompt({ const prompt = buildAgentSystemPrompt({
workspaceDir: "/tmp/clawd", workspaceDir: "/tmp/clawd",
toolNames: ["gateway", "bash"], toolNames: ["gateway", "exec"],
}); });
expect(prompt).toContain("## Clawdbot Self-Update"); expect(prompt).toContain("## Clawdbot Self-Update");

View file

@ -53,8 +53,8 @@ export function buildAgentSystemPrompt(params: {
grep: "Search file contents for patterns", grep: "Search file contents for patterns",
find: "Find files by glob pattern", find: "Find files by glob pattern",
ls: "List directory contents", ls: "List directory contents",
bash: "Run shell commands", exec: "Run shell commands",
process: "Manage background bash sessions", process: "Manage background exec sessions",
// Provider docking: add provider login tools here when a provider needs interactive linking. // Provider docking: add provider login tools here when a provider needs interactive linking.
browser: "Control web browser", browser: "Control web browser",
canvas: "Present/eval/snapshot the Canvas", canvas: "Present/eval/snapshot the Canvas",
@ -80,7 +80,7 @@ export function buildAgentSystemPrompt(params: {
"grep", "grep",
"find", "find",
"ls", "ls",
"bash", "exec",
"process", "process",
"browser", "browser",
"canvas", "canvas",
@ -133,7 +133,7 @@ export function buildAgentSystemPrompt(params: {
const hasGateway = availableTools.has("gateway"); const hasGateway = availableTools.has("gateway");
const readToolName = resolveToolName("read"); const readToolName = resolveToolName("read");
const bashToolName = resolveToolName("bash"); const execToolName = resolveToolName("exec");
const processToolName = resolveToolName("process"); const processToolName = resolveToolName("process");
const extraSystemPrompt = params.extraSystemPrompt?.trim(); const extraSystemPrompt = params.extraSystemPrompt?.trim();
const ownerNumbers = (params.ownerNumbers ?? []) const ownerNumbers = (params.ownerNumbers ?? [])
@ -195,8 +195,8 @@ export function buildAgentSystemPrompt(params: {
"- grep: search file contents for patterns", "- grep: search file contents for patterns",
"- find: find files by glob pattern", "- find: find files by glob pattern",
"- ls: list directory contents", "- ls: list directory contents",
`- ${bashToolName}: run shell commands (supports background via yieldMs/background)`, `- ${execToolName}: run shell commands (supports background via yieldMs/background)`,
`- ${processToolName}: manage background bash sessions`, `- ${processToolName}: manage background exec sessions`,
"- browser: control clawd's dedicated browser", "- browser: control clawd's dedicated browser",
"- canvas: present/eval/snapshot the Canvas", "- canvas: present/eval/snapshot the Canvas",
"- nodes: list/describe/notify/camera/screen on paired nodes", "- nodes: list/describe/notify/camera/screen on paired nodes",
@ -277,7 +277,7 @@ export function buildAgentSystemPrompt(params: {
)}` )}`
: "", : "",
params.sandboxInfo.elevated?.allowed params.sandboxInfo.elevated?.allowed
? "Elevated bash is available for this session." ? "Elevated exec is available for this session."
: "", : "",
params.sandboxInfo.elevated?.allowed params.sandboxInfo.elevated?.allowed
? "User can toggle with /elevated on|off." ? "User can toggle with /elevated on|off."
@ -288,7 +288,7 @@ export function buildAgentSystemPrompt(params: {
params.sandboxInfo.elevated?.allowed params.sandboxInfo.elevated?.allowed
? `Current elevated level: ${ ? `Current elevated level: ${
params.sandboxInfo.elevated.defaultLevel params.sandboxInfo.elevated.defaultLevel
} (on runs bash on host; off runs in sandbox).` } (on runs exec on host; off runs in sandbox).`
: "", : "",
] ]
.filter(Boolean) .filter(Boolean)
@ -315,7 +315,7 @@ export function buildAgentSystemPrompt(params: {
"## Messaging", "## Messaging",
"- Reply in current session → automatically routes to the source provider (Signal, Telegram, etc.)", "- Reply in current session → automatically routes to the source provider (Signal, Telegram, etc.)",
"- Cross-session messaging → use sessions_send(sessionKey, message)", "- Cross-session messaging → use sessions_send(sessionKey, message)",
"- Never use bash/curl for provider messaging; Clawdbot handles all routing internally.", "- Never use exec/curl for provider messaging; Clawdbot handles all routing internally.",
availableTools.has("message") availableTools.has("message")
? [ ? [
"", "",

View file

@ -25,9 +25,9 @@
] ]
}, },
"tools": { "tools": {
"bash": { "exec": {
"emoji": "🛠️", "emoji": "🛠️",
"title": "Bash", "title": "Exec",
"detailKeys": ["command"] "detailKeys": ["command"]
}, },
"process": { "process": {

View file

@ -1100,7 +1100,7 @@ describe("legacy config detection", () => {
expect(res.changes).toContain("Moved agent.tools.allow → tools.allow."); expect(res.changes).toContain("Moved agent.tools.allow → tools.allow.");
expect(res.changes).toContain("Moved agent.tools.deny → tools.deny."); expect(res.changes).toContain("Moved agent.tools.deny → tools.deny.");
expect(res.changes).toContain("Moved agent.elevated → tools.elevated."); expect(res.changes).toContain("Moved agent.elevated → tools.elevated.");
expect(res.changes).toContain("Moved agent.bash → tools.bash."); expect(res.changes).toContain("Moved agent.bash → tools.exec.");
expect(res.changes).toContain( expect(res.changes).toContain(
"Moved agent.sandbox.tools → tools.sandbox.tools.", "Moved agent.sandbox.tools → tools.sandbox.tools.",
); );
@ -1118,7 +1118,7 @@ describe("legacy config detection", () => {
enabled: true, enabled: true,
allowFrom: { discord: ["user:1"] }, allowFrom: { discord: ["user:1"] },
}); });
expect(res.config?.tools?.bash).toEqual({ timeoutSec: 12 }); expect(res.config?.tools?.exec).toEqual({ timeoutSec: 12 });
expect(res.config?.tools?.sandbox?.tools).toEqual({ expect(res.config?.tools?.sandbox?.tools).toEqual({
allow: ["browser.open"], allow: ["browser.open"],
}); });

View file

@ -179,7 +179,7 @@ const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
{ {
path: ["agent"], path: ["agent"],
message: message:
"agent.* was moved; use agents.defaults (and tools.* for tool/elevated/bash settings) instead (run `clawdbot doctor` to migrate).", "agent.* was moved; use agents.defaults (and tools.* for tool/elevated/exec settings) instead (run `clawdbot doctor` to migrate).",
}, },
{ {
path: ["agent", "model"], path: ["agent", "model"],
@ -819,9 +819,11 @@ const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [
const bash = getRecord(agent.bash); const bash = getRecord(agent.bash);
if (bash) { if (bash) {
if (tools.bash === undefined) { if (tools.exec === undefined && tools.bash === undefined) {
tools.bash = bash; tools.exec = bash;
changes.push("Moved agent.bash → tools.bash."); changes.push("Moved agent.bash → tools.exec.");
} else if (tools.exec !== undefined) {
changes.push("Removed agent.bash (tools.exec already set).");
} else { } else {
changes.push("Removed agent.bash (tools.bash already set)."); changes.push("Removed agent.bash (tools.bash already set).");
} }

View file

@ -972,7 +972,7 @@ export type QueueConfig = {
export type AgentToolsConfig = { export type AgentToolsConfig = {
allow?: string[]; allow?: string[];
deny?: string[]; deny?: string[];
/** Per-agent elevated bash gate (can only further restrict global tools.elevated). */ /** Per-agent elevated exec gate (can only further restrict global tools.elevated). */
elevated?: { elevated?: {
/** Enable or disable elevated mode for this agent (default: true). */ /** Enable or disable elevated mode for this agent (default: true). */
enabled?: boolean; enabled?: boolean;
@ -1003,14 +1003,23 @@ export type ToolsConfig = {
/** Allowlist of agent ids or patterns (implementation-defined). */ /** Allowlist of agent ids or patterns (implementation-defined). */
allow?: string[]; allow?: string[];
}; };
/** Elevated bash permissions for the host machine. */ /** Elevated exec permissions for the host machine. */
elevated?: { elevated?: {
/** Enable or disable elevated mode (default: true). */ /** Enable or disable elevated mode (default: true). */
enabled?: boolean; enabled?: boolean;
/** Approved senders for /elevated (per-provider allowlists). */ /** Approved senders for /elevated (per-provider allowlists). */
allowFrom?: AgentElevatedAllowFromConfig; allowFrom?: AgentElevatedAllowFromConfig;
}; };
/** Bash tool defaults. */ /** Exec tool defaults. */
exec?: {
/** Default time (ms) before an exec command auto-backgrounds. */
backgroundMs?: number;
/** Default timeout (seconds) before auto-killing exec commands. */
timeoutSec?: number;
/** How long to keep finished sessions in memory (ms). */
cleanupMs?: number;
};
/** @deprecated Use tools.exec. */
bash?: { bash?: {
/** Default time (ms) before a bash command auto-backgrounds. */ /** Default time (ms) before a bash command auto-backgrounds. */
backgroundMs?: number; backgroundMs?: number;

View file

@ -905,6 +905,13 @@ const ToolsSchema = z
allowFrom: ElevatedAllowFromSchema, allowFrom: ElevatedAllowFromSchema,
}) })
.optional(), .optional(),
exec: z
.object({
backgroundMs: z.number().int().positive().optional(),
timeoutSec: z.number().int().positive().optional(),
cleanupMs: z.number().int().positive().optional(),
})
.optional(),
bash: z bash: z
.object({ .object({
backgroundMs: z.number().int().positive().optional(), backgroundMs: z.number().int().positive().optional(),

View file

@ -419,14 +419,14 @@ describeLive("gateway live (dev agent, profile keys)", () => {
`write-${runIdTool}.txt`, `write-${runIdTool}.txt`,
); );
const bashReadProbe = await client.request<AgentFinalPayload>( const execReadProbe = await client.request<AgentFinalPayload>(
"agent", "agent",
{ {
sessionKey, sessionKey,
idempotencyKey: `idem-${runIdTool}-bash-read`, idempotencyKey: `idem-${runIdTool}-exec-read`,
message: message:
"Clawdbot live tool probe (local, safe): " + "Clawdbot live tool probe (local, safe): " +
"use the tool named `bash` (or `Bash`) to run this command: " + "use the tool named `exec` (or `Exec`) to run this command: " +
`mkdir -p "${tempDir}" && printf '%s' '${nonceC}' > "${toolWritePath}". ` + `mkdir -p "${tempDir}" && printf '%s' '${nonceC}' > "${toolWritePath}". ` +
`Then use the tool named \`read\` (or \`Read\`) with JSON arguments {"path":"${toolWritePath}"}. ` + `Then use the tool named \`read\` (or \`Read\`) with JSON arguments {"path":"${toolWritePath}"}. ` +
"Finally reply including the nonce text you read back.", "Finally reply including the nonce text you read back.",
@ -434,15 +434,15 @@ describeLive("gateway live (dev agent, profile keys)", () => {
}, },
{ expectFinal: true }, { expectFinal: true },
); );
if (bashReadProbe?.status !== "ok") { if (execReadProbe?.status !== "ok") {
throw new Error( throw new Error(
`bash+read probe failed: status=${String(bashReadProbe?.status)}`, `exec+read probe failed: status=${String(execReadProbe?.status)}`,
); );
} }
const bashReadText = extractPayloadText(bashReadProbe?.result); const execReadText = extractPayloadText(execReadProbe?.result);
if (!bashReadText.includes(nonceC)) { if (!execReadText.includes(nonceC)) {
throw new Error( throw new Error(
`bash+read probe missing nonce: ${bashReadText}`, `exec+read probe missing nonce: ${execReadText}`,
); );
} }