🇬🇧 English · 🇻🇳 Tiếng Việt
A small CLI for inspecting and cleaning up the hidden nvim processes that the
herdr-nvim plugin keeps alive behind
every tab in Herdr.
herdr-nvim gives you a full-height Neovim sidebar, one key away. What makes it pleasant is also what confuses people: each tab keeps its own headless nvim process, and that process survives when you toggle the sidebar off. This is deliberate — buffers, cursor position and pending annotations are all still there when you toggle it back on.
The cost shows up a few hours into a session. ps returns a pile of nvim --headless --listen ... lines you don't remember starting. Some drag along
vtsls, tsserver, biome and lazygit; a tab sitting on a TypeScript repo
can reach 2 GB across its whole process tree. Worse, when you close a tab its
nvim has nowhere to return to — it becomes an orphan and just holds memory.
This script answers three questions:
- How many nvim processes are running, and which tab does each belong to as
you see it on screen, not as an opaque id like
wW:t2? - How do I get back to that tab and keep using it?
- Which ones are orphans, and how do I clean them up safely?
- Herdr (the script must run inside a Herdr session, i.e.
HERDR_ENV=1) - The
chmarax.herdr-nvimplugin, installed bash,jq,pgrep(Linux: procps; preinstalled on macOS)watchis optional, only for thewatchcommand. Without it (the macOS default) the script redraws the screen itself once a second.- Linux, WSL included, and macOS. On Linux it reads
/proc/<pid>/cmdlineand/proc/<pid>/status; macOS has no/proc, so it reads the same facts fromps.
mkdir -p ~/.local/bin
curl -fsSL https://gist.githubusercontent.com/lamngockhuong/095fc74e3dcb1b294aea5a080198519c/raw/herdr-nvim-ctl \
-o ~/.local/bin/herdr-nvim-ctl
chmod +x ~/.local/bin/herdr-nvim-ctlMessages come out in English or Vietnamese depending on
HERDR_NVIM_CTL_LANG; unset, it falls back to LC_ALL/LANG, then to
English. Force one with export HERDR_NVIM_CTL_LANG=vi (or =en).
Check that ~/.local/bin is on your PATH:
case ":$PATH:" in *":$HOME/.local/bin:"*) echo "OK" ;; *) echo 'export PATH="$HOME/.local/bin:$PATH" # add to ~/.zshrc or ~/.bashrc' ;; esacherdr-nvim-ctlPID TAB RAM WORKSPACE TAB
264566 wV:t2 33MB 10. aidd-test 2. 2
268701 wW:t2 53MB TAB CLOSED — run: herdr-nvim-ctl gc
271487 wW:t1 32MB 11. ~ 1. 1
The last two columns are what you actually see on Herdr's workspace and tab
bars, so there is no id to decode. A row marked TAB CLOSED is an orphan: its
tab is gone, it cannot be reopened, it can only be reaped.
herdr-nvim-ctl focus wW:t2Herdr switches to that workspace and tab. Once there, press prefix+i to bring
the sidebar back with its previous state intact.
herdr-nvim-ctl gcThis kills nothing on its own. It delegates to the plugin's own herdr-nvim daemon-gc, preserving the behavior its author designed: only daemons whose tab
no longer exists are stopped, then their socket and state file are removed.
Daemons belonging to open tabs are never touched.
The plugin already runs this opportunistically every time you press prefix+i,
so you rarely need it by hand. Reach for it after closing a batch of tabs, when
you want the memory back right away.
herdr-nvim-ctl kill wW:t2 # one tab
herdr-nvim-ctl kill all # every daemonIt sends SIGTERM first so nvim can run its exit autocmds and write shada,
waits up to 2 seconds, then escalates to SIGKILL only for processes that
ignored it. Unsaved buffers are lost, so for a tab you still care about,
focus it and quit nvim properly instead.
herdr-nvim-ctl watchRefreshes once a second. Run it in one pane and toggle the sidebar in another
to watch the count change. Ctrl+C to quit.
Herdr calls a tab wR:t2, while its socket file is wR_t2.sock — filenames
can't carry a colon. The two are easy to mix up, and plain herdr tab focus
only accepts the colon form:
❯ herdr tab focus wR_t2
{"error":{"code":"tab_not_found","message":"tab wR_t2 not found"},"id":"cli:tab:focus"}
The script normalizes the argument, so focus and kill take both:
herdr-nvim-ctl focus wR_t2 # pasted from ls or ps
herdr-nvim-ctl focus wR:t2 # tab idNothing is guessed from process names or ordering. The plugin always spawns nvim with:
nvim --headless --listen /run/user/<uid>/herdr-nvim/<workspace>_<tab>.sock ...
The script reads the process command line (/proc/<pid>/cmdline on Linux,
ps on macOS), takes the socket filename, turns _
into : to get the tab id, then looks that up in herdr workspace list and
herdr tab list for the displayed number and label. A tab id missing from
those lists means the tab is closed — an orphaned daemon.
One caveat when scripting against this: ids like wW are stable, but
workspace numbers shift whenever you create or close another workspace. Use
ids in commands; use the numbers to know where to look on screen.
list,focus,gcandwatchnever stop a daemon belonging to an open tab.killis the only command that terminates processes, and only when you ask.- Process matching is
pgrep -x nvim, an exact name match, so the Herdr server, agent panes and unrelated system processes are never in scope. listandfocusrequireHERDR_ENV=1and refuse to run outside a Herdr session rather than acting on someone else's session.
Herdr exposes no command that lists these daemons, so the script infers them from two internal conventions of the plugin:
- nvim is spawned with
--listen <dir>/<workspace>_<tab>.sock; - the socket filename replaces
:with_, matching the plugin's owntab_key().
Both are implementation details, not a committed interface. If a future plugin
release renames its sockets, the TAB column goes blank and kill finds
nothing — patch socket_of_pid and tab_of_pid when that happens. gc is
safe either way: it shells out to the plugin's own herdr-nvim daemon-gc.
MIT