|
1 | 1 | #!/usr/bin/env python3 |
2 | 2 | """ |
3 | 3 | Buddi Hook |
4 | | -- Sends session state to Buddi.app via Unix socket |
| 4 | +- Sends session state to Buddi.app via Unix socket (local) or TCP (remote) |
5 | 5 | - For PermissionRequest: waits for user decision from the app |
| 6 | +
|
| 7 | +Environment variables: |
| 8 | + BUDDI_HOST If set (e.g. "localhost:9999"), connect over TCP instead of |
| 9 | + a Unix socket. Intended for Claude Code running on a remote |
| 10 | + VM with an SSH reverse port-forward back to the Mac running |
| 11 | + Buddi. Always tunnel over SSH — events may include prompts, |
| 12 | + tool calls, and file paths. |
| 13 | + BUDDI_SOCKET Override the default Unix socket path (/tmp/buddi.sock). |
| 14 | + Ignored when BUDDI_HOST is set. |
6 | 15 | """ |
7 | 16 | import json |
8 | 17 | import os |
9 | 18 | import socket |
10 | 19 | import sys |
11 | 20 |
|
12 | | -SOCKET_PATH = "/tmp/buddi.sock" |
| 21 | +BUDDI_SOCKET = os.environ.get("BUDDI_SOCKET", "/tmp/buddi.sock") |
| 22 | +BUDDI_HOST = os.environ.get("BUDDI_HOST") |
13 | 23 | TIMEOUT_SECONDS = 300 # 5 minutes for permission decisions |
14 | 24 |
|
| 25 | +if BUDDI_HOST: |
| 26 | + _host = BUDDI_HOST.rpartition(":")[0].strip("[]") |
| 27 | + if _host not in ("localhost", "127.0.0.1", "::1"): |
| 28 | + print( |
| 29 | + f"buddi-hook: warning: BUDDI_HOST={BUDDI_HOST!r} is not a loopback " |
| 30 | + "address; events contain prompts and tool inputs — only use over " |
| 31 | + "an SSH tunnel.", |
| 32 | + file=sys.stderr, |
| 33 | + ) |
| 34 | + |
| 35 | + |
| 36 | +def _connect_to_buddi(): |
| 37 | + """Open a connection to Buddi (TCP if BUDDI_HOST is set, else Unix socket).""" |
| 38 | + if BUDDI_HOST: |
| 39 | + host, sep, port = BUDDI_HOST.rpartition(":") |
| 40 | + if not sep or not host: |
| 41 | + raise OSError(f"BUDDI_HOST must be host:port, got {BUDDI_HOST!r}") |
| 42 | + try: |
| 43 | + port_num = int(port) |
| 44 | + except ValueError: |
| 45 | + raise OSError(f"BUDDI_HOST port must be an integer, got {port!r}") |
| 46 | + if not 0 < port_num <= 65535: |
| 47 | + raise OSError(f"BUDDI_HOST port {port_num} out of range (1-65535)") |
| 48 | + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 49 | + sock.settimeout(TIMEOUT_SECONDS) |
| 50 | + sock.connect((host, port_num)) |
| 51 | + else: |
| 52 | + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
| 53 | + sock.settimeout(TIMEOUT_SECONDS) |
| 54 | + sock.connect(BUDDI_SOCKET) |
| 55 | + return sock |
| 56 | + |
15 | 57 |
|
16 | 58 | def get_tty(): |
17 | 59 | """Get the TTY of the Claude process (parent)""" |
@@ -83,9 +125,7 @@ def get_cmux_surface(): |
83 | 125 | def send_event(state): |
84 | 126 | """Send event to app, return response if any""" |
85 | 127 | try: |
86 | | - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) |
87 | | - sock.settimeout(TIMEOUT_SECONDS) |
88 | | - sock.connect(SOCKET_PATH) |
| 128 | + sock = _connect_to_buddi() |
89 | 129 | sock.sendall(json.dumps(state).encode()) |
90 | 130 |
|
91 | 131 | # For permission requests, wait for response |
|
0 commit comments