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)
- 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"
- 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
- 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
- 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
- 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.
- 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
- 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.
- 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.