Like many technical folks these days, I use Claude Code constantly. But here’s the thing - I’m usually running multiple Claude sessions in parallel, either working on different features of the same project or juggling entirely different codebases. And when you’re deep in the zone with 5+ Claude sessions spread across tmux windows and panes, you need to know immediately when one of them needs your attention.
I tried the various web UIs and orchestrators out there - vibe-kanban, claudecodeui, Claudia. They’re nice for getting an overview, but they introduce too much friction when you live in the terminal. Context switching to a web UI just to check on Claude? That’s a workflow killer.
So I built my own notification system. Nothing fancy - just a clean way to get native macOS notifications that, when clicked, take me directly to the right tmux pane. No web UI, no extra chrome, just a notification and a single click to get back to work.
The basic flow: Claude finishes or needs input → triggers a notification → click it → boom, you’re in the right tmux pane. The whole thing runs on n8n for orchestration, Gotify for managing notifications across devices, and some simple webhook scripts for the local integration.
graph TB
A[Claude] -->|Hook| B[n8n]
B --> C[Gotify]
B --> D[Webhook]
C --> E[Clients]
D --> F[Notify]
F -->|Click| G[tmux]
style A fill:#fef3c7,stroke:#f59e0b
style G fill:#d1fae5,stroke:#10b981
A Claude Code session in tmux pane (e.g., 1:0.1) completes or needs input
Claude hook triggers and calls the n8n webhook with tmux location data
n8n sends notifications to both Gotify and local webhook server
Gotify notifies all registered clients while webhook triggers terminal-notifier
Clicking the notification calls another webhook that activates the tmux pane
Notification Example
Architecture Decisions
Why not use n8n alone instead of Gotify? While technically possible and probably easier to achieve notifications on specific applications such as Telegram, I wanted to decouple the orchestrator from the notification mechanism. Gotify is my messaging center for many different applications and it is actually hosted on a server that is not the machine where claude-code is started. Gotify also has the concepts of users, applications and clients which offer more flexibility than n8n to manage notifications properly.
Why not use n8n to execute scripts on the host? This would work if n8n was started on the same machine as where claude-code runs which is not my case. My n8n instance is hosted, as Gotify, on a different server and it used for different machines. Separating the orchestrator from the executor is still a good idea regardless and it offers the possibility to extend our notification system to receive notifications from different machines (e.g. a remote server) and execute actions there.
Why build around Claude Code? I want to build around Claude-code and not with Claude-code. As a result, I expect to be able to have Claude-code using the notification system that we will develop here. This is probably the most exciting use-case because it then becomes possible to have several claude-instance communicating with each other (spoiler alert, this is not going to end well!).
Warning
To make it easy for people to try it, I will consider that all components are started on the same machine and I will provide the instructions for macOS. In real life scenarios, you would typically separate n8n and Gotify on dedicated servers.
First, let’s prepare the services that will form the backbone of our notification system. Each service plays a specific role in the notification pipeline, and we’ll set them up in the order of their dependencies.
Gotify serves as our centralized notification server. It provides a clean API for sending notifications and supports multiple clients (browser, mobile apps, etc.).
Create Docker volume:
1
docker volume create gotify_data
Start Gotify container:
1
docker run -p 8456:80 --name=gotify -v gotify_data:/app/data gotify/server
The webhook server runs locally and executes scripts when triggered. This is what allows us to show system notifications and activate tmux panes when you click on them.
Install webhook:
1
brew install webhook
Create LaunchAgent configuration:
Create ~/Library/LaunchAgents/com.github.adnanh.webhook.plist (adjust paths to match your username):
1 2 3 4 5 6 7 8 91011121314151617181920212223
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plistversion="1.0"><dict><key>Label</key><string>com.github.adnanh.webhook</string><key>ProgramArguments</key><array><string>/opt/homebrew/bin/webhook</string><string>-hooks</string><string>/Users/aquemy/.hooks.json</string><string>-port</string><string>9000</string><string>-hotreload</string><string>-logfile</string><string>/Users/aquemy/.webhook_log.txt</string></array><key>RunAtLoad</key><true/><key>KeepAlive</key><true/><key>StandardOutPath</key><string>/opt/homebrew/var/log/webhook.log</string><key>StandardErrorPath</key><string>/opt/homebrew/var/log/webhook.log</string></dict></plist>
[webhook] 2025/08/04 15:15:07 version 2.8.2 starting
[webhook] 2025/08/04 15:15:07 setting up os signal watcher
[webhook] 2025/08/04 15:15:07 attempting to load hooks from /Users/aquemy/.hooks.json
[webhook] 2025/08/04 15:15:07 os signal watcher ready
Troubleshooting▶
If webhook fails to start, check if port 9000 is in use: lsof -i :9000
To restart webhook: launchctl kickstart -k gui/$(id -u)/com.github.adnanh.webhook
View logs: tail -f ~/.webhook_log.txt
Linux Alternative
On Linux, use systemd instead of launchctl. Create /etc/systemd/system/webhook.service
For the notification system to work, Claude Code needs to know which tmux pane it’s running in. We’ll set up environment variables that capture this information automatically when a new pane is created.
Add environment variables to .zshrc:
123456
# Automatically set these variables for new processes in tmuxif[ -n "$TMUX"]&&[ -z "$WS_TMUX_LOCATION"];thenexportWS_TMUX_LOCATION=$(tmux display-message -p '#{session_name}:#{window_index}.#{pane_index}')exportWS_TMUX_SESSION_NAME=$(tmux display-message -p '#{session_name}')exportWS_TMUX_WINDOW_NAME=$(tmux display-message -p '#{window_name}')fi
Now we need to create an application in Gotify that will handle our Claude Code notifications. This application will have its own token that we’ll use in n8n.
Create Gotify application:
1234
curl -u admin:admin \
-F "name=Claude Code"\
-F "description=Notifications from Claude Code"\
http://localhost:8456/application
Save the application token:
The response includes a token (e.g., "token":"Abis_ERmy-hxAx5"):
1
{"id":2,"token":"Abis_ERmy-hxAx5","name":"Claude Code","description":"description=Notifications from Claude Code","internal":false,"image":"static/defaultapp.png","defaultPriority":0,"lastUsed":null}
We need two scripts to handle the notification flow:
ghostty-notify-wrapper.sh - Generates the macOS notification using terminal-notifier with contextual information from Claude Code
go-tmux.sh - Handles the notification click event and switches to the correct tmux pane
Both scripts will be registered as webhooks. The notification generated by the first script will include a click action that triggers the second script. Since webhook was started with the --hotreload flag, changes to these scripts take effect immediately without restarting the service.
The architecture isolates terminal-specific components (ghostty, terminal-notifier) in these scripts, making it easy to adapt for other terminals or notification systems.
Note
Important: These scripts must use absolute paths for all executables (e.g., /opt/homebrew/bin/terminal-notifier instead of just terminal-notifier) because they run in a limited environment context.
The first script generates the macOS notification. It takes all the contextual information from Claude (tmux location, session name, project, etc.) and creates a clickable notification. The magic happens in the -execute parameter - when you click the notification, it triggers a webhook that calls our second script.
The second script handles the actual navigation. When you click a notification, this script parses the tmux location, switches to the right window and pane, then brings Ghostty to the foreground. The logging is verbose on purpose - when something goes wrong with tmux navigation, you want to know exactly what happened.
Now we need to configure webhook to know about these scripts. The .hooks.json file defines two webhook endpoints - one for each script. The key thing to understand is how webhook passes data: { "source": "payload", "name": "tmux_location" } means “take the tmux_location field from the POST request body and pass it as a command-line argument to the script”. For the first webhook, { "source": "url", "name": "tmux_location" } means “extract tmux_location from the URL query parameters” - this is how the notification click passes data back.
Claude hooks allow us to trigger actions when specific events occur in Claude Code. For our notification system, we’re interested in two events:
Stop: When Claude Code finishes executing and is waiting for the next instruction
Notification: When Claude Code needs user attention (e.g., asking for clarification)
Claude hooks provide a rich payload containing session information, working directory, and event type. We’ll enhance this payload with our tmux location data before forwarding it to n8n.
When a hook triggers, Claude Code provides a JSON payload with fields like:
- session_id: Unique identifier for the Claude session
- cwd: Current working directory
- hook_event_name: The event that triggered the hook (Stop, Notification, etc.)
- transcript_path: Path to the session transcript
We’ll add our tmux environment variables to this payload using jq:
Create a new workflow and add a Webhook node. This will be the entry point for all Claude Code notifications.
Webhook Trigger Configuration
Important settings:
- HTTP Method: POST
- Path: claude-code-notification (must match the path in your Claude hooks)
- Response Mode: “When last node finishes” (ensures proper acknowledgment)
The Edit Fields node allows us to transform the incoming data. While optional, it’s useful for extracting meaningful information. For example, we extract the project name from the working directory path:
Edit Fields Configuration
Expression to extract project name:
1
{{$json.body.cwd.split('/').last()}}
This takes the working directory path (e.g., /Users/aquemy/projects/my-app) and extracts just the project name (my-app).
That’s it - a simple notification system that keeps me in the flow while juggling multiple Claude sessions. No more wondering which session needs attention or losing track of where I was working. Just a clean notification and a single click to get back to the right tmux pane.
The best part? Each component does one thing well. n8n handles the orchestration, Gotify manages notifications across devices, and simple bash scripts handle the local integration. When something breaks (and it will), it’s easy to debug because each piece is isolated.
I’ve been thinking about a few extensions that might be interesting:
Web UI with tmux controls - Show the full tmux layout with live status of each Claude session. Maybe add touch screen support or macro pad shortcuts for quick navigation.
Claude-to-Claude communication - Since Claude can now trigger notifications, why not let different Claude instances communicate? I tried this already - spoiler: it gets weird fast when they start coordinating.
Home Assistant integration - Visual feedback through smart lights when Claude needs attention. Because why not?
Event database - Track notification patterns to understand when Claude gets stuck most often. Could help optimize prompts.
Beyond tmux - Support for screen, Zellij, or even Windows Terminal.
The notification system is intentionally built to be extended. Want to add Discord notifications? Just add another node in n8n. Prefer Linux? Swap terminal-notifier for notify-send and osascript for xdotool. The architecture stays the same.
If you build something cool with this, let me know. I’m always curious to see how others adapt these tools to their workflow.