Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Shell Tool

execute_command

Execute a shell command and return its output.

Permission: Read (sandboxed) / Write (unsandboxed)

Parameters

NameTypeRequiredDescription
commandstringyesThe shell command to execute
timeout_msintegernoTimeout in milliseconds (default: 30000)
scratchpadstringnoSave output to the scratchpad under this name

Behavior

  • Executes the command via sh -c "<command>" on Unix, or powershell.exe -NoProfile -NonInteractive -Command "<command>" on Windows (same shell in both sandboxed and unsandboxed mode).
  • Captures both stdout and stderr.
  • Returns the exit code along with the output if non-zero.
  • Oversized output is losslessly persisted to the scratchpad by the agent layer; the tool itself never truncates.
  • Default timeout is 30 seconds. If the command exceeds the timeout, it is killed (on Unix, via the process group so backgrounded grandchildren are caught too).
  • Supports cancellation: pressing Ctrl+C while a command is running kills the child process.

Shell-specific semantics

  • Unix (sh -c): POSIX $VAR expansion applies. Pass a literal $ with single quotes ('$foo') or backslash escape (\$foo).
  • Windows (powershell.exe -Command): The script body reaches PowerShell directly. Use PowerShell syntax ($var = ..., $env:PATH), and crucially, do not wrap your command in another powershell -Command "...". The outer PowerShell will expand your inner $var references to empty strings before the inner shell runs, producing a parser error on mangled syntax. If you need to invoke a nested script, drop it into a .ps1 file and run it by path, use -EncodedCommand <base64>, or escape each $ as `$.

Read-Only Sandbox

In read mode, commands run inside a sandbox that blocks writes to the user’s real data. Reads, program execution, and network access still work normally: the threat model is “no state mutation, but curl http://x | pdftotext must keep working.”

What’s blocked vs allowed (across all backends)

SurfaceBlockedAllowed
Filesystem writes outside tmp / Low-integrity paths
Filesystem reads
Program execution
Outbound network (TCP/UDP)
dbus / systemd-user state mutationsBubblewrap / macOSLandlock / Windows
Mach IPC state mutation (launchd, pasteboard, LaunchServices)macOSLinux / Windows
COM / RPC to Low-integrity-accepting services (Windows)
Inheritance of sensitive parent env vars (API keys, OAuth tokens, …)✓ (all platforms)

The sandbox is not an adversarial containment boundary; it’s defense-in-depth against an agent accidentally modifying user data. Set permission to none if you don’t trust a turn at all.

Environment variable scrubbing

Read-mode sandboxes still permit outbound network (the threat model intentionally keeps curl http://x | pdftotext-style pipelines working), so any secret in the parent process’s environment (ANTHROPIC_API_KEY, AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, OAuth tokens, etc.) would be a live exfiltration vector under prompt injection. meka scrubs the child environment at spawn time across every backend (Bubblewrap, Landlock, Seatbelt, Windows Low-integrity).

  • Unix (Linux + macOS): allow-list. Only a curated set of vars survives into the read-mode child: PATH, HOME, USER, LOGNAME, SHELL, PWD, TERM, COLORTERM, LANG, TMPDIR, TMP, TEMP, plus everything matching the LC_* and XDG_* prefixes. Anything else is dropped, including credential-shaped vars (AWS_*, GITHUB_TOKEN, OPENAI_API_KEY, …) and credential-pointer vars (SSH_AUTH_SOCK, KUBECONFIG, GNUPGHOME, NETRC, GIT_ASKPASS, GIT_SSH_COMMAND, etc.) as well as benign-but-unlisted vars like EDITOR, PAGER, DISPLAY, custom toolchain vars, and so on. Unknown vars are dropped by default.

  • Windows: deny-list. PowerShell pulls in a long tail of system vars (PSModulePath, APPDATA, ProgramFiles, etc.) that don’t fit a tidy allow-list, so the Windows path lets everything through except names that match a heuristic deny-list. Dropped names include:

    • Credential-shaped substrings: *TOKEN*, *SECRET*, *PASSWORD*, *PASSPHRASE*, *API_KEY*, *_KEY*, *BEARER*, *CREDENTIAL*, etc.
    • Credential-pointer substrings: SSH_AUTH_SOCK, KUBECONFIG, GNUPGHOME, NETRC, GIT_ASKPASS, SSH_ASKPASS, GIT_SSH_COMMAND.
    • Provider / service prefixes: ANTHROPIC_*, OPENAI_*, AWS_*, GCP_*, GOOGLE_*, AZURE_*, GITHUB_*, OPENROUTER_*, GROQ_*, MISTRAL_*, COHERE_*, DATABASE_*, POSTGRES_*, MONGO_*, STRIPE_*, CLOUDFLARE_*, VAULT_*, OAUTH_*, JWT_*, SENTRY_*, SLACK_*, DISCORD_*, and others; see is_sensitive_env_name in src/sandbox.rs for the full list.

    The deny-list is intentionally aggressive on false positives (a legitimate GITHUB_ACTOR is dropped alongside GITHUB_TOKEN) because the cost of a missing env var is a confusing tool error, while the cost of a leaked credential is a live exfiltration channel.

Write mode keeps the full parent environment. Write mode is the trusted-operation path where users legitimately need NPM_TOKEN for npm publish, AWS_* creds for aws s3 cp, GH_TOKEN for gh pr create, etc. If you need a specific var inside a read-mode shell command, switch to write mode for that turn.

Linux: pick a backend

Linux supports two backends, selected via [shell].sandbox_backend in config.toml:

  • Bubblewrap (sandbox_backend = "bubblewrap", recommended): wraps the command in bwrap with --ro-bind /, tmpfs masks over /run, /tmp, /var/tmp, and $XDG_RUNTIME_DIR, plus --unshare-user --unshare-pid --unshare-uts --unshare-ipc. The tmpfs masks make the dbus session bus, systemd-user socket, and other socket-on-disk IPC paths unreachable, so systemctl --user start <unit>, dbus-send, and similar state-changing calls fail. Network is not unshared. Requires the bubblewrap package and a kernel with user-namespace creation enabled.
  • Landlock (sandbox_backend = "landlock", legacy / fallback): uses the Landlock LSM (kernel 5.13+). Blocks filesystem writes via landlock_restrict_self. Does not block dbus / systemd-user IPC, so a sandboxed shell can still invoke state-mutating dbus methods.

sandbox_backend is unset unless you pin it yourself; meka provider add does not write it. When unset, meka probes Bubblewrap once at startup and prefers it when available, falling back to Landlock with a one-shot warning that points at the install path and the suppress-this-warning escape hatch.

[shell]
sandbox = true                       # default; set to false to disable
sandbox_backend = "bubblewrap"       # or "landlock"; unset = auto-detect

macOS and Windows

  • macOS: Uses sandbox-exec with a hardened SBPL profile (modeled after Codex’s vendored seatbelt policy, which is itself based on Chrome’s renderer sandbox). The profile is closed-by-default: filesystem writes are blocked, Mach-lookup is restricted to a curated allow-list of safe services, and mutation paths (launchd job control, pasteboard, LaunchServices, distributed notifications) are not in the allow-list. Network and DNS resolution remain available. The sandbox_backend config key is ignored.
  • Windows: Spawns the child with a duplicated primary token dropped to Low integrity (SECURITY_MANDATORY_LOW_RID) via SetTokenInformation(TokenIntegrityLevel, …). Writes to the home directory, %APPDATA%, Program Files, and system directories (any location with Medium-or-higher integrity ACLs) are blocked by the kernel. Low integrity also strips token privileges, and the same env scrubbing applied on Unix runs here (see Environment variable scrubbing above). The sandbox_backend config key is ignored.

Low integrity is not a total write-denial: the child can still write to the small residual Low-integrity-writable surface (%LOCALAPPDATA%\Low, %TEMP%\Low, any path with an explicit Low-integrity write ACE) and to files it creates itself.

When the configured backend is unavailable

If sandbox_backend = "bubblewrap" is set but bwrap isn’t on $PATH (or user namespaces are denied), execute_command in read mode returns a hard error rather than silently falling back. The error names the configured backend and the specific failure reason. Either install bubblewrap, set sandbox_backend = "landlock", or switch to write mode (Shift+Tab).

Disabling the sandbox entirely

To disable sandboxed shell execution in read mode altogether, set sandbox = false under [shell]. When disabled, shell commands require write mode.

[shell]
sandbox = false