Troubleshooting
This page covers the most likely real failure modes when building or evaluating a DreamCLI app.
Use it alongside CLI Semantics and the Support Matrix: those pages describe product truth and exact rules; this page translates the common failure cases into quick diagnosis steps.
Prompts Never Appear
Symptom:
- a flag has
.prompt()configured, but the CLI errors instead of asking; - the same command prompts locally but not in CI or when piped.
Cause:
- DreamCLI only auto-prompts when a prompter exists and
stdinIsTTYistrue.
Check:
- are you running in CI, a pipe, or redirected stdin context;
- did an earlier source already resolve the value from CLI, env, or config.
Fix:
- provide the value through CLI, env, config, stdin-backed inputs, or a default;
- in tests, inject answers through
runCommand()instead of relying on terminal behavior.
References: Interactive Prompts, CLI Semantics
Config Values Are Ignored
Symptom:
- a config file exists, but the command still uses env, prompt, or default values;
- a config value works for one flag but not another.
Cause:
- config only participates for inputs wired with
.config(path); - config is lower priority than CLI, stdin, and env on both surfaces.
Check:
- the CLI is configured with
cli().config('<app-name>'); - the specific flag or argument uses the expected
.config('a.b.c')path; - a higher-priority source did not already win.
Fix:
- add or correct the input's
.config()path; - remove the higher-priority value while testing precedence.
References: Config Files, CLI Semantics
Config Parsing Fails For YAML Or TOML
Symptom:
- DreamCLI reports a config parse or load error for a non-JSON file.
Cause:
- built-in config discovery is JSON-only.
Fix:
- stay on JSON for the default path;
- or register a custom loader with
configFormat()andconfigLoader().
References: Config Files, Limitations And Workarounds
Piped Stdin Does Not Reach An Input
Symptom:
- you pipe data into the command, but the flag or argument stays empty or falls through to env/default.
Cause:
- a flag or argument reads stdin only when it declared
.stdin(); - a
{ when: 'dash' }binding reads the stream only for an explicit-; - a
{ when: 'missing' }binding treats a typed-as the literal string.
Check:
- the declaration includes
.stdin(); - the binding's
whenmatches how the value is being passed; - the CLI token or
--flag valuedid not already satisfy the input first.
Fix:
- opt the input into
.stdin()if piped data is part of the intended contract; - widen
whento the default'dash-or-missing'to accept both forms; - otherwise pass the value explicitly on argv.
References: CLI Semantics, Arguments, Flags
A Piped Value Carries A Trailing Newline
Symptom:
- a piped string compares unequal to the value you expected.
Cause:
- codecs that preserve text terminators keep the stdin buffer byte for byte by default. This includes strings and paths, so
flag.path()andarg.path()keep the terminator unless{ trim: true }is set. - other codecs interpret the text and drop one trailing
\n,\r\n, or\rbefore decoding.echo 42reachesflag.number()as42, andecho 30sreachesflag.duration()as30s.
Check:
- whether the binding passed
{ trim: true }; - the input's codec. String and path codecs carry the terminator this far, so
trimchanges nothing on decoding codecs. - whether the producer appends a newline.
echodoes;printfwithout\ndoes not.
Fix:
- declare
.stdin({ trim: true }), which drops one trailing\n,\r\n, or\rfrom a single value before any check runs; - or emit the value without a terminator, for example
printf '%s' './docs' | mycli; - or declare the input as a collection, where line splitting treats a final terminator as framing and drops it.
arg.path({ mustExist: true }).stdin({ trim: true });$ echo ./docs | mycli check
# the mustExist check runs against './docs'trim applies to a single value. A collection's terminators separate its elements, so .split({ stdin }) decides those and trim has nothing to do.
References: CLI Semantics, Flags, Arguments
A Piped Collection Loses Or Duplicates Elements
Symptom:
- a
-occurrence on an array, key-value, or variadic input produces nothing, or produces more elements than the pipe carried; - the pipe's elements land in the wrong position in the resolved list.
Cause:
- a
-occurrence stands for the whole stdin source at the position it holds, and the decoded elements are spliced in there. Two-occurrences therefore splice the same buffer twice. - when every occurrence is
-and nothing was piped, the input produces no CLI value at all, so a later source or the default supplies the result. - an input that never declared
.stdin()treats-as an ordinary element and never reads the stream. - the buffer decodes under the stdin policy,
'lines'by default, not under the CLI separator.
Check:
- the declaration includes
.stdin(), and itswhenaccepts a dash; - how many
-occurrences the invocation actually passes; - whether
.split({ stdin })matches the shape being piped, for example'json'for a piped JSON document.
Fix:
- pass
-once for one splice; - set
.split({ stdin: 'json' })or a delimiter when the pipe is not line-oriented; - pass the values on argv when the pipe was not meant to be the source.
References: Collections, CLI Semantics
A - Occurrence Fails With Nothing Piped
Symptom:
- a command that mixes typed values with a
-exits 2 before the action runs:
No piped stdin for the '-' occurrence of flag --tag
Suggestion: Pipe a value to stdin, or drop the '-' occurrence of --tagNo piped stdin for the '-' occurrence of argument <files>
Suggestion: Pipe a value to stdin, or drop the '-' from <files>Cause:
- a
-among other occurrences is one element of the collection, and nothing was piped for it to stand for. Resolution fails withMISSING_STDINrather than shortening the collection behind the caller's back. - occurrences of nothing but
-behave differently: they are the whole value, so with nothing piped they fall through to env, config, prompt, and the default, the way an absent input does. - a scalar
-behaves that way too. It is the whole value, so dropping it loses nothing and resolution falls through.
Check:
- whether the producer feeding the pipe actually wrote anything;
- whether the invocation is a shell that opened no pipe at all;
- whether the typed occurrences beside the
-were meant to be there.
Fix:
- pipe a value to stdin;
- or drop the
-and let the remaining occurrences stand alone; - or declare
{ when: 'missing' }, which leaves a typed-as the literal string and reads the stream only when the input is absent.
References: Collections, CLI Semantics
A - Stdin Selector Is Not A Literal Value
Symptom:
- an input whose binding treats
-as a stdin selector cannot hold the one-character string-; the token reads the stream, or fails because nothing was piped.
Cause:
- with
when: 'dash'orwhen: 'dash-or-missing', the token names the source before anything reads it as text, so-is not data. Withwhen: 'missing', a typed-remains a literal value.
Check:
- whether the value the caller wants really is a bare
-, rather than a path or a name that begins with one; - which
whenthe binding declares.
Fix:
- declare
{ when: 'missing' }, which reads the stream only for an absent input and leaves a typed-literal; - or drop
.stdin()from that input and read the stream on a different one; - or pass the value through
.env(),.config(), or a config file.
There is no escape syntax for a stdin-enabled input. -- ends flag parsing, so it does not make a following - literal either.
References: Flags, Arguments, CLI Semantics
An Error Message Says <redacted> Instead Of The Value
Symptom:
- a validation error names the input, the source, and the reason, but prints
'<redacted>'where the offending value used to be; error.details.valueis missing on the same error.
Cause:
- the flag or argument is marked
.sensitive(). Sensitivity applies equally to argv, stdin, env, config, prompt, and default values; - sensitive framework diagnostics omit raw-derived values, collection keys, validator paths, filesystem paths, and adapter causes.
Check:
- whether the declaration calls
.sensitive()or a definition setssensitive: true; - which source produced the value.
details.sourcenames it, anddetails.envVarordetails.configPathnames the binding, but source does not decide redaction; - whether the reason is enough on its own.
expected,constraint,bound, andallowedall survive redaction.
Fix:
- read the value from the source you control rather than from the diagnostic;
- remove
.sensitive()or call.sensitive(false)only when the value is safe to write to terminals, logs, and CI transcripts; - for a
flag.custom()parse function, write the thrown message to describe the expectation rather than interpolating the input, since your own message is shown verbatim and the framework cannot redact it.
References: Diagnostics and redaction, Upgrading to 4.0
A Defaulted Input Looks Like The User Supplied It
Symptom:
- a mutually-exclusive rule fires when the user passed nothing;
- a merge over a project manifest uses the CLI default instead of the manifest value;
flags.x !== undefinedis true for every invocation.
Cause:
- a
.default()value is a real resolved value, so readingflagsalone cannot separate "the user supplied it" from "the chain fell through to the default".
Check:
sources.flags.xin the handler, orsources.args.xfor a positional. An input the default filled reports{ stage: 'default' }, and one no source filled has no record at all.
Fix:
- use
wasExplicit(sources.flags.x), which is false for'default'and for an absent record and true for every other stage; - read
sources.flags.x?.stage === 'cli'when the question is narrower, "did the user type it on this command line"; - do not drop
.default()to detect this. That also dropsdefaultValuefrom the definition document and the(default: …)suffix from help.
References: Value provenance, Limitations And Workarounds
An Argument Declared After A Variadic One Throws
Symptom:
- building the command throws before any argv is read:
Argument <target> comes after variadic argument <files>, which consumes every remaining positional
Suggestion: Declare <target> before <files>, or drop .variadic() from <files>Cause:
- a variadic argument takes every remaining positional token, so anything registered behind it could never be filled, and a second variadic one would have nothing left to collect.
Check:
- the order of the
.arg()calls, or of theargsentries in a definition; details, which carries the command incommand, the argument that could never fill inarg, and the greedy one invariadicArg.
Fix:
- move the variadic argument last;
- or drop
.variadic()from the earlier one.
The code is INVALID_BUILDER_STATE on both construction paths, and a definition tree reports the nested command that declared the pair.
References: Variadic Arguments, Upgrading to 4.0
A Collection Modifier Throws On An Argument
Symptom:
.separator(),.split(),.unique(), or.duplicateKeys()is refused by the compiler, or a definition throwsINVALID_SCHEMA:
Arg schema field 'separator' requires a collection, received a non-variadic 'string' arg
Suggestion: Add 'variadic: true', declare the arg as kind 'keyValue', or drop 'separator'Arg schema field 'unique' requires a variadic arg of a list kind
Suggestion: Add 'variadic: true' on a list kind, or drop 'unique'Cause:
- these four are collection modifiers, and an argument that aggregates nothing has no elements to split, dedupe, or fold. Each states the shape it needs:
.separator()and.split()want a variadic argument orarg.keyValue(),.unique()a variadic argument of a list kind, and.duplicateKeys()arg.keyValue().
Check:
- whether
.variadic()sits ahead of the modifier in the chain; - for
.unique(), that the kind is a list rather thanarg.keyValue(), which folds repeated keys through.duplicateKeys()instead.
Fix:
- add
.variadic()before the modifier; - or declare the argument as
arg.keyValue(); - or drop the call, which changed nothing on a single-value argument anyway.
References: Collections, Upgrading to 4.0
Two Inputs Both Want Stdin
Symptom:
- building the command throws
DUPLICATE_STDIN_INPUTbefore any argv is read.
Cause:
- one command has one exclusive stdin consumer, and a second
.stdin()input of either surface claims a stream that is already spoken for.
Fix:
- keep
.stdin()on a single input; - or declare every stdin input on that command with
{ consume: 'broadcast' }, which hands the same buffer to each of them.
--json Changes The Output Shape
Symptom:
- spinner or progress output disappears;
- decorative output does not show up when stdout is piped;
- logs look different in tests than in an interactive terminal.
Cause:
- DreamCLI intentionally changes output policy in JSON mode and non-TTY contexts.
Fix:
- treat JSON mode as a machine-readable surface, not a styled terminal surface;
- test interactive and non-interactive output separately when both matter;
- use the captured
stdout,stderr, andactivityarrays fromrunCommand()to assert exact behavior.
References: Output, Testing Commands, Output Contract
Completion Script Installs, But Suggestions Look Wrong
Symptom:
- the generated completion script loads, but expected commands or flags are missing;
- root-level completion behaves differently than expected.
Cause:
- hidden commands stay executable but are omitted from help and completions;
- root completion behavior depends on default-command visibility and root mode;
- the wrong shell script may have been installed for the active shell.
Check:
- which shell script you generated and installed;
- whether the command or flag is intentionally hidden;
- whether root behavior depends on a visible default command.
Fix:
- regenerate completions for the exact target shell;
- confirm the command-tree visibility rules in your schema;
- review root/default-command completion semantics before assuming generation is broken.
References: Shell Completions, CLI Semantics
Tests Behave Differently From Real CLI Runs
Symptom:
- a command passes in
runCommand()but behaves differently from manual terminal usage; - prompt or TTY-sensitive behavior does not line up.
Cause:
- the test harness is in-process and fully controlled by
RunOptions.
Check:
- whether the test set
jsonMode,isTTY,stdinData,env,config, oranswers; - whether the real CLI run has different stdin or terminal conditions.
Fix:
- make the test conditions explicit instead of relying on defaults;
- add separate cases for interactive TTY and non-interactive execution when behavior diverges by design.
References: Testing Commands, Runtime Support
Still Stuck?
Use this order:
- Check CLI Semantics for precedence or root-surface rules.
- Confirm in Support Matrix that the surface is actually shipped.
- Review Limitations And Workarounds for intentional constraints.
- Reduce the command to one failing flag or arg and reproduce it under
runCommand().