twpayne/chezmoi · error

: : 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.

Solutions

  1. Remove ".." components from the external's target path so it stays under the destination directory
  2. Install the file outside the target using a separate mechanism (e.g. a script) instead of an external
  3. 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

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


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

Appendix: 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)