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