Run a coding agent and your editor side by side in tmux, and let the agent show you the file it is talking about.
termwire up creates a tmux workspace with Neovim already listening on an RPC
socket. Every process in that workspace inherits the socket address, so a
coding agent running in one pane can open a file at a line in the Neovim
running in another pane. Add -w and the workspace gets its own Git worktree,
so several agents work on several branches at once without fighting over one
checkout.
No Neovim plugin is involved. Termwire talks to Neovim over its built-in remote RPC.
Above: termwire up demo -w opens a workspace on its own worktree, OpenCode is
asked where something lives, and it puts that file in front of the editor. The
two panes come from a layout file; the default workspace puts the
editor and the shell in separate windows.
npm install -g @termwire/cli
termwire up dev # workspace in the current directory
termwire up fix/login -w # workspace in a sibling worktree on that branch
termwire open src/app.ts:42 # jump the editor to that line
termwire ls # what is running where
termwire down fix/login -w # tear it down, worktree included- Node 22.12 or newer
- tmux 3.2 or newer
- Neovim 0.9 or newer
Bun works too and is what the repository is developed with, but nothing published here requires it.
npx @termwire/cli installPicks a layout — the window arrangement is drawn beside the list as you move
through it — writes it to ~/.config/termwire/config.jsonc or the repository's
.termwire.jsonc, and installs a termwire-open skill for the agents you
select. Everything it does is also a flag, and a run whose flags
leave nothing to ask never prompts: termwire install --layout focused --agents claude. See
the CLI README for what it will not do.
termwire up <name> creates or attaches to the tmux session
<project>-<name>. A new session gets two windows: editor, running
nvim --listen /tmp/termwire/<session>.sock, and shell. Running the same
command again attaches to the existing session and changes nothing else.
Every process in the workspace inherits three variables:
| Variable | Meaning |
|---|---|
TERMWIRE_SESSION |
tmux session name |
TERMWIRE_SOCKET |
Neovim RPC socket path |
TERMWIRE_EDITOR_PANE |
tmux pane id of the editor pane |
This is the whole of Termwire's state. Nothing is written to disk, so there is
no state file to go stale: termwire ls derives the list of workspaces from tmux
and Git, and termwire down is tmux kill-session plus, on request,
git worktree remove.
up also installs one global tmux session-closed hook, so closing a session
kills everything started in it, including background agent jobs that outlive
kill-session on their own. Installing it again never duplicates it and never
removes a session-closed hook of your own. See
the CLI README for what is killed and
what escapes.
Termwire does not start an agent for you. Start one in the shell window, or
declare it as a pane command in a layout file.
| Command | Workspace directory | Git branch |
|---|---|---|
termwire up dev |
current directory | unchanged |
termwire up dev -b feature/api |
current directory | switch to it, or create it from HEAD |
termwire up chore/improve -w |
sibling <project>-chore-improve |
chore/improve |
termwire up dev -w legacy |
sibling <project>-legacy |
legacy |
termwire up dev -w -b feature/api |
sibling <project>-dev |
feature/api |
The session name always decides the tmux identity. A bare -w derives the
worktree directory and the branch from the session name. Slashes survive in Git
branch names and are replaced only in directory names. Attaching to an existing
session never touches Git.
$ termwire ls
SESSION A BRANCH DIRECTORY PROCS RSS
termwire-dev * master ~/projects/termwire 7 1.2G
termwire-demo demo ~/projects/termwire-demo 4 780M
One row per workspace, printed once and then done. A marks an attached session,
and PROCS and RSS count the processes still labeled with that session, so a
background agent job shows up as well as a pane. A session is a workspace only
when its tmux environment carries TERMWIRE_SESSION, so a session of your own
with a similar name is never listed. --json prints the same rows for machines.
termwire down <name> kills that workspace's session, and a bare termwire down
inside a workspace kills that one — which is how a worktree workspace is torn
down from the inside. -w also removes the worktree the session sits in. A
worktree with uncommitted changes is refused unless --force is given, and so is
the main checkout. The branch always outlives the workspace.
termwire open <path>[:line] opens the file in the workspace Neovim and
focuses the editor pane. A trailing :42 selects the line; --line 42 does
the same and leaves the path untouched, which matters for a filename that ends
in a colon and digits.
The command needs TERMWIRE_SOCKET, so the agent must run in a shell created
by termwire up. Outside a workspace it fails with not inside a termwire workspace.
Any agent that can run a shell command can use it, and termwire install is
what tells yours how. It writes a termwire-open skill where the agent looks for
skills — ~/.claude/skills/termwire-open/SKILL.md for Claude Code,
$XDG_CONFIG_HOME/opencode/skills/ for OpenCode. Codex has no skills, so the
command prints the one line to add to ~/.codex/AGENTS.md instead:
To show the user a file, run `termwire open <path>:<line>`.A skill costs nothing to keep: the agent runs one command when it has a file
worth showing. Allow it once, with a rule like Bash(termwire open:*), and the
approval prompt stops too.
For an agent that may not run shell commands at all, @termwire/mcp exposes the
same operation as an MCP tool and @termwire/opencode-plugin as a native OpenCode
tool. Both are still published, and the MCP server is the one thing here that costs
a process: it lives for as long as the agent session does, so a machine running a
dozen agents carries a dozen of them. Prefer the skill, and configure exactly one
of the three — two means the agent sees the same tool twice.
npm install -g @termwire/mcp
claude mcp add termwire -- termwire-mcpRegister it from inside a Termwire shell, so the server inherits the socket, and
point it at the installed binary rather than npx -y @termwire/mcp: a package
runner stays alive beside every server it launched.
A new session uses the default editor-and-shell layout unless a JSONC file says
otherwise. Termwire reads $XDG_CONFIG_HOME/termwire/config.jsonc (or
~/.config/termwire/config.jsonc) and .termwire.jsonc in the workspace Git
root. A project file's windows replace the global ones rather than merging.
Exactly one pane carries role: "editor", and that pane runs Neovim. Layouts
apply only when a session is created. The CLI
reference has the full schema and
a layout cookbook.
| Package | What it is |
|---|---|
@termwire/cli |
the termwire command: up, ls, down and open |
@termwire/mcp |
MCP server exposing termwire_open |
@termwire/opencode-plugin |
native OpenCode tool |
@termwire/tmux |
typed tmux adapter |
@termwire/nvim |
typed Neovim RPC adapter |
bun install
bun run lint
bun test
bunx tsc --noEmit
bun run build
bun run verify:node
bun packages/cli/bin/termwire.ts up devThe repository is developed with Bun, but every published package must run on
Node. bun run verify:node executes the built CLI and MCP server under Node
and fails on a Bun-only API or an extensionless relative import. CI runs it on
every push.
Run bun run hooks:install once to opt into the local pre-commit hook. It
formats staged files, applies safe Biome fixes, then runs lint and tests.
Partial staging is rejected, because a staged file with unstaged changes cannot
be checked honestly. Hooks are bypassable, so CI stays authoritative. See
releasing for the publish flow.
PDR.md records design intent and is not a description of what ships
today.
CONTRIBUTING.md has the setup, the checks to run, and what a
change needs before it is a pull request.
MIT

{ "version": 1, "windows": [ { "name": "work", "panes": [ { "id": "editor", "role": "editor" }, { "id": "agent", "splitFrom": "editor", "direction": "horizontal", "sizePercent": 40, "command": ["opencode"], }, ], }, ], }