Skill: jivvei/zellij (version 1.0.1)

--- name: zellij description: Remote-control zellij sessions for interactive CLIs by sending keystrokes and scraping pane output. homepage: https://zellij.dev metadata: {"moltbot":{"emoji":"🪟","os":["darwin","linux"],"requires":{"bins":["zellij","jq"]},"install":[{"id":"brew","kind":"brew","formula":"zellij","bins":["zellij"],"label":"Install Zellij (brew)"},{"id":"cargo","kind":"cargo","crate":"zellij","bins":["zellij"],"label":"Install Zellij (Cargo)"}]}} --- # zellij Skill (Moltbot) Use zellij only when you need an interactive TTY. Prefer exec background mode for long-running, non-interactive tasks. ## Quickstart (data dir, exec tool) ```bash DATA_DIR="${CLAWDBOT_ZELLIJ_DATA_DIR:-${TMPDIR:-/tmp}/moltbot-zellij-data}" mkdir -p "$DATA_DIR" SESSION=moltbot-python zellij --data-dir "$DATA_DIR" new-session --session "$SESSION" --layout "default" --detach zellij --data-dir "$DATA_DIR" run --session "$SESSION" --name repl -- python3 -q zellij --data-dir "$DATA_DIR" pipe --session "$SESSION" --pane-id 0 ``` After starting a session, always print monitor commands: ``` To monitor: zellij --data-dir "$DATA_DIR" attach --session "$SESSION" zellij --data-dir "$DATA_DIR" pipe --session "$SESSION" --pane-id 0 ``` ## Data directory convention - Use `CLAWDBOT_ZELLIJ_DATA_DIR` (default `${TMPDIR:-/tmp}/moltbot-zellij-data`). - Zellij stores state (sessions, plugins, etc.) in this directory. ## Targeting panes and naming - Zellij uses `pane-id` (numeric) to target specific panes. - Find pane IDs: `zellij --data-dir "$DATA_DIR" list-sessions --long` or use `list-panes.sh`. - Keep session names short; avoid spaces. ## Finding sessions - List sessions on your data dir: `zellij --data-dir "$DATA_DIR" list-sessions`. - List sessions across all data dirs: `{baseDir}/scripts/find-sessions.sh --all` (uses `CLAWDBOT_ZELLIJ_DATA_DIR`). ## Sending input safely - Use `zellij action` to send keystrokes: `zellij --data-dir "$DATA_DIR" action --session "$SESSION" write-chars --chars "$cmd"`. - Control keys: `zellij --data-dir "$DATA_DIR" action --session "$SESSION" write 2` (Ctrl+C). ## Watching output - Capture pane output: `zellij --data-dir "$DATA_DIR" pipe --session "$SESSION" --pane-id 0`. - Wait for prompts: `{baseDir}/scripts/wait-for-text.sh -s "$SESSION" -p 0 -p 'pattern'`. - Attaching is OK; detach with `Ctrl+p d` (zellij default detach). ## Spawning processes - For python REPLs, zellij works well with standard `python3 -q`. - No special flags needed like tmux's `PYTHON_BASIC_REPL=1`. ## Windows / WSL - zellij is supported on macOS/Linux. On Windows, use WSL and install zellij inside WSL. - This skill is gated to `darwin`/`linux` and requires `zellij` on PATH. ## Orchestrating Coding Agents (Codex, Claude Code) zellij excels at running multiple coding agents in parallel: ```bash DATA_DIR="${TMPDIR:-/tmp}/codex-army-data" # Create multiple sessions for i in 1 2 3 4 5; do zellij --data-dir "$DATA_DIR" new-session --session "agent-$i" --layout "compact" --detach done # Launch agents in different workdirs zellij --data-dir "$DATA_DIR" action --session "agent-1" write-chars --chars "cd /tmp/project1 && codex --yolo 'Fix bug X'\n" zellij --data-dir "$DATA_DIR" action --session "agent-2" write-chars --chars "cd /tmp/project2 && codex --yolo 'Fix bug Y'\n" # Poll for completion (check if prompt returned) for sess in agent-1 agent-2; do pane_id=$(zellij --data-dir "$DATA_DIR" list-sessions --long | grep "\"$sess\"" | jq -r '.tabs[0].panes[0].id') if zellij --data-dir "$DATA_DIR" pipe --session "$sess" --pane-id "$pane_id" | grep -q "❯"; then echo "$sess: DONE" else echo "$sess: Running..." fi done # Get full output from completed session zellij --data-dir "$DATA_DIR" pipe --session "agent-1" --pane-id 0 ``` **Tips:** - Use separate git worktrees for parallel fixes (no branch conflicts) - `pnpm install` first before running codex in fresh clones - Check for shell prompt (`❯` or `$`) to detect completion - Codex needs `--yolo` or `--full-auto` for non-interactive fixes ## Cleanup - Kill a session: `zellij --data-dir "$DATA_DIR" delete-session --session "$SESSION"`. - Kill all sessions on a data dir: use `{baseDir}/scripts/cleanup-sessions.sh "$DATA_DIR"`. ## Zellij vs Tmux Quick Reference | Task | tmux | zellij | |------|------|--------| | List sessions | `list-sessions` | `list-sessions` | | Create session | `new-session -d` | `new-session --detach` | | Attach | `attach -t` | `attach --session` | | Send keys | `send-keys` | `action write-chars` | | Capture pane | `capture-pane` | `pipe` | | Kill session | `kill-session` | `delete-session` | | Detach | `Ctrl+b d` | `Ctrl+p d` | ## Helper: wait-for-text.sh `{baseDir}/scripts/wait-for-text.sh` polls a pane for a regex (or fixed string) with a timeout. ```bash {baseDir}/scripts/wait-for-text.sh -s session -p pane-id -r 'pattern' [-F] [-T 20] [-i 0.5] ``` - `-s`/`--session` session name (required) - `-p`/`--pane-id` pane ID (required) - `-r`/`--pattern` regex to match (required); add `-F` for fixed string - `-T` timeout seconds (integer, default 15) - `-i` poll interval seconds (default 0.5) ## Helper: find-panes.sh `{baseDir}/scripts/find-panes.sh` lists panes for a given session. ```bash {baseDir}/scripts/find-panes.sh -s session [-d data-dir] ``` - `-s`/`--session` session name (required) - `-d`/`--data-dir` zellij data dir (uses `CLAWDBOT_ZELLIJ_DATA_DIR` if not specified)

