conceptual guide

Zellij — sessions, modes, and persistence

What problem a multiplexer solves

A terminal multiplexer puts a long-lived session process between your terminal emulator and your shells. Your shells talk to the session, the session talks to whatever terminal happens to be attached. Detach the terminal — close wezterm, reboot a flaky SSH connection, switch desks — and the session keeps running. Attach a new terminal and the same shells, the same scrollback, the same layout come back as if nothing happened.

This is different from what i3 and wezterm already give you. i3 manages X windows: when you close a window, the program inside it dies. Wezterm gives you tabs and splits inside one terminal process: when you close that process, the shells inside die with it. A multiplexer is what makes shell state itself outlive the terminal.

LayerWhat it ownsSurvives what
i3wmWindow placement on outputsPrograms surviving X restart? No.
weztermTerminal tabs & splits inside its processClosing wezterm? No.
zellijSessions, tabs, panes inside a sessionClosing wezterm, reboots (with resurrection), SSH drops. Yes.
Practical rule of thumb. The win from zellij grows with how much you do over SSH or how often your terminal dies on you. Locally on i3+wezterm, the main payoff is "I closed the wrong tab and lost my work" never being a problem again.

Sessions, tabs, and panes

A zellij session contains tabs. Each tab contains a tree of panes. Panes are leaves. The tab itself isn't a leaf — it's a labeled root that owns a pane tree. This is similar to i3's tree, but rooted at a session and stopping at the pane (zellij doesn't span monitors).

session "main" one zellij process tab "code" focused · Ctrl+T 1 tab "build" Ctrl+T 2 tab "scratch" Ctrl+T 3 vsplit arranges children top/bottom pane (editor) nvim · 70% hsplit side-by-side pane (shell) bash pane (logs) tail -f session tab split (parent pane) leaf pane (runs a process)
Session → tabs → pane tree. Each tab has its own independent tree.

Floating panes

Inside any tab you can also have floating panes: free-positioned overlays on top of the tiled tree. Toggle the floating layer with Alt+f or in PANE mode with w. A pane can move back and forth: Ctrl+P e embeds a floating pane into the tile tree, and vice-versa.

Pinned panes

A floating pane can be pinned (Ctrl+P i): it stays visible across all tabs in the session. Good for a always-visible system monitor or chat client without forcing it into the tile tree.

Modes — the modal keybind machine

Zellij doesn't use a single "leader key" the way tmux does (Ctrl-b then a letter). Instead, every category of action lives in its own mode, and from NORMAL you switch to one mode at a time. While inside a mode, plain letters do things; press Enter or Esc to return.

This is good because the status bar at the bottom always shows what the keys mean. It's annoying because Ctrl+S and Ctrl+P are also keys editors care about — hence the LOCKED mode escape hatch.

NORMAL keys go to shell PANE Ctrl+P TAB Ctrl+T RESIZE Ctrl+N SCROLL Ctrl+S / B SESSION Ctrl+O MOVE Ctrl+H LOCKED Ctrl+G SEARCH / within SCROLL solid: Ctrl-shortcut switches in · dashed: Enter/Esc returns to NORMAL
The zellij mode graph. Each mode swallows letter keys for its category; Enter/Esc returns.

The escape hatch: LOCKED

Some programs — vim, htop, tig, IPython — use the same Ctrl-prefixed keys that zellij claims. Press Ctrl+G and zellij enters LOCKED: every keystroke is forwarded to the running pane untouched. Press Ctrl+G again to come back. The status bar shows a padlock so you don't forget.

Useful trick. Most pane keys (new pane, focus left/right, resize) have NORMAL-mode Alt+letter aliases. So in practice you can avoid PANE mode entirely most of the time, and only enter modes when you need a less-common action.

Layouts are blueprints, not state

A layout file describes the initial structure of a session: tabs, splits, pane sizes, what command runs in each pane. Zellij reads the layout once, when the session is created, then forgets about it. Subsequent edits, splits, and resizes happen on the live session — they don't update the layout file. Re-running zellij --layout dev creates a fresh session at that initial structure.

layout {
  tab name="code" focus=true {
    pane size="70%" name="editor"
    pane split_direction="vertical" {
      pane name="shell"
      pane name="logs" command="tail" {
        args "-F" "/var/log/syslog"
      }
    }
  }
  tab name="scratch" {
    pane
  }
}

Key primitives

pane
A leaf or a parent. Without children, it runs your shell (or a command).
split_direction
On a parent pane, lays children top-to-bottom (horizontal) or side-by-side (vertical).
size
Percentage ("50%") or fixed cells (10). Omit to share remaining space.
command + args
Run a process instead of the shell. Pane closes when the process exits — unless you also set close_on_exit false.
cwd
Working directory for the pane (file path, supports ~).
name
Label shown on the pane frame and in the pane switcher.
edit
Open a file in $EDITOR instead of running a shell.
floating_panes
Block containing panes positioned with x, y, width, height.
pane_template / tab_template
Define a reusable structure and reference it by name later in the file.
Mental model: a layout is to a zellij session what a Dockerfile is to a container. It describes the starting image. After "run", the container has its own life.

How persistence actually works

There are two distinct things that "survive" with zellij, and confusing them leads to surprises.

1. Detach — the session process keeps running

When you press Ctrl+O d (or your terminal dies and on_force_close "detach" is set), zellij does not stop. It just disconnects the terminal. The zellij server process is still alive, holding your shells, scrollback, layout, and pane state in memory. zellij ls shows it as a live session you can re-attach to instantly.

This is the fast path. It works as long as the machine doesn't reboot.

2. Resurrection — disk-backed sessions

