A modern tab manager for tmux with grouping, a clickable vertical sidebar, and deep linking for notifications.
- About This Project
- Key Features
- All Features
- Installation
- Usage
- Configuration
- Tab Grouping
- Widgets
- Command-Line Reference
- Development
- macOS Notifications with Deep Links
- Session Persistence (tmux-resurrect)
- Troubleshooting
- Contributing
- License
Tabby started as an opinionated solution to a personal problem: managing dozens of tmux windows across multiple projects without losing context. It grew into something others might find useful.
Design Philosophy:
- Customizable - support for Nerd Fonts, emoji, ASCII, and various terminal features
- Modular - enable only the features you need (sidebar, pane headers, widgets, etc.)
- Extensible - widget system for adding custom sidebar content (clock, git status, pet, stats, Claude usage)
- Terminal-agnostic - works with most modern terminals (Ghostty, iTerm, Kitty, Alacritty, etc.)
Contributing: PRs are welcome. This is actively developed but cannot promise support for all terminal emulators or use cases. If you find Tabby useful or have ideas, contributions are appreciated.
A persistent, clickable sidebar that works across all your windows. Left-click to switch, right-click for context menus, middle-click to close. Full mouse support that tmux's native status bar can't provide.
Organize windows by project with color-coded groups. Windows are automatically grouped by pattern matching or manually assigned via right-click menu. Each group has its own theme colors and optional icon.
Click a notification to jump directly to the right tmux session, window, and pane. Perfect for long-running tasks - get notified when done and click to return instantly.
# Example: notification that deep-links back to tmux
terminal-notifier -title "Build Done" -message "Click to return" \
-execute "~/.tmux/plugins/tabby/bin/tabby-hook focus-pane main:2.1"- Vertical sidebar — clickable, persistent across windows; collapse via hamburger, keyboard, or CLI
- Window grouping — color-coded project organization with per-group working directories
- Deep link navigation — click notifications to jump to exact pane
- Automatic window naming — shows running command, locks on manual rename
- Activity indicators — bell, activity, silence, busy, input, and SSH CC hook indicators forwarded via OSC 7700
- Mouse support — click, right-click menus, middle-click close, mouse-drag OSC 52 clipboard copy
- Custom tab colors — per-window color overrides, including transparent mode
- Pane headers — per-pane titles, inactive-pane dimming, border styling
- Pane management — rename with title locking, swap/cycle, smart kill-pane that preserves ratios
- Group management — create, rename, color, collapse, set working directories
- Responsive mobile/tablet/desktop profiles — sidebar adapts per-client width, phone gets a window-header with nav buttons
- Cross-window sidebar width sync — drag in one window, all windows in the same profile follow
- Auto theme + CLIENT_DARK_MODE forwarding — follow system light/dark, even on remote hosts over SSH
- Widgets — pinnable clock, pet, stats, git, session, and Claude cost widgets
- tmux-resurrect integration — clean save/restore of sidebar state
- SSH bell notifications — auto-enable bells on remote command completion
- Keyboard navigation — intuitive shortcuts for everything
Add to your ~/.tmux.conf:
set -g @plugin 'brendandebeasi/tabby'Then reload tmux and install:
tmux source ~/.tmux.conf
# Press prefix + I to install pluginsgit clone https://github.com/brendandebeasi/tabby ~/.tmux/plugins/tabby
cd ~/.tmux/plugins/tabby
./scripts/install.shAdd to your ~/.tmux.conf:
run-shell ~/.tmux/plugins/tabby/tabby.tmuxTabby follows standard tmux keybindings. All standard tmux shortcuts work as expected.
| Key | Action |
|---|---|
prefix + c |
Create new window |
prefix + n |
Next window |
prefix + p |
Previous window |
prefix + x |
Kill current pane |
prefix + q |
Display pane numbers |
prefix + w |
Window list |
prefix + , |
Rename window |
prefix + " |
Split horizontal |
prefix + % |
Split vertical |
prefix + d |
Detach from session |
prefix + 1-9,0 |
Switch to window by number |
| Key | Action |
|---|---|
prefix + Tab |
Toggle vertical sidebar (enable/disable daemon) |
prefix + G |
Create new group |
Cmd + Shift + \ |
Hide/show sidebar (stash/restore — requires terminal config) |
Alt + a |
Toggle all-windows overview mode |
When the sidebar is focused, press m to open the marker picker for the active window.
- Left click: Switch to window/pane
- Click right edge: Click the divider to collapse sidebar
- Middle click: Close window (with confirmation)
- Right click on window: Context menu with options:
- Rename (with title locking)
- Unlock Name (restore automatic naming)
- Collapse/Expand Panes
- Move to Group
- Set Marker (searchable emoji picker)
- Set Tab Color (including transparent)
- Split Horizontal/Vertical
- Open in Finder
- Kill window
- Right click on pane: Pane-specific options:
- Rename pane (with title locking)
- Unlock pane name
- Split pane
- Focus pane
- Break to new window
- Close pane
- Right click on group: Group management:
- New window in group
- Collapse/Expand group
- Rename group
- Change group color
- Set Marker (searchable emoji picker)
- Set working directory
- Delete group
- Close all windows in group
The sidebar can be placed on either side of the window and can span the full height or attach to a single pane.
Set via tmux options:
# Position: left (default) or right
tmux set-option -g @tabby_sidebar_position right
# Mode: full (default, spans full window) or partial (attaches to one pane)
tmux set-option -g @tabby_sidebar_mode partialToggle the sidebar off and on (prefix + Tab twice) after changing these options.
Hide the sidebar temporarily to reclaim screen space. Collapse is a stash/restore operation (tmux break-pane / join-pane under the hood), not a shrink — the sidebar-renderer process keeps running in a hidden holding window, so bringing it back is instant.
How to trigger:
- Mobile: tap the
≡(hamburger) button on the window header - Keyboard:
Cmd + Shift + \(requires terminal config) - Shell:
tabby hook toggle-collapse-sidebar— fires the same action the hamburger does
prefix + Tab is different — it fully disables tabby for the session (stops the daemon, removes hooks). Use the collapse path when you just want to reclaim space temporarily.
Cmd + Shift + \ is bound in tmux to tabby hook toggle-collapse-sidebar, reached via CSI u encoding. tabby.tmux registers the binding automatically; to rebind manually:
bind-key -n 'C-S-\' run-shell -b '/path/to/tabby/bin/tabby hook toggle-collapse-sidebar'Requires extended-keys in your tmux.conf (tmux 3.2+):
set -g extended-keys on
set -sa terminal-features 'xterm*:extkeys'To use Cmd+Shift+\ as the trigger, your terminal must send the CSI u sequence \x1b[92;6u (which tmux decodes as Ctrl+Shift+\). Configuration varies by terminal:
Blink Shell — the recommended mobile terminal for Tabby on iPad/iPhone. See configuration below.
Ghostty — add to ~/.config/ghostty/config:
keybind = super+shift+backslash=text:\x1b[92;6u
keybind = cmd+left_bracket=text:\x1b{
keybind = cmd+right_bracket=text:\x1b}
keybind = cmd+grave_accent=text:\x1b`
The cmd+[ / cmd+] bindings enable window navigation (prev_window / next_window). cmd+shift+[/] is left free for Ghostty's native tab switching. The cmd+`` `` binding enables pane cycling (swap_pane); it overrides macOS's "cycle same-app windows" shortcut within Ghostty.
On macOS, Cmd+Shift+\ is bound to "Show All Tabs" by default. Disable it:
defaults write com.mitchellh.ghostty NSUserKeyEquivalents -dict-add "Show All Tabs" '\0'
# Restart Ghostty after running thisBlink Shell (iPad) — https://blink.sh/ — add to your keyboard configuration (kb.json):
{
"keys": "cmd+shift+\\",
"action": "hex",
"value": "1b5b39323b3675"
}The hex value 1b5b39323b3675 is the byte representation of \x1b[92;6u.
iTerm2 — add a key mapping in Preferences → Profiles → Keys:
- Shortcut:
⌘⇧\ - Action: Send Escape Sequence
- Value:
[92;6u
Kitty — add to ~/.config/kitty/kitty.conf:
map cmd+shift+backslash send_text all \x1b[92;6u
Other terminals — any terminal that can send arbitrary escape sequences will work. Map Cmd+Shift+\ (or your preferred shortcut) to send the bytes \x1b[92;6u (ESC [ 9 2 ; 6 u).
Tabby adapts its chrome to the attached client's dimensions. The same tmux session works on a desktop monitor and a phone attached to the same server without manual reconfig.
Three profiles, selected by window width:
| Profile | Window width | Default sidebar width |
|---|---|---|
| Mobile | ≤ 110 cols | 15 |
| Tablet | ≤ 170 cols | 20 |
| Desktop | > 170 cols | 25 |
Override the default widths via tmux options:
tmux set-option -g @tabby_sidebar_width_mobile 15
tmux set-option -g @tabby_sidebar_width_tablet 20
tmux set-option -g @tabby_sidebar_width_desktop 25
# Breakpoints
tmux set-option -g @tabby_sidebar_mobile_max_window_cols 110
tmux set-option -g @tabby_sidebar_tablet_max_window_cols 170Window header (phone only): narrow clients get a one-row window-header pane above each window with buttons: ◀ previous, ≡ hamburger (hide/show sidebar), + new, × close, ▶ next.
Sidebar width sync: drag the sidebar border in any window and all other windows in the same profile follow on the next signal tick. The new width is also persisted to the active window's @tabby_sidebar_width_<profile> so it survives daemon restarts.
Keyboard clamp: when a client's height drops below the keyboard threshold (default 38 rows, set via @tabby_sidebar_mobile_keyboard_rows), the sidebar clamps to @tabby_sidebar_width_mobile_keyboard for ~4 s so the on-screen keyboard doesn't crush the content pane.
Automatically receive bell notifications when commands complete in SSH sessions.
Add to your ~/.ssh/config:
Host *
RemoteCommand bash -c 'PROMPT_COMMAND="printf \"\a\""; exec bash -l'
RequestTTY force
This works by injecting a bell into the remote shell's prompt, so you get a notification after every command.
Note: This uses RemoteCommand which may interfere with tools like scp, rsync, and git over SSH. If you encounter issues, override for specific hosts:
Host github.com gitlab.com bitbucket.org
RemoteCommand none
RequestTTY auto
If you control the remote servers, add this to ~/.bashrc on each server:
export PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND; }printf '\a'"This approach doesn't require SSH config changes and won't interfere with other tools.
Tabby can auto-swap between a light and a dark theme based on the operating system appearance. Useful in two scenarios:
- Local tabby (macOS dark-mode toggle) — tabby runs on the same Mac that toggles.
- Remote tabby (laptop → ssh → bastion with tmux+tabby) — the host running tabby has no desktop, so tabby can't read OS appearance locally. A
CLIENT_DARK_MODEenv var forwarded from the laptop tells the remote daemon which theme to use, and tabby re-reads it on every theme-decision tick so live flips propagate.
Add to ~/.config/tabby/config.yaml:
auto_theme:
enabled: true
mode: system # "system" (macOS/GNOME/KDE) or "time" (HH:MM-based)
light: rose-pine-dawn
dark: rose-pineisSystemDarkMode() consults these in order — first decisive answer wins:
- tmux global env
CLIENT_DARK_MODE— re-read every tick (see push setup below). Lets external agents override with zero daemon restarts. - Process env
CLIENT_DARK_MODE— snapshot at daemon spawn (via SSH SendEnv + tmuxupdate-environmentimport). - macOS
defaults read -g AppleInterfaceStyle. - GNOME
gsettings get org.gnome.desktop.interface color-scheme. - KDE
kreadconfig5 --group General --key ColorScheme.
Accepted CLIENT_DARK_MODE values: 1/dark/true/yes → dark; 0/light/false/no → light; anything else falls through.
Three plumbing pieces on top of tabby itself:
1. Laptop shell — export CLIENT_DARK_MODE from macOS appearance, refreshed on every prompt so ssh always sends the current value:
# ~/.zshrc (local macOS only, not inside SSH'd-in shells)
if [[ "$OSTYPE" == darwin* && -z "$SSH_CONNECTION" ]]; then
_update_client_dark_mode() {
if defaults read -g AppleInterfaceStyle 2>/dev/null | grep -q Dark; then
export CLIENT_DARK_MODE=1
else
export CLIENT_DARK_MODE=0
fi
}
_update_client_dark_mode
autoload -Uz add-zsh-hook && add-zsh-hook preexec _update_client_dark_mode
fi2. SSH — forward the var between hosts:
# ~/.ssh/config (client side)
Host *
SendEnv COLORTERM TERM_PROGRAM TERM_PROGRAM_VERSION CLIENT_DARK_MODE
# /etc/ssh/sshd_config (server side, each hop)
AcceptEnv LANG LC_* COLORTERM TERM_PROGRAM TERM_PROGRAM_VERSION CLIENT_DARK_MODE
On jump hosts that ssh onward, also drop an ssh_config.d client file so the var keeps going:
# /etc/ssh/ssh_config.d/10-env-forward.conf
Host *
SendEnv COLORTERM TERM_PROGRAM TERM_PROGRAM_VERSION CLIENT_DARK_MODE
Reload the server-side daemon after editing sshd_config:
sudo systemctl reload ssh # Linux
sudo launchctl kickstart -k system/com.openssh.sshd # macOS3. tmux update-environment — import the var from the attaching client into the session env, so new panes and inner ssh see it:
set-option -ga update-environment " COLORTERM TERM_PROGRAM TERM_PROGRAM_VERSION CLIENT_DARK_MODE"Add to /etc/tmux.conf or ~/.tmux.conf. Existing tmux servers need tmux source-file ~/.tmux.conf to pick it up.
With just the above, flipping OS appearance propagates on the next SSH connect + tmux reattach. To make it automatic within a few seconds, run a small agent on the laptop that watches macOS Appearance and pushes changes into each remote's tmux global env:
#!/usr/bin/env bash
# ~/bin/sync-color-theme.sh
HOSTS=(bdm1 shared-bastion) # tabby-running hosts
POLL_SEC=5
prev=""
while true; do
cur=$(defaults read -g AppleInterfaceStyle 2>/dev/null | grep -q Dark && echo 1 || echo 0)
if [[ "$cur" != "$prev" ]]; then
for h in "${HOSTS[@]}"; do
ssh -o BatchMode=yes -o ConnectTimeout=5 "$h" \
"tmux set-environment -g CLIENT_DARK_MODE $cur 2>/dev/null" &
done
wait
prev=$cur
fi
sleep "$POLL_SEC"
doneWrap in a LaunchAgent at ~/Library/LaunchAgents/com.example.colortheme-sync.plist:
<plist version="1.0"><dict>
<key>Label</key><string>com.example.colortheme-sync</string>
<key>ProgramArguments</key><array>
<string>/Users/you/bin/sync-color-theme.sh</string>
</array>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/Users/you/Library/Logs/colortheme-sync.log</string>
<key>StandardErrorPath</key><string>/Users/you/Library/Logs/colortheme-sync.log</string>
</dict></plist>Bootstrap once:
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.colortheme-sync.plistWith the agent running, a macOS Appearance change pushes CLIENT_DARK_MODE to each host's tmux global env. Tabby's next auto-theme tick (a few seconds) re-reads it via tmux show-environment -g CLIENT_DARK_MODE and flips theme + global tmux window-style accordingly. No daemon restart, no reattach.
| Category | Path | Env Override |
|---|---|---|
| Config | ~/.config/tabby/config.yaml |
TABBY_CONFIG_DIR |
| Pet state | ~/.local/state/tabby/pet.json |
TABBY_STATE_DIR |
| Thought cache | ~/.local/state/tabby/thought_buffer.txt |
TABBY_STATE_DIR |
| Runtime | /tmp/tabby-* |
-- |
Edit ~/.config/tabby/config.yaml:
# Tab grouping rules (first match wins)
groups:
- name: "Frontend"
pattern: "^FE|"
working_dir: "~/projects/frontend" # Default dir for new windows
theme:
bg: "#e74c3c"
fg: "#ffffff"
active_bg: "#c0392b"
active_fg: "#ffffff"
icon: ""
- name: "Backend"
pattern: "^BE|"
working_dir: "~/projects/backend"
theme:
bg: "#27ae60"
fg: "#ffffff"
active_bg: "#1e8449"
active_fg: "#ffffff"
icon: ""
- name: "Default"
pattern: ".*"
theme:
bg: "#3498db"
fg: "#ffffff"
active_bg: "#2980b9"
active_fg: "#ffffff"
# Indicators
indicators:
activity:
enabled: false
icon: "!"
color: "#000000"
bell:
enabled: true
icon: "◆"
color: "#000000"
bg: "#ffff00" # Yellow background for visibility
silence:
enabled: true
icon: "○"
color: "#000000"
busy:
enabled: true
icon: "◐"
color: "#ff0000"
frames: ["◐", "◓", "◑", "◒"] # Animation frames
input:
enabled: true
icon: "?"
color: "#ffffff"
bg: "#9b59b6" # Purple - needs attention
frames: ["?", "?"] # Can add blinking: ["?", " "]
# Vertical sidebar settings
sidebar:
position: left # "left" or "right"
mode: full # "full" (full window height) or "partial" (attach to pane)
new_tab_button: true
new_group_button: true
show_empty_groups: true
close_button: false
sort_by: "group" # "group" or "index"
colors:
disclosure_fg: "#000000"
disclosure_expanded: "⊟"
disclosure_collapsed: "⊞"
active_indicator: "◀" # Active window/pane indicator
active_indicator_fg: "auto" # "auto" uses group/window bg colorWindows are organized into groups based on name patterns or manual assignment:
+---------------------------+
| SIDEBAR | SESSION
| | |
| Frontend [group] | +-- Frontend (group)
| 0. dashboard | | +-- 0. dashboard (window)
| 1. components | | | +-- pane 0: vim
| | | | +-- pane 1: terminal
| Backend [group] | | +-- 1. components (window)
| 2. api | | +-- pane 0: npm run dev
| 3. tests | |
| | +-- Backend (group)
| Default [group] | | +-- 2. api (window)
| > 4. vim | | +-- 3. tests (window)
| 5. notes | |
| | +-- Default (group)
| [+] New Tab | +-- 4. vim (window) <- active
+---------------------------+ +-- 5. notes (window)
By pattern - Windows matching a regex are auto-grouped:
^FE|matchesFE|dashboard,FE|components^BE|matchesBE|api,BE|tests.*catches everything else in Default
By right-click menu - Select "Move to Group" to manually assign
By tmux option - Set programmatically:
tmux set-window-option -t :0 @tabby_group "Frontend"Set custom colors for individual windows or groups:
Window colors - Right-click window → Set Tab Color:
- Predefined colors: Red, Orange, Yellow, Green, Blue, Purple, Pink, Cyan, Gray
- Transparent: No background, simple text color (minimal visual)
- Reset to default group color
Group colors - Right-click group → Edit Group → Change Color:
- Same color options as windows
- Transparent: Clean text-only display for the entire group
- Affects all windows in the group (unless they have custom colors)
Set programmatically:
# Set window to transparent
tmux set-window-option -t :0 @tabby_color "transparent"
# Set window to custom color
tmux set-window-option -t :0 @tabby_color "#e91e63"
# Reset to group color
tmux set-window-option -t :0 -u @tabby_colorSet a default working directory for each group. New windows created in the group will automatically use this directory:
Via context menu: Right-click group → Edit Group → Set Working Directory
In config.yaml:
groups:
- name: "MyProject"
working_dir: "~/projects/myproject"
# ...Rename panes with title locking (like window names):
- Right-click pane → Rename
- Locked titles persist until manually unlocked
- Right-click pane → Unlock Name to restore automatic naming
Set programmatically:
# Set locked pane title
tmux set-option -p -t %123 @tabby_pane_title "My Pane"
# Clear locked title
tmux set-option -p -t %123 -u @tabby_pane_titleThe sidebar hosts pinnable widgets below the window list. Enable and configure per widget in config.yaml under widgets:.
| Widget | Default | What it shows |
|---|---|---|
clock |
on | Local time and date |
pet |
on | Terminal pet with Claude-powered thought bubbles, hunger/happiness state, feeding, and adventure mode |
stats |
on | CPU, memory, and battery — emoji or bar style |
git |
off | Branch, dirty/clean, ahead/behind, stash count for the active pane's cwd |
session |
off | Current tmux session, client, and window count |
claude |
off | Claude Code usage for today / week / month / total, read from the sqlite history DB |
Each widget supports pin, priority (render order), position: top|bottom, padding, margins, dividers, and per-field colors. See config.yaml for the full schema.
User-facing entry points. Internal subcommands (daemon, watchdog, render …) are spawned automatically by tabby.tmux and shouldn't be invoked by hand.
| Command | What it does |
|---|---|
tabby toggle |
Enable or disable tabby for the current tmux session (daemon lifecycle). Bound by default to prefix + Tab. |
tabby hook toggle-collapse-sidebar |
Hide/show the sidebar via stash/restore. Same action as the mobile hamburger. Bound by default to Cmd+Shift+\. |
tabby hook focus-pane <session:window.pane> |
Jump to a specific pane. Useful from macOS notification deep-links. |
tabby cycle-pane [--ensure-content | --dim-only] |
Cycle the active content pane. --ensure-content moves focus to a content pane only if a sidebar/header is active (invoked from window-switch hooks). --dim-only just re-applies inactive-pane dimming. |
tabby new-window [name] |
Create a new window that inherits the current group's working directory and color. |
tabby manage-group |
Interactive TUI to edit window-group entries in config.yaml. |
tabby pane-picker |
Interactive pane picker for keyboard-driven pane selection. |
tabby setup |
Guided wizard for first-time configuration. |
tabby hook has many more subcommands used by tmux hook lines — ensure-sidebar, on-pane-resize, preserve-pane-ratios, kill-pane, kill-window, split-pane, new-group, set-indicator, osc-handler, resurrect-save, resurrect-restore, etc. These are registered by tabby.tmux and not meant for manual use.
cd ~/.tmux/plugins/tabby
./scripts/install.sh# Install the local git hooks
./scripts/install-git-hooks.shThis installs the tracked repo hooks, including the pre-commit guard that blocks committing logs, local agent state, temporary files, and likely hardcoded secrets. It also replaces any stale external hook-manager wrappers with local repo hooks so commits and pushes do not depend on extra binaries.
# Comprehensive visual tests
./tests/e2e/test_visual_comprehensive.sh
# Tab stability tests
./tests/e2e/test_tab_stability.sh
# Edge case tests
./tests/e2e/test_edge_cases.shTabby includes helper scripts for creating notifications that deep-link back to specific tmux windows/panes. When clicked, the notification brings your terminal to the foreground and navigates to the target location.
Works with both Claude Code and OpenCode out of the box.
- Install a notification tool via Homebrew:
# Recommended: growlrrr (supports custom emoji icons as thumbnails)
brew install growlrrr
# Alternative: terminal-notifier (basic notifications)
brew install terminal-notifier- Configure your terminal app in
config.yaml:
# Options: Ghostty, iTerm, Terminal, Alacritty, kitty, WezTerm
terminal_app: GhosttyThe tabby-hook focus-pane script activates your terminal and navigates tmux:
# Focus window 2, pane 0
~/.tmux/plugins/tabby/bin/tabby-hook focus-pane 2
# Focus window 1, pane 2
~/.tmux/plugins/tabby/bin/tabby-hook focus-pane 1.2
# Focus specific session, window, and pane
~/.tmux/plugins/tabby/bin/tabby-hook focus-pane main:2.1# Simple notification that jumps to window 2
terminal-notifier -title "Build Complete" -message "Click to view" \
-execute "$HOME/.tmux/plugins/tabby/bin/tabby-hook focus-pane 2"
# Notification with current location (useful in scripts/hooks)
TARGET=$(tmux display-message -p '#{window_index}.#{pane_index}')
terminal-notifier -title "Task Done" -message "Click to return" \
-execute "$HOME/.tmux/plugins/tabby/bin/tabby-hook focus-pane $TARGET"Claude Code hooks run as subprocesses, so you need to capture the correct pane — not the currently focused one. The key is using the TMUX_PANE environment variable with tmux display-message -t.
Important: Using tmux display-message -p (without -t) returns the currently focused pane, which may have changed while Claude was working. Using -t "$TMUX_PANE" queries the specific pane where the hook originated.
Add to ~/.claude/settings.json:
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "<tabby-dir>/bin/tabby-hook set-indicator busy 1"
},
{
"type": "command",
"command": "<tabby-dir>/bin/tabby-hook set-indicator input 0"
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "/path/to/your/claude-stop-notify.sh"
}
]
}
],
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "/path/to/your/claude-notification.sh"
}
]
}
]
}
}Notification scripts should:
- Read hook JSON from stdin (
jq -r '.transcript_path'for transcript) - Use
TMUX_PANEenv var to query the originating pane (not current focus) - Call
tabby-hook focus-panefor click-to-navigate deep links - Set tabby indicators (
tabby-hook set-indicator busy 0,bell 1, etc.) - Use growlrrr with
--imagefor emoji group icon thumbnails
See the example hook scripts or use the built-in OpenCode hook as a reference.
#!/usr/bin/env bash
# claude-stop-notify.sh — Rich notification with emoji icons + deep linking
set -u
TABBY_DIR="${HOME}/.tmux/plugins/tabby"
INDICATOR="$TABBY_DIR/bin/tabby-hook set-indicator"
# Read hook JSON from stdin (Claude provides session info)
HOOK_JSON=$(cat)
TRANSCRIPT_PATH=$(echo "$HOOK_JSON" | jq -r '.transcript_path // empty')
# Get tmux info for the SPECIFIC pane where Claude runs
# CRITICAL: Use -t "$TMUX_PANE" to query the originating pane, not current focus
if [[ -n "${TMUX:-}" && -n "${TMUX_PANE:-}" ]]; then
WINDOW_NAME=$(tmux display-message -t "$TMUX_PANE" -p '#W')
TMUX_TARGET=$(tmux display-message -t "$TMUX_PANE" -p '#{session_name}:#{window_index}.#{pane_index}')
fi
# Extract last assistant message from transcript
MESSAGE="Session complete"
if [[ -n "$TRANSCRIPT_PATH" && -f "$TRANSCRIPT_PATH" ]]; then
LAST_MSG=$(tac "$TRANSCRIPT_PATH" | grep -m1 '"type":"assistant"' | jq -r '
.message.content |
if type == "array" then
[.[] | select(.type == "text") | .text] | join(" ")
elif type == "string" then .
else empty end
' 2>/dev/null)
[[ -n "$LAST_MSG" && "$LAST_MSG" != "null" ]] && \
MESSAGE=$(echo "$LAST_MSG" | tr '\n' ' ' | sed 's/ */ /g' | cut -c1-300)
fi
# Send notification with click-to-focus (growlrrr preferred, terminal-notifier fallback)
if command -v growlrrr &>/dev/null; then
growlrrr send --appId ClaudeCode --title "$WINDOW_NAME" \
--subtitle "Task complete" --sound default \
--execute "$TABBY_DIR/bin/tabby-hook focus-pane $TMUX_TARGET" \
"$MESSAGE" &>/dev/null &
elif command -v terminal-notifier &>/dev/null; then
terminal-notifier -title "$WINDOW_NAME" -message "$MESSAGE" \
-sound default -execute "$TABBY_DIR/bin/tabby-hook focus-pane $TMUX_TARGET" &>/dev/null &
fi
# Set tabby indicators
"$INDICATOR" busy 0
"$INDICATOR" bell 1Tabby includes a built-in OpenCode hook at scripts/opencode-tabby-hook.sh. It supports:
- All OpenCode events (complete, permission, question, error, start)
- Emoji group icon thumbnails via growlrrr
- SQLite-based message extraction from OpenCode's database
- Process tree walking to find the correct
TMUX_PANE - Tabby sidebar indicators (busy, input, bell)
Create ~/.config/opencode/opencode-notifier.json:
{
"sound": false,
"notification": false,
"command": {
"enabled": true,
"path": "<tabby-dir>/scripts/opencode-tabby-hook.sh",
"args": ["{event}", "{projectName}", "{sessionTitle}", "{message}"],
"minDuration": 0
},
"events": {
"complete": { "sound": false, "notification": false },
"permission": { "sound": false, "notification": false },
"error": { "sound": false, "notification": false }
}
}Set sound and notification to false in the notifier config since the hook script handles notifications directly via growlrrr/terminal-notifier.
By default, macOS banner notifications disappear after ~5 seconds. To make them persist until clicked:
- Open System Settings → Notifications → terminal-notifier (or growlrrr)
- Change notification style from Banners to Alerts
To avoid duplicate notifications when using custom hooks:
Claude Code — add to ~/.claude/settings.json:
{
"preferredNotifChannel": "none"
}OpenCode — set sound and notification to false in opencode-notifier.json (shown above).
Tabby integrates with tmux-resurrect so your sessions survive tmux server restarts and reboots. When resurrect is installed, Tabby automatically:
- On save (
prefix + Ctrl-s): Strips Tabby utility panes (sidebar, pane headers) from the save file so they don't create zombie shell panes on restore. - On restore (
prefix + Ctrl-r): Cleans stale runtime state, kills leftover processes, and re-initializes the sidebar based on your saved mode.
Install tmux-resurrect via TPM:
# Add to ~/.tmux.conf (before the Tabby plugin line)
set -g @plugin 'tmux-plugins/tmux-resurrect'Then prefix + I to install, or tmux source ~/.tmux.conf to reload. That's it — Tabby detects resurrect and wires the hooks automatically.
| Preserved by resurrect | Restored by Tabby |
|---|---|
| Window layout and names | Sidebar UI |
| Pane working directories | Pane headers |
| Running programs (vim, etc.) | Daemon process |
Global options (@tabby_sidebar mode) |
Mouse state cleanup |
| Window groups and custom colors | Runtime files |
git clone https://github.com/tmux-plugins/tmux-resurrect ~/.tmux/plugins/tmux-resurrectAdd to ~/.tmux.conf (before Tabby's run-shell line):
run-shell ~/.tmux/plugins/tmux-resurrect/resurrect.tmuxTabby only sets the resurrect hook options if they are unset or already owned by Tabby. If you have custom resurrect hooks configured, Tabby will not override them. To use both, chain them in a wrapper script:
#!/usr/bin/env bash
# my-resurrect-restore-wrapper.sh
/path/to/your/custom-hook.sh
~/.tmux/plugins/tabby/bin/tabby-hook resurrect-restore- Mosh does not support mouse events — Mosh strips mouse escape sequences, so sidebar clicks, right-click context menus, and middle-click close will not work over mosh connections. Keyboard navigation works normally. If you need mouse support, use SSH directly instead of mosh.
- Ensure Tabby is not explicitly disabled:
tmux show -gv @tabby_enabled(should not be0) - Run
tmux source ~/.tmux.confto reload - Check if binaries exist:
ls ~/.tmux/plugins/tabby/bin/
- Verify the toggle key binding:
tmux list-keys | grep toggle_sidebar - Check if the sidebar binary is running:
ps aux | grep sidebar
tabby-zj — A port of Tabby to Zellij as a single Rust WASM plugin. Same grouped tab/pane sidebar, context menus, indicators, and widgets — no tmux required.
- cmux — AI-powered tmux session manager with intelligent window organization
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Run the test suite
- Submit a pull request
MIT License - see LICENSE file for details


