···11-# SSH Notifications Over SSH
22-33-## Goal
44-55-Show Supacode notifications when a coding agent is running on a remote machine over `ssh`.
66-77-## Status
88-99-This is still a design doc, not shipped behavior.
1010-1111-The current codebase remains socket-only for agent hooks:
1212-1313-- `AgentHookSettingsCommand` only generates Unix-socket commands
1414-- `ClaudeHookSettings` and `CodexHookSettings` only install those socket commands
1515-- there is no `SUPACODE_REMOTE_NOTIFICATIONS`, `SSH_CONNECTION`, or `SSH_TTY` transport switch in the app
1616-1717-## Decision
1818-1919-This is feasible.
2020-2121-The simplest path is to support remote notifications by emitting terminal notification escape sequences from the remote host and letting the existing local Ghostty -> Supacode notification path handle them.
2222-2323-Structured remote hook parity with the current local socket-based integration is also feasible, but it is a larger feature and should be treated as a second phase.
2424-2525-## Current Supacode Behavior
2626-2727-### Local terminal notifications already work
2828-2929-Supacode already receives terminal desktop notifications from Ghostty:
3030-3131-- `ThirdParty/ghostty/src/terminal/osc.zig`
3232-- `ThirdParty/ghostty/src/terminal/osc/parsers/osc9.zig`
3333-- `ThirdParty/ghostty/src/terminal/osc/parsers/rxvt_extension.zig`
3434-- `ThirdParty/ghostty/src/Surface.zig`
3535-- `supacode/Infrastructure/Ghostty/GhosttySurfaceBridge.swift`
3636-- `supacode/Features/Terminal/Models/WorktreeTerminalState.swift`
3737-- `supacode/Features/Terminal/BusinessLogic/WorktreeTerminalManager.swift`
3838-- `supacode/Features/App/Reducer/AppFeature.swift`
3939-- `supacode/Clients/Notifications/SystemNotificationClient.swift`
4040-4141-The flow is:
4242-4343-1. A process inside the terminal emits `OSC 9` or `OSC 777`.
4444-2. Ghostty parses the escape sequence into a desktop notification action.
4545-3. `GhosttySurfaceBridge` forwards the title/body into `WorktreeTerminalState`.
4646-4. `WorktreeTerminalState` stores the in-app notification and emits a terminal event.
4747-5. `AppFeature` decides whether to show a macOS system notification and/or sound.
4848-4949-This path is transport-agnostic. If the bytes arrive through a local shell or through a remote `ssh` PTY, the local Ghostty surface sees the same terminal stream.
5050-5151-### Local structured coding-agent hooks do not work remotely
5252-5353-The current Claude/Codex integrations are local-only.
5454-5555-Each terminal surface injects:
5656-5757-- `SUPACODE_WORKTREE_ID`
5858-- `SUPACODE_TAB_ID`
5959-- `SUPACODE_SURFACE_ID`
6060-- `SUPACODE_SOCKET_PATH`
6161-6262-Relevant files:
6363-6464-- `supacode/Features/Terminal/Models/WorktreeTerminalState.swift`
6565-- `supacode/Features/Settings/BusinessLogic/AgentHookSettingsCommand.swift`
6666-- `supacode/Features/Settings/BusinessLogic/ClaudeHookSettings.swift`
6767-- `supacode/Features/Settings/BusinessLogic/CodexHookSettings.swift`
6868-- `supacode/Infrastructure/AgentHookSocketServer.swift`
6969-7070-The installed hook commands send either busy-state updates or raw JSON payloads to a local Unix domain socket under `/tmp/supacode-<uid>/pid-<pid>`.
7171-7272-That cannot work from a remote host:
7373-7474-- the remote process cannot access the local Unix socket
7575-- the remote shell does not automatically inherit the local `SUPACODE_*` environment
7676-- the current hook command shape assumes local socket connectivity
7777-7878-## Existing Signals In The Repo
7979-8080-There are already local helpers for the terminal-notification path:
8181-8282-- `bins/osc9-notify.sh`
8383-- `bins/osc777-notify.sh`
8484-8585-There are already tests that verify command generation, desktop notification delivery, and deduplication behavior:
8686-8787-- `supacodeTests/GhosttySurfaceBridgeTests.swift`
8888-- `supacodeTests/AgentBusyStateTests.swift`
8989-- `supacodeTests/AgentHookCommandTests.swift`
9090-9191-This means the local app side is already prepared to accept terminal-originated notifications and coalesce them against richer hook notifications.
9292-9393-## Recommended Implementation
9494-9595-## Phase 1
9696-9797-Add an SSH-safe remote notification mode that uses terminal escape sequences instead of the local Unix socket.
9898-9999-### Why this is the right first step
100100-101101-- It reuses the existing app-side pipeline.
102102-- It avoids new transport infrastructure.
103103-- It solves the user-visible problem in `SUP-39`.
104104-- It keeps the change narrow and forward-only.
105105-106106-### Notification protocol choice
107107-108108-Use `OSC 777` for agent hooks.
109109-110110-Why:
111111-112112-- `OSC 9` only gives a body in the current Ghostty path.
113113-- `OSC 777` supports both title and body.
114114-- Supacode already receives both title and body from Ghostty for desktop notifications.
115115-116116-`OSC 9` can remain useful for smoke testing and manual scripts, but `OSC 777` is the better hook transport.
117117-118118-### Implementation shape
119119-120120-Add a second hook command builder that emits terminal notifications instead of writing to the local socket.
121121-122122-The current command-generation choke point is `AgentHookSettingsCommand`. `ClaudeHookSettings` and `CodexHookSettings` only embed those generated commands into their respective settings files, so the remote path should be added there rather than duplicated per agent.
123123-124124-The hook command should:
125125-126126-1. Read the raw JSON payload from `stdin`.
127127-2. Extract:
128128- - `title`
129129- - `message`
130130- - `last_assistant_message`
131131- - `hook_event_name`
132132-3. Choose a title/body:
133133- - title = payload title if present, otherwise agent name
134134- - body = `message` or `last_assistant_message`
135135-4. Sanitize terminal control characters and delimiters.
136136-5. Emit `OSC 777`.
137137-138138-The easiest portable implementation is a small helper script that uses `python3` to parse JSON and print the escape sequence.
139139-140140-### Command selection
141141-142142-The hook installer should generate commands using this rule:
143143-144144-- if `SUPACODE_SOCKET_PATH` exists, use the current local socket command
145145-- otherwise, if the process is running in an SSH session, use the remote OSC command
146146-147147-Remote detection should prefer explicit signal over inference:
148148-149149-- first: `SUPACODE_REMOTE_NOTIFICATIONS=1`
150150-- second: `SSH_CONNECTION` or `SSH_TTY`
151151-152152-This avoids changing local behavior and keeps the new path opt-in or clearly scoped to remote sessions.
153153-154154-### Scope
155155-156156-Phase 1 should only cover notifications.
157157-158158-It should not try to preserve:
159159-160160-- local hook busy-state updates
161161-- tab/surface targeting parity beyond the active remote terminal stream
162162-- remote install automation
163163-164164-### Acceptance criteria
165165-166166-- a remote Claude/Codex hook can emit a Supacode notification over `ssh`
167167-- the local app records the notification in the worktree list
168168-- the local app can show a macOS system notification
169169-- the existing hook-vs-OSC deduplication still works
170170-- local non-SSH hook behavior is unchanged
171171-172172-### Tests
173173-174174-Add tests for:
175175-176176-- remote helper payload parsing
177177-- `OSC 777` title/body delivery through the existing bridge callback
178178-- command selection logic between local socket and remote OSC modes
179179-- sanitization of control characters
180180-181181-## Phase 2
182182-183183-Add a real remote relay for structured hook parity.
184184-185185-This is the path if Supacode needs:
186186-187187-- busy-state rings for remote agents
188188-- richer remote targeting semantics
189189-- remote commands that land in the local socket transport instead of going through terminal escape sequences
190190-- remote installation/bootstrap owned by Supacode
191191-192192-### Architecture
193193-194194-The relay shape should be:
195195-196196-1. Supacode starts a local authenticated relay server.
197197-2. Supacode opens an SSH reverse tunnel back to that local relay.
198198-3. Supacode installs remote metadata or helper wrappers on the remote host.
199199-4. Remote hook commands talk to the tunneled relay endpoint.
200200-5. The local relay forwards the request into the existing local Unix socket or directly into app state.
201201-202202-### What this unlocks
203203-204204-- remote busy-state updates with the existing structured model
205205-- remote notifications without shell JSON parsing in the app-specific hook command
206206-- future remote agent commands beyond notifications
207207-208208-### Why this is not phase 1
209209-210210-- it introduces authentication, bootstrap, lifecycle, and reconnect behavior
211211-- it is a larger surface area than the problem requires
212212-- it should be justified by remote busy-state or broader remote orchestration goals
213213-214214-## Risks And Open Questions
215215-216216-### tmux and nested terminal layers
217217-218218-Plain `ssh` PTY delivery should work. Nested layers such as `tmux` may require validation because escape-sequence passthrough behavior depends on terminal configuration.
219219-220220-This should be treated as a compatibility matrix item for phase 1 validation, not as a blocker to the first implementation.
221221-222222-### Remote helper availability
223223-224224-The simplest JSON parser is `python3`. If remote environments without Python need to be supported, Supacode should ship a tiny helper binary or install a standalone script during remote bootstrap.
225225-226226-### Automatic installation on the remote host
227227-228228-Phase 1 does not require Supacode to edit remote config files automatically. If product requirements demand a one-click remote install, that belongs with the relay/bootstrap work in phase 2.
229229-230230-## Proposed Work Split
231231-232232-### Issue 1
233233-234234-Implement SSH-safe remote notifications via `OSC 777`.
235235-236236-### Issue 2
237237-238238-Add a remote relay for structured hook parity and busy-state updates.
239239-240240-## Summary
241241-242242-Supacode can show notifications over `ssh` today if the remote process emits terminal notification escape sequences.
243243-244244-The missing piece is not the app-side notification pipeline. The missing piece is the remote integration path.
245245-246246-The recommended order is:
247247-248248-1. ship SSH-safe hook notifications over `OSC 777`
249249-2. add a real remote relay only if remote structured parity is needed