Interactive session picker for tmux
A CLI that gives you fast, fuzzy session management from bare shell,
with project memory, path aliases, and a keyboard-driven TUI.
Install · Quick Start · Commands · Shell Integration · Configuration
Portal is a CLI that runs at bare shell (before entering tmux) and provides an interactive TUI for picking, creating, and managing tmux sessions. It remembers your projects, resolves paths via aliases and zoxide, auto-detects git roots for new sessions, and automatically starts the tmux server and restores your saved sessions after a reboot.
After shell integration, you interact with Portal through two functions: x (session picker / opener) and xctl (subcommands like list, kill, alias). The function names are customizable — see --cmd below.
- tmux ≥ 3.0 (released Feb 2020) — Portal uses array-indexed global hooks
(
set-hook -ga) which require 3.0+. Earlier versions are not supported; Portal exits withPortal requires tmux ≥ 3.0 (found <version>). Please upgrade. - Go (build from source), macOS or Linux.
macOS
brew install leeovery/tools/portalLinux
curl -fsSL https://raw.githubusercontent.com/leeovery/portal/main/scripts/install.sh | bashGo
go install github.com/leeovery/portal@latest# Add shell integration (creates x() and xctl() functions)
echo 'eval "$(portal init zsh)"' >> ~/.zshrc
# Launch the interactive picker
x
# Open a session at a path
x ~/Code/myproject
# Open with a command
x ~/Code/myproject -e "make dev"
# Set up an alias
xctl alias set work ~/Code/work-project
x work
# List running sessions
xctl list
# Kill a session
xctl kill myprojectPortal generates shell functions via portal init. Add to your shell profile:
# zsh
eval "$(portal init zsh)"
# bash
eval "$(portal init bash)"
# fish
portal init fish | sourceThis creates two functions:
x()— launches Portal (interactive picker or path-based session creation)xctl()— direct access to Portal subcommands (list,kill,alias, etc.)
Customize the function name with --cmd:
eval "$(portal init zsh --cmd p)" # creates p() and pctl()Examples below use the default
x/xctlfunction names. If you used--cmd p, substitutepandpctl. You can also call theportalbinary directly.
Interactive session picker or path-based session creation. x maps to portal open.
x # interactive TUI
x ~/Code/myproject # open session at path
x myalias # resolve alias → path → session
x ~/Code/app -e "make dev" # run command in new session
x ~/Code/app -- npm start # alternative command syntax| Flag | Description |
|---|---|
-e, --exec |
Command to execute in the new session |
Path resolution order: aliases → zoxide → TUI with filter.
New sessions auto-resolve to the git repository root when applicable.
Attach to an existing tmux session by name.
xctl attach myprojectList running tmux sessions.
xctl list # auto-detect format
xctl list --long # full details
xctl list --short # names only| Flag | Description |
|---|---|
--long |
Full session details (name, status, window count) |
--short |
Session names only, one per line |
Kill a tmux session by name.
xctl kill myprojectManage path aliases for quick session access.
xctl alias set work ~/Code/work # create alias
xctl alias rm work # remove alias
xctl alias list # list all aliasesRegister per-pane commands that re-execute automatically when a session is attached after a reboot. Must be run from inside a tmux pane.
xctl hooks set --on-resume "npm start" # register a resume hook
xctl hooks rm --on-resume # remove the hook
xctl hooks list # list all hooksWhen hooks fire: Portal fires resume hooks ONLY when a pane is freshly recreated
from saved state on reboot recovery — i.e., the tmux server has just been started
fresh and Portal has restored sessions. Hooks do NOT fire on every detach / reattach
within a single server lifetime. If a pane still exists, its hook process either
already ran or was explicitly killed; firing again would double-launch long-running
processes like claude --resume. This is deliberate.
Remove stale projects whose directories no longer exist on disk, and prune hooks for panes that no longer exist.
xctl cleanInspect or tear down Portal's saved-session state used for reboot restoration.
xctl state status # daemon + state health
xctl state cleanup # remove hooks + stop daemon
xctl state cleanup --purge # also wipe ~/.config/portal/state/xctl state status— print daemon status, last save time, captured counts, state size, and recent warnings. Exits non-zero when the daemon is down, last save is stale, or warnings are present in the last hour.xctl state cleanup [--purge]— kills the daemon and removes Portal's tmux hook entries. With--purge, also removes~/.config/portal/state/.
Print the Portal version.
xctl versionOutput shell integration script for eval. See Shell Integration. This is the one command you call via the portal binary directly.
portal init zsh
portal init bash --cmd p| Key | Action |
|---|---|
↑/k |
Move up |
↓/j |
Move down |
Enter |
Select session / confirm |
/ |
Filter mode (fuzzy search) |
R |
Rename session |
K |
Kill session |
q/Esc |
Quit |
The TUI has three views: session list, project picker, and file browser.
Portal automatically starts the tmux server if absent AND restores saved sessions in the same bootstrap step. After a reboot, your sessions return with structure, layout, zoom, working directories, and scrollback (including ANSI colour). On any tmux-needing command, Portal checks the server, starts it if missing, and re-creates saved sessions that aren't already live. Scrollback injects lazily when you attach. Resume hooks fire on freshly-recreated panes. The TUI shows a "Restoring sessions…" loading screen for at most ~1.2s; the CLI is silent.
Replaces tmux-continuum/tmux-resurrect for session persistence — uninstall those
plugins if you have them (or set @continuum-restore off for tmux-resurrect/continuum)
to avoid duplicate restoration.
Pair this with resume hooks to automatically re-run pane commands (dev servers, editors, etc.) after a reboot.
Portal resolves its config directory using XDG: $XDG_CONFIG_HOME/portal/ if set, otherwise ~/.config/portal/. Each file also has a per-file env var override that takes full precedence.
| File | Purpose | Env override |
|---|---|---|
aliases |
Path aliases (key=value, one per line) | PORTAL_ALIASES_FILE |
projects.json |
Remembered project directories | PORTAL_PROJECTS_FILE |
hooks.json |
Per-pane resume hooks (pane → event → command) | PORTAL_HOOKS_FILE |
state/ |
Saved session structure + scrollback for automatic restoration on reboot. Contains: sessions.json (structure index), scrollback/*.bin (per-pane content), daemon.pid + daemon.version (liveness markers), portal.log (diagnostics). See Privacy Considerations. |
PORTAL_STATE_DIR |
Projects are auto-populated when you create new sessions and cleaned with xctl clean.
Portal persists pane scrollback to ~/.config/portal/state/ (override via
PORTAL_STATE_DIR) so it can rehydrate sessions after a reboot. Files are written
mode 0600, directories 0700.
- Same local-filesystem trust model as your shell history — anything visible in your terminal can end up in the saved state.
- No encryption at rest. If a pane displays secrets (tokens, credentials, diffs of sensitive files), they will be captured.
- Mitigations: for sensitive panes, run
tmux set-option -w history-limit 0to prevent scrollback from accumulating, ortmux clear-historyon demand (run before the next save, which lands at most ~30s later). - v1 has no per-session opt-out; tmux-native workarounds above are the supported path.
Two paths depending on whether you want to keep your saved state:
- Just remove the binary —
brew uninstall portalorrm $(which portal). The defensivecommand -v portalguard in the registered tmux hooks short-circuits when the binary is gone, so tmux keeps running normally. Your saved state is preserved; reinstalling Portal picks up where it left off. - Explicit teardown — run
portal state cleanup(kills the daemon and removes Portal's tmux hook entries), orportal state cleanup --purgeto also wipe saved state under~/.config/portal/state/. Then uninstall the binary. Use--purgefor a completely clean slate. Non-state config (hooks.json,projects.json,aliases) is preserved either way — remove manually if desired.
MIT