DH
12 min read

Git Remove Untracked Files: The Safe Way to Purge Build Artifacts and Junk

Master git clean with preview mode, selective filtering, and safety guards. Remove build artifacts without losing uncommitted work.

automationnodejs

You've got a working tree full of node_modules, stray .env.local copies, compiled .pyc files, and a dist/ folder that a build script left three weeks ago. git status is a wall of red. You want it gone—but not the one file you forgot to commit and desperately need.

Here's the direct answer, then we'll slow down and do it properly.

git clean -n

That's your preview. It shows exactly what would be deleted without deleting anything. Once you've confirmed the list is safe:

git clean -f

That removes untracked files (not directories, not ignored files — we'll get to those). Everything past this point is about controlling the blast radius, because git clean is one of the few Git commands with no undo.

What "untracked" actually means

Git divides your working tree into a few buckets: tracked files (already committed, or staged via git add), untracked files (new files Git has never seen), and ignored files (untracked files that match a pattern in .gitignore).

git clean operates exclusively on untracked files—files not currently tracked in Git's index. As of Git's official documentation, git clean targets files based on their index state at the time the command runs, not their commit history. If a file has been committed but its tracking is later removed (for example, via git rm --cached, which stages a deletion), it becomes untracked and falls within git clean's scope—even though it exists in the repository's history. That's why previewing with git clean -n before running the actual command is essential: git clean will remove it.

Files staged for deletion or otherwise removed from the index are indistinguishable from newly created untracked files. To safely remove files from version control without deleting them from disk, use git rm --cached (which removes tracking but keeps the file on disk); to remove them from disk as well, use git rm. To restore a previously committed file after it's been deleted from the working tree, use git checkout or git restore, which read from history.

This distinction matters because the anxiety around "will this delete my work" is usually misplaced in one direction and correctly placed in another. Your committed code is safe from git clean no matter what flags you throw at it. Your uncommitted, untracked work — that new module you haven't run git add on yet — is exactly what's in the blast radius. git clean is a working-tree hygiene tool, but it is fully destructive within its scope.

The safety guardrail: why git clean alone does nothing

Run bare git clean with no flags and you'll get a fatal error instead of a deletion:

fatal: clean.requireForce defaults to true and neither -i, -n, nor -f given; refusing to clean

This is a deliberate safety mechanism. Git includes a configuration setting, clean.requireForce, that is set to require explicit confirmation before any files are deleted. You satisfy that requirement one of three ways: pass -f (force, actually delete), pass -n (dry run, show what would happen), or pass -i (interactive, walk through choices one at a time).

You can flip clean.requireForce to false in your Git config and let bare git clean delete without confirmation. I'd advise against it. The entire value of this guardrail is that it forces a moment of friction before an irreversible action. Keep the friction — it's saved people from themselves more than it's ever slowed anyone down.

Dry run first: -n / --dry-run

This is the step most people skip and the one that matters most. Before you force anything, preview it:

git clean -n

or the long form:

git clean --dry-run

Output looks like:

Would remove build/output.log
Would remove tmp/cache.json
Would remove scratch.py

No deletion happens. Git just tells you what it would do if you added -f. Read that list line by line. This is the point where you catch the config file you forgot to add to .gitignore, the local .env you meant to keep, or the debug script you're still using.

Treat -n as a prefix you attach to every git clean variant below before you run the real thing — including the interactive mode. The workflow I'd recommend: git clean -n (or the -d/-x variant you're considering) → read the output → swap -n for -f → run it for real. Never skip straight to -f on a repo you didn't mess up yourself.

Files-only vs directories: the -d flag

By default, git clean -f removes untracked files but leaves untracked directories alone — even empty ones, even ones full of untracked files. If your build output landed in a whole new directory (dist/, .next/, __pycache__/), plain -f won't touch the directory itself. This is standard Git behaviour.

To recurse into and remove untracked directories too, add -d:

git clean -fd

Preview first:

git clean -nd

This is the flag that catches people off guard. Some assume -f alone clears everything and are confused when a directory survives. Others add -d reflexively and are surprised at how much more disappears — because -d recurses into any untracked directory and removes its entire contents. If there's a directory in your tree that's untracked as a whole (Git never saw it), -d treats the whole thing as fair game, contents included.

Ignored files: -x and -X

Neither -f nor -fd touches files matched by .gitignore. That's deliberate — ignored files are usually things you want kept around locally (editor configs, local env files, cached dependencies) even though you never commit them. But sometimes you specifically want to nuke ignored build artifacts, and that's what -x and -X are for.

-x includes ignored files in the cleanup, in addition to regular untracked files:

git clean -fdx

This is the "burn it down to exactly what's in the last commit" command. It removes untracked files, untracked directories, and anything matched by .gitignorenode_modules, virtual environments, .next, compiled artifacts, log files, all of it.

-X (capital) is more surgical: it removes only ignored files, leaving other untracked files alone:

git clean -fdX

Use -X when you want to clear caches and build output specifically but still want to review or keep other untracked files that aren't in .gitignore.

Combined flags: -fd and -fdx in practice

The common invocations:

CommandRemovesLeaves alone
git clean -fUntracked files in the current directoryUntracked directories, ignored files
git clean -fdUntracked files and directories (recursively)Ignored files
git clean -fdxUntracked files, directories, and ignored filesNothing untracked survives
git clean -fdXOnly ignored files and directoriesNon-ignored untracked files

