FAQ #
The config
Why TypeScript for the config? Because the config needs loops, constants, functions, and types, and inventing a DSL for those is how YAML grows a template language. TypeScript has them, editors understand it, and the shipped recipes are ordinary functions. The config only describes: it runs in a sandbox and emits JSON. Zap evaluates it once, before it reads the machine; what executes is the plan built from that JSON, and none of your TypeScript runs on the target.
What runs the config? Can I use Node, Deno, or Bun? fgro eval, Gro's
runtime. Zap is a compiled Rust binary and can't run TypeScript itself, so fgro evaluates the config in a kernel sandbox that reads only the config's
project and the packages it links (the sandbox).
Each project pins one fgro build by hash in zap.lock. Node, Deno, and Bun
have no such sandbox, so Zap doesn't support them. Nothing runs on the target
but shell.
Where are secrets stored? Nowhere Zap owns. They live in your environment
or an env file you keep out of version control, referenced with zap.env('NAME'), and
resolved when Zap acts. Resolved values are masked in every output, and the lock
file and last-run report hold a fixed token for each recognized secret
(env.md; list others with zap.mask()).
What is zap.lock and should I commit it? It pins the fgro build that
evaluates the config, and fetched content (a remote file, a build source) to
the hash of the bytes you approved. Commit
it with zap.ts. It changes only when you run zap update, zap lock, or an apply --wetrun that has a tracked source to pin. lock.md.
Running it
Why is the dry run the default? So the safe thing is the zero-effort thing,
and so a script, a CI job, or an assistant can't act by accident. Tools that
default to acting rely on an interactive prompt, which is exactly what gets
auto-approved. Here --wetrun is the act, and a wetrun with nothing reviewed
asks for the first characters of the plan's hash, which nothing can type before
the plan is on the screen.
What does --wetrun refusing mean? A wetrun executes only the plan you
reviewed. If its fresh plan doesn't match the most recent preview on that host,
or the hash given to --expect-plan, it shows the plan, changes nothing, and
exits 2; running it again executes if nothing moved. On an interactive terminal
without --expect-plan, it asks instead: type the start of the plan's hash to
run it. concepts.md.
Why did zap plan exit 0 when there are changes, but zap apply exit 2? plan reports; it doesn't judge. apply judges: 0 converged, 2 changes pending,
1 something couldn't be established. Use apply in a script that needs the
verdict. cli.md.
What is undetermined? Zap couldn't read the resource and refuses to guess.
It is held out of the plan, reported with its reason, and the run exits 1. The
usual causes: a remote path using ~ with no home declared, a tracked remote file with no lock entry, a probe that failed or timed out, sudo that
wouldn't authenticate. Fix the cause and plan again; nothing carries over. concepts.md.
Why does a script run on every apply? Its check fails on a converged
host. The check is the whole idempotency contract: make it exit 0 exactly
when the work is done. After a wetrun Zap runs the check again and reports RESIDUAL if it still fails.
Why does apply write to /var/lib/zap/pending/ on my host? Each marker
there is a restart or reload a service still owes, written before the change that
triggers it and cleared after. It is how a reload a failed run owed survives to
the next plan (concepts.md). The rest of what Zap leaves
on a target, beside what you declared, is listed in install.md.
Scope
Does Zap need anything installed on the target? sshd, a POSIX shell with
coreutils or BusyBox, and sudo. No agent, no Python, no runtime. targets.md has the per-distro list.
I removed a resource from the config. Why is it still on the machine? Zap
never removes what you stop declaring; it reports it as orphaned in zap status.
To remove something, declare the removal (a script whose check confirms it
is gone, or sudo: false on a user). This is the same posture as Ansible and
Puppet, and the opposite of Terraform and NixOS.
Can one config manage several hosts? One target per config. For a few hosts, one config per host (sharing modules), one env file per host, a shell loop. For a fleet with parallel execution, Zap is the wrong tool; see comparison.md.
Does Zap work on macOS, Arch, Fedora, Windows? No. Debian/Ubuntu and
Alpine. pacman, brew, and launchd parse in the schema but have no backend,
so their resources read undetermined. Running Zap from macOS against a Linux
host is unsupported: fgro's sandbox needs Linux, so your side does too.
Is Zap stable? No: pre-alpha, one maintainer, and the schema and CLI change without compatibility layers. stability.md names the surfaces whose breaks a release calls out. Read security.md before pointing Zap at a machine you care about.
Agents and alternatives
Can an AI agent run Zap? Yes, and it is designed for that: the default
command is read-only, the plan is a hash-named fact the agent can show you, the
wetrun executes only that plan, and --json streams typed events. Give the
agent zap plan and zap apply freely and keep --wetrun for the human, or
let it wetrun with --expect-plan set to the hash you approved.
How is this different from a shell script with if guards? That script is
reaching toward what Zap declares: detection (skip what's done), idempotent
re-runs, a preview before acting, and a report after. Zap adds the parts a
script never grows: a refusal to guess when a read fails, a re-read after acting,
secrets masked everywhere, and a review bound to the act. script resources let
you keep the shell where it is the right tool.
How is this different from Ansible / Terraform / NixOS / Kamal? comparison.md, with tables.