path: scripts/cleanup-sessions.sh
content: #!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<'USAGE'
Usage: cleanup-sessions.sh [-D data-dir]

Delete all zellij sessions in the specified data directory.

Options:
  -D, --data-dir    zellij data directory (uses CLAWDBOT_ZELLIJ_DATA_DIR if not set)
  -y, --yes         skip confirmation prompt
  -h, --help        show this help
USAGE
}

data_dir=""
skip_confirm=false
zellij_data_dir="${CLAWDBOT_ZELLIJ_DATA_DIR:-${TMPDIR:-/tmp}/moltbot-zellij-data}"

while [[ $# -gt 0 ]]; do
  case "$1" in
    -D|--data-dir)  data_dir="${2-}"; shift 2 ;;
    -y|--yes)       skip_confirm=true; shift ;;
    -h|--help)      usage; exit 0 ;;
    *) echo "Unknown option: $1" >&2; usage; exit 1 ;;
  esac
done

if ! command -v zellij >/dev/null 2>&1; then
  echo "zellij not found in PATH" >&2
  exit 1
fi

if ! command -v jq >/dev/null 2>&1; then
  echo "jq is required to parse zellij output" >&2
  exit 1
fi

if [[ -z "$data_dir" ]]; then
  data_dir="$zellij_data_dir"
fi

if [[ ! -d "$data_dir" ]]; then
  echo "Data directory not found: $data_dir" >&2
  exit 1
fi

# Get list of sessions
if ! sessions_json="$(zellij --data-dir "$data_dir" list-sessions 2>/dev/null)"; then
  echo "No zellij sessions found in $data_dir" >&2
  exit 0
fi

session_count=$(printf '%s\n' "$sessions_json" | jq 'length')

if [[ "$session_count" -eq 0 ]]; then
  echo "No sessions to delete"
  exit 0
fi

# Show sessions to be deleted
echo "Sessions in $data_dir:"
printf '%s\n' "$sessions_json" | jq -r '.[] | "  - \(.name) (pid: \(.pid))"'

# Confirm unless -y flag
if [[ "$skip_confirm" != true ]]; then
  echo ""
  read -p "Delete all ${session_count} sessions? [y/N] " -n 1 -r
  echo ""
  if [[ ! $REPLY =~ ^[Yy]$ ]]; then
    echo "Aborted"
    exit 0
  fi
fi

# Delete all sessions
printf '%s\n' "$sessions_json" | jq -r '.[].name' | while read -r session_name; do
  echo "Deleting session: $session_name"
  zellij --data-dir "$data_dir" delete-session --yes --session "$session_name" 2>/dev/null || true
done

echo "Done"


path: scripts/find-panes.sh
content: #!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<'USAGE'
Usage: find-panes.sh -s session [-D data-dir] [-q pattern]

