Repository: electron/electron
Pull request: fix: use proper quoting for exe paths and args on Windows

#### Description of Change

Previously, `GetProtocolLaunchPath` and `FormatCommandLineString` in `browser_win.cc` used naive quoting  which could break when paths or arguments contained backslashes, spaces, or embedded quotes.

Fix by extracting the `CommandLineToArgvW`-compatible quoting logic from `relauncher_win.cc` into a shared utility (`shell/common/command_line_util_win`) and use it in both `browser_win.cc` and `relauncher_win.cc` to properly quote the exe path and each argument individually.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included
- [ ] I have built and tested this PR
- [x] `npm test` passes
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: none

Changed files:
filename: filenames.gni
patch: @@ -117,6 +117,8 @@ filenames = {
     "shell/browser/win/scoped_hstring.h",
     "shell/common/api/electron_api_native_image_win.cc",
     "shell/common/application_info_win.cc",
+    "shell/common/command_line_util_win.cc",
+    "shell/common/command_line_util_win.h",
     "shell/common/language_util_win.cc",
     "shell/common/node_bindings_win.cc",
     "shell/common/node_bindings_win.h",

filename: shell/browser/browser_win.cc
patch: @@ -37,6 +37,7 @@
 #include "shell/browser/ui/win/jump_list.h"
 #include "shell/browser/window_list.h"
 #include "shell/common/application_info.h"
+#include "shell/common/command_line_util_win.h"
 #include "shell/common/gin_converters/file_path_converter.h"
 #include "shell/common/gin_converters/image_converter.h"
 #include "shell/common/gin_converters/login_item_settings_converter.h"
@@ -79,13 +80,22 @@ bool GetProtocolLaunchPath(gin::Arguments* args, std::wstring* exe) {
     return false;
   }
 
+  // Strip surrounding double quotes before re-quoting with AddQuoteForArg.
+  if (exe->size() >= 2 && exe->front() == L'"' && exe->back() == L'"') {
+    *exe = exe->substr(1, exe->size() - 2);
+  }
+
   // Read in optional args arg
   std::vector<std::wstring> launch_args;
   if (args->GetNext(&launch_args) && !launch_args.empty()) {
-    std::wstring joined_args = base::JoinString(launch_args, L"\" \"");
-    *exe = base::StrCat({L"\"", *exe, L"\" \"", joined_args, L"\" \"%1\""});
+    std::wstring result = electron::AddQuoteForArg(*exe);
+    for (const auto& arg : launch_args) {
+      result += L' ';
+      result += electron::AddQuoteForArg(arg);
+    }
+    *exe = base::StrCat({result, L" \"%1\""});
   } else {
-    *exe = base::StrCat({L"\"", *exe, L"\" \"%1\""});
+    *exe = base::StrCat({electron::AddQuoteForArg(*exe), L" \"%1\""});
   }
 
   return true;
@@ -153,9 +163,18 @@ bool FormatCommandLineString(std::wstring* exe,
     return false;
   }
 
+  // Strip surrounding double quotes before re-quoting with AddQuoteForArg.
+  if (exe->size() >= 2 && exe->front() == L'"' && exe->back() == L'"') {
+    *exe = exe->substr(1, exe->size() - 2);
+  }
+
+  *exe = electron::AddQuoteForArg(*exe);
+
   if (!launch_args.empty()) {
-    std::u16string joined_launch_args = base::JoinString(launch_args, u" ");
-    *exe = base::StrCat({*exe, L" ", base::AsWStringView(joined_launch_args)});
+    for (const auto& arg : launch_args) {
+      *exe += L' ';
+      *exe += electron::AddQuoteForArg(std::wstring(base::AsWStringView(arg)));
+    }
   }
 
   return true;

filename: shell/browser/relauncher_win.cc
patch: @@ -14,6 +14,7 @@
 #include "base/win/scoped_handle.h"
 #include "sandbox/win/src/nt_internals.h"
 #include "sandbox/win/src/win_utils.h"
+#include "shell/common/command_line_util_win.h"
 
 namespace relauncher::internal {
 
@@ -50,49 +51,6 @@ HANDLE GetParentProcessHandle(base::ProcessHandle handle) {
   return ::OpenProcess(PROCESS_ALL_ACCESS, TRUE, ppid);
 }
 
-StringType AddQuoteForArg(const StringType& arg) {
-  // We follow the quoting rules of CommandLineToArgvW.
-  // http://msdn.microsoft.com/en-us/library/17w5ykft.aspx
-  std::wstring quotable_chars(L" \\\"");
-  if (arg.find_first_of(quotable_chars) == std::wstring::npos) {
-    // No quoting necessary.
-    return arg;
-  }
-
-  std::wstring out;
-  out.push_back(L'"');
-  for (size_t i = 0; i < arg.size(); ++i) {
-    if (arg[i] == '\\') {
-      // Find the extent of this run of backslashes.
-      size_t start = i, end = start + 1;
-      for (; end < arg.size() && arg[end] == '\\'; ++end) {
-      }
-      size_t backslash_count = end - start;
-
-      // Backslashes are escapes only if the run is followed by a double quote.
-      // Since we also will end the string with a double quote, we escape for
-      // either a double quote or the end of the string.
-      if (end == arg.size() || arg[end] == '"') {
-        // To quote, we need to output 2x as many backslashes.
-        backslash_count *= 2;
-      }
-      for (size_t j = 0; j < backslash_count; ++j)
-        out.push_back('\\');
-
-      // Advance i to one before the end to balance i++ in loop.
-      i = end - 1;
-    } else if (arg[i] == '"') {
-      out.push_back('\\');
-      out.push_back('"');
-    } else {
-      out.push_back(arg[i]);
-    }
-  }
-  out.push_back('"');
-
-  return out;
-}
-
 }  // namespace
 
 StringType GetWaitEventName(base::ProcessId pid) {
@@ -105,7 +63,7 @@ StringType ArgvToCommandLineString(const StringVector& argv) {
   for (const StringType& arg : argv) {
     if (!command_line.empty())
       command_line += L' ';
-    command_line += AddQuoteForArg(arg);
+    command_line += electron::AddQuoteForArg(arg);
   }
   return command_line;
 }

filename: shell/common/command_line_util_win.cc
patch: @@ -0,0 +1,54 @@
+// Copyright (c) 2026 Microsoft GmbH.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#include "shell/common/command_line_util_win.h"
+
+#include <string>
+
+namespace electron {
+
+std::wstring AddQuoteForArg(const std::wstring& arg) {
+  // We follow the quoting rules of CommandLineToArgvW.
+  // http://msdn.microsoft.com/en-us/library/17w5ykft.aspx
+  constexpr wchar_t kQuotableChars[] = L" \\\"";
+  if (arg.find_first_of(kQuotableChars) == std::wstring::npos) {
+    // No quoting necessary.
+    return arg;
+  }
+
+  std::wstring out;
+  out.push_back(L'"');
+  for (size_t i = 0; i < arg.size(); ++i) {
+    if (arg[i] == '\\') {
+      // Find the extent of this run of backslashes.
+      size_t start = i, end = start + 1;
+      for (; end < arg.size() && arg[end] == '\\'; ++end) {
+      }
+      size_t backslash_count = end - start;
+
+      // Backslashes are escapes only if the run is followed by a double quote.
+      // Since we also will end the string with a double quote, we escape for
+      // either a double quote or the end of the string.
+      if (end == arg.size() || arg[end] == '"') {
+        // To quote, we need to output 2x as many backslashes.
+        backslash_count *= 2;
+      }
+      for (size_t j = 0; j < backslash_count; ++j)
+        out.push_back('\\');
+
+      // Advance i to one before the end to balance i++ in loop.
+      i = end - 1;
+    } else if (arg[i] == '"') {
+      out.push_back('\\');
+      out.push_back('"');
+    } else {
+      out.push_back(arg[i]);
+    }
+  }
+  out.push_back('"');
+
+  return out;
+}
+
+}  // namespace electron

filename: shell/common/command_line_util_win.h
patch: @@ -0,0 +1,20 @@
+// Copyright (c) 2026 Microsoft GmbH.
+// Use of this source code is governed by the MIT license that can be
+// found in the LICENSE file.
+
+#ifndef ELECTRON_SHELL_COMMON_COMMAND_LINE_UTIL_WIN_H_
+#define ELECTRON_SHELL_COMMON_COMMAND_LINE_UTIL_WIN_H_
+
+#include <string>
+
+namespace electron {
+
+// Quotes |arg| using CommandLineToArgvW-compatible quoting rules so that
+// the argument round-trips correctly through CreateProcess →
+// CommandLineToArgvW. If no quoting is necessary the string is returned
+// unchanged. See http://msdn.microsoft.com/en-us/library/17w5ykft.aspx
+std::wstring AddQuoteForArg(const std::wstring& arg);
+
+}  // namespace electron
+
+#endif  // ELECTRON_SHELL_COMMON_COMMAND_LINE_UTIL_WIN_H_