Skip to content

The output contract

shipped 1.0.0

Human 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

CodeMeaning
0success
1generic error
2configuration error
3missing dependency
4remote error
5refused by a safety guard
6aborted 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.