← Blog

Why your .app can't find git or node

June 7, 2026 · Adrián De León · packaging, java

Almost every desktop app that shells out to a CLI hits this bug: it works when you run it from a terminal, then a user double-clicks the packaged app and git, node, mmdc, or a language server “isn’t installed.” It is installed, but the app 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, for Git, the Mermaid CLI, every LSP server, the debug adapters, and 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 piece that matters most is the login-shell PATH. Editora runs your shell once, as $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).

Hardcoding directories isn’t enough, 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.)

If your app shells out too

If your app shells out and “works from the terminal but not when double-clicked,” this is almost certainly why. Reconstruct the PATH the user has in their shell instead of trusting the one a GUI launch hands you, and resolve binaries to absolute paths yourself.