Recently Written · git

deploy-handoff

Claude Code plugin: the agent drives the browser to the last deploy, billing, or pull request step. You make the final click.

git clone https://github.com/equwal/deploy-handoff

Log | Files | Refs


commit 655a10406e1f1e4ea35a6805fc4d9b115bcd5552
equwal <13551856+equwal@users.noreply.github.com>
2026-09-26 13:34:05 -0700

Keep the agent browser running when its window closes

The browser that "handoff.py browser" starts stopped when its last
window closed. On 2026-09-26 this occurred two times in one task. The
window of the agent browser looks like the other Brave windows on the
shared desktop, and a person or a program closed it. Then the agent got
"Failed to open a new tab" and "connect ECONNREFUSED 127.0.0.1:9333".

After each stop, the profile showed a normal exit and no crash report.
The end of the tool call did not stop the browser: a browser started
from Git Bash in a tool call ran for more than 11 minutes after the call
ended.

Start the browser with --no-startup-window. With this flag and a remote
debugging port, Chromium keeps the browser running with no window. The
agent opens a window when it opens a page.

The new test starts the real browser with a temporary profile and closes
all of its tabs. Then it makes sure that the browser still runs and can
open a new tab. The test fails without the flag. README tells how to
stop the browser, because a click on the close button no longer does.

 README.md                             |  6 ++--
 skills/deploy-handoff/handoff.py      |  4 +++
 skills/deploy-handoff/test_handoff.py | 55 ++++++++++++++++++++++++++++++++++-
 3 files changed, 62 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 5eb1f30..9601039 100644
--- a/README.md
+++ b/README.md
@@ -65,7 +65,9 @@ The command starts Brave, or Chrome if Brave is not installed. `--exe PATH` name
 
 Sign in to each console one time in this browser. The profile keeps the sign-in. When the agent finds a sign-in page, it gives the sign-in to you as a human step.
 
-Any program on your computer can control this browser through the port. Use the profile only for deploy consoles. Close the browser when you do not need it.
+The browser starts with no window. The agent opens a window when it opens a page. If you close the window, the browser does not stop, and the agent can open a new window.
+
+Any program on your computer can control this browser through the port. Use the profile only for deploy consoles. When you do not need the browser, stop its process: the `brave.exe` or `chrome.exe` process with `--remote-debugging-port=9333` in its command line. On Windows, Task Manager shows the command line on the Details tab when you add the "Command line" column. On macOS and Linux, run `pkill -f -- --remote-debugging-port=9333`.
 
 The agent gets only the tools that look at the page and act on it. It gets no tool that runs code in the page or reads the network traffic, and no shell, file, registry, or clipboard tool. It can upload only files in the project folder.
 
@@ -94,7 +96,7 @@ python3 -m venv .venv
 .venv/bin/python -m unittest discover -s skills/deploy-handoff
 ```
 
-On Windows, use `.venv\Scripts\python`. `pip install --group` needs pip 25.1 or later. The dialog tests show small windows for a short time.
+On Windows, use `.venv\Scripts\python`. `pip install --group` needs pip 25.1 or later. The dialog tests show small windows for a short time. One browser test starts Brave or Chrome with a temporary profile. If you have neither, unittest skips that test.
 
 ## More projects
 
diff --git a/skills/deploy-handoff/handoff.py b/skills/deploy-handoff/handoff.py
index 7273844..c272f56 100644
--- a/skills/deploy-handoff/handoff.py
+++ b/skills/deploy-handoff/handoff.py
@@ -196,6 +196,10 @@ def browser_command(exe: Path, profile: Path, port: int) -> list[str]:
         str(exe),
         f"--user-data-dir={profile}",
         f"--remote-debugging-port={port}",
+        # Start with no window. With this flag and a remote debugging port, Chromium keeps
+        # running after its last window closes. Without it, the browser stops when a person or
+        # a program closes its window, and the agent cannot connect to the port.
+        "--no-startup-window",
         "--no-first-run",
         "--no-default-browser-check",
         # Keep the pages active when other windows cover the window of this browser.
diff --git a/skills/deploy-handoff/test_handoff.py b/skills/deploy-handoff/test_handoff.py
index 2a724fd..7c24554 100644
--- a/skills/deploy-handoff/test_handoff.py
+++ b/skills/deploy-handoff/test_handoff.py
@@ -1,15 +1,18 @@
 """Tests for handoff.py. README.md tells how to run them.
 
-The dialog tests show small windows for a short time.
+The dialog tests show small windows for a short time. BrowserWindowTest starts Brave or
+Chrome with a temporary profile, and shows its window for a short time.
 """
 
 import http.server
 import json
+import socket
 import subprocess
 import tempfile
 import threading
 import tkinter as tk
 import unittest
+import urllib.request
 from contextlib import redirect_stdout
 from io import StringIO
 from pathlib import Path
@@ -304,6 +307,56 @@ class BrowserTest(unittest.TestCase):
         self.assertIn(f"http://127.0.0.1:{handoff.DEBUG_PORT}", server["args"])
 
 
+def free_port() -> int:
+    """Return a port on 127.0.0.1 that no program uses now."""
+    with socket.socket() as sock:
+        sock.bind(("127.0.0.1", 0))
+        port: int = sock.getsockname()[1]
+    return port
+
+
+def devtools(port: int, path: str, method: str = "GET") -> str:
+    """Send one HTTP request to the remote debugging port of a browser. Return the body."""
+    request = urllib.request.Request(f"http://127.0.0.1:{port}{path}", method=method)
+    with handoff.LOCAL_OPENER.open(request, timeout=10) as response:
+        body: bytes = response.read()
+    return body.decode()
+
+
+class BrowserWindowTest(unittest.TestCase):
+    def test_command_starts_without_a_window(self) -> None:
+        command = handoff.browser_command(Path("brave.exe"), Path("profile"), 9333)
+        self.assertIn("--no-startup-window", command)
+
+    def test_keeps_running_when_its_last_window_closes(self) -> None:
+        # On 2026-09-26 the browser stopped two times, because its window closed. Then the
+        # agent got "Failed to open a new tab" and "connect ECONNREFUSED 127.0.0.1:9333".
+        try:
+            exe = handoff.find_browser(handoff.browser_candidates())
+        except HandoffError:
+            self.skipTest("Found no Brave or Chrome.")
+        folder = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
+        self.addCleanup(folder.cleanup)
+        port = free_port()
+        browser = subprocess.Popen(
+            handoff.browser_command(exe, Path(folder.name), port),
+            stdin=subprocess.DEVNULL,
+            stdout=subprocess.DEVNULL,
+            stderr=subprocess.DEVNULL,
+        )
+        self.addCleanup(browser.wait, 30)
+        self.addCleanup(browser.kill)
+        handoff.wait_for_browser(port, handoff.BROWSER_START_SECONDS)
+        # The agent opens a page. Then the user closes each tab, as the close button does.
+        devtools(port, "/json/new?about:blank", "PUT")
+        for target in json.loads(devtools(port, "/json/list")):
+            if target["type"] == "page":
+                devtools(port, f"/json/close/{target['id']}")
+        with self.assertRaises(subprocess.TimeoutExpired):
+            browser.wait(timeout=5)
+        self.assertIn("about:blank", devtools(port, "/json/new?about:blank", "PUT"))
+
+
 class GithubRepoTest(unittest.TestCase):
     @given(OWNER, NAME, st.sampled_from(REMOTE_FORMS))
     def test_round_trip(self, owner: str, name: str, form: str) -> None: