# Execution Tools

Tools that run commands in the foreground or in the background.

| Tool | Description |
|---|---|
| `run_command` | Execute a binary with argv (argv-only schema, sandbox-wrapped via `go-pkg/sandbox`) in the foreground and return its combined output once it exits. A plain command with no shell metacharacter (`\|`, `&&`, `>`, `*`, `~`) is called directly, never wrapped; pipes, redirects and globbing need an explicit `['sh','-c','...']`, whose script is parsed and validated command-by-command — every binary must be a **bare command name**, and anything on `denied_command` is rejected outright. `['cd', ...]` is rejected: the work directory belongs to the user (set in the TUI with `Shift+P`), so a call passes absolute paths or `['sh','-c','cd <dir> && <command>']` instead — the agent `cd` that switched `Executor.WorkDir` was Removed in v1.3.0; `rm` is routed to the trash rather than deleting (inside the working directory it runs under `auto`, elsewhere in `$HOME` it asks, outside `$HOME` it needs the system password), and is rejected inside `sh -c`; on macOS `open` is rejected with a pointer to `open_file`, since the sandbox has no route to the app-launch service. Each call is capped at 30 min (`RUN_COMMAND_TIMEOUT`); a file watcher, a job likely over 5 minutes (`BACKGROUND_THRESHOLD_MIN`) or 跑完告訴我 / 背景跑 goes to `background_command`. Since v1.0.25 the sandbox keeps networking off by default: a command that has to reach a host (`git clone` / `fetch` / `pull` / `push`, `npm` / `pip install`, `go mod download`, `curl`) sets `network: true`, and such a call never counts as read-only, so it confirms under `safe` and runs without a prompt under `auto`. Since v1.3.2 a system package is installed through `run_command` itself: Linux → `['sudo','apt-get','install','-y','<pkg>']` (or dnf / yum / pacman / apk), macOS → `['brew','install','<pkg>']` with `sandbox: false`, never `sudo` with brew. Since v1.3.3 the manager comes from the system prompt's `Host OS` line, which also gives the OS version; `command -v` is the fallback only when that line names none. Commands on the read-only allowlist (`configs/jsons/read_only_command.json`, 532 entries since v1.3.3, 409 after `cd` left it in v1.3.0, merged with `read_only_command` from `config.json`) skip the confirm gate: since v1.0.25 the binary plus up to two following arguments are matched (`git status`, `docker ps`, `kubectl get`, `ls`, ...), and a match is cancelled when any argument is a sensitive path. The dedicated read-only command tool added in v1.0.23 (`run_command_readonly`, which ran in parallel) was removed in v1.0.25, and the short-lived `run_script` never shipped in a release; use `run_command`; since v1.3.3 calls that match the allowlist run concurrently, everything else serially. To write outside `$HOME` the call carries `write_paths`, which are bound in only after a password-backed approval. Since v1.3.2 argv `['sudo','<binary>', ...]` is accepted: no sudo flags, no nested `sudo` / `su`, and the target must be a bare binary name not on `denied_command`; it raises a confirmation labelled `sudo: <cmd>` that always demands the system password (even in allow-all mode; allow rules never skip it), then runs as `sudo -n <binary> ...` outside the sandbox; since v1.3.11 cancelling it (here or in `background_command`) sends `SIGTERM` to `sudo`, which relays it to the root command, and `sudo` is killed only if it is still running after the 2-second `WaitDelay`. `sudo rm` deletes for real, while plain `rm` still moves to the trash. `sudo` inside `sh -c` is still rejected, with an error pointing to the argv form. Since v1.3.0 `sandbox: false` runs the command outside the sandbox with the user's full permissions, and `network` / `write_paths` are then ignored; it is meant for a command the sandbox blocked with a permission error that `network` or `write_paths` cannot fix (Homebrew needs it, since its own sandbox cannot nest), and every such call asks for the system password — allow rules and allow-all never skip that prompt, and a channel that cannot collect the password gets the call back unrun. When a sandboxed command fails with a permission error and no path outside `$HOME` can be granted through `write_paths`, the result appends advice that treats leaving the sandbox as the last resort (since v1.3.11; from v1.3.2 to v1.3.10 it said to re-run right away with `sandbox: false`): a connection or auth failure (`Permission denied (publickey)`, `could not resolve host`) → `network: true` or report the credential problem; a write → a path under the work directory or `write_paths`; a genuine system socket, device or service, or a root-owned target → `sandbox: false` or argv `['sudo', ...]`; otherwise report what was blocked and stop. Since v1.3.11 streamed output and the result are secret-masked, and an argv containing a masked value from an earlier tool result is refused with `[Invalid]`; a command that needs a key reads it inside the same call (`['sh','-c','set -a; . ./.env; set +a; curl ...']`). Since v1.0.11 a file watcher is rejected before it starts: `--watch` / `--watch=...`, a `chokidar` binary, or an `npm` / `pnpm` / `yarn` / `bun` script that resolves to one — the package script is parsed out of `package.json` and followed up to 4 levels (5 before v1.1.2), through `sh -c` too, and since v1.1.2 a script already visited is not followed again — because `run_command` waits for the process to exit and a watcher never does. Since v1.3.1 the error sends the watcher to the background tool (`background_command` since v1.3.2) with `timeout_min`, or to a one-shot build |
| `background_command` | `run_command_background` was renamed `background_command` (Removed in v1.3.2). Added in v1.3.1 as `run_command_background`; v1.3.0 shipped the same behavior as a background mode of `run_command`, which now runs in the foreground only. Takes the same `argv`, `network`, `write_paths` and `sandbox` parameters (plus `objective` and `next` since v1.3.8) under the same sandbox, confirmation, read-only allowlist and allow-rule checks, and returns a task id at once so the turn continues. Meant for a file watcher (which runs only here), a job very likely over 5 minutes whose output does not decide the next tool call, or 跑完告訴我 / 背景跑. `timeout_min` (1-60, default 60) kills the command and reports a timeout; the final report keeps the last 31 KiB of output. The output is always watched: a new output line containing one of `keywords` (case-insensitive; default `error`, `err!`, `fail`, `fatal`, `panic`, `exception`, `traceback`, `segmentation fault`, `core dumped`, `out of memory`, `permission denied`, `command not found`, `no such file`) reports the latest output (up to 8 KiB) while the command keeps running, once the output settles (2 s of quiet, at most 10 s), and at most one report per 30 s. With a decision model set, a decision check first drops benign hits (such as `0 errors`) and repeats of the last report; a failed check sends the output unchecked with the failure noted. Without a decision model every hit is reported. The `interval` and `prompt` parameters were Removed in v1.3.2; use `keywords` instead. While the command runs, its output is appended to `~/.config/agenvoy/sessions/<session>/background/<task id>.log`, which is removed when the command ends; the start result gives that path and tells the agent to read its tail with `read_files` when asked how far it got. Excluded unless the request comes from the TUI (`cli-` origin with no deliver target) — since v1.3.2 no longer tied to having a decision model. Each session runs at most `MAX_BACKGROUND_TASKS` background tasks (`DEFAULT_CONCURRENCY`: half the CPU cores, at least 3), a pool shared with `background_subagents`. Since v1.3.8 legs started in the same turn form one group: its reports go to the starting task while that task is running or pending, and once it has ended the group runs `objective` + `next` + every report as a new task when all its legs finish (before, each finished task resumed the original task on its own). Pausing or cancelling the starting task stops its legs; a single leg is cancelled from the TUI `Esc` menu |

`pkg_manage` was Removed in v1.3.2; use `run_command` with argv `['sudo', ...]` (Linux package managers) or `sandbox: false` (Homebrew) instead.
