Launchee: my own keyboard launcher for macOS

A native launcher I built with Claude Code to replace Alfred 5 after it froze. ⌘Space, a few letters, ↩: apps, commands, URLs, folders and file filters.

The Launchee app icon: a white launcher bar with a red edelweiss on a teal gradient square, with the name Launchee under it

Launchee is the launcher behind ⌘Space on my Mac. I type a few letters and press ↩ to start an app, run 1 of my commands, open a URL or a folder, or find a file by type. It replaces Alfred 5, which I used daily since 2020. For my Alfred setup, see Alfred.

I built it with Claude Code on 24th October 2026, and switched Alfred off the same evening. Alfred's other jobs went to my other apps:

What's left is 1 bar, my commands and a runner that can't freeze.

The Launchee bar in light mode on QA test data: the edelweiss logo and the query 'a', then 9 rows with icons, titles, subtitles and ⌘1 to ⌘9 key caps, the second row 'Amazon UK' selected as a solid red block, and a footer with the pack name and the keys

The screenshots and code examples in this note use dummy data: the commands, apps and paths come from Launchee's QA tests or are made up for the example. None of it is my real setup.

Why I replaced Alfred

It froze

On 19th October 2026, Alfred stopped answering. Its main thread was stuck in a macOS call that opens a URL, waiting for 1 action of a file search workflow. The whole bar was dead, and with it my music control, my clipboard history and every keyword. That's about 55 actions a day, gone.

That's a design problem, not a bug: Alfred runs some actions on the same thread that draws the bar, with no deadline. 1 slow volume or 1 stuck app is enough.

The usage review

Before writing a line of code, Claude read 30 days of Alfred's own usage database:

What I found Value
Actions in 30 days 1,678, about 55 a day, peaks at 09:00-12:00 and 15:00
App launches 762, across 74 apps
Letters typed to start an app 3.6 on average
Music control 443 actions
Workflows installed 162: 141 of mine, 21 from others
Workflows used in 30 days 26
Memory 49 MB footprint, 137 MB resident after 12 hours
App size 18 MB

4 jobs were 84% of the use: start an app, control my music, and 2 of my own commands. Everything else was a long tail of 1 to 48 uses. So version 1 had to make those 4 work from day 1, with the same letters, and leave the rest for later.

Commands an agent can edit

Alfred stores a workflow as a graph of objects in a plist, built in its GUI. Claude can read that, but editing it is fragile. I wanted every command to be a few lines of text that Claude and I edit, check and diff.

Why not something that already exists

Option For Against
A native app (the pick) Its own process, my budgets About 2 days of build
Raycast Ready today An account, network calls, its own extension format
Spotlight in macOS 26 Built in, has quick keys and actions 1 Shortcut per command, no import of Alfred's learning, no text commands
Hammerspoon hs.chooser No new app Same process as my keyboard taps: 1 hang kills every key

What it does

The bar

⌘Space opens the bar on the screen I work on, with the query field at 25% of the screen's height. The app I was in stays in front. The bar is drawn once, invisible, at launch, so the 1st press is as fast as the next ones. It never opens on the screen I share in calls.

Ranking that remembers me

Results rank in 5 tiers:

  1. Latched: these letters picked this item 3 times or more, and nothing else. Always first.
  2. Exact keyword.
  3. Learned: these letters picked this item before, weighted by count and halved every 30 days.
  4. Keyword prefix: md2 finds md2doc and md2pdf.
  5. Fuzzy: titles, app names, aliases and keywords.

A Google search always closes the list. The import carried over Alfred's learned typing, so bra was Brave from the first press. Claude turned every pair that Alfred had logged 3 times or more into a test: 79 of 79 rank first in Launchee.

The Launchee bar in dark mode on QA test data: the query 'md' with md2doc selected in red and md2pdf under it, then built-in commands to check, reload and open the Launchee commands, and a Google search fallback

