KhueApps
Home/DevOps/Fix 'fatal: unable to checkout working tree' from invalid paths in Git

Fix 'fatal: unable to checkout working tree' from invalid paths in Git

Last updated: October 07, 2025

Overview

Git can fail to check out a branch with:

fatal: unable to checkout working tree

This often happens when paths in the commit are invalid for your OS (common on Windows). Causes include:

  • Invalid characters: < > : " | ? * or backslashes in filenames (Windows)
  • Trailing spaces or dots in names (e.g., "file .txt", "name.")
  • Reserved device names: CON, PRN, AUX, NUL, COM1–COM9, LPT1–LPT9
  • Case-only conflicts on case-insensitive filesystems (Windows, macOS default)
  • Path length limits (Windows without long paths enabled)

Quickstart (most common fixes)

  1. List offending paths in the target branch:
# Show every path in the branch you want to checkout
BRANCH=main
git ls-tree -r --name-only "$BRANCH"
  1. On Windows, enable long paths and safer defaults:
# Requires admin PowerShell
reg add HKLM\SYSTEM\CurrentControlSet\Control\FileSystem /v LongPathsEnabled /t REG_DWORD /d 1 /f
# Restart after this change

# Let Git use long paths
git config --system core.longpaths true
# Prevent adding invalid NTFS names in the future
git config --global core.protectNTFS true
  1. Rename or remove invalid paths on a machine that can read them (e.g., Linux/WSL), then commit and push:
git checkout -b fix-invalid-paths
# Example renames
git mv "bad:name.txt" "bad-name.txt"
git mv "dir/CON" "dir/CON_"
# Case-only rename needs an intermediate name on Windows/macOS:
# git mv File.txt file.tmp && git mv file.tmp file.txt

git commit -m "Rename invalid paths for Windows compatibility"
git push -u origin fix-invalid-paths
  1. If you cannot change history, use a workaround:
  • Sparse-checkout to exclude problematic directories
  • Work inside WSL2/Linux container where those paths are valid

Minimal working example (reproduce and fix)

# On Linux or WSL
mkdir demo-invalid-paths && cd demo-invalid-paths
git init
printf 'demo\n' > 'a:b.txt'     # Colon is invalid on Windows
printf 'x\n'   > 'dir/CON'       # Reserved name on Windows

git add -A && git commit -m "add invalid Windows paths"

# Simulate Windows checkout failure by cloning into a Windows drive
# (Do this on a Windows host):
# git clone <repo-url>
# -> Expect errors like:
#   error: invalid path 'a:b.txt'
#   error: unable to create file dir/CON: Invalid argument

# Fix by renaming (on Linux/WSL), then push
git mv 'a:b.txt' 'a-b.txt'
git mv 'dir/CON' 'dir/CON_'
git commit -m "Rename invalid filenames"

Diagnose invalid paths

  • Inspect checkout errors: they usually print the offending path.
  • Enumerate all paths in the branch and scan for Windows-invalid patterns.

Bash (cross-platform scan with basic checks):

# Run from repo root; scans HEAD by default
pat='[<>:"\|?*]'
reserved='^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|$)'

