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 cleannever deletes tracked files, even if they’re modified. - For each untracked path, it checks your
.gitignorerules (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-dand/or-x) before adding-f, and actually read the output. - Prefer
git clean -iwhen 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
.gitignoreaccurate and up to date so plaingit clean -fd(without-x) is usually enough — you shouldn’t need-xoften 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 --hardonly 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 statusto see them, then usegit cleanwith the appropriate flags to preview and then remove all of it in one pass. Confirm withgit statusthat the working tree is clean. - Add a
.gitignoreentry for*.tmp, create ascratch.tmpfile, and verify that a plaingit clean -n -fddoes not list it. Then run the dry run again with-xand observe that it now appears. Do not force-delete it — just confirm the difference in output. - Stage a new file with
git add, then rungit clean -n -fdand confirm the staged file is never listed as something that would be removed. This demonstrates thatgit cleanonly ever affects untracked paths, never staged or committed ones.
Summary
git cleandeletes 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 cleanis permanent — there’s no reflog or reset to bring them back. clean.requireForcedefaults totrue, so you must pass-fto delete anything; use-nfirst to preview.-dincludes untracked directories,-xincludes ignored files,-Xtargets only ignored files, and-igives an interactive picker.git reset --hardandgit clean -fdare complementary: the first resets tracked files, the second removes untracked ones — use both together for a truly pristine working tree.
