Relationships
#2259 Route log-mode output to stderr by default, with commands opting stdout back in
Opened by skunk-ape · 9/17/2026
Proposal
Invert the CLI's default log stream: LogTape console records go to stderr, and commands whose stdout is genuinely their output opt back in. Today it is the other way round — records go to stdout by default, and a single stderrOnly flag exists for one caller.
Why
swamp-club#2254 is the fifth time this has been fixed one command at a time:
- #235 — json-mode audit, so nothing leaks to stdout through the logging pipeline
- #1466 — stop printing a plaintext token to stdout
- #1768 —
vault read-secretmade byte-exact on stdout, no trailing newline - #2180 —
swamp invite linkspecified stdout as the bare URL, prose on stderr - #2254 —
-vbreaks that contract;vault read-secret -vwrites a log line into a secret
Each fix holds only for the command it touched. A new command that puts a value on stdout acquires the bug by default, and nothing catches it — which is exactly how #2254 shipped.
Why this was never decided
The current behaviour is accretion, not a decision. There is nothing in design/ about CLI log stream routing. The terminal-output skill documents sinks, formatters, timestamps and colours in detail and never says which stream any of it goes to. The stdout/stderr split comes from LogTape's getConsoleSink(), which dispatches by level — debug/info to stdout, warn/error to stderr — so nobody chose it. And stderrOnly itself arrived in #1765 as a mechanical necessity for dispatch runners, whose stdout carries RPC frames; its doc comment says so.
Blast radius (measured 2026-09-17)
This is a product decision about what log mode means, not a config flip:
- 53 of 134 renderer files emit their primary human-facing output via
logger.info.swamp versionis entirelylogger.info— its whole output would move. - ~153 of ~301 swamp-uat test files assert on
.stdout; that suite keeps the streams separate (runCommandreturns{exitCode, stdout, stderr}).
Narrower than it first looks: writeOutput() is a direct console.log and JSON renderers use console.log, so neither moves. Only LogTape-routed records move — those 53 renderers.
Starting point
The fix for #2254 adds an architecture fitness test pinning which commands claim the value-on-stdout contract (invite link, first-rule, vault read-secret). That pinned list is the inventory this work starts from: the inversion is complete when the list is empty because the default has moved.
Scope of the work
- Decide the contract and write it into
design/— the missing decision record. - Audit the 53 logger-emitting renderers: which are output (opt back into stdout) and which are commentary (stay on stderr).
- Flip the default in
initializeLogging; retirestderrOnlyin favour of the inverse opt-in. - Coordinate the swamp-uat sweep before merge — assertions move stream, not content.
- Update
.claude/skills/terminal-output/references/logtape.md.
Filed out of the triage of #2254, which deliberately ships the narrow allowlist fix instead.
Open
No activity in this phase yet.
Sign in to post a ripple.