Startup banner goes to stdout and --json does not suppress it, so every --json document is preceded by three lines of prose #106
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while fixing
issue #82. Filing rather
than fixing drive-by: #82 is about the logger's sink, and this is a
different writer on a different code path.
Problem
internal/cli/entry.go:20-27prints the startup banner to stdout beforecobra parses anything:
bannerSuppressedInArgsrecognizes only--quiet,-qand--cron.It does not recognize
--json, so every--jsonsubcommand emits twobanner lines and a blank line onto stdout ahead of its document.
--jsonpaths that passopts.JSONasextraQuiettorunVaultikApp(e.g.
snapshot remove) do not help: that callsUI.SetQuiet(true)inan fx
OnStarthook, which runs long afterEntryhas already printedthe banner.
Reproduction
With a config at any permissions and the binary from
main:The workaround is
-q, which suppresses the banner as a side effect —but a machine consumer should not have to know that, and
--jsonalready communicates the intent.
Affected
snapshot list --json,snapshot verify --json,snapshot remove --json,prune --json,remote info --json.Expected
--jsonimplies the banner is suppressed. stdout carries the documentand nothing else, which is the same contract
issue #82 established for
the logger.
Notes
contamination, so the banner is the remaining one.
bannerSuppressedInArgsalready scans rawos.Argsfor flags becausethe banner precedes cobra parsing; adding
--jsonthere is consistentwith how
--quiet/--cronare handled, though it does mean asubcommand-level flag is matched anywhere in the argument vector.
Whether that is acceptable or whether the banner should instead move
after parsing is the design question this issue needs answered.
--jsoninvocation parses as exactly one JSON document. The existing
TestListSnapshots_JSONStdoutIsOnlyTheDocumentcovers the librarycall and cannot see
Entry, so this one belongs ininternal/cli.Scope addition — one more item lands with this, from the
PR #107 review.
AGENTS.mdpolicy 9 (lines 86-87) is now false. It states the logformat is keyed on stdout's TTY-ness. As of #82 the logger writes to
stderr and the format choice tests
os.Stderr. The rule's operativecontent is preserved — structured output when the log stream is not a
terminal — but the stream it names is wrong.
Fix it in the same change as the banner, since both are about which
stream carries what. One-line amendment: key the wording on the log
stream rather than stdout.
Worth doing rather than deferring: this repo has now corrected false
claims in
snapshot.go's manifest comment (#81), theVaultik.UI--croncomment (#84),TTYHandler'sWithAttrsdoc (#97), and the--cronflag help (#87). A rules file that misdescribes the code is thesame defect in the place agents are most likely to trust it.
Revised definition of done
--jsonsuppresses the startup banner; stdout carries the documentand nothing else.
AGENTS.mdpolicy 9 names the log stream, not stdout.internal/cliasserting a--jsoninvocation'sstdout parses as exactly one JSON document — the existing
TestListSnapshots_JSONStdoutIsOnlyTheDocumentcovers the librarycall and cannot see
Entry.either
bannerSuppressedInArgsscans rawos.Argsfor--jsonas italready does for
--quiet/--cron(accepting that a subcommand-levelflag is matched anywhere in the vector), or the banner moves after
parsing. Say which and why.
script/cibuildexits 0.Two smaller observations from the same review, fix if cheap:
Vaultik.Stderrnow has zero production writers andenv.stderrzeroreaders.
vaultik.godocuments keeping it deliberately — confirm thatstill reads true, or remove it.
bytesAttrKey's human-readable byte formatting does not survivegrouping (
x.bytesno longer matches the key check). Unreachabletoday; worth a comment if not a fix.
Implementation plan, on branch
fix-json-banneroffmain.1. The design question — extend
bannerSuppressedInArgs, do not move the banner.Moving the banner after parsing costs more than it buys.
root.goalready documents why it is printed first: it must appear even when
cobra rejects the arguments (
requires at least 2 arg(s)) and on--help. After parsing there is no single place that runs on all ofthose paths, so "after parsing" means reimplementing the banner in
several handlers, or accepting that it disappears from the failure
paths where a human most wants to know which build they just ran.
The stated objection to the scan is that
--jsonis a subcommand flagmatched anywhere in the vector. That objection applies equally to
--cron, which is already in the list and is also a subcommand flag —it exists only on
snapshot create(
internal/cli/snapshot.go:137). So this adds no new class ofimprecision, only another instance of one the code already accepts.
And the two failure directions are not symmetric: a false positive
loses a decorative banner, a false negative corrupts a document on
stdout. The scan should err toward suppression, and does.
2.
--jsonand--json=join--quiet/-q/--cronin the scan.--json=falsewill suppress the banner, exactly as--quiet=falsealready does. Making the value parse honest for
--jsonalone wouldmake it inconsistent with its two neighbours, and fixing all three is a
behaviour change nobody asked for; the doc comment states the bias
instead.
3. Tests, in
internal/cli. Two layers:TestEntryJSONStdoutIsExactlyOneDocumentrunsEntryitself with areal argument vector over the process's real stdout descriptor, all
the way through cobra and fx to the document, and asserts the capture
decodes as exactly one JSON value with nothing after it. It is
hermetic because
storage_url: file://is exempt from the S3credential checks and
snapshot listtreats a destination store withno
metadata/as an empty list rather than a failure — so theend-to-end path needs no network and no credentials.
--jsoncommands(plus
--jsonbefore the subcommand,--json=true, and aflag-with-value ahead of it), and a paired test asserting the banner
is still printed without a suppressing flag, so the first test cannot
be satisfied by deleting the banner.
4.
AGENTS.mdpolicy 9 re-worded to key on the log stream ratherthan stdout, preserving the operative rule.
5. The two smaller observations.
Vaultik.Stderr: kept, comment amended to say outright that nothingwrites to it today, so the next reader does not go looking for the
writer.
listEnv.stderrinsnapshot_list_test.gois genuinely dead— assigned, never read, because those tests capture the process's
stderr instead — and is removed.
bytesAttrKey: fixed rather than commented, since the fix is threelines. The key reaching
writeAttris group-qualified, so the matchis made against the final dot-separated segment. Covered by a test
over both the grouped and ungrouped renderings.
Verification:
script/cibuild, plusvaultik snapshot list --json | jq .against a real config with no other flags, and the same for
snapshot verify,snapshot remove,pruneandremote info. The new test willbe confirmed to fail against the unfixed
bannerSuppressedInArgsbyreverting it, not by assumption.
Implemented in
PR #109 (branch
fix-json-banner, one commit).Built.
--jsonand--json=join--quiet/-q/--croninbannerSuppressedInArgs. The design question is answered in favour ofextending the raw-argv scan: the banner is printed before parsing
deliberately, so that it survives cobra rejecting the arguments and
--help, and after parsing no single place covers those paths. Thesubcommand-vs-persistent distinction does not decide it, because
--cronis already in the list and is also a subcommand flag — itexists only on
snapshot create. Full reasoning in the PR body.AGENTS.mdpolicy 9 now names the log stream.bytesAttrKeymatches onthe final dot-separated segment, so byte formatting survives grouping.
Vaultik.Stderrkept with the comment amended to say outright thatnothing writes to it;
listEnv.stderrremoved as dead.Verified. The three new
internal/clitests were confirmed to failagainst the unfixed scan by reverting it, not by assumption — including
TestEntryJSONStdoutIsExactlyOneDocument, which runsEntryitself overthe process's real stdout descriptor through cobra and fx to the
document. End to end against a real config and a real snapshot, with no
other flags and no
-q:snapshot list --json | jq .,snapshot verify --json,snapshot remove --jsonandremote info --jsonall exit 0.script/cibuildexit 0, 15okpackages, 0
(cached), 134s.One item of the definition of done is not met, deliberately.
prune --json | jqstill fails, for a reason that is not the banner:pruneLocalSnapshotswrites three lines of prose to stdout with no--jsonawareness, it reproduces identically onmainbefore thischange, and
-qnever suppressed it either. Filed asissue #108 rather than
fixed here — a different writer on a different code path, the same
reason this issue was filed out of
issue #82 instead of
being folded into it. It also needs a decision this issue does not
contain: whether those lines belong in the document, on the logger, or
suppressed outright.