The output contract
shipped 1.0.0Human output goes to stderr. Machine output goes to stdout. They never mix.
So cwp pull --json > result.json gives you clean JSON and still shows
progress in the terminal.
The envelope
--json prints exactly one object on stdout:
{
"ok": true,
"command": "content push",
"env": "dev",
"steps": [],
"warnings": [],
"error": null,
"data": null
}
steps is the narrative; data is the answer. steps is what the command
did, in order: the same thing the human output shows. data is the structured
result for commands that have one, and neither derives from the other.
data is always present and null when a command has no structured result,
so “this command has none” stays distinguishable from “this cwp is too old to
have data”. It is an object or null, never a bare array, so a payload can grow
a second key without breaking anything reading the first.
command is the op id: the same string that keys the command reference.
guards, in data, on every command that can write to a remote
Dry run included:
{ "guards": { "protectedEnvironment": "not-applicable",
"restorePoint": "taken",
"source": "present",
"sourceExemption": null } }
So a guard that silently did not run is a missing line in machine-readable output, rather than an absence nobody can see.
sourceExemption says why, when source is exempt. An upward write
normally names the committed file it came from. Two in the tool cannot, and
both say so rather than getting a waiver. cwp mcp mints
an application password, a credential that cannot enter the tree and shows
once. cwp bricks post-types changes
a builder setting in place.
{ "guards": { "source": "exempt",
"sourceExemption": "an application password is a credential — it cannot be committed…" } }
It is null in every other case. An exempt with nothing beside it would be a
mitigation you cannot read, and a mitigation you cannot read is not one.
Exit codes
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | generic error |
| 2 | configuration error |
| 3 | missing dependency |
| 4 | remote error |
| 5 | refused by a safety guard |
| 6 | aborted by the user |
A pass-through whose program was killed by a signal reports the shell’s
128 + signum instead. So interrupting cwp tail -f with Ctrl-C exits 130,
the way that command normally ends rather than a failure.
Three commands are the exception
cwp wp, cwp shell and
cwp tail have no --json. Stdout carries the
other program’s own output, and an envelope around it would corrupt the thing
you asked for. For WP-CLI, use its own --format=json.
The three also forward that program’s exit code verbatim, so a code from the table above may there mean whatever the program meant by it. The refusals cwp makes itself still use these codes, and all of them happen before the program starts.
Declining to declare --json keeps the exception checkable: cwp wp --json is
an unknown option rather than a flag that cwp accepts and quietly ignores.
Where you type the shared flags does not matter
cwp projects path x --json and cwp projects --json path x mean the same
thing. This is not free: commander assigns a flag declared on both a parent and
its subcommand to the parent, so cwp walks the ancestor chain and ORs the
booleans up it.
Every user-facing error carries a fix hint. “Cloudron CLI not found” is not
acceptable output; “Cloudron CLI not found — install it with npm i -g cloudron”
is.