Electron Stagewrightdocs

Launch, attach, or inject

Three ways to get a session against an Electron app, depending on who starts the process and what it exposes:

Your situation Tool Transport
Start a development entry (main) electron_launch Playwright Electron
Start a packaged executable (no main) electron_launch Raw CDP
The app is already running with a CDP debug port electron_attach Raw CDP
The app is already running without any debug flag electron_inject Node inspector

All three return { ok, session_id, transport, windows, capabilities }; the session_id threads through every later call.

Optional MCP progress

When an MCP host requests standard progress, lifecycle calls that cross 250 ms report privacy-safe semantic phases such as resolving the runtime, connecting to a CDP endpoint, registering the session, waiting for the initial renderer, or stopping the app. Discovery uses the same mechanism while scanning loopback endpoints. Calls that finish before the threshold remain silent, and force-kill or detach do not emit synthetic progress for work they cannot meaningfully subdivide. The final success/error envelope—not a progress notification—is always the completion signal.

Launch — Stagewright owns the process

electron_launch { "main": "/abs/path/to/app/main.js" }

Dev-server-backed apps (Vite/webpack dev mode): the window appears before the bundle loads, so raise readyTimeoutMs, or treat renderer_ready: false as "not yet" and wait for a known element:

electron_wait_for_selector { "selector": "#app", "state": "visible", "timeoutMs": 30000 }

