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
- Set a non-empty type and url (and exact target path) for the external entry in the .chezmoiexternal file
- If the path comes from a template, ensure the template variable is defined and non-empty
- 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
- Always set a concrete target path key for every external entry
- Never leave path/template fields blank in .chezmoiexternal files
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
- expected a string, got a %T
- %s: %s: relative path outside target directory
- encryption not configured
- %s: unknown compression format
- expected a []string, got a %T
AI-assisted analysis of twpayne/chezmoi@f901167e46 (2026-09-01).
Data as JSON: /api/errors/2e7e470b679a82e0.
Report an issue: GitHub.