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

  1. Set XDG_CONFIG_HOME (Linux/macOS) to a writable directory, e.g. export XDG_CONFIG_HOME="$HOME/.config".
  2. Ensure HOME is set for the user running sops (docker run -e HOME=/root, cron/systemd Environment=HOME=...).
  3. Provide the identity explicitly via SOPS_AGE_KEY (inline) or SOPS_AGE_KEY_FILE so the config-dir fallback isn't needed.
  4. Create the config directory: mkdir -p ~/.config/sops/age and place keys.txt there.
  5. 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

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


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