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
FAQ
Use the -empty test with -type d: find . -type d -empty prints every directory under the current path that has zero entries. Add -type f instead of -type d to find empty (zero-byte) files. Drop the -type filter entirely and find . -empty returns both.
Because it is not actually empty. A directory counts as empty only when it has zero entries, and a hidden file is still an entry. A folder holding a single .gitkeep, .DS_Store, or .env is non-empty, and find correctly skips it. So is a folder that contains one subdirectory, even an empty subdirectory. Run ls -la (with the -a flag) on the directory to see the hidden entries.
Run find . -depth -type d -empty -delete. The -depth flag is what makes nested empty trees collapse: it tells find to process a directory's contents before the directory itself, so deleting an inner empty directory makes its parent empty in time for the same pass to delete that too. Test with -print in place of -delete first; there is no undo.
Without -depth, find visits each directory before its contents. A parent that contains only an empty subdirectory is not yet empty when find checks it, so it gets skipped, and only the innermost leaf is removed. With -depth, find deletes the leaf first; the parent then becomes empty and is deleted on the same pass. find -delete implies -depth in modern find, but writing it explicitly makes the command correct and the intent clear.
Run two passes, files first: find . -type f -empty -delete then find . -depth -type d -empty -delete. The first pass removes every zero-byte file, which empties out the directories that held only those files. The second pass then sees those directories as empty and removes them. Running the directory pass first would skip folders that the file pass had not emptied yet.
Add a -not -path clause: find . -type d -empty -not -path '*/node_modules/*'. The */ and /* wildcards match the excluded directory at any depth. For a large excluded subtree, -prune is faster because it stops find from descending at all: find . -path '*/node_modules' -prune -o -type d -empty -print.
Yes. macOS ships BSD find, and BSD find supports -empty, -depth, -delete, -not, -path, and -prune with the same meaning as GNU find. The empty-directory and empty-file workflow is fully portable between Linux and macOS. The only flag missing on BSD is -printf, which you would only need for a newline-safe count of matches.