Failure modes worth knowing: SINGLE_INSTANCE_LOCK (another copy of the app holds Electron's single-instance lock — close it first), ALREADY_RUNNING (one live session per server by default; pass allowMultiple: true to run several), LAUNCH_TIMEOUT (no window within timeoutMs, at most 50 000 ms so the launch ends before the dispatch backstop; retryable; usually a main process blocked before its first window, for example on an OS prompt under "credentialStore": "system" or on a startup dialog), and LAUNCH_FAILED (the packaged app could not start or exited before a driveable renderer appeared). FUSES_BLOCK_LAUNCH applies only to a main-based Playwright launch whose selected runtime disables Electron's Node CLI inspect fuse. For a shipped build, omit main and launch the executable over CDP instead. Unknown or unreadable fuse state does not block a Playwright launch. ELECTRON_RUNTIME_MISSING means a main-based launch without executablePath found no downloaded Electron binary for the server. Electron fetches it on first use, which would write to the MCP stream, so run install-electron with the server's --package list first. When the server was started with --app-root <dir>, launch paths outside that root are refused — useful when the operator wants to confine what an agent can start.

Launch a packaged app

electron_launch {
  "executablePath": "/Applications/Your App.app/Contents/MacOS/Your App"
}

Stagewright owns --remote-debugging-address and --remote-debugging-port for this path; do not pass either through args. The endpoint is loopback-only, each discovery probe is bounded, and a failed handshake reaps the process before returning. Electron documents --remote-debugging-port as the HTTP CDP endpoint switch.

Credential-store default

Every launch, development or packaged, defaults to "credentialStore": "testing":

This default is for reliable automation, not security verification. A mock macOS keychain or Linux basic store does not prove genuine OS-backed safeStorage sealing. Use "credentialStore": "system" when that behavior is part of the assertion; the launch can then surface an OS credential prompt and may require an already-unlocked test account. Chromium's own test launcher uses the same macOS/Linux defaults to prevent test UI and timeouts; Electron's safeStorage documentation describes the platform security difference and the Linux basic_text backend.

Use the target project's Electron runtime

An app that loads native addons can require the Electron ABI it was built against. Start the server with an operator-chosen app root, and launches use that project's Electron package:

electron-stagewright --app-root /absolute/path/to/your-app --tool-profile essential
electron_launch { "main": "/absolute/path/to/your-app/dist/main.js" }

With --app-root set, a main launch uses the Electron installed in that project by default and reports runtime_source: "project". When the project has no Electron of its own, the launch keeps the server's Electron and returns the reason in runtime_fallback. Pass runtime: "server" to use the server's Electron anyway, or runtime: "project" to fail instead of falling back.

The project runtime requires main and resolves Electron only below --app-root; it never accepts a root from a tool call. The resolver reads Electron package metadata rather than evaluating the target app or Electron package entry point, and rejects a package or binary that resolves outside the configured root. An explicit executablePath is still authoritative, so do not set both unless you deliberately want that explicit binary.

Because launches use the project's Electron, the server does not need an Electron package of its own: doctor reports a missing server Electron as a warning, not a failure, when the project's resolves and runs. Keep one in the server's package list only for runtime: "server", for a project Electron whose fuses block Playwright, or for projects without Electron.

Run a non-mutating comparison before launch when native modules are involved:

electron-stagewright doctor --json --app-root /absolute/path/to/your-app

The runtime.server and runtime.project fields report the core, Playwright, Electron, Node, V8, and NODE_MODULE_VERSION facts used for the comparison, plus a bounded inventory of potential native addons. A project-runtime warning is diagnostic: it does not prevent launch, but it tells you to use the app-local runtime or repair the project installation.

Attach — the app is running with a debug endpoint

Start your app with a CDP port (during development, usually a script flag):

your-electron-app --remote-debugging-port=9222

Find it, then attach:

electron_discover_running {}
electron_attach { "port": 9222 }

electron_discover_running scans the conventional loopback ports (9222–9225 by default) and returns { targets: [{ targetId, port, appName, pid }], scanned } — an empty result is unambiguous because scanned reports exactly what was probed. Attach accepts port (+ optional loopback host) or a full loopback cdpUrl. Three notes:

Inject — the app is running with no debug flag

electron_inject { "pid": 12345 }

Injection triggers the Node inspector inside the running main process and attaches to it — no restart, no pre-arranged flag. The session it produces drives the main process only: main evaluation (behind --allow-eval=main or bare --allow-eval), window listing, and main-process console capture. Renderer reads and interaction need a CDP endpoint — when you control how the app starts, prefer --remote-debugging-port + electron_attach.

The injected target is verified to belong to the pid you named (attaching to a different process that happens to own the default inspector port is refused with INJECT_FAILED). Main evaluation receives the electron module as electronApp, as on a launch session. On Windows the inject trigger is unreliable on some Electron versions; the same discovery path still attaches if the app was started with --inspect, and the error message says exactly that when the trigger fails.

When the connection drops

An attached CDP session survives a dropped debugging socket. The next call that touches the app reconnects (up to three attempts within 10 s), re-discovers the windows, re-selects the one you had selected (by id, or by URL and title if the app restarted), and resumes console, dialog, and network capture. Stored snapshots are discarded on reconnect, so take a fresh electron_snapshot before acting on refs. If the endpoint stays down, the call fails with a retryable CDP_DISCONNECTED and the next call tries again. Buffered console, dialog, and network reads never wait on a reconnect.

A launched session cannot reconnect: its connection only drops when the process exits or crashes. Calls that reach the app then fail with the non-retryable SESSION_DISCONNECTED; the buffered diagnostics stay readable. Release it with electron_stop and launch again.

Ending a session

Call What happens
electron_stop Graceful close, bounded by timeoutMs (default 10 s); escalates to SIGKILL on timeout and reports escalated: true.
electron_force_kill Straight to SIGKILL.
electron_detach Releases an attached/injected session without touching the app. Launch-owned Playwright and packaged-CDP sessions reject detach.

A stopped session's process is never orphaned: either the close landed, or the escalation reaped it. For attached sessions, escalation needs the pid you optionally passed at attach time.


Design background: the three-transport model and its capability matrix are ADR-003; target-runtime selection is ADR-024; launch preflight, ready-wait, and stop escalation semantics follow the agent-native principles in ADR-007. The model behind sessions, transports, and capabilities: Concepts.