2026-08-05 Claude Follow-Mode Split the Tabpage Root Instead of Claude’s Window
What I set out to do
A one-line bug report: with Claude pulled up in follow mode, the file buffer lands on the left and Claude gets pushed to the right. The tricky part was that buffer-left/Claude-right is also the intended layout, so the report only makes sense once you find a case where the file window arrives somewhere it has no business being.
What I actually did
Traced it to the PostToolUse hook neovim-open-file.sh, which shows every file Claude edits while the terminal holds focus. When the tabpage has no ordinary file window it created one with nvim_open_win(fbuf, false, { split = "left", win = -1 }). win = -1 does not mean “left of the current window” — it splits the tabpage root, inserting a new outermost column. With anything else on screen that is not a file buffer (a sidebar, a second terminal), the file jumps to the far left of the whole screen and every existing column, Claude included, shifts right.
Probed it headlessly before changing anything, which is where it became unambiguous. Starting from row{ sidebar, claude }, the hook produced row{ file, sidebar, claude }. Anchoring to Claude’s own window instead gives row{ sidebar, file, claude }. Same two-window screenshot in the degenerate single-window case, which is why it had gone unnoticed for so long.
Pulled the window targeting out of the bash heredoc into nvim/lua/user/claude_window.lua. Partly because lua embedded in a shell string cannot be tested, and this repo’s protocol is baseline → fix → verify; partly because the VimResized autocmd had independently reimplemented the split_width_percentage lookup to restore Claude’s width after wincmd =. Both callers want the same two answers — which window is Claude’s, and how wide it should be — so both now ask the same module.
The fix itself: split the file window out of Claude’s own window, on the side away from Claude’s configured split_side, then hand Claude its configured width back (the split alone leaves both at half the box). Claude keeps its edge of the screen and nothing else in the layout moves.
Two adjacent bugs fell out of reading the same function. find_normal_win iterated nvim_list_wins(), which spans all tabpages, so it could hand back a window in a tab the user cannot see and nvim_win_call would quietly edit the file there. Now scoped to the current tabpage.
The second one the end-to-end run caught, and I would not have found it any other way. find_claude_win goes through claudecode.terminal.get_active_terminal_bufnr(), and when that returns nothing the anchor falls back to -1 — straight back into the original bug. The e2e harness reproduced it by accident: its stand-in terminal was a real terminal but not a claudecode-registered one, and the layout came back file | SIDEBAR | CLAUDE, exactly the symptom I had just “fixed”. Added a fallback to the focused window when it is a terminal, which is sound precisely here because the hook only runs while Claude’s terminal has focus.
What was striking
The unit spec was green while the real hook still reproduced the bug. Not a bad test — it asserted the right thing about the code path it exercised — but it fed the anchor in directly and so never touched the discovery step that was failing. The registry lookup was the one part no unit test could cover, and it was the part that broke.
Also worth remembering for any future headless nvim work: vim.o.columns is settable in a headless instance and completely decoupled from actual window widths. Set it to 200 and windows still report 80. Width assertions are meaningless without a UI attached; winlayout() ordering is the only thing that survives. That is why the spec asserts order and the e2e (running against a real nvim) is what confirmed Claude lands at exactly 40%.
Verification
New headless spec nvim/tests/claude_window_spec.lua, 21 checks, confirmed failing against the old anchoring by temporarily reverting win and the hardcoded side — 4 targeted failures, the rest green. End-to-end: the real hook, both the Write and Edit branches, driven over RPC against a real nvim loading the real config, went from file | SIDEBAR | CLAUDE to SIDEBAR | file | CLAUDE with Claude at 32 of 80 columns. just check passes; hm switch applied and the nix-store copy of the hook no longer contains win = -1.
Related
2026-07-21 Neovim Claude Integration Fixes · 2026-07-02 Neovim 0.12 Config Fixes and lua_ls Split-Root