Summary
LocalBrowserWatchdog._kill_stale_chrome_for_profile() scans all running
processes and sends SIGKILL to every Chrome/Chromium/Brave whose
--user-data-dir resolves to the profile it is about to use. There is no check
for whether that process belongs to this instance, whether it is still alive and
healthy, or whether it is currently writing to the profile.
With a single instance this is harmless housekeeping. With two or more
instances sharing a profile — which is the default for the MCP server, since
every server defaults to ~/.config/openbrowser/profiles/default
(openbrowser/mcp/server.py:314) — each instance's first browser call kills the
other's live browser.
Where
- Kill loop, no ownership/liveness check:
openbrowser/browser/watchdogs/local_browser_watchdog.py:542-573
- Called before every launch:
local_browser_watchdog.py:137
- Called again from CDP recovery, with the default profile hardcoded as fallback:
openbrowser/mcp/server.py:392-398
- Called a third time from the launch-retry path after profile errors:
local_browser_watchdog.py:233
- Shared default profile +
keep_alive: True: openbrowser/mcp/server.py:313-314
Why it matters beyond a lost browser
keep_alive: True means the browser outlives the call and keeps holding the
profile, so the kill lands on a browser that is alive but idle — including while
Chrome is flushing cookies, Local State and LevelDB right after finishing work.
A SIGKILL at that moment gives Chrome no opportunity to complete those writes.
On our workstation this is not hypothetical: 13 MCP server processes were running
concurrently, all pointing at the same profile — one whose months-old browsing
state (cookies, site reputation) is expensive to recreate.
Reproduction
- Start two MCP servers with the default configuration (both therefore use
~/.config/openbrowser/profiles/default).
- In server A, run any
execute_code that navigates — a browser launches and
stays alive (keep_alive).
- In server B, run any
execute_code. Its _ensure_namespace() →
_launch_browser() → _kill_stale_chrome_for_profile() SIGKILLs A's browser
before starting its own.
- Server A's next call hits a dead CDP connection, recovers, and kills B's
browser in turn.
Evidence that SIGKILL skips Chrome's shutdown path
Chrome writes profile.exit_type into <profile>/Default/Preferences:
"Crashed" while running, rewritten on shutdown. Measured on throwaway profiles
(Chrome 150.0.7871.186):
| Signal |
exit_type after the process is gone |
| SIGKILL (current behaviour) |
Crashed |
| SIGTERM, then SIGKILL after a 5s grace window |
SessionEnded |
That the value was rewritten in the second case is the point: Chrome ran its
shutdown path and flushed.
Proposed fix
Send SIGTERM first and escalate to SIGKILL only for the processes we signalled,
and only if the already existing 5-second wait loop
(local_browser_watchdog.py:581-601) still finds them. This keeps the function's
contract intact — it still returns only once the profile lock is free — while
giving a healthy browser the chance to shut down cleanly. It also preserves the
behaviour the kill exists for (reaping browsers left behind by a crashed server),
because an unresponsive process is still killed after the grace window.
I have this running locally; a PR with the fix and regression tests is ready and
will follow this issue.
Environment
openbrowser-ai 0.1.50 (current release), Python 3.13, Chrome 150.0.7871.186,
Linux. The same code is present in main at the time of writing.
Summary
LocalBrowserWatchdog._kill_stale_chrome_for_profile()scans all runningprocesses and sends SIGKILL to every Chrome/Chromium/Brave whose
--user-data-dirresolves to the profile it is about to use. There is no checkfor whether that process belongs to this instance, whether it is still alive and
healthy, or whether it is currently writing to the profile.
With a single instance this is harmless housekeeping. With two or more
instances sharing a profile — which is the default for the MCP server, since
every server defaults to
~/.config/openbrowser/profiles/default(
openbrowser/mcp/server.py:314) — each instance's first browser call kills theother's live browser.
Where
openbrowser/browser/watchdogs/local_browser_watchdog.py:542-573local_browser_watchdog.py:137openbrowser/mcp/server.py:392-398local_browser_watchdog.py:233keep_alive: True:openbrowser/mcp/server.py:313-314Why it matters beyond a lost browser
keep_alive: Truemeans the browser outlives the call and keeps holding theprofile, so the kill lands on a browser that is alive but idle — including while
Chrome is flushing cookies,
Local Stateand LevelDB right after finishing work.A SIGKILL at that moment gives Chrome no opportunity to complete those writes.
On our workstation this is not hypothetical: 13 MCP server processes were running
concurrently, all pointing at the same profile — one whose months-old browsing
state (cookies, site reputation) is expensive to recreate.
Reproduction
~/.config/openbrowser/profiles/default).execute_codethat navigates — a browser launches andstays alive (
keep_alive).execute_code. Its_ensure_namespace()→_launch_browser()→_kill_stale_chrome_for_profile()SIGKILLs A's browserbefore starting its own.
browser in turn.
Evidence that SIGKILL skips Chrome's shutdown path
Chrome writes
profile.exit_typeinto<profile>/Default/Preferences:"Crashed"while running, rewritten on shutdown. Measured on throwaway profiles(Chrome 150.0.7871.186):
exit_typeafter the process is goneCrashedSessionEndedThat the value was rewritten in the second case is the point: Chrome ran its
shutdown path and flushed.
Proposed fix
Send SIGTERM first and escalate to SIGKILL only for the processes we signalled,
and only if the already existing 5-second wait loop
(
local_browser_watchdog.py:581-601) still finds them. This keeps the function'scontract intact — it still returns only once the profile lock is free — while
giving a healthy browser the chance to shut down cleanly. It also preserves the
behaviour the kill exists for (reaping browsers left behind by a crashed server),
because an unresponsive process is still killed after the grace window.
I have this running locally; a PR with the fix and regression tests is ready and
will follow this issue.
Environment
openbrowser-ai 0.1.50 (current release), Python 3.13, Chrome 150.0.7871.186,
Linux. The same code is present in
mainat the time of writing.