Why this exists
Companion to the "any-Mac hardening" issue linked below. That issue covers what breaks on a second Mac; this one is the genuine platform-enablement project — everything required for the app to actually run on Linux (and eventually Windows). Today the app is macOS-only by design: bundled tooling, credential storage, window chrome, and the keyboard layer are all mac-shaped.
This is intentionally parked — it's not a bug pass, it's a new-platform effort with a real cost. Filing it now so the failure points are captured while fresh; it should get its own explicit green-light and scoping before work starts.
Full audit writeup lives at temp/portability-audit-issue.md in-repo.
Findings
B1 · CRITICAL (Linux) — No Linux/Windows bundled binaries anywhere; getPlatformKey() returns null off-darwin
src/main/setup/runtimeTools.ts:84-102 · third_party/{mitmproxy,tmux,cloudflared}/manifest.json (only darwin-arm64 + darwin-x86_64)
On Linux, getPlatformKey() returns null → resolveBundledTool() yields null → mitmproxy (Claude proxy-streaming spine, #119), tmux (pane persistence, #120), and cloudflared (remote tunnel) are all absent. tmux/cloudflared have a no-PATH-fallback policy, so those features are simply dead; mitmproxy falls back to PATH but nothing's bundled. scripts/runtime-tools/fetch-*.mjs also build a linux-x64 key that has no manifest entry, so even fetching fails.
Fix: add linux-x86_64/linux-arm64 (and Windows) entries to each manifest with per-arch sha256 + URL templates, extend getPlatformKey, and teach the fetch/verify + copy-packaged-resources.mjs those platforms.
B2 · CRITICAL (Linux) — node-pty has no Linux prebuild; postinstall compiles it from source
package.json postinstall · node-pty prebuilds ship only darwin-* + win32-*
A fresh npm install on Linux compiles pty.node from source, requiring python3 + make + a C++ toolchain. On a box without build-essential, install fails at postinstall with no prebuilt fallback.
Fix: pin/verify a Linux prebuild per arch, or document the required toolchain.
B3 · CRITICAL (Linux) — No linux/win electron-builder target exists at all
electron-builder.yml (only a mac: block) · every package.json script is --mac; release.yml is macOS-only
There's no config path to produce a Linux/Windows artifact, and entitlements.mac.plist + hardenedRuntime are mac-specific.
Fix: add linux (AppImage/deb) and win (nsis) target blocks + matching runtime:prepare:* scripts, gated behind B1/B2.
B4 · HIGH — Primary global keydown handler is hard-bound to metaKey (Cmd) → entire keyboard surface dead on Linux/Windows
src/renderer/src/workspace/tile-tree/useKeybinds.ts:159 (const cmd = e.metaKey)
This handler drives the command palette, tile navigation, tab open/close, and dispatch-digit chords. On Linux/Windows the primary modifier is Ctrl (metaKey = Super/Win), so every shortcut becomes unreachable. The codebase already uses the right pattern elsewhere (CommandPalette.tsx:1088, DebugBundleNotePrompt.tsx:72 use metaKey || ctrlKey).
Fix: const cmd = e.metaKey || e.ctrlKey (or a platform-aware primaryModifier helper).
B5 · HIGH — Claude/Codex usage is hard-wired to macOS Keychain via security; hard throw off-darwin
src/main/usage/claudeUsage.ts:34-50 (throw 'Claude usage currently requires macOS Keychain credentials.' + security find-generic-password) · same shape in codexUsage.ts
The usage/limits panel can never populate on Linux/Windows — a permanent thrown error, with no fallback to Claude Code's on-disk credential file (~/.claude/.credentials.json) or libsecret/secret-tool.
Fix: add per-platform credential readers (file-based first, then libsecret/Windows Credential Manager); degrade to a clean "not available" state, not an exception. (The remote-auth secret in secret.ts already uses a portable 0o600 file pattern to mirror.)
B6–B10 · MEDIUM/LOW (Linux)
- B6 —
titleBarStyle: 'hiddenInset' (mainWindow.ts:155) is mac-only chrome; off-mac you get double chrome or a frameless window with no min/close/max controls the renderer never draws. Fix: platform-branch the title-bar style + inset padding.
- B7 — PATH augmentation prepends only Homebrew dirs
/opt/homebrew/bin, /usr/local/bin (toolchain.ts:93-94); on Linux these usually don't exist. Fix: append /usr/bin, ~/.local/bin etc. on Linux.
- B8 — mitmdump absolute-path fallback list omits
/usr/bin/mitmdump (proxyServer.ts:636-637), the standard Linux location. Fix: command -v before the hardcoded list.
- B9 —
caffeinate keep-awake and the dictation Fn default binding have no Linux equivalent (guarded, degrade cleanly). Fix: wire systemd-inhibit / a platform-aware default binding.
- B10 —
STATE_DIR ignores $XDG_CONFIG_HOME (paths.ts:18) — per-user-correct but not XDG-honoring on Linux. Fix: honor XDG_CONFIG_HOME when set. (Overlaps A10 in the companion issue.)
- B11 (note) — The provider/terminal core (
HeadlessTerminal, node-pty) is genuinely display-free, but the app shell is an Electron BrowserWindow, so a truly headless Linux/SSH host (no X/Wayland) can't launch the GUI at all. Inherent to the GUI; stated for expectation-setting.
Suggested execution order (only if green-lit)
- B1 → B2 → B3 — make it run (binaries, prebuild, build target).
- B4 + B5 + B6 — make it usable (keyboard, credentials, window chrome).
- B7–B10 — the polish tail.
Already-verified cross-platform (no work needed)
From the sweep: mitmproxy CA trust is env-var based (NODE_EXTRA_CA_CERTS/SSL_CERT_FILE/…), not Keychain / system-trust mutation — fully portable. Ports are dynamic (listen(0)). Atomic same-dir renames avoid EXDEV. Fonts have generic fallback chains. shell.openExternal/showItemInFolder use cross-platform Electron APIs (no raw xdg-open shelling). editorFs already path-separates on win32.
🤖 Drafted from a 6-agent portability sweep; see temp/portability-audit-issue.md.
Why this exists
Companion to the "any-Mac hardening" issue linked below. That issue covers what breaks on a second Mac; this one is the genuine platform-enablement project — everything required for the app to actually run on Linux (and eventually Windows). Today the app is macOS-only by design: bundled tooling, credential storage, window chrome, and the keyboard layer are all mac-shaped.
This is intentionally parked — it's not a bug pass, it's a new-platform effort with a real cost. Filing it now so the failure points are captured while fresh; it should get its own explicit green-light and scoping before work starts.
Findings
B1 · CRITICAL (Linux) — No Linux/Windows bundled binaries anywhere;
getPlatformKey()returnsnulloff-darwinsrc/main/setup/runtimeTools.ts:84-102·third_party/{mitmproxy,tmux,cloudflared}/manifest.json(onlydarwin-arm64+darwin-x86_64)On Linux,
getPlatformKey()returnsnull→resolveBundledTool()yieldsnull→ mitmproxy (Claude proxy-streaming spine, #119), tmux (pane persistence, #120), and cloudflared (remote tunnel) are all absent. tmux/cloudflared have a no-PATH-fallback policy, so those features are simply dead; mitmproxy falls back to PATH but nothing's bundled.scripts/runtime-tools/fetch-*.mjsalso build alinux-x64key that has no manifest entry, so even fetching fails.Fix: add
linux-x86_64/linux-arm64(and Windows) entries to each manifest with per-arch sha256 + URL templates, extendgetPlatformKey, and teach the fetch/verify +copy-packaged-resources.mjsthose platforms.B2 · CRITICAL (Linux) —
node-ptyhas no Linux prebuild;postinstallcompiles it from sourcepackage.jsonpostinstall· node-pty prebuilds ship onlydarwin-*+win32-*A fresh
npm installon Linux compilespty.nodefrom source, requiring python3 + make + a C++ toolchain. On a box without build-essential, install fails at postinstall with no prebuilt fallback.Fix: pin/verify a Linux prebuild per arch, or document the required toolchain.
B3 · CRITICAL (Linux) — No
linux/winelectron-builder target exists at allelectron-builder.yml(only amac:block) · everypackage.jsonscript is--mac;release.ymlis macOS-onlyThere's no config path to produce a Linux/Windows artifact, and
entitlements.mac.plist+hardenedRuntimeare mac-specific.Fix: add
linux(AppImage/deb) andwin(nsis) target blocks + matchingruntime:prepare:*scripts, gated behind B1/B2.B4 · HIGH — Primary global keydown handler is hard-bound to
metaKey(Cmd) → entire keyboard surface dead on Linux/Windowssrc/renderer/src/workspace/tile-tree/useKeybinds.ts:159(const cmd = e.metaKey)This handler drives the command palette, tile navigation, tab open/close, and dispatch-digit chords. On Linux/Windows the primary modifier is Ctrl (
metaKey= Super/Win), so every shortcut becomes unreachable. The codebase already uses the right pattern elsewhere (CommandPalette.tsx:1088,DebugBundleNotePrompt.tsx:72usemetaKey || ctrlKey).Fix:
const cmd = e.metaKey || e.ctrlKey(or a platform-awareprimaryModifierhelper).B5 · HIGH — Claude/Codex usage is hard-wired to macOS Keychain via
security; hard throw off-darwinsrc/main/usage/claudeUsage.ts:34-50(throw 'Claude usage currently requires macOS Keychain credentials.'+security find-generic-password) · same shape incodexUsage.tsThe usage/limits panel can never populate on Linux/Windows — a permanent thrown error, with no fallback to Claude Code's on-disk credential file (
~/.claude/.credentials.json) or libsecret/secret-tool.Fix: add per-platform credential readers (file-based first, then libsecret/Windows Credential Manager); degrade to a clean "not available" state, not an exception. (The remote-auth secret in
secret.tsalready uses a portable0o600file pattern to mirror.)B6–B10 · MEDIUM/LOW (Linux)
titleBarStyle: 'hiddenInset'(mainWindow.ts:155) is mac-only chrome; off-mac you get double chrome or a frameless window with no min/close/max controls the renderer never draws. Fix: platform-branch the title-bar style + inset padding./opt/homebrew/bin,/usr/local/bin(toolchain.ts:93-94); on Linux these usually don't exist. Fix: append/usr/bin,~/.local/binetc. on Linux./usr/bin/mitmdump(proxyServer.ts:636-637), the standard Linux location. Fix:command -vbefore the hardcoded list.caffeinatekeep-awake and the dictationFndefault binding have no Linux equivalent (guarded, degrade cleanly). Fix: wiresystemd-inhibit/ a platform-aware default binding.STATE_DIRignores$XDG_CONFIG_HOME(paths.ts:18) — per-user-correct but not XDG-honoring on Linux. Fix: honorXDG_CONFIG_HOMEwhen set. (Overlaps A10 in the companion issue.)HeadlessTerminal, node-pty) is genuinely display-free, but the app shell is an ElectronBrowserWindow, so a truly headless Linux/SSH host (no X/Wayland) can't launch the GUI at all. Inherent to the GUI; stated for expectation-setting.Suggested execution order (only if green-lit)
Already-verified cross-platform (no work needed)
From the sweep: mitmproxy CA trust is env-var based (
NODE_EXTRA_CA_CERTS/SSL_CERT_FILE/…), not Keychain / system-trust mutation — fully portable. Ports are dynamic (listen(0)). Atomic same-dir renames avoidEXDEV. Fonts have generic fallback chains.shell.openExternal/showItemInFolderuse cross-platform Electron APIs (no rawxdg-openshelling).editorFsalready path-separates onwin32.🤖 Drafted from a 6-agent portability sweep; see
temp/portability-audit-issue.md.