twpayne/chezmoi · error
%s: %s: %w
Error message
%s: %s: %w
What it means
After confirming the external path is relative, chezmoi computes filepath.Rel('.', externalPath). If that computation itself fails (malformed path for the OS), the raw error is wrapped with both the external source file and the target path. This precedes the explicit 'outside target directory' and 'empty relative path' checks.
Source
Thrown at internal/chezmoi/sourcestate.go:1463
return fmt.Errorf("%s: %w", sourceAbsPath, err)
}
s.mutex.Lock()
defer s.mutex.Unlock()
for targetPath, external := range externals {
if external.TargetPath != "" {
targetPath = external.TargetPath
}
if targetPath == "" {
return fmt.Errorf("%s: empty path", sourceAbsPath)
}
externalPath := path.Clean(targetPath)
if strings.HasPrefix(externalPath, "/") || filepath.IsAbs(externalPath) {
return fmt.Errorf("%s: %s: path is not relative", sourceAbsPath, targetPath)
}
switch relPath, err := filepath.Rel(".", externalPath); {
case err != nil:
return fmt.Errorf("%s: %s: %w", sourceAbsPath, targetPath, err)
case relPath == ".":
return fmt.Errorf("%s: %s: empty relative path", sourceAbsPath, targetPath)
case relPath == "..", strings.HasPrefix(relPath, "../"),
runtime.GOOS == "windows" && strings.HasPrefix(relPath, ".."+string(filepath.Separator)):
return fmt.Errorf("%s: %s: relative path outside target directory", sourceAbsPath, targetPath)
}
targetRelPath := parentTargetSourceRelPath.JoinString(externalPath)
external.sourceAbsPath = sourceAbsPath
s.externals[targetRelPath] = append(s.externals[targetRelPath], &external)
}
return nil
}
// addExternalDir adds all externals in externalsDirAbsPath to s.
func (s *SourceState) addExternalDir(ctx context.Context, externalsDirAbsPath AbsPath) error {
walkFunc := func(ctx context.Context, externalAbsPath AbsPath, fileInfo fs.FileInfo, err error) error {
if externalAbsPath == externalsDirAbsPath {
return nilView on GitHub (pinned to f901167e46)
Solutions
- Use a simple forward-slash relative path like 'dir/name' for the external target path.
- Remove drive-letter or drive-relative components from the path.
- Test the config on the target OS with 'chezmoi apply --dry-run' to catch OS-specific path issues.
Example fix
// before (Windows drive-relative) ["C:tools\\tool"] type = "archive" url = "https://example.com/tool.zip" // after ["tools/tool"] type = "archive" url = "https://example.com/tool.zip"
Defensive patterns
Strategy: validation
Validate before calling
if _, err := filepath.Rel(".", path.Clean(target)); err != nil {
return fmt.Errorf("external path %q is not a valid relative path: %w", target, err)
} Prevention
- Use simple POSIX-style relative paths in externals
- Avoid drive-relative Windows paths like C:foo
- Test externals on each target OS before rolling out
When it happens
Trigger: filepath.Rel(".", externalPath) returning an error, e.g. paths that cannot be made relative on the current OS (volume-relative Windows paths like 'C:tool', malformed path syntax).
Common situations: Windows-specific path forms ('C:foo' drive-relative paths) in externals; externally generated external configs with OS-invalid path strings.
Related errors
- %s: empty path
- %s: %s: path is not relative
- %s: %w
- %s: cannot add chezmoi's config file to chezmoi, use a confi
- %s: cannot add chezmoi file to chezmoi (%s is protected)
AI-assisted analysis of twpayne/chezmoi@f901167e46 (2026-09-01).
Data as JSON: /api/errors/b929eae610b0e131.
Report an issue: GitHub.