twpayne/chezmoi · error

%s: %s: empty relative path

Error message

%s: %s: empty relative path

What it means

chezmoi refuses to process an external entry whose resolved path is not a meaningful relative path under the target directory. filepath.Rel(".", externalPath) returned ".", meaning externalPath is empty or resolves to the current directory, so there is no valid target relative path for the external. This guards against malformed external definitions writing to an undefined location.

Source

Thrown at internal/chezmoi/sourcestate.go:1465

	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
		}
		if err == nil && fileInfo.Mode().Type() == fs.ModeSymlink {

View on GitHub (pinned to f901167e46)

Solutions

  1. Set a non-empty type and url (and exact target path) for the external entry in the .chezmoiexternal file
  2. If the path comes from a template, ensure the template variable is defined and non-empty
  3. Run chezmoi doctor / validate the external file format for your chezmoi version

Example fix

# before
[".config/tool"]
  type = "archive"
  url = "https://example.com/tool.tar.gz"
  # path component empty -> externalPath resolves to "."
# after
[".config/tool"]
  type = "archive"
  url = "https://example.com/tool.tar.gz"
  exact = true
Defensive patterns

Strategy: validation

Validate before calling

if externalPath == "" || externalPath == "." {
    return fmt.Errorf("external %q: target path must be a non-empty relative path", name)
}

Type guard

func hasValidRelPath(p string) bool {
    if p == "" || p == "." { return false }
    return !filepath.IsAbs(p)
}

Prevention

When it happens

Trigger: An external entry in .chezmoiexternal.toml (or an externals directory file) whose URL/path-derived externalPath is empty or equals "." when evaluated relative to ".".

Common situations: A hand-edited .chezmoiexternal file with a missing or blank path/URL component, template evaluation producing an empty string for the path, or copy-paste mistakes leaving the type/url key populated but the target path empty.

Related errors


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