11 KiB
Exec Tool - How It Works & Sandboxing
Overview
The exec tool allows the AI agent to execute shell commands on the host system. It supports both synchronous execution and background sessions with PTY support.
How Exec Works
Registration Flow
The exec tool is registered in pkg/agent/instance.go (lines 104-112):
if cfg.Tools.IsToolEnabled("exec") {
execTool, err := tools.NewExecToolWithConfig(workspace, restrict, cfg, allowReadPaths)
if err != nil {
logger.ErrorCF("agent", "Failed to initialize exec tool; continuing without exec", ...)
} else {
toolsRegistry.Register(execTool)
}
}
Prerequisites: The exec tool only registers when tools.exec.enabled = true in config.
LLM Invocation
The LLM calls the exec tool with JSON arguments:
{
"action": "run", // run, list, poll, read, write, kill, send-keys
"command": "ls -la", // shell command to execute
"workdir": "/workspace", // optional, defaults to workspace
"background": false, // run in background (returns sessionId)
"pty": false // use pseudo-terminal (Unix only)
}
Execution Flow
-
Action Routing (
pkg/tools/shell.go):run→runSync()orrunBackground()list→ list active sessionspoll→ check background process statusread→ read output from background sessionwrite→ write input to background session (PTY mode)kill→ terminate background sessionsend-keys→ send key sequence to PTY session
-
Synchronous Execution (
runSync()):- Command parsed and validated against deny patterns
exec.CommandContext()creates the command- Isolation applied via
isolation.Start(cmd)(bubblewrap on Linux) - stdout/stderr captured (max 1MB output)
- Waits for completion or timeout (default 60s)
-
Background Execution (
runBackground()):- Starts process in background
- Creates
ProcessSessiontracked by globalSessionManager - Returns
sessionIdfor later management - Sessions auto-cleanup 30 minutes after process exits
-
Isolation Wrapper (
pkg/isolation/):- Linux: bubblewrap sandbox (namespace isolation, readonly filesystem)
- Windows: Restricted tokens (limited process access)
- macOS: Currently no isolation (CGO_ENABLED=1 required)
Session Management
Background processes are tracked by SessionManager (singleton in shell.go):
type ProcessSession struct {
ID string
Cmd *exec.Cmd
Pty *pty.Pty
IsBackground bool
Output *bytes.Buffer
// ...
}
- Sessions stored in global
sessionManager listaction returns all active sessionspollreturns exit code and output- Auto-cleanup after 30 minutes
Security Measures
Built-in Deny Patterns
The exec tool blocks 40+ dangerous command patterns (shell.go lines 50-98):
| Category | Blocked Patterns |
|---|---|
| File Deletion | rm -rf, rm /f, rmdir /s, del /f |
| Disk Operations | format, mkfs, diskpart, dd if= |
| Block Devices | Writes to /dev/sd*, /dev/hd*, /dev/nvme* |
| System Control | shutdown, reboot, poweroff |
| Fork Bombs | :(){ :|:& };: (function fork bomb) |
| Command Substitution | Backticks, $(...), ${...} |
| Pipe to Shell | ` |
| Privilege Escalation | sudo, su - |
| Permission Changes | chmod 777, chmod -R 777, chown |
| Process Killing | kill -9, killall, pkill |
| Package Managers | npm install -g, pip install --user, apt/yum/dnf install/remove |
| Containers | docker run, docker exec, podman |
| Version Control | git push, git reset --hard, git force |
| Remote Access | ssh ...@, scp, rsync -e ssh |
| Shell Scripts | source *.sh, bash *.sh, eval |
Configuration-Based Security
| Config Key | Description | Default |
|---|---|---|
tools.exec.enabled |
Master switch for exec tool | true |
tools.exec.enable_deny_patterns |
Enable/disable built-in deny patterns | true |
tools.exec.custom_deny_patterns |
Additional regex patterns to block | [] |
tools.exec.custom_allow_patterns |
Exempt specific commands from deny checks | [] |
tools.exec.allow_remote |
Allow exec from external channels (Telegram, Discord, etc.) | true |
tools.exec.timeout_seconds |
Command timeout in seconds | 60 |
Channel Restrictions
When tools.exec.allow_remote = false:
- Allowed channels:
cli,system,subagent(defined inpkg/constants/channels.go) - Blocked channels:
telegram,discord,slack,wechat, etc.
Workspace Restriction
When agents.defaults.restrict_to_workspace = true:
- Commands cannot access paths outside workspace
- Path traversal (
..) blocked in commands - Symlinks resolved and checked against workspace bounds
How to Disable Exec
Method 1: Disable via Config (Recommended)
config.json:
{
"tools": {
"exec": {
"enabled": false
}
}
}
Environment variable:
export PICOCLAW_TOOLS_EXEC_ENABLED=false
Effect: The exec tool is not registered. The agent cannot execute shell commands. Other tools (read_file, write_file, etc.) remain available.
Method 2: Disable Remote Access Only
{
"tools": {
"exec": {
"enabled": true,
"allow_remote": false
}
}
}
Effect: Exec only works from cli, system, and subagent channels. External chat channels cannot use exec.
Method 3: Strict Timeout
{
"tools": {
"exec": {
"enabled": true,
"timeout_seconds": 10
}
}
}
Effect: All commands timeout after 10 seconds (default 60s).
How to Completely Remove Exec
To completely remove the exec tool from the codebase:
Step 1: Delete Implementation Files
rm pkg/tools/shell.go # Main ExecTool implementation
rm pkg/tools/shell_test.go # Tests
rm pkg/tools/shell_process_unix.go
rm pkg/tools/shell_process_windows.go
rm pkg/tools/shell_timeout_unix_test.go
rm pkg/tools/session.go # ProcessSession, SessionManager
rm pkg/tools/session_test.go
rm pkg/tools/spawn.go # Spawn tool (depends on exec)
rm pkg/tools/spawn_test.go
rm pkg/tools/spawn_status.go
rm pkg/tools/spawn_status_test.go
Step 2: Remove Registration Code
In pkg/agent/instance.go, remove lines ~104-112:
// REMOVE:
if cfg.Tools.IsToolEnabled("exec") {
execTool, err := tools.NewExecToolWithConfig(workspace, restrict, cfg, allowReadPaths)
if err != nil {
logger.ErrorCF("agent", "Failed to initialize exec tool; continuing without exec", ...)
} else {
toolsRegistry.Register(execTool)
}
}
Step 3: Remove Config Structure
In pkg/config/config.go:
- Delete
ExecConfigstruct (around line 763-770) - Remove
Exec ExecConfigfield fromToolsConfig(around line 822) - Remove
"exec"case fromIsToolEnabled()function (around line 1534)
Step 4: Remove Defaults
In pkg/config/defaults.go, delete the Exec: ExecConfig{...} block (around lines 365-372).
Step 5: Remove Isolation (Optional)
If no other tools use isolation:
rm -rf pkg/isolation/
Step 6: Update Documentation
Remove exec references from:
docs/reference/tools-api.mddocs/reference/tools_configuration.md
How to Further Sandbox Exec
A. Enable Subprocess Isolation
Linux (bubblewrap):
{
"isolation": {
"enabled": true,
"expose_paths": [
{"source": "/workspace", "target": "/workspace", "mode": "rw"},
{"source": "/usr", "target": "/usr", "mode": "ro"},
{"source": "/lib", "target": "/lib", "mode": "ro"},
{"source": "/bin", "target": "/bin", "mode": "ro"}
]
}
}
Windows (restricted tokens):
{
"isolation": {
"enabled": true
}
}
B. Restrict to Workspace Only
{
"agents": {
"defaults": {
"restrict_to_workspace": true,
"allow_read_outside_workspace": false
}
}
}
C. Add Custom Deny Patterns
{
"tools": {
"exec": {
"custom_deny_patterns": [
"nano", "vi", "vim", "emacs",
"apt-get", "yum", "dnf",
"docker", "podman"
]
}
}
}
D. Block All Package Managers
{
"tools": {
"exec": {
"custom_deny_patterns": [
"apt", "apt-get", "yum", "dnf", "pacman", "zypper",
"npm", "yarn", "pnpm",
"pip", "pip3", "conda",
"gem", "bundle",
"cargo", "rustup",
"go get", "go install"
]
}
}
}
E. Combined Strict Mode
{
"agents": {
"defaults": {
"restrict_to_workspace": true
}
},
"tools": {
"exec": {
"enabled": true,
"enable_deny_patterns": true,
"allow_remote": false,
"timeout_seconds": 30,
"custom_deny_patterns": [
"apt", "yum", "dnf", "npm", "pip", "docker"
]
}
},
"isolation": {
"enabled": true
}
}
Key Files Reference
| File | Purpose |
|---|---|
pkg/tools/shell.go |
ExecTool implementation, deny patterns, command guard |
pkg/tools/session.go |
ProcessSession and SessionManager |
pkg/tools/spawn.go |
Spawn tool (uses exec internally) |
pkg/tools/registry.go |
ToolRegistry - how tools are registered |
pkg/agent/instance.go |
Where exec tool is registered (lines 104-112) |
pkg/config/config.go |
ExecConfig struct (line 763), IsToolEnabled (line 1534) |
pkg/config/defaults.go |
Default exec settings (lines 365-372) |
pkg/isolation/runtime.go |
Subprocess isolation implementation |
pkg/constants/channels.go |
Internal channel definitions |
Tool Result Structure
The exec tool returns *ToolResult:
type ToolResult struct {
ForLLM string // Command output for LLM processing
ForUser string // Human-readable output for chat
MediaURLs []string // Media attachments (if any)
IsError bool // Whether execution failed
Async bool // True if background process
Err error // Underlying Go error
}
Example success result:
{
"ForLLM": "total 48\ndrwxr-xr-x 2 user user 4096 ...",
"ForUser": "Command executed successfully",
"IsError": false
}
Example error result:
{
"ForLLM": "Command blocked: matches deny pattern 'rm -rf'",
"ForUser": "This command is not allowed for security reasons",
"IsError": true
}
## Permission System (New)
When `tools.exec.ask_permission = true` (default), the exec tool will ask for user permission before accessing paths outside workspace.
### How It Works
1. Exec tool detects command accesses path outside workspace
2. Checks PermissionCache - if no permission, returns early
3. LLM calls `request_permission` tool
4. Tool returns prompt for user: "Allow once" or "Allow for session"
5. User responds, LLM re-calls exec tool
6. Permission cached for "once" (consumed after use) or "session" (persists)
### Request Permission Tool
| Field | Description |
|-------|-------------|
| `path` | Path that needs permission |
| `command` | Original command (for context) |
### Permission Options
- **once**: Permission consumed after first use
- **session**: Permission persists for entire session
- **no**: Access denied