On a brand-new install, follow Getting started: run lectern up first. It starts a private local
runtime, detects the agent CLIs on PATH, adds the current git repository as a
project, and opens your browser, signed in, on Start an agent. lectern doctor checks what Lectern needs and can use, and whether agents can reach it
to report status and ask for approval; every problem comes with its fix.
Optional things (other agents, the terminal viewer, push alerts) are WARN or
-- lines, and doctor exits 0 when an agent can run.
lectern help lists the commands by group, and lectern help COMMAND or
lectern COMMAND --help shows one with examples. lectern update installs
the latest release in place, after checking its checksum.
lectern claude [args...] and lectern codex [args...] are the fastest way
into a tracked session, matching Happy's happy claude: no dashboard, no
picking a project first.
$ cd ~/projects/lectern
$ lectern claude
Session #14 "lectern · one-command" — also in your browser: run lectern up to open it.
Run it in any folder. It starts (or reuses) Lectern exactly like every other
client command — an explicit LECTERN_API/LECTERN_AUTH_TOKEN, else a
Lectern service running on this machine, else the private local runtime,
started automatically if it isn't already up (see
Which server) — then:
- Creates a session for
agent=claude(orcodex) andworkdir=$PWD. - Names it after the folder, plus the checked-out git branch when there is
one:
lectern · one-command. - Attaches it to the project whose registered repository contains
$PWD. On the private local runtime, a git folder with no project yet becomes one (named after the repository); elsewhere the session just has a workdir. - Refuses a built-in agent that is not installed where Lectern runs, naming
the ones that are, instead of opening a pane that says
command not found. - If a live session already exists for that same agent and folder, an
interactive terminal asks
Attach to existing session 'x' (#id)? [Y/n](default yes).--newalways starts a fresh one;--attachalways reuses (and errors if there is nothing to reuse). A non-interactive caller (a script, a pipe) defaults to reuse. --remote-control(or--rc, claude only) also opens the session in claude.ai/code and the Claude app, and turns it on for a session it reuses. The dashboard's session menu has the same Enable Remote Control entry. See remote-control.md.- Attaches your terminal to it with the same native attach
lectern attach session IDuses. The bar at the top saysCtrl+] menu · Ctrl+] d leave · Ctrl+\\ send file · double-click opens paths. Ctrl+], then d leaves and returns you to your shell; the session keeps running and stays reachable from the dashboard and your phone. (Ctrl-b, then d, still works.)
Extra arguments are the documented subset the session API actually accepts:
--model NAME and --resume. Anything else is a clear error rather than a
silently dropped argument, since there is no field for arbitrary pass-through
CLI flags.
Run lectern in an interactive terminal, or lectern console, for the live
dashboard. It opens on Sessions, groups by project, and refreshes automatically.
A single left-click on a session row attaches in the current terminal.
Click a group heading to fold it; use arrows/j/k to select a session for preview
without attaching. Ctrl+], then d comes back to the same selection.
Mouse reporting is restored after returning, including inside tmux.
Use lectern serve to start the server explicitly. Existing systemd/container
launches with no arguments and no terminal still start the server. Either way
it listens only on 127.0.0.1 unless a token or Tailscale identity protects it
(see Remote access).
Demo projects with scripted agent responses.
The bottom line is the key bar: it shows only the keys that work for the
pane and row you are on, most useful first, with q quit and ? keys always
at the right. ? opens every key for the current view; it scrolls, and /
filters it. The panes are numbered: 1 Sessions · 2 Approvals · 3 Projects ·
4 Tasks. The Approvals tab shows how many are waiting, and a line above the
list says when something needs you.
Status words are the same as on the web: Working, Needs you (only when a person is actually needed: an approval, or a permission prompt), Idle (at its prompt; your turn) and Ended, sometimes with a reason such as "Ended · interrupted"; an agent that exited while its terminal stays open is Stopped.
| Keys | Action |
|---|---|
| ↑/↓ or j/k | Move |
| Enter / click | Attach here; Ctrl+] then d comes back to the same selection |
| o / right-click | Open the session in a new terminal window; the list stays open |
| b | Select several sessions: click/Space marks them, Enter opens all of them |
| n | New session (on Projects: new project; on Tasks: new task) |
| x / d / Delete | End the session, after a confirmation. An adopted session is released (it keeps running) instead |
| r | Restore: start an agent that exited again, track an ended row again, or open the Restore list |
| y / a | On a session that needs you: allow once / allow for this session (asks first) |
| y / a / n | On the Approvals pane: allow once / allow for this session / deny |
| v | Review changes; c in the review commits |
| / | Filter the list (start with @ ! # & for at-its-prompt / running / quiet / failed) |
| m | Short menu for the selected row (at most eight items), each with its key |
| : or Ctrl+K | Every command, searched by plain words; Enter runs the highlighted one |
| 1–4, Tab / Shift+Tab, ←/→ | Switch panes |
| p, PgUp/PgDn | Focus / scroll the preview |
| Esc | Back one level; never quits |
| q | Back in a sub-view; quits at the top (agents keep running) |
| ? | Every key for this view |
| Ctrl+R | Refresh now (the list also refreshes every 3 seconds) |
Everything else is in the palette (:): Routines and Machines, find and
track running agents, search past conversations, saved conversations, blank
shells, launch profiles, agent runners, notification settings, usage, the API
explorer, grouping, the archive and including ended sessions.
Keys that moved in this release. The old key still works unless noted:
C → r (Restore list); R → r on a session whose agent exited; r
(refresh) → Ctrl+R, since r now restores; Tab (focus preview) → p, since
Tab now switches panes; 2 3 4 now open Approvals, Projects and Tasks
(Routines moved to the palette), while 5 still opens Machines and 6
Approvals; q in a review goes back instead of quitting. U F H O P Q S G A,
7 8 9, e h u s f g w z keep working and are also listed in the palette.
New session is one screen: Where (the project that contains the folder
you started lectern in, or that folder itself, or a new empty folder),
Agent (only agents installed on that machine; the project's default agent
when it is installed, otherwise the first installed one), Approvals (ask
before running commands, recommended; the server's default setting is
respected) and an optional First message. Enter moves to the next question
and starts the session on the last one; Ctrl+S starts it from anywhere. More
options… holds the rest: name, launch profile, model, machine, a separate
Git worktree, extra repositories, resume, the project brief and a group. The
new session is selected and attached, like lectern claude.
In Projects (key 3), select a project and press Enter to open a
persistent shell in its repository on the project's machine. No agent is
launched. You can inspect files and run commands directly; Ctrl+], then
d returns to the project list. A configured tmux default-command does
not replace this shell with an agent launcher.
Press o or right-click a session to open a separate terminal window while keeping the dashboard open. On Linux desktops Lectern launches Kitty, Konsole, Alacritty, GNOME Terminal or xterm. The desktop client may connect to a remote Lectern server; each new window attaches through its configured SSH connection.
Press b to select several sessions. Click or press Space to toggle each checkbox: selected rows show [x], and the header counts the selections. Selections survive searching/filtering. Enter opens one terminal per selected session, then clears the successfully completed selection. b cancels selection mode. Ordinary Enter and left-click still attach in place outside selection mode. Right-click always opens just that session immediately.
A terminal launched on a server through plain SSH cannot create desktop windows on your laptop without a client-side launcher. When no desktop display is available, Lectern uses tmux workspace tabs instead: a private workspace uses Ctrl-g n/p and Ctrl-g 0 for Sessions, or adds windows to an existing tmux workspace. Use the locally installed desktop client for separate laptop terminal windows.
Ctrl-] d closes an attached view without stopping its agent. Closing a desktop window or quitting the dashboard leaves the other desktop windows and agents running. Quitting a private tmux workspace closes its views but preserves underlying agents.
For optional workflow packs, use Projects → Actions → Workflows (Spec Kit / Maestro). Choose the provider and pack, then Enable or Disable and save with Ctrl-s. Each provider's availability is shown in the picker. Start a new session to load changes; disabling preserves generated documents. In the plain console, use the project's workflows action and select a pack to toggle it.
The wide layout shows a live preview beside the list. Narrow terminals keep one focused pane visible; Tab switches between the list and preview. Forms use named project/target choices, accept multiline prompts, and keep your draft after an API error. Tab changes fields, arrows choose options, Enter moves on and submits on the last field, Ctrl-s submits from anywhere, Esc cancels. Task creation can dispatch into an isolated worktree; Tasks → Actions also offers routine takeover, follow-up, diff review, completion and cancellation.
For a live unassigned native session, open Actions → Promote conversation. The
preview proves the exact agent, process, directory, terminal, and conversation
before offering compatible existing projects or a new project name. The final
confirmation keeps the same session, terminal, and native history. The same
flow is available from a terminal with lectern promote SESSION-ID.
lectern console --plain retains the line-oriented menu. Redirected input or
output selects it automatically, so scripts keep working. The JSON API commands
below are unchanged. The TUI polls the existing API; it does not run another
agent collector or maintain a second session database.
Use lectern shell [MACHINE] when you want to work directly in a machine's
shell. With no machine argument, an interactive client shows a searchable
picker; scripts should pass the target name or numeric ID. The shell is tracked
as a durable session and starts the target user's interactive shell in a fresh
Lectern scratch directory. It does not select a project, agent, model, launch
profile, memory, or worktree.
Use an existing SSH alias for the Lectern server. Download
/desktop/install-lectern-cli.sh from your Lectern instance, then run:
bash install-lectern-cli.sh --server lectern --api https://YOUR_SERVER:8443
lecternThe installer copies the client from your server over SSH, checks the machine
architecture, preserves any previous client, and installs into ~/.local/bin.
The installed launcher opens the console by default. Re-run it to update.
The server and client must use the same Linux architecture for this installer;
other architectures can build the Go binary from source.
For Windows, download /desktop/install-lectern-cli.ps1 and run it with
-Server YOUR_SSH_ALIAS (and -Api http://127.0.0.1:9110 when the server uses
a non-default API address). It installs an OpenSSH launcher on your user PATH;
management runs on the server with an explicit hosted API environment.
lectern upload stages local files over SCP.
The native Windows launcher requires OpenSSH, with your existing host/key setup.
For a terminal workspace that runs entirely on the current computer, follow Getting started instead: it needs no control-plane URL or SSH server.
This client works in your existing terminal. The separate desktop URI installers enable opening an attachment from the web UI.
A native attachment (lectern attach, lectern claude, Enter in the
dashboard, or lectern shell) gets the same key bar and keys whether or not
tmux is installed:
- With tmux on this machine, the attachment runs inside a private tmux
server Lectern owns for that one attachment. The agent's session, the
control plane's tmux, and your own tmux config and server are untouched:
the wrapper binds its own socket in a private
0700directory and removes it when the attachment ends. - Without tmux (the usual case on macOS, Windows and minimal Linux, where
sessions live on Lectern's own PTY host), the
lecternclient draws the session itself in a terminal emulator, with the key bar on the bottom row. SetLECTERN_NATIVE_BARE=1to use it even where tmux is installed.
The bar reads Ctrl+] menu · Ctrl+] d leave · Ctrl+\\ send file · double-click opens paths. Below 60 columns it keeps Ctrl+] menu · Ctrl+] d leave. When the session is waiting for an approval, the bar starts with
⏸ Needs you · Ctrl+] y allow · Ctrl+] m more, then what it asks; the keys
come first so a narrow terminal cuts the request, not the way to answer it.
Pressing Ctrl+] turns the bar into the list of keys that can follow it, until you press one: `m actions · d leave · u send file · | shell right ·
- shell below · e open a link · ? all keys`. Ctrl+] ? opens a menu with every attach key, which you can also click; clicking the bar does the same without tmux.
| Keys while attached | Action |
|---|---|
| Ctrl+] then d | Leave; the session keeps running and you return where you started. This is Lectern's own key, so it also works inside your own tmux |
| Ctrl+\ | Send a file from this machine to the agent as context (one chord). Always taken by Lectern: it never reaches the agent as SIGQUIT, which would end it |
| Double-click a path or link | Open it on this machine (see below) |
| Right-click a path or link | Its menu: open, download, copy, send to the agent, web viewer |
| Ctrl+] then y | Allow the request the bar says is waiting, once |
| Ctrl+] then m | Lectern actions for this session. When it needs you, the first items answer the approval (y allow once, a allow for this session) and the popup closes back to the agent |
| Ctrl+] then e | Label every path and link on screen; type a label to open it, Shift+label for its menu |
| Ctrl+] then u | Same file sender as Ctrl+\ |
| Ctrl+] then | | A shell on the session's machine, in the agent's directory, beside the agent |
| Ctrl+] then - | The same shell, below the agent (Ctrl+] % and " split the same way; Ctrl+] c opens it in a new window) |
| Ctrl+] then o / x | Without tmux: move to the next pane / close a shell pane (its shell session keeps running) |
| Ctrl+] then [ | Scroll back (↑↓ PgUp PgDn; q or Esc returns). Without tmux, the wheel does the same when the program does not use the mouse |
| Drag | Without tmux: select text and copy it to your clipboard (OSC 52) |
| Ctrl+] then ? | Every attach key in a menu |
| Ctrl+] then Ctrl+] | Send a literal Ctrl+] to the agent |
| Ctrl-b … | Everything the agent's own tmux normally does, unchanged; Ctrl-b then d also leaves |
Direct SSH launchers also get this bar from the server;
updated clients that provide their own controls mark the connection to avoid
a second wrapper. Update an older installed native client and reattach to use
the current behavior. The Ctrl+] m menu is the dashboard's own short menu for
the current session or task, with its key beside each item, and : inside it
searches every command. Native attach actions are hidden there, because the
popup never nests another terminal inside itself. Esc from that menu closes
the popup; Esc from a form or a code review returns to the menu. A popup
opened for one action (Ctrl+\ to send a file) closes back to the agent when
that action is done or cancelled.
Without tmux, a program that turns on mouse reporting gets clicks in its own coordinates (hold Shift to select text instead), keyboard protocols the agent asks for (kitty keys, modifyOtherKeys) are passed through, and a copy the agent makes (OSC 52) reaches your clipboard; a request to read the clipboard is never passed on. Ctrl-b keys go to the session as typed: on the PTY host there is no agent-side tmux, so leave with Ctrl+] d.
The popup runs on the machine where the native client runs, so upload paths refer to files on that machine. Reviews still show the agent's workspace on its target. When a direct SSH launcher uses the server-provided controls bar, upload paths are on that server; a native client running locally can upload files from the laptop. Authentication reuses the client's own API base and token, handed to the private tmux server through its environment only — never through a command line, a config file, or the status line.
When the upload finishes, the popup types the stored remote path back into
the attached terminal, shell-quoted, without pressing Enter — the same behavior
as the browser's Attach files action. This holds whether the upload was
opened with Ctrl+\ (or Ctrl-] u) or chosen from the Ctrl-] m controls
menu. The path refers to the agent's workspace on its target, so it stays
usable wherever the agent is running; submit it yourself when your prompt is
ready. If the client cannot reach the attached pane, or the path cannot be
typed literally, the popup reports the path instead of inserting it.
Split an attachment into [the agent | a shell where the agent is]: the
shell runs on the session's machine (this one, an SSH host or a container), in
the directory the agent's pane is in now (tmux's pane_current_path there;
the session's workdir if that cannot be read). Each one is a new tracked
Lectern shell session on that machine, so it also appears in the web UI and
keeps running if its pane closes. Three ways in:
-
Ctrl+] | side by side, Ctrl+] - stacked. Ctrl+] % and " do the same, and so does Ctrl+] c for a new window.
-
Right-click → Split: shell in project (or … (stacked)), in the link menu and at the top of tmux's own menu off a link.
-
lectern splitfrom a split of your own terminal:lectern split [--session current|ID|KIND/ID] [--dir agent|workdir] [--pick]It finds the session attached in the neighbouring window: every native attachment leaves a small record (no credential) in a private directory (
$XDG_RUNTIME_DIR/lectern-attachments-UID), its window's focus marks it as the latest, and one in the same kitty or WezTerm instance wins. When two were focused within a second of each other, or with--pick, it asks. The shell opens with the same Ctrl+] controls. Bind it to your terminal's split:kitty (
kitty.conf; thesplitslayout must be enabled):enabled_layouts splits,stack map ctrl+shift+backslash launch --location=vsplit lectern split map ctrl+shift+minus launch --location=hsplit lectern splitWezTerm (
wezterm.lua):config.keys = { { key = '|', mods = 'CTRL|SHIFT', action = wezterm.action.SplitHorizontal { args = { 'lectern', 'split' } } }, }
Ghostty's split keys (
new_split:right) start your shell and cannot run a command; typelectern splitin the new split.
Behind all three, the private tmux server's default-command is a small
Lectern script that asks the server for the shell
(POST /api/term/KIND/ID/split) and becomes it; through an SSH alias
(LECTERN_ATTACH_HOST) the hosted server resolves it, exactly as for attach.
Your own tmux and its configuration are untouched; Ctrl-b % inside the
agent's own tmux still splits that tmux as it always has. A split with an
explicit command of its own runs that command here. If the server cannot be
reached, the pane says why and waits for Enter.
Copies reach the clipboard of the terminal you are sitting at, over SSH too, through OSC 52:
- An agent's copy. Claude Code copies inside tmux with
tmux load-buffer -w; the session's tmux sends it on to the attached client as OSC 52 (with tmux's defaultset-clipboard external), and this attachment's private server passes it through to your terminal (set-clipboard onthere). - A selection here. Dragging to select in copy mode (where the agent does not use the mouse itself) copies to the clipboard the same way.
The terminal must accept OSC 52 writes. kitty, WezTerm, Ghostty, foot and
iTerm2 do by default; xterm needs allowWindowOps. The private server
declares the clipboard feature for every terminal, and a terminal that cannot
take OSC 52 ignores it. Because the private server passes OSC 52 on, a program
in the attachment can set your clipboard, the same as it could in your
terminal without tmux. Your own tmux configuration is not touched; if your
tmux on the session's machine has set-clipboard off, agents' copies stop
there.
Double-click a path or web address in an attached terminal and it opens on
the machine you are sitting at, even when the session runs on a server: a
web address in your default browser, a file (a PDF, an image, a report) in its
default app. The file is fetched read-only through the Lectern API into a
private folder (0700) under its own name, and swept after a day. It works
for paths outside the workspace (/home/you/reports/report.pdf, ~/notes/x.md)
and for paths an agent's TUI wrapped across rows: detection is the Go port of
the web terminal's (internal/filelinks), run on the same test vectors. A bare
name (report.md) counts only if the workspace has that file. Double-clicking
anything else still selects the word, as tmux always does. This holds for
agents that track the mouse too (Claude Code's full-screen view, for one): the
link check always runs first, and only a click that is not on a path or link
goes to the agent. Single clicks, drags and the wheel are the agent's as
before.
The path flashes (every row of a wrapped one) and the status line says what
happened: Opening report.pdf…, Opened report.pdf, Downloaded to ~/Downloads/report.pdf, or Couldn't open: <reason>. While the flash shows,
what you type still reaches the agent.
Ctrl+] e — hints. tmux tells nothing about the pointer until you click,
so there is no hover: instead this labels every path and link on screen with
a letter or two, the way a terminal's hints mode does, drawn over the pane
(which is otherwise dimmed; nothing is sent to the agent). Type a label to
open that link as a double-click would; type it with Shift to get its menu at
the bottom (open, download, copy, send to the agent, web viewer). Esc leaves.
It works over the remote attach and on tmux 3.4 to 3.7.
Right-click one for a menu:
| Item | What it does |
|---|---|
| Open on this machine | The same as a double-click |
| Download to ~/Downloads | Saves it there (a second copy gets (1)), and says where |
| Copy path | The full path, to your clipboard (OSC 52, so it works over SSH too) |
| Send path to the agent | Types it at the prompt, quoted, without pressing Enter |
| Open in web viewer | The session's page in your browser, with the file open |
A web address offers Open in browser and Copy link. Right-clicking anything else shows tmux's own menu.
Where nothing can open on "this machine" — the client runs on a server over
SSH (SSH_CONNECTION is set), or a Linux machine without a desktop session —
a web address is copied to your clipboard with a note to open it in your
browser, and a file opens in a viewer popup instead: text in a pager, a PDF
as its text (pdftotext). The menu offers View here in place of Open and
Download. Opening uses open on macOS, wslview (or explorer.exe) in WSL,
xdg-open on Linux, and the default handler on Windows. Files an OS would
run rather than show (.sh, .desktop, .exe, .bat, .js, .py, .app
and the like, or no extension at all) are not opened; download or view them.
Safety:
- The bindings live only on this attachment's private tmux server. Your own tmux and its configuration are not changed, and in copy mode both clicks keep tmux's meaning.
- The only way in is a mouse event on that private pane. Output an agent prints — including escape sequences that imitate a mouse report — is output, not input, and cannot open anything. There is no "open on the client" API: the click is handled on your machine, which asks the server for the file.
- Reading outside the workspace needs a person (a token holder or your signed-in identity): with Tailscale sign-in, a process on the Lectern machine is refused. The API credential lives only in the private tmux server's environment; the attachment inside it (and so the agent) runs without it.
- OSC 8 hyperlinks count when tmux passes them to the private pane
(
#{mouse_hyperlink}, tmux 3.4+); otherwise the text of the path is used.
lectern controls [KIND ID] opens the same control-only dashboard directly
(session, task, attempt, or project). An attempt resolves to its owning
task; when the requested row is not in the current list — an ended or archived
session, a task from another view — the popup says so instead of acting on a
different row.
The wrapper needs a working tmux on the machine where the native client runs
(Linux and macOS native clients). Set LECTERN_NATIVE_CONTROLS=0, or run where
tmux is missing or stdin/stdout is not a terminal, to keep the older behavior
with every key going to the agent. A missing tmux or non-interactive terminal
prints a note that controls are unavailable.
Claude Code's fullscreen mode ("tui": "fullscreen") turns on full mouse
tracking, so the terminal forwards every click to Claude: selection, copying,
clicking links, and the double-click and right-click actions above all stop
working in its sessions (tmux shows mouse_any_flag 1 for the pane). Lectern
therefore starts Claude Code, and OpenClaude, with CLAUDE_CODE_DISABLE_MOUSE=1:
fullscreen rendering stays, the mouse stays with the terminal. Claude then keeps its
transcript to itself (tmux has no history for the pane), so the browser terminal
turns the wheel and finger drags into Claude's own Page Up and Page Down keys
whenever the pane is a full-screen program that has not asked for the mouse. Exporting the
variable in ~/.bashrc does not reach these sessions, because Lectern launches
through a non-interactive bash -c that reads no rc file.
It applies to every launch, resume, fork, restore and account swap. Turn it
off under Settings → Workspace & terminal if you prefer Claude's own mouse
scrolling; a project can override it in its settings, and an explicit
CLAUDE_CODE_DISABLE_MOUSE in the project, agent or launch-profile environment
always wins. Sessions already running keep what they started with until they
are resumed or restored.
In the browser terminal, Ctrl+] then m opens Tools, and Esc returns to typing. The shortcut is visible beside Tools on desktop. Phone controls omit hardware-keyboard hints; tap Tools there. Ctrl+] twice sends a literal Ctrl+] here too.
lectern api GET /launch-profiles
lectern agent list
lectern agent save @agents.json
lectern api POST /sessions '{"name":"Work","profile_id":7,"scratch":true}'
lectern api GET /sessions
lectern api POST /sessions '{"name":"Scratch","agent":"codex","scratch":true}'
lectern shell build-box
lectern api POST /tasks/12/takeover '{}'
lectern api PATCH /routines/3 '{"enabled":false}'
lectern api POST /sessions/4/send '{"text":"Run the tests"}'
lectern api POST /sessions/4/setup/cancel '{}' # request checkout cancellation; retain files
lectern api POST /sessions/4/worktree/recover '{}' # validate interrupted allocation; keep files
lectern upload session 4 ./requirements.pdf
lectern files session 4
lectern download session 4 reports/result.txt ./result.txt
lectern post ./demo.mp4 --title "Checkout flow passing"
lectern attach session 4
lectern promote 4lectern agent save accepts the runner fields shown in Settings → Agents:
name, required command, optional args, model_flag, provider endpoint
environment, prompt_arg, resume_args, yolo_args, and env. The command
starts the runner; provider URLs and models configure that runner through its
environment contract.
Uploads print the stored remote path. They do not submit a message: mention the
path in your prompt, or paste it in the attached terminal. Upload also accepts
task, attempt, and project. Files/download use session, attempt, or
project. Downloads preserve an existing destination file.
An agent can put evidence in front of you instead of describing it. The
post_media MCP tool and lectern post both publish to the Media view: a
screen recording of the feature working, a screenshot, a generated report, a
log, or a link to the dev server the agent started.
lectern post ./demo.mp4 --title "Checkout flow passing" --note "Watch the total"
lectern post ./report.html --title "Test report"
lectern post http://127.0.0.1:5173 --title "Dev server"
lectern post ./trace.zip --title "Playwright trace" --session 4Inside a Lectern session the post attaches itself to that session: the
poster reads the tmux session it is running in, so an agent never needs to know
its own id. --session (or the tool's session_id) is for scripts running
somewhere else. A post from outside any session still lands, unattributed.
Files are copied into Lectern's media store, so a recording outlives the
worktree that produced it. Video and audio play in place and seek, images and
PDFs render inline, HTML renders in a sandboxed frame that cannot act as the
Lectern origin, and text files preview on demand. A link posted as
127.0.0.1 or localhost opens on the address you reached Lectern at, since
that is the machine the agent meant — the site has to listen on 0.0.0.0 for
that to work from another device.
LECTERN_MEDIA_DIR moves the store (default: lectern-media beside the
database) and LECTERN_MEDIA_MAX_MB caps one file (default 1024).
Every blank shell and every session started without a project gets its own
directory under the target's scratch root (~/lectern-scratch, or
LECTERN_SCRATCH_ROOT). Once an hour the server sweeps them. A directory is
removed only when all of this is true: no session is running in it, no session
there belongs to a project, it holds no files and no commits, no conversation
was recorded there, and it has been idle for LECTERN_SCRATCH_DAYS (default 7;
0 turns the sweep off). Each directory it takes is named in the server log.
A conversation counts as work even when the directory is empty: Claude's history is looked up by directory, a session's recorded conversation identity is checked, and Codex's dated history is searched for the path. That last search is run only for directories that already pass every other test, and a search that fails counts as a hit. Symlinks are never followed, so a scratch name that now points at a promoted project is left alone.
The Sessions view keeps the two kinds of scratch apart. Sessions and projects
lists agent sessions; Scratch terminals lists the blank shells — the quick
shells with no project or agent opened from Terminal's New terminal,
lectern shell, or the native dashboard's S action. Both headings stay visible when
their list is empty, search covers both, and Group by and the show-scope
filter apply to each. A project-less agent session is work, not a terminal, so
it stays in Sessions and projects. Make a project on a scratch card (or
More → Make a project on a project-less agent session) uses the same
promotion endpoint: the directory and the running terminal are neither moved nor
restarted, and the card moves to Sessions and projects on the next refresh.
Removal is a move into .trash inside the scratch root (or
LECTERN_SCRATCH_TRASH), purged after LECTERN_SCRATCH_TRASH_DAYS (default
14). Anything the sweep will not decide — work that no project claims — is listed
under Sessions → Scratch directory cleanup, where you keep it for good or
discard it.
lectern api GET /scratch # what the sweep sees; changes nothing
lectern api POST /scratch/sweep '{"dry_run":true}'
lectern api POST /scratch/keep '{"target_id":1,"name":"shell-20260918-qfEK6Y"}'
lectern api POST /scratch/discard '{"target_id":1,"name":"codex-20260909-Y7AYgu"}'An agent's dev server, an admin console, a browser automation in headed mode:
they all live on the machine the agent runs on, bound to its 127.0.0.1 or
drawing to a display nobody can see. Two things in Media → Live bring them
to the device you are actually using.
Expose a port. A link posted as http://127.0.0.1:5173 gets an Expose
button; so does any port you type, on any machine. Lectern opens a port of its
own and carries the TCP connection to that machine's loopback — over the SSH
connection it already holds, for a remote one. It is the port that is forwarded,
not a path, so absolute asset URLs, redirects and websockets all work and the
application never knows.
A live desktop. + Live desktop starts a private virtual display on the
machine, with noVNC in front of it, and shows it in the page. The card gives its
DISPLAY: anything started with that in its environment draws there, so
DISPLAY=:90 your-tool --headed is all it takes to watch a run. The address bar
opens a browser on that desktop, and that browser sees the machine's own
localhost — no forward needed. You watch by default; Take control passes your
mouse and keyboard through. An agent can ask for one with the open_live_view
tool, which returns the DISPLAY for it to use. With computer use allowed, the
agent can also screenshot, click and type on that desktop itself; see
Browser, which also covers the session Browser pane.
lectern expose 5173 --title "Dev server" # this session's machine
lectern expose 8080 --machine lxc-104-work
lectern live http://127.0.0.1:18080 --title "Watching the replay"
lectern live list
lectern live stop 3Live views are off by default: start the server with LECTERN_LIVE=1 to
turn them on. They open listening ports of their own, and open_live_view lets
an agent start a desktop that can be driven from the network, which is not
something an upgrade should hand anyone. With it off, Media offers none of this
and the API and the tool say how to enable it.
Both make something loopback-only reachable by whoever can reach Lectern, so
even when enabled neither happens on its own: a posted localhost link is never exposed until you
press the button, every open view is listed with an exposed badge, and each
one closes when its session ends, after four hours (ttl_minutes, at most a
day), when you stop it, or when the server restarts. Forwards only ever reach
the target's 127.0.0.1. A forwarded port cannot check a bearer token, so a
server with LECTERN_AUTH_TOKEN set refuses to open one unless
LECTERN_LIVE_UNAUTHENTICATED=1. LECTERN_LIVE_PORTS sets the range
Lectern listens on (default 19200-19299).
A desktop needs Xvfb, x11vnc, websockify and novnc on the machine that
hosts it (apt install xvfb x11vnc novnc websockify); a machine without them
says which are missing. A browser is optional — a tool that brings its own, as
Playwright does, only needs the DISPLAY. Targets reached through a command
prefix or pct cannot forward, because their loopback is not the SSH host's.
Claude Code, Codex, Gemini CLI, Qwen Code, OpenCode, GitHub Copilot CLI, Kilo,
MiMo Code, Muse, Devin, Command Code, Pi and Amp can discover skills on the
selected target and attach them to a project (the --agent names are claude,
codex, gemini, qwen, opencode, copilot, kilo, mimo, muse,
devin, command-code, pi, amp). Configure additional target-local source directories with the project
API; repository skills are discovered by walking up from the project's Git root:
lectern api PATCH /projects/7 '{"skill_sources":["/srv/agent-skills"]}'
lectern skill list 7 --agent codex
lectern skill attach 7 'configured:<source-hash>/lint' --agent codex
lectern skill attached 7 --agent codex
lectern skill detach 7 12The web and terminal dashboards provide the same discovery and attach/detach
actions. Sources are read on the target and linked into .claude/skills (Claude)
or the shared .agents/skills (every other supported agent) in the project and
any selected worktree; source files are not copied. Discovery also lists each
agent's own user skill directories (~/.gemini/skills, ~/.qwen/skills,
~/.config/opencode/skills, ~/.copilot/skills, and ~/.agents/skills).
Gemini CLI 0.61, Qwen Code 0.24, OpenCode 1.18 and Copilot CLI 1.0.88 were each
checked to load a symlinked skill from .agents/skills. Other catalog agents
(Aider, Goose, Amp, Cursor, Crush, Kimi, Cline) are refused: Lectern has not
confirmed a skills directory they read, so it does not write files they would
ignore. Custom agents get skills only when saved under one of the names above. Detach removes only links proven to be Lectern-owned. Native
repository skills and changed or foreign destinations are preserved.
api writes JSON to stdout and failures to stderr with a nonzero exit code.
Bodies accept inline JSON, @filename, or - for stdin. Every web operation is
available through the same API; specialized menus cover frequent operations,
and Full API accepts the remainder without opening a browser.
| Resource | Operations |
|---|---|
/sessions |
GET list, POST create; GET/PATCH/DELETE /{id} |
/shells |
POST create a tracked blank shell on a target (target_id or machine) |
/sessions/discover, /sessions/adopt |
GET running agents, POST track |
/sessions/{id}/send, /handoff, /promote |
POST message/key, handoff, associate project (promotion requires the exact preview identity) |
/sessions/{id}/reader, /wraps |
GET conversation, handoff records |
/tasks |
GET list, POST create; GET/PATCH/DELETE /{id} |
/tasks/{id}/dispatch, /takeover, /followup, /complete, /cancel, /commit, /cleanup |
POST actions |
/tasks/{id}/messages, /events, /diff |
GET; messages also POST |
/tasks/clear |
POST completed-task cleanup |
/routines, /projects, /targets |
GET list, POST create; PATCH/DELETE /{id} |
/routines/{id}/run, /targets/{id}/check |
POST run/probe |
/projects/import/scan, /projects/import |
GET scan with target_id/root; POST import |
/projects/{id}/brief, /notes, /wraps, /capability |
GET project context; DELETE /notes/{noteID} |
/approvals, /approvals/{id}/decision |
GET queue, POST approved/denied decision |
/agents, /templates, /settings |
GET or PUT configuration |
/models, /stats, /health, /projects/usage |
GET available models, usage and health |
/settings/test-notification, /admin/janitor |
POST test or maintenance |
Configure LECTERN_API and LECTERN_AUTH_TOKEN for HTTP. Set
LECTERN_ATTACH_HOST to the server's SSH alias on a remote Linux client;
attachment is resolved on the server, where its tmux sessions and SSH targets
exist. The Linux installer sets the URL and alias in its launcher.
Inside an existing tmux workspace, native attachment opens a full-size popup (tmux 3.2 or newer). The attachment owns its keyboard input; Ctrl+] d closes it and returns to the same dashboard selection without detaching the outer workspace.
With LECTERN_API set, every client command uses that server. Without it, a
command uses the Lectern service running on this machine when one answers on
127.0.0.1 at LECTERN_PORT (default 9110), and the private local runtime
otherwise; see local.md for the details. lectern local COMMAND
always uses the private runtime. lectern doctor prints the choice on its
server line, and an interactive command prints one line when both are
running:
lectern: using the Lectern service on :9110; `lectern local …` uses your private runtime
On a session or project, press v for live Git review. Left/right changes files,
s switches working-tree versus staged changes, PgUp/PgDn scrolls, r refreshes
the current file, and Esc or q returns. c commits every change in a
session's workspace with the message you type. On a session that works
directly on main (or master), the form offers a new branch first; choosing
the branch itself asks once more. Tasks retain their captured diff on v; their
actions menu also offers Review live changes for an existing attempt.
The same review is available on the web through a session's More → Review changes or an attached terminal's Tools → Review changes. It includes file search, line numbers, mobile wrapping, and separate staged/working counts. It reads a snapshot when opened/refreshed; the agent can continue editing. Large patches are explicitly truncated at 512 KiB. Git and Python 3 run on the target, including SSH targets; there is no local-checkout assumption and no staging or checkout mutation.
Anything that closed can come back from one list: sessions you ended (from the
web, the dashboard or a chat's end_session), archived ones, ones that exited
on their own, and ones a host restart interrupted. Each row says why it closed,
shows its last message, and names what reopening does:
| Action | When | What comes back |
|---|---|---|
| Resume | a Claude/Codex conversation is bound to the record | the conversation, under the same name; not the old scrollback |
| Track again | tracking was stopped but the terminal kept running | the running terminal, untouched |
| Relaunch | a restart interrupted it and no conversation was bound | a fresh agent in the same folder and record, primed with its last handoff if any |
| New shell here | a shell | a new shell in the same folder; scrollback is gone |
| Continue from handoff | no conversation, but a handoff was written | a new session primed with it |
| Choose history | Claude/Codex with no bound conversation | the saved-conversation picker for its folder |
| Start fresh here | nothing was saved | a new session in the same folder |
An archived record is unarchived first, and archived again if reopening fails. A record another session already continued is not listed again.
- Web and phone: Sessions → ↺ Restore, with search and project groups. Other agent… opens the Switch picker and continues the session in the agent, model or saved provider you pick, primed with its last handoff or the end of its conversation (native conversations do not carry across agents). Ending, stopping tracking or stopping and archiving a session shows a toast with Undo for ten seconds.
- Terminal dashboard:
ropens the list (/filters it, Enter restores,hopens history, Esc returns);xends the selected session after a confirmation, and "Undo: reopen the session ended last" in the:palette (orU) reopens it without attaching. - CLI:
lectern restorelists;lectern restore QUERYrestores the one match (several matches are listed, not guessed);lectern restore ID,--last,--agent NAME,--model M,--profile ID,--no-attach. - Chat connector / MCP:
restore_session, see sessions-mcp.md. - API:
GET /api/sessions/restorable?q=&limit=&all=trueandPOST /api/sessions/{id}/reopenwith an optionalagent,model,profile_idorname. A 409 withneeds_history: truemeans pick a conversation from the history picker.
An agent that exits but leaves its terminal open (the pane drops to a shell
prompt) is shown as Stopped, not idle — on its card, in the Now strip,
in the terminal view and in the dashboard. Lectern checks each pane's root
process about every ten seconds: a Lectern launch runs the agent under
bash -c "…; exec bash", so a bare bash there means the agent returned. For
an adopted session it looks for a shell in the foreground and no agent left on
the terminal. ↻ Revive (web, phone terminal, r on that session in the dashboard,
POST /api/sessions/{id}/revive) resumes the saved conversation in a new
terminal, or starts the agent fresh in the same folder when none was saved.
An adopted session's shell is yours, so Revive releases it and leaves it open
instead of closing it.
After a host restart, a Lectern-launched session with a bound conversation is
relaunched with it automatically on the next poll. Sessions then shows
Relaunched N sessions after a restart, naming each one, until you dismiss it
(GET /api/sessions/relaunched, POST /api/sessions/relaunched/dismiss); the
dashboard shows the same line once when it opens. Sessions it cannot bring back
(shells, agents without a saved conversation, or a failed relaunch) stay
interrupted: their cards show ↺ Restore, and Sessions shows a banner that
restores them all. Adopted sessions you started yourself are not relaunched; they
appear in the list as exited. When one ends, Lectern looks for the single saved
Claude/Codex conversation in its folder whose last write falls within five
minutes before, to two minutes after, the session's last observed activity, and
that no other session holds. If exactly one qualifies, Restore offers Resume
marked likely match; if none or several do, it keeps Choose history.
On a Claude or Codex session, press H (or Actions → Saved conversations / fork)
to choose a saved conversation from that workspace on its target. Read it in the
preview, use O for the preceding page, or choose Fork in the picker. The
confirmation names the exact conversation ID. Choose Use the same files or
New isolated Git worktree in Workspace for forks. Isolation creates a branch
from the selected committed base (HEAD by default); uncommitted changes stay in
the parent workspace. A blank branch name gets a unique name. The original
conversation is unchanged, and forking submits no new prompt.
The web offers Saved conversations in the session's More menu and the attached terminal's Tools menu. Choose a conversation explicitly; messages are grouped by role, tool activity folds away, and Load earlier messages pages back without replacing the messages already on screen. Codex's saved thread names are used when present. Long messages and discovery limits are labelled.
The web fork confirmation offers the same Workspace, branch and base controls. The confirmation uses the dialog space; Cancel returns to the transcript. Scripts pass an optional worktree object to POST /sessions/ID/fork, for example {"conversation_id":"UUID","worktree":{"branch":"experiment","base":"HEAD"}}. Omitting worktree keeps the shared-files behavior. Resume always continues in the recorded directory; it does not allocate another worktree. A removed or unavailable directory produces an error until the workspace is restored.
History reads native JSONL stores using the target's CODEX_HOME or
CLAUDE_CONFIG_DIR (including configured agent environment overrides), and
checks the stored working directory. It never guesses that a directory's most
recent conversation belongs to the selected terminal. Other agents retain the
terminal history/reader. Native history is currently limited to these two agent
formats; private reasoning/system/developer records are omitted.
GET /sessions/{id}/conversations lists candidates;
GET /sessions/{id}/conversations/{uuid}?before=BYTE_OFFSET reads a page;
POST /sessions/{id}/fork accepts conversation_id and an optional name.
Custom Claude/Codex definitions can provide fork_args with an {id} placeholder.
Without it, the picker offers reading only. A fork is a conversation branch, not
a Git worktree; interactive worktree isolation is a separate remaining feature.
Optional installed-CLI checks (no new prompt/model turn):
python3 tests/native_codex_fork.py verifies a distinct persisted Codex ID,
copied fixture history, and unchanged original; python3 tests/native_claude_fork.py
verifies Claude loads fixture history with its native fork flag and leaves the
original unchanged. Claude need not write the new transcript before a new turn.
The regular suite tests target lookup, pagination, workspace boundaries and
actual tmux launch with scripted agents, without requiring either paid CLI.
In New session, select a project and enable Isolate in a new Git worktree
(under Advanced options).
The terminal dashboard's n form has the same choice and also accepts an
explicit repository directory. Choose a base branch/tag/commit (blank means
committed HEAD) and a new branch name, or let Lectern allocate a unique name.
The agent starts in a separate directory beside the repository. Uncommitted
source edits are not copied; this mode starts a fresh conversation.
Both interfaces show the branch, allocation state and working directory. On
the web, expand the worktree line to see its path/base. After ending a session,
use Include ended and untracked sessions (web) or z (TUI) to find it again.
More/Actions → Remove worktree removes the directory only after its sessions
and tmux terminals have left, and only when Git reports no changed, untracked
or ignored files. The branch and committed work remain. There is no force-delete
option. Cleanup checks the recorded repository, branch and ownership marker.
POST /api/sessions accepts "worktree":{"base":"main","branch":"feature/example"};
it can use a project or an absolute workdir on a local/SSH target. Session
responses include workspace. DELETE /api/sessions/{id}/worktree performs
checked removal; ending/dismissing a session never removes its worktree.
Allocation is recorded before Git runs; failed allocations remain in ended
sessions with their planned path. A launch failure does not erase files.
This is separate from saved-conversation forking: native conversation forks currently share their original directory. Interactive worktrees are not yet a multi-repository workspace, and setup hooks/profile templates remain separate work on the parity roadmap.
Groups organize sessions across projects and targets. Press G or choose
Actions → Move to group; use a path such as Work/Client, or clear it to
ungroup. g cycles project, target, none and named-group ordering. Search also
matches group paths and worktree branches/directories. New-session forms accept
a group, and conversation forks and handoff successors inherit it.
On the web, More → Move to group offers existing names and keeps a failed
edit open for correction. Group by selects named group, project, target or
none. Named groups form collapsible nested sections with session/waiting counts.
The selected grouping and collapsed sections survive reloads in that browser
tab; searching opens matching sections. The API's group_path is shared across
clients, while these display preferences are local to the browser.
PATCH /api/sessions/{id} accepts {"group_path":"Work/Client"} or
{"group_path":""}. Names are normalized by trimming each level; empty levels,
control characters and more than eight levels are rejected. Moving a group
label does not move files, change the project, or restart the agent.
When a live Claude or Codex conversation can be verified, Saved conversations marks it Current terminal and selects it initially. The web reader opens its saved messages immediately; the terminal dashboard defaults its conversation choice to that entry. Refresh preserves a different conversation you selected.
Identification is read-only and currently uses Linux process information, including
on SSH/WSL targets. Claude supplies a runtime record tied to its process start;
Codex must hold its transcript open in the native codex process. The active tmux
pane and its identity must remain stable. Ambiguous, unavailable or stale evidence
leaves manual selection available. A new Claude conversation can be identified
before it has saved any readable messages; it becomes readable after persistence.
Older active terminals are checked through their current pane without changing
their tracking metadata. Renamed Codex binaries may require manual selection.
The key bar always shows the keys that work where you are. m opens a short
menu for the selected row, with each item's key beside it, so the menu teaches
the shortcuts; pressing an item's key runs it. : (or Ctrl+K) opens every
command, searched by plain words: try end, past conversation, running agents or closed. A label word that starts with what you typed ranks first,
so end finds End session before Send message. Enter runs the highlighted
item; arrow keys move the highlight. Esc clears the search, then closes.
Star the sessions you are working on, then open them all in one go.
*stars or unstars the selected session. Starred rows show ★ before the name. A star works on ended sessions too.0opens the ★ Favorites tab. It lists starred sessions only, ended ones included and marked as ended. Live, ready favorites are selected when the tab opens.- Space unselects the ones you do not want right now. Enter opens the rest, all at once.
- Ended favorites that can be restored are tracked again first, the same as r, then opened with the live ones. Ones that cannot be restored are skipped, and the notice says how many. Press r to open the Restore list for those.
- 1 goes back to all sessions.
The star is stored on the session (favorite_at in the session JSON, set with
PUT /api/sessions/{id}/favorite), so it survives the session ending. It is
per session, not per device. It is not the same as the per-device favorite
models in the web client.
Not included: favorites do not sort to the top of the Sessions list, and favoriting does not start or restore anything by itself.


