Zellij Session Manager — Every Terminal, Named, Resumable, Remote
The Problem
Two laptops, two cities, terminal chaos. Sessions unnamed, untracked, and gone after reboot.
Cities
2
Bengaluru ↔ Guwahati
Sessions
??
Unnamed, untracked
After reboot
0
Everything gone
What was happening
1
Open terminal, cd ~/git/kri, start working
2
Open another terminal, cd ~/git/atlas, start working
3
Laptop restarts — both terminals gone, no idea what was running
4
SSH from Guwahati — can’t reach any sessions, start over
The core issue: Terminal sessions are anonymous, ephemeral, and local. No naming, no persistence, no remote access.
Why Zellij (not tmux)
Zellij gives us session resurrection without plugins, discoverable keybindings without memorization, and floating panes without configuration.
Zellij wins
| Session restore | Built-in, no plugins |
|---|---|
| Discoverability | Live keybinding hints |
| Floating panes | Built-in (Alt-p) |
| Layout files | KDL (human-readable) |
| Config to be productive | Zero lines needed |
tmux wins
| Pre-installed | Every server |
|---|---|
| Plugin ecosystem | Mature (TPM) |
| Scripting | Shell-scriptable |
| Community | 15+ years of guides |
| Memory | ~4MB vs ~22MB |
The decision: Zellij for local dev (better UX, built-in resurrection). tmux stays on remote servers (pre-installed everywhere). The shell hook + Tailscale bridge the gap.
Architecture
4 scripts + 1 config + 1 shell hook = every terminal tracked, named, and resumable.
zj
capture
Existing terminal → Zellij session
sess
fzf
Pick from all sessions
shell hook
auto
New terminal → auto-create session
Complete architecture — animated LIVE
Session Lifecycle
From opening a terminal to resuming after reboot — every state transition.
State machine — animated transitions
Reboot Recovery
How sessions survive a laptop restart.
The chain
1
Zellij saves to disk — every pane layout, tab, scrollback → ~/.local/share/zellij/
2
Laptop reboots — all processes die, disk persists
3
Tailscale auto-connects — stable IP 198.51.100.27 returns
4
Open terminal — shell hook runs, finds saved session
5
Zellij restores — layout + scrollback back, empty shells in right positions
Important: Zellij restores the layout (panes, tabs, positions) and scrollback. It does not restore running processes. A python server.py that was running will need to be re-started. This is true for every multiplexer — no tool survives a reboot and brings back live processes.
config.kdl
Zellij configuration — mode-based keybindings, dark theme, sensible defaults.
Key design choices
| Setting | Value | Why |
|---|---|---|
| default_shell | bash | Consistent across terminals |
| scroll_buffer_size | 50000 | Deep scrollback for logs |
| pane_frames | false | Clean look, less visual noise |
| mirror_session | true | New panes open in current dir |
| theme | custom dark | Matches blog design |
Keybinding summary
| Binding | Action | Mode |
|---|---|---|
| Alt-d / Alt-r | New pane (down / right) | Pane |
| Alt-hjkl | Move between panes | Pane |
| Alt-f | Toggle fullscreen pane | Pane |
| Alt-p | Floating pane | Global |
| Alt-t | New tab | Tab |
| Alt-1..5 | Switch to tab N | Tab |
| Alt-s | Detach | Session |
| Ctrl-p | Enter pane mode (see all keys) | Global |
| Ctrl-t | Enter tab mode (see all keys) | Global |
zj — Capture Existing Terminal
The missing piece: bring an already-running terminal into Zellij.
Usage
# In any terminal — capture this shell into a Zellij session
$ zj
Creating session: kri
# Capture with explicit name
$ zj my-project
Creating session: my-project
# List all sessions
$ zj -l
kri 3 panes
atlas 5 panes
# Kill a session
$ zj -k old-project
# Attach from outside Zellij
$ zj -a kri
What “capture” means
zj creates a new Zellij session with the same $PWD. Your current shell continues until you type exit. On the next new terminal, the shell hook auto-attaches to this session.
Inside Zellij already? zj creates a new tab instead of a new session. This is how you expand your workspace without leaving Zellij.
sess — Fuzzy Session Picker
Type sess and fzf shows all Zellij sessions. Type to filter, enter to attach.
sess flow — animated
zj-migrate — Capture All Existing Terminals
One command to scan every running terminal and offer to create Zellij sessions for them.
How it works
$ zj-migrate
Scanning running terminals...
Found 3 terminal(s) to capture:
DIRECTORY SESSION NAME
────────────────────────────────────── ────────────
/home/dk/Documents/git/kri kri (new)
/home/dk/Documents/git/atlas atlas (new)
/home/dk/Documents/git/blog-drafts blog-drafts (new)
Create Zellij sessions for these? [y/N] y
CREATE kri → /home/dk/Documents/git/kri
CREATE atlas → /home/dk/Documents/git/atlas
CREATE blog-drafts → /home/dk/Documents/git/blog-drafts
Done! 3 session(s) created.
List sessions: zellij list-sessions
Attach: zellij attach <session>
Fuzzy picker: sess
What it scans: All bash/zsh/fish processes, all terminal emulator processes (gnome-terminal, kitty, alacritty, wezterm). Derives session names from their current working directories. Skips duplicates and Zellij-managed panes.
Shell Hook
The magic: every new terminal auto-creates/attaches to a Zellij session named after the directory.
The hook
# ~/.config/zellij/sess.sh — add to ~/.bashrc
if [[ -z "$ZELLIJ" ]]; then
[[ $- == *i* ]] || return
local session=$(basename "$PWD" | tr '.' '-')
[[ "$session" == "$(basename "$HOME")" ]] && session="main"
command -v zellij &>/dev/null || return
if zellij list-sessions 2>/dev/null | grep -q "^${session}:"; then
exec zellij attach "$session"
else
exec zellij --session "$session"
fi
fi
How it works: On every shell startup, check if we’re inside Zellij. If not, derive a session name from $PWD. If the session exists → attach. If not → create. The exec replaces the shell process so Zellij takes over cleanly.
Edge cases handled
| Scenario | Behavior |
|---|---|
| In $HOME | Session named “main” (not “dk”) |
| Non-interactive shell | Skipped (scripts, cron) |
| Zellij not installed | Skipped gracefully |
| Already inside Zellij | Skipped (no nested Zellij) |
| Directory with dots | Dots → dashes (e.g. my.project → my-project) |
Install
Everything you need, in order.
Quick install
# 1. Install Zellij (pre-built binary, fastest)
$ curl -sL https://github.com/zellij-org/zellij/releases/download/v0.44.3/zellij-x86_64-unknown-linux-musl.tar.gz | tar xz
$ mv zellij ~/.local/bin/
# 2. Create config
$ mkdir -p ~/.config/zellij
$ cat > ~/.config/zellij/config.kdl ooter
# 3. Add shell integration
$ echo 'source ~/.config/zellij/sess.sh' >> ~/.bashrc
# 4. Install scripts
$ cp zj zj-migrate sess ~/.local/bin/
$ chmod +x ~/.local/bin/{zj,zj-migrate,sess}
# 5. Restart shell
$ source ~/.bashrc
# 6. Capture existing terminals
$ zj-migrate
Prerequisites
| Tool | Required | Install |
|---|---|---|
| Zellij | Yes | Binary above or cargo install zellij |
| fzf | For sess | apt install fzf |
| Tailscale | For remote | Already installed |
Key Bindings
Shell
| zj | Capture terminal → session |
|---|---|
| zj name | Capture with name |
| zj -l | List sessions |
| zj -k name | Kill session |
| zj -a name | Attach to session |
| sess | Fuzzy session picker |
| zj-migrate | Capture all terminals |
Inside Zellij
| Alt-d / Alt-r | New pane (down/right) |
|---|---|
| Alt-hjkl | Move between panes |
| Alt-f | Toggle fullscreen |
| Alt-p | Floating pane |
| Alt-t | New tab |
| Alt-1..5 | Switch tab |
| Ctrl-p | Pane mode (all keys) |
| Ctrl-t | Tab mode (all keys) |
Remote Access via Tailscale
From Guwahati, from any city, from any machine on your Tailscale network.
The flow
# From Guwahati (or anywhere on Tailscale)
$ ssh 198.51.100.27
# List all sessions
$ sess
# → fzf shows: ● kri ○ atlas ○ blog
# Or attach directly
$ zellij attach kri
# Everything is exactly where you left it
# Pane layouts, scrollback, tab names — all preserved
Tailscale advantage: Stable IP (198.51.100.27) that survives network changes. Auto-connects on boot. WireGuard encryption. No port forwarding, no DNS setup.
Cheat Sheet
| Session Management |
|---|
| zj |
| zj name |
| zj -l |
| zj -k name |
| sess |
| zj-migrate |
| Inside Zellij (Alt-based, no prefix) |
| Alt-d / Alt-r |
| Alt-h / Alt-j / Alt-k / Alt-l |
| Alt-f |
| Alt-p |
| Alt-t |
| Alt-1..5 |
| Alt-s |
| Alt-x |
| Alt-w |
| Ctrl-p |
| Ctrl-t |
| Ctrl-s |
Enjoyed this post?
Get the next one in your inbox — only when I ship something worth reading.
Newsletter form not configured.
Or follow on Substack for the newsletter.
Comments via GitHub Discussions
Comments not configured. Set GISCUS env vars to enable.