DEV Community

Krish Verma
Krish Verma

Posted on

How I built an app launcher for my desktop AI companion: an index, not a shell command

How I built an app launcher for my desktop AI companion: an index, not a shell command

"Open Notepad" sounds like the simplest request a desktop companion can get. It nearly became the most dangerous one.

The obvious implementation is a shell command: take the app name, run it, done. But a shell command has a child process, and a child that outlives its turn can hold the voice queue open — the exact bug that used to freeze voice entirely. And worse, a shell is a string that gets re-parsed: whatever the model puts in it becomes code. "Open Notepad" should never be one metacharacter away from running something else.

So I built open_app around one decision: the model picks a name from an index, never a path. If it isn't in the index, it doesn't launch — it asks.

The index is the only source of launchable targets

Ankita scans the machine's installed apps and caches them in apps-index.json (mode 0o600, capped at 600 entries). On Windows the primary source is PowerShell's Get-StartApps, which reports display names plus AppIDs — including Store apps whose executable is not a normal file path. Start-menu .lnk shortcuts are the fallback when PowerShell is blocked. macOS walks /Applications, Linux reads .desktop entries. The cache is at most 7 days old, rescans are floored at 30 seconds apart, and a miss on a fresh index triggers one automatic rescan — a miss usually means "installed since the last scan."

Discovery is best-effort: a failed scan yields fewer apps, never an error to explain. And the cache is a launch authority, so it is validated on load, never trusted blindly.

Matching that refuses to guess

matchApps runs a strict ladder: exact name, then unique prefix, then substring, then multi-token matching for requests like "photo editor" where the words arrive in a different order. At every step, if more than one app matches, the result is ambiguous — with up to 5 candidates — and the assistant asks which one. It never picks between alternatives on the model's behalf.

Deduplication has the same discipline. One app appears several ways — a Start-menu AppID, a .lnk shortcut, a Store AppID — and deduping on name alone leaves three "Notepad" rows, making every exact lookup ambiguous. So the earliest source wins: Start-menu AppIDs are canonical, shortcuts the fallback. Junk is filtered at the door: separator-only rows, == Site == banners from web shortcuts, and URLs masquerading as shortcut names never enter the index.

Launching without a shell

This is the part I care most about. A launch target is one of two shapes, and the code treats them very differently:

  • Windows AppIDs go through cmd /c start — because Windows resolves them there — but only if they satisfy a strict shape check: a Store package ID, or {GUID}\program validated character by character. Those patterns cannot contain &, |, ^, " or %VAR%, so cmd has nothing to re-parse. The shape check lives in the scanner module so the scanner and the launcher can never disagree about what counts as an AppID.
  • Real file paths never touch cmd at all. They spawn directly with shell: false, and only after two checks: the path must be absolute (a bare name would resolve through PATH to whatever happens to be first, which is not what the index promised) and it must still exist. Arguments from .desktop files are filtered to plain tokens — anything containing a shell metacharacter is dropped.

The launch itself is detached, with stdio ignored and unref() called — and deliberately not registered in the assistant's job state. That is the bug fix disguised as a design choice: a background child registered as a job becomes a voice resource hold, which is exactly what made "open notepad" block the next request. The launcher returns as soon as the start request is accepted, and the conversation moves on.

Two smaller details: approval is needed only for an app that isn't in the index at all — a known app launches without a prompt, an unknown one asks first, and unknown state is treated as risky. And the audit log records the app name and source, never its absolute path, because that path contains the OS username and belongs nowhere in a shared log.

What it doesn't do yet

The index is a snapshot, not a live view: an app uninstalled between scans fails the existence check, so the failure surfaces as "no longer installed" rather than a crash — acceptable, but a stale index is still a stale index. Store apps launch through the shell handler by necessity, so that path trusts Windows' resolver rather than a file on disk. And matching is purely textual: it can't tell two apps with genuinely identical names apart except by asking you.

This landed on main for the upcoming 2.7.5 — the piece that makes "open notepad" safe to say out loud.

Tell me I'm wrong: is an index-only launcher the right line, or just a shell with extra steps? Where exactly would it break first — the 7-day cache, the AppID shape check, or the "ask on ambiguity" rule the moment a user has three apps called the same thing?


Ankita is an open-source, local-first personal AI companion for the desktop — a little companion, a lot more possible. The code is at github.com/akyourowngames/A.N.K.I.T.A, and I'd genuinely like to hear where this design breaks.

Top comments (0)