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

  1. Fix the SOPS_AGE_SSH_PRIVATE_KEY_CMD value so it parses as a simple argv list: balanced quotes, no shell operators like && or |.
  2. 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.
  3. 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".
  4. 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

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

Related errors


AI-assisted analysis of getsops/sops@13442bb981 (2026-09-01). Data as JSON: /api/errors/a65f0e674fb355f8. Report an issue: GitHub.