Git remove untracked files: the safe git clean ritual
Preview with -nd, delete with -fd, reach ignored files with -x — the three zones of your working tree, the three causes of 'clean removes nothing', and stash -u as the undo.
■ space · prose lane
git clean -nd previews, git clean -fd deletes — clean is permanent; the trash is never involved.
Clean removes nothing? The files sit in an untracked directory (add -d) or are ignored (add -x).
Want them recoverable? git stash -u before cleaning — there is no undo after -f.
THE BLAST RADIUS
Every file in your tree lives in one of three zones — and every clean flag reaches into exactly one ring:
clean never touches them. Edits: git restore. Removal: git rm.
reached by clean -f, and by -d inside untracked directories
reached only with -x (or -X for ignored-only)
the fourth bucket is the trap: ignored files don't even show as ?? — a plain clean silently skips them, which is why it "removes nothing"
WHAT "UNTRACKED" MEANS
Untracked means git sees the file on disk but has never been told to track it — not in the index, no commit history. git status groups everything into buckets, and each needs a different removal tool:
$ git status --short
M src/app.ts # modified: tracked, changed
?? notes.txt # untracked: new file git doesn't know
?? build/ # untracked directory: entirely new to git
Tracked-but-changed files are reverted with git restore or committed away — git clean will not touch them. Only the ?? lines are clean territory. If your real problem is a tracked file that should never have been committed, cleaning is the wrong tool — that is a git rm --cached job, or a reset of the last commit.
THE RITUAL
# 1. see exactly what will be deleted (dry run — deletes nothing)
git clean -nd
# Would remove:
# notes.txt
# build/
# 2. confirm nothing precious is in the list, then delete for real
git clean -fd
verify: git clean -nd → "Would remove notes.txt build/" · after -fd: git status --short lists no ?? lines
A habit worth copying: treat git clean -nd like git diff — you look before you commit, you look before you clean. Flag by flag:
-f/--force— required. Actually deletes untracked files. Without it git refuses and prints a warning — a deliberate safety rail.-d— recurse into untracked directories. Plain-fonly removes untracked files at the top level and reports the directories it refused to touch.-n/--dry-run— show what would be removed. Always first.-x— also delete ignored files (node_modules, build output,.env).-X— delete only ignored files, keeping untracked-but-not-ignored ones.-i— interactive mode; useful when the dry-run list is long.-e <path>— exclude — "clean everything except notes.txt" isgit clean -fd -e notes.txt.
"CLEAN REMOVES NOTHING" — THE THREE CAUSES
- The files are inside an untracked directory. With only
-f, git removes loose files but stops at directories — even reportingWould remove build/without deleting it in a real run. Add-d:git clean -fd. - The files are gitignored.
node_modules/,dist/,.venv/— ignored paths are invisible to a plain clean; the dry run won't list them either. Opt in explicitly:git clean -fdx. - A nested git repository or submodule is in the way. Git never deletes another repo's contents from the outside. Remove the submodule properly or pass
--forcetwice (git clean -ffd) — prefer the first.
If the dry run lists nothing but git status still shows ?? entries, you are probably in the wrong working tree — git rev-parse --show-toplevel and check you are inside the repo you meant to clean.
ONE DIRECTORY ONLY
Scope the clean by passing a path — everything else is left alone. The path is relative to your current directory, so running from the repo root scopes to the whole repo; from a subdirectory, to that subtree:
git clean -fd build/ # only inside build/
git clean -fd src/generated # one specific tree
REMOVE WITHOUT DELETING
When the dry run shows files you might want later, do not gamble — preserve first, clean second. git stash -u moves untracked files out of the tree but keeps them recoverable — that is the "remove without deleting" semantics people are actually looking for:
# stash untracked files (including ignored with -a) without deleting
git stash push --include-untracked
git clean -fd # tree is clean
git stash pop # bring them back when needed
verify: git stash list → stash@{0} holds what git stash -u lifted
For a preview you can keep: git clean -nd > clean-plan.txt. There is no undo after git clean -f: deleted means gone — the trash is never involved.
CLEAN VS RM VS RESTORE
| Command | Touches | Removes from disk | Use when |
|---|---|---|---|
git clean -fd | Untracked files/dirs | Yes | Delete files git has never tracked |
git clean -fdx | Untracked + ignored | Yes | Full reset including node_modules, build output |
git rm <file> | Tracked files | Yes (staged) | Delete a file and record the deletion in git |
git rm --cached <file> | Tracked files | No | Stop tracking a file, keep it on disk |
git restore <file> | Tracked files | No | Discard local edits, keep the file |
The one-line rule: clean manages what git doesn't know about; rm and restore manage what it does. Mixing them up is how people lose work — running git clean -fdx while believing it behaves like git restore.
08WHAT NEVER TO RUN
- Never run
git clean -fdxblindly in a monorepo or workspace. It deletes every ignored directory — everynode_modules, every virtualenv, every local.envin the tree. Regaining them can mean an hour of reinstallation, and a deleted.envmay not be recoverable at all. - Never alias clean with force baked in.
git config alias.wipe "clean -fd"feels efficient until you typo the path. Keep the dry run one keystroke away and make it a two-command ritual: preview, then delete.
The whole workflow, condensed:
git status --short # what's in the tree?
git clean -nd # preview: what WOULD go?
git clean -fd # delete untracked files + directories
git clean -fdX # (optional) clear only ignored build output
git status --short # confirm: working tree clean
Preview, delete, verify — thirty seconds, zero regret, and git status finally reads clean again. The cousin of this problem: .gitignore rules that seem to do nothing — if your rules are being ignored, it is usually tracked files rather than syntax. And a tidy tree right before a fork sync keeps the merge surface small — the cheapest conflict insurance there is.
git status --short output above is the shape every repo actually prints.
end of the clean page · nothing fetched · everything local