List panes in a zellij session.

Options:
  -s, --session     session name (required)
  -D, --data-dir    zellij data directory (uses CLAWDBOT_ZELLIJ_DATA_DIR if not set)
  -q, --query       case-insensitive substring to filter pane content
  -h, --help        show this help
USAGE
}

session=""
data_dir=""
query=""
zellij_data_dir="${CLAWDBOT_ZELLIJ_DATA_DIR:-${TMPDIR:-/tmp}/moltbot-zellij-data}"

while [[ $# -gt 0 ]]; do
  case "$1" in
    -s|--session)   session="${2-}"; shift 2 ;;
    -D|--data-dir)  data_dir="${2-}"; shift 2 ;;
    -q|--query)     query="${2-}"; shift 2 ;;
    -h|--help)      usage; exit 0 ;;
    *) echo "Unknown option: $1" >&2; usage; exit 1 ;;
  esac
done

if [[ -z "$session" ]]; then
  echo "session is required" >&2
  usage
  exit 1
fi

if ! command -v zellij >/dev/null 2>&1; then
  echo "zellij not found in PATH" >&2
  exit 1
fi

if ! command -v jq >/dev/null 2>&1; then
  echo "jq is required to parse zellij output" >&2
  exit 1
fi

if [[ -z "$data_dir" ]]; then
  data_dir="$zellij_data_dir"
fi

if [[ ! -d "$data_dir" ]]; then
  echo "Data directory not found: $data_dir" >&2
  exit 1
fi

# Get session details
if ! session_info="$(zellij --data-dir "$data_dir" list-sessions)"; then
  echo "Failed to list sessions in $data_dir" >&2
  exit 1
fi

# Find the requested session
session_json=$(printf '%s\n' "$session_info" | jq --arg s "$session" '.[] | select(.name == $s)') || {
  echo "Session not found: $session" >&2
  exit 1
}

# List panes in the session
echo "Panes in session '$session':"
printf '%s\n' "$session_json" | jq -r '.tabs[] |
  "  Tab: \(.name // "unnamed") (id: \(.id))",
  (.panes[] | "    Pane: \(.id) (\(.title // "no title"))")'

# If query is provided, try to find matching pane content
if [[ -n "$query" ]]; then
  echo ""
  echo "Searching for pane content matching: $query"
  printf '%s\n' "$session_json" | jq -r '.tabs[].panes[].id' | while read -r pane_id; do
    if pane_content=$(zellij --data-dir "$data_dir" pipe --session "$session" --pane-id "$pane_id" 2>/dev/null); then
      if grep -qi -- "$query" <<< "$pane_content"; then
        echo "  ✓ Found in pane $pane_id"
      fi
    fi
  done
fi


path: scripts/find-sessions.sh
content: #!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<'USAGE'
Usage: find-sessions.sh [-D data-dir|-A] [-q pattern]

List zellij sessions on a data directory.

Options:
  -D, --data-dir    zellij data directory path
  -A, --all         scan all data directories under CLAWDBOT_ZELLIJ_DATA_DIR
  -q, --query       case-insensitive substring to filter session names
  -h, --help        show this help
USAGE
}

data_dir=""
query=""
scan_all=false
zellij_data_dir="${CLAWDBOT_ZELLIJ_DATA_DIR:-${TMPDIR:-/tmp}/moltbot-zellij-data}"

while [[ $# -gt 0 ]]; do
  case "$1" in
    -D|--data-dir) data_dir="${2-}"; shift 2 ;;
    -A|--all)       scan_all=true; shift ;;
    -q|--query)     query="${2-}"; shift 2 ;;
    -h|--help)      usage; exit 0 ;;
    *) echo "Unknown option: $1" >&2; usage; exit 1 ;;
  esac
done

if [[ "$scan_all" == true && -n "$data_dir" ]]; then
  echo "Cannot combine --all with -D" >&2
  exit 1
fi

if ! command -v zellij >/dev/null 2>&1; then
  echo "zellij not found in PATH" >&2
  exit 1
fi

list_sessions() {
  local dir="$1"
  local label="$2"

  if [[ ! -d "$dir" ]]; then
    echo "Data directory not found: $dir" >&2
    return 1
  fi

  if ! sessions_json="$(zellij --data-dir "$dir" list-sessions 2>/dev/null)"; then
    echo "No zellij sessions found in $label" >&2
    return 1
  fi

  if ! command -v jq >/dev/null 2>&1; then
    echo "jq is required to parse zellij output" >&2
    return 1
  fi

  if [[ -n "$query" ]]; then
    sessions_json="$(printf '%s\n' "$sessions_json" | jq --arg q "$query" '[.[] | select(.name | ascii_downcase | contains($q | ascii_downcase))]' 2>/dev/null || echo '[]')"
  fi

  local count
  count=$(printf '%s\n' "$sessions_json" | jq 'length')

  if [[ "$count" -eq 0 ]]; then
    echo "No sessions found in $label"
    return 0
  fi

  echo "Sessions in $label:"
  printf '%s\n' "$sessions_json" | jq -r '.[] |
    "  - \(.name) (pid: \(.pid), tabs: \(.tabs | length))"'
}

