Forum

AbsoluteTelnet + Cl...
 
Notifications
Clear all

AbsoluteTelnet + Claude Code: a notification sound unique to Claude (32-bit AT only — it relies on VB scripting)

1 Posts
1 Users
0 Reactions
7 Views
(@taa1)
Estimable Member
Joined: 12 years ago
Posts: 71
Topic starter   [#9117]

Context / goal

I run Claude Code (Anthropic's terminal coding agent) on a remote host over SSH, with AbsoluteTelnet as my local terminal on Windows. During a long task, Claude Code periodically pauses to ask me for input or permission. When that happens I'm usually looking at another window, so I want the terminal to actively get my attention — audibly — even when it isn't the focused window.

The goal is more specific than "make a sound", though: the alert must be unique to "Claude needs me". My desktop beeps and dings all day — mail, chat, system dialogs, other terminal sessions. What I need is a sound that, the instant I hear it, can only mean one thing: Claude Code is waiting on me. That uniqueness requirement is what drives everything below.

One more interest of mine, up front: finding a solution that requires no user-facing AbsoluteTelnet GUI changes — because I know GUI settings work isn't free. My initial idea was going to be a GUI addition: a bell-sound picker that overrides the Windows default (C:\Windows\Media\Windows Exclamation.wav) on a per-connection-file (.tnt) basis; a user would launch that .tnt whenever an SSH session was going to be mostly or entirely Claude interactions. But that seemed like it would be more work for you, so I started thinking about ways that wouldn't require a GUI change at all. The solution below is where that led: it rides entirely on the existing script engine, and any future improvements could stay under the hood as new scripting-API functions rather than new GUI surface.

Why the ordinary terminal bell can't be unique

The classic signaling channel is the ASCII BEL (0x07, Ctrl-G). Claude Code can emit one when it needs attention (its preferredNotifChannel: terminal_bell setting), and any CLI can do the same with printf '\a'.

But a BEL is just one byte. It carries no identity — nothing says who rang it. The receiving terminal's bell configuration decides what sound is made, and it makes that same sound for every BEL from any remote program running over any connection (anything on the host that writes a BEL into the session — vi's error beep, less bumping into end-of-file, a failed bash tab-completion, a stray ^G scrolling past in a log).

AbsoluteTelnet's bell plays the Windows "Exclamation" program-event sound — typically Windows Exclamation.wav. AT itself offers no way to change it, though it can be changed Windows-wide — thanks for the annotated Control Panel walk-through you emailed (the path is in the appendix below). The catch: a Windows-wide change affects every application that plays the Exclamation event, so it still can't serve as a Claude signature — the Exclamation sound is the stock Windows alert used by many Windows applications (system dialogs, error popups, all sorts of apps), and hearing it tells you nothing about who wants you. And even if you could point AT's bell at a custom WAV using the AT bell-sound picker I mention above, it would still ring for every BEL from every remote program — not just Claude. The bell channel simply cannot carry the "this is Claude" indicator.

So a unique sound needs a signal richer than BEL — something in the data stream that only Claude emits, that the terminal can recognize.

The solution (proven end-to-end): Claude's notification channel + a tiny global script

Claude Code has a setting, preferredNotifChannel, that controls how it signals "I need you." Setting it to ghostty makes Claude emit an OSC 777 notification escape sequence into its own terminal output at exactly the right moments:

ESC ] 777 ; notify ; Claude Code ; <message> BEL

— where <message> is e.g. Claude needs your permission (a permission prompt) or Claude is waiting for your input (Claude finished its turn and is waiting). In my testing, AbsoluteTelnet swallows OSC sequences it doesn't understand, so nothing displays and no bell rings (verified in 13.16 and 13.17 RC9) — but the payload still passes through the raw stream, where the script engine's WaitFor can see it. That yields a complete solution in two small steps — one paste, one setting — with nothing else installed anywhere:

