whisk

Name the folder.
Whisk keeps it clean.

A rule-driven tidying utility for the macOS menu bar. Downloads, Desktop, an inbox — give each one a few rules, and files get moved, renamed, aged into the Trash, or handed to your scripts. You stop doing it by hand.

$brew install --cask alexnodeland/tap/whisk
Source & releases

macOS 14+ · universal binary · unsigned for now — one xattr clears the first-launch block

The loop

Notice. Snapshot. Plan. Act.

Watching a folder is easy; trusting what you watched is not. Whisk treats every wake-up — a file event, an age timer, the safety-net rescan — as a reason to look again, never as the truth itself.

STEP 1 notice a change FSEvents · timers · rescan STEP 2 re-enumerate events trigger, never decide STEP 3 plan, purely rules → actions · 100% covered STEP 4 act, safely move · rename · trash · run a sweep re-reads the whole folder — a missed event costs nothing
re-enumerate

The folder is the truth

FSEvents coalesces, drops, and lies by omission. Acting on events directly means acting on a stale picture — so Whisk only ever acts on a fresh listing.

pure core

One function decides everything

Snapshot + rules in, planned actions out — no I/O inside. CI holds the planner at 100% region coverage, so every branch of the matching and budget logic is exercised before it ships.

ordered rules

Ordering is a feature

Rules run top to bottom; a file consumed by move or trash is skipped by later rules in the same sweep. Without that, two overlapping rules quietly fight over the same file.

The file

Rules, not chores

One JSON5 file is the whole configuration — comments and trailing commas welcome, hot-reloaded on every save, yours to sync and version. A built-in editor covers the common cases — and saving from it preserves your comments — while a Settings window (⌘,) holds everything else.

// ~/.config/whisk/rules.json — reloaded whenever it changes
{
  version: 1,
  targets: [{
    path: "~/Downloads",
    rules: [
      { id: "file-images",
        match: { name: { glob: "*.{png,jpg,heic}" } },
        actions: [{ move: { to: "~/Pictures/Inbox/{date.modified:yyyy-MM}" } }] },
      { id: "trash-old-installers",
        match: { all: [
          { extension: ["dmg", "pkg"] },
          { age: { olderThan: "7d" } },
        ]},
        actions: [{ trash: {} }] },
    ],
  }],
}
conditions

Compose, to any depth

Name globs and regexes, extensions, file/folder kind, size, and age (created, modified, or added) — nested under all / any / not.

patterns

Destinations that sort themselves

Move targets and rename templates take {name}, {ext}, and {date.modified:yyyy-MM}-style tokens, with Finder-style conflict renaming built in.

reload

Broken saves don't break Whisk

A rules file mid-edit is often invalid. On a parse error Whisk keeps the last good ruleset running, shows the error, and picks up your next save.

Trust

Built to refuse to make a mess

A tool that moves your files unattended has one job before all others: never surprise you. Everything Whisk does is previewable, budgeted, and reversible.

DRY-RUN report.pdf ~/Docs previewed, not performed every action, announced first BUDGETS ⏸ unpack-zips auto-paused runaways pause themselves cooldowns · ledgers · hard budgets UNDO-FRIENDLY Trash, never delete every removal stays recoverable
shell commands run argv-only, in a sanitized environment, after a one-time approval
loop guard

It can't chase its own tail

A cleanup tool's classic failure is reacting to its own writes forever. Whisk keeps a ledger of what it just wrote, cools down per file-and-rule, and refuses moves into the source's own path at plan time.

shell approval

Your scripts, once approved

A rules file syncs between machines — a command you didn't write could arrive with it. New or changed commands are held until you approve that exact command from the menu, once.

consent

Only the folders you list

No sandbox theater, no blanket disk access: macOS asks for each folder your rules target, and a denied folder shows a "No access" badge instead of failing silently.

Automation

A URL scheme and a log file

Whisk is a good citizen of your own automation: drive it from Shortcuts, Raycast, or a script, and read back exactly what it did.

URLEffect
whisk://sweepsweep every target now
whisk://pause?minutes=60pause — for a while, or indefinitely
whisk://resumeresume sweeping
whisk://dry-run?enabled=truetoggle preview mode
PathHolds
~/.config/whisk/rules.jsonyour rules — the only file that's yours to edit
…/Whisk/activity.jsonlevery action, one JSON object per line
…/Whisk/approvals.jsonthe shell commands you've approved
Install

Two commands to a clean folder

macOS 14 (Sonoma) or later, Apple Silicon or Intel — one universal binary.

first
brew tap alexnodeland/tap

Newer Homebrew asks for a one-time brew trust alexnodeland/tap — it will tell you if yours does.

then
brew install --cask whisk

Builds are unsigned for now — if macOS blocks the first launch: xattr -dr com.apple.quarantine /Applications/Whisk.app

run
open -a Whisk

First launch seeds ~/.config/whisk/rules.json with a commented starter file, and macOS asks for access to the folders your rules touch.

Direct download

Grab Whisk-universal.zip from the latest release, drag to Applications, and clear quarantine once: xattr -dr com.apple.quarantine /Applications/Whisk.app

It keeps itself fresh

Whisk checks for new releases daily and offers them in the menu — flip Install updates automatically and it handles the rest.

Build from source

Pure swiftc — no Xcode project to open. just setup && just dev builds and launches; just check runs the full CI gate, 100% coverage included.

A note on what it touches

Whisk acts only on the folders your rules list, asks macOS for consent to each one, and prefers reversible operations — the Trash over deletion, previews over surprises. It's a personal tool, shared as-is.