Skip to content

Repository files navigation

tmux-nvim-link

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.

How it works

  1. 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.
  2. tmux/Herdr click handling or the macOS URL app validates the complete request.
  3. 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.

Threat model

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.

Prerequisites

  • 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.

Install from cloned source

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.sh

The 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.

Quick start

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'

Click semantics

  • 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 signed pi-nvim:// hyperlinks. Unsigned hyperlinks are not opened normally.
  • Option-click (M-MouseDown1Pane) explicitly allows a local file:// hyperlink or asks tmux to resolve the plain path under the pointer, including path: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.

Pi integration

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.

Claude, Codex, and Amp

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.

Herdr

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.

Configuration

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 and uninstall

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 token

Uninstall 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.

Troubleshooting

  • Nothing happens: run tmux source-file ~/.tmux.conf, check the four ~/.local/bin/tmux-nvim-* links, and confirm tmux show -g mouse is on.
  • 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 nvim in a discovered location or set the absolute override before direct --print-plan/--open use.
  • macOS opens the wrong app: rerun ./install.sh to 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.

Compatibility

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.

Development

npm run ci

Tests 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

About

Signed terminal file links that open in focused tmux Neovim splits

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages