Why Electron Deep Links Work on macOS but Fail on Windows

macOS delivers deep links as events; Windows/Linux relies on a second instance passing arguments, requiring the single instance lock.
The article explains why the same Electron code works for deep links on macOS but fails on Windows: the platforms deliver custom protocol URLs differently. macOS delivers the URL as an event to the running app instance, so listening for open-url suffices; Windows and Linux launch a second copy and pass the URL via command-line arguments—without the single instance lock, the new instance runs independently or exits, losing the URL. The fix is to branch by platform in the main process: macOS uses event listening; Windows/Linux requests the single instance lock and listens for second-instance, extracts the URL from arguments, focuses the existing window, and navigates—both paths converging on a unified URL router. It also warns against hardcoding argument positions, groups Linux under the second-instance model, and recommends validating deep links on Windows during development.
Why Electron Deep Links Work on macOS but Fail on Windows

The same Electron code base works on macOS—clicking a custom protocol link launches the app and navigates correctly—but after deploying to Windows, nothing happens. This is a pitfall many cross-platform desktop developers have encountered. The problem isn't that the protocol registration code is wrong; it's that the operating systems deliver URLs through fundamentally different mechanisms.
Two Platforms, Two Delivery Models
According to the original analysis, macOS and Windows/Linux adopt completely different strategies for handling deep links.
macOS hands custom-scheme URLs directly to the already-running app instance, and the system is responsible for delivering the event into the app. This means developers only need to listen for the corresponding event in the main process to receive the URL and handle routing.
Windows and Linux have no such "hand-off" mechanism. The system launches a second copy of the app and stuffs the URL into that process's command-line arguments. This leads to a typical symptom: after the user clicks the link, Task Manager shows an extra Electron process, but the original window doesn't change at all—the new instance may exit immediately due to the single-instance lock, and the URL is lost along with it.
The fundamental difference can be summarized as: macOS uses "event delivery," while Windows/Linux uses "process-argument delivery."
Custom URL Scheme is an operating-system-level mechanism: an app registers an identifier such as myapp:// with the system during installation or first run. After that, whenever any program (browser, email client, other apps) opens a link with that prefix, the system looks up the corresponding registration and launches the target app. On macOS, the registration information is written to the LaunchServices database (usually declared via CFBundleURLTypes in Info.plist); on Windows, it's written to the registry under HKEY_CLASSES_ROOT (by the installer). Precisely because registration and invocation are managed by the operating system, cross-platform differences don't show up in code APIs—they show up in how the system invokes the app and where it hands the URL to the app.
Single Instance Lock is a process-level mutual exclusion mechanism provided by Electron. When app.requestSingleInstanceLock() is called, the first process acquires the lock and continues running; subsequent processes fail to acquire it, exit immediately, and trigger the second-instance event on the lock-holding process. The event callback carries the second process's command-line arguments and working directory, so it naturally becomes the channel for receiving deep link URLs on Windows/Linux. Without acquiring the lock, each copy runs independently, the event never fires, and the URL remains stuck in that new process the user promptly ignores.
The Main Process Needs Two Sets of Handling Logic
The original solution implements separate handling for the two platform categories in the main process.
For macOS, the core is listening to the app-level open-url event, parsing the incoming URL in the callback, and performing navigation or business dispatch. This path assumes the app is already running and that the system delivers the event accurately.
For Windows and Linux, the key is the combination of the single instance lock and the second-instance event. The app requests the single instance lock at startup; when a second instance starts with a URL argument, it's detected and triggers the second-instance event. The original instance reads the command-line arguments in the event callback, extracts the URL, brings the existing window to the foreground, and completes the navigation.
Without the single instance lock, the second instance runs independently. Although the new process receives the URL, the user still sees the old window—which, from the user's perspective, is equivalent to "the deep link failed."
Why This Difference Is Easy to Overlook
Passing validation on macOS during development easily leads one to assume the deep link feature is complete. Problems on Windows often only surface after packaging and distribution, when real users click the link.
Another hidden pitfall is argument parsing: on Windows, the URL is inside the command-line argument array, and you need to correctly identify its position—you can't simply take a fixed index. Different launch methods (e.g., via browser, via shortcut) may produce inconsistent argument structures. The original article emphasizes branching by platform precisely to avoid this "looks the same, actually different" trap.
The command-line argument structure is not as stable as it appears. On Windows, the first few entries of process.argv vary by launch method: double-clicking the exe directly, being invoked via a registry protocol, or being started through a packaging tool (such as Squirrel) can all produce different numbers and contents of preceding arguments. URLs with spaces, Chinese characters, or percent-encoding may also be wrapped in quotes or split apart. The safe approach is not to hardcode indices but to filter the argument array for the item matching the scheme:// form, or to use app.isPackaged to decide and then parse from the tail. In contrast, macOS's open-url event directly provides the complete URL string, so there's no parsing burden—one of the technical reasons this difference is easily overlooked.
Engineering Practice Recommendations
A more robust approach is to abstract deep link handling into a standalone module and register different listeners per platform. The macOS side uses event listening; the Windows/Linux side uses the single instance lock plus argument parsing. Both paths ultimately converge on the same URL routing function, avoiding duplicated business logic.
At the same time, validate deep links in a Windows environment during development rather than waiting for user feedback after release. For teams that need to cover Linux, Linux should also be treated under the "second instance + arguments" model rather than applying the macOS event approach.
FAQ
Q: Why does clicking a deep link on Windows do nothing? A: Because the system launches a second instance, and the original instance doesn't handle the second-instance event, so the URL isn't delivered to the existing window.
Q: Does macOS need the single instance lock? A: Deep link delivery itself relies on the event mechanism, but if the app doesn't want multiple instances, the single instance lock is still a separate design consideration.
Q: Is Linux's behavior the same as Windows? A: According to the original article, Linux and Windows both fall under the "launch a second copy and pass arguments" model, so the handling approach is the same.
One-line Summary
macOS delivers deep links as events, while Windows/Linux relies on a second instance passing arguments—requiring the single instance lock.
Paragraph Summary
The article explains the root cause of why the same Electron code works for deep links on macOS but fails on Windows: the two platforms deliver custom protocol URLs through different mechanisms. macOS delivers the URL as an event directly to the running app instance, so developers only need to listen for open-url; Windows and Linux launch a second copy of the app and stuff the URL into command-line arguments—without the single instance lock, the new instance runs independently or exits immediately, the URL is lost, and the user sees nothing happen. The solution is to branch by platform in the main process: macOS uses event listening; Windows/Linux requests the single instance lock and listens for the second-instance event, extracts the URL from command-line arguments, focuses the existing window, and navigates—both paths ultimately converging on a unified URL routing function. The article also warns against hardcoding argument positions, notes that Linux should be grouped under the second-instance model, and recommends validating deep links on Windows during development.
Related articles
Tech FrontiersGemini Omni 1.2 Flash: AI Video Moves Toward Draft-Style Editing
Google's Gemini Omni 1.2 Flash update enables conversational, draft-style editing of the same video—swap scenes while keeping products, adjust lighting while keeping characters. Clips up to 15s, 720p costs 30% less than the previous Flash—ideal for e-commerce ads, previs, and short-asset production.

Codex Tutorial: One-Command Install and Run Your First Task
Codex tutorial: one-command install and run your first task. Covers the four Codex forms, sandbox and approval guardrails, AGENTS.md and MCP configs, cloud network policy, and the iron rule of human review—everything you need to get started with this autonomous coding agent.
Tech FrontiersLocalix: An Open-Source Agentic Development Environment with Multi-Model and Local AI Integration
Localix is an MIT open-source Agentic Development Environment (ADE) supporting seamless Claude Code plugin loading, multi-model cloud and local execution, Mini Apps extensions, and agent context management—an open AI collaboration workbench for developers.