Expand description
Process / shell integration.
(exec-check CMD ARG…) → 0 on success, non-zero exit code otherwise
Streams stdin/stdout/stderr to the parent.
(exec-capture CMD ARG…) → the CAPTURE RECORD (below)
Captures stdout + stderr, exposes exit code.
(exec-ok? CMD ARG…) → bool; true iff exit code is 0
(sh-exec STR) → convenience: run STR through sh -c
returning the capture-form result
Credential-carrying variants. Use these instead of putting a secret in an argument: argv is world-readable from the process table, and in CI it is readable by co-tenant steps and sibling containers.
(exec-with-stdin IN CMD ARG…) → capture form; IN is written to the
child’s stdin. The --password-stdin
shape that docker / helm / skopeo / gh
already support. Preferred.
(exec-with-env ENV CMD ARG…) → capture form; ENV is an alist of
(KEY VALUE) pairs set for the child only.
For tools with no stdin form. Weaker than
stdin — the environment is readable by the
same uid — but far stronger than argv.
No implicit shell interpolation. Arguments are passed literally to
the underlying process; no glob / word-splitting / $VAR substitution.
Scripts that want shell features use sh-exec explicitly.
── THE CAPTURE RECORD ─────────────────────────────────────────────────── Every capture-form primitive above returns the SAME eight-field alist, so a caller never has to know which form produced it:
(:status N) exit code, or -1 when killed by a signal (:stdout “…”) captured stdout (:stderr “…”) captured stderr (:argv (“cmd” “a”)) the argv LIST, exact and unsplit (:program “cmd”) the program as ASKED FOR (:resolved “/nix/…”) the program as RESOLVED — “” when PATH lookup failed (:cwd “/…”) the directory the child inherited (:duration-ms N) wall-clock milliseconds
The last five landed 2026-08-17 and are not decoration. A deshellify port’s
correctness argument is always “the new thing does what the old thing did”,
which is a COMPARISON — and you cannot compare an invocation you did not
record. Concretely: :resolved answers “did the RIGHT tool run” (the
silent-PATH-fallback class — Command::new("kubectl") runs whatever PATH
found first, and 218 such bare spawns were measured in pleme-io/forge);
:cwd distinguishes a write to a flake’s read-only /nix/store source copy
from one to the work tree, which otherwise surfaces only as a bare
Permission denied (os error 13); :duration-ms separates “failed” from
“hung”, which a status alone cannot.
:argv is a LIST and never a joined string, because re-quoting a command
changes it — a single string cannot be compared against what was run.
ADDITIVE by construction: status-of / stdout-of / stderr-of and every
other alist-get consumer are unaffected, and nothing in the tree asserted
the record’s length.
Canonical technique: pleme-io/docs/controlled-subprocess.md (rung 2).