zap is a coding agent that executes shell commands and edits files under the user's identity. By default it runs with the same privileges as the user invoking it — full access to the filesystem, network, and processes that the user has.
The shell tool lets the LLM run arbitrary commands via sh -c. There is a
confused-model guardrail (a substring denylist) that blocks obviously
destructive patterns (rm -rf /, mkfs, :(){ :|:& };: etc.). This
guardrail is not a security boundary — it is trivially bypassed with
encoding, variable indirection, or path tricks. Its purpose is to catch
low-effort LLM mistakes, not to stop a determined adversary or a jailbroken
model.
Set sandbox in ~/.agent.toml (or AGENT_SANDBOX env var) to one of:
- Sets
current_dir(std::env::current_dir())on the child process. - Does not parse the command to reject paths — if the LLM references
/etc/passwd, the command runs but the OS-level path resolution will fail because the working directory is the project root. This is an actual boundary but not a hardened one. - Environment variables are inherited from the parent process (as in all modes).
- Requires Docker or Podman on
PATH. If neither is found, the command fails with an error. - The container image is
alpine:latest. No extra packages are installed. - Execution wrapper:
docker run --rm --network none \ -v '/project/path':'/project/path':ro \ --tmpfs /tmp:exec \ -w '/project/path' \ alpine:latest sh -c '<escaped-command>' - What is isolated: filesystem (project is read-only, host is invisible), network (none), process namespace.
- What is NOT isolated: CPU, memory, and disk I/O — a fork-bomb or
memory-exhaustion command can still impact the host. The container has
write access to
/tmp. - The container is destroyed immediately after the command completes
(
--rm).
-
Confused-model footguns — An LLM that is trying to help but generates a destructive command (
rm -rfon the wrong directory,git push --forceto the wrong branch). The substring denylist catches the most common patterns. -
Filesystem escape — In
workdirmode, accidental writes outside the project root are blocked by the working-directory jail. Incontainermode, the host filesystem is read-only. -
Network exfiltration — In
containermode, the network is disabled (--network none), preventing the LLM or any tool from phoning home.
-
Jailbroken / adversarial models — If the model is actively trying to cause harm, the substring denylist is trivial to bypass. Use
containermode for meaningful isolation. -
Shell injection within the tool — The
shelltool intentionally passes commands tosh -c. There is no sanitization of the command string beyond the denylist. This is by design: the tool is meant to let the LLM run arbitrary commands. -
Resource exhaustion — The container does not have CPU/memory limits. An LLM could still run
:(){ :|:& };:or allocate large amounts of memory. -
Supply chain attacks — If the LLM runs
pip installornpm install, that code executes inside the container (or on the host inoff/workdirmode). There is no vetting of downloaded packages. -
File editing tools — The
edit_file,write_file, andbatch_edittools always write to the host filesystem and are not affected by sandbox mode. They respectguard_path(which blocks writing outside the project root and to hidden/system directories), but this is also a guardrail, not a security boundary. Incontainermode the shell is isolated but file edits are not — the LLM could usewrite_fileto overwrite project files even in container mode. -
User-initiated actions — If the user explicitly approves a dangerous command via the permission dialog, zap will execute it regardless of sandbox settings.
Hooks (.zap/hooks.json) and MCP servers (.mcp.json) execute code on your
machine. zap loads them from two places:
- Global (
~/.zap/hooks.json,~/.zap/mcp.json) — your own machine config; always loaded. - Project-local (
.zap/hooks.json,.mcp.jsonin the working directory) — ships inside the repo. These run only when the directory is trusted, so cloning and opening an untrusted repository does not run itsSessionStarthook or spawn its MCP servers.
A directory is trusted when any of these hold:
- env
ZAP_TRUST_PROJECTis1/true/yes - a
.zap/trustedmarker file exists in the project - the project's canonical path is listed in
~/.zap/trusted_dirs
When project-local config is skipped, zap prints a one-line notice telling you how to opt in. Only trust repositories you have reviewed.
read_file, write_file, edit_file, and batch_edit run every path through
a guard that (1) resolves symlinks to their real on-disk target before checking
(so a link inside the project cannot reach a blocked location) and (2) rejects a
denylist of credential stores (~/.ssh, ~/.aws, ~/.config/gcloud,
~/.config/gh, ~/.npmrc, SSH key files by name, shell history, ~/.agent.toml,
/etc/{passwd,shadow,sudoers}, and more).
Writes are additionally jailed. write_file, edit_file, and batch_edit
require the resolved target to live under the project root, the system temp
dir, or a configured allowed_paths root — a prompt-injected or confused
overwrite cannot escape the workspace. Add extra write roots in ~/.agent.toml:
allowed_paths = ["~/scratch", "/data/out"]Reads stay intentionally broad (zap legitimately reads arbitrary project files,
temp files, and /dev/null) but remain symlink-safe and denylisted. Use the
shell tool if write access to a path outside the jail is genuinely intentional.
Before content is sent to a cloud model, zap scans for credentials at every entry
point: tool results are redacted, injected project context (ZAP.md,
context_paths) is redacted, and the user's own message is scanned and a
visible warning is shown (user-typed text is warned, not auto-redacted — it may be
intentional). Detection combines ~40 known credential patterns with an
entropy detector for random tokens that match no known prefix, tuned to avoid
false positives on git SHAs and hashes. Local / LAN endpoints are exempt — that
content never leaves the network.
/remote tunnels the session to a public URL, gated by a per-session access
token. The token is drawn from the OS CSPRNG and appended to the printed URL
(?token=…); both the page and the WebSocket upgrade reject any request without
it, so a leaked URL minus the token is inert. /remote also refuses to start
in auto permission mode, where a leaked URL could otherwise drive the shell
unattended — switch to ask mode first. Keep the printed URL secret; the token
is the only access control.
If you discover a security issue in zap's sandboxing or tool guards, please
open a GitHub issue with the security label.
