docker/compose · error

git subdirectory escapes base directory: %s

Error message

git subdirectory escapes base directory: %s

What it means

Final containment check of `validateGitSubDir`: even after cleaning and joining, the resolved target's path relative to the base escapes upward (`..` prefix), so the sub-directory would resolve outside the git checkout. Compose blocks it to keep includes confined to the cloned repo.

Source

Thrown at pkg/remote/git.go:166

		return fmt.Errorf("git subdirectory path traversal detected: %s", subDir)
	}

	if len(cleanSubDir) >= 2 && cleanSubDir[1] == ':' {
		return fmt.Errorf("git subdirectory must be relative, got: %s", subDir)
	}

	targetPath := filepath.Join(base, cleanSubDir)
	cleanBase := filepath.Clean(base)
	cleanTarget := filepath.Clean(targetPath)

	// Ensure the target starts with the base path
	relPath, err := filepath.Rel(cleanBase, cleanTarget)
	if err != nil {
		return fmt.Errorf("invalid git subdirectory path: %w", err)
	}

	if relPath == ".." || strings.HasPrefix(relPath, "../") || strings.HasPrefix(relPath, "..\\") {
		return fmt.Errorf("git subdirectory escapes base directory: %s", subDir)
	}

	return nil
}

func (g gitRemoteLoader) resolveGitRef(ctx context.Context, path string, ref *gitutil.GitRef) error {
	if !commitSHA.MatchString(ref.Ref) {
		cmd := exec.CommandContext(ctx, "git", "ls-remote", "--exit-code", ref.Remote, ref.Ref)
		cmd.Env = g.gitCommandEnv()
		out, err := cmd.CombinedOutput()
		if err != nil {
			if cmd.ProcessState.ExitCode() == 2 {
				return fmt.Errorf("repository does not contain ref %s, output: %q: %w", path, string(out), err)
			}
			return fmt.Errorf("failed to access repository at %s:\n %s", ref.Remote, out)
		}
		if len(out) < 40 {
			return fmt.Errorf("unexpected git command output: %q", string(out))

View on GitHub (pinned to ddc4b044b6)

Solutions

  1. Simplify the fragment to a direct sub-path within the repository
  2. Treat hitting this error as a red flag if the path came from an untrusted source (CI variable, remote config)

Example fix

# before
include:
  - path: git://github.com/org/repo.git#main:sub/../../escape

# after
include:
  - path: git://github.com/org/repo.git#main:sub
Defensive patterns

Strategy: validation

Validate before calling

# normalize then assert containment, mirroring the library check
python3 - <<'EOF'
import sys, posixpath
base = '/checkout/root'
for sub in sys.argv[1:]:
    rel = posixpath.relpath(posixpath.normpath(posixpath.join(base, sub)), posixpath.normpath(base))
    if rel == '..' or rel.startswith('../'):
        sys.exit(f"fragment escapes base: {sub}")
EOF

Prevention

When it happens

Trigger: Sub-paths like `a/../../escape` where intermediate components rejoin under base per cleaning rules but `filepath.Join` + `Rel` still yields an escaping relative path; symlink-free lexical traversal that the earlier prefix checks didn't catch.

Common situations: Maliciously crafted include fragments attempting cache escape; over-clever relative paths that try to reference repos cached next to each other.

Related errors


AI-assisted analysis of docker/compose@ddc4b044b6 (2026-08-15). Data as JSON: /api/errors/fb83cd7fad431e08. Report an issue: GitHub.