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
- Check the exact path in the error: `ls -l` it and verify the running user can read it (`sudo -u appuser cat <path>`)
- Fix ownership/permissions: chown to the service user or chmod 640 with a shared group; keep 0600 for single-user
- If rotation replaces files, rotate atomically (write temp + rename in place) so the path never dangles
- 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
- Set 0600 service-user ownership during provisioning; verify in entrypoint
- Rotate secrets with write-temp+rename so reads never hit a dangling path
- Never downgrade to a plaintext credential on this error
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
- create output dir: %w
- failed to save config: %w
- ✗ failed to create skills directory: %w
- failed to create media temp dir: %w
- failed to read security config: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/973c8127e5265339.
Report an issue: GitHub.