@@ -63,6 +63,39 @@ const DEFAULT_RECONNECT_CONFIG: ReconnectConfig = {
6363 maxDelay : 30000 ,
6464} ;
6565
66+ /**
67+ * Get the Node.js-compatible executable path for spawning child processes.
68+ *
69+ * On macOS in packaged mode, using `process.execPath` directly causes the
70+ * child process to appear as a separate dock icon (named "exec") because the
71+ * binary lives inside a `.app` bundle that macOS treats as a GUI application.
72+ *
73+ * To avoid this, we resolve the Electron Helper binary which has
74+ * `LSUIElement` set in its Info.plist, preventing dock icon creation.
75+ * Falls back to `process.execPath` if the Helper binary is not found.
76+ */
77+ function getNodeExecutablePath ( ) : string {
78+ if ( process . platform === 'darwin' && app . isPackaged ) {
79+ // Electron Helper binary lives at:
80+ // <App>.app/Contents/Frameworks/<ProductName> Helper.app/Contents/MacOS/<ProductName> Helper
81+ const appName = app . getName ( ) ;
82+ const helperName = `${ appName } Helper` ;
83+ const helperPath = path . join (
84+ path . dirname ( process . execPath ) , // .../Contents/MacOS
85+ '../Frameworks' ,
86+ `${ helperName } .app` ,
87+ 'Contents/MacOS' ,
88+ helperName ,
89+ ) ;
90+ if ( existsSync ( helperPath ) ) {
91+ logger . info ( `Using Electron Helper binary to avoid dock icon: ${ helperPath } ` ) ;
92+ return helperPath ;
93+ }
94+ logger . warn ( `Electron Helper binary not found at ${ helperPath } , falling back to process.execPath` ) ;
95+ }
96+ return process . execPath ;
97+ }
98+
6699/**
67100 * Gateway Manager
68101 * Handles starting, stopping, and communicating with the OpenClaw Gateway
@@ -377,11 +410,13 @@ export class GatewayManager extends EventEmitter {
377410 const gatewayArgs = [ 'gateway' , '--port' , String ( this . status . port ) , '--token' , gatewayToken , '--dev' , '--allow-unconfigured' ] ;
378411
379412 if ( app . isPackaged ) {
380- // Production: always use Electron binary as Node.js via ELECTRON_RUN_AS_NODE
413+ // Production: use Electron binary as Node.js via ELECTRON_RUN_AS_NODE
414+ // On macOS, use the Electron Helper binary to avoid extra dock icons
381415 if ( existsSync ( entryScript ) ) {
382- command = process . execPath ;
416+ command = getNodeExecutablePath ( ) ;
383417 args = [ entryScript , ...gatewayArgs ] ;
384418 logger . info ( 'Starting Gateway in PACKAGED mode (ELECTRON_RUN_AS_NODE)' ) ;
419+ logger . info ( `Using executable: ${ command } ` ) ;
385420 } else {
386421 const errMsg = `OpenClaw entry script not found at: ${ entryScript } ` ;
387422 logger . error ( errMsg ) ;
@@ -449,6 +484,15 @@ export class GatewayManager extends EventEmitter {
449484 // Critical: In packaged mode, make Electron binary act as Node.js
450485 if ( app . isPackaged ) {
451486 spawnEnv [ 'ELECTRON_RUN_AS_NODE' ] = '1' ;
487+ // Prevent OpenClaw entry.ts from respawning itself (which would create
488+ // another child process and a second "exec" dock icon on macOS)
489+ spawnEnv [ 'OPENCLAW_NO_RESPAWN' ] = '1' ;
490+ // Pre-set the NODE_OPTIONS that entry.ts would have added via respawn
491+ const existingNodeOpts = spawnEnv [ 'NODE_OPTIONS' ] ?? '' ;
492+ if ( ! existingNodeOpts . includes ( '--disable-warning=ExperimentalWarning' ) &&
493+ ! existingNodeOpts . includes ( '--no-warnings' ) ) {
494+ spawnEnv [ 'NODE_OPTIONS' ] = `${ existingNodeOpts } --disable-warning=ExperimentalWarning` . trim ( ) ;
495+ }
452496 }
453497
454498 this . process = spawn ( command , args , {
0 commit comments