twpayne/chezmoi · error
%s: %s: relative path outside target directory
Error message
%s: %s: relative path outside target directory
What it means
chezmoi rejects an external entry whose path escapes the destination (target) directory. filepath.Rel(".", externalPath) produced ".." or a "../"-prefixed path (including Windows backslash separator variants), meaning the external would be written outside chezmoi's target tree. This is a safety check against path traversal.
Source
Thrown at internal/chezmoi/sourcestate.go:1468
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 {
fileInfo, err = s.system.Stat(externalAbsPath)
}
switch {View on GitHub (pinned to f901167e46)
Solutions
- Remove ".." components from the external's target path so it stays under the destination directory
- Install the file outside the target using a separate mechanism (e.g. a script) instead of an external
- Check symlinked paths to ensure they do not resolve outside the target
Example fix
# before ["../bin/tool"] type = "file" url = "https://example.com/tool" # after [".local/bin/tool"] type = "file" url = "https://example.com/tool"
Defensive patterns
Strategy: validation
Validate before calling
rel, err := filepath.Rel(".", externalPath)
if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, "../") {
return fmt.Errorf("external path %q must stay inside the target directory", externalPath)
} Type guard
func insideTarget(p string) bool {
rel, err := filepath.Rel(".", filepath.Clean(p))
return err == nil && rel != "." && rel != ".." && !strings.HasPrefix(rel, "../")
} Prevention
- Never use ".." in external target paths
- Clean and relativize paths from templates before writing them into external definitions
When it happens
Trigger: An external entry target path containing ".." segments, or an absolute path that relativizes to something outside "." when processed in sourceStateExternal walk of internal/chezmoi/sourcestate.go.
Common situations: Typo'd target paths like "../scripts/tool.sh", symlinked or templated paths that resolve above the destination directory, users trying to install files into locations outside the target with an external.
Related errors
- %s: %s: empty relative path
- expected a string, got a %T
- %s: %w
- %s: empty path
- %s: %s: path is not relative
AI-assisted analysis of twpayne/chezmoi@f901167e46 (2026-09-01).
Data as JSON: /api/errors/29753f132542ac5b.
Report an issue: GitHub.