# 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.

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:

```sh title="Terminal" group="text-error"
stacks show go-clii
```

```text title="stderr" group="text-error"
Not found.
  Id: go-clii
  Did you mean: go-cli?
  Next: run stacks list to see every stack and fragment id.
```

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:

```sh title="Terminal" group="json-error"
stacks show go-clii --json
```

```json title="stderr" group="json-error"
{"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"}}}
```

| 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):

```json title="stderr"
{"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)"]}}}
```

### 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.

```sh title="Terminal" group="schema"
stacks schema
```

```text title="Output" group="schema"
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
```

## 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](https://stacks.steamfield.io/docs/reference/configuration.md#environment-variables).

## 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](https://stacks.steamfield.io/docs/reference/telemetry.md).

## 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.