If session_serialization true is in your config, zellij also periodically writes the session's layout to disk under ~/.local/share/zellij/$VER/sessions/. With pane_viewport_serialization true + scrollback_lines_to_serialize N, it also serializes the visible viewport and the last N scrollback lines.

When the actual process is gone (system reboot, OOM kill), zellij attach NAME against a serialized session rebuilds it: same tabs, same pane tree, same scrollback. Running processes are not resurrected — they died with the original process — but your shells start fresh in the right places.

ATTACHED terminal connected process alive, in memory DETACHED no terminal process alive, in memory RESURRECTABLE no process state on disk DEAD delete-session nothing on disk Ctrl+O d attach reboot / OOM attach Ctrl+Q / kill-session delete-session
Detach is cheap and lossless. Resurrection is slower and only rebuilds structure, not running processes.
The kill question. Ctrl+Q and zellij kill-session end the process and (depending on config) leave the on-disk state behind. After that, zellij attach NAME still resurrects the structure. Use zellij delete-session only when you want the state gone for good.

Where things live on disk

~/.config/zellij/config.kdl
Main config. Hot-reloaded on save.
~/.config/zellij/layouts/
Your named layouts (zellij --layout NAME looks here first).
~/.config/zellij/themes/
Custom themes (KDL).
~/.local/share/zellij/$VER/sessions/
Serialized session state for resurrection.
~/.cache/zellij/$VER/
Runtime control sockets and per-session cache. Wiped when a session ends.
/tmp/zellij-<uid>
Active IPC sockets — what zellij ls reads to find running sessions.

Implication: /tmp is volatile

On most distros /tmp is tmpfs and wiped at boot. That's why "active" sessions never survive a reboot — their socket is gone. Serialized state under ~/.local/share/zellij/ does survive, and powers resurrection.

Plugins, briefly

Zellij's UI is largely plugins. The status bar at the bottom, the tab bar at the top, the strider file picker, the session manager — all WASM plugins loaded from zellij:NAME URLs. You can write your own in Rust (or any WASM-compatible language) and load them with file:/path, then pin them as panes in a layout:

pane size=2 borderless=true {
  plugin location="zellij:tab-bar"
}
pane {
  plugin location="file:/home/erick/dev/my-plugin/target/wasm32-wasip1/release/plugin.wasm"
}

For day one you don't need this — the bundled plugins are doing a lot of work invisibly. But it's worth knowing the model: the UI is replaceable.

Key conflicts with this stack

Keystrokes flow top-down through your stack: i3wm sees them first, then wezterm, then zellij, then whatever's running inside the pane (nvim, shell). Every layer can swallow a key before it reaches the next.

i3wm Mod4-prefixed wezterm Ctrl+Shift+* zellij modes via Alt/Ctrl nvim / shell whatever's left A key only reaches a layer if every layer to its left declines to handle it.
Each layer is a gate. Keys that aren't bound at one layer fall through to the next.

i3wm — no conflicts

i3 uses Mod4 (Super) as its prefix and never claims plain Ctrl+letter. Even compound shortcuts like Mod4+Ctrl+l only fire on the exact 3-key combo, so they don't shadow plain Ctrl+L.

Wezterm — was eating Ctrl+H and Ctrl+L

The original config bound Ctrl+H / Ctrl+L for tab navigation. Those keys are also nvim's window-direction (<C-w>h shortcut), shell flow-control / clear-screen, and zellij's MOVE-mode entry. They've been moved to Ctrl+Alt+H / Ctrl+Alt+L — a Ctrl+Alt combo that nothing else on this system claims (gsd-media-keys is not running, so its ghost gsettings entries are inert).

Zellij — rebinds for nvim friendliness

Zellij's defaults put mode entry on Ctrl+P (PANE) and Ctrl+O (SESSION). Both are heavily used in nvim: <C-p> is the file picker / FZF idiom, <C-o> is the jumplist. This config moves those to Alt+p and Alt+o so nvim keeps them when running inside a zellij pane.

Zellij tab nav lives on Alt+h / Alt+l (overriding the default MoveFocusOrTab on those keys) so that Ctrl+H / Ctrl+L remain a clean passthrough to nvim window-direction and bash. The MoveFocusOrTab behavior is still available on Alt+arrow keys for pane focus that crosses tab edges.

KeyDefault zellijThis configWhy
Ctrl+PPANE mode(unbound) — passes to nvimnvim file picker
Ctrl+OSESSION mode(unbound) — passes to nvimnvim jumplist
Alt+p(unbound)PANE modereplacement for Ctrl+P
Alt+o(unbound)SESSION modereplacement for Ctrl+O
Ctrl+HMOVE mode(unbound) — passes throughnvim window-left, bash backspace
Ctrl+L(unbound)(unbound) — passes throughnvim window-right, bash clear-screen
Alt+hMoveFocusOrTab Leftzellij tab prevdedicated tab nav (focus moves on Alt+arrow)
Alt+lMoveFocusOrTab Rightzellij tab nextdedicated tab nav

The LOCKED escape hatch

The remaining defaults (Ctrl+T, Ctrl+S, Ctrl+N, Ctrl+G, Ctrl+Q) still shadow some nvim and shell keys, but they're either rarely-used or the action is more useful at the zellij layer. When you do hit a collision — say, inside htop or a vim plugin that uses Ctrl+T — press Ctrl+G to enter LOCKED and every key after that passes straight through to the inner program. Ctrl+G again to unlock.

Trade-off you accepted: two extra Ctrl-letter chords (Ctrl+P, Ctrl+O) belong to nvim; one extra Alt-letter chord (Alt+o for detach) belongs to your fingers. For a heavy nvim user this is the right side of the trade.

One-page summary