git ls-tree -r -z --name-only HEAD | \
  tr '\0' '\n' | while IFS= read -r p; do
    base="${p##*/}"
    if printf '%s' "$p" | grep -Eq "$pat"; then echo "INVALID_CHARS: $p"; fi
    if printf '%s' "$base" | grep -Eq "$reserved"; then echo "RESERVED_NAME: $p"; fi
    if printf '%s' "$base" | grep -Eq '\\.$|\\s$'; then echo "TRAILING_DOT_OR_SPACE: $p"; fi
    # Approximate path length check (260 typical limit when long paths disabled)
    if [ ${#p} -gt 240 ]; then echo "POSSIBLE_LONG_PATH: $p"; fi
  done

PowerShell (Windows):

$branch = "HEAD"
$invalidChars = '[<>:"\|?*]'
$reserved = '^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|$)'

(git ls-tree -r --name-only $branch) | ForEach-Object {
  $p = $_
  $base = [System.IO.Path]::GetFileName($p)
  if ($p -match $invalidChars) { "INVALID_CHARS: $p" }
  if ($base -match $reserved) { "RESERVED_NAME: $p" }
  if ($base -match '(\.|\s)$') { "TRAILING_DOT_OR_SPACE: $p" }
  if ($p.Length -gt 240) { "POSSIBLE_LONG_PATH: $p" }
}

Case-only conflicts:

# Detect pairs that differ only by case
lc=$(git ls-tree -r --name-only HEAD | awk '{print tolower($0)}' | sort)
orig=$(git ls-tree -r --name-only HEAD | sort)
# Compare or use a script to find duplicates among $lc vs $orig counts

Fix strategies

  1. Rename and commit (forward-only)
  • Best when you control the repository and history can keep the invalid names in older commits.
  • Use git mv for each offending path. For case-only rename on case-insensitive FS, use an intermediate name.
  1. Rewrite history (all commits)
  • Use when invalid paths appear throughout history and break clones.
  • Recommended tool: git-filter-repo (fast, safer than filter-branch).
# Install git-filter-repo first (outside the scope here)
# Example: rewrite all history, renaming bad:name.txt to bad-name.txt
git filter-repo --path-rename 'bad:name.txt=bad-name.txt'

# Drop an entire problematic directory across history
# git filter-repo --path dir/CON --invert-paths

After rewriting history:

  • Force-push the rewritten branches
  • Ask collaborators to re-clone or hard-reset to the new history
  1. Workarounds if you cannot change the repo
  • Sparse-checkout to exclude bad paths:
git clone <url> repo && cd repo
git sparse-checkout init --cone
git sparse-checkout set src docs  # exclude problematic dirs by omission
  • Use WSL2/Linux container and keep the working tree on a Linux filesystem.
  1. Windows configuration tips
  • Enable long paths (system + Git): see Quickstart.
  • Keep defaults: core.protectNTFS=true, core.ignorecase=true on Windows.

Common pitfalls

  • Case-only renames on Windows/macOS: must use a two-step rename.
  • Rewriting history requires force-push; CI caches, forks, and open PRs will need updates.
  • Enabling LongPathsEnabled requires a reboot and Git 2.10+ with core.longpaths.
  • core.longpaths does not bypass invalid characters or reserved names.
  • Trailing spaces/dots often come from auto-generated artifacts—add .gitignore rules.

Performance notes

  • Scanning paths with git ls-tree is linear in files; fast on most repos.
  • git-filter-repo is significantly faster than filter-branch/BFG for complex rewrites, but still scales with repository size.
  • On Windows, antivirus scanning can slow checkouts; consider excluding the working directory during large operations.

Prevention

  • Add a pre-commit hook to block invalid filenames:
#!/usr/bin/env bash
pat='[<>:"\|?*]'
reserved='^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.|$)'
fail=0
for p in $(git diff --cached --name-only); do
  b="${p##*/}"
  if [[ $p =~ $pat || $b =~ $reserved || $b =~ [[:space:].]$ ]]; then
    echo "Invalid path detected: $p"; fail=1
  fi
  if [ ${#p} -gt 240 ]; then echo "Too long: $p"; fail=1; fi
done
[ $fail -eq 0 ]
  • On servers, use a pre-receive hook with similar checks.

Tiny FAQ

  • Will core.longpaths fix invalid characters? No; it only helps with path length.
  • Why does it work on Linux but not Windows? Filesystem rules differ; Windows forbids certain names and characters.
  • Do I need to re-clone after history rewrite? Yes, or hard-reset to the new refs.
  • Can I avoid editing history? Yes: rename forward-only or use sparse-checkout/WSL as workarounds.

Series: Git

DevOps