← Blog

Why your .app can't find git or node

June 7, 2026 · Adrian De Leon · packaging, java

Here’s a bug that bites almost every desktop app that shells out to a CLI: it works perfectly when you run it from a terminal, then a user double-clicks the packaged app and suddenly git, node, mmdc, or a language server “isn’t installed.” It is installed. The app just can’t see it.

The cause: a stripped PATH

When you launch an app from Finder (or a .desktop file on Linux), it does not inherit your shell’s environment. It gets a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin) with none of the directories your tools actually live in: Homebrew (/opt/homebrew/bin), npm globals, and especially anything under a version manager.

Editora shells out a lot: Git, the Mermaid CLI, every LSP server, the debug adapters, the run feature, so “can’t find the binary” would have been a constant support burden.

The fix: rebuild the PATH the app should have had

ProcessRunner is the one place Editora launches a subprocess, and it builds an augmented PATH = the inherited PATH + the user’s login-shell PATH + the usual install dirs, then rewrites a bare command name (git) to its absolute path against that PATH before launching. (Java’s Unix ProcessBuilder resolves the executable against the JVM’s own stripped PATH, not the child env, so both the augmentation and the rewrite are needed.)

The crucial piece is the login-shell PATH. Editora runs your shell once: $SHELL -l -i -c 'printf …$PATH…', with stdin from /dev/null, stderr discarded, a 5-second timeout, and marker strings fencing the PATH off from any .zshrc banner output (the pure marker-parse is unit-tested). This is the same approach VS Code uses (resolveShellEnv).

Why go to that trouble instead of hardcoding directories? Because the important ones are version-specific and can’t be hardcoded: nvm’s ~/.nvm/versions/node/<version>/bin (where npm-global tools like typescript-language-server and pyright-langserver live) changes with every Node version, and fnm/asdf/volta are similar. Only your shell knows the current one. A list of hardcoded dirs (/opt/homebrew/bin, ~/.cargo/bin, ~/go/bin, …) remains as a fallback.

Making it cheap

Spawning a login shell isn’t free, so the result is cached after the first call, and the LSP manager warms it off-thread at startup, so neither tool detection nor the first subprocess launch blocks the UI. (Windows has no POSIX login-shell convention, so it skips this and relies on the hardcoded dirs plus any Settings override.)

The takeaway

If your app shells out and “works from the terminal but not when double-clicked,” this is almost certainly why. Don’t trust the PATH a GUI launch hands you, reconstruct the one the user actually has, and resolve binaries to absolute paths yourself.