Install
$ agentstack add mcp-pieterpel-harnt-nvim ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
harnt.nvim
> Run any coding agent in Neovim, at full native fidelity, behind one shared > editor layer. The agent keeps its own TUI — harnt never renders a chat box, so > no feature is ever lost.
One diff flow, one approval popup, one keymap set — three different agents, each in its own native TUI, each reaching back into the same editor layer:
| Claude Code | Codex | Antigravity | |---|---|---| | | | |
Each is recorded against the real CLI (just demo; see [DEMO.md](./docs/DEMO.md)).
Status: beta. Working end-to-end for Claude Code, Codex, Antigravity (agy), and OpenCode — diffs, approvals, editor-context, a change-log, and one keymap set across all three, each verified against the real CLI. Pure Lua, no daemon. Not yet on luarocks (install from git — see below). See [PLAN.md](./docs/PLAN.md) for the roadmap and [BET.md](./docs/BET.md) for why this exists.
The idea in one paragraph
Every modern coding agent ships its own terminal UI and a way to reach into your editor for editor-shaped work — open this diff, give me the selection, what are the diagnostics, apply these edits, ask the user to approve. Claude Code calls it its IDE integration, Codex has /ide and its app-server, Antigravity has lifecycle hooks, OpenCode hosts an HTTP server its own TUI is a client of. Different wires, one shape: the editor is a tool-server the agent reaches back into. harnt hosts that channel for many agents and unifies the genuinely shared part — context, diff, approvals, apply, and one set of keymaps — while each agent's own TUI stays untouched in a terminal split. That is the opposite of ACP-style tools, which unify the agent protocol and strip the deep features in the process.
What it is (and isn't)
- ✅ One keymap / diff / approval / config surface across every agent.
- ✅ Full native fidelity — plan mode, slash commands,
/compact, streaming,
resume all live in the agent's own TUI, which we never replace.
- ✅ Interactive diffs and approvals shown in Neovim, even when the
agent would otherwise auto-apply; plus a per-session change-log (:Harnt changes) of everything the agent touched.
- ✅ Pure Lua, in-process, on
vim.uv. No mandatory Node/Bun runtime, no daemon. - ❌ Not a chat UI, and not a driver for headless-only agents (ACP /
app-server as sole surface). Use an ACP client for those — see the non-goal in [PLAN.md](./docs/PLAN.md).
Providers
| Provider | How harnt connects | Diffs / approvals | Editor context | Status | |---|---|---|---|---| | Claude Code | WebSocket IDE integration (lockfile) | openDiff | IDE tools (live) | ✅ verified | | Codex | app-server proxy (stdio) behind codex --remote; /ide unix socket for context | app-server tap | /ide socket (pull) | ✅ verified | | Antigravity (agy) | lifecycle hooks (.agents/hooks.json) | PreToolUse gate | PreInvocation inject | ✅ verified | | OpenCode | client of the agent's own HTTP server (opencode serve) behind opencode attach; /event SSE tap | edit → diff review, cmd → approval (HTTP reply) | @-mention push (/tui/append-prompt) | ✅ wire verified | | Fake | in-process | — | — | test seam | | Qwen | companion spec | — | — | planned |
Each agent is met in its own native protocol — there is deliberately no universal wire format (that's the whole bet). Adding an agent is a Lua table on the shared editor services; third parties can register one without a core PR. See [PROVIDERS.md](./docs/PROVIDERS.md).
Requirements
- Neovim 0.10+.
- The CLI for each agent you want to use, installed and authenticated:
- Claude Code (
claude), Codex (codex), Antigravity (agy), OpenCode (opencode). nc(netcat) — only for Antigravity's hook bridge.- Run
:checkhealth harnt— it reports, per provider, what's present and what's missing.
Installation
Pure Lua, no dependencies beyond Neovim. Not published to luarocks yet, so install from git.
lazy.nvim
{
"harnt.nvim",
url = "https://github.com/PieterPel/harnt.nvim",
opts = {},
-- optional: your own launch keymaps
keys = {
{ "ac", "Harnt open claude", desc = "harnt: Claude" },
{ "ax", "Harnt open codex", desc = "harnt: Codex" },
{ "ag", "Harnt open antigravity", desc = "harnt: Antigravity" },
{ "ao", "Harnt open opencode", desc = "harnt: OpenCode" },
},
}
rocks.nvim
:Rocks install harnt.nvim " once published to luarocks
Quickstart
require("harnt").setup({}) -- registers claude, codex, antigravity, opencode
Then:
:Harnt open claude " launch the agent's TUI in a split; harnt hosts its channel
Work with the agent as you normally would. When it proposes an edit, harnt shows it as a diff in Neovim; when it wants to run a command, an approval popup appears. The keys are the same for every agent.
Commands
| Command | Does | |---|---| | :Harnt open [provider] | Launch a provider's TUI (default claude) | | :Harnt toggle [provider] | Show/hide (or launch) a provider's terminal | | :Harnt stop [provider] | Stop one provider, or all | | :Harnt send | Send the current file/selection to running agents (@-mention) | | :Harnt accept / reject | Accept / reject the current diff | | :Harnt review | Reject the diff and send your inline comments back as feedback | | :Harnt changes | Browse the session change-log (read-only) | | :Harnt health | :checkhealth harnt |
Keymaps (buffer-local, inside a diff/review)
| Key | Action | |---|---| | a | accept the diff | | r | reject the diff | | c | comment on the current line | | R | submit review (reject + send comments as feedback) |
Configuration
Everything lives under require("harnt").setup{}. The full set of options and their defaults:
require("harnt").setup({
keymaps = {
diff = { -- buffer-local, active inside a diff/review window
accept = "a",
reject = "r",
comment = "c", -- comment on the current line
review = "R", -- reject + send the comments back as feedback
},
},
diff = { presenter = nil }, -- how a proposed change is *shown* (see below)
approvals = { chooser = nil }, -- how an approval prompt is *shown* (see below)
})
Rendering diffs your own way
harnt's diff service owns the change's lifecycle (baseline vs. proposal buffers, accept/reject, writing the result) and stays UI-agnostic; how it's laid out is a presenter you can replace. The default lays the two buffers out as a side-by-side vimdiff in a new tab, with a winbar showing the keys.
A presenter is given the buffers the service already created and returns a teardown to dismiss whatever windows it opened:
require("harnt").setup({
diff = {
-- view = { path, original_buf, proposed_buf }
presenter = function(view)
-- e.g. a floating window instead of a tab:
local win = vim.api.nvim_open_win(view.proposed_buf, true, {
relative = "editor", width = 100, height = 30, row = 2, col = 4,
border = "rounded", title = " " .. vim.fn.fnamemodify(view.path, ":t") .. " ",
})
return { teardown = function() pcall(vim.api.nvim_win_close, win, true) end }
end,
},
})
The keys (accept/reject/comment/review) are bound on those buffers by the service regardless of presenter, so a custom presenter never has to re-implement them. view.original_buf is absent for review-only diffs (an agent that applies its own edit and only wants a verdict), where proposed_buf is a rendered patch.
Rendering approvals your own way
Command approvals go through a chooser (default: vim.ui.select, so it already follows whatever UI you've wired vim.ui.select to — dressing.nvim, snacks, telescope, …). Replace it for a fully custom prompt:
require("harnt").setup({
approvals = {
-- req = { key, prompt, detail? }
-- decision ∈ "allow_once" | "allow_always" | "deny_once" | "deny_always"
chooser = function(req, on_choice)
vim.ui.select({ "allow once", "allow always", "deny once", "deny always" }, {
prompt = req.prompt,
}, function(_, idx)
on_choice(({ "allow_once", "allow_always", "deny_once", "deny_always" })[idx or 1])
end)
end,
},
})
Per-provider notes
- Claude Code — harnt writes
~/.claude/ide/.lockand hosts the
WebSocket the CLI connects into; edits are also recorded via a PostToolUse hook so auto-applied changes still show in :Harnt changes.
- Codex — harnt proxies
codex app-server(stdio) and launches the native
TUI via codex --remote; diffs/approvals are tapped from that stream, while a separate /ide unix socket feeds the agent your live selection + open files.
- Antigravity (
agy) — harnt merges aharnthook into.agents/hooks.json
(non-destructively, restored on stop): PreToolUse gates edits/commands (blocking = interactive diffs), PreInvocation injects editor context. Needs nc. Runs agy interactively in the split.
- OpenCode — harnt spawns
opencode serve(its own HTTP+SSE server) and
launches the native TUI via opencode attach; it taps the /event stream. Edit permissions show as an interactive diff (accept/reject/comment, reconstructed from the tool call and answered over HTTP with free-text feedback on reject); command permissions use the approval popup; auto-applied edits land in :Harnt changes; and :Harnt send pushes the current file/selection as a native @-mention. The TUI drives and renders; harnt observes. OpenCode also ships a headless opencode acp surface — the exact thing harnt refuses to render; we use the native TUI + server instead. See [OPENCODE.md](./docs/OPENCODE.md).
No feature loss (the guarantee)
Because harnt never renders the agent's chat, the agent's own features are untouched. Verified end-to-end per provider (just e2e-*):
| Agent | Verified intact through harnt | |---|---| | Claude | plan mode, slash commands, /compact; openDiff + diagnostics + selection | | Codex | /ide context ("IDE context is on"), app-server diffs + approvals, native TUI | | Antigravity | PreToolUse diff gate (reject blocks / accept writes), PreInvocation context | | OpenCode | native TUI (plan/build agents, slash commands, sessions) untouched; edit permission → diff review, command → approval, session.diff → change-log, :Harnt send → @-mention |
Extending
Add an agent with a table — see [PROVIDERS.md](./docs/PROVIDERS.md). The shared services (context / diff / approvals / apply / changes) are the reusable heart; a provider is mostly discovery + env + a tool/decision map on top.
Development
Everything runs through Nix + just, identically on your machine and in CI:
nix develop # enter the toolchain (stylua, selene, emmylua, busted, …)
just # list tasks
just ci # the full gate: fmt-check · lint · typecheck · test · smoke
just test # busted, with Neovim as the Lua interpreter (nlua)
just smoke # clean-room load check (fresh install, nothing else on rtp)
just e2e-codex-ide # real-CLI smoke (needs the agent authed); see the justfile
Tooling decisions live in [TOOLS.md](./docs/TOOLS.md).
License
[MIT](./LICENSE) © 2026 Pieter Pel
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: PieterPel
- Source: PieterPel/harnt.nvim
- License: MIT
- Homepage: https://github.com/PieterPel/harnt.nvim
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.