getsops/sops · error
user config directory could not be determined: %w
Error message
user config directory could not be determined: %w
What it means
SOPS tries to fall back to the default age key at <userConfigDir>/sops/age/keys.txt, but os.UserConfigDir failed (XDG_CONFIG_HOME unset/unwritable on Linux, AppData missing on Windows) and no other key source was available, so this becomes a candidate-location error. It only surfaces when no identities were found via SOPS_AGE_KEY, SOPS_AGE_KEY_FILE, or SOPS_AGE_KEY_CMD.
Source
Thrown at age/keysource.go:456
}
if ageKeyCmd, ok := os.LookupEnv(SopsAgeKeyCmdEnv); ok {
out, err := getOutputFromCmd(ageKeyCmd, []string{fmt.Sprintf("%s=%s", SopsAgeRecipientEnv, key.Recipient)})
if err != nil {
errs = append(errs, err)
} else {
readers[SopsAgeKeyCmdEnv] = identityReader{
reader: bytes.NewReader(out),
allowMultipleKeysPerLine: false,
}
}
} else {
unusedLocations = append(unusedLocations, SopsAgeKeyCmdEnv)
}
userConfigDir, err := getUserConfigDir()
if err != nil && len(readers) == 0 && len(identities) == 0 {
errs = append(errs, fmt.Errorf("user config directory could not be determined: %w", err))
} else if userConfigDir != "" {
ageKeyFilePath := filepath.Join(userConfigDir, filepath.FromSlash(SopsAgeKeyUserConfigPath))
f, err := os.Open(ageKeyFilePath)
if err != nil && !errors.Is(err, os.ErrNotExist) {
errs = append(errs, fmt.Errorf("failed to open file: %w", err))
} else if errors.Is(err, os.ErrNotExist) && len(readers) == 0 && len(identities) == 0 {
unusedLocations = append(unusedLocations, ageKeyFilePath)
} else if err == nil {
defer f.Close()
readers[ageKeyFilePath] = identityReader{
reader: f,
allowMultipleKeysPerLine: false,
}
}
}
for location, r := range readers {
ids, err := unwrapIdentities(location, r.reader, r.allowMultipleKeysPerLine)View on GitHub (pinned to 13442bb981)
Solutions
- Set XDG_CONFIG_HOME (Linux/macOS) to a writable directory, e.g. export XDG_CONFIG_HOME="$HOME/.config".
- Ensure HOME is set for the user running sops (docker run -e HOME=/root, cron/systemd Environment=HOME=...).
- Provide the identity explicitly via SOPS_AGE_KEY (inline) or SOPS_AGE_KEY_FILE so the config-dir fallback isn't needed.
- Create the config directory: mkdir -p ~/.config/sops/age and place keys.txt there.
- On Windows, verify %AppData% resolves in the environment running sops.
Example fix
# before: bare container shell $ sops -d secrets.enc.yaml # user config directory could not be determined: $HOME is not defined # after export HOME=/root export XDG_CONFIG_HOME="$HOME/.config" mkdir -p "$XDG_CONFIG_HOME/sops/age" mv keys.txt "$XDG_CONFIG_HOME/sops/age/keys.txt"
Defensive patterns
Strategy: validation
Validate before calling
// shell: ensure sops can resolve a config dir, or supply the key directly
: "${XDG_CONFIG_HOME:=$HOME/.config}"
export XDG_CONFIG_HOME
[ -n "${HOME:-}" ] || { echo "HOME must be set for sops"; exit 1; }
mkdir -p "$XDG_CONFIG_HOME/sops/age" Try / catch
if err := runSopsDecrypt(); err != nil &&
strings.Contains(err.Error(), "user config directory could not be determined") {
return fmt.Errorf("set XDG_CONFIG_HOME/HOME or provide SOPS_AGE_KEY directly: %w", err)
} Prevention
- Always set HOME (and XDG_CONFIG_HOME on Linux/macOS) in containers, cron, and systemd units.
- Prefer explicit SOPS_AGE_KEY or SOPS_AGE_KEY_FILE in ephemeral environments so config-dir lookup never matters.
- Create ~/.config/sops/age/keys.txt as part of onboarding docs/scripts.
- For services, define Environment=HOME=/var/lib/<svc> in the unit file.
- On Windows, don't override AppData in service contexts.
When it happens
Trigger: loadIdentities calls getUserConfigDir; getUserConfigDir returns an error from os.UserConfigDir, and readers/identities are both empty at that point, so the error is appended to errs.
Common situations: Containers or CI images with HOME and XDG_CONFIG_HOME unset; running sops as a different user (systemd service, cron) without a writable home; Windows machines with redirected/missing AppData.
Related errors
- failed to parse command %s: %w
- failed to execute command %s: %w
- failed to open %s file: %w
- incorrect passphrase
- failed to decrypt identity file: %v
AI-assisted analysis of getsops/sops@13442bb981 (2026-09-01).
Data as JSON: /api/errors/a5c59a7be1054580.
Report an issue: GitHub.