git clean -fdx is the nuclear option — it produces a working tree that matches what a fresh git clone would give you, minus your commits staying intact. I reach for it when a build has gone sideways and I want to rule out "stale artifact" as the cause, or before packaging a release where I need certainty that nothing untracked leaked in. Run it with -n first, because it deletes things like local environment files and IDE-generated caches that aren't always safely regenerable.

Interactive mode: -i

If -n feels too passive and -f feels too aggressive, -i is the middle ground:

git clean -id

This drops you into an interactive prompt with options like:

*** Commands ***
1: clean 2: filter by pattern
3: select by numbers 4: ask each
5: quit 6: help

You can filter which files are candidates by pattern, select specific ones by number, or step through and confirm each file individually. It's slower than -f, but for a working tree with a mix of things you want gone and things you're uncertain about, it beats guessing.

Excluding specific files: -e / --exclude

Sometimes you want to clean broadly but protect one or two specific files or patterns without permanently adding them to .gitignore. That's what -e (or --exclude) does:

git clean -fd -e "*.local.env"

This runs the standard clean but skips anything matching *.local.env. You can pass -e multiple times for multiple patterns:

git clean -fdx -e "config/local.json" -e "*.pem"

This is particularly useful for the -x case, where you're clearing ignored files broadly but there's a specific ignored file — a local secrets file, a manually placed SSL cert — that you don't want swept up.

Caveat: untracked directories hide their contents from -f

If you have an untracked directory containing files you meant to keep — say, a migrations/ folder you meant to git add but forgot — plain git clean -f (without -d) won't touch that directory or anything in it. Run git clean -fd, though, and the entire directory, including any files you meant to keep, disappears in one pass.

The lesson: always run -n with the same flags you intend to run for real. Previewing with git clean -n and then executing git clean -fd isn't a true preview — directories only show up in the dry-run output once you include -d:

git clean -nd # preview for -fd
git clean -fd # execute

Troubleshooting: "git clean isn't removing anything"

A few common causes:

  • You're missing -f or -i. Bare git clean or git clean -n alone will never delete — that's dry-run or refusal, by design.
  • The files are ignored, and you didn't pass -x or -X. Check .gitignore — if the file matches a pattern there, standard git clean -fd skips it silently.
  • The files are actually tracked. If a file was ever committed, git clean won't remove it. Check with git status — tracked-but-modified files show differently than untracked ones. Use git checkout or git reset for those.
  • You're not in the directory you think you are. git clean operates relative to your current working directory. Either cd to the repo root first or pass the path explicitly: git clean -fd ./some/subdir.

git clean vs reset, checkout, and stash

These four commands all "clean up" a working tree, but they operate on entirely different sets of files:

CommandAffectsTypical use
git cleanUntracked files/directories onlyRemove build artifacts, temp files, generated output
git checkout -- <path>Tracked files with uncommitted modificationsDiscard local edits to a specific tracked file
git reset --hardTracked files; moves branch pointer; discards staged/unstaged changesDiscard all local commits/changes and reset tracked files to a specific commit
git stashTracked and optionally untracked changes; saves rather than deletesTemporarily shelve work to switch context, recoverable later

The key distinction: git reset --hard and git checkout operate on tracked files and are generally recoverable via reflog. git clean operates on untracked files, and once they're gone, there's no reflog, no stash, no commit history to fall back on. git stash is the outlier — it's non-destructive by default, moving changes into a stash entry you can pop back later.

For a fully pristine working tree — matching what a fresh clone would give you — the common combo is:

git reset --hard
git clean -fdx

git reset --hard snaps all tracked files back to the last commit. git clean -fdx then removes everything untracked and ignored. Run the git clean half with -n first — git reset --hard is often recoverable via reflog, but git clean is not.

FAQ

Does git clean delete files listed in .gitignore? No, not by default. Plain git clean -f or -fd skips ignored files. You need -x (remove ignored files along with everything else untracked) or -X (remove only ignored files) to touch them.

Can I undo git clean after running it? No. Unlike git reset --hard, which you can often recover from via git reflog, git clean permanently deletes files that were never committed. There's no built-in undo. This is why the dry-run step is non-optional — it's your only safety net.

Why did git clean -fd remove more than I expected? Most likely an untracked directory contained files you cared about, and since the whole directory was untracked, -d removed it and everything inside in one pass. Always run git clean -nd before git clean -fd to see the full list first.

What's the difference between git clean -fdx and a fresh git clone? Functionally very similar for the working tree: both leave you with only tracked files at their last-committed state, nothing untracked, nothing ignored. The difference is that git clean -fdx doesn't touch your commit history, branches, or remotes — it only clears the working tree. A fresh clone also gives you a clean .git directory, whereas your existing repo's .git metadata (reflog, stash entries, local branches) remains untouched.

Why does git clean need -f when I'm the only one who'd run it? Because the cost of an accidental deletion is high and irreversible, and the cost of typing two extra characters is negligible. The clean.requireForce guardrail exists specifically to insert a pause before a command that can't be undone — it's friction as a safety mechanism.

Damian Hodgkiss

Damian Hodgkiss

Senior Staff Engineer at Sumo Group, leading development of AppSumo marketplace. Technical solopreneur with 25+ years of experience building SaaS products.

Creating Freedom

Join me on the journey from engineer to solopreneur. Learn how to build profitable SaaS products while keeping your technical edge.

    Proven strategies

    Learn the counterintuitive ways to find and validate SaaS ideas

    Technical insights

    From choosing tech stacks to building your MVP efficiently

    Founder mindset

    Transform from engineer to entrepreneur with practical steps