getsops/sops · error

failed to read '%s': %w

Error message

failed to read '%s': %w

What it means

unwrapIdentities reads an age identity file (decrypting it if needed) with io.ReadAll bounded to 16 MiB. Any read error before that limit is wrapped as "failed to read '%s': %w", naming the file location. It indicates the underlying reader failed while consuming the key file, not a key-matching problem.

Source

Thrown at age/encrypted_keys.go:124

	return fileKey, err
}

func unwrapIdentities(location string, reader io.Reader, allowMultipleKeysPerLine bool) (ParsedIdentities, error) {
	b := bufio.NewReader(reader)
	p, _ := b.Peek(14) // length of "age-encryption" and "-----BEGIN AGE"
	peeked := string(p)

	switch {
	// An age encrypted file, plain or armored.
	case peeked == "age-encryption" || peeked == "-----BEGIN AGE":
		var r io.Reader = b
		if peeked == "-----BEGIN AGE" {
			r = armor.NewReader(r)
		}
		const privateKeySizeLimit = 1 << 24 // 16 MiB
		contents, err := io.ReadAll(io.LimitReader(r, privateKeySizeLimit))
		if err != nil {
			return nil, fmt.Errorf("failed to read '%s': %w", location, err)
		}
		if len(contents) == privateKeySizeLimit {
			return nil, fmt.Errorf("failed to read '%s': file too long", location)
		}
		// We use Base32 encoding instead of Base64 encoding, since our GPG agent package percent-encodes
		// the cache ID. Base64 has two characters ('+' and '/') that would end up as a longer sequence,
		// whence using Base64 encoding can suddenly blow up the cache key to more than GPG's maximum of
		// 50 characters.
		// By using 25 bytes, that translate to 25 / 5 * 8 = 40 letters/digits, we have a cache key of
		// length 47, whose percent-encoding always has 47 characters.
		contentsHash := sha256.Sum256(contents)
		cacheKey := fmt.Sprintf("SopsAge%s", base32.StdEncoding.EncodeToString(contentsHash[:25]))
		IncorrectPassphrase := func() {
			conn, err := gpgagent.NewConn()
			if err != nil {
				return
			}
			defer func(conn *gpgagent.Conn) {

View on GitHub (pinned to 13442bb981)

Solutions

  1. Check the file path exists and is readable by the current user (permissions/ownership).
  2. Retry if the failure came from a transient network filesystem error.
  3. Inspect the wrapped cause (%w) to identify the underlying I/O failure and fix that source.

Example fix

// before
$ ls -l /run/secrets/age-key  # root-only readable
// after
$ chmod 600 /run/secrets/age-key && chown $USER /run/secrets/age-key
Defensive patterns

Strategy: validation

Validate before calling

info, err := os.Stat(location)
if err != nil {
    return fmt.Errorf("identity file %s not accessible: %w", location, err)
}
if !info.Mode().IsRegular() || info.Size() > 1<<24 {
    return fmt.Errorf("identity file %s is not a regular file or too large", location)
}

Try / catch

keys, err := loadIdentities(loc)
if err != nil && strings.Contains(err.Error(), "failed to read") {
    // check permissions/mount and retry once
}

Prevention

When it happens

Trigger: Loading identities via loadIdentities when io.ReadAll fails on the identity file's reader — permission errors, missing file opened lazily, network filesystem dropout, or a failing custom io.Reader.

Common situations: Wrong path or unreadable permissions on the file referenced by SOPS_AGE_KEY_FILE; SSH agent or key provider streams that error mid-read; NFS/network mounts dropping connection during read.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


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