Open trustworthy local-file links in a focused right-hand Neovim split without
leaving your terminal workflow. tmux-nvim-link gives terminal agents and scripts a
small, signed tmux-nvim:// protocol that is bound to the originating tmux or Herdr
pane and socket.
- A CLI, output filter, or Pi adapter resolves an existing local file and signs its path, cwd, location, mux, pane, and socket with HMAC-SHA256.
- tmux/Herdr click handling or the macOS URL app validates the complete request.
- The handler opens Neovim in a horizontal right-hand split and focuses it.
The Node runtime lives in src/; the AppKit/CryptoKit URL handler is in native/.
adapters/pi/, integrations/herdr/, and config/tmux.conf are independently
reviewable integrations.
The signature prevents an untrusted terminal hyperlink from inventing a file-open
request. Validation is strict: unknown/duplicate fields, malformed locations,
modified signatures, nonexistent paths/cwds, wrong muxes, missing pane/socket context,
and mismatched panes or sockets are rejected. Normal click accepts only signed custom
schemes; unsigned file:// URLs and plain paths require an explicit Option-click.
The token is canonical Base64 encoding of at least 32 random bytes and mode 0600.
This is not a sandbox: a process that can read your token can sign links, and Neovim
retains your normal user privileges. Option-click is an explicit trust decision for
terminal text under the pointer. Signed URLs are sensitive and are never logged.
- Node.js 20 or later
- tmux with mouse/hyperlink formats used by modern tmux releases; tmux 3.3+ recommended
- Neovim available in
~/.local/bin, Homebrew, or a system executable location - macOS 13+ and Xcode command-line tools (
swiftc,codesign) for URL handling - Optional: Pi and/or Herdr 0.8+
- Ghostty or another OSC 8-capable terminal
The source distribution is macOS-first for custom-scheme handling. Node generation, filtering, and Herdr plugin pieces are portable, but Linux has no bundled desktop URL handler.
Check out a tagged release, review the scripts, then run them—there is intentionally
no curl | sh path:
git clone https://github.com/lanthissa/tmux-nvim-link.git
cd tmux-nvim-link
git checkout v1.0.0
./install.shThe idempotent installer preserves a valid existing token, copies runtime files to
~/.local/share/tmux-nvim-link, creates only safe ~/.local/bin symlinks, builds and
ad-hoc-signs ~/Applications/TmuxNvimLink.app, registers its URL schemes, links the
Herdr plugin when Herdr is present, and adds one marked source-file block to
~/.tmux.conf. It refuses to overwrite unrelated files or symlinks. Pi is installed
when detected; use --pi, --no-pi, --no-tmux, or --no-herdr to override
optional pieces. The installer checks Node 20+, Neovim, tmux, and macOS build tools
before changing managed paths, then prints the bin directory that must be on PATH.
Restart tmux or run tmux source-file ~/.tmux.conf.
The app is ad-hoc signed, not notarized. Gatekeeper policy can vary by macOS version and how the source was downloaded; inspect/build locally and approve it in System Settings if macOS prompts.
Inside tmux or Herdr:
tmux-nvim-url src/main.ts:42:7
tmux-nvim-url --markdown --label 'main.ts:42' src/main.ts:42
printf 'See src/main.ts:42\n' | tmux-nvim-filter
tmux-nvim-agent claude 'review src/main.ts'- In Ghostty, Command-click an OSC 8
tmux-nvim://link to pass it through the macOS URL handler. - In tmux, a normal click opens only signed
tmux-nvim://and temporary signedpi-nvim://hyperlinks. Unsigned hyperlinks are not opened normally. - Option-click (
M-MouseDown1Pane) explicitly allows a localfile://hyperlink or asks tmux to resolve the plain path under the pointer, includingpath:line:column. This modifier is the trust boundary for unsigned terminal text. - File links resolve relative to the signed or clicked pane cwd, never an arbitrary handler cwd.
Terminal/desktop modifier mapping is configurable; verify Ghostty and macOS settings if Command or Option is remapped.
The installer copies adapters/pi/tmux-nvim-file-links.ts to Pi's extension directory.
In interactive TUI mode it appends a local-file-link formatting hint before an agent
starts and rewrites eligible Markdown links in completed assistant messages. It also
provides /tmux-nvim-links plus /nvim-links. Re-run with --pi if Pi was installed
later. It does not rewrite non-TUI output and cannot sign outside tmux/Herdr or without
a valid canonical token.
These agents do not expose one common supported interactive final-answer hook.
tmux-nvim-agent therefore uses their documented noninteractive modes and pipes
complete text lines through tmux-nvim-filter:
tmux-nvim-agent claude 'explain src/server.ts'
tmux-nvim-agent codex 'review src/server.ts'
tmux-nvim-agent amp 'find the bug in src/server.ts'This does not preserve their interactive TUIs. Claude gets a citation-format system instruction; Codex and Amp preserve configured instructions and only link citations they emit. The conservative filter links existing files, not arbitrary URL-like text.
When herdr is present, installation stages the required herdr-plugin.toml and runs
herdr plugin link <support-directory> --enabled. A link failure is a warning rather
than a broken core install. The plugin handles canonical and legacy links and opens
its nvim pane on the right with focus. Check with herdr plugin list. Herdr's pane
and socket environment are included in every signature.
| Variable | Meaning | Default |
|---|---|---|
TMUX_NVIM_HOME |
Node/runtime support directory | ~/.local/share/tmux-nvim-link |
TMUX_NVIM_TOKEN |
Exact signing-token file | $TMUX_NVIM_HOME/token |
TMUX_NVIM_DEBUG=1 |
Opt in to redacted debug logging | disabled |
XDG_STATE_HOME |
Debug state root | ~/.local/state |
TMUX_NVIM_NVIM |
Absolute native-handler Neovim override | discovered safely |
TMUX_NVIM_TMUX |
Absolute native-handler tmux override | discovered safely |
TMUX_NVIM_HERDR |
Absolute native-handler Herdr override | discovered safely |
Installer path overrides must be absolute. --root DIR deliberately ignores
TMUX_NVIM_HOME and TMUX_NVIM_TOKEN, making lifecycle tests unable to escape
DIR/home.
Native discovery checks user-local, Apple Silicon Homebrew, Intel Homebrew, and system
paths without evaluating a shell. GUI apps may not inherit terminal-only environment
variables; use standard discovered locations for URL-handler launches. Token lookup
uses override, canonical, then temporary legacy paths; the first strictly valid token
wins in Node, Pi, and Swift. Rotate by replacing/invalidation of that first token, not
by expecting a later fallback to verify simultaneously. The temporary legacy fallback
is ~/.local/share/pi-nvim-link/token; new installs only write the canonical token and
create the legacy support symlink when safe.
Debug output is opt-in, redacted, capped, and stored mode 0600 at
$XDG_STATE_HOME/tmux-nvim-link/debug.log. Both runtimes refuse a symlink or
nonregular log target. Logs contain status metadata, not clicked lines, file paths,
full URLs, or signatures. Releases contain no logs.
Upgrade from a cloned/tagged checkout by running ./install.sh again. The token and
managed tmux block are preserved; owned runtime/app files are refreshed. Reinstalling
with --no-pi or --no-tmux removes those previously owned optional components.
For migration from the earlier manual implementation, first review the detected paths,
then run ./install.sh --adopt. Adoption remains narrow: it accepts only a support tree
containing the tmux-nvim runtime, a Pi adapter containing tmux-nvim integration code,
and an app with bundle ID dev.tmux.nvim-link. Without --adopt, these unmarked paths
are refused; unrelated paths are always refused.
./uninstall.sh # remove owned runtime/integrations/app; retain token
./uninstall.sh --purge # also remove the owned tokenUninstall is idempotent and handles marker-only partial installs. It removes only
matching symlinks, a marker-owned app, manifest-owned Pi/Herdr integration, an exact
complete managed tmux block, and known runtime files. Malformed tmux markers are
refused without editing the file. By default the token remains for a future reinstall.
--purge canonicalizes its path, rejects traversal/symlinks, and removes it only below
the installation home or support root. Unrelated paths are left untouched.
- Nothing happens: run
tmux source-file ~/.tmux.conf, check the four~/.local/bin/tmux-nvim-*links, and confirmtmux show -g mouseison. - Invalid signature: ensure all producers and the native app read the same token;
check
TMUX_NVIM_HOME/TMUX_NVIM_TOKEN, then regenerate with uninstall--purge. - Wrong pane/socket: links intentionally expire when moved to another tmux/Herdr server or pane. Generate a fresh link in the target pane.
- No Neovim executable: install
nvimin a discovered location or set the absolute override before direct--print-plan/--openuse. - macOS opens the wrong app: rerun
./install.shto rebuild and register the app. - Need diagnostics: set
TMUX_NVIM_DEBUG=1; reproduce once; inspect the mode-0600 state log. It does not contain full signed URLs or signatures.
tmux-nvim:// is canonical. pi-nvim:// scheme handling, PI_NVIM_* pane variables,
/nvim-links, and the old token path are temporary migration aids and may be removed
in a future major release. Generate only canonical links unless testing migration.
npm run ciTests inject temporary tokens and never read the live token. macOS tests also compile
the Swift handler, verify Node-to-Swift signing with --print-plan, and exercise an
installer lifecycle under an isolated root.
MIT © 2026 Andrew Bilgore