v0.9.0
The Claude Code join no longer reads your terminal's window title. That mechanism is gone, and the joins that replace it are exact, which is what makes remote sessions work with Claude Code's own titles left on. Settings has been rebuilt around a sidebar, opencode joins alongside Claude Code, and you can now point dictation or polishing at Mistral's hosted models instead of the local ones.
localvoxtral runs on your Mac and sends nothing anywhere unless you choose a hosted engine. Managed local mode is what a fresh install uses.
Joining a Claude Code session¶
Window-title markers are gone. localvoxtral used to plant a short marker in your terminal's title and look for it again when you started dictating. Titles are not yours alone: Claude Code writes its conversation title over yours mid-turn, multiplexers rewrite pane titles, and you rename windows yourself. On a measured session the marker was actually the visible title for 1.26 % of the time, 0.88 seconds out of 69, and Claude Code's own title held it for the rest. A join that depended on catching that window was a lottery, not a binding. It has been removed rather than patched.
You no longer need to disable Claude Code's titles. If you exported
CLAUDE_CODE_DISABLE_TERMINAL_TITLE=1 to make remote joins work, you can drop
it. Titles can stay on.
Remote sessions over herdr now join properly through Ghostty and Terminal.app. Inside a herdr session the app asks herdr itself which pane is focused and binds to that pane by id. Any ambiguity, including two live herdr sessions, attaches nothing rather than guessing.
herdr 0.9 federation is supported. A 0.9 client attaches several SSH machines
and shows their panes in one local window, so the agent you are dictating to
may be on another machine with no ssh anywhere on your terminal. localvoxtral
reads which machine the client is showing, and joins the Claude Code session on
that machine over its own SSH forward. If it cannot tell which machine a window
shows, because two herdr clients are open, or herdr's saved state is
unreadable, it attaches nothing and says so. Settings lists your saved herdr
machines under Remote hosts, and Import… pre-fills the enrollment form from one
of them.
A plain ssh session joins again, and remote enrollment is one automated flow.
In Settings, Set Up in the enrollment sheet, or Update host… followed by Set Up
in an enrolled host's row, runs six steps in order: the ~/.ssh/config block,
the shell startup export, the remote plugin install-or-update, the LC_LVX_TTY
crossing check, the herdr agents-panel row when herdr is installed, and the
final setup check. The sheet shows one consent sentence naming the local files
and SSH host, a Details link to the exact commands, and one progress line per
step. It shows no token, command, or file contents.
By hand, if you prefer, add this to your shell's rc file on your Mac:
if [ -z "${LC_LVX_TTY:-}" ] && [ -z "${SSH_TTY:-}" ]; then
case "$(tty 2>/dev/null)" in /dev/*) LC_LVX_TTY="$(tty)"; export LC_LVX_TTY ;; esac
fi
Your terminal's tty then travels into the remote session with the SSH session
itself, and localvoxtral joins the window you are dictating into by comparing
it against the window it can see. Because ssh carries environment per session,
this works through a jump host (ProxyJump) and through ControlMaster, the
two setups a network-level match cannot handle. Open a NEW terminal window
afterwards, either way: the value is fixed when a session starts.
If you enrolled a host before this release, run Update host… on it. That also
refreshes its ~/.ssh/config block, which is where the SendEnv line lives.
Without it there is still a zero-setup path: the session is matched to the TCP
connection it arrived on. That one cannot see through a jump host or a shared
ControlMaster connection, and says so when it hits one.
Neither joins a session inside tmux, screen or zellij: a multiplexer keeps the first client's environment, so a pane describes whichever window started the server. There, herdr is the arm that works, because it binds the pane itself.
Sessions inside cmux join by surface id over cmux's control socket, and the pane's text is read the same way. cmux draws its terminal with libghostty, so neither the tty join nor a normal screen read works there.
A Claude Code Remote Control session in your browser joins from the focused tab, so dictating into the web client grounds on the session that tab shows.
The TTY join needs Ghostty 1.4 or newer, iTerm2, or Terminal.app. A Ghostty tip build counts: tip stamps a commit hash where the version number goes, so the app now also reads Ghostty's scripting dictionary and treats the build as join-capable when the dictionary declares what the join needs. Other terminals abstain rather than half-join.
The overlay names what it joined. Its header shows the joined workspace when a session attached, says "No Claude session" when none did, and shows nothing when no join was attempted. A join that silently did not happen used to look exactly like one that worked.
Settings, rebuilt¶
Settings now has a sidebar instead of a row of tabs, with one pane per topic and a status dot on the rows that have a state: green means detected and set up, yellow means a setup step is pending, grey means not installed.
A new Integrations section has one pane per harness, each with a one-line status and buttons that do the whole setup, so you never edit a config file by hand. Claude Code collects the plugin, the status line, the cmux join and your remote SSH hosts. Context holds what the polisher may see, with one line per toggle naming what leaves this Mac. opencode and herdr have their own panes. Each agent and terminal row carries that product's own black-and-white mark, so you can find a row by its icon.
A Terminals section lists one pane per terminal app, showing whether it is
installed and what it supports: dictation everywhere, session join and screen
context on Ghostty, iTerm2, Terminal.app and cmux. iTerm2 and Terminal.app ask
for the Automation permission the first time they join a session. Add app…
treats any application as a terminal for dictation, which replaces the old
terminal_apps.toml. If you had that file, it is read once at launch, its
entries move into the list, and the file is left alone.
The old Backends pane is called Engines, and it holds the Dictation and Polishing choices.
herdr is found where it is actually installed. A GUI app's PATH is only
/usr/bin:/bin:/usr/sbin:/sbin, so a Homebrew herdr read as "Not found." in
Settings. The app now also looks in ~/.local/bin, /opt/homebrew/bin,
/usr/local/bin and the Nix profile directories.
opencode¶
opencode sessions join and ground polishing the way Claude Code sessions do.
Install the plugin from Settings → Integrations → opencode: it writes one
bundled file and one line in tui.json, and the same row removes both.
The plugin publishes the session the TUI is currently showing, so one opencode
process hosting several sessions on one terminal still resolves to the right
one. Under opencode run and opencode serve it deliberately publishes
nothing, since there is no pane to name.
Claude Code status line¶
A connection indicator can live in Claude Code's bottom bar, so you can see
whether localvoxtral is attached without opening anything. The Status line row
in Settings → Integrations → Claude Code installs it into
~/.claude/settings.json after a one-sentence consent, and never over a status
line you wrote yourself. On a remote host the indicator reads the last hook
result the plugin recorded, so it never dials the tunnel itself.
Mistral API mode¶
Dictation and polishing can each run on Mistral's hosted models instead of the local ones. Switch either engine to Mistral API in Settings → Engines and paste one API key from console.mistral.ai; the pane's Mistral API group also has a Check key button and a one-press "Use Mistral for dictation and polishing". The two engines switch independently, so hosted dictation with local polishing works, and so does the reverse.
This is opt-in and nothing selects it for you. In this mode your audio and your transcripts are sent to Mistral. Clipboard, terminal screen, repository vocabulary and Claude Code session context still require the trusted-endpoint opt-in, exactly as for any other non-local endpoint. Models and prices are in Under the hood.
API keys move to your Keychain¶
The External URL dictation key, the External URL polishing key and the Mistral
key are stored as login Keychain items under the service
com.localvoxtral.api-keys. They used to sit as plain strings in
~/Library/Preferences, readable by any of your own processes, carried into
backups, and printed by defaults export.
Keys you already entered move on first launch, and the preferences copy is deleted once the move succeeds. You do not need to re-enter anything. If the Keychain refuses, the app keeps using the old value for that launch and shows one sentence saying so, so a locked Keychain never reads as a missing key.
Action required if you have enrolled a remote host¶
Enrolled hosts do not pick up the new plugin by re-running the setup commands.
Run this on each enrolled host to get the 1.8.x remote plugin. The plain-ssh
join needs at least 1.7.0, and 1.8.0 adds the compact status line.
claude plugin marketplace update localvoxtral
claude plugin update localvoxtral-remote@localvoxtral
Order matters: refreshing the marketplace clone first is what makes the second command an update. Your token is preserved. In the app, every row under Remote Claude Code over SSH has an Update host… button that shows one consent sentence and can run them over SSH for you.
Nothing breaks if you skip it. An older plugin keeps working and simply sends a response the app now ignores. You just will not get the new join behaviour until you update.
Audio¶
Multi-channel input devices are supported. Professional interfaces that present many input channels now work, and an Input Channel submenu next to the microphone picker in the menu bar popover lets you pick which preamp your microphone is on. It appears only above stereo, where the choice means something.
Fixes¶
Polishing no longer stalls on a long unbroken run of characters. A pasted minified bundle, base64 payload or JWT in the clipboard made the token guard quadratic in run length: one 40,000-character run cost 20 seconds, or 130 seconds when it was hyphen-dense. Recognition is now linear.
Live Auto-Paste no longer types the trailing space that dismissed a TUI autocomplete popup. Dictating one slash-command token into Claude Code used to confirm the popup before you could pick anything.
A remote forward left behind by a crash or a force-quit is reaped at launch. It used to survive, keep holding the remote port, and make every later forward report "Port held" with a Retry that could only fail.
A rejected remote connection says why, in the log and in Settings, instead of blaming a healthy plugin for a connection that never sent a credential.
For contributors¶
Three things landed that only matter if you work on localvoxtral: an instrumented dogfood build with a one-command install, an SSH gate that drives the app's UI on a Mac for scripted checks, and a CI split that moved the portable half onto GitHub-hosted runners. The UI gate is deliberately never installed or updated by CI, because it is the trust boundary, so reinstall it yourself after upgrading.