diff --git a/README.md b/README.md
index e7756af..1244383 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,34 @@ npm install -g badclaude
badclaude
```
+## Platform support
+
+- Windows: full support.
+- macOS: full support after granting Accessibility permissions to the terminal or packaged app.
+- Linux X11/Xorg: full whip mode when `xdotool` is installed.
+- Linux Wayland: the app prefers X11/XWayland automatically when available so the overlay can render more reliably; global typing into native Wayland apps may still be unavailable by design.
+
+### Linux notes
+
+For full Linux automation on X11/Xorg, install `xdotool` first.
+
+Examples:
+
+```bash
+# Debian/Ubuntu
+sudo apt install xdotool
+
+# Fedora
+sudo dnf install xdotool
+
+# Arch
+sudo pacman -S xdotool
+```
+
+Wayland sessions can still run the app, but some desktops block the exact cross-app keyboard automation this toy relies on. On GNOME-like desktops, tray support may also depend on AppIndicator/StatusNotifier extensions.
+
+On GNOME, badclaude also shows a small floating launcher window because the tray icon is often hidden entirely.
+
## Controls
- Click tray icon: spawn whip.
@@ -24,4 +52,4 @@ badclaude
- [x] Cease and desist letter from Anthropic
- [ ] Crypto miner
- [ ] Logs of how many times you whipped claude so when the robots come we can order people nicely for them
-- [ ] Updated whip physics
\ No newline at end of file
+- [ ] Updated whip physics
diff --git a/bin/badclaude.js b/bin/badclaude.js
index 29f8c7e..0f4341e 100644
--- a/bin/badclaude.js
+++ b/bin/badclaude.js
@@ -11,9 +11,16 @@ try {
}
const appPath = path.resolve(__dirname, '..');
+const extraArgs = process.argv.slice(2);
+const env = { ...process.env };
-const child = spawn(electronBinary, [appPath], {
+if (process.platform === 'linux' && env.WAYLAND_DISPLAY && env.DISPLAY && !env.ELECTRON_OZONE_PLATFORM_HINT) {
+ env.ELECTRON_OZONE_PLATFORM_HINT = 'x11';
+}
+
+const child = spawn(electronBinary, [appPath, ...extraArgs], {
detached: true,
+ env,
stdio: 'ignore',
windowsHide: true,
});
diff --git a/launcher.html b/launcher.html
new file mode 100644
index 0000000..8a009ec
--- /dev/null
+++ b/launcher.html
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main.js b/main.js
index 526d9ec..d3848b7 100644
--- a/main.js
+++ b/main.js
@@ -3,6 +3,33 @@ const path = require('path');
const fs = require('fs');
const os = require('os');
const { execFile } = require('child_process');
+const { promisify } = require('util');
+
+const execFileAsync = promisify(execFile);
+const linuxSessionType = process.platform === 'linux'
+ ? ((process.env.XDG_SESSION_TYPE || '').toLowerCase() || (process.env.WAYLAND_DISPLAY ? 'wayland' : (process.env.DISPLAY ? 'x11' : 'unknown')))
+ : 'unsupported';
+const initialLinuxDesktop = process.platform === 'linux'
+ ? `${process.env.XDG_CURRENT_DESKTOP || ''}:${process.env.DESKTOP_SESSION || ''}`.toLowerCase()
+ : '';
+const preferLinuxX11 = process.platform === 'linux'
+ && linuxSessionType === 'wayland'
+ && Boolean(process.env.DISPLAY)
+ && !initialLinuxDesktop.includes('gnome');
+const linuxOzonePlatformHint = process.platform === 'linux'
+ ? (process.env.ELECTRON_OZONE_PLATFORM_HINT || '').trim()
+ : '';
+const hasExplicitLinuxOzonePlatform = process.platform === 'linux'
+ && (Boolean(linuxOzonePlatformHint) || app.commandLine.hasSwitch('ozone-platform'));
+const forceLinuxX11 = preferLinuxX11 && !hasExplicitLinuxOzonePlatform;
+
+if (process.platform === 'linux') {
+ app.disableHardwareAcceleration();
+ if (forceLinuxX11) {
+ app.commandLine.appendSwitch('ozone-platform', 'x11');
+ }
+ app.commandLine.appendSwitch('enable-transparent-visuals');
+}
// ── Win32 FFI (Windows only) ────────────────────────────────────────────────
let keybd_event, VkKeyScanA;
@@ -18,9 +45,19 @@ if (process.platform === 'win32') {
}
// ── Globals ─────────────────────────────────────────────────────────────────
-let tray, overlay;
+let tray, overlay, launcher;
let overlayReady = false;
let spawnQueued = false;
+const linuxDesktop = initialLinuxDesktop;
+const linuxState = {
+ sessionType: detectLinuxSessionType(),
+ prefersX11: forceLinuxX11,
+ automationBackend: 'none',
+ usesOverlayFallback: process.env.BADCLAUDE_OPAQUE_OVERLAY === '1',
+ needsLauncher: linuxDesktop.includes('gnome'),
+ previousWindowId: null,
+ warned: new Set(),
+};
const VK_CONTROL = 0x11;
const VK_RETURN = 0x0D;
@@ -29,32 +66,188 @@ const VK_MENU = 0x12; // Alt
const VK_TAB = 0x09;
const KEYUP = 0x0002;
+function detectLinuxSessionType() {
+ if (process.platform !== 'linux') return 'unsupported';
+ if (forceLinuxX11) return 'x11';
+ const session = (process.env.XDG_SESSION_TYPE || '').toLowerCase();
+ if (session === 'wayland' || session === 'x11') return session;
+ if (process.env.WAYLAND_DISPLAY) return 'wayland';
+ if (process.env.DISPLAY) return 'x11';
+ return 'unknown';
+}
+
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+function formatCommandError(err) {
+ const stderr = err?.stderr?.toString().trim();
+ if (stderr) return stderr;
+ return err?.message || String(err);
+}
+
+function warnOnce(key, message) {
+ if (linuxState.warned.has(key)) return;
+ linuxState.warned.add(key);
+ console.warn(message);
+}
+
+async function execFileChecked(file, args, options = {}) {
+ return execFileAsync(file, args, {
+ timeout: 2500,
+ windowsHide: true,
+ ...options,
+ });
+}
+
+async function detectLinuxAutomationBackend() {
+ if (process.platform !== 'linux' || !process.env.DISPLAY) {
+ return 'none';
+ }
+ try {
+ const { stdout } = await execFileChecked('xdotool', ['getactivewindow']);
+ if (stdout.trim()) return 'xdotool';
+ } catch {
+ return 'none';
+ }
+ return 'none';
+}
+
+async function detectLinuxNeedsLauncher() {
+ if (process.platform !== 'linux' || !linuxDesktop.includes('gnome')) {
+ return false;
+ }
+
+ try {
+ const { stdout } = await execFileChecked('gsettings', ['get', 'org.gnome.shell', 'enabled-extensions']);
+ const enabled = stdout.toLowerCase();
+ const hasTrayExtension = enabled.includes('appindicator')
+ || enabled.includes('kstatusnotifier')
+ || enabled.includes('trayicons');
+ return !hasTrayExtension;
+ } catch {
+ return true;
+ }
+}
+
+async function refreshLinuxAutomationBackend() {
+ if (process.platform !== 'linux') return 'none';
+ linuxState.sessionType = detectLinuxSessionType();
+ linuxState.automationBackend = await detectLinuxAutomationBackend();
+ return linuxState.automationBackend;
+}
+
+async function capturePreviousAppContext() {
+ if (process.platform !== 'linux') return;
+ linuxState.previousWindowId = null;
+ if (linuxState.automationBackend === 'none') {
+ await refreshLinuxAutomationBackend();
+ }
+ if (linuxState.automationBackend !== 'xdotool') return;
+ try {
+ const { stdout } = await execFileChecked('xdotool', ['getactivewindow']);
+ const windowId = stdout.trim();
+ if (windowId) linuxState.previousWindowId = windowId;
+ } catch (err) {
+ warnOnce('linux-window-capture', `badclaude: could not capture the previous Linux window: ${formatCommandError(err)}`);
+ }
+}
+
+async function activateLinuxWindow(windowId) {
+ if (!windowId) return;
+ await execFileChecked('xdotool', ['windowactivate', '--sync', windowId]);
+}
+
+async function sendMacroLinux(text) {
+ if (linuxState.automationBackend === 'none') {
+ await refreshLinuxAutomationBackend();
+ }
+
+ if (linuxState.automationBackend !== 'xdotool') {
+ const message = linuxState.sessionType === 'wayland'
+ ? 'badclaude: Linux automation is unavailable on this Wayland session. The overlay still works, but full whip mode needs X11/xdotool.'
+ : 'badclaude: Linux automation is unavailable because xdotool could not be used in this session.';
+ warnOnce('linux-automation-unavailable', message);
+ return;
+ }
+
+ if (!linuxState.previousWindowId) {
+ warnOnce('linux-no-target-window', 'badclaude: no previous Linux window was captured, so the whip macro was skipped.');
+ return;
+ }
+
+ try {
+ await activateLinuxWindow(linuxState.previousWindowId);
+ await sleep(40);
+ await execFileChecked('xdotool', ['key', '--clearmodifiers', '--window', linuxState.previousWindowId, 'ctrl+c']);
+ await sleep(30);
+ await execFileChecked('xdotool', ['type', '--delay', '1', '--clearmodifiers', '--window', linuxState.previousWindowId, text]);
+ await sleep(30);
+ await execFileChecked('xdotool', ['key', '--clearmodifiers', '--window', linuxState.previousWindowId, 'Return']);
+ } catch (err) {
+ warnOnce('linux-macro-failed', `badclaude: Linux whip macro failed: ${formatCommandError(err)}`);
+ }
+}
+
+async function initializeLinuxSupport() {
+ if (process.platform !== 'linux') return;
+ await refreshLinuxAutomationBackend();
+ linuxState.needsLauncher = await detectLinuxNeedsLauncher();
+ console.log(`badclaude: linux session=${linuxSessionType}, windowing=${linuxState.sessionType}, automation=${linuxState.automationBackend}`);
+ if (linuxState.prefersX11) {
+ console.log('badclaude: forcing Electron onto X11/XWayland so the overlay can render on Wayland desktops that expose DISPLAY.');
+ }
+ if (linuxSessionType === 'wayland' && !linuxState.prefersX11) {
+ console.log('badclaude: keeping native Wayland on this desktop so the overlay can stay compositor-native.');
+ }
+ if (linuxState.needsLauncher) {
+ console.log('badclaude: GNOME detected, enabling the floating launcher because tray icons are often hidden.');
+ console.log('badclaude: for a more integrated tray experience on GNOME, install/enable the "AppIndicator and KStatusNotifierItem Support" extension.');
+ }
+}
+
/** One Alt+Tab / Cmd+Tab so focus returns to the previously active app after tray click. */
-function refocusPreviousApp() {
- const delayMs = 80;
- const run = () => {
- if (process.platform === 'win32') {
- if (!keybd_event) return;
- keybd_event(VK_MENU, 0, 0, 0);
- keybd_event(VK_TAB, 0, 0, 0);
- keybd_event(VK_TAB, 0, KEYUP, 0);
- keybd_event(VK_MENU, 0, KEYUP, 0);
- } else if (process.platform === 'darwin') {
- const script = [
- 'tell application "System Events"',
- ' key down command',
- ' key code 48', // Tab
- ' key up command',
- 'end tell',
- ].join('\n');
- execFile('osascript', ['-e', script], err => {
- if (err) {
- console.warn('refocus previous app (Cmd+Tab) failed:', err.message);
- }
- });
+async function refocusPreviousApp() {
+ await sleep(80);
+ if (process.platform === 'win32') {
+ if (!keybd_event) return;
+ keybd_event(VK_MENU, 0, 0, 0);
+ keybd_event(VK_TAB, 0, 0, 0);
+ keybd_event(VK_TAB, 0, KEYUP, 0);
+ keybd_event(VK_MENU, 0, KEYUP, 0);
+ return;
+ }
+ if (process.platform === 'darwin') {
+ const script = [
+ 'tell application "System Events"',
+ ' key down command',
+ ' key code 48', // Tab
+ ' key up command',
+ 'end tell',
+ ].join('\n');
+ try {
+ await execFileChecked('osascript', ['-e', script]);
+ } catch (err) {
+ console.warn('refocus previous app (Cmd+Tab) failed:', formatCommandError(err));
}
- };
- setTimeout(run, delayMs);
+ return;
+ }
+ if (process.platform === 'linux') {
+ if (linuxState.automationBackend === 'none') {
+ await refreshLinuxAutomationBackend();
+ }
+ if (linuxState.automationBackend !== 'xdotool' || !linuxState.previousWindowId) {
+ if (linuxState.sessionType === 'wayland') {
+ warnOnce('linux-refocus-unavailable', 'badclaude: Linux focus restore is limited on Wayland, so the overlay may stay in front until you click away.');
+ }
+ return;
+ }
+ try {
+ await activateLinuxWindow(linuxState.previousWindowId);
+ } catch (err) {
+ warnOnce('linux-refocus-failed', `badclaude: could not refocus the previous Linux window: ${formatCommandError(err)}`);
+ }
+ }
}
function createTrayIconFallback() {
@@ -116,14 +309,17 @@ async function getTrayIcon() {
// ── Overlay window ──────────────────────────────────────────────────────────
function createOverlay() {
- const { bounds } = screen.getPrimaryDisplay();
+ const { bounds } = screen.getDisplayNearestPoint(screen.getCursorScreenPoint());
+ const useLinuxOverlayFallback = process.platform === 'linux' && linuxState.usesOverlayFallback;
overlay = new BrowserWindow({
x: bounds.x, y: bounds.y,
width: bounds.width, height: bounds.height,
- transparent: true,
+ show: false,
+ transparent: !useLinuxOverlayFallback,
+ backgroundColor: useLinuxOverlayFallback ? '#12000000' : '#00ffffff',
frame: false,
alwaysOnTop: true,
- focusable: false,
+ focusable: useLinuxOverlayFallback,
skipTaskbar: true,
resizable: false,
hasShadow: false,
@@ -132,14 +328,29 @@ function createOverlay() {
},
});
overlay.setAlwaysOnTop(true, 'screen-saver');
+ overlay.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
+ overlay.setFullScreenable(false);
+ if (useLinuxOverlayFallback) {
+ overlay.setOpacity(0.985);
+ }
overlayReady = false;
overlay.loadFile('overlay.html');
+ overlay.once('ready-to-show', () => {
+ if (overlay && !overlay.isVisible()) {
+ overlay.show();
+ }
+ });
overlay.webContents.on('did-finish-load', () => {
overlayReady = true;
+ if (process.platform === 'linux') {
+ overlay.webContents.send('set-linux-overlay-mode', {
+ fallback: useLinuxOverlayFallback,
+ });
+ }
if (spawnQueued && overlay && overlay.isVisible()) {
spawnQueued = false;
overlay.webContents.send('spawn-whip');
- refocusPreviousApp();
+ void refocusPreviousApp();
}
});
overlay.on('closed', () => {
@@ -149,16 +360,68 @@ function createOverlay() {
});
}
-function toggleOverlay() {
+function createLauncherWindow() {
+ if (process.platform !== 'linux' || !linuxState.needsLauncher || launcher) return;
+ const workArea = screen.getPrimaryDisplay().workArea;
+ launcher = new BrowserWindow({
+ x: workArea.x + workArea.width - 122,
+ y: workArea.y + 18,
+ width: 104,
+ height: 132,
+ show: false,
+ frame: false,
+ transparent: true,
+ resizable: false,
+ movable: true,
+ minimizable: false,
+ maximizable: false,
+ fullscreenable: false,
+ skipTaskbar: true,
+ alwaysOnTop: true,
+ title: 'badclaude launcher',
+ webPreferences: {
+ preload: path.join(__dirname, 'preload.js'),
+ },
+ });
+ launcher.setAlwaysOnTop(true, 'floating');
+ launcher.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
+ launcher.removeMenu();
+ launcher.loadFile('launcher.html');
+ launcher.once('ready-to-show', () => {
+ launcher?.show();
+ });
+ launcher.on('closed', () => {
+ launcher = null;
+ });
+}
+
+function showLauncherWindow() {
+ createLauncherWindow();
+ if (!launcher) return;
+ launcher.show();
+ launcher.moveTop();
+ launcher.focus();
+}
+
+function hideLauncherWindow() {
+ launcher?.hide();
+}
+
+async function toggleOverlay() {
if (overlay && overlay.isVisible()) {
overlay.webContents.send('drop-whip');
return;
}
+ await capturePreviousAppContext();
if (!overlay) createOverlay();
overlay.show();
+ overlay.moveTop();
+ if (process.platform === 'linux' && linuxState.usesOverlayFallback) {
+ overlay.focus();
+ }
if (overlayReady) {
overlay.webContents.send('spawn-whip');
- refocusPreviousApp();
+ void refocusPreviousApp();
} else {
spawnQueued = true;
}
@@ -166,16 +429,20 @@ function toggleOverlay() {
// ── IPC ─────────────────────────────────────────────────────────────────────
ipcMain.on('whip-crack', () => {
- try {
- sendMacro();
- } catch (err) {
+ void sendMacro().catch(err => {
console.warn('sendMacro failed:', err?.message || err);
- }
+ });
});
ipcMain.on('hide-overlay', () => { if (overlay) overlay.hide(); });
+ipcMain.on('toggle-overlay', () => {
+ void toggleOverlay();
+});
+ipcMain.on('quit-app', () => {
+ app.quit();
+});
// ── Macro: immediate Ctrl+C, type "Go FASER", Enter ───────────────────────
-function sendMacro() {
+async function sendMacro() {
// Pick a random phrase from a list of similar phrases and type it out
const phrases = [
'FASTER',
@@ -191,7 +458,9 @@ function sendMacro() {
if (process.platform === 'win32') {
sendMacroWindows(chosen);
} else if (process.platform === 'darwin') {
- sendMacroMac(chosen);
+ await sendMacroMac(chosen);
+ } else if (process.platform === 'linux') {
+ await sendMacroLinux(chosen);
}
}
@@ -221,7 +490,7 @@ function sendMacroWindows(text) {
keybd_event(VK_RETURN, 0, KEYUP, 0);
}
-function sendMacroMac(text) {
+async function sendMacroMac(text) {
const escaped = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
const script = [
'tell application "System Events"',
@@ -232,23 +501,32 @@ function sendMacroMac(text) {
'end tell'
].join('\n');
- execFile('osascript', ['-e', script], err => {
- if (err) {
- console.warn('mac macro failed (enable Accessibility for terminal/app):', err.message);
- }
- });
+ try {
+ await execFileChecked('osascript', ['-e', script]);
+ } catch (err) {
+ console.warn('mac macro failed (enable Accessibility for terminal/app):', formatCommandError(err));
+ }
}
// ── App lifecycle ───────────────────────────────────────────────────────────
app.whenReady().then(async () => {
+ await initializeLinuxSupport();
tray = new Tray(await getTrayIcon());
tray.setToolTip('Bad Claude – click for whip');
tray.setContextMenu(
Menu.buildFromTemplate([
+ { label: 'Whip', click: () => void toggleOverlay() },
+ { type: 'separator' },
+ { label: 'Show Launcher', click: () => showLauncherWindow() },
+ { label: 'Hide Launcher', click: () => hideLauncherWindow() },
+ { type: 'separator' },
{ label: 'Quit', click: () => app.quit() },
])
);
- tray.on('click', toggleOverlay);
+ tray.on('click', () => {
+ void toggleOverlay();
+ });
+ createLauncherWindow();
});
app.on('window-all-closed', e => e.preventDefault()); // keep alive in tray
diff --git a/overlay.html b/overlay.html
index 2354b86..c6c66aa 100644
--- a/overlay.html
+++ b/overlay.html
@@ -2,8 +2,10 @@
@@ -85,16 +87,24 @@
let handleAngle = P.baseTargetAngle;
let handleAngVel = 0;
+function setCursorHidden(hidden) {
+ document.body.classList.toggle('whip-active', hidden);
+}
+
const WHIP_CRACK_SOUNDS = ['sounds/A.mp3', 'sounds/B.mp3', 'sounds/C.mp3', 'sounds/D.mp3', 'sounds/E.mp3'];
document.addEventListener('mousemove', e => { mouseX = e.clientX; mouseY = e.clientY; });
document.addEventListener('mousedown', () => {
- if (whip && !dropping) dropping = true;
+ if (whip && !dropping) {
+ dropping = true;
+ setCursorHidden(false);
+ }
});
// ── Whip creation ───────────────────────────────────────────────────────────
function spawnWhip(mx, my) {
dropping = false;
+ setCursorHidden(true);
lastCrackTime = 0;
whipSpawnTime = Date.now();
const pts = [];
@@ -370,6 +380,7 @@
if (dropping && whip.every(p => p.y > H + 60)) {
whip = null;
dropping = false;
+ setCursorHidden(false);
window.bridge.hideOverlay();
}
prevMouseX = mouseX;
@@ -439,6 +450,7 @@
window.bridge.onSpawnWhip(() => {
whip = spawnWhip(mouseX || W / 2, mouseY || H / 2);
dropping = false;
+ setCursorHidden(true);
prevMouseX = mouseX;
prevMouseY = mouseY;
handleAngle = P.baseTargetAngle;
@@ -446,7 +458,14 @@
});
window.bridge.onDropWhip(() => {
- if (whip && !dropping) dropping = true;
+ if (whip && !dropping) {
+ dropping = true;
+ setCursorHidden(false);
+ }
+});
+
+window.bridge.onLinuxOverlayMode?.(({ fallback }) => {
+ document.body.classList.toggle('linux-overlay-fallback', Boolean(fallback));
});
diff --git a/package-lock.json b/package-lock.json
index 5816cb2..edb6529 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,12 +7,21 @@
"": {
"name": "badclaude",
"version": "1.0.2",
+ "license": "MIT",
+ "os": [
+ "darwin",
+ "linux",
+ "win32"
+ ],
"dependencies": {
"electron": "^33.0.0",
"koffi": "^2.9.0"
},
"bin": {
"badclaude": "bin/badclaude.js"
+ },
+ "engines": {
+ "node": ">=18.0.0"
}
},
"node_modules/@electron/get": {
diff --git a/package.json b/package.json
index 463b497..0fd9ea8 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
},
"os": [
"darwin",
+ "linux",
"win32"
],
"engines": {
@@ -32,14 +33,16 @@
"main.js",
"preload.js",
"overlay.html",
+ "launcher.html",
+ "scripts/start.js",
"sounds",
"icon",
"bin/badclaude.js",
"README.md"
],
"scripts": {
- "start": "electron .",
- "dev": "electron .",
+ "start": "node scripts/start.js",
+ "dev": "node scripts/start.js",
"pack": "npm pack"
},
"dependencies": {
diff --git a/preload.js b/preload.js
index ae83618..a83942d 100644
--- a/preload.js
+++ b/preload.js
@@ -3,6 +3,9 @@ const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('bridge', {
whipCrack: () => ipcRenderer.send('whip-crack'),
hideOverlay: () => ipcRenderer.send('hide-overlay'),
+ toggleOverlay: () => ipcRenderer.send('toggle-overlay'),
+ quitApp: () => ipcRenderer.send('quit-app'),
onSpawnWhip: (fn) => ipcRenderer.on('spawn-whip', () => fn()),
onDropWhip: (fn) => ipcRenderer.on('drop-whip', () => fn()),
+ onLinuxOverlayMode: (fn) => ipcRenderer.on('set-linux-overlay-mode', (_event, payload) => fn(payload)),
});
diff --git a/scripts/start.js b/scripts/start.js
new file mode 100644
index 0000000..75d5f9f
--- /dev/null
+++ b/scripts/start.js
@@ -0,0 +1,38 @@
+#!/usr/bin/env node
+const path = require('path');
+const { spawn } = require('child_process');
+
+let electronBinary;
+try {
+ electronBinary = require('electron');
+} catch (err) {
+ console.error('Could not load Electron. Run `npm install` first.');
+ process.exit(1);
+}
+
+const appPath = path.resolve(__dirname, '..');
+const extraArgs = process.argv.slice(2);
+const env = { ...process.env };
+
+if (process.platform === 'linux' && env.WAYLAND_DISPLAY && env.DISPLAY && !env.ELECTRON_OZONE_PLATFORM_HINT) {
+ env.ELECTRON_OZONE_PLATFORM_HINT = 'x11';
+}
+
+const child = spawn(electronBinary, [appPath, ...extraArgs], {
+ env,
+ stdio: 'inherit',
+ windowsHide: true,
+});
+
+child.on('error', err => {
+ console.error('Failed to start badclaude:', err.message);
+ process.exit(1);
+});
+
+child.on('exit', (code, signal) => {
+ if (signal) {
+ process.kill(process.pid, signal);
+ return;
+ }
+ process.exit(code ?? 0);
+});