Skip to content

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 stdinIsTTY is true.

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() and configLoader().

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 when matches how the value is being passed;
  • the CLI token or --flag value did not already satisfy the input first.

Fix:

  • opt the input into .stdin() if piped data is part of the intended contract;
  • widen when to 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() and arg.path() keep the terminator unless { trim: true } is set.
  • other codecs interpret the text and drop one trailing \n, \r\n, or \r before decoding. echo 42 reaches flag.number() as 42, and echo 30s reaches flag.duration() as 30s.

Check:

  • whether the binding passed { trim: true };
  • the input's codec. String and path codecs carry the terminator this far, so trim changes nothing on decoding codecs.
  • whether the producer appends a newline. echo does; printf without \n does not.

Fix:

  • declare .stdin({ trim: true }), which drops one trailing \n, \r\n, or \r from 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.
ts
arg.path({ mustExist: true }).stdin({ trim: true });
bash
$ 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 its when accepts 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 --tag
No 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 with MISSING_STDIN rather 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' or when: 'dash-or-missing', the token names the source before anything reads it as text, so - is not data. With when: '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 when the 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.value is 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 sets sensitive: true;
  • which source produced the value. details.source names it, and details.envVar or details.configPath names the binding, but source does not decide redaction;
  • whether the reason is enough on its own. expected, constraint, bound, and allowed all 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 !== undefined is true for every invocation.

Cause:

  • a .default() value is a real resolved value, so reading flags alone cannot separate "the user supplied it" from "the chain fell through to the default".

Check:

  • sources.flags.x in the handler, or sources.args.x for 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 drops defaultValue from 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 the args entries in a definition;
  • details, which carries the command in command, the argument that could never fill in arg, and the greedy one in variadicArg.

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 throws INVALID_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 or arg.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 than arg.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_INPUT before 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.

References: Arguments, Flags

--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, and activity arrays from runCommand() 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, or answers;
  • 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:

  1. Check CLI Semantics for precedence or root-surface rules.
  2. Confirm in Support Matrix that the surface is actually shipped.
  3. Review Limitations And Workarounds for intentional constraints.
  4. Reduce the command to one failing flag or arg and reproduce it under runCommand().

Released under the MIT License.