A vim-native alternative frontend for Claude Code.
Run Claude Code from any directory on your machine, pick any prior session from any project, and drive it from inside neovim — with real vim motions, buffer-level copy/paste, per-session tabs, and a status bar that shows context usage and git branch.
Mental model: claude.nvim is a TUI replacement, not a code-assist sidebar. A Claude session fills its own tabpage (transcript + prompt); opening a file from inside that tab pops the file into a new tab so the chat layout is never disturbed. If you want "AI in a split alongside my code", use avante.nvim or codecompanion.nvim — they serve a different workflow.
cclauncher — run it from any shell, anywhere. You get a fuzzy picker across every Claude Code session on your machine, each row showing the project it belongs to, when you last touched it, and its first prompt.- Real vim motions in the prompt. The input is a normal nvim buffer;
ciw/./macros/registers/LSP/snippets all just work. - Clean copy from responses. Transcript uses
wrap + linebreak, so visual-selecting and yanking returns the original unwrapped text — no more stripping terminal-wrap linebreaks by hand. - Per-session tabs. Each pick opens in its own tab; switch with native
gt/gT. Tabline labels tabs by project cwd. - Shares sessions with the official CLI. Reads and writes the same
~/.claude/projects/…/*.jsonlstore, so a session you start in nvim is visible inclaude -cand vice versa. - Compact tool-call rendering. Each call shows as a muted italic
one-liner (
↳ Bash ls -la); output is hidden unless it's an error. - User-turn styling. User messages get a subtle background tint, bold
weight, and a
»prefix. Claude's replies are plain. Visually distinct turns without cluttering the view with labels. - Animated turn indicator. Right-aligned spinner + phase + elapsed time + queued count, rendered as a virt_text extmark on the prompt pane (off the statusline, so it doesn't flicker).
- Message queueing. Press send while a turn is in flight — your message lands in the transcript immediately and auto-fires when the previous turn completes.
- Winbar at the top of each Claude window shows: git branch, context %, optional 5-hour subscription usage %. No session id noise.
- Interactive permission prompts. Optional: route tool-permission decisions into a nvim modal instead of auto-accepting.
- Cross-pane navigation.
kat the top of the prompt jumps to the transcript;jat an empty bottom of the transcript jumps back to the prompt.<leader>ca/<leader>ctalso work. - Prompt history.
K/Jin normal mode cycles through previously sent messages in the prompt buffer (shell-style: up = older, down = newer). Per-tab, last 100 entries. @-mention file completion. Typing@in the prompt pops the native completion menu with file paths from the session cwd (git ls-fileswhen available,vim.fs.dirfallback). The resulting@pathis what the Claude CLI reads as a file reference.- Slash-command picker.
<leader>/opens a filterable picker over the commands we can actually emulate client-side (/clear,/model,/cost,/memory). Type to filter,<CR>to insert. The CLI's-pmode doesn't interpret slash commands on stdin, so the picker is scoped to things we can run ourselves.
- Neovim ≥ 0.10 (uses
vim.system,vim.uv, modern extmark API) - Claude Code CLI on
$PATH(claude --versionshould work) - Python 3 (only for the permission-forwarding hook; stdlib-only)
- Optional:
- telescope.nvim — for
the session picker (falls back to
vim.ui.selectotherwise) - oil.nvim or neo-tree.nvim — for the file pane (falls back to netrw)
- telescope.nvim — for
the session picker (falls back to
{
"your-fork/claude.nvim",
cmd = { "Claude", "ClaudePick", "ClaudeNew", "ClaudeQuit" },
dependencies = {
"nvim-telescope/telescope.nvim", -- optional
},
opts = {
-- see "Configuration" below
},
}use {
"your-fork/claude.nvim",
requires = { "nvim-telescope/telescope.nvim" },
config = function() require("claude").setup({}) end,
}git clone https://github.com/your-fork/claude.nvim ~/code/claude.nvimvim.opt.rtp:append("~/code/claude.nvim")
require("claude").setup({})Symlink it onto your $PATH:
ln -s ~/code/claude.nvim/bin/cc ~/.local/bin/cccc is a self-bootstrapping shell script — it invokes nvim with the
plugin's runtimepath appended, so it works even if you haven't wired
claude.nvim into your nvim config. If you have wired it, the duplicate
rtp entry is a no-op.
⚠
ccis also the conventional name for the system C compiler on Unix-likes (/usr/bin/cc). If you compile C code, this symlink shadows it — either useclang/gccdirectly, or rename to something else (e.g.ln -s ...bin/cc ~/.local/bin/ccedto keep the old name).
$ cc # anywhere on disk — picker across all sessionsFrom inside nvim:
:Claude— open the picker:ClaudePick— alias; opens in a new tab if a different session is picked:ClaudeNew— new session in current cwd:ClaudeSend— send the prompt buffer contents:ClaudeYankBlock,:ClaudeYankLast— copy helpers:ClaudeQuit— close all Claude tabs and exit nvim
| Keys | Action |
|---|---|
<CR> (prompt, normal) |
Send message |
<C-c> (prompt) |
Interrupt in-flight turn |
<leader>ca |
Focus the prompt (enter insert) |
<leader>ct |
Focus the transcript |
i/a/o/I/A/O (transcript) |
Jump to prompt + insert (read-only redirect) |
]m / [m (transcript) |
Jump next / prev message |
<CR> (transcript, on a tool-call line) |
Peek the referenced file in a float |
<leader>cs |
Open picker (new tab) |
<leader>cn |
New session in cwd |
<leader>cc |
Close this Claude tab |
<leader>cq |
Close all + quit nvim |
<leader>cy |
Yank last assistant reply |
gy (transcript) |
Yank fenced code block under cursor |
gt / gT |
Switch between Claude tabs (vim native) |
K / J (prompt, normal) |
Older / newer prompt history |
@ (prompt, insert) |
Trigger @-mention file completion |
<leader>/ (prompt, normal) |
Slash-command picker |
All of these are configurable; see below.
The transcript renders each tool call as a single ↳ Read/Write/Edit path
line. Put the cursor on any such line and press <CR>: the referenced
file opens in a centered read-only floating window with syntax
highlighting. <Esc> or q dismisses it. The chat keeps streaming
underneath. Only Read, Write, and Edit tool calls are peekable
today — those are the ones with an unambiguous file_path.
The transcript and prompt windows refuse to host anything but themselves.
If a telescope pick, :edit, gf, or any other flow tries to load a
non-chat buffer into one of them, the buffer is automatically diverted
into a new tab so the chat layout stays intact. Switch to it with gt
and back with gT.
Every option lives under require("claude").setup({...}) (or opts = {...}
in lazy.nvim). Defaults:
require("claude").setup({
-- Claude CLI / subprocess
claude_bin = "claude",
dangerously_skip_permissions = true, -- pass --dangerously-skip-permissions
permission_mode = "acceptEdits", -- fallback when both perms off
model = nil, -- nil inherits CLI default
-- Session discovery
projects_dir = vim.fn.expand("~/.claude/projects"),
title_max_len = 80,
-- Layout (fractions of available space)
layout = {
prompt_height = 0.33,
prompt_height_min = 6,
},
-- Role markers. Only errors get a gutter bar by default; user/assistant
-- rely on background tint / italic + inline prefix for differentiation.
signs = {
char = "▎",
user = nil, -- nil = no gutter bar for that role
assistant = nil,
tool = nil,
error = "ClaudeErrorSign",
user_prefix = "» ", -- inline prefix on each user turn
},
tool_output_max_lines = 14,
-- Winbar / tabline / subscription
tabline = true,
subscription_usage = true, -- 5h quota in the winbar
-- Interactive permissions (off by default; when on, supersedes
-- dangerously_skip_permissions and routes every listed tool through a
-- nvim confirm)
ask_permissions = false,
permission_tools = { "Bash", "Write", "Edit" },
permission_always_allow = {},
-- Keymaps (set any to false or "" to disable)
keymaps = {
send = "<CR>",
interrupt = "<C-c>",
focus_prompt = "<leader>ca",
focus_transcript = "<leader>ct",
pick = "<leader>cs",
new_here = "<leader>cn",
yank_last = "<leader>cy",
yank_block = "gy",
close_tab = "<leader>cc",
quit_all = "<leader>cq",
next_marker = "]m",
prev_marker = "[m",
peek_file = "<CR>",
history_prev = "K",
history_next = "J",
slash_cmd = "<leader>/",
transcript_to_insert = { "i", "a", "o", "I", "A", "O" },
},
})Override in your colorscheme or via :hi:
| Group | Default | Where it shows |
|---|---|---|
ClaudeUserLine |
CursorLine.bg + bold |
Full-line bg tint on user rows |
ClaudeUserPrefix |
ClaudeUserSign |
» prefix on user turns |
ClaudeAssistantSign |
Function |
Right-aligned spinner fg |
ClaudeToolLine |
Comment.fg + italic |
Tool-call one-liners |
ClaudeErrorSign |
DiagnosticError |
Gutter bar on error rows |
ClaudePromptBg |
NormalFloat |
Prompt pane background |
ClaudeTab / ClaudeTabSel |
TabLine / TabLineSel |
Tabline inactive / active |
vim.api.nvim_set_hl(0, "ClaudeUserLine", { bg = "#2a2c3c", bold = true })
vim.api.nvim_set_hl(0, "ClaudeAssistantSign", { fg = "#cba6f7" })
vim.api.nvim_set_hl(0, "ClaudeToolLine", { fg = "#6c7086", italic = true })subscription_usage = true (default) adds a 5h XX% segment to the winbar.
The plugin reads your Claude Code OAuth token from the macOS keychain
(fallback: ~/.claude/.credentials.json) and POSTs it to the undocumented
endpoint https://api.anthropic.com/api/oauth/usage with a 180s disk cache
- 30s min-interval lock (same mechanism as
ccstatusline). This is not an official API; it can break or rate-limit at any time. Set tofalseto disable.:ClaudeUsageDebugprints the current fetch state.
Set ask_permissions = true to route tool-permission prompts into nvim
instead of auto-accepting. On any listed tool, Claude's run is suspended and
you see:
[claude.nvim] Bash wants to run:
$ rm -rf /tmp/data # Cleaning up
Approve? (Y)es (N)o (A)lways Bash
Mechanics: a PreToolUse hook in a temp --settings JSON points at
bin/claude-nvim-auth, which forwards the tool details to nvim via
--remote-expr. Your answer travels back to Claude as the hook's
permissionDecision.
The "Always X" choice is scoped to the current tab only.
Skills like /fma sometimes need you to pick from options mid-turn via
the AskUserQuestion tool. Claude's -p mode normally errors out on
this, so we intercept the tool at PreToolUse and render a custom
floating picker instead — no config needed, wired unconditionally.
╭──────── Framing ────────╮
│ Does this scope match │
│ what you want FMA'd? │
│ ──────────────────── │
│ ▶ [✓] Looks right │
│ [ ] Adjust scope │
│ [✓] Add context │
│ ──────────────────── │
│ Full description of the │
│ focused option here. │
│ ──────────────────── │
│ j/k: nav <Space>: │
│ toggle <CR>: confirm │
│ <Esc>: cancel │
╰─────────────────────────╯
Keybindings: j/k navigate, <Space> toggles (multi-select only),
<CR> confirms, <Esc> cancels. For single-select questions the
selected option is returned; for multi-select, all checked options are.
Cancelling returns "user dismissed; proceed with your best judgement"
so Claude can carry on.
Mechanics: the hook writes a temp answer-file with an <IN-FLIGHT>
sentinel, tells nvim to run the picker asynchronously, then polls the
file until it's populated. Running the picker inline inside the
--remote-expr call would freeze the terminal (the main thread can't
pump keyboard input during synchronous expr eval).
Each turn spawns:
claude -p --output-format stream-json --verbose --include-partial-messages \
--resume <session-id> --permission-mode <mode>
vim.system pipes your prompt into stdin and streams newline-delimited JSON
events from stdout. A small parser (stream.lua) turns each event into
renderer calls via vim.schedule. There's no long-running Claude process —
one claude -p subprocess per turn. Session identity is just the JSONL
filename UUID; resume with --resume.
claude.nvim/
├── bin/
│ ├── cc shell launcher
│ └── claude-nvim-auth PreToolUse hook (Python 3)
├── lua/claude/
│ ├── init.lua setup, launch, pick, new_here, quit
│ ├── config.lua defaults + user overrides
│ ├── state.lua per-tab session records
│ ├── session.lua discover sessions in ~/.claude/projects/
│ ├── picker.lua Telescope + vim.ui.select fallback
│ ├── layout.lua create/close/focus tabs + panes
│ ├── render.lua transcript rendering, gutter signs
│ ├── prompt.lua prompt buffer, :ClaudeSend, handlers
│ ├── spawn.lua vim.system wrapper around claude -p
│ ├── stream.lua NDJSON stream-json parser
│ ├── statusline.lua per-tab statusline
│ ├── tabline.lua per-tab tabline
│ ├── yank.lua clean-copy helpers
│ ├── permissions.lua PreToolUse prompt handler
│ ├── questions.lua AskUserQuestion floating picker
│ ├── float_picker.lua shared floating-window picker primitive
│ ├── peek.lua <CR>-on-tool-call file preview float
│ ├── commands.lua client-side slash-command dispatcher
│ ├── slash.lua slash-command picker
│ ├── mentions.lua @-mention file completion source
│ ├── usage.lua OAuth /api/oauth/usage client (opt-in)
│ └── git.lua branch lookup w/ TTL cache
├── plugin/claude.lua user commands + hl group defaults
├── doc/claude.txt :h claude
└── README.md
-
"no nvim socket; cannot forward permissions" — you set
ask_permissions = truebut your nvim has no server socket. The plugin callsvim.fn.serverstart()on first send, so this shouldn't happen unless your config disables servers. Check:echo v:servername. -
ctx X%on resume — seeded from the persistedmessage.usagein the JSONL. On a fresh new session you start atctx 0%; each subsequent turn refreshes it from themessage_start.usageevent. -
Statusline flickers — our status info is rendered in the winbar (top of each Claude window), not the statusline. The bottom statusline is whatever your plugin manager / AstroNvim / heirline normally draws; we don't touch it. If the winbar itself flickers, AstroNvim may be overriding it on
BufEnter— we re-apply onWinEnter/BufEnter/TabEnterbut if a plugin fights harder, you may need to disable it. -
Turn hangs silently — if
claude -phits a network/API issue it can hang without emitting events.<C-c>in the prompt SIGINTs the subprocess and resets state. (Your in-flight message won't be in the JSONL because claude -p never persisted it.) -
Telescope picker doesn't find a session — the picker only reads
~/.claude/projects/*/*.jsonl. If a project doesn't show, check that directory. Title extraction skips boilerplate prefixes (<local-command-…>,<system-reminder>, etc.) and may fall through to "(no prompt)" for sessions with only slash commands or caveats as user turns. -
Permissions hang forever — the auth hook blocks on
nvim --remote-expr. If the socket path becomes stale (e.g. original nvim died), the hook times out afterCLAUDE_NVIM_TIMEOUTseconds (default 3600) and returns "deny". -
Subscription usage shows
5h —— Anthropic returned 429 or no token found. Check~/.cache/claude.nvim/usage.jsonand the lockfile.
PRs welcome. A few ground rules:
- Keep the core dependency-free (no plenary, no nui). Optional plugins are graceful degradations.
- Prefer pure Lua over shelling out, except where a shell tool is the
simplest honest answer (
git,osascript,curl). - Don't bind global keymaps; scope to Claude buffers.
- Tests: smoke-test in headless nvim (
nvim --headless -c ...) and verify module load + behavior before opening a PR.
MIT. See LICENSE.