Skip to content

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 local agent acp binary.
  • 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.stream so that every time Avante starts or stops streaming a response, Herdr's sidebar flips between working and idle for that pane.
  • It wraps avante.api.switch_provider so the sidebar's $provider token 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 setup and feat(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's grim → hyprpicker/slurp → tesseract → wl-copy flow as a portable script and installer package list, bound to Ctrl+Super+Print.
  • A persistent per-user SSH agent as a systemd --user service, 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 env and a full git filter-repo history rewrite — Remmina profiles, Git identity, and local IPs were pulled out into an ignored .env (with a fake .env.example checked 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 with hyprctl 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, and nvidia-hibernate.service — the driver's own hooks for saving and restoring GPU state across suspend — were disabled by default. install-hyprland-cachyos.sh now 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's allow_token_by_default was flipped from true to false (requiring an explicit prompt before any app can screen-share or screenshot), then later found reverted back to true on 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.