███╗ ███╗ ██╗ ██╗ ██╗ ██╗ ████████╗ ███████╗ ██████╗ ███╗ ███╗
████╗ ████║ ██║ ██║ ╚██╗██╔╝ ╚══██╔══╝ ██╔════╝ ██╔══██╗ ████╗ ████║
██╔████╔██║ ██║ ██║ ╚███╔╝ ██║ █████╗ ██████╔╝ ██╔████╔██║
██║╚██╔╝██║ ██║ ██║ ██╔██╗ ██║ ██╔══╝ ██╔══██╗ ██║╚██╔╝██║
██║ ╚═╝ ██║ ╚██████╔╝ ██╔╝ ██╗ ██║ ███████╗ ██║ ██║ ██║ ╚═╝ ██║
╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝
An iTerm-style terminal emulator for macOS, written in Rust — with one twist: every pane is backed by its own tmux session. Quit the app and your shells, running processes, and scrollback stay alive; relaunch and the exact tab/split layout reattaches to them. Closing a pane (cmd+w, or the shell exiting) is what kills its session.
Because each pane is a real tmux session, panes are also inspectable and scriptable from outside — which is what powers the built-in agent mesh for running teams of AI agents.
muxterm is an opinionated terminal. It ships a point of view as defaults rather than exposing every choice as configuration:
- Your muscle memory already works. Keybindings are iTerm's — cmd+t, cmd+d, cmd+1…9 — with no prefix chords or modes to learn. tmux is the engine under every pane, never something you drive by hand.
- Nothing is ever lost. Every pane outlives the app: quit, crash, or relaunch, and your shells, processes, layout, and scrollback are still there. The end state of this opinion is forever memory — terminal output as a permanent, searchable log instead of a ring buffer (today: 100k lines of tmux history per pane; on the roadmap: a durable on-disk archive).
- AI is a first-class tenant, not a bolt-on. It cuts both ways. Coding
agents run in panes as peers: tabs are teams, panes are teammates, and
the
muxCLI gives them shared context — they read each other's terminals, message each other, and share a scratchpad (see agent mesh). And AI is one keystroke from any shell: type?at an idle prompt and the line becomes a compose box — enter runs your question through Claude Code, Codex, Pi, or OpenCode right in the pane, with your recent scrollback piped in as context, so? why did this build failjust works. - Themed, not themeable-to-death. A curated set of themes plus a small
override surface (
config.toml, edits apply live). Tab bar, dividers, and borders derive from the palette, so the whole window always matches.
cargo run --releaseRequires tmux 3.6 or newer (brew install tmux).
make install # builds muxterm.app and ships it to /Applicationsmake app assembles an ad-hoc-signed target/release/muxterm.app (plist
template and icon live in packaging/; regenerate the icon with
make icon); make install copies it to /Applications and refreshes
~/.cargo/bin/mux so the CLI in your panes matches the app. Quit muxterm
first if it's running — sessions survive, the relaunch restores them.
Launched from Finder/Dock everything works shell-less: TERM is set at
startup, tmux is probed at fixed paths, and agent binaries are resolved
through your login shell.
Use Browse... in New Workspace (cmd+n), Settings > Projects, or the automation form to choose a folder with the native file picker. Selecting a folder fills the path; cancelling keeps the current value.
| Chord | Action |
|---|---|
| cmd+t | new tab |
| cmd+n | new workspace: saved-project picker, or folder form if no projects are saved |
| cmd+shift+n | saved-project picker; opens Projects settings if none are saved |
| cmd+w | close focused pane (kills its tmux session; closes tab when last pane) |
| cmd+d / cmd+shift+d | split side-by-side / stacked |
| cmd+shift+[ / cmd+shift+] | previous / next tab |
| cmd+1 … cmd+9 | jump to tab |
| cmd+opt+arrows | move focus between panes directionally¹ |
| cmd+[ / cmd+] | cycle focus through panes |
| cmd+c / cmd+v | copy / paste |
| mouse select | copies to the clipboard automatically (copy_on_select, toggle in settings) |
| cmd+click | open the URL or file path under the pointer (hold cmd to see links underlined; relative paths resolve against the pane's cwd and open only if they exist) |
| shift+PageUp or mouse wheel | tmux copy-mode scrollback |
| cmd+f | search scrollback (tmux history; enter/⇧enter or cmd+g/cmd+shift+g walk matches, esc closes the bar and leaves the view at the match) |
| cmd+p | switch workspace (type to filter by title, description, branch, #pr; ↑↓ move, enter jumps, esc closes) |
| cmd+, | settings (esc closes) |
? (at an empty shell prompt) |
ask the AI agent — enter runs it in the pane, esc cancels |
¹ Window managers like Rectangle bind cmd+opt+arrows globally; free those hotkeys or use cmd+[ / cmd+].
Open Settings → Projects → Import from Conductor to preview local Conductor repositories. Expand each project's details, select the projects, and click Import selected projects. Existing names or repository paths are skipped. Importing saves configuration only; it does not run scripts or move Conductor workspaces.
Projects support an optional base branch (for example origin/prod) and file
patterns to copy into new worktrees before setup. New branches start at that
base; selecting an existing branch preserves its history. An explicit remote
base is fetched without changing the source checkout's branch.
Enter one copy pattern per line: .env* matches at any depth,
apps/web/.env* is relative to the repository root, and !.env.example
excludes matches. A trailing / includes a directory's contents. Exclusions
always win. Files, including ignored files, are copied from the source checkout;
matching tracked files in the destination are replaced. Symlinks, paths outside
the repository, and Git metadata are rejected. A copy failure stops setup.
The importer reads Conductor's local database using sqlite3 in read-only mode,
then merges global settings, legacy repository settings, shared repository TOML,
and local repository TOML in that order. Archive scripts, run scripts, prompts,
environment settings, and agent preferences are not migrated. Review the preview
warnings; absent copy patterns default to .env*. Saved projects can be edited
before creating their first workspace with Cmd+N.
Type ? as the first character at an idle shell prompt and the pane
switches to an accent-colored compose line. Enter types mux ask '…' into
the pane like any command; mux ask runs your question as a one-shot
Claude Code (default), Codex, Pi, or OpenCode query — pick the agent in
settings (cmd+,) — and streams the answer as it's generated, with any
tool calls the agent makes shown as dim » one-liners. The last
agent_context_lines of the pane's scrollback are captured to a temp file
and redirected to the agent's stdin, so it sees what you were just doing.
Questions default to each agent's fast model (haiku for Claude/Pi,
gpt-5.6-terra for Codex); OpenCode uses the default model in your OpenCode
configuration. Set agent_model in config.toml to choose another model.
mux ask also works from any plain terminal.
Every tab whose focused pane sits in a git checkout shows its branch next
to the title (on by default; git_status in settings or config.toml turns
it off): a state dot — green when the tree is clean, yellow when dirty —
the branch name, then compact *N (changed + untracked files), ↑N
(commits ahead of upstream) and ↓N (behind) markers. Hover for the
breakdown. Split tabs get the same chip per pane, so panes in different
worktrees or branches read at a glance. It's all local git status, no
network, scanned every few seconds; a detached HEAD shows the short commit.
Every tab whose focused pane sits in a git checkout shows that branch's
GitHub PR next to the tab title (on by default; pr_status in settings or
config.toml turns it off): a status dot — green (checks passing / approved), yellow (running),
red (failing or changes requested), magenta (merged) — plus the PR number.
Hover for the rollup, click to open the PR page; split tabs get the same
chip per pane next to the pane-title badges. Needs an authenticated
gh. Local state (pane cwds, branches) is scanned
every few seconds; GitHub itself is asked at most once per minute per
(repo, branch), no matter how many panes share a checkout.
~/Library/Application Support/muxterm/config.toml (created on first run,
edits apply live while the app is running):
theme = "iterm-dark" # bbs | iterm-light | github-light — each theme is a
# full look: palette + font + pane-border weight
# (Monaco for the iterms, IBM VGA 8x16 for bbs,
# SF Mono for github-light)
agent = "claude" # "?" prompt: claude | codex | pi | opencode
agent_model = "" # --model for "?" answers; empty = the agent's fast
# or configured default
agent_context_lines = 200 # pane scrollback sent as context (0 = none)
dim_inactive_panes = 0.12 # unfocused-split fade toward bg (0.0 - 0.8)
pane_titles = true # per-pane title badge (top-right) on split tabs;
# also a checkbox in settings (cmd+,)
copy_on_select = true # mouse selections copy to the clipboard as
# they finish; off = select, then cmd+c
pr_status = true # the branch's GitHub PR beside the tab title
# (status dot + number, click opens); needs gh
bar_font = "Menlo" # the status line (pane title + chips + search
bar_font_size = 12.0 # hints); defaults to the theme's own font at a
# size the theme picked, independent of [font]
bar_cwd = true # the pane's folder as a chip beside the title
# (click opens it in Finder); on for iterm-light
[font] # overrides the theme's font for every theme
family = "Menlo" # font name in the macOS font folders, or a file path
size = 14.0 # delete both lines to get each theme's own font
[colors] # override any color of the chosen theme
background = "#1d1e23"
accent = "#4a90d9" # focused-pane border + active-tab underline
bright_green = "#5ffa68"Tab bar, dividers, and pane-border chrome are derived from the palette
automatically, so a theme change restyles the whole window — font
included. The bbs theme's IBM VGA font is Px437 from the
Ultimate Oldschool PC Font Pack
by VileR (CC BY-SA 4.0; see assets/fonts/LICENSE.txt).
Run AI agents (claude, aider, any interactive CLI) in panes and let them
coordinate through the bundled mux CLI — scoped per tab: the panes of
one tab form a team; agents cannot see or message other tabs. Because every
pane is a tmux session, agents can read each other's terminals with zero
cooperation from the program running there (works for top, vim, anything).
mux join reviewer --role code-review # register this pane (tab relabels)
mux run writer --role impl -- claude # join + brief + launch + auto-leave
mux peers # who's on the team
mux read writer -n 300 # snapshot a teammate's terminal
mux post writer "left comments in review.md" # inbox + one [mux] nudge
mux inbox --consume # read messages sent to you
mux tell writer "run the tests again" # type into their terminal directly
mux ctx set build.status green # shared per-tab scratchpad
mux split --run 'mux run w2 -- claude' # grow the team: split your own pane
mux new-tab --cwd ~/dev/api --run 'npm run dev' # open a new background tab
mux brief # paste-ready team briefing for a promptTypical setup: split a tab, then launch each agent with
mux run <name> --role <role> -- <agent-command> — it registers the pane,
prints the team briefing so it lands in the agent's first screenful, execs
the command, and deregisters when it exits. (Or register manually with
mux join and paste mux brief output into the agent's prompt.) Agents
shell out to mux like any other command.
Agents can also grow the team themselves: mux split [right|down] [--run <command>] asks the GUI to split the calling pane and prints the
new pane's session name — an orchestrator can fan work out to parallel
subagents (mux split --run 'mux run helper -- claude …') and coordinate
them with read/post/tell. Splits stay GUI-owned (a session created
behind the app's back would never render and would be GC'd), so the CLI
spools a request that the app's poll loop applies: the new pane lands next
to the requester without stealing focus, capped at 8 panes per tab.
mux new-tab [--cwd <dir>] [--run <command>] [--title <t>] uses the same
handshake to open a whole new tab instead of a split — a bare shell that
starts in --cwd and runs --run, landing in the background (never stealing
focus) and printing its session name. It's meant for spinning up an
independent shell or long-running command (a dev server, a build), so the
brief tells agents to use it only when the user explicitly asks for a new
tab or window — never on their own initiative.
Prefer post for anything a teammate should act on (it queues durably and
injects a single [mux] new message from … nudge no matter how many
messages pile up); tell types straight into their input — immediate, but
it can interleave with whatever they're doing. Messages up to 16 KiB
(post) / 64 KiB (tell); registry, inboxes, and per-tab context live
under ~/Library/Application Support/muxterm/, cleaned automatically when
panes close. Isolation is cooperative — mux enforces the tab boundary,
but anything with socket access can drive tmux directly.
- Panes spawned by muxterm export
MUXTERM=1andMUXTERM_SESSION=<name>, so scripts and hooks can detect the mesh without probing tmux (panes created before this feature keep their old environment until recreated). - Claude Code becomes fully self-configuring with two hooks in
~/.claude/settings.json: aSessionStarthook runningmux brief 2>/dev/null || true(the briefing lands in context the moment a session starts inside a muxterm pane; silent no-op elsewhere) and aUserPromptSubmithook that drainsmux inbox --consumeinto context — teammate messages arrive mid-conversation without the agent having to fetch. Combined withmux run reviewer -- claude, a pane is a named, briefed, reachable agent with zero manual prompting. - Naming nudge: while a tab still wears its auto-generated
adjective-animalcodename, the brief leads with an imperative "rename this as your first action" ask (naming the codename so the agent sees it), and muxterm's own auto-installedUserPromptSubmitstatus hook (mux agent-event working --nudge-name) injects a one-line reminder on each prompt — so an agent names its workspace when a task starts instead of leaving it on the codename. Both fall silent the moment it's renamed. - Agent status is self-configuring for all four built-in agents. Muxterm
installs lifecycle hooks for Claude and Codex, a Pi extension, and an
auto-discovered OpenCode plugin in
~/.config/opencode/plugins/muxterm-status.js. OpenCode busy/idle, permission, question, error, and shutdown events drive the same sidebar working/attention/idle states as the other agents; every integration is inert outside a muxterm pane. - Other agents:
mux runprints the briefing above them at launch, or wiremux briefinto whatever startup-context mechanism they have (aider conventions file, custom system prompts).
- Panes run
tmux new-session -A -D -s mux-<id>on a dedicated socket (tmux -L muxterm), isolated from your own tmux server.-Amakes attach-or-create idempotent, so fresh spawn and post-relaunch restore are the same code path (after a reboot you get the same layout with fresh shells). - Quitting the app just drops the tmux clients — the daemonized server and its sessions survive.
- Layout (windows → tabs → split tree → session names) is saved to
~/Library/Application Support/muxterm/state.jsonon every mutation, so even a crash restores. - On startup,
mux-*sessions not referenced by the saved state are killed (skipped entirely if the state file is corrupt).
src/— the app: split tree (layout.rs), tmux lifecycle (tmux.rs), persistence (state.rs), shortcuts (keys.rs), agent-mesh state (mesh.rs), the "?" prompt (ai_prompt.rs,ask.rs), the settings panel (settings.rs), cmd+click link opening (links.rs), render loop (app.rs).src/bin/mux.rs— themuxCLI (agent mesh,mux ask).crates/egui_term/— vendored egui_term 0.1.0 (terminal widget on alacritty_terminal) with local patches (input gating, tmux mouse reporting, copy-on-select, IME, bracketed paste, render batching, cmd+click links); seecrates/egui_term/VENDOR.md.