git clean

git clean deletes files from your working directory that Git isn’t tracking — stray build output, editor swap files, log files, or anything else that has never been git add-ed. It’s the tool you reach for when git status shows a long list of untracked clutter and you just want a pristine working tree. Unlike git reset or git restore, which operate on files Git already knows about, git clean only ever touches untracked files — but because it deletes them from disk rather than from Git’s history, its effects are usually permanent and unrecoverable.

Overview / How it works

To understand git clean, it helps to remember how Git classifies every file in your working directory. Git divides the world into three buckets: tracked files (already committed, or staged in the index), ignored files (matched by a pattern in .gitignore), and untracked files (everything else — new files Git has never seen and that aren’t ignored). git status lists untracked files under "Untracked files", but it never deletes anything; it just reports. git clean is the command that actually removes those untracked files from disk.

This matters because none of Git’s undo machinery — commits, the reflog, the object database — ever stores untracked files. A commit is a snapshot built from whatever is in the index (the staging area) at the time you ran git commit; a file that was never staged was never turned into a blob object, never referenced by a tree, and therefore never became part of any commit. If you delete an untracked file with git clean, there is no blob to recover it from — it behaves like rm, not like git reset. This is the single most important thing to internalize before you ever run it with -f.

Because deleting files is dangerous, Git ships with a safety valve: the clean.requireForce configuration setting defaults to true, which means a bare git clean with no flags refuses to do anything and just prints a warning. You must explicitly pass -f (or --force) to actually delete files, or use -n to preview what would happen. This default is deliberate — treat it as a feature, not friction.

By default, git clean leaves two categories of files alone: untracked directories (unless you pass -d) and ignored files (unless you pass -x). This means running git clean -f on its own is conservative — it removes loose untracked files in the current directory tree but won’t touch your node_modules/ or build/ folders, and won’t delete things your .gitignore is protecting, like a local .env file, unless you specifically ask it to.

Syntax

git clean [-n] [-f] [-d] [-x | -X] [-i] [-e <pattern>] [--] [<path>...]
Flag Meaning
-n, --dry-run Show what would be deleted, without deleting anything. Always run this first.
-f, --force Actually perform the deletion. Required because clean.requireForce defaults to true.
-d Also remove untracked directories, not just untracked files.
-x Also remove files ignored by .gitignore. Dangerous — this can delete build caches, downloaded dependencies, or local config you meant to keep.
-X Remove only ignored files, leaving ordinary untracked files alone. Handy for "reset my build artifacts" without touching new work-in-progress files.
-i, --interactive Prompt before deleting, letting you pick files one at a time or by pattern.
-e <pattern> Exclude files matching <pattern> from removal, in addition to .gitignore rules.
<path>... Limit the operation to specific files or directories instead of the whole working tree.

Examples

Example 1: Previewing and removing stray untracked files

git status
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	debug.log
	notes.txt

nothing added to commit but untracked files present (use "git add" to track)
git clean -n
Would remove debug.log
Would remove notes.txt
git clean -f
Removing debug.log
Removing notes.txt

The dry run with -n lists exactly what would be deleted — two untracked files sitting in the working directory. Once satisfied nothing important is listed, running git clean -f deletes them for real and prints one "Removing" line per file. Note that git status is now clean, but this had nothing to do with commits, staging, or history — the files are simply gone from disk.

Example 2: Removing untracked directories with -d

mkdir build
echo "compiled output" > build/app.o
git clean -n -d
Would remove build/
git clean -f -d
Removing build/

Without -d, git clean -f would silently skip the whole build/ directory because it treats untracked directories differently from untracked files. Adding -d tells Git to also recurse into and remove untracked directories (and everything inside them), which is what you usually want when clearing out a build folder that isn’t in .gitignore.

Example 3: Including ignored files with -x

git clean -n -d -x
Would remove build/
Would remove debug.log
Would remove node_modules/

Adding -x extends the search to files and directories matched by .gitignore as well — here it picks up node_modules/, which is normally protected because it’s listed in .gitignore. This is powerful for getting back to a truly pristine, "just cloned" working tree, but it’s also the most dangerous form of git clean: it can delete downloaded dependencies, local .env files, and IDE settings that you never intended to lose. Always pair -x with -n first.

Example 4: Interactive cleaning

git clean -i
Would remove the following items:
  debug.log  notes.txt
*** Commands ***
    1: clean                2: filter by pattern    3: select by numbers   4: ask each             5: quit                 6: help
What now>

