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 nil

View on GitHub (pinned to f901167e46)

Solutions

  1. Use a simple forward-slash relative path like 'dir/name' for the external target path.
  2. Remove drive-letter or drive-relative components from the path.
  3. 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

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


AI-assisted analysis of twpayne/chezmoi@f901167e46 (2026-09-01). Data as JSON: /api/errors/b929eae610b0e131. Report an issue: GitHub.