Building a Perfect Shell: Neovim, Avante, Herdr, WezTerm, and a Team of AI Agents¶
Over the past several weeks I rebuilt my Linux desktop and terminal setup from
the ground up, using AI coding agents not just as tools I called on
occasionally but as active participants in the work — reading my own commit
history, fixing regressions they introduced, and diagnosing hardware quirks
that would have taken me hours to track down alone. This is the story of how
Neovim, Avante, Herdr, and WezTerm came together into one coherent setup, and
what it looked like to have Codex, Cursor, and Claude Code all contribute to
the same ~/.dotfiles repository.
The starting point: nine years of accumulated vim¶
My dotfiles repo goes back to 2016. Two hundred and sixty-plus commits trace a slow evolution from raw Vim with Vundle, through PHP and Java tooling, NVM and SDKMAN installers, tmux mappings, and eventually a full switch to Neovim and LazyVim. That history matters, because none of the recent AI-agent work happened in a vacuum — every fix had to respect years of muscle memory (leader keys, undo mappings, clipboard behavior) baked into how I actually use a terminal.
The recent phase of the project had a different goal: stop maintaining a pile of Vim tricks and instead build a reproducible, portable setup — one where an AI agent could read the repo, understand the intent, and safely make changes without me having to hand-hold every step.
Avante: giving Neovim a seat at the agent table¶
The centerpiece of the AI integration is avante.nvim,
configured in nvim/lua/plugins/avante.lua. Avante turns Neovim into an agent
client rather than a plain editor — it can drive an AI coding agent's
Agent-Client Protocol (ACP) directly from inside a buffer.
The setup went through a few iterations of provider work:
feat(nvim): use Codex ACP for Avante— wired up Codex as the primary provider, forcing ChatGPT/browser authentication so no API key needs to live in the dotfiles.feat(nvim): add Cursor provider to Avante— added Cursor as a second, switchable backend (:AvanteSwitchProvider), pointed at the localagent acpbinary.feat(herdr): integrate Avante ACP providers— the piece that ties Avante into Herdr (below), so whichever agent is actually driving Neovim shows up correctly in the terminal UI.
The two providers are declared side by side in acp_providers, each with its
own command, args, and auth_method, so switching from Codex to Cursor
mid-session is a single command rather than a config reload.
Herdr: making agent state visible in the terminal¶
Herdr is the piece that turns "is an agent still
thinking?" from a guess into something visible. It renders a small sidebar of
agent rows per terminal pane, and nvim/lua/config/herdr_avante.lua bridges
Avante into it:
- It wraps
avante.llm.streamso that every time Avante starts or stops streaming a response, Herdr's sidebar flips betweenworkingandidlefor that pane. - It wraps
avante.api.switch_providerso the sidebar's$providertoken updates immediately when I switch from Codex ACP to Cursor ACP. - It releases the agent row cleanly on
VimLeavePre, so closing Neovim doesn't leave a stale "still working" indicator behind.
Everything here degrades gracefully: is_available() checks for
HERDR_ENV, a pane ID, and the herdr binary before doing anything, so the
exact same Neovim config works unchanged outside of a Herdr session.
Herdr itself needed its own fixes along the way — Fix Herdr wrapper state
tracking, Fix Herdr highlighted text contrast, and most recently Tune
Herdr panel contrast colors, which corrected a theme regression where
Herdr 0.8.2 started drawing tab labels in near-white text on a light panel
background, making them unreadable. herdr/config.toml now pins
panel_bg = "reset" and darker custom surface colors specifically to avoid
that.
The clipboard rabbit hole¶
One of the least glamorous but most persistent fights was getting copy/paste
to work correctly across Neovim, Herdr, and a Wayland compositor. The commit
history has an honest trail of attempts: feat(clipboard): fix nvim clipboard
with herdr, Fix Neovim clipboard in HerdR, feat(copy,hyprland): repair
copy-paste from nvim, allow hyprland capture text (untested) (twice — the
first attempt needed a second pass).
The eventual fix, in nvim/lua/config/clipboard.lua, is small but specific:
Neovim's clipboard option is set to unnamedplus, and a TextYankPost
autocommand explicitly pushes yanked text out over OSC 52 using
vim.ui.clipboard.osc52. OSC 52 is the terminal escape sequence that lets an
application set the system clipboard through the terminal emulator itself
rather than talking to a display server directly — which matters a lot when
the "terminal" is a remote SSH session or a pane inside Herdr, neither of
which has a normal path to the Wayland clipboard.
The very last piece of this chain broke by accident: require("config.clipboard")
started returning a bare Lua true instead of a table, and init.lua called
.setup() on it — a one-line regression that took Neovim down with attempt
to index a boolean value until it was caught and fixed as part of a larger
desktop cleanup commit.
WezTerm: from Windows-first config to a portable, host-aware setup¶
.wezterm.lua started life heavily Windows-oriented — Visual Studio dev
shells, MSYS2 launch menu entries, WSL profiles. Getting it to behave well as
the default terminal on a CachyOS/Hyprland desktop took several rounds:
feat(wezterm): add wezterm config,define better keys,add linux zsh launcher— the initial Linux-side config.Hide WezTerm tab bar for single tab— small ergonomics.Fix WezTerm behavior on Hyprland— made WezTerm respect native-Wayland vs. XWayland behavior per host, since a fresh GUI process can't inherit an already-running XWayland backend.- Custom mouse bindings to keep Herdr's terminal mouse-reporting (which it needs for its own UI) from stealing middle-click primary-selection paste — WezTerm now intercepts only the middle button while mouse reporting is active, leaving left/right clicks, drags, and the wheel to Herdr.
The most recent WezTerm fix is a good example of what "having an AI agent
that reads its own history" actually buys you: a repo-wide history
sanitization pass (removing personal hostnames, IPs, and emails from every
commit) had replaced the real hostname in .wezterm.lua's
enable_wayland check with the literal placeholder string "example-host"
instead of moving it into the ignored .env file, the way every other
host-specific value in the repo is handled. The comparison silently always
evaluated false, so WezTerm had been quietly falling back to XWayland the
entire time. On a 4K display running at 1.5x fractional scaling, XWayland
renders at 1x and gets upscaled by the compositor — which shows up as visible
pixelation, most noticeably in a dense terminal UI like Herdr's. The fix
mirrors the pattern already used in hyprland.lua: read the intended
hostname from .env's CACHYOS_HARDWARE_HOSTNAME, compare it against
wezterm.hostname(), and only enable native Wayland — and, while at it, a
~19% smaller font size tuned for this display — on the one machine it's meant
for.
The CachyOS/Hyprland desktop, built to be reproducible¶
In parallel, the desktop itself became a proper reproducible install rather than a pile of manual tweaks:
Add CachyOS Hyprland desktop setupandfeat(cachyos): make desktop setup reproducible— Hyprland, Waybar (styled after Omarchy's top bar, with an OS-detecting launcher script so the same config renders correctly whether it's running under CachyOS or another distro), hyprlock, and a daily wallpaper picker that chooses one random image per calendar day and keeps it stable until the next.- OCR screen capture (
capture-text), reproducing Omarchy'sgrim → hyprpicker/slurp → tesseract → wl-copyflow as a portable script and installer package list, bound toCtrl+Super+Print. - A persistent per-user SSH agent as a
systemd --userservice, so the key passphrase is entered once per login rather than once per terminal. feat(hypr): load hardware layouts by hostname— monitor layouts and GPU workarounds now live in per-machine profile files (hypr-cachyos/hosts/<hostname>.lua), keyed off/etc/hostname, so installing on a second machine can never silently inherit this machine's physical monitor arrangement.security(dotfiles): move host values to ignored envand a fullgit filter-repohistory rewrite — Remmina profiles, Git identity, and local IPs were pulled out into an ignored.env(with a fake.env.examplechecked in), and the rewrite scrubbed the same values out of every prior commit before the branch was ever pushed publicly.
Where the agents disagreed with each other (and with physics)¶
A few of the more interesting fixes in this repo exist specifically because different agents, working at different times, changed the same machine:
- Waybar kept crashing. Adding automatic idle-lock behavior via
hypridle(lock after 5 minutes idle, suspend after 15) is good security practice, but the first version also cycled the display off and back on withhyprctl dispatch dpms off/on. On this machine's NVIDIA GPU, that DPMS cycle reliably broke Waybar's Wayland connection (Error 22, dispatching to Wayland display), killing the bar a few minutes into every idle period. The fix dropped the DPMS listener and kept only the lock and suspend timeouts — the lock screen already covers the actual security goal. - WezTerm crashed on wake from suspend. After enabling the idle-suspend
behavior above, WezTerm occasionally greeted a resume with
running message loop: Io error: Resource temporarily unavailable (os error 11). The real cause wasn't WezTerm at all:nvidia-suspend.service,nvidia-resume.service, andnvidia-hibernate.service— the driver's own hooks for saving and restoring GPU state across suspend — were disabled by default.install-hyprland-cachyos.shnow enables all three automatically whenever it detects an NVIDIA install, so a fresh setup doesn't inherit the same multi-day-old crash. - A security fix got quietly reverted.
hypr/xdph.conf'sallow_token_by_defaultwas flipped fromtruetofalse(requiring an explicit prompt before any app can screen-share or screenshot), then later found reverted back totrueon disk by a separate, unreviewed change — a reminder that "portable and reproducible" also means periodically re-checking that a fix actually stuck.
The throughline¶
None of this reads as a single clean plan executed start to finish — it reads
like what it actually was: a running conversation across multiple AI agents
and multiple sessions, each one picking up from git history and live system
state rather than from a shared memory of "what we already decided." Avante
and Herdr made that conversation visible from inside Neovim; the .env +
per-hostname profile pattern made it safe to keep experimenting on one real
machine without ever risking another; and the two funniest bugs in the whole
project — Waybar dying on DPMS, WezTerm dying on suspend — were both cases
where fixing one problem (idle security) quietly created another, which is
exactly the kind of thing that's easy to miss when a human makes one change at
a time, and much easier to catch when an agent can grep its own recent
history, correlate a crash timestamp against journalctl, and go looking for
the real root cause instead of the first plausible one.
This article summarizes changes tracked in ~/.dotfiles on the
feature/portable-linux-bootstrap branch, current as of commit 87e5fd6.