Related on the site: Gitignore Not Working? Here's the Real Fix.
TL;DR: to remove untracked files in git, run git clean -fd — but always preview with git clean -nd first, because clean deletes permanently and they never hit the trash. Use -f for files, -fd for files and directories, and -fdx if gitignored build output should go too. The single most common complaint — "git clean is not removing my untracked files" — almost always means the files live inside an untracked directory (add -d) or they are ignored files (add -x). Untracked clutter is the normal byproduct of experiments, builds and clone-adjacent scripts; this guide shows how to preview every deletion, remove files from one directory only, and which combinations to never run in a repo you care about.
What does "untracked files" mean in git status?
Untracked means git sees the file on disk but has never been told to track it — it is not in the index and has no commit history. git status groups everything into three buckets:
$ 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
That distinction matters because each bucket needs a different removal tool. Tracked-but-changed files are reverted with git restore or committed away — git clean will not touch them. Only the ?? lines are git clean territory. Ignored files (anything matched by .gitignore) are a hidden fourth bucket: they do not even show as ??, and git clean skips them unless you explicitly opt in with -x.
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 as covered in git undo last commit: keep the changes.
How do I remove untracked files in git?
The core command is git clean -f. Without -f git refuses to delete anything and just prints a warning — a deliberate safety rail. The full routine looks like this:
# 1. See exactly what will be deleted (dry run — deletes nothing)
git clean -nd
# Would remove:
# notes.txt
# build/
# scratch/
# 2. Confirm nothing precious is in the list, then delete for real
git clean -fd
Flag by flag:
-
-f/--force— required. Actually deletes untracked files. -
-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 run this 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.
A habit worth copying: treat git clean -nd like git diff — you look before you commit, you look before you clean.
Why is git clean not removing my untracked files?
Three real causes, in order of how often they bite:
1. The files are inside an untracked directory. With only -f, git removes loose untracked files but stops at directories, even reporting Would remove build/ without deleting it in a real run. Add -d:
git clean -fd
2. The files are gitignored. node_modules/, dist/, .venv/ — ignored paths are invisible to a plain clean. The dry run will not list them, and neither will the clean remove them. Opt in explicitly:
git clean -fdx # untracked + ignored files and directories
3. 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 --force twice (git clean -ffd), and prefer the first option.
If the dry run lists nothing but git status still shows ?? entries, you are probably in the wrong working tree — run git rev-parse --show-toplevel and check you are inside the repo you meant to clean.
How do I remove untracked files from a specific directory only?
Scope the clean by passing a path — everything else is left alone:
git clean -fd build/ # only inside build/
git clean -fd src/generated # one specific tree
This is the answer to "I want to remove untracked files and folders in build/ but keep my scratch notes in the repo root". The path is relative to your current directory, so running from the repo root scopes to the whole repo; running from a subdirectory scopes to that subtree.
How do I remove untracked files without deleting them?
When the dry run shows files you might want later, do not gamble — preserve first, clean second:
# Stash untracked files (including ignored ones with -a) without deleting
git stash push --include-untracked
git clean -fd # tree is clean
git stash pop # bring them back when needed
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. For a preview you can keep, git clean -nd > clean-plan.txt gives you the exact list before committing to anything. There is no undo after git clean -f: deleted means gone.
git clean vs git rm vs git restore: which one when?
| 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.
What should you never run git clean on?
Two habits to avoid outright:
-
Never run
git clean -fdxblindly in a monorepo or workspace. It deletes every ignored directory — that is 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 (git clean -nd) and make it a two-command ritual, preview then delete.
Also worth knowing: cleaning untracked files right before a [sync a fork with upstream] pull keeps the merge surface small — a tidy tree is the cheapest conflict insurance there is: sync a fork with upstream, step by step.
The safe git clean 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.
This ships daily at mrsaynothing.dev — the full archive, every piece in 19 languages, zero missed days. New posts land in the newsletter the moment they ship: join it here. Code at GitHub.
Top comments (1)
The -n dry-run is the whole safety story here: git clean is the one command in the family that deletes files git never tracked, so once they go, no reflog brings them back. Has clean ever eaten something you meant to keep, or do you still dry-run it every time like the rest of us?