-i drops you into an interactive menu instead of deleting everything at once. You can choose clean to remove everything listed, filter by pattern to exclude files matching a glob, select by numbers to pick specific files, or quit to back out without deleting anything. This is a good middle ground between the all-or-nothing of -f and manually deleting files one by one.

How it works step by step

When you run git clean, Git does the following:

  • It walks the working directory tree (optionally scoped to the <path> arguments you gave it).
  • For every file and directory it finds, it checks the index: if the path is tracked (staged or committed), it is left alone completely — git clean never deletes tracked files, even if they’re modified.
  • For each untracked path, it checks your .gitignore rules (and .git/info/exclude). Ignored paths are skipped unless you passed -x (include ignored) or -X (only ignored).
  • Untracked directories are skipped entirely unless you passed -d.
  • Whatever remains after these filters is either printed (with -n), deleted from the filesystem with a plain filesystem delete (with -f), or presented as a menu (with -i).

Crucially, none of this reads or writes any Git objects. HEAD, the current branch pointer, and the index are untouched — git clean is a working-directory-only operation. That’s also why it can’t be undone with git reset, git revert, or the reflog: those tools all operate on commits, and the deleted files were never part of a commit in the first place.

Common Mistakes

Mistake 1: Running -fd without a dry run first

git clean -fd

This immediately deletes every untracked file and directory in the working tree with no preview and no confirmation. If that half-finished script you hadn’t staged yet, or that new module you were about to git add, happens to be untracked, it is gone permanently — there’s no blob object to recover it from. The fix is to always run git clean -n -fd (dry run) first, read the list carefully, and only then re-run with -f once you’re sure.

Mistake 2: Assuming git reset –hard also removes untracked files

git reset --hard origin/main

git reset --hard resets tracked files and the index to match the given commit, but it does not touch untracked files at all — they’re outside its scope. Developers who expect a "fully clean" working tree after a hard reset are often surprised to still see leftover untracked files in git status. To fully reset both tracked and untracked state, combine the two: git reset --hard <commit> followed by git clean -fd.

Mistake 3: Disabling the safety net

git config clean.requireForce false

Setting clean.requireForce to false makes a bare git clean (no -f) delete files immediately, removing the one guard rail that stops an accidental keystroke from wiping out work. It’s rarely worth the convenience; leave the default in place and get in the habit of typing -n before -f instead.

Best Practices

  • Always run git clean -n (optionally with -d and/or -x) before adding -f, and actually read the output.
  • Prefer git clean -i when you’re unsure exactly which files are safe to delete — it lets you filter and select interactively instead of committing to an all-or-nothing delete.
  • Keep .gitignore accurate and up to date so plain git clean -fd (without -x) is usually enough — you shouldn’t need -x often if ignore rules are well maintained.
  • Be especially careful with -x: it deletes ignored files, which can include local secrets (.env), IDE settings, and dependency caches (node_modules/) that are expensive or awkward to regenerate.
  • Commit or stash anything you might want to keep before running a broad clean — once a file is staged or committed it’s protected, even from -x.
  • Use git clean -fd <path> to scope the operation to a specific directory when you only want to tidy up one part of the project.
  • Combine with git reset --hard only when you deliberately want to discard both tracked changes and untracked files — know that this combination is unrecoverable for anything not already committed.

Practice Exercises

  • In a scratch Git repository, create two untracked files and an untracked directory containing a file. Run git status to see them, then use git clean with the appropriate flags to preview and then remove all of it in one pass. Confirm with git status that the working tree is clean.
  • Add a .gitignore entry for *.tmp, create a scratch.tmp file, and verify that a plain git clean -n -fd does not list it. Then run the dry run again with -x and observe that it now appears. Do not force-delete it — just confirm the difference in output.
  • Stage a new file with git add, then run git clean -n -fd and confirm the staged file is never listed as something that would be removed. This demonstrates that git clean only ever affects untracked paths, never staged or committed ones.

Summary

  • git clean deletes untracked files (and, with -d, untracked directories) from the working tree; it never touches tracked files, the index, or commit history.
  • Because untracked files were never turned into Git objects, deleting them with git clean is permanent — there’s no reflog or reset to bring them back.
  • clean.requireForce defaults to true, so you must pass -f to delete anything; use -n first to preview.
  • -d includes untracked directories, -x includes ignored files, -X targets only ignored files, and -i gives an interactive picker.
  • git reset --hard and git clean -fd are complementary: the first resets tracked files, the second removes untracked ones — use both together for a truly pristine working tree.