Skip to content
stacks
Docs menu

Output, JSON and exit codes

Parse stacks output reliably, with the three output modes, the JSON error envelope, every exit code and how signals stop a command.

View as MarkdownOpen in ClaudeOpen in ChatGPT

Every stacks command follows the same output rules: results on stdout, diagnostics on stderr, one JSON error envelope with --json, and an exit code that maps to one next action.

Output modes

stacks picks one of three output modes per invocation:

Mode When What you get
JSON --json is set One result object on stdout, or one error envelope as the last stderr line
Human stdout is a terminal, no coding agent is detected, and CI is unset Full text, color, a live progress line, logs on stderr
Agent Everything else: an agent's shell, a pipe, CI The same result as human output, with less text (listed below)

stacks detects an agent from session markers in the environment: CLAUDECODE, CLAUDE_CODE, CODEX_SANDBOX, CODEX_CI, CODEX_THREAD_ID, OPENCODE, OPENCODE_CLIENT, CURSOR_AGENT, CURSOR_EXTENSION_HOST_ROLE=agent-exec and AI_AGENT.

Human and agent output carry the same result with the same exit code and files. Agent output differs from human output in these ways:

  • No log lines on stderr. They go only to STACKS_LOG_FILE when it's set. Stderr holds the failure message, its detail lines and Next:, plus verify's > <step> line as each step starts.
  • Paths in results print relative to the working directory when inside it. Paths in error details stay absolute, and --json keeps every path absolute.
  • new prints no written-file listing (the brief lists the files) and no absolute Project: line.
  • verify prints no rows for passed steps (the summary counts them), and none on a changed run. Failed and skipped rows, output tails and Next: stay.
  • validate prints only the size rows over their limit.
  • list prints each description on one line.

Color and live progress

Only human output is styled. At a terminal, status words use color, new and verify redraw one live progress line on stderr, and long text wraps to the terminal width.

Variable Effect
NO_COLOR Any nonempty value turns off color and the live line. Wins over FORCE_COLOR
FORCE_COLOR=1 Colors stderr when it isn't a terminal, as long as stdout is one

Piping stdout switches to agent output, which is never colored. Agent and JSON output never carry ANSI escapes, even with FORCE_COLOR.

stdout and stderr

Stdout carries results only. On failure stdout stays empty and stderr holds the diagnostic:

stacks show go-clii
Terminal
Not found.
  Id: go-clii
  Did you mean: go-cli?
  Next: run stacks list to see every stack and fragment id.
stderr

Two cases still write to stdout:

  • A failure that carries a report writes the report to stdout before the error: VerifyFailed, VerifyChanged and ValidationFailed. Its Next: line goes to stdout too.
  • A command that hands the terminal to another program (new launching an agent) writes its output so far first, and that stays on stdout if the command later fails.

--version --json prints { "version": "<version>" }, and JSON help is { "help": "..." }. A --json after -- belongs to the command you're passing arguments to, not to stacks.

The error envelope

With --json, a failure writes exactly one envelope as the last line of stderr:

stacks show go-clii --json
Terminal
{"error":{"tag":"NotFound","message":"Not found.","retryable":false,"next":"run stacks list to see every stack and fragment id.","details":{"suggestions":["go-cli"],"id":"go-clii"}}}
stderr
Field Meaning
tag The error type. Each tag maps to one exit code
message Fixed per tag, so it's safe to match on
retryable Whether running the same command again can succeed
next What to do about the failure. Every tag except BrokenPipe has one
details Optional. A closed per-tag object holding only values the CLI produced or arguments you passed

Text modes print the same next text after Next:.

An unknown flag is a Usage error whose details name the flag as typed, the command path without stacks, and up to three suggestions. A suggestion can point at a positional argument instead of a flag, as in this envelope for new --dir my-app --json (new takes the directory as a positional argument):

{"error":{"tag":"Usage","message":"Invalid command.","retryable":false,"next":"run stacks new --help to list the flags and arguments.","details":{"flag":"--dir","command":"new","suggestions":["<dir> (positional argument)"]}}}
stderr

Verify events

verify --json also streams live events to stderr, one JSON object per line, while it runs:

Event Fields
step-started event, name, phase
step-finished event, name, phase, status (passed, failed or skipped), durationMs, and failureKind (step, env, timeout or interrupted) when the step failed

A failure's envelope still comes last, so read the last stderr line for the error and skip lines that have an event key.

JSON schemas

stacks schema lists every JSON shape the CLI prints. stacks schema <name> prints one as a JSON Schema (draft 2020-12, with closed objects): each command's --json result, error for the envelope, and verify-event for verify's events.

stacks schema
Terminal
add             add --json
create          create --json
doctor          doctor --json
error           the last stderr line of any failed --json command
...
verify          verify --json
verify-event    each live stderr line verify --json writes before the result
Output

Logs

Human output writes logs to stderr before any error. JSON and agent output send logs only to the optional log file, so stderr stays short and parseable. To see why a Defect happened, set STACKS_LOG_FILE to a JSONL path or rerun at a terminal. LOG_LEVEL and STACKS_LOG_FORMAT are in Configuration.

Telemetry notice

The first time telemetry is enabled on a machine, stacks prints a one-time notice: on stderr for human and agent output, and as an optional notice string in the JSON result. That invocation sends nothing. If you're an agent and stacks prints a notice, tell the user once, in one sentence, including how to turn it off. Telemetry failures never change a command's output or exit code. See Telemetry.

Exit codes

Each exit code means one next action, and a published code never changes meaning.

Exit Tags Next action
0 Success. For verify, check the outcome: partial exits 0 but isn't done
1 Defect A bug in stacks. Not retryable
2 Usage, BadInput, NotFound, EntryInvalid, ComposeConflict, TargetNotEmpty, ValidationFailed Fix the input. ValidationFailed keeps its report on stdout
3 NotConfigured, InvalidConfig Fix the configuration or manifest, or upgrade @steamfield/stacks for a newer schema
4 UpstreamUnavailable Retry later
5 VerifyFailed, VerifyChanged Read the verify report on stdout
6 PermissionDenied, UpdateFailed Ask a person
130 Interrupted The run was stopped and cleanup ran

Other exit code rules:

  • After new launches an agent, the exit code is that agent's own status (128 plus the signal number if a signal killed it), outside this table.
  • A closed stdout pipe exits quietly with 0, except after a failure that keeps its report. That failure keeps its exit (2 for validate, 5 for verify), so a failed verify piped into head never reads as a pass.
  • A Defect never becomes success.

Tip

Branch on the exit code, then read tag and next from the envelope. Don't parse message for details; use details.

Signals

Signal Effect
Ctrl+C (SIGINT), SIGTERM, SIGHUP The command stops after its cleanup, kills the processes it started, and exits 130
A second Ctrl+C within one second Treated as the same keypress, since wrappers such as npx forward a copy
A second Ctrl+C a second or more later Exits at once, killing started processes without further cleanup

While new has handed the terminal to an agent, Ctrl+C and SIGQUIT belong to that agent, and SIGTERM and SIGHUP are passed on to it. Once the agent exits, stacks stops too, with exit 130.

Processes started by verify and update run in their own process group. On interruption, timeout or the end of a step, the group gets SIGTERM, then SIGKILL after 5 seconds.