find . -type d -empty lists every empty directory under the current path. Swap -type d for -type f and you get every empty file instead. Both rely on the -empty test, which matches anything with nothing in it: zero entries for a directory, zero bytes for a file.
The subtlety that catches people is what "empty" means, and the order find walks the tree when you add -delete. An empty directory stops being empty the moment it holds a single hidden file or one subdirectory, even an empty one. And find -delete collapses whole nested empty trees in a single pass only because of a flag most people forget. This page is the reference I keep open when I write disk-cleanup and post-build scripts.
Set your values
Set your OS and search path. Every find example below updates with your values.
The one-liner
Empty directories:
find :search_path -type d -emptyEmpty files:
find :search_path -type f -empty-type d restricts the match to directories, -type f to regular files, and -empty does the actual emptiness test. Drop the -type filter and find :search_path -empty returns both at once, which is occasionally what you want for a raw inventory but rarely what you want before a delete.
What "empty" actually means
For a file, empty is simple: zero bytes. A file with a single newline or a single space is not empty; it has content.
For a directory, empty means zero entries. This is where the surprises live:
- A directory that contains one subdirectory is not empty, even if that subdirectory is itself empty. The subdirectory counts as an entry.
- A directory that contains one hidden file (
.gitkeep,.DS_Store,.env) is not empty.-emptydoes not skip dotfiles; a hidden entry is still an entry. - A directory that contains a broken symlink is not empty. The link is an entry regardless of whether its target exists.
That last point about hidden files is the single most common reason find -type d -empty returns fewer directories than someone expects. A folder that looks empty in Finder or in a plain ls can be holding a .DS_Store that macOS dropped there, and find correctly reports it as non-empty. Run ls -la (note the -a) on a directory before you assume find is wrong.
Delete empty directories
The cleanup form adds -delete, and it needs one more flag to behave:
find :search_path -depth -type d -empty -deleteThe -depth flag is doing real work here, and leaving it off is the most common mistake in this whole topic.
By default find processes a directory before its contents (pre-order traversal). With -depth, it processes the contents first and the directory itself last (post-order, depth-first). That ordering matters for -delete because of a chain reaction.
Picture a/b/c where c is empty, b contains only c, and a contains only b. Without -depth, find visits a first; at that moment a is not empty (it holds b), so a is skipped. It then visits b (not empty, holds c) and skips it, then visits c, finds it empty, and deletes it. Result: only c is gone, a and b survive even though the entire tree was effectively empty.
With -depth, find visits c first and deletes it. Now b is empty, so when find reaches b (after its children), b is deleted too. Then a becomes empty and gets deleted. The whole nested tree collapses in one pass.
find -delete implies -depth automatically in modern GNU and BSD find, so the bare find . -type d -empty -delete often works. But the -empty test is evaluated as find walks the tree, and whether it sees the post-deletion state depends on traversal order being applied before the test runs. Writing -depth explicitly, before the other tests, removes all doubt and makes the intent obvious to whoever reads the script next. I always write it.
As with any destructive find, dry-run first. Swap -delete for -print (or -print plus -depth to see the exact order), eyeball the list, then put -delete back. There is no confirmation and no undo. The find and delete files article covers the safe-delete pattern in more depth.
The two-pass cleanup
A directory full of empty files is not itself empty, so a single -type d -empty -delete pass will not touch it. The standard fix is two passes: delete the empty files first, then delete the directories that the first pass just emptied out.
find :search_path -type f -empty -delete
find :search_path -depth -type d -empty -deletePass one removes every zero-byte file. Pass two then sees directories that held only those files as genuinely empty and removes them, with -depth collapsing any nested chains. The order is not optional: run the directory pass first and it skips folders that the file pass would have emptied, so you would have to run it again.
This is the pattern I reach for after a failed export, a half-finished rsync, or a build that scattered placeholder files. One run, two lines, and the stale skeleton is gone.
Find empty directories, excluding some paths
When the search path contains a subtree you never want to touch (node_modules, .git, a mounted backup), filter it out with -not -path:
find :search_path -type d -empty -not -path '*/node_modules/*' -not -path '*/.git/*'-not -path PATTERN (you can also write it ! -path PATTERN) drops any path matching the glob. The */ prefix and /* suffix make it match the directory at any depth. Chain as many -not -path clauses as you need.
For large trees there is a faster idiom: -prune stops find from descending into a subtree at all, rather than visiting every entry and discarding it. find :search_path -path '*/node_modules' -prune -o -type d -empty -print skips node_modules wholesale. -prune is the better choice when the excluded subtree is huge; -not -path is simpler when it is small. The find command cheat sheet has the full -prune pattern.
Count empty directories
Pipe the listing into wc -l to get a number instead of a list:
find :search_path -type d -empty | wc -lwc -l counts newlines, so each matched path adds one to the total. The caveat that applies to every find | wc -l: a directory name containing a literal newline would be counted twice. That is rare enough to ignore for a quick disk audit, but if you are counting paths you do not control, the safe form is find :search_path -type d -empty -printf '.' | wc -c on GNU (BSD find has no -printf).
macOS BSD vs GNU find
The -empty test is one of the well-behaved corners of find. Both implementations support the flags this article uses, with the same meaning:
| Behavior | GNU find | BSD find (macOS default) |
|---|---|---|
-empty test | Supported | Supported |
-depth (post-order traversal) | Supported | Supported |
-delete action | Supported (implies -depth) | Supported (implies -depth) |
-not / ! operator | Supported | Supported |
-path test | Supported | Supported |
-prune action | Supported | Supported |
-printf (for safe counting) | Supported | NOT supported; use -exec |
So the empty-directory and empty-file workflow is portable: a script written with -empty, -depth, -delete, -not -path, and -prune runs unchanged on both Linux and macOS. The only gap is -printf, which you only need for the newline-safe count. If a cleanup script has to run on both platforms, stick to that intersection and you will not get bitten.
Common mistakes
1. Forgetting -depth when deleting directories. Without it, find evaluates a parent directory before deleting its children, sees the parent as non-empty, and skips it. Nested empty trees survive except for the innermost leaf. find -delete implies -depth, but write -depth explicitly so the intent is unambiguous and the command is correct even if you later swap -delete for -exec rmdir.
2. The hidden-file gotcha. A directory holding a single .gitkeep, .DS_Store, or .env is not empty, and find -type d -empty correctly skips it. People see a "blank" folder in a file browser, run the command, get nothing, and assume find is broken. It is not. Run ls -la on the directory to see the dotfile.
3. Treating a directory of empty files as empty. A folder containing zero-byte files is itself non-empty; it has entries. A single -type d -empty pass will not remove it. Use the two-pass cleanup: empty files first, then empty directories.
4. Running it against /. find / -type d -empty -delete is a foot-cannon. It will walk the entire filesystem, including /proc, /sys, and every mounted volume, and happily delete empty directories that the OS or other software depends on. Always anchor the search at a real project or scratch directory, and add -xdev if you want to stay on one filesystem.
5. Skipping the dry run. find -delete has no confirmation prompt and no undo. Always run the command with -print first, read the list, then swap in -delete. This costs two seconds and has saved me from deleting directories I forgot were load-bearing.
6. Putting -depth after the tests. -depth is a global option; placing it after -type d is technically legal but reads as if it were a test. Convention, and clarity, put it right after the search path: find :search_path -depth -type d -empty -delete.
When NOT to use this
find -empty is the right tool for cleaning up structural cruft, but reach for something else when:
- You want to free disk space. Empty directories take essentially no space. If the goal is reclaiming gigabytes, you want
du -sh */ | sort -hor find files larger than a size, not an empty-directory sweep. - The "empty" directories hold intentional placeholders. Many repos use
.gitkeepto keep an otherwise-empty directory tracked in Git. Those are not empty (the dotfile is an entry), sofind -type d -emptyskips them correctly. But if you have a habit of using truly empty directories as markers, a blanket delete will wipe your markers. Know your tree. - You need the directories gone in a specific order with logging.
find -deleteis silent and order is fixed by-depth. If you need to log each removal, handle errors per directory, or delete conditionally, loop with-exec rmdir {} \;or a bash while loop over-print0output instead. - Another process is writing into the tree.
findwalking a directory while a build or an upload is still creating files in it will produce a stale snapshot, and a delete pass can race the writer. Run cleanup only when nothing else is touching the path.
See also
- find Command Cheat Sheet: the full find reference covering name, type, size, time, perms,
-prune, and-exec - Find and delete files safely with find -delete: the cleanup-script playbook, including the directory-deletion-order trap
- Find files by extension: match files by suffix with
-nameand-inameglobs - External: GNU findutils manual, FreeBSD find(1) man page





