Command lineJSON output
JSON output
Every command's --json output is one envelope on stdout, with the same fields for every command. Read it from scripts, and follow long jobs with progress events.
Get JSON from any command
Add --json to any command. stdout then holds exactly one JSON document, even when the command fails. Human messages go to stderr, and so do progress events when you ask for them.
census findings --safety safe --json
{
"schema_version": 5,
"command": "findings",
"status": "ok",
"exit_code": 0,
"errors": [],
"warnings": [],
"results": {
"findings": [],
"totals": {
"safe": { "allocated_bytes": 0, "count": 0 },
"review": { "allocated_bytes": 0, "count": 0 },
"keep": { "allocated_bytes": 0, "count": 0 }
},
"truncated": 0
}
}
Options can go before or after the command, so census --json top is the same as census top --json. Help text is always plain text, never JSON.
The envelope
Every JSON document has these top-level fields:
| Field | Meaning |
|---|---|
schema_version | The contract version. The current version is 5. |
command | The command as invoked, such as top or clean. The dupes alias reports duplicates. It is null when the arguments failed to parse before a command was known. |
status | ok, complete, partial, failed, not_found or cancelled. |
exit_code | The process’s exit code. |
errors | Failures, as {code, message} entries. Empty when exit_code is 0, never empty otherwise. |
warnings | Warning strings. |
Queries add scan, the saved scan they read, and results, which differs by command. Each command’s page shows its results.
Statuses
| Status | Exit code | Meaning |
|---|---|---|
ok | 0 | A query succeeded, or a cleanup deleted everything it could. Skipped items are normal. |
complete | 0 | A scan read everything in its scope. |
failed | 1 or 2 | A bad request (1) or an operational failure (2). |
partial | 4 | Some folders couldn’t be read, some items couldn’t be fully deleted, or a maintenance task failed. |
not_found | 7 | There is no saved scan, or the path or app isn’t in it. |
cancelled | 130 | Ctrl-C or SIGTERM stopped a long operation. |
Exit codes and errors lists every error code.
Paths
Paths and names in query results are PathValue objects, because a macOS file name doesn’t have to be valid UTF-8:
| Field | Meaning |
|---|---|
display | The path as text. Home folders appear as they are on your Mac, such as /Users/me. |
lossy | true when display had to replace bytes that aren’t valid UTF-8. |
bytes_hex | The exact bytes, in hex. Present only when display loses them. |
{ "display": "/Applications/Xcode.app/Contents", "lossy": false }
review --json is the exception: its path is a plain string, because selection files for apply use exactly those strings.
Read JSON in a script
Check exit_code before reading results. These examples use jq:
# The five largest files, size then path
census top --files --limit 5 --json \
| jq -r '.results.files[] | "\(.allocated_bytes)\t\(.path.display)"'
# Stop with census's own message when a command fails
out=$(census findings --json)
if [ "$(jq -r .exit_code <<<"$out")" -ne 0 ]; then
jq -r '.errors[0].message' <<<"$out" >&2
exit 1
fi
The same in Python:
import json
import subprocess
proc = subprocess.run(
["census", "findings", "--safety", "safe", "--json"],
capture_output=True,
text=True,
)
doc = json.loads(proc.stdout) # stdout is JSON even when the command fails
if doc["exit_code"] != 0:
raise SystemExit(doc["errors"][0]["message"])
safe = doc["results"]["totals"]["safe"]
print(f"{safe['count']} safe items, {safe['allocated_bytes'] / 1e9:.1f} GB")
When stdout closes early, as in census top | head -1, census exits quietly with status 141 (SIGPIPE), like other Unix tools.
Follow progress
Long jobs accept --progress. It writes one JSON record per line (NDJSON) to stderr, flushed as it goes, so stdout still holds only the result:
census scan --json --progress 2>progress.ndjson
--progress works with scan, duplicates, exports that include duplicates, any query with --refresh, clean, review --refresh, apply, uninstall, maintain with tasks, optimize and empty-trash. Each record starts with event and elapsed_ms (milliseconds since the command started), then the fields of its phase:
| Phase | Fields |
|---|---|
loading_cache | None |
replaying_changes | None |
walking | directories_read, files_seen, allocated_bytes |
summarizing | None |
saving | None |
hashing | stage (starts or contents), files_done, files_total, bytes_read, bytes_total |
deleting | items_done, items_total, bytes_freed: once before the first item, then after each |
maintaining | task (null in the last record), tasks_done, tasks_total |
{"event":"progress","elapsed_ms":1234,"phase":"walking","directories_read":1200,"files_seen":32000,"allocated_bytes":987654321}
A full scan’s last walking record matches the report’s metrics.files_seen, then come summarizing and, unless --no-save, saving. An incremental rescan reports loading_cache, replaying_changes, walking, summarizing and saving, and its walking totals count only the folders it read again.
Versioning
schema_version goes up only when a change breaks the contract. New fields can appear at any time without a new version, so ignore fields you don’t know. Version 5:
- calls the scan command
scan(it wasscan.start); - adds
exit_codeto every envelope; - names the scan’s unreadable-path summary
read_errors, leavingerrorsfor command failures; - returns
top --files --jsonresults as{files, filters}.