Commands are plain files

Each topic gets 1 JSONC file in ~/.launchee/commands/. Launchee reloads a changed file within 1 s. A file with an error keeps its last good version loaded, and the menu bar icon shows a red dot.

{"commands": [
  {"id": "amazon", "title": "Amazon", "keyword": "a", "arg": "required", "url": "https://www.amazon.de/s?k={query}"},
  {"id": "todo", "title": "To Do", "keyword": "todo", "open": "~/todo.md"},
  {"id": "report", "title": "Weekly report", "keyword": "wr", "run": ["~/py/report.py"], "terminal": true},
  {"id": "pdf", "title": "Find PDF files", "keyword": "pdf", "files": {"types": ["com.adobe.pdf"], "folders": ["~/Documents"]}}
]}
Action Does
url Opens a URL, with the query percent-encoded, in the browser I name
open Opens a file, folder or app
run Runs a program with arguments, in the background or in a terminal
osascript Runs an AppleScript
quit Quits an app
copy Copies a template with the query, the clipboard or today's date
files A file filter: file types plus folders

The folder is its own git repository, and launchee push backs it up. An app can write its own pack: Hifee writes the file with its music commands, so the launcher never needs to know about music.

launchee check runs after every edit. It reports broken JSONC, duplicate ids, keywords and hotkeys, commands with 0 or 2 actions, missing scripts, and warns on URLs that carry token=, password= or key=.

Paths, URLs and file filters

  • A path: ~/ or / lists the folder's entries, and Tab completes a name. A folder opens in Filee.
  • A URL: "Open " comes first.
  • A file filter: pdf manual finds PDFs whose name holds "manual". 14 filters, for PDFs, Office files, images, videos and screenshots, each limited to its own folders. Files that Lastee saw in the last 30 days come first.

Keys

Key Does
↩ Runs the selected row
⌘1-⌘9 Runs row 1 to 9, shown as key caps on the rows
Tab Completes the keyword, or the folder name
⌘↩ The alternative: reveal the file, or the command's 2nd action
⌥↩ Copies the row's target: URL, path or command line
⌘Y Quick Look
⌘K The selected row's actions
⌘E Opens the command's pack file in my editor
⌘= ⌘- ⌘0 Zoom, which persists
Esc Clears the query, then closes

The numbers

Metric Launchee, measured Alfred
Hotkey to the bar About 1 ms (in-app) Not measured
Keystroke ranking, 800 items, p95 3.5 to 4.3 ms Not measured
File filter keystroke, 20,000 files, p95 0.2 ms Not measured
Main-thread work to start an action 0.4 ms Froze on 19 Oct
Cold launch to a ready bar 120 to 240 ms Not measured
Memory, idle 23 MB 49 MB
Memory, 211 apps after 79 searches 37 MB -
CPU at idle 0.0% 0%
App size 1.5 MB 18 MB

How it works

The no-hang rules

These rules are binding. A change that breaks one needs a measured reason.

  • The main thread never waits. It reads keys, ranks lists in memory and draws. It never reads a file, waits for a process, sends an Apple Event, runs Spotlight or asks LaunchServices for anything.
  • Hide first, act second. ↩ hides the bar, records the use, and hands the action to a queue.
  • Every action has a deadline. 10 s to open a URL or file, 15 s for AppleScript, 120 s for a background script, 1 s for a folder listing. Then SIGTERM, then SIGKILL 2 s later, and a notice.
  • Every open is a child process. URLs, files and apps open through /usr/bin/open. A stuck LaunchServices call hangs the child, never Launchee. This is the exact rule that would have saved Alfred on 19th October.
  • A watchdog guards the main thread. A stall over 250 ms goes to a log. A stall of 3 s quits the app, and launchd starts it again in about 1 s.
  • Late results die. Each keystroke starts a new generation, and an answer for an older one is dropped.

File filters without Spotlight in the keystroke

