Lightweight Neovim integration for OpenAI Codex CLI. Opens the Codex TUI in a terminal split.
This plugin follows the structure of base.nvim and references ideas from claudecode.nvim, but is intentionally minimal.
- Codex CLI installed and on PATH:
npm i -g @openai/codexorbrew install codex - Optional:
gitfor project root detection
require('codex').setup()
-- Example mappings
vim.keymap.set('n', '<leader>co', function() require('codex').open() end, { desc = 'Codex: Open TUI' })
vim.keymap.set('n', '<leader>ct', function() require('codex').toggle() end, { desc = 'Codex: Toggle terminal' })- Log Level:
log_level = 'trace'|'debug'|'info'|'warn'|'error'(case‑insensitive). Also supportsCODEX_LOG_LEVELenv var. - Log to File: set
log_to_file = trueto write logs to a fixed tmp file. The file is truncated on every setup so you always get a fresh log for a new debug session.- Default path:
$TMPDIR/codex.nvim.log(uses your OS tmp dir; on macOS/Linux typically/tmp/codex.nvim.log). - Override path: set
log_file = '/custom/path/codex.nvim.log'or provide a string value tolog_to_file.
- Default path:
Example:
require('codex').setup({
log_level = 'trace',
log_to_file = true, -- writes to $TMPDIR/codex.nvim.log and truncates on setup
-- log_to_file = '/tmp/codex.nvim.log', -- custom path (also truncates)
})- Direction: 'horizontal' (bottom/top split) or 'vertical' (left/right split).
- Size: split height/width.
- Number >= 1: absolute rows (horizontal) or columns (vertical).
- 0 < Number < 1: fraction of your current window (not full screen).
- Example: 0.33 → one-third of the current window.
- Position: 'left' | 'right' | 'top' | 'bottom'. When omitted, respects your
splitright/splitbelowsettings. - Provider: 'native' | 'snacks' | 'auto'.
- 'native': use Neovim splits.
- 'snacks': use
folke/snacks.nvimterminal. Errors if Snacks is missing (falls back in practice, with a warning). - 'auto': use Snacks when available, otherwise silently fall back to native.
- Reuse: reuse the previous terminal buffer if present.
- Auto Insert Mode: enter insert mode automatically after opening.
- When set to false, the terminal opens without taking focus and stays in normal mode (both native and Snacks providers).
- Fix Display Corruption: schedule a
redraw!after opening to remedy rare artifacts.
-- Vertical split with 40% of the current window width
require('codex').setup({
terminal = { direction = 'vertical', size = 0.40 }
})
-- Horizontal split with 12 rows
require('codex').setup({
terminal = { direction = 'horizontal', size = 12 }
})
-- Explicit side + fraction
require('codex').setup({
terminal = { direction = 'vertical', position = 'right', size = 0.25 }
})
-- Auto provider: Snacks if installed, else native
require('codex').setup({
terminal = { provider = 'auto', direction = 'horizontal', size = 0.33 }
})- Fractional Size: calculated against the current window before splitting, for both native and Snacks providers.
- Toggle:
:CodexToggletoggles the most recent Codex terminal (works with both providers). - Reuse: when enabled, new runs reattach to the existing terminal buffer and start the command again.
- Headless: in headless (no UI) mode, commands run as background jobs without creating splits.
- Compatibility: legacy
split_width_percentageis accepted as an alias forterminal.sizewhensizeis not set. - Logging to File: when enabled, the plugin truncates the log file on setup and appends new entries with timestamps; warn/error also go through
vim.notify.
:CodexOpen [prompt]— Open Codex TUI (optionally seeded with an initial prompt):CodexToggle— Toggle Codex terminal split
Send file paths and code selections directly to an open Codex terminal:
:CodexSendPath— Send current buffer path to attached terminal with space separator for continuous sending:CodexSendSelection— Send visual selection as reference or content based on configuration:CodexSendReference— Send visual selection as file reference with space separator (e.g.,@file.lua#L10-L20):CodexSendContent— Send visual selection with actual code content
Usage Examples:
" Send current file path
:CodexSendPath
" Visual mode: select code then send reference
:'<,'>CodexSendSelection
" Send specific line range as reference
:187,188CodexSendReferencecodex.nvim can optionally use folke/snacks.nvim for opening the terminal with its window manager.
- Install and set up
folke/snacks.nvimin your config. - Set
terminal.provider = 'snacks'to always use Snacks, orterminal.provider = 'auto'to use Snacks only when it’s available (falls back to native splits). terminal.direction,size, andpositionare respected:direction = 'horizontal'uses a bottom/top split;sizeapplies to height.direction = 'vertical'uses a left/right split;sizeapplies to width.positioncan be'left' | 'right' | 'top' | 'bottom'. When omitted, yoursplitright/splitbelowsettings decide the side.- When
sizeis< 1, it’s treated as a fraction of your current window (not the full screen), for both native and Snacks providers.
Example with Snacks:
require('codex').setup({
terminal = {
provider = 'snacks',
direction = 'vertical',
size = 0.35, -- 35% of the current window width
position = 'right', -- or 'left', 'bottom', 'top'
}
})Tip: legacy split_width_percentage is still accepted as an alias for terminal.size when size is not provided.
- Job Exit Alert and Idle Completion Alert: system notification on completion.
require('codex').setup({
-- Exactly one of these is recommended for clarity:
alert_on_idle = true, -- notify when terminal output becomes idle (job keeps running)
-- alert_on_exit = true, -- notify when the job exits
notification = {
enabled = true,
sound = 'Glass', -- macOS notification sound name
include_project_path = true, -- include project/cwd in the message
speak = false, -- off by default (no TTS)
backend = 'terminal-notifier', -- prefer terminal-notifier on macOS
terminal_notifier = {
ignore_dnd = true, -- pass -ignoreDnD
sender = 'com.apple.Terminal',
group = 'codex.nvim',
activate = 'com.apple.Terminal',
},
-- Voice is only used if speak=true
-- voice = 'Samantha',
-- Idle detection tuning (used when alert_on_idle=true)
idle = {
check_interval = 1500, -- ms between checks
idle_checks = 3, -- consecutive no-change checks to consider idle
lines_to_check = 40, -- how many tail lines to hash
require_activity = true, -- require seeing output changes before considering idle
min_change_ticks = 3, -- require at least N changes before eligible
},
},
})Behavior:
- macOS: prefers
terminal-notifierfor native banner + sound; falls back toosascriptif available; otherwise usesvim.notify. - Other OS: falls back to
vim.notify(no system sound). - Idle alert is one-shot: once notified, the idle monitor stops until you re-run Codex.
- Exit alert is de-duplicated: suppresses repeated success notifications within a short window.
- Idle alert suppression: if the tail content contains cancellation markers (e.g., "Request interrupted by user", "Canceled", "取消"), no notification is sent. Configure via
notification.idle.cancel_markers.- Included by default: "🖐 Tell the model what to do differently" and its plain variant without the emoji.
Idle Parameters Explained:
check_interval(ms): polling interval to sample terminal tail content.idle_checks: number of consecutive identical samples to consider idle.lines_to_check: how many tail lines are included in the hash comparison.require_activity: only consider idle after seeing at least one change.min_change_ticks: require at least N changes before an idle period can trigger.
Tip: Prefer enabling either alert_on_idle or alert_on_exit to avoid redundant signals. Internally, duplicate notifications are still suppressed for safety.
Chinese Documentation: see README.zh-CN.md.
The terminal bridge allows sending file paths and code selections directly to an open Codex terminal:
require('codex').setup({
terminal_bridge = {
path_format = 'abs', -- 'abs' | 'rel' | 'basename' - format for file paths
path_prefix = '@', -- prefix added to paths (e.g., '@' for Claude Code)
auto_attach = true, -- automatically attach terminals created by CodexOpen
selection_mode = 'reference', -- 'reference' | 'content' - default for visual selections
},
})Configuration Options:
path_format: How file paths are formatted:'abs': Absolute paths (e.g.,@/Users/name/project/file.lua)'rel': Relative to current working directory (e.g.,@file.luaor@src/module.lua)'basename': File name only (e.g.,@file.lua)
path_prefix: String prepended to paths (typically'@'for Claude Code compatibility)auto_attach: Whentrue, terminals opened by:CodexOpenare automatically attached to current tabselection_mode: Default behavior for:CodexSendSelection:'reference': Send file reference like@file.lua#L10-L20'content': Send reference + actual code content
Tab Isolation: Each tab maintains its own terminal connection, preventing confusion when working with multiple tabs.
Workflow:
- Open Codex terminal:
:CodexOpen(automatically attaches to current tab) - Send file path:
:CodexSendPath(adds space after path for continuous input) - Select code and send reference:
:'<,'>CodexSendSelection
Note: Path and reference commands append a space instead of newline, allowing multiple paths/references to be sent on the same line. For example, executing :CodexSendPath twice will result in @file1.lua @file2.lua on the same line.
Example Key Mappings:
-- Terminal bridge shortcuts
vim.keymap.set('n', '<leader>cp', ':CodexSendPath<CR>', { desc = 'Codex: Send file path' })
vim.keymap.set('v', '<leader>cs', ":'<,'>CodexSendSelection<CR>", { desc = 'Codex: Send selection' })
vim.keymap.set('v', '<leader>cr', ":'<,'>CodexSendReference<CR>", { desc = 'Codex: Send reference' })- Root detection defaults to the Git repo root. Override via
cwd_provider = 'cwd'or'file'. - Environment variables and extra args can be passed via
envandextra_argsin setup. - For Codex CLI usage and flags, see: https://github.com/openai/codex (README and docs)
- Requirements:
nvimavailable on PATH. - Tests run headless and do not require the real Codex binary (they use
echo).
Run all tests:
make test- Error:
Invalid argument: envwhen using Snacks provider- Cause: some environments or older Neovim builds do not accept an empty
envoption through terminal APIs. - Fix: codex.nvim now omits
envwhen empty and sanitizes values to strings. If you explicitly setenv, ensure it is a table of string→string pairs. Consider upgrading Neovim if the issue persists.
- Cause: some environments or older Neovim builds do not accept an empty