Summary
Proper subcommands silently ignore a colliding global option when it appears before the subcommand. For example, sops --output-type json decrypt encrypted.yaml exits 0 but writes YAML. The same option after decrypt, and the legacy -d form, produce JSON.
This is unsafe for automation because a successful exit status can falsely attest to a requested output format.
Minimal synthetic reproduction
age-keygen -o key.txt
recipient=$(age-keygen -y key.txt)
printf 'apiVersion: v1\nkind: Synthetic\nmetadata:\n name: parsing-fixture\nspec:\n enabled: true\n count: 42\n' > plaintext.yaml
sops encrypt --age "$recipient" --output encrypted.yaml plaintext.yaml
SOPS_AGE_KEY_FILE=key.txt sops --output-type json decrypt encrypted.yaml > bad.out
echo $? # 0
cat bad.out # YAML, not JSON
jq . < bad.out # parse error
bad.out is:
apiVersion: v1
kind: Synthetic
metadata:
name: parsing-fixture
spec:
enabled: true
count: 42
Control cases:
SOPS_AGE_KEY_FILE=key.txt sops decrypt --output-type json encrypted.yaml | jq .
SOPS_AGE_KEY_FILE=key.txt sops -d --output-type json encrypted.yaml | jq .
Both controls exit 0 and emit valid JSON. No non-synthetic key or secret is involved.
Expected result
The ambiguous form must not silently succeed with a different output format. Either honor the option consistently or reject the command before producing output with a nonzero status. A rejection is the least surprising compatibility-safe behavior because the proper subcommand already advertises its own option.
Actual result
The global parser consumes --output-type; the decrypt action reads its local c.String("output-type"), finds the local default, and falls back to the YAML extension. It returns success.
Version matrix
| SOPS version |
Platform |
sops --output-type json decrypt encrypted.yaml |
| 3.8.1 |
macOS arm64 |
rejected (proper subcommand not yet available) |
| 3.9.0 |
macOS arm64 |
exit 0, YAML |
| 3.9.1 |
macOS arm64 |
exit 0, YAML |
| 3.9.4 |
macOS arm64 and Debian 13 amd64 |
exit 0, YAML |
| 3.10.0 |
macOS arm64 |
exit 0, YAML |
| 3.13.2 |
macOS arm64 |
exit 0, YAML |
The behavior begins with the proper-subcommand migration in #1333 / #1391 (released in 3.9.0).
Scope
The same global-before-subcommand shadowing class was reproduced for decrypt with --input-type, --output, --extract, and --ignore-mac, and for analogous encrypt and rotate input/output options. This is therefore not only an output-format issue.
Related but distinct reports:
Proposed fix
Reject a global option when a proper subcommand declares the same option and that command-local option was not set. This preserves valid local placement and prevents a successful command from silently applying a different behavior. A draft PR implementing that guard and black-box regression tests will be linked here.
Automation impact
Format-sensitive preflight pipelines can treat exit status 0 as proof of JSON and then pass YAML to a JSON parser. The command fails later and less clearly, or can make a recovery/preflight decision from the wrong representation. Fail-closed CLI parsing is important where decrypted output is machine-consumed.
Summary
Proper subcommands silently ignore a colliding global option when it appears before the subcommand. For example,
sops --output-type json decrypt encrypted.yamlexits 0 but writes YAML. The same option afterdecrypt, and the legacy-dform, produce JSON.This is unsafe for automation because a successful exit status can falsely attest to a requested output format.
Minimal synthetic reproduction
bad.outis:Control cases:
Both controls exit 0 and emit valid JSON. No non-synthetic key or secret is involved.
Expected result
The ambiguous form must not silently succeed with a different output format. Either honor the option consistently or reject the command before producing output with a nonzero status. A rejection is the least surprising compatibility-safe behavior because the proper subcommand already advertises its own option.
Actual result
The global parser consumes
--output-type; thedecryptaction reads its localc.String("output-type"), finds the local default, and falls back to the YAML extension. It returns success.Version matrix
sops --output-type json decrypt encrypted.yamlThe behavior begins with the proper-subcommand migration in #1333 / #1391 (released in 3.9.0).
Scope
The same global-before-subcommand shadowing class was reproduced for
decryptwith--input-type,--output,--extract, and--ignore-mac, and for analogousencryptandrotateinput/output options. This is therefore not only an output-format issue.Related but distinct reports:
--output-typemust appear before-d#1259 concerns legacy-dordering.Proposed fix
Reject a global option when a proper subcommand declares the same option and that command-local option was not set. This preserves valid local placement and prevents a successful command from silently applying a different behavior. A draft PR implementing that guard and black-box regression tests will be linked here.
Automation impact
Format-sensitive preflight pipelines can treat exit status 0 as proof of JSON and then pass YAML to a JSON parser. The command fails later and less clearly, or can make a recovery/preflight decision from the wrong representation. Fail-closed CLI parsing is important where decrypted output is machine-consumed.