Programmatic Zellij automation for humans, scripts, and agents alike.
zjctl is to Zellij what Playwright is to the web: a reliable, programmable way to control it.
zjctl is a CLI + plugin that lets you script Zellij end-to-end (actions,
status, setup, and pane operations) via a single CLI command.
- Target panes by selector (id/title/cmd/regex/focused)
- Operate on panes directly: send, focus, rename, resize, capture, wait-idle
- Launch panes and get back a selector
- JSON output for automation
- Action passthrough for
zellij action
Requires Zellij 0.43+.
cargo install zjctl
zjctl install --loadzjctl install downloads the plugin, updates config.kdl, and can load it in
the current session.
# Download CLI binary
curl -L https://github.com/mrshu/zjctl/releases/latest/download/zjctl-x86_64-linux.tar.gz | \
tar -xz -C ~/.local/bin/# Download plugin binary
mkdir -p ~/.config/zellij/plugins
curl -L https://github.com/mrshu/zjctl/releases/latest/download/zrpc.wasm \
-o ~/.config/zellij/plugins/zrpc.wasm# Load the plugin in the current session
zellij action launch-plugin "file:~/.config/zellij/plugins/zrpc.wasm"To auto-load on startup, add to ~/.config/zellij/config.kdl:
load_plugins {
"file:~/.config/zellij/plugins/zrpc.wasm"
}Accept the permissions when prompted. The plugin runs as a background service.
Verify setup:
zjctl doctor
zjctl doctor --json# Prerequisites
rustup target add wasm32-wasip1
# Build and install CLI
cargo build --release -p zjctl
cp target/release/zjctl ~/.local/bin/
# Build and install plugin
cargo build --release -p zjctl-zrpc --target wasm32-wasip1
cp target/wasm32-wasip1/release/zrpc.wasm ~/.config/zellij/plugins/If you prefer crates.io for the plugin:
rustup target add wasm32-wasip1
cargo install zjctl-zrpc --target wasm32-wasip1 --root ~/.local
mkdir -p ~/.config/zellij/plugins
cp ~/.local/bin/zrpc.wasm ~/.config/zellij/plugins/zrpc.wasm# 1) Install + verify the plugin
zjctl install --load
zjctl doctor
# 2) Launch a shell pane and run a command
pane=$(zjctl pane launch -- "zsh")
zjctl pane send --pane "$pane" -- "ls -la\n"
# 3) Wait, capture, and clean up
zjctl pane wait-idle --pane "$pane" --idle-time 2 --timeout 30
zjctl pane capture --pane "$pane"
zjctl pane close --pane "$pane"Zellij is great interactively, but it doesn’t give you a stable “handle” to a pane for scripting.
zjctl adds that missing layer: pane-addressed operations you can use from scripts, CI, or
agents, without relying on brittle keybindings or “the currently focused pane”.
Common reasons:
- Repeatable workflows: open a layout, start services, tail logs, and tear everything down.
- Automation + introspection: run commands in a pane,
wait-idle, thencaptureoutput for parsing. - Agent-friendly control: let an LLM drive Zellij by selectors (id/title/regex) instead of keystrokes.
- Safe orchestration: selectors + refusal to close focused panes (unless
--force) reduce footguns. - Glue for existing tools: spawn
fzf,rg, test runners, REPLs, etc. in dedicated panes and drive them.
If you’ve ever wanted “tmux-style scripting” for Zellij panes (with reliable targeting and output capture), that’s the core value.
| Selector | Description |
|---|---|
id:terminal:N |
Terminal pane with ID N |
id:plugin:N |
Plugin pane with ID N |
focused |
Currently focused pane |
title:substring |
Panes with title containing substring |
title:/regex/ |
Panes with title matching regex |
cmd:substring |
Panes running command containing substring |
cmd:/regex/ |
Panes running command matching regex |
tab:N:index:M |
Pane at index M in tab N |
- Always launch a shell pane (prefer
zsh) before running commands; if a command exits, you can lose output. zjctl pane sendwaits 1s before Enter by default; use--enter=falseor--delay-enter 0for immediate input.zjctl pane closerefuses to close the focused pane unless--force.
pane=$(zjctl pane launch -- "zsh")
zjctl pane send --pane "$pane" -- "python script.py\n"
zjctl pane wait-idle --pane "$pane" --idle-time 2 --timeout 30
zjctl pane capture --pane "$pane"
zjctl pane close --pane "$pane"# Inventory and status
zjctl panes ls
zjctl panes ls --json
zjctl status
zjctl status --json
# Send input
zjctl pane send --pane id:terminal:3 -- "ls -la\n"
zjctl pane send --pane id:terminal:3 --enter=false -- "ls -la"
# Navigation and layout
zjctl pane focus --pane title:server
zjctl pane rename --pane focused "API Server"
zjctl pane resize --pane focused --increase --direction right --step 5
# Capture and wait
zjctl pane capture --pane focused
zjctl pane capture --pane focused --full
zjctl pane wait-idle --pane focused --idle-time 3 --timeout 60
# Signals
zjctl pane interrupt --pane id:terminal:3
zjctl pane escape --pane id:terminal:3
# Close / launch
zjctl pane close --pane id:terminal:3
zjctl pane close --pane focused --force
zjctl pane launch --direction right -- "python"
# Help / passthrough
zjctl help
zjctl action new-panewait-idle is useful after pane send: it polls the target pane’s rendered
output until it stops changing for --idle-time seconds (or errors after
--timeout).
- Use
zjctl panes ls --jsonfor selection logic. - Prefer
wait-idleinstead of pollingcapture.
zjctl pane send --pane id:terminal:3 -- "analyze this code\n"
zjctl pane wait-idle --pane id:terminal:3 --idle-time 3.0
zjctl pane capture --pane id:terminal:3# Diagnose setup issues
zjctl doctor
# Reinstall and re-load the plugin if needed
zjctl install --force
zjctl install --load┌─────────┐ ┌──────────────┐ ┌─────────────────┐
│ zjctl │─────▶│ zellij pipe │─────▶│ zrpc plugin │
│ (CLI) │◀─────│ (transport) │◀─────│ (WASM) │
└─────────┘ └──────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Zellij shim API │
│ (pane ops) │
└─────────────────┘
- zjctl: Native CLI binary, sends JSON-RPC requests via
zellij pipe - zrpc: WASM plugin running in Zellij, receives pipe messages, executes pane operations
- Protocol: Newline-delimited JSON (jsonl) with UUID correlation
The plugin requests these Zellij permissions:
ReadApplicationState- to track pane/tab stateWriteToStdin- to send input to panesChangeApplicationState- to focus/rename/resize panesReadCliPipes- to respond to CLI pipe messages
Note: The plugin runs as a hidden background service and won't appear as a visible pane.
# Check all crates
cargo check
# Run tests
cargo test
# Format
cargo fmt
# Lint
cargo clippySome tests require a live Zellij session. To run them locally:
export ZELLIJ_SESSION_NAME="$(zellij list-sessions | head -n1 | awk '{print $1}')"
export ZJCTL_INTEGRATION=1
cargo test -p zjctl --test zellij_integrationMIT