CLI Lane runs interactive CLI agents — Codex, Claude Code, Kimi, or any other long-running command — in the background, each in its own lane with an identity you can still trust days later. Reconnect through SSH, Mosh, or Termius to inspect, attach, send input, press keys, stop, or remove a task, or read task state across a fleet of hosts.
Plenty of tools keep a process alive; CLI Lane exists for what happens at the
edges. A task keeps its identity even after its pid is reused, so stop and
rm can never touch an innocent process. Logs survive the tmux server dying
and come back as recoverable orphans, not silent losses. send refuses input
an agent would receive as garbage bytes and key presses real keys instead.
Exit statuses distinguish signals from exit codes, and --on-exit reports a
finished task instead of making you poll. Every destructive path re-verifies
what it is about to touch before touching it.
CLI Lane is process-agnostic. It manages commands that you launch through
clilane; it does not depend on an agent framework or discover unrelated
processes.
Laptop / Termius / another node
│
SSH or Mosh
│
▼
host running clilane
│
isolated tmux server
├── codex-project
├── kimi-research
└── development-server
- macOS or Linux
- Python 3.10 or newer
- tmux 3.6 or newer
tailfor persistent log reading- OpenSSH and key-based host access for
clilane fleet
CLI Lane uses only the Python standard library. The lifecycle, fleet, and
interactive switcher paths are verified on macOS and Ubuntu in CI, both on
tmux 3.7. tmux 3.6 is required because the 3.4 and 3.5 servers can crash while
the switcher opens and closes its popup, which ends every lane on that server
at once; Ubuntu 24.04's packaged tmux is 3.4, so build a newer release or use
Homebrew there. run refuses to start on anything older than 3.6. Releases
older than 3.3 also do not report the signal that ended a task, which would
silently corrupt exit statuses.
brew install minglong51/tap/clilane
clilane --versiongit clone https://github.com/minglong51/clilane.git
cd clilane
mkdir -p "$HOME/.local/bin"
install -m 0755 bin/clilane "$HOME/.local/bin/clilane"
export PATH="$HOME/.local/bin:$PATH"
clilane --versionPersist the PATH change in your shell configuration if ~/.local/bin is not
already available in new shells.
Start a command. Everything after -- is passed as exact arguments without an
extra shell:
clilane run demo -- python3 -u -c 'import time; print("hello"); time.sleep(300)'You can immediately see and inspect it:
clilane ps
clilane status demo
clilane read demoAttach to its terminal when you need full interaction. Press Ctrl-B once to
return to Sessions. Select Detach and press Enter to leave with jobs
running:
clilane attach demoStop and remove it when finished:
clilane stop demo
clilane rm demoAgent CLIs use the same lifecycle:
clilane run codex-app -C ~/projects/sample-app -- codex
clilane run kimi-research -C ~/projects/research -- kimi
clilane run claude-api -C ~/projects/api -- claudeThe named command must already be installed on that host.
Profiles make an agent's launch environment explicit. Create
$XDG_CONFIG_HOME/clilane/profiles.json, or
~/.config/clilane/profiles.json when XDG_CONFIG_HOME is unset:
{
"schema_version": 1,
"profiles": {
"claude-clean": {
"provider": "claude",
"command": ["claude"],
"cwd": { "mode": "explicit" },
"env": {
"inherit": ["ANTHROPIC_API_KEY"],
"set": { "PATH": "/opt/homebrew/bin:/usr/bin:/bin" }
}
}
}
}Launch it with clilane run NAME --profile PROFILE [-C DIR]. The profile owns
the complete argument vector and cannot be combined with raw -- COMMAND.
cwd.mode: explicit requires -C; cwd.mode: fixed requires an absolute
path and forbids -C. Profile and provider names are case-sensitive and use
the same 1–64-character safe-name grammar as task names.
The child environment starts with the present values of HOME, PATH, USER,
LOGNAME, SHELL, TMPDIR, LANG, LC_ALL, and LC_CTYPE. CLI Lane then
copies the keys named by env.inherit and applies env.set, which takes
precedence. A missing inherited key fails the launch. TERM, TMUX,
TMUX_PANE, CLILANE_*, and AGT_* are reserved. Keys ending in _TOKEN,
_KEY, _SECRET, or _PASSWORD, case-insensitively, cannot appear in
env.set; inherit those values instead.
The strict schema-v1 file is limited to 256 KiB. It must be a regular, non-symlink file owned by the current user and not writable by group or others. The fully materialized launch must also fit CLI Lane's portable 64 KiB launch budget. Profiles provide environment isolation, not a security sandbox. The command still runs with your Unix user's permissions.
clilane fleet reads a fixed set of hosts concurrently; --watch keeps the
table refreshing. clilane fleet status HOST:TASK and
clilane fleet log HOST:TASK read one task on one host. The fleet surface is
deliberately read-only: it cannot start, attach, send to, stop, or remove a
remote task.
Install CLI Lane on every host. The fleet commands consume the remote side's
existing list --json, status --json, and log output, so hosts on older
releases keep answering newer clients.
Configure SSH aliases, keys, users, ports, and jump hosts in ~/.ssh/config.
Connect to each alias once so strict host-key checking succeeds:
ssh agent-host trueOn the machine where you run fleet, mark that machine as local and add any
hosts it can reach as ssh. The names are display labels, and each destination
is an SSH alias. CLI Lane has no built-in knowledge of your machines, roles, or
network topology.
Create ~/.config/clilane/fleet.json:
{
"schema_version": 1,
"hosts": [
{
"name": "workstation",
"transport": "local"
},
{
"name": "agent-host",
"transport": "ssh",
"destination": "agent-host"
}
]
}The config must contain exactly one local entry. Each ssh destination must be
a plain SSH-config alias. Host names and destinations must begin with an ASCII
letter or number; their remaining characters may be letters, numbers, dots,
dashes, or underscores, up to 64 characters total. A config may contain at most
32 hosts; unknown fields and schema versions are rejected. CLI Lane never
enumerates your SSH config or probes hosts that are not listed.
Run the fleet snapshot:
clilane fleetHOST NAME STATE AGE PID PROJECT COMMAND
workstation kimi-research running 4m 8124 research kimi
agent-host codex-project running 15h 21686 sample-app codex
Override the config or per-host timeout when needed:
clilane fleet --config ~/fleet-lab.json --timeout 10
clilane fleet --jsonAn offline or invalid host does not hide healthy results. Human output reports
that host on stderr; JSON embeds the error. fleet exits 1 whenever the
snapshot is incomplete and 0 only when every configured host succeeds.
Queries use at most eight concurrent SSH connections. The default per-host SSH
transport timeout is 5 seconds and accepted values are greater than 0 and at
most 300. Each host may return at most 512 tasks and 256 KiB; with the 32-host
config limit, successful raw fleet data is capped at 8 MiB.
Fleet JSON has this top-level shape:
{
"schema_version": 1,
"complete": false,
"generated_at": "2026-08-19T12:00:00+00:00",
"hosts": [
{
"name": "workstation",
"status": "ok",
"clilane_version": "0.4.0",
"tasks": [],
"error": null
},
{
"name": "agent-host",
"status": "error",
"clilane_version": null,
"tasks": [],
"error": {
"kind": "timeout",
"message": "timed out after 5 seconds"
}
}
]
}Fleet scope is explicit rather than inferred: “complete” means every host in this config answered successfully. It does not mean every computer or process on your network was discovered.
Run clilane with no subcommand to open the native session switcher. clilane hub
is an explicit alias for the same view.
clilanePress Ctrl-B once to open Sessions, use Up/Down to choose, and press Enter to open. Esc returns without clearing your draft. Help and Leave CliLane · jobs keep running are choices in the same list; you can select or click them. The top bar’s Back to sessions button opens the same list. Use the Control key on both Mac and PC keyboards. Everyday navigation needs no Command, Option, Fn, or Page keys. Windows Terminal clients can use WSL or SSH to a machine running CLI Lane; CLI Lane itself requires a Unix environment and tmux.
Upgrading from 0.18: Ctrl-B opens Sessions immediately; the extra w and
other prefix letters are no longer needed. Choose the next job, Help, or Leave CliLane
in the list. Ctrl-Q reaches the agent's native command, including
OMP's follow-up action. Modified Tab/Page keys
also stay with the terminal or agent. In current OMP, Ctrl-D exits when the editor
is empty; with a draft it deletes forward. Ctrl-C leaves first-run setup and
enters chat. Leaving CliLane keeps OMP running.
| Key | Behavior |
|---|---|
Ctrl-B |
Open Sessions immediately from an agent. |
Select Help, then Enter |
Open scrollable keyboard help. F1 also opens help in Sessions. |
Select Leave CliLane, then Enter |
Leave; jobs keep running. Drafts require confirmation before discarding. |
Tab / Shift-Tab |
Choose the next / previous agent and select New session. |
Up / Down |
Select a running or finished job, New session, Help, or Leave CliLane. |
Enter |
Open the selected job, including a finished pane. On New session, open the chosen agent; a typed first message starts and submits it for Codex, Kimi, Claude, OMP, or supported Kiro chat commands. In a live preview, send the typed reply. |
Space |
With an empty composer, preview the selected job. Type in a live preview to reply to that job. Space again closes an empty preview. |
Ctrl-R |
Explicitly resume or rerun the selected finished job using the action shown. See resume behavior below. The previous log becomes the new job's .previous log until it rotates. |
Ctrl-X |
Ask to stop a running job or remove a finished one. Enter confirms; Esc cancels. The confirmation has no timer. |
Esc / Ctrl-C in Sessions |
Close help or a preview, otherwise return to the source job or leave Sessions. Drafts are preserved until you leave CliLane; leaving asks before discarding them. Replies stay with their own job when selection changes. |
Ctrl-Q in Sessions |
Leave CliLane, keeping jobs running. If drafts exist, Enter confirms discarding them and Esc cancels. |
Left / Right |
Move the composer cursor. With an empty composer, Right opens the selected pane or New session. |
Ctrl-A / Ctrl-E, Home / End |
Move to the beginning / end of the composer. |
Ctrl-Left / Ctrl-Right, Option-Left / Option-Right |
Move by word when the terminal forwards these keys. Option-b/f also work when Option sends Meta. |
Ctrl-W / Option-Delete |
Erase the previous word. |
Delete (PC) / Fn-Delete (Mac) |
Delete the next character. |
Ctrl-D in Sessions |
Delete the next character; with an empty composer, request to leave CliLane. |
Ctrl-K, Ctrl-U, Ctrl-Y |
Erase to the end, clear the draft, or paste the last cut text. |
Left / Right in an agent |
At a recognized empty Claude, Codex, Kimi, OMP, or Kiro prompt, switch running sessions. Drafts, native menus, and copy mode retain ordinary arrows. If a prompt layout is not recognized, use Ctrl-B or click Back to sessions. |
Drafts are temporary and belong to the attached client. Reopening Sessions restores
them; leaving CliLane discards them after confirmation. Once Sessions is visible,
pressing Ctrl-B again returns and sends a literal Control-B to the source agent. Use Esc to return
without sending a key.
Inside an outer tmux with its default prefix, press Ctrl-B twice to
reach CLI Lane's Sessions; the first prefix is handled by the outer tmux.
OMP and Kiro arrow navigation is enabled only for recognized empty-editor layouts. It was checked with OMP 18.16.0 and Kiro 3.1.0’s V2 and V3 interfaces on macOS. Kiro checks used its built-in mock responses; they do not verify account sign-in or production model calls. A changed layout or theme keeps arrows inside the agent.
Kiro’s first message waits for a recognized empty chat editor, then pastes and submits once. Setup and sign-in screens keep the draft unsent. Standard chat commands and supported model/agent options accept this flow; resume, headless, pre-filled, and unrecognized command variants keep first-message entry inside Kiro.
Tracked Claude jobs resume the conversation recorded at a clean native exit, using the original profile and launch environment. If that exit record is missing or the launch identity changed, CLI Lane refuses automatic resume; use Claude's native session picker with the original profile instead.
Other agent jobs preserve an explicit resume selection when present. Otherwise,
Claude and Kimi use --continue, and Codex uses resume --last; these select the
provider's latest conversation, which may differ from the job you selected.
Automatic final-conversation tracking for Codex and Kimi is not available. These
jobs relaunch through a matching configured preset, including its clean profile,
or through the raw environment when no preset matches. Unsupported fork commands
are refused; non-agent jobs rerun their saved command.
The list is split into ONGOING jobs and HISTORY, newest first. History shows
the latest 20 finished jobs; moving the selection past the last one reveals the
next 20. Set CLILANE_HUB_HISTORY to change the batch size. Nothing is removed
by hiding it: list, log, and rm see every job, and Ctrl-X removes one.
The dashboard identifies jobs started from a terminal as LOCAL and jobs started
by OpenClaw or another automation lane as BOT. All of them are ordinary CLI Lane
tasks on the current host and tmux socket. CLI Lane does not import unrelated
processes or historical conversations from an agent's own session store.
Selecting New session and pressing Enter with an empty composer opens any
configured agent. Codex, Kimi, Claude, OMP, and supported Kiro chat commands also
accept a first message in the switcher; typing selects New session, and Enter starts the agent and delivers
the message. For the other native-launch tools, type inside the agent.
The message is not stored in the command arguments or task metadata. It can still
appear in the terminal log, just as if you had typed it directly into the agent.
When the optional ~/.config/clilane/hub.json file is absent (or absent under
$XDG_CONFIG_HOME), CLI Lane discovers installed Codex, Kimi, Claude, OMP, Kiro CLI,
Gemini, OpenCode, Copilot, Aider, Amp, Goose, Pi, Cursor Agent, Droid, Qwen Code,
and Hermes commands from the shell PATH captured when the switcher opens. New
sessions start in the directory where you ran clilane; returning from a managed
job uses that job's directory instead.
A present config file is authoritative, including an intentionally empty agents list. Use it to override commands, directories, ordering among same-kind presets, or add other agents. Codex, Kimi, and Claude presets are shown first; other configured presets remain available.
{
"schema_version": 2,
"agents": [
{ "name": "codex", "command": ["codex"], "dir": "~/projects/app" },
{ "name": "claude", "profile": "claude-clean", "dir": "~/projects/app" },
{ "name": "kimi", "profile": "kimi-fixed" }
]
}Each schema-v2 preset contains exactly one of command or profile. A command
preset is a raw launch using the filtered environment captured when the switcher
opens. An explicit-cwd profile requires dir; a fixed-cwd profile forbids it.
dir must be absolute after ~ expansion. Only profile-backed v2 presets use a
clean environment. Discovered presets and schema-v1 configs remain accepted with
their legacy raw environment behavior.
CLI Lane rereads schema-v2 presets and profiles immediately before launch. If the
selected preset, profile, or resolved directory changed while the menu was open,
reopen the switcher. Every preset still starts an ordinary task, so list, log,
stop, and the fleet commands all see it.
The switcher runs on CLI Lane's own tmux server, so it nests safely inside your normal tmux: your prefix, configuration, and other panes are untouched, and both survive a dropped connection independently.
Tasks live on the host, so disconnecting SSH, Mosh, or Termius does not stop them while that host and CLI Lane's dedicated tmux server remain alive.
From an interactive remote login:
ssh agent-host
clilane ps
clilane attach codex-projectFor a one-shot command whose non-login PATH does not include CLI Lane or Homebrew, invoke a login shell:
ssh agent-host 'zsh -lc "clilane ps"'attach removes inherited outer-tmux variables before connecting to CLI Lane's
isolated server. CLI Lane starts that server with -f /dev/null, so it does not
load or alter your normal tmux configuration. The default internal socket remains
named agt for compatibility with tasks created before the project was renamed.
| Command | Behavior |
|---|---|
run NAME [-C DIR] [--on-exit COMMAND] [--wait] -- COMMAND [ARG ...] |
Start a raw background task. DIR defaults to the current directory. --wait blocks and returns the command's exit status; --timeout returns 124. |
run NAME --profile PROFILE [-C DIR] [--on-exit COMMAND] [--wait] |
Start a task from a clean launch profile. The profile's cwd rule determines whether -C is required or forbidden. |
list, ls, ps |
List tasks on the current host. --json emits an array. |
fleet |
Read the configured local and SSH hosts. Supports --config, --timeout, --json, and --watch (--interval defaults to 2 seconds). |
fleet status HOST:TASK |
Show one task on a fleet host as JSON. |
fleet log HOST:TASK |
Read a fleet task's persistent log. --lines defaults to 200. |
status TARGET |
Show one task by name or unique ID prefix. Supports --json. |
attach TARGET |
Attach interactively. Ctrl-B returns to Sessions. |
hub |
Open the same switcher as bare clilane. --config overrides the presets file. |
read TARGET |
Print the last 200 terminal lines. --all prints retained scrollback. |
log TARGET |
Read persistent output. --lines defaults to 200; --follow follows rotation. |
send TARGET MESSAGE |
Paste text and press Enter. --no-enter only pastes. Control characters are rejected; use key. |
key TARGET KEY [KEY ...] |
Press named keys, such as Enter, Escape, Up, Tab, BTab, or C-c. --list prints supported names. |
wait TARGET |
Wait and return the task's exit status. --timeout returns 124 on timeout. |
stop TARGET |
Send TERM, then KILL after --grace seconds. The default grace is 3 seconds. |
rm, remove TARGET |
Remove a finished task and its logs. --force stops a running task first. |
clean |
Remove completed, stopped, incomplete, and lost tasks. Running tasks and orphaned logs remain. --dry-run reports only; --orphans also deletes orphaned logs. |
orphans |
List tasks whose tmux server died but whose logs survive. Supports --json. |
watch |
Refresh the current-host task list. --interval defaults to 1 second. |
The --on-exit hook runs through /bin/sh -c in the task's working directory
after the task process exits, with CLILANE_TASK_NAME, CLILANE_TASK_ID,
CLILANE_LOG, CLILANE_EXIT_CODE, and CLILANE_SIGNAL (the signal name, or
empty) in its environment. Hook output is captured in the task log. The hook
gets 30 seconds, then it is killed; its exit status never changes the task's.
With --wait, the wait returns after the hook finishes. Stopping or removing
the task while the hook runs delivers TERM to the hook first and kills it after
the grace period; the task's recorded exit status is unaffected either way. A
profile launch gives the hook the same selected environment plus the exit-result
keys.
Task names are unique per host. They are 1–64 characters, begin with an ASCII letter or number, and otherwise contain ASCII letters, numbers, dots, dashes, or underscores.
CLI Lane reports process lifecycle rather than semantic agent state:
initializingrunningstoppedsucceededfailed
It cannot determine whether an agent is waiting for input, blocked on approval,
or idle at a prompt. Use read, attach, or send to inspect that condition.
list --json returns an array and status --json returns one object. A task has:
id, name, state, command, cwd, created, log,
pid, attached, exit_code, signal
pid is null after exit. exit_code and signal are null while a task is
running.
| Setting | Purpose |
|---|---|
~/.config/clilane/fleet.json |
Default versioned fleet inventory. |
~/.config/clilane/hub.json |
Optional switcher overrides, up to nine agents; absent files enable agent discovery. |
~/.config/clilane/profiles.json |
Strict clean-launch profile definitions. |
XDG_CONFIG_HOME |
Moves the default fleet, hub, and profile configs under $XDG_CONFIG_HOME/clilane/. |
CLILANE_TMUX_SOCKET |
Overrides the dedicated local tmux socket. |
CLILANE_STATE_HOME |
Overrides the local state root; it must be absolute. |
XDG_STATE_HOME |
Uses $XDG_STATE_HOME/clilane for state. |
AGT_TMUX_SOCKET, AGT_STATE_HOME |
Legacy compatibility settings. |
The default state root is ~/.local/state/clilane. Each task also writes a small
JSON record under the state root's registry/ directory so its log can be
identified and reclaimed after the tmux server or the machine restarts. Older
logs under ~/.local/state/agt remain readable for compatibility.
- Commands run as exact argument vectors through
execvpe; CLI Lane does not insert a shell. Runsh -lcexplicitly when you want pipelines or redirection. - Profiles provide environment isolation, not a security sandbox. Every task still runs with your Unix user's permissions.
- Raw, discovered, and hub-v1 launches receive a filtered snapshot of the launching environment. Profile launches receive only the baseline keys, explicitly inherited keys, literal assignments, and CLI Lane/tmux runtime keys. Inherited values are not written to task records or the hub selection digest, but the child can expose them through terminal output and logs.
- Logs contain raw terminal bytes, ANSI controls, and potentially sensitive program output.
- Local and fleet JSON include full command arguments, working directories, and log paths. Avoid putting secrets in command arguments.
- Control and log directories use mode
0700; current logs use mode0600. - Each task retains a 50 MiB current log and one previous rotated log.
stopretains logs.rmandcleandelete the current and previous logs.- After a tmux server crash or reboot, logs survive and are listed by
orphans; plaincleanpreserves them andclean --orphansdeletes them. - Fleet SSH runs without a PTY, stdin, agent forwarding, X11, or forwarding. It uses batch mode, strict known-host checking, one connection attempt, a transport timeout, a remote command limited to fixed read verbs with a pattern-validated task argument, and a 256 KiB per-host response limit.
- Fleet config cannot inject SSH flags or remote commands. Connection details
belong in your normal SSH config. Treat that config as trusted: connection
plumbing such as
ProxyCommand,KnownHostsCommand, andMatch execcan run local programs before CLI Lane connects.
clilane: command not found: add its install directory toPATH. For a one-shot SSH command, invoke a login shell as shown above.clilane: tmux is required: install tmux and ensure Homebrew's bin directory is available in that shell.fleet config not found: create the default file or pass--config FILE.- SSH host-key failure: run
ssh ALIAS trueonce and verify the key before retrying the fleet query. - A remote reports that
clilaneis missing: install it there and confirmssh ALIAS 'zsh -lc "clilane --version"'succeeds. - A task is missing from
ps: confirm it was started by the same Unix user with the sameCLILANE_TMUX_SOCKETvalue. cannot find terminfo for xterm-ghostty(or another uncommonTERM): this host has no terminfo entry for your terminal, soattachand the switcher usexterm-256colorfor that session and say so. To keep the real terminal, install the entry on this host, for exampleinfocmp -x xterm-ghostty | ssh HOST tic -x -from the machine that has it.
- Only tasks started through CLI Lane are visible.
- Each host exposes one Unix user's selected CLI Lane tmux socket.
- No reboot or CLI Lane tmux-server-crash adoption of running tasks; their exit
status is lost, though their logs survive and are listed by
orphans. - A descendant that creates a new POSIX session can escape lifecycle control.
- No queueing, by design: a task starts immediately or not at all. Sequential
workflows compose from
run --waitand--on-exit; anything beyond that belongs in a real scheduler running above CLI Lane, not inside it. - No cross-host mutation, retries, dependency graphs, notifications beyond
--on-exit, web UI, or automatic SSH-host discovery. - Fleet snapshots are point-in-time; a task can change immediately afterward.
- Fleet ages assume host clocks are reasonably synchronized.
- Raw terminal logs are not structured or redacted.
- Native Windows is unsupported; use WSL or SSH to a Unix host.
