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
- Development
- Tabby Web (Local-Only)
- macOS Notifications with Deep Links
- 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/scripts/focus_pane.sh main:2.1"- Vertical sidebar - clickable, persistent across windows with collapse/expand
- Window grouping - color-coded project organization with 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, and input alerts
- Mouse support - click, right-click menus, middle-click close
- Custom tab colors - per-window color overrides, including transparent mode
- Pane management - rename panes with title locking
- Group management - create, rename, color, collapse, and set working directories
- 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 |
prefix + G |
Create new group |
Ctrl + < or Alt + < |
Collapse/expand sidebar |
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.
The sidebar can be collapsed to maximize screen space:
- Click the right edge (divider area) to collapse
- Keyboard:
Ctrl+<orAlt+<to toggle - Collapsed state: Shows
>down the entire height - click anywhere to expand
When collapsed, the sidebar takes only 2 characters of width. When expanded, it restores to your configured width.
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.
| 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 |
| Web token | ~/.local/state/tabby/web-token |
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_titlecd ~/.tmux/plugins/tabby
./scripts/install.sh# Install the local pre-commit guard
./scripts/install-git-hooks.shThis installs a pre-commit hook that blocks committing logs, local agent state, temporary files, and likely hardcoded secrets.
# 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 Web runs a local-only bridge that exposes tmux + sidebar over WebSocket. The bridge only binds to loopback and requires user/password for access.
Note: the web interface is currently alpha and is not guaranteed to work in all setups yet.
Add this to ~/.config/tabby/config.yaml:
web:
enabled: true
host: "127.0.0.1"
port: 8080
auth_user: "tabby"
auth_pass: "testpass"# Start daemon for a session
go run ./cmd/tabby-daemon -session tabby-web-test
# Start web bridge (loopback only) with auth
go run ./cmd/tabby-web-bridge -session tabby-web-test -host 127.0.0.1 -port 8080 -auth-user tabby -auth-pass testpasscd web
npm install
npm run devOpen http://127.0.0.1:5173/?token=<token>&user=<user>&pass=<pass>&pane=<pane_id>&ws=127.0.0.1:8080
- Token is stored at
~/.local/state/tabby/web-token - Pane ID can be retrieved with
tmux list-panes -t tabby-web-test -F '#{pane_id}' - The bridge rejects non-loopback requests
tabby/
├── cmd/
│ ├── tabby-daemon/ # Session coordinator + render payloads
│ ├── sidebar-renderer/ # Per-window sidebar TUI client
│ ├── pane-header/ # Per-pane header TUI client
│ └── render-status/ # Native tmux status rendering helpers
├── pkg/
│ ├── config/ # Configuration loading
│ ├── grouping/ # Tab grouping logic
│ ├── paths/ # XDG config/state paths
│ └── tmux/ # Tmux integration
├── scripts/
│ ├── install.sh # Build and install
│ ├── toggle_sidebar.sh
│ └── ensure_sidebar.sh
├── tests/ # Test suites
├── config.yaml # Default configuration template
└── tabby.tmux # Plugin entry point
Tabby 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 focus_pane.sh script activates your terminal and navigates tmux:
# Focus window 2, pane 0
~/.tmux/plugins/tabby/scripts/focus_pane.sh 2
# Focus window 1, pane 2
~/.tmux/plugins/tabby/scripts/focus_pane.sh 1.2
# Focus specific session, window, and pane
~/.tmux/plugins/tabby/scripts/focus_pane.sh main:2.1# Simple notification that jumps to window 2
terminal-notifier -title "Build Complete" -message "Click to view" \
-execute "$HOME/.tmux/plugins/tabby/scripts/focus_pane.sh 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/scripts/focus_pane.sh $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>/scripts/set-tabby-indicator.sh busy 1"
},
{
"type": "command",
"command": "<tabby-dir>/scripts/set-tabby-indicator.sh 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
focus_pane.shfor click-to-navigate deep links - Set tabby indicators (
set-tabby-indicator.sh 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/scripts/set-tabby-indicator.sh"
# 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/scripts/focus_pane.sh $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/scripts/focus_pane.sh $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).
- 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
- 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


