Skip to main content
← Back to list
01Issue
FeatureOpenSwamp CLIPublic
AssigneesNone

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-secret made byte-exact on stdout, no trailing newline
  • #2180 — swamp invite link specified stdout as the bare URL, prose on stderr
  • #2254 — -v breaks that contract; vault read-secret -v writes 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 version is entirely logger.info — its whole output would move.
  • ~153 of ~301 swamp-uat test files assert on .stdout; that suite keeps the streams separate (runCommand returns {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

  1. Decide the contract and write it into design/ — the missing decision record.
  2. Audit the 53 logger-emitting renderers: which are output (opt back into stdout) and which are commentary (stay on stderr).
  3. Flip the default in initializeLogging; retire stderrOnly in favour of the inverse opt-in.
  4. Coordinate the swamp-uat sweep before merge — assertions move stream, not content.
  5. Update .claude/skills/terminal-output/references/logtape.md.

Filed out of the triage of #2254, which deliberately ships the narrow allowlist fix instead.

02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED

Open

9/17/2026, 9:27:38 PM

No activity in this phase yet.

03Sludge Pulse

Sign in to post a ripple.