sipeed/picoclaw · error

credential: failed to resolve credential file path %q: %w

Error message

credential: failed to resolve credential file path %q: %w

What it means

Returned by Resolver.Resolve when filepath.EvalSymlinks fails on the joined path configDir/filename — overwhelmingly because the target file does not exist (ENOENT), but also for unreadable path components or I/O errors. Symlinks are resolved deliberately before containment enforcement, so a dangling symlink also lands here. The message includes the unresolved keyPath for debugging.

Source

Thrown at pkg/credential/credential.go:130

	if raw == "" {
		return "", nil
	}

	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) {

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Check the path printed in the error: does <configDir>/<filename> exist? Create it with the secret content if missing
  2. If the file exists elsewhere, either move/copy it into the config dir or fix the filename in the file:// reference
  3. Fix or remove dangling symlinks inside the config dir (they fail EvalSymlinks even when the final name is right)
  4. Verify which config dir the resolver uses (resolvedConfigDir overrides configDir when set) before assuming the location

Example fix

# before: config references file://openai.key, file absent
ls config/openai.key  # -> No such file

# after: create the file with the secret
echo -n 'sk-...' > config/openai.key && chmod 600 config/openai.key
Defensive patterns

Strategy: validation

Validate before calling

// Pre-check existence inside the resolver's base dir.
base := resolver.BaseDir() // resolvedConfigDir, falling back to configDir
p := filepath.Join(base, filename)
if _, err := os.Stat(p); err != nil {
	return fmt.Errorf("credential file %s missing: create it before boot", p)
}

Try / catch

val, err := resolver.Resolve(raw)
if err != nil {
	var pathErr *fs.PathError
	if errors.As(err, &pathErr) && errors.Is(pathErr.Err, fs.ErrNotExist) {
		// missing/dangling file: actionable message with the printed keyPath
	}
	return "", err
}

Prevention

When it happens

Trigger: `file://openai.key` in config but openai.key was never created in the resolved config dir; filename typo; file lives in a different directory than the resolver's baseDir; a symlink inside configDir pointing to a deleted target (EvalSymlinks returns ENOENT).

Common situations: Fresh clones/machines where credential files aren't checked in (they're secrets) and the setup step that writes them was skipped; running with a different --config dir or env var than the one where keys were placed; renaming a key file without updating the config.

Related errors


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