getsops/sops · error
failed to parse command %s: %w
Error message
failed to parse command %s: %w
What it means
SOPS splits the SOPS_AGE_SSH_PRIVATE_KEY_CMD value with shlex so it can exec a command; if the command string is not valid shell syntax (unbalanced quotes, dangling backslash, etc.), shlex.Split fails and the command string is echoed back in the error. No command is executed in this case.
Source
Thrown at age/keysource.go:305
out["recipient"] = key.Recipient
out["enc"] = key.EncryptedKey
return out
}
// TypeToIdentifier returns the string identifier for the MasterKey type.
func (key *MasterKey) TypeToIdentifier() string {
return KeyTypeIdentifier
}
// getOutputFromCmd executes a shell command provided in param 'cmdString',
// optionally adding env vars provided in param 'envVars',
// and returns the command's output and error
func getOutputFromCmd(cmdString string, envVars []string) ([]byte, error) {
var out []byte
args, err := shlex.Split(cmdString)
if err != nil {
return nil, fmt.Errorf("failed to parse command %s: %w", cmdString, err)
}
cmd := exec.Command(args[0], args[1:]...)
if envVars != nil {
cmd.Env = append(os.Environ(), envVars[0:]...)
}
out, err = cmd.Output()
if err != nil {
return nil, fmt.Errorf("failed to execute command %s: %w", cmdString, err)
}
return out, nil
}
// loadAgeSSHIdentity attempts to load age SSH identities in this order:
// 1. An SSH private key from the SopsAgeSshPrivateKeyFileEnv environment variable.
// 2. An SSH private key returned by executing the command from the
// SopsAgeSshPrivateKeyCmdEnv environment variable
// 3. `~/.ssh/id_ed25519` or `~/.ssh/id_rsa`.View on GitHub (pinned to 13442bb981)
Solutions
- Fix the SOPS_AGE_SSH_PRIVATE_KEY_CMD value so it parses as a simple argv list: balanced quotes, no shell operators like && or |.
- Test the command string with a shell-free parser mindset: sops passes args[0] as the binary and the rest as arguments, no shell involved.
- If you need pipes or redirection, point the var at a wrapper script and set SOPS_AGE_SSH_PRIVATE_KEY_CMD="/path/to/wrapper.sh".
- Export the variable in a clean form, e.g. export SOPS_AGE_SSH_PRIVATE_KEY_CMD='pass show age-identity', and verify with echo before running sops.
Example fix
// before export SOPS_AGE_SSH_PRIVATE_KEY_CMD="pass show age-key && cat" // shlex treats && as an argument; parse error // after export SOPS_AGE_SSH_PRIVATE_KEY_CMD="pass show age-key" # or use a wrapper script for shell logic: export SOPS_AGE_SSH_PRIVATE_KEY_CMD="$HOME/bin/age-key-cmd.sh"
Defensive patterns
Strategy: validation
Validate before calling
// shell: validate the command parses as simple argv before exporting export SOPS_AGE_SSH_PRIVATE_KEY_CMD='pass show age-identity' python3 - <<'EOF' import shlex, os args = shlex.split(os.environ['SOPS_AGE_SSH_PRIVATE_KEY_CMD']) assert args and os.path.basename(args[0]), "unparseable command" print(args) EOF
Try / catch
if err := runSopsDecrypt(); err != nil {
if strings.Contains(err.Error(), "failed to parse command") {
return fmt.Errorf("SOPS_AGE_SSH_PRIVATE_KEY_CMD is not valid argv syntax: %w", err)
}
return err
} Prevention
- Never put shell operators (&&, |, ;, >) in SOPS_AGE_SSH_PRIVATE_KEY_CMD; sexec uses shlex argv, not a shell.
- Use a wrapper script when you need shell logic, and point the env var at that script alone.
- Keep the value in single quotes to prevent your own shell from mangling quotes.
- Echo the variable in CI logs (redacted) to catch quoting drift between environments.
- Test with a trivial command like 'echo hi' when debugging.
When it happens
Trigger: loadIdentities -> loadAgeSSHIdentities reads SOPS_AGE_SSH_PRIVATE_KEY_CMD and getOutputFromCmd calls shlex.Split on it; any malformed quoting/token in the env var value produces this error before exec.
Common situations: Setting SOPS_AGE_SSH_PRIVATE_KEY_CMD="ssh-add -L" with unbalanced quotes in shell config; wrapping the command in shell metacharacters (&&, |) that shlex treats as tokens; trailing backslashes from line continuations.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- failed to execute command %s: %w
- failed to open %s file: %w
- user config directory could not be determined: %w
- incorrect passphrase
- failed to decrypt identity file: %v
AI-assisted analysis of getsops/sops@13442bb981 (2026-09-01).
Data as JSON: /api/errors/a65f0e674fb355f8.
Report an issue: GitHub.