Skip to content

bug: YAML list and map syntax in the config file is silently corrupted or exits 1 with no message #1086

Description

@JonJagger

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:

  1. 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.
  2. 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.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions