Like claude agents, but for all your harnesses and machines.

Fake agent (multi)plexer: a status list for the Claude Code, Codex and OpenCode

sessions you already have, on every machine you can ssh to. Fake because it multiplexes

nothing. The harnesses keep their own sessions running; faplex reads what they report and

opens the harness's own CLI on the row you pick.

- One grouped, prioritized, real-time list. Working, Needs input, Finished, across every machine and harness, with context tokens and subagent counts.

- No setup on top of your harnesses. No hooks, plugins or wrapper to launch through, and nothing to install on remote machines: plain ssh and what the harness daemons already expose.

- It opens the real CLI. A row is claude attach,codex resumeoropencode -shanded your terminal, so every harness feature works and looks the way it does without faplex.

- No windows kept running. No pane or client per session. One starts when you open a row and is closed after 15 minutes out of view; the agent carries on in its daemon.

- The keys you know from claude agents. ↑↓ to move, → to open, ← to come back.

- Sessions started anywhere show up, not only the ones you start from faplex.

- Safe to quit. It holds nothing the sessions depend on.

bunx faplex # runs without installing; `npx faplex` works too

bun add -g faplex # or install the `faplex` command (`npm i -g faplex` likewise)

faplex devbox pi # this machine plus two ssh hosts, no config neededfaplex runs on Bun. Started through npm or npx it finds the bun on your

PATH, and tells you how to install it if there is none.

✅ yes ·

Filled in from each project's docs in October 2026; corrections welcome. Close relatives: Claude Squad (like Agent Deck), cmux (like Herdr), rejoin (like gctrl).

The short version: faplex owns neither the terminal nor the conversation. Agent Deck and Herdr own the terminal, which gets them persistent panes for any CLI at the price of a client per session. Paseo and T3 Code own the conversation, which gets them one UI everywhere at the price of integrating deeply with every provider. gctrl is the nearest relative: same hands-off stance, but local only, no new sessions, and status inferred from files.

faplex asks very little of a harness: a list of sessions with a status, a command that attaches to one by id, and a command that starts one (the Providers table is the whole contract). It never parses terminal output or speaks a harness's conversation protocol, so a harness can change its UI, its tools and its transcript rendering without faplex noticing.

What it does depend on is young: claude agents --json, the OpenCode 2.x service API and the

Codex app-server socket are all recent, and a change to one needs an adapter fix. When that

happens the damage is contained: every response is schema-checked, so one harness on one machine

shows an error above the list and the rest of the list keeps working. Context tokens and subagent

counts lean on more (Claude's are read from its transcript files); they are optional and drop

out on their own without affecting the list.

None is needed: with no file, faplex lists this machine, called local. Other machines can be

named on the command line. faplex devbox pi lists this machine plus those two for this run:

an argument is a machine id from the config file, else an ssh alias/host. Nothing is written.

To keep them, ~/.config/faplex/config.json, every key optional:

{

"$schema": "https://raw.githubusercontent.com/kkrausse/faplex/main/schema.json",

"machines": [

{ "id": "laptop" },

{ "ssh": "devbox", "dir": "~/src", "short": "dev" },

{ "ssh": "pi" }

],

"archiveAfterHours": 24,

"closeHiddenAfterMinutes": 15,

"uploadDrops": false,

"opencodeMini": false

}- machines: the machines besides this one. This machine is always listed; an entry without- sshis this machine and can rename it or set its other fields.- ssh: an alias/host from your ssh config (key auth; the dashboard never prompts).

- id: the name shown in the list. Defaults to- ssh, or to- localfor this machine.

- short: a shorter name for list rows (e.g.- "dev"); the footer and new-session picker keep- id.

- dir: default start directory for new sessions there (default- ~).

- color: label color for the host (hex); defaults to a palette color by position in the list.

- path: extra PATH entries on the host.- ~/.local/bin,- ~/.bun/binand- ~/.opencode/binare always added, since non-interactive ssh skips- .bashrc.

- archiveAfterHours(default 24): a finished session untouched for this long moves to Archived. This is the knob for how quickly Finished empties.

- closeHiddenAfterMinutes(default 15): how long a session client you aren't looking at is kept before it is closed.

- uploadDrops(default false): with a remote session open, a pasted or dropped path to a local file is copied to the remote machine's temp directory, and the path there is typed instead. See Keys.

- opencodeMini(default false): offer OpenCode Mini as its own choice in the new-session picker. See Providers.

$schema gives editors completion and checking. A file that doesn't fit stops faplex at

startup with the path and the field. An older machines.json (just the machines array) is

still read when there is no config.json.

All ssh traffic to a host shares one connection (ControlMaster sockets in

~/.local/state/faplex/ssh). sshd allows 10 sessions per connection by default

(MaxSessions); each open remote pane and the host's status loop take one.

Sections: Working, Needs input, Finished (done, failed, interrupted, idle) and

Archived (collapsed; ↓ past the last row above it opens it, moving back up closes it). Each row is machine·harness, title, status line, then

subs (active/total subagents), tok (tokens in the context window) and age (time

since the last update). A ⑂ or $ after the status mark means subagents or a shell are still

running behind the row (Claude only so far): a question asked with a dev server up is still

Needs input, and a finished turn with one left running is still Finished. A session is archived when its harness archived it, you archived it

here, or it has been finished and untouched for 24 hours (archiveAfterHours). Archiving here is not final: opening

the session, or anything happening in it afterwards, brings it back. Unprompted drafts are hidden.

Failed turns are at the top of Finished. A line above the list appears when something needs

you: a machine that can't be reached or a harness whose answer couldn't be read (red; that

source's last known rows stay, dimmed), and a harness that is installed but whose daemon isn't

running, with the command that starts it (codex app-server daemon start,

opencode service start). Harnesses that aren't installed are listed dimly at the bottom.

docs/design.md has the details: ordering, how each harness's token and subagent numbers are measured, and where archive marks live.

List: ↑↓/jk move · ⏎/→ open · n new · x stop + archive · r restore · R reattach · tab into/out of archived · / filter · q quit.

R is for a session pane that looks stale or stuck: it closes the client faplex kept for that

session and attaches a new one. The agent itself isn't touched.

x moves the row to Archived at once, then in the background stops whatever is still running

in the session, so an archived session can't wake itself up later (a Claude /loop or

scheduled wakeup), and writes the mark. With Archived shown, the row is red with a spinner

until that is done. If the stop fails the session is archived anyway and stays red (the

Archived heading says how many are still running); x on it tries the stop again.

The conversation is kept either way; opening the session resumes it and takes it out of the archive.

faplex --help has the command line. faplex ps lists the running dashboards and marks the ones whose terminal is gone;

faplex kill stops those (faplex kill all stops every one). Sessions are not affected.

New session: ↑↓ machine · ←→ harness · tab edit start dir · ⏎ open · esc back. Combinations whose harness is missing or unsupported are greyed out.

In an open session, go back to the list with:

- ctrl+] anywhere

- ← on an empty prompt

Every other key, ctrl+c included, goes to the harness. The client stays alive in the

background (●) and repaints when reopened; clients not shown for 15 minutes

(closeHiddenAfterMinutes) are closed. Opening a session clears the terminal's screen and scrollback first. An unused new chat is cached for reuse ("·ready")

and stays out of the list; n reopens the picker on it.

A terminal handles a dropped file by pasting its local path, which names nothing on another

machine. With uploadDrops on and a remote session open, a paste that is only paths to local

files (of any size) is held while they are copied over the shared ssh connection to

${TMPDIR:-/tmp}/faplex-<uid>/drops/ on that machine, then typed with those paths. A path that

also exists on the remote is left as it is, and so is the whole paste if a copy fails. Copies

older than a day are deleted at the next drop; nothing else is written on the remote.

What a harness needs to appear here, per machine:

- A daemon that owns its sessions, so they outlive any client, including the dashboard's pane.

- Presence: a cheap check that says installed / too old / daemon not running (not errors: the first two are shown dimly, the last with the command that starts the daemon).

- List: every session with id, title, cwd, last update, model, whether the harness archived it,

and a status that maps onto working | needs | done | failed | interrupted | idle. Activity (working, needs input) must come from the live process, not from a self-reported note.

- Change signal: an event stream that triggers a re-list, or polling. Polling also re-lists on demand, which the dashboard asks for when you leave a pane.

- Open: a CLI that attaches to a session by id in a terminal (run over ssh for remote machines).

- New: a CLI that starts a session in a directory, plus a way to recognise it in the list (an id it prints, or the first new session in that directory).

- Stop (optional): end whatever is still running so nothing starts again unprompted. xruns it before archiving. A harness whose sessions can wake themselves (Claude's/loop, scheduled wakeups) must stop the process; one that only runs when prompted just interrupts the turn.

OpenCode Mini is opt-in (opencodeMini in the config). With it on, the new-session picker

offers opencode and opencode-mini separately; list rows show oc and ocm. Mini is the minimal, scrollback-style interface shipped in the same

OpenCode executable, so it updates with OpenCode. Faplex stores the Mini choice in

session metadata (faplex.interface: mini), preserving it across dashboard restarts

and machines. Both choices share one status connection and each session appears only

once. Untagged sessions, including ones started outside faplex, default to oc. With

opencodeMini off there is no Mini choice and every OpenCode session is an oc row.

Remote OpenCode ports and Codex sockets are forwarded over the shared ssh connection.

OpenCode 1.x and Claude Code without agents --json show as unsupported.

Interactive Claude sessions (plain claude in another terminal) are listed but

marked view: claude attach only accepts background jobs.

ln -s "$PWD/bin/faplex" ~/.local/bin/faplexbin/faplex runs a standalone build in dist/ and rebuilds it (under a second) whenever the

source is newer. bun start runs straight from source. docs/design.md covers

how it works.

e2e/ is an integration rig: docker compose starts three machines that ssh to each other, each

with the real Claude Code, Codex and OpenCode pointed at a fake model API, and seeds a few

sessions (e2e/compose.yaml lists the commands). The demo above is recorded on it:

e2e/record.sh writes e2e/out/demo.gif, which is copied to docs/demo.gif.

bun run release # patch; or: bun run release minor | major | x.y.zThat is the whole release: it pulls main, runs npm login if you aren't logged in, runs the

tests, bumps the version, publishes (npm asks for 2FA), then commits and pushes the bump. Don't

run npm publish by hand: it doesn't bump the version, so it fails on the one already published.