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

  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.