Step 1 — the AbsoluteTelnet global script

Paste via Options → Properties → Global → "Edit global connection script", save, then close and reopen the connection — verified: the global script is re-read at each connection start, so no AT restart is needed (already-connected sessions keep the old script until reconnected):

Sub Main
    Do While WaitFor("notify;" & "Claude Code")
        Set sh  = CreateObject("WScript.Shell")
        Set fso = CreateObject("Scripting.FileSystemObject")
        wav = sh.ExpandEnvironmentStrings("%APPDATA%") & "\AbsoluteTelnet\claude-notify.wav"
        If Not fso.FileExists(wav) Then wav = sh.ExpandEnvironmentStrings("%WINDIR%") & "\Media\tada.wav"
        sh.Run "powershell -NoProfile -WindowStyle Hidden -c " & _
               "(New-Object Media.SoundPlayer '" & wav & "').PlaySync()", 0, False
    Loop
End Sub
  • WAV lookup: %APPDATA%\AbsoluteTelnet\claude-notify.wav (your chosen unique sound); if absent, %WINDIR%\Media\tada.wav so it works out of the box. To change the sound, replace the file — no script edit needed. (The claude-notify.wav I use — a marimba — is attached to this post if you'd like to hear it.)
  • The WaitFor string is split ("notify;" & "Claude Code") so a display of the script's own text never contains the contiguous match and can't ring the VB script's WaitFor listener.

Step 2 — the Claude-side setting

In a Claude Code session, run /config and set the notification channel to ghostty. The setting is stored per user per host (in ~/.claude.json), applies to every project and session on that host, and persists until changed.

That's it. Proven end-to-end 2026-07-22 (AbsoluteTelnet 13.16, Claude Code v2.1.217): both a live permission prompt and an idle wait rang the global connection script's WAV — zero host-side scripting.

Notes and limitations found

  • 32-bit AbsoluteTelnet only: scripting is disabled in the combined 32/64-bit build.
  • Timing: Claude suppresses notifications while you are actively typing (a user-is-present guard), so the sound fires once you're hands-off — not the instant a prompt appears. (Claude's messageIdleNotifThresholdMs setting — see the appendix — tunes how long a wait must last before the idle notification fires.)
  • The message text itself is never visible — it lives inside the swallowed OSC. The sound is its only manifestation today; see the wishlist for the toast idea that would fix that.
  • Versions tested: proven on AT 13.16 (2026-07-22); the swallow/no-bell/listener results re-verified on 13.17 RC9 (2026-07-23). Worth spot-checking any future release that changes stream handling.
  • tmux / screen: a multiplexer may filter escape sequences (newer tmux versions may need tmux's own allow-passthrough option enabled — that's a tmux setting, not a Claude one).
  • The WaitFor match string rides on Claude's own payload wording — written split here, as in the script, so that displaying this note in a live terminal can't ring the listener: "notify;" & "Claude Code". A future Claude release could change the wording (the wishlist's proposed WaitForRegex(pattern) item would harden this).

Wishlist: what native AbsoluteTelnet support could add

The script route works today, but native support would make it better — for every CLI user, not just Claude users:

1. Native OSC 777 / OSC 9 desktop notifications — the headline ask. AT already swallows these sequences; rendering them as a Windows toast would show the message text — "Claude needs your permission" instead of just a sound — right in the notification center. Long builds, CI, backup jobs and chat CLIs already emit these sequences.

2. Focus-aware BEL handling: taskbar flash (FlashWindowEx) and/or toast when a BEL arrives while the window is unfocused; per-profile sound selection; bell de-bounce for bursts.

3. Scripting-API additions. (Terminology note to me (Todd): Sub / Do While are VB keywords — the language; WaitFor is part of AbsoluteTelnet's scripting API — the functions the app exposes to scripts.) These are suggestions for new API functions/events.

  • WaitForRegex(pattern) plus a way to read what matched (e.g. LastMatch() / capture groups) — robust against wording changes, and enables payload-carrying alerts (which host, what's being asked — where the host name is taken from the connection's own Options → Connection Properties → Connection → Hostname, NOT something the remote host would have to emit).
  • Event handlers instead of a polling loop: OnOsc(number, text) and OnBell().
  • PlaySound(path) — native WAV playback, no PowerShell shell-out.
  • Notify(title, body) — raise a real Windows toast from script.
  • FlashTaskbar() / SetTabAlert() — visual attention.
  • IsFocused() — stay quiet when the user is already looking.
  • SessionName() / SessionInfo() — which connection/tab the match arrived on. I really like this idea but I don't know how it would surface: the tab bar labels tabs only by connection filename (duplicates are common when several tabs open the same .tnt, and with many tabs open you scroll to find them), so I'm not sure what a unique identifier would look like — your call whether it's practical. The existing behavior where an unseen/unheard BEL flashes the yellow "!" triangle on the tab may have to be good enough; advice welcome.
  • Timer / de-bounce helpers — coalesce a burst of matches into one alert.

4. Script-editor usability: a larger, monospaced (or configurable) font. The global-script (and the per-connection login script) edit box's small proportional font made me introduce two hard-to-spot one-character paste bugs in a single day — a stray ) and a t that became a space — each of which silently kills the whole script. A monospaced font would help as much as a larger one; even just reusing the session's terminal font/size (the Appearance tab's Text Font + Screen Font Size) would be a win. (I realize this is the one item here that is a user-facing GUI change — the only one I'm asking for, having tried to avoid them everywhere else.)

Also tried (background — so you know what's behind the recipe)

  • A private-sentinel hook (worked; the previous primary approach): a Claude Notification hook (see the appendix) runs a small script on the SSH host that writes a private OSC 777 sentinel to the session's tty — found by walking up the process tree, because hook processes have no controlling terminal (/dev/tty fails) — and the global script matched that sentinel. Fully proven too, and by construction unique to "Claude needs me". But it needs a hook plus an emitter script installed on every remote host, so the two-step ghostty route replaced it as the recommendation. I would consider reinstating it as a fallback if Claude's message wording ever changes.
  • Matching the BEL itself (WaitFor(Chr(7))): rejected — any BEL from any program would ring the "Claude" sound; the uniqueness that motivated all this is lost.
  • Scraping the TUI's prompt text: rejected — breaks when Claude's UI wording changes, false-fires when the phrase merely scrolls past (e.g. reading a log), and the TUI may interleave color/positioning escape codes inside the phrase, defeating a literal match.

TL;DR

Set Claude Code's notification channel to ghostty (one setting — see the appendix) and paste a small WaitFor loop into AbsoluteTelnet's global connection script: Claude then announces "I need you" with an invisible OSC 777 message that rings a WAV no other application uses. Proven end-to-end in AT 13.16 (re-verified on 13.17 RC9) with zero host-side scripting. The big native-support ask from my wishlist: render OSC 777 / OSC 9 as Windows toasts, so the alert can show what Claude is asking — not just that it asks.

Appendix: where the settings I mention get changed

Claude Code (in-session slash commands):

  • preferredNotifChannel (terminal_bell, ghostty, …): /config command → notification channel (stored per user per host in ~/.claude.json)
  • messageIdleNotifThresholdMs (idle-notification threshold): /config command
  • Notification hooks (the private-sentinel approach under "Also tried"): /hooks command
  • Claude Code version (the vX.Y.Z figures cited above) — checkable from inside a chat session: /status command (it also shows the model and account)

Windows (the default alert sound AT's bell plays — your emailed walk-through):

  • Settings → System → Sound → "More sound settings" → Sounds tab → Program Events → Exclamation → pick a .wav (affects every application that plays the Exclamation event, which is why it can't be a Claude-only sound)

 

claude-notify.wav:



   
Quote
Share: