sipeed/picoclaw · error
credential: file:// path escapes config directory
Error message
credential: file:// path escapes config directory
What it means
Returned by Resolver.Resolve as a deliberate security guard: after resolving symlinks, the real path of the credential file is not inside the resolver's base config directory. Because EvalSymlinks ran first, this catches both lexical escapes (../ in the filename) and symlink-based escapes (a link inside configDir pointing outside). This is fail-closed: no credential is read, and the error is returned immediately.
Source
Thrown at pkg/credential/credential.go:133
if strings.HasPrefix(raw, FileScheme) {
fileName := strings.TrimSpace(strings.TrimPrefix(raw, FileScheme))
if fileName == "" {
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)
}
View on GitHub (pinned to 49183d7e8d)
Solutions
- Copy (not symlink) the credential file into the config dir and reference it plainly: `file://openai.key`
- If you need mounted secrets, have provisioning materialize the file inside the config dir (e.g. initContainer/sidecar copy step) rather than linking out
- Remove any symlinks inside the config dir that point outside it — they will always be rejected
- Never construct file:// values from user input without rejecting `..` and absolute-ish components first
Example fix
# before: symlink escape ln -s /run/secrets/api_key config/openai.key # always rejected # after: copy into config dir cp /run/secrets/api_key config/openai.key && chmod 600 config/openai.key
Defensive patterns
Strategy: validation
Validate before calling
// Reject traversal before calling Resolve.
func safeFileRef(filename string) error {
filename = strings.TrimSpace(filename)
if filename == "" || strings.Contains(filename, "..") || filepath.IsAbs(filename) {
return fmt.Errorf("unsafe credential filename %q", filename)
}
return nil
} Type guard
func isContainedFilename(name string) bool {
name = strings.TrimSpace(name)
return name != "" && !strings.Contains(name, "..") && !filepath.IsAbs(name)
} Try / catch
if _, err := resolver.Resolve(raw); err != nil {
if strings.Contains(err.Error(), "escapes config directory") {
// never retry with sanitized input automatically; surface for human review (security)
}
return err
} Prevention
- Never symlink secrets from outside into the config dir — copy them
- Treat an escapes-config-directory error as a security finding, not a transient failure
- Provision mounted secrets by copying into the config dir at deploy time
When it happens
Trigger: `file://../../etc/passwd` or `file://../shared/keys/openai.key` (lexical traversal after join+resolve lands outside baseDir); or a legit-looking `file://openai.key` where openai.key is a symlink to ~/.ssh/id_rsa or /run/secrets/... — EvalSymlinks exposes the real target outside the dir and the guard fires.
Common situations: Trying to reuse existing secrets (Docker/Kubernetes mounted at /run/secrets, ~/.ssh) by symlinking them into the config dir; sharing a keys directory between projects via ../; or hardening scans probing with traversal payloads. The design requires secrets to physically live inside the config dir.
Related errors
- credential: file:// reference has no filename
- credential: failed to resolve credential file path %q: %w
- credential: credential file %q is empty
- credential: enc:// passphrase required
- credential: enc:// decryption failed (wrong passphrase or SS
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/66c5842e1ccf1824.
Report an issue: GitHub.