A Spotlight query over my main documents folder takes 0.75 to 1.25 s, whatever the query. That time sits in Apple's code. So the 1st use of a filter runs 1 mdfind in a child process (a "Searching…" row for about 1 s) and keeps the path and date of each file in memory. Every keystroke after that filters the cache in under 1 ms. A filter with more than 25,000 files keeps no cache and asks Spotlight per query instead. A cache unused for 10 minutes goes: I use the filters a few times a month.

The runner

The runner keeps the rules of my old Alfred runner script, so every converted script behaves as before. A Python script runs with the nearest virtualenv. There is no shell: the query is 1 argument, never split. terminal: true opens the command in a new Ghostty window. Up to 4 background runs at once, and a log with the exit code, duration and the last 20 lines of output.

Stack

  • Swift and AppKit, frame-based layout, no third-party code. About 5,000 lines of Swift, with the ranking, packs and file matching in a LauncheeCore package that runs under unit tests.
  • A Carbon hotkey for ⌘Space: no Accessibility permission, no event tap.
  • SQLite for the learning and a silent usage log.
  • A Python CLI and an Alfred converter, standard library only.
  • Code copied, not shared from Clipee (watchdog, runner, panel, notice card) and Lastee (the fuzzy matcher).

Memory lessons

  • App icons from the app's own .icns file, at 72 pixels. The usual macOS call keeps large images and system icon data: about 20 MB more with 200 apps.
  • Read Info.plist directly. Opening an app as a Bundle keeps every bundle in memory for the life of the process.

Agent interface

A CLI wraps the app, with --json on every command, so my NicAI agents can use it:

launchee status --json                 # running, version, commands and apps loaded, hotkeys held
launchee list -n 50 --json             # command ids, keywords, packs, uses in 30 days
launchee run amazon "usb c hub" --json # run 1 command with a query, confirmed by the app
launchee check --json                  # pack errors and warnings
launchee usage --days 7 -n 10 --json   # the most used items

The CLI never prints the action log or what I typed: ids, keywords and counts only.

How I built it

The first draft spec was written on the day of the freeze, 19th October. The build day was 24th October 2026:

  1. Usage review and questions. 38 decisions in 2 rounds. The biggest call: convert only the 26 workflows I used in 30 days. The other 116 of mine and the 21 third-party ones went to a review list, each with what it does, so I can pick the ones worth rewriting.
  2. Spec and build. 8 commits between 17:17 and 20:02.
  3. QA. 19 unit tests and 92 end-to-end checks in 6 suites. QA runs in a scratch folder with a fake open and a fake Spotlight, so it never touches my clipboard, files or hotkeys. 1 suite tests the hangs on purpose: an opener that never returns, a script that sleeps 600 s, a listing that blocks, and a 4 s main-thread stall that the watchdog must end.
  4. Independent review. A separate agent found 13 issues, among them a reload loop, a watchdog that could fire after the Mac woke from sleep, and stale rows. All fixed.
  5. Switch-over. The converter read Alfred's files read-only and wrote the packs, then a script quit Alfred and started Launchee. To go back, I quit Launchee and open Alfred: its files are untouched.

On day 2, the selected row became a solid red block with white text, and the bar lost its hover state: only the keyboard selection is drawn.

What's next

The usage log runs silently. After 30 days, it shows which Google fallbacks repeat, which is where a missing command shows up. Version 2, ranked by value:

  • My picks from the review list, most of them 1 line in a pack.
  • List commands: a script that prints Alfred's Script Filter JSON shows its items as rows.
  • A terminal line: > runs the rest of the line in Ghostty.
  • Open with on a file row.
  • A calculator, units, dates and time zones, offline.

Further reading

The app Launchee replaces:

The tool I built it with:

NicAI
Written by NicAI, Nic's AI assistant, for his personal knowledge base. Researched and drafted by the model, not hand-written by Nic. Verify anything you plan to act on.