if [[ "$scan_all" == true ]]; then
  if [[ ! -d "$zellij_data_dir" ]]; then
    echo "Zellij data directory not found: $zellij_data_dir" >&2
    exit 1
  fi

  shopt -s nullglob
  data_dirs=("$zellij_data_dir"/*)
  shopt -u nullglob

  if [[ "${#data_dirs[@]}" -eq 0 ]]; then
    echo "No data directories found under $zellij_data_dir" >&2
    exit 1
  fi

  exit_code=0
  for d in "${data_dirs[@]}"; do
    if [[ ! -d "$d" ]]; then
      continue
    fi
    list_sessions "$d" "data directory '$d'" || exit_code=$?
    echo ""
  done
  exit "$exit_code"
fi

# Use provided data dir or default
if [[ -z "$data_dir" ]]; then
  data_dir="$zellij_data_dir"
fi

list_sessions "$data_dir" "data directory '$data_dir'"


path: scripts/wait-for-text.sh
content: #!/usr/bin/env bash
set -euo pipefail

usage() {
  cat <<'USAGE'
Usage: wait-for-text.sh -s session -p pane-id -r pattern [options]

Poll a zellij pane for text and exit when found.

Options:
  -s, --session    zellij session name (required)
  -p, --pane-id    pane ID (required)
  -r, --pattern    regex pattern to look for (required)
  -F, --fixed      treat pattern as a fixed string (grep -F)
  -T, --timeout    seconds to wait (integer, default: 15)
  -i, --interval   poll interval in seconds (default: 0.5)
  -D, --data-dir   zellij data dir (uses CLAWDBOT_ZELLIJ_DATA_DIR if not set)
  -h, --help       show this help
USAGE
}

session=""
pane_id=""
pattern=""
grep_flag="-E"
timeout=15
interval=0.5
data_dir=""
zellij_data_dir="${CLAWDBOT_ZELLIJ_DATA_DIR:-${TMPDIR:-/tmp}/moltbot-zellij-data}"

while [[ $# -gt 0 ]]; do
  case "$1" in
    -s|--session)   session="${2-}"; shift 2 ;;
    -p|--pane-id)   pane_id="${2-}"; shift 2 ;;
    -r|--pattern)   pattern="${2-}"; shift 2 ;;
    -F|--fixed)     grep_flag="-F"; shift ;;
    -T|--timeout)   timeout="${2-}"; shift 2 ;;
    -i|--interval)  interval="${2-}"; shift 2 ;;
    -D|--data-dir)  data_dir="${2-}"; shift 2 ;;
    -h|--help)      usage; exit 0 ;;
    *) echo "Unknown option: $1" >&2; usage; exit 1 ;;
  esac
done

if [[ -z "$session" || -z "$pane_id" || -z "$pattern" ]]; then
  echo "session, pane-id, and pattern are required" >&2
  usage
  exit 1
fi

if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then
  echo "timeout must be an integer number of seconds" >&2
  exit 1
fi

if ! command -v zellij >/dev/null 2>&1; then
  echo "zellij not found in PATH" >&2
  exit 1
fi

if [[ -z "$data_dir" ]]; then
  data_dir="$zellij_data_dir"
fi

if [[ ! -d "$data_dir" ]]; then
  echo "Data directory not found: $data_dir" >&2
  exit 1
fi

# End time in epoch seconds (integer, good enough for polling)
start_epoch=$(date +%s)
deadline=$((start_epoch + timeout))

while true; do
  # Read pane content
  if ! pane_text="$(zellij --data-dir "$data_dir" pipe --session "$session" --pane-id "$pane_id" 2>/dev/null)"; then
    pane_text=""
  fi

  if printf '%s\n' "$pane_text" | grep $grep_flag -- "$pattern" >/dev/null 2>&1; then
    exit 0
  fi

  now=$(date +%s)
  if (( now >= deadline )); then
    echo "Timed out after ${timeout}s waiting for pattern: $pattern" >&2
    echo "Content from pane $pane_id in session $session:" >&2
    printf '%s\n' "$pane_text" >&2
    exit 1
  fi

  sleep "$interval"
done


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)