世界上只有一种真正的英雄主义,就是认清了生活的真相后还依然热爱它
27 March 2026
If you run Claude Code inside tmux on a remote server, you might encounter a problem: the task finishes, but you don’t know about it. Claude Code’s notification mechanism only works locally by default, so in remote environments you need to find another way.
After some searching, I found a few articles discussing this issue:
The approach in these solutions is: Claude Code sends HTTP request → n8n workflow processes it → Gotify pushes notification → local webhook receives it → system notification.
Sounds complete, but the problem is: it’s too heavy! You need:
The whole setup takes at least 30 minutes, plus ongoing maintenance of these services.
Actually, terminals themselves support notification mechanisms. OSC (Operating System Command) is a type of terminal escape sequence, and OSC 777 is specifically designed for notifications.
The problem is that tmux intercepts these escape sequences. The solution is to use tmux’s passthrough feature to wrap the OSC sequences:
ESC Ptmux ; ESC <OSC sequence> ESC \
ESC ] 777 ; notify ; <title> ; <body> BEL
printf '\033Ptmux;\033\033]777;notify;Title;Body\007\033\\'
One line, zero dependencies.
First, ensure tmux allows passthrough:
# ~/.tmux.conf
set -g allow-passthrough on
set -ga terminal-overrides ',*:allow-passthrough=on'
Key insight: Hook scripts run in the background, so direct
printfoutput won’t appear in the foreground pane. The solution is to create a temporary pane to send the notification, which then auto-closes.
# ~/.claude/hooks/cmux-remote-notify.sh
#!/bin/bash
# Only run in tmux
[ -n "$TMUX" ] || exit 0
# Get context info
LOCATION=$(tmux display-message -t "$TMUX_PANE" -p '#{session_name}:#{window_index}')
SHORT_PATH=$(tmux display-message -t "$TMUX_PANE" -p '#{pane_current_path}' | sed 's/.*\/\(.*\/.*\)/\1/')
osc_notify() {
local body="${1:-}"
body="${body:0:100}"
# Create temp pane to send notification, auto-closes when done
tmux split-window -v -l 1 "printf '\033Ptmux;\033\033]777;notify;Claude @ tmux:$LOCATION;$body\007\033\\\\'" 2>/dev/null
}
# jq fallback
json_extract() {
local json="$1" key="$2"
echo "$json" | grep -o "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \
| sed 's/.*: *"\([^"]*\)".*/\1/' | head -1
}
INPUT=$(cat)
EVENT="$1"
case "$EVENT" in
stop|idle)
osc_notify "$SHORT_PATH ✓"
;;
notification|notify)
if command -v jq &>/dev/null; then
BODY=$(echo "$INPUT" | jq -r '.body // "Needs input"' 2>/dev/null | head -c 100)
else
BODY=$(json_extract "$INPUT" "body")
[ -z "$BODY" ] && BODY="Needs input"
fi
osc_notify "$SHORT_PATH: $BODY"
;;
esac
Notification format: Claude @ tmux:3:1 → Projects/ruodojo ✓ (title shows location, body shows directory)
// ~/.claude/settings.json
{
"hooks": {
"Stop": [{
"matcher": "",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/cmux-remote-notify.sh stop" }]
}],
"Notification": [{
"matcher": "",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/cmux-remote-notify.sh notification" }]
}]
}
}
chmod +x ~/.claude/hooks/cmux-remote-notify.sh
Notifications are passed back to your local terminal through the SSH connection, then triggered as system notifications by the terminal (e.g., iTerm2, Ghostty, cmux, etc.).
This solution depends on:
If your terminal doesn’t support OSC 777, consider using OSC 9 (Windows Terminal) or OSC 99 (some terminal emulators).
| Feature | OSC Passthrough | n8n + Gotify |
|---|---|---|
| Dependencies | None | n8n, Gotify, Docker, webhook |
| Setup time | 2 minutes | 30+ minutes |
| Maintenance | None | Need to maintain multiple services |
| Click to jump | ❌ | ✅ |
| Multi-device | ❌ | ✅ |
| Complexity | Low | High |
If you just want to receive notifications when remote Claude Code finishes tasks, you don’t need to build a complex service stack. tmux passthrough + OSC 777 is enough, done in two minutes.
Of course, if you need to click notifications to jump to specific locations, or push to multiple devices, the n8n solution might be more suitable. But for most scenarios, simple is beautiful.