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 0410ef7ba8957da5063c5a28a67ec1cb6b575f30
equwal <13551856+equwal@users.noreply.github.com>
2026-10-03 19:17:32 -0700

Report an error when no Wayland session can show bemenu

Over SSH on g, WAYLAND_DISPLAY is unset. bemenu then exits with code 1, the same code as Escape, so handoff.py told the agent that the user answered Not done. MenuDialog now refuses to start without WAYLAND_DISPLAY, and the agent gets status error. Release 0.6.1.

 .claude-plugin/marketplace.json       |  2 +-
 .claude-plugin/plugin.json            |  2 +-
 skills/deploy-handoff/handoff.py      |  3 +++
 skills/deploy-handoff/test_handoff.py | 11 +++++++++++
 4 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index c0655a2..60cfaf3 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -10,7 +10,7 @@
     {
       "name": "deploy-handoff",
       "description": "Drives your browser through deploy, billing, and sign-in pages, and opens pull request forms. Then a dialog shows you the final click. Claude never makes it for you.",
-      "version": "0.6.0",
+      "version": "0.6.1",
       "author": {
         "name": "equwal"
       },
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 8808ccd..c2d102c 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,6 +1,6 @@
 {
   "name": "deploy-handoff",
-  "version": "0.6.0",
+  "version": "0.6.1",
   "description": "Drives your browser through deploy, billing, and sign-in pages, and opens pull request forms. Then a dialog shows you the final click. Claude never makes it for you.",
   "author": {
     "name": "equwal",
diff --git a/skills/deploy-handoff/handoff.py b/skills/deploy-handoff/handoff.py
index e597666..e08a80e 100644
--- a/skills/deploy-handoff/handoff.py
+++ b/skills/deploy-handoff/handoff.py
@@ -529,6 +529,9 @@ class MenuDialog:
     def __init__(self, request: Request, timeout_minutes: float, reopen: Callable[[], None]):
         if shutil.which("bemenu") is None:
             raise HandoffError("Cannot show the steps: install Python Tk or bemenu.")
+        # Without a session, bemenu exits as if the user pressed Escape.
+        if not os.environ.get("WAYLAND_DISPLAY"):
+            raise HandoffError("Cannot show the steps: WAYLAND_DISPLAY is not set.")
         self.title = request.title
         self.deadline = time.monotonic() + timeout_minutes * 60
         steps = [f"{number}. {step}" for number, step in enumerate(request.steps, 1)]
diff --git a/skills/deploy-handoff/test_handoff.py b/skills/deploy-handoff/test_handoff.py
index 86ff32d..26223ca 100644
--- a/skills/deploy-handoff/test_handoff.py
+++ b/skills/deploy-handoff/test_handoff.py
@@ -654,6 +654,9 @@ class MenuDialogTest(unittest.TestCase):
         which = mock.patch("shutil.which", return_value="/usr/bin/bemenu")
         which.start()
         self.addCleanup(which.stop)
+        display = mock.patch.dict("os.environ", {"WAYLAND_DISPLAY": "wayland-1"})
+        display.start()
+        self.addCleanup(display.stop)
         self.request = Request("Publish 1.2", "https://github.com/o", ("Click it.",))
 
     def answer(self, *results: subprocess.CompletedProcess[str] | Exception) -> mock.MagicMock:
@@ -706,6 +709,14 @@ class MenuDialogTest(unittest.TestCase):
         ):
             handoff.MenuDialog(self.request, 1, lambda: None)
 
+    def test_no_wayland_session_is_an_error(self) -> None:
+        # Over SSH on g, bemenu exited with code 1 and the script said "not_done".
+        with (
+            mock.patch.dict("os.environ", {"WAYLAND_DISPLAY": ""}),
+            self.assertRaises(HandoffError),
+        ):
+            handoff.MenuDialog(self.request, 1, lambda: None)
+
 
 class MainTest(unittest.TestCase):
     """Test main with a fake browser and a fake dialog."""