From 4ff1729abca27ba7a7e9599f4d97cac5c0c15085 Mon Sep 17 00:00:00 2001 From: Eshan Gupta Date: Sun, 26 Apr 2026 23:05:17 +0530 Subject: [PATCH 1/3] feat: support remote Claude Code sessions via BUDDI_HOST env var Adds optional BUDDI_HOST and BUDDI_SOCKET environment variables to buddi-hook.py. When BUDDI_HOST is set (e.g. "localhost:9999"), the hook connects over TCP instead of the Unix socket, enabling Claude Code on a remote VM to forward hook events to Buddi on a local Mac via SSH reverse port-forwarding (typically bridged with socat on the Mac). Behavior is unchanged when neither variable is set. Co-Authored-By: Claude Opus 4.7 (1M context) --- buddi/Buddi/Resources/buddi-hook.py | 34 ++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/buddi/Buddi/Resources/buddi-hook.py b/buddi/Buddi/Resources/buddi-hook.py index a303349..5cbf3b3 100644 --- a/buddi/Buddi/Resources/buddi-hook.py +++ b/buddi/Buddi/Resources/buddi-hook.py @@ -1,18 +1,44 @@ #!/usr/bin/env python3 """ Buddi Hook -- Sends session state to Buddi.app via Unix socket +- Sends session state to Buddi.app via Unix socket (local) or TCP (remote) - For PermissionRequest: waits for user decision from the app + +Environment variables: + BUDDI_HOST If set (e.g. "localhost:9999"), connect over TCP instead of + a Unix socket. Intended for Claude Code running on a remote + VM with an SSH reverse port-forward back to the Mac running + Buddi. Always tunnel over SSH — events may include prompts, + tool calls, and file paths. + BUDDI_SOCKET Override the default Unix socket path (/tmp/buddi.sock). + Ignored when BUDDI_HOST is set. """ import json import os import socket import sys -SOCKET_PATH = "/tmp/buddi.sock" +BUDDI_SOCKET = os.environ.get("BUDDI_SOCKET", "/tmp/buddi.sock") +BUDDI_HOST = os.environ.get("BUDDI_HOST") TIMEOUT_SECONDS = 300 # 5 minutes for permission decisions +def _connect_to_buddi(): + """Open a connection to Buddi (TCP if BUDDI_HOST is set, else Unix socket).""" + if BUDDI_HOST: + host, sep, port = BUDDI_HOST.rpartition(":") + if not sep or not host: + raise OSError(f"BUDDI_HOST must be host:port, got {BUDDI_HOST!r}") + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(TIMEOUT_SECONDS) + sock.connect((host, int(port))) + else: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.settimeout(TIMEOUT_SECONDS) + sock.connect(BUDDI_SOCKET) + return sock + + def get_tty(): """Get the TTY of the Claude process (parent)""" import subprocess @@ -83,9 +109,7 @@ def get_cmux_surface(): def send_event(state): """Send event to app, return response if any""" try: - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.settimeout(TIMEOUT_SECONDS) - sock.connect(SOCKET_PATH) + sock = _connect_to_buddi() sock.sendall(json.dumps(state).encode()) # For permission requests, wait for response From 6b9b4d94ae4309475872b6cf9cc2fb47ac876961 Mon Sep 17 00:00:00 2001 From: Eshan Gupta Date: Sun, 26 Apr 2026 23:17:41 +0530 Subject: [PATCH 2/3] fix: handle invalid BUDDI_HOST port values gracefully Non-numeric or out-of-range port values would raise ValueError or OverflowError uncaught by send_event, crashing the hook with a traceback. Convert both to OSError so the existing handler silently drops the event the same way other connection failures do. Co-Authored-By: Claude Opus 4.7 (1M context) --- buddi/Buddi/Resources/buddi-hook.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/buddi/Buddi/Resources/buddi-hook.py b/buddi/Buddi/Resources/buddi-hook.py index 5cbf3b3..f8b4895 100644 --- a/buddi/Buddi/Resources/buddi-hook.py +++ b/buddi/Buddi/Resources/buddi-hook.py @@ -29,9 +29,15 @@ def _connect_to_buddi(): host, sep, port = BUDDI_HOST.rpartition(":") if not sep or not host: raise OSError(f"BUDDI_HOST must be host:port, got {BUDDI_HOST!r}") + try: + port_num = int(port) + except ValueError: + raise OSError(f"BUDDI_HOST port must be an integer, got {port!r}") + if not 0 < port_num <= 65535: + raise OSError(f"BUDDI_HOST port {port_num} out of range (1-65535)") sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(TIMEOUT_SECONDS) - sock.connect((host, int(port))) + sock.connect((host, port_num)) else: sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(TIMEOUT_SECONDS) From 0f8c91c27bafff5647d26058c9875b6a47ed67c7 Mon Sep 17 00:00:00 2001 From: Eshan Gupta Date: Mon, 27 Apr 2026 13:37:24 +0530 Subject: [PATCH 3/3] feat: warn when BUDDI_HOST is not a loopback address Sending hook events (which include prompts and tool inputs) over an untunneled non-loopback link is a data-exposure footgun. Print a non-blocking stderr warning at module load if BUDDI_HOST resolves to anything other than localhost / 127.0.0.1 / ::1, keeping the env var as an advanced escape hatch while flagging the risk. Co-Authored-By: Claude Opus 4.7 (1M context) --- buddi/Buddi/Resources/buddi-hook.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/buddi/Buddi/Resources/buddi-hook.py b/buddi/Buddi/Resources/buddi-hook.py index f8b4895..d4bf220 100644 --- a/buddi/Buddi/Resources/buddi-hook.py +++ b/buddi/Buddi/Resources/buddi-hook.py @@ -22,6 +22,16 @@ BUDDI_HOST = os.environ.get("BUDDI_HOST") TIMEOUT_SECONDS = 300 # 5 minutes for permission decisions +if BUDDI_HOST: + _host = BUDDI_HOST.rpartition(":")[0].strip("[]") + if _host not in ("localhost", "127.0.0.1", "::1"): + print( + f"buddi-hook: warning: BUDDI_HOST={BUDDI_HOST!r} is not a loopback " + "address; events contain prompts and tool inputs — only use over " + "an SSH tunnel.", + file=sys.stderr, + ) + def _connect_to_buddi(): """Open a connection to Buddi (TCP if BUDDI_HOST is set, else Unix socket)."""