sweep removes empty directories below a directory you give it.
A directory counts as empty if it holds nothing at all, only a .DS_Store file, or only directories that are themselves empty. So a tree of nested empty folders, each with a stray .DS_Store, goes in one pass. The directory you point it at is never removed, even if it ends up empty.
sweep [DIRECTORY] [--dry-run] [--yes] [--jev]
| Flag | Effect |
|---|---|
-n, --dry-run |
Show what would be removed. Nothing is touched. |
-y, --yes |
Remove without the confirmation prompt. |
-J, --jev |
Ask TypeSafe's Jev model whether other files are junk. See Jev. |
--jev-threshold |
Probability at or above which Jev's answer makes a file junk. Default 0.8. |
--no-cache |
With --jev, ask Jev afresh instead of reusing cached decisions. |
--jev-results |
With --jev, print each file's decision after the main results. |
-v, --version |
Print the version. |
-h, --help |
Print help. |
DIRECTORY defaults to the current directory.
sweep --dry-run ~/Downloads
sweep ~/Projects
sweep --yes /Volumes/Backup | tee sweep.log
In a terminal, sweep:
- Scans with a spinner and a live count of directories and empties found.
- Shows a tree of the directories it is about to remove. A removed directory lists its
.DS_Storeand how many empty directories sit inside it. You can scroll the tree. - Asks
Remove N empty directories? (y/N). Anything butyremoves nothing. - Removes them behind a progress bar, then prints the tree and a summary.
Without a terminal on both stdin and stdout (in a pipe, cron or CI), there is no prompt. A real run then needs --yes, and output comes without colour.
On its own, sweep only treats .DS_Store as junk. With --jev, a directory also counts as empty when every other file in it is OS junk or a useless leftover, such as Thumbs.db, desktop.ini, Office lock files or crash dumps. The rule still cascades: a parent holding only such directories goes too.
Jev is TypeSafe's decision model: instead of writing text, it returns the probability that a yes/no question's answer is yes. sweep asks it one question per file, whether the file is junk that can be deleted without losing anything a person would want. It sends the directory's path below the root, each file's name and size, and the names of its empty subdirectories. sweep never sends file contents. A file is junk when Jev's probability reaches --jev-threshold.
To keep calls down, sweep only asks about a directory when nothing else stops it being empty. It asks the deepest directories first, several at once, and never asks about a parent once a child has been kept. Directories with more than 32 files are kept without asking.
Files Jev picked are marked with * in the tree, and the summary counts how many directories it judged. Add --jev-results to also list every decision after the main results: each file's probability, junk or keep, and whether its directory counts as empty. Directories Jev wasn't asked about are counted in that list's heading rather than shown. In a real run, the same list appears on the review screen before you confirm.
sweep --jev --dry-run ~/Downloads
sweep --jev --dry-run --jev-results ~/Downloads # same scan, answers from the cache
sweep --jev --jev-threshold 0.9 ~/Downloads
Jev's answers are kept in a local SQLite database (a single file) at ~/.cache/sweep/judgements.db (or $XDG_CACHE_HOME/sweep when that is set). So sweep --jev --dry-run DIR followed by sweep --jev DIR asks Jev once, and the real run acts on exactly the answers you reviewed.
The ~/.cache/sweep directory is created at the start of every --jev run, before anything else happens. Each answer is keyed to what it was based on: the directory's path, each file's name, size and modification time, the empty subdirectories, and the model and question wording. A directory with any file added, removed, resized or touched since is asked again; the rest come from the cache. Checking costs nothing beyond the scan itself, because file contents are not read. Probabilities are stored rather than verdicts, so changing --jev-threshold still uses the cache. Entries expire after 30 days, as the model behind jev-latest can change.
Cached decisions are marked cached in the output, and the summary counts them. Pass --no-cache to ask afresh without reading or writing the cache.
The API key is read from the first of these that is set: $TYPESAFE_API_KEY, $JEV_API_KEY, $__TYPESAFE_API_KEY. A rejected key stops the run before anything is removed. Answers near the threshold can change between fresh calls, so check the dry run before a real one; the cache then keeps the real run consistent with it.
- Directories are deleted with
os.Remove, never a recursive delete. If a file appears in a directory between the scan and the removal, that directory and its parents are kept and the failure is reported. - Symbolic links are never followed. A symlink counts as content, so the directory holding it stays.
- An unreadable directory is treated as having content. It is kept, along with its parents, and the error is reported.
.DS_Storeis deleted only from directories that are being removed.- A file Jev judged junk is deleted only if its size and modification time haven't changed since the scan. Otherwise that directory and its parents are kept.
| Code | Meaning |
|---|---|
| 0 | Finished, nothing to do, or you answered no |
| 1 | Bad arguments, or some directories could not be read or removed |
| 130 | Interrupted with ctrl+c |
./build.sh # build ./sweep
./build.sh --check # run formatting, vet, lint and tests first
./build.sh --install # install into ~/.local/bin ($SWEEP_INSTALL_DIR overrides)
./build.sh --output PATH # write the binary somewhere else
build.sh checks your Go version against go.mod, downloads and verifies modules, and stamps the version from git describe. It needs Go 1.27.1 or later; with an older Go, the Go toolchain downloads the right version unless GOTOOLCHAIN=local is set. golangci-lint is used by --check when installed.
To install without cloning the repository:
go install github.com/synmux/sweep@latest
gofmt -l .
go vet ./...
golangci-lint run ./...
go test -race ./...
Layout:
| Path | Contents |
|---|---|
main.go |
Entry point. Hands the cobra command to fang. |
internal/cli |
Flags, terminal detection, plain mode, exit codes |
internal/sweep |
Scanning, judging and removal. No UI code. |
internal/jevjudge |
The Jev-backed judge and API key lookup |
internal/judgecache |
SQLite cache of Jev decisions |
internal/report |
Tree, summary and error boxes |
internal/theme |
Light and dark styles from Charm's charmtone palette |
internal/ui |
Bubble Tea model |
Built with Bubble Tea, Bubbles, Lip Gloss and Fang.