Clean untracked files from a git repo
Delete files that git doesn't know about (build artifacts, scratch files, `.DS_Store`, `node_modules`) — the destructive sibling of `git restore`. ALWAYS dry-run first; there is no reflog recovery.
How to clean untracked files from a git repo in each shell
git clean -fdn`-n` (or `--dry-run`) is the SAFE form — lists what WOULD be deleted, deletes nothing. Drop the `n` for the real run: `git clean -fd`. `-f` (force) is REQUIRED by default; without it git refuses to act (configurable via `clean.requireForce=false`, not recommended). `-d` adds untracked DIRECTORIES (bare `-f` skips them).
git clean -fdngit clean -fdngit clean -fdnSame git binary. After dry-run, `git clean -fd` to commit. pwsh does NOT pre-expand the `-fdn` short-flag cluster — git parses it natively. Check `$LASTEXITCODE` (NOT `$?`) — non-zero means "not a git repo" or "refused for lack of force".
git clean -fdnSame git binary. cmd has no recovery story for unintended deletions (Recycle Bin is bypassed by `git clean`), so the dry-run discipline matters MORE on Windows. After dry-run, `git clean -fd`. For "blow away build cache, keep my WIP source" use `git clean -fdX` (capital X — only ignored files).
Equivalents listed for Bash, Zsh, Fish, PowerShell, cmd.exe.
Gotchas & notes
- **`-f` is mandatory by default, on purpose.** `git clean` refuses to run without `-f` (you get `fatal: clean.requireForce defaults to true`) — the friction is intentional. Setting `git config --global clean.requireForce false` removes the friction; do not. The combination `-fd` (force + directories) is what most users actually want — without `-d`, bare `-f` deletes untracked FILES at the top level but leaves untracked DIRECTORIES alone (so `new-folder/` survives). Always dry-run first: `git clean -fdn` lists what WOULD be deleted. The `-i` interactive mode (`git clean -id`) prompts before each batch — slower but the safest middle ground when you're unsure.
- **`-x` vs `-X` is the gotcha most likely to lose work.** `-x` (lowercase) means "ALSO remove ignored files" — nukes `node_modules`, `__pycache__`, `.venv`, `dist/`, `build/`, AND your untracked source. `-X` (UPPERCASE) means "ONLY remove ignored files" — keeps your new untracked source, removes build cache only. The "blow away build artifacts before reinstall" idiom is `git clean -fdX`, NOT `-fdx`. Confusing the two is the canonical lost-work story: lowercase x sounds like "extreme" so people use it; capital X is the actual "extreme but safe" form.
- **No reflog, no Recycle Bin — recovery means filesystem snapshots.** `git reset --hard` is recoverable from reflog (90 days); `git clean` is NOT — the files were never tracked, never hashed, never in `.git/objects/`. Recovery options: (1) APFS / ZFS / Btrfs / Time Machine / Windows VSS snapshots if enabled; (2) editor-level undo for files you had open; (3) `extundelete` / `photorec` on the underlying filesystem if you act before the blocks are reused (Linux only, ext family). On macOS, `rm`-via-Finder uses Trash; `git clean` bypasses it. On Linux, GNU `rm` bypasses `~/.local/share/Trash/`. The `trash-cli` package (`brew install trash`, `apt install trash-cli`) provides a recoverable alternative, but `git clean` doesn't use it.
- **`--exclude=<pattern>` whitelists files to keep.** Pattern syntax matches `.gitignore`: `git clean -fdx --exclude=.env --exclude=.env.local` removes everything ignored EXCEPT those two files. Multiple `--exclude` flags accumulate. Useful when CI policy bans committing `.env` but you don't want the local `.env` deleted on cleanup. Alternative: temporarily un-ignore via `git ls-files -o -i --exclude-from=<custom-rules>` then `xargs -0 rm`. The exclude pattern is path-relative to the repo root, not CWD — `--exclude=src/.env` matches only that location.
- **Scope: CWD vs repo root.** `git clean -fd` operates on the CWD subtree by default (same as `git restore .`). To clean the entire repo regardless of CWD, prefix `cd $(git rev-parse --show-toplevel)` or pass the path explicitly: `git clean -fd .git/../` is awkward — `cd` then run is clearer. Submodules are NOT cleaned by default (you'd be deleting another repo's files unannounced); add `--recurse-submodules` to descend into them. CAVEAT: that flag interacts oddly with `submodule.recurse=true` — explicit flag wins.
Related commands
Related tasks
- Discard uncommitted changes in a git repo— Throw away local edits and reset the working tree to the last commit — the "I messed up, start over" gesture. Three subtly-different idioms (`git restore`, `git checkout --`, `git reset --hard`) cover three different scopes; picking the wrong one either keeps junk or nukes work you wanted to keep.
- List modified files in a git repo— Print the paths of files with uncommitted changes (staged or unstaged) — for pre-commit hooks, CI lint-only-changed gates, and "what am I about to commit".
- Show git commit history as a graph— Render the commit DAG as an ASCII graph so branch topology — merges, forks, fast-forwards, and orphan tips — is visible at a glance, instead of the default linear `git log` that hides every parallel branch.