Floats or docks — stays anchored to the code.
A dedicated Neovim window for Claude Code or Codex, with live editor context on every prompt.
- The official agent TUI, docked in Neovim. Run Claude Code or Codex in a split or float that stays anchored beside your code — no chat UI to maintain.
- Context on every prompt. A prompt hook automatically attaches the current file, cursor, visual selection, and open buffers to what you send — no tool call, no extra round trip.
- The agent reads back. buoy's private CLI lets the agent pull targeted buffer ranges and diagnostics, or move your cursor to a resolved location, mid-turn.
- Layout that adapts. Auto-picks a right-side split while there is room, a floating overlay once the editor gets narrow, and follows editor resizes. Either layout can be pinned explicitly.
- Neovim 0.11+ (required for exact visual-selection capture)
- The Codex and/or Claude Code CLI on your
$PATH
buoy.nvim runs inside Neovim, so installing it means cloning it where Neovim
looks. Neovim loads anything in its built-in pack/*/start/ folder
automatically at startup, and buoy configures itself with sensible defaults on
first load. On Linux and macOS, the clone is the only step: buoy enriches every
prompt with live editor state and attaches its private CLI when it launches the
agent (see Live editor bridge).
Linux/macOS:
git clone https://github.com/cmfcruz/buoy.nvim \
~/.local/share/nvim/site/pack/buoy/start/buoy.nvimWindows (PowerShell):
git clone https://github.com/cmfcruz/buoy.nvim `
"$env:LOCALAPPDATA\nvim-data\site\pack\buoy\start\buoy.nvim"Start Neovim, open any file, and press <F2> — the selected agent's TUI
opens beside the editor. (buoy auto-detects which agent CLI is on your $PATH,
preferring Claude Code; no config file required.)
On Windows, the terminal UI works normally, but buoy does not attach the POSIX prompt hook or live editor CLI. Buoy warns once when the agent starts.
To update buoy later, pull the clone:
git -C ~/.local/share/nvim/site/pack/buoy/start/buoy.nvim pullOn Linux and macOS, the agent's view of your live editor state needs no extra setup — it is configured at launch; see Per-prompt context enrichment and Live editor bridge for how it works.
buoy works with zero configuration: it auto-detects your agent CLI (Claude
Code first, then Codex) and maps <F2>. Call setup() only to override a
default — put it in your init.lua (~/.config/nvim/init.lua, or
~/AppData/Local/nvim/init.lua on Windows):
require("buoy").setup({
agent = "codex", -- pin the agent: "auto" (default) | "claude" | "codex"
keymaps = {
primary = "<F2>", -- focus in a vsplit, show/hide in a float; false to disable
secondary = "<S-F2>", -- show/hide in a vsplit, focus in a float; false to disable
},
-- cmd = "codex", -- override the agent binary if it isn't on $PATH by name
window = {
style = "auto", -- "auto" (default) | "vsplit" | "float"
width = 80, -- fixed columns of text for the agent (integer, minimum 40)
border = "rounded", -- used by the floating window
stay = false, -- keep the agent split open after all other windows close
},
context = {
expose_buffers = true, -- let the agent read live buffer contents (get_buffer_range)
expose_diagnostics = true, -- let the agent read buffer diagnostics (get_diagnostics)
expose_editor_context = true, -- attach the per-prompt editor snapshot + selection handoff
},
})- Switch to Codex: set
agent = "codex". (With the default"auto", buoy uses Codex anyway if it's the only CLI on your$PATH.) - Override per session: set the
BUOY_AGENTenvironment variable (e.g.BUOY_AGENT=codex nvim) — it takes precedence over theagentconfigured insetup(). - Config applies at startup: buoy initializes once per Neovim session —
the first
setup()(or the zero-config defaults, shortly after startup) wins, and later calls only warn. Callsetup()during startup rather than from a deferred hook, edit + restart Neovim to change the configuration, and useBUOY_AGENTfor a one-off agent switch. - Layout-aware keys: the two mappings are named by role —
primaryandsecondary— rather than by action, because the action each performs depends on the agent's layout. The primary key is always the one you reach for: in avsplitthe agent is always visible, so it just moves focus; in afloatit overlaps your code, so it shows/hides the window instead. The secondary key does the other. When the agent is closed, the layout it would open into decides.- Primary —
<F2>(keymaps.primary): focus-switch between the terminal and your last window in avsplit; show/hide the window in afloat. Opens the agent when it's closed. - Secondary —
<S-F2>(keymaps.secondary): the other action — show/hide in avsplit, focus-switch in afloat. Hiding never kills the agent session. If your terminal emulator doesn't deliver<S-F2>, setkeymaps.secondaryto another key; either mapping can also befalse.:BuoyToggleshows or hides the window, while:Buoyopens or focuses it.
- Primary —
- Window layout:
"auto"(default) chooses a right-sidevsplitwhile every code window would stay wider thanwindow.width, otherwise afloatoverlay so your code is never squeezed below the agent's own width. The decision reads the actual window layout, not just the editor width: a tab already divided into columns floats where a tab holding one full-width buffer splits, and horizontal splits still get avsplitbecause they keep their full width. Setwindow.styleto"vsplit"or"float"to pin one.window.widthis a fixed column count applied to both layouts (clamped to fit a narrow editor); it must be an integer of at least 40, andsetup()raises a configuration error otherwise.window.borderapplies only to the floating window. While the agent is open, resizing the editor keeps it in step: an"auto"window flips between split and float as it crosses the width boundary — reusing the running agent session — and a float stays anchored to the resized editor. The split holds its column count against<C-w>=and new splits (it setswinfixwidth), and rearranging your own windows never relayouts the agent; the layout is chosen when it opens and re-evaluated when the editor resizes. - Close with the last window: by default buoy quits the agent split once it
is the last ordinary window in its tabpage — on the final tab, that quits
Neovim, mirroring file-tree plugins like neo-tree, so a
vsplitagent window never lingers after you close your other windows. Setwindow.stay = trueto keep it open instead; if you then hide an agent that has outlived every other window, buoy restores an ordinary window beside it first, so hiding always hides. Floats are unaffected (they never strand an ordinary window). - Limit what the agent sees: the
contextswitches gate buoy's agent-facing surfaces, all enabled by default. Setexpose_buffers = falseto disableget_buffer_range(live buffer contents),expose_diagnostics = falseto disableget_diagnostics, andexpose_editor_context = falseto drop the per-prompt editor snapshot and the visual-selection handoff. A disabled capability is both omitted from the agent's instructions and refused if called. Turn all three off to disable buoy's buffer-content, diagnostic, and per-prompt context surfaces. This is a capability/privacy control, not a security boundary — a hosted agent can still read files through its own tools. Cursor navigation (set_cursor_position) is always available; when invoked, it returns navigation metadata including the destination's absolute path and final cursor position, but not file contents. - Every key is optional; anything you omit keeps its default.
On Linux and macOS, buoy registers a UserPromptSubmit hook
(bridge/context_hook.lua) with both agents: before the model sees each prompt,
the hook prints a focused snapshot of your editor state — cwd, current file,
cursor, visual selection, and open buffers — which the agent attaches as
context. Enrichment is deterministic (there is no tool call for the model to
skip) and costs no extra inference round trip.
For Claude Code the hook rides in an inline --settings JSON. For Codex it is
a session-scoped -c hooks.UserPromptSubmit=... override. Codex requires you
to review and trust the hook definition before it can run, then remembers that
trust while the definition stays unchanged.
If the hook cannot reach your Neovim it prints nothing and never blocks the prompt.
┌─ Neovim ──────────────────────┬─ Agent (official TUI) ─┐
│ editing buffers │ › what does this │
│ autocmds cache: │ selection do? │
│ file / cursor / selection │ │
│ ▲ │ [context_hook enriches│
│ │ msgpack-RPC │ every prompt with the│
│ ┌─────┴──────────────┐ │ private agent CLI │
│ │ context_hook / │ │ widen context │
│ │ agent_cli │◄──────┤ mid-turn] │
│ └────────────────────┘ │ │
│ (spawned by the agent) │ │
└───────────────────────────────┴────────────────────────┘
On Linux and macOS, buoy gives the agent a compact command prefix for its
private bridge/agent_cli.lua adapter. The agent invokes it through its normal
shell tool when it needs a live buffer range, diagnostics, or an explicitly
requested cursor jump. The CLI connects only to the Neovim session that
launched the terminal, returns one bounded JSON object, and follows the agent's
normal shell approval policy. It is an internal integration surface, not a
globally installed user command.
buoy automatically adds its Neovim context guidance when it launches the
agent. For Claude Code it uses --append-system-prompt. For Codex, buoy first
asks codex app-server for the effective developer_instructions at Neovim's
working directory, then appends its guidance without changing Codex's normal
configuration precedence.
If the Codex configuration cannot be resolved within two seconds, buoy shows a warning and launches Codex without a developer-instructions override. This preserves the instructions Codex would normally load instead of replacing them with incomplete context. The prompt hook remains active, but on-demand live operations are unavailable for that session; buoy does not retry configuration resolution.
<F2>opens the window. Once it's open the keys are layout-aware: in avsplit,<F2>switches focus between the agent and your code while<S-F2>shows/hides the window; in afloatthose swap, so<F2>shows/hides and<S-F2>switches focus.:Buoyalways opens or focuses the agent and:BuoyTogglealways shows or hides it; the agent session survives hiding.- Edit normally, select code in visual mode, then bring up the agent from the
selection with
:Buoy(or the focus key for your layout). Both preserve the handoff selection, so the next prompt automatically carries its range and text.
- On Linux and macOS, editor context refreshes when you submit a prompt and when the agent invokes the private CLI; buoy does not stream selection-changed events continuously.
- Windows supports the terminal UI but not the prompt hook or live editor CLI.
open_diff/ in-editor approval is intentionally out of scope: the official TUI already renders diffs and approvals, which is the point.
Contributions go through pull requests; main is protected by CI.
-
Tests — run every headless spec with:
for spec in tests/*_spec.lua; do nvim --headless -u NONE -i NONE -l "$spec" done
-
Formatting — StyLua. Run
stylua .(orstylua --check .to verify). -
Linting — Selene. Run
selene .. The Neovim runtime is described invim.yml. -
Pre-commit —
pip install pre-commit && pre-commit installwires StyLua and a few hygiene hooks into your commits (StyLua's binary is fetched automatically; install Selene separately if you want it locally).
CI (.github/workflows/ci.yml) runs the tests, formatting check, and lint on
every PR.
Versioning is automated with
Release Please using
Conventional Commits. Merging
feat: / fix: commits to main opens a release PR that bumps
version.txt, updates the changelog, and — once merged — tags the
release. Use feat:/fix: in commit subjects (and feat!: or a
BREAKING CHANGE: footer for breaking changes).

