sipeed/picoclaw · error

credential: failed to read credential file %q: %w

Error message

credential: failed to read credential file %q: %w

What it means

Returned by Resolver.Resolve when os.ReadFile fails on the already-symlink-resolved, containment-checked credential path. EvalSymlinks succeeded moments earlier, so the usual cause is a permission problem (file or parent dir not readable by the process) or a race where the file disappeared/was replaced between resolution and read. The real (post-symlink) path is included in the message.

Source

Thrown at pkg/credential/credential.go:137

			return "", fmt.Errorf("credential: file:// reference has no filename")
		}

		baseDir := r.resolvedConfigDir
		if baseDir == "" {
			baseDir = r.configDir
		}
		keyPath := filepath.Join(baseDir, fileName)
		// Resolve symlinks before enforcing containment to prevent escaping via symlinks.
		realKeyPath, err := filepath.EvalSymlinks(keyPath)
		if err != nil {
			return "", fmt.Errorf("credential: failed to resolve credential file path %q: %w", keyPath, err)
		}
		if !isWithinDir(realKeyPath, baseDir) {
			return "", fmt.Errorf("credential: file:// path escapes config directory")
		}
		data, err := os.ReadFile(realKeyPath)
		if err != nil {
			return "", fmt.Errorf("credential: failed to read credential file %q: %w", realKeyPath, err)
		}

		value := strings.TrimSpace(string(data))
		if value == "" {
			return "", fmt.Errorf("credential: credential file %q is empty", realKeyPath)
		}

		return value, nil
	}

	if strings.HasPrefix(raw, EncScheme) {
		return resolveEncrypted(raw)
	}

	// Plaintext credential — return unchanged.
	return raw, nil
}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Check the exact path in the error: `ls -l` it and verify the running user can read it (`sudo -u appuser cat <path>`)
  2. Fix ownership/permissions: chown to the service user or chmod 640 with a shared group; keep 0600 for single-user
  3. If rotation replaces files, rotate atomically (write temp + rename in place) so the path never dangles
  4. Re-run the resolve after fixing to confirm

Example fix

# before
-rw------- 1 root root config/openai.key

# after
chown appuser:appuser config/openai.key && chmod 600 config/openai.key
Defensive patterns

Strategy: try-catch

Validate before calling

// Preflight readability as the service user before boot.
if f, err := os.Open(keyPath); err == nil {
	f.Close()
} else {
	return fmt.Errorf("credential %s unreadable: %w", keyPath, err)
}

Try / catch

val, err := resolver.Resolve(raw)
if err != nil {
	var pathErr *fs.PathError
	if errors.As(err, &pathErr) && errors.Is(pathErr.Err, fs.ErrPermission) {
		// ownership/permissions fix; do not fall back to plaintext
	}
	return "", err
}

Prevention

When it happens

Trigger: Credential file exists but is mode 0600 owned by another user (service runs as different uid); parent directory loses +x/search permission; file deleted or moved between the EvalSymlinks and ReadFile calls; SELinux/AppArmor denying the read.

Common situations: Service user changes (systemd unit hardening, container USER directive) leaving root-owned key files; automated secret rotation briefly unlinking/replacing the file; security modules denying reads from the config location.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/973c8127e5265339. Report an issue: GitHub.