# Execution Guards

The write guard, argv-only subprocess schema and timeouts that sit alongside the sandbox.

## Filesystem write guard

`go-pkg/filesystem` write APIs (`WriteFile`, `WriteText`, `WriteJSON`, `AppendText`, `Copy`, `Move`, `Remove`) all enforce `IsDenied` internally; `CheckDir` only creates directories and does not check it. Any agenvoy code that bypasses `go-pkg/filesystem` and writes via `os.WriteFile` directly **escapes the policy** — this is forbidden.

The `internal/filesystem` package retains only path computation, runtime limits, and domain wrappers. It does not duplicate read/write logic.

Outside `$HOME`, `run_command` and `background_command` need the paths declared up front: the call carries `write_paths` (absolute paths only), the user approves them with the system password, and only then are they bound read-write into the sandbox for that session. A write to an unapproved path fails with an explicit message, and a permission error on an unbound path outside `$HOME` gets a hint to re-run with `write_paths` rather than a bare error the model would misread as an ownership problem. Since v1.3.2, when a sandboxed command fails with a permission marker (`permission denied`, `operation not permitted`, `read-only file system`, `EACCES`, `not writable`) and the output names no path outside `$HOME` to grant through `write_paths`, the result appends advice that treats leaving the sandbox as the last resort, not the default retry (since v1.3.11; from v1.3.2 to v1.3.10 it told the agent to re-run at once with `sandbox: false`): a connection or auth failure such as `Permission denied (publickey)` or `could not resolve host` → `network: true`, or report the credential problem; a write → a path under the work directory, or `write_paths` for the path that failed; a task that genuinely needs a system socket, device or service, or a root-owned target → `sandbox: false` or argv `['sudo', ...]`, approved with the system password; otherwise report what was blocked and stop.

## Subprocess argv-only schema

`run_command` accepts only `argv: string[]` (minItems 1) plus optional `write_paths`, since v1.0.25 `network` (boolean, default off), and since v1.3.0 `sandbox` (boolean, default true; `false` runs outside the sandbox after a password check and ignores `network` / `write_paths`). `background_command` adds `timeout_min` (1-60, default 60) and `keywords` (the error keywords its output watch looks for) to the same set; `run_command_background` was renamed `background_command` and its `interval` and `prompt` parameters were dropped (Removed in v1.3.2). It does **not** accept a `command: string` with auto-tokenization. This zero-parsing approach removes shell-injection surface in the agent layer.

At the top level, since v1.3.2 `['sudo', '<binary>', ...]` is accepted: no sudo flags, no nested `sudo` / `su`, and the target must be a bare binary not on `denied_command`; it raises a `sudo: <cmd>` confirmation that always asks for the system password and then runs as `sudo -n <binary> ...` outside the sandbox. Since v1.3.11 cancelling such a call (foreground or `background_command`) sends `SIGTERM` to `sudo`, which relays it to the root command, instead of `SIGKILL`ing the process group, which a root child would survive; if it has not exited within the 2-second `WaitDelay`, `sudo` is then killed. `rm` is routed to the trash (`sudo rm` deletes for real); its targets follow the path tiers — inside the working directory it runs under `auto`, elsewhere in `$HOME` it asks, outside `$HOME` it needs the system password — and `cd` is rejected because the work directory is set by the user in the TUI; the agent `cd` that switched the work directory was Removed in v1.3.0.

Shell features (pipes, redirects) require an explicit `["sh", "-c", "cmd | pipe"]` (or `bash -c`). That script is **parsed** with `mvdan.cc/sh` rather than string-matched, and every command node inside it is checked:

| Rule | Effect |
|---|---|
| Bare command names only | `/usr/bin/curl` is rejected — the binary must be written as `curl` |
| No dynamic commands | A command built from a variable or command substitution is rejected outright |
| No `rm` or `sudo` | Both are rejected inside `sh -c`; the `sudo` error points to the argv form `['sudo', '<binary>', ...]` |
| Denied binaries | Anything on `denied_command` is rejected, inside `sh -c` as well |
| Shell builtins | A fixed set (`cd`, `echo`, `test`, `export`, ...) passes without an allowlist entry |
| Nested `sh -c` | Recursively validated with the same rules; a nested script that is not a static string is rejected |

## Timeouts

Every tool carries its own timeout, defaulting to 15 minutes (`DEFAULT_TOOL_TIMEOUT`; one minute before v1.1.2) and overridden per tool at registration (`open_file` 10 s, `html_template` 30 s, `fetch_page` and `search_web` 90 s, `generate_audio` 5 min, `download_file` 10 min, `generate_image` 15 min, `run_command` 30 min; `ask_user` has none). A background run is limited by its own argument instead: `timeout_min` up to 60 min (default 60) on `background_command` and `background_subagents`; `run_websocket` and its 24 h `timeout` were dropped (Removed in v1.3.2). Subagent invocations, including slot-wait time, are capped by `MAX_SUBAGENT_TIMEOUT_MIN` (30 min).

These are package-level values — the environment-variable overrides that used to control them were removed.
