Problem
Multi-value flags are documented as comma-separated lists - the CLI's own help
text says so throughout ("The comma separated list of ...", root.go:135,
:233-251 and many more) - and map-valued flags take k=v pairs. Both work
correctly from a config file:
org: demo
environment: prod,staging # -> ["prod", "staging"]
external-url: docs=https://example.com/docs # -> {"docs": {"href": "..."}}
The problem is what happens when a user writes the YAML-native form instead,
which is the natural thing to reach for in a YAML file. Neither is supported, and
neither is rejected:
| Config file value |
Result |
environment: - prod - staging |
one element, "[prod staging]". Exit 0, no warning. |
external-url: docs: https://... |
exit 1 with no output at all |
Both come from the same line. bindFlags applies config values by stringifying
them with %v:
// cmd/kosli/root.go:599
if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil {
logger.Error("failed to set flag: %v", err)
}
val is an any decoded from YAML. A scalar formats back to itself, so the
supported forms survive. A list formats as [prod staging] and a map as
map[docs:https://...], neither of which any pflag Value can parse back.
The map case produces no message because of two things acting together.
logger.Error is Fatalf (internal/logger/logger.go:64-67), so it terminates
the process, and the error stream is not wired up at this point -
root.go:557-559 documents exactly that, with the fix commented out:
// for some reason, logger does not print errors at the point
// of calling this function, so we ensure to point errors to stderr
// logger.SetErrOut(errOut)
So the message goes nowhere and the user gets a bare exit 1.
Why it matters
Two distinct defects, worth separating because they have different weights.
The silent exit 1 is a bug outright. Whether or not YAML map syntax is
supported, a config value the CLI cannot apply must produce a message. Exiting 1
with nothing on stdout or stderr is the worst available outcome, and it is
currently the documented, accepted behaviour of that code path. Any future
Set failure in bindFlags - not just this one - is equally invisible.
The silent corruption is narrower but still real. A YAML list is not a
supported representation, so this is unsupported input rather than broken
functionality. But it is unsupported input that is accepted and mangled rather
than rejected: kosli create flow with a YAML-list template exits 0 and
writes a flow whose template requires an attestation literally named
[coverage unit-test], which nothing can ever satisfy.
Whether a given flag fails loudly is incidental - it depends on whether the
command validates the value locally. --attachments stats the path and so
errors with stat [first.txt second.txt]; --template is sent straight to the
server and so does not.
The YAML-native form is the obvious thing to write in a YAML file, and nothing
in the CLI says otherwise at the point of use.
Reproduce
Verified 2026-08-12 against a binary built from f260dee, --dry-run throughout.
Supported forms, both correct:
$ cat kosli.yml
org: demo
environment: prod,staging
$ kosli attach-policy mypolicy --config-file kosli.yml --dry-run
... .../environments/demo/prod/policies
... .../environments/demo/staging/policies
YAML list, silently corrupted:
$ cat kosli.yml
org: demo
template:
- coverage
- unit-test
$ kosli create flow myflow --config-file kosli.yml --dry-run
"template": [
"[coverage unit-test]",
"artifact"
]
YAML map, silent exit 1:
$ cat kosli.yml
org: demo
external-url:
docs: https://example.com/docs
$ kosli attest generic --config-file kosli.yml --fingerprint <sha> --name foo \
--flow f --trail t --dry-run
$ echo $?
1
No output on stdout or stderr.
Fix
The silent exit 1 should be fixed regardless of anything else: uncomment or
replace logger.SetErrOut(errOut) at root.go:559 so failures in bindFlags
are printed, and consider whether logger.Error being Fatalf is right here - a
bad config value is user input, and would read better as a returned error naming
the flag and the offending value.
For the non-scalar values themselves there are two directions, and this issue
does not assume one:
- Reject them with a clear error. Strictly a bug fix, and consistent with
the comma-separated contract the help text advertises. The message should
name the flag and the expected format.
- Support them. Use
pflag.SliceValue.Replace for lists and build k=v
pairs for maps. Friendlier, and arguably what a YAML config file ought to
mean, but it is a feature rather than a fix and widens the scope.
Note for whoever implements option 2: Replace bypasses FlagSet.Set, so
Flag.Changed is not set and required-flag validation then fails. That was found
by test, not by reading.
Tests
- The supported forms keep working: a comma-separated string for a slice flag and
a k=v string for a map flag, asserted on the parsed flag value.
- A YAML list and a YAML map each produce whatever the chosen behaviour is,
asserted on the parsed flag value or the error, not merely on exit code.
- A
Set failure inside bindFlags prints a message. This one cannot be written
as an ordinary in-process test while logger.Error calls os.Exit - it needs
the error-handling change first, or a subprocess test.
Related
Found while auditing empty flag-value handling for kosli-dev/server#6070. This
issue is independent of empty values and can be fixed on its own.
Problem
Multi-value flags are documented as comma-separated lists - the CLI's own help
text says so throughout ("The comma separated list of ...",
root.go:135,:233-251and many more) - and map-valued flags takek=vpairs. Both workcorrectly from a config file:
The problem is what happens when a user writes the YAML-native form instead,
which is the natural thing to reach for in a YAML file. Neither is supported, and
neither is rejected:
environment:- prod- staging"[prod staging]". Exit 0, no warning.external-url:docs: https://...Both come from the same line.
bindFlagsapplies config values by stringifyingthem with
%v:valis ananydecoded from YAML. A scalar formats back to itself, so thesupported forms survive. A list formats as
[prod staging]and a map asmap[docs:https://...], neither of which any pflagValuecan parse back.The map case produces no message because of two things acting together.
logger.ErrorisFatalf(internal/logger/logger.go:64-67), so it terminatesthe process, and the error stream is not wired up at this point -
root.go:557-559documents exactly that, with the fix commented out:So the message goes nowhere and the user gets a bare exit 1.
Why it matters
Two distinct defects, worth separating because they have different weights.
The silent exit 1 is a bug outright. Whether or not YAML map syntax is
supported, a config value the CLI cannot apply must produce a message. Exiting 1
with nothing on stdout or stderr is the worst available outcome, and it is
currently the documented, accepted behaviour of that code path. Any future
Setfailure inbindFlags- not just this one - is equally invisible.The silent corruption is narrower but still real. A YAML list is not a
supported representation, so this is unsupported input rather than broken
functionality. But it is unsupported input that is accepted and mangled rather
than rejected:
kosli create flowwith a YAML-listtemplateexits 0 andwrites a flow whose template requires an attestation literally named
[coverage unit-test], which nothing can ever satisfy.Whether a given flag fails loudly is incidental - it depends on whether the
command validates the value locally.
--attachmentsstats the path and soerrors with
stat [first.txt second.txt];--templateis sent straight to theserver and so does not.
The YAML-native form is the obvious thing to write in a YAML file, and nothing
in the CLI says otherwise at the point of use.
Reproduce
Verified 2026-08-12 against a binary built from f260dee,
--dry-runthroughout.Supported forms, both correct:
YAML list, silently corrupted:
YAML map, silent exit 1:
No output on stdout or stderr.
Fix
The silent exit 1 should be fixed regardless of anything else: uncomment or
replace
logger.SetErrOut(errOut)atroot.go:559so failures inbindFlagsare printed, and consider whether
logger.ErrorbeingFatalfis right here - abad config value is user input, and would read better as a returned error naming
the flag and the offending value.
For the non-scalar values themselves there are two directions, and this issue
does not assume one:
the comma-separated contract the help text advertises. The message should
name the flag and the expected format.
pflag.SliceValue.Replacefor lists and buildk=vpairs for maps. Friendlier, and arguably what a YAML config file ought to
mean, but it is a feature rather than a fix and widens the scope.
Note for whoever implements option 2:
ReplacebypassesFlagSet.Set, soFlag.Changedis not set and required-flag validation then fails. That was foundby test, not by reading.
Tests
a
k=vstring for a map flag, asserted on the parsed flag value.asserted on the parsed flag value or the error, not merely on exit code.
Setfailure insidebindFlagsprints a message. This one cannot be writtenas an ordinary in-process test while
logger.Errorcallsos.Exit- it needsthe error-handling change first, or a subprocess test.
Related
Found while auditing empty flag-value handling for kosli-dev/server#6070. This
issue is independent of empty values and can be fixed on its own.