Learn to code by hand without leaving the editor to look things up.
Write a question as a comment that starts with ?. A read-only answer appears
under it as ghost text: either the code to write, or a few short steps when the
answer isn't code. You type it in yourself.

// ? read the file named by the first argument into a String, exit with a message if it fails
┆ let path = args.get(1).unwrap_or_else(|| {
┆ eprintln!("usage: demo <file>");
┆ std::process::exit(1);
┆ });
┆ let contents = std::fs::read_to_string(path).unwrap_or_else(|e| {
┆ eprintln!("can't read {path}: {e}");
┆ std::process::exit(1);
┆ });
-- ? split the window to the right and jump back and forth
┆ Press <Space>| to split the window right.
┆ Press <C-L> to jump to the right split, <C-H> to jump back.
┆ (These are Neovim splits; tmux panes use prefix C-a then h/j/k/l.)
The second answer uses your keybindings. ghost reads your live Neovim
keymaps, tmux bindings and shell aliases, so it says <Space>| and C-a, not
the textbook defaults.
.ghost/, so
reopening a file shows them again without another request.ghost "how do I attach to my tmux session"
answers using the same setup context, for when you're not in a file (see
Shell command).claude (Claude Code), logged in. This is the default.codex (Codex CLI), logged intmux, for answers that use your tmux bindings{
"mrwatts88/ghost.nvim",
event = "VeryLazy",
opts = {},
}
Neovim 0.12's built-in vim.pack:
vim.pack.add({ "https://github.com/mrwatts88/ghost.nvim" })
require("ghost").setup({})
Then run :checkhealth ghost.
?. Any comment style works:
// ?, # ?, -- ?, ; ?, /* ? */, <!-- ? -->. A long question can
continue on the comment lines directly below it.<leader>ii.⋯ thinking shows while it works. The answer replaces it a few seconds later.| Key | Command | What it does |
|---|---|---|
<leader>ii |
:Ghost ask |
Ask the question under the cursor (shows the saved answer if there is one) |
<leader>ia |
:Ghost all |
Ask every question in the file that has no answer yet |
<leader>ir |
:Ghost regenerate |
Ask again and replace the saved answer |
<leader>it |
:Ghost toggle |
Hide or show answers in this buffer, to quiz yourself |
<leader>ie |
:Ghost expand |
Expand answers longer than max_lines |
:Ghost setup |
Open the setup context that gets sent with questions |
Answers follow their question. They're keyed by the question's text and the file's language, not by line or file. You can move a comment anywhere in the project and its answer comes with it, with no new request. The same question in a Rust file and a Python file gets two separate answers. Edit the question and it counts as a new one.
These are the defaults. Pass only what you want to change.
require("ghost").setup({
-- "claude", "codex", or { cmd = { ... } } for any other CLI (see Backends)
backend = "claude",
-- Passed through to the CLI. nil = the CLI's default
-- (claude: "sonnet"; codex: your ~/.codex/config.toml)
model = nil,
-- claude: low | medium | high | xhigh | max
-- codex: minimal | low | medium | high
effort = nil,
-- Extra flags appended to the built-in backend's command
args = {},
-- Path to the CLI, if it isn't on $PATH under the backend's name
cmd = nil,
-- Ask every unanswered question in the file on :w (like <leader>ia)
ask_on_save = false,
max_lines = 30,
timeout_ms = 90000,
-- Set to false and map require("ghost").ask / .ask_all / .regenerate /
-- .toggle / .expand yourself
keys = {
ask = "<leader>ii",
ask_all = "<leader>ia",
regenerate = "<leader>ir",
toggle = "<leader>it",
expand = "<leader>ie",
},
-- What "your setup" context is sent with each question
context = {
nvim = true, -- keymaps that have a description
tmux = true, -- effective tmux bindings, plus your tmux.conf
-- "aliases": only alias / function / bindkey lines from shell_files,
-- each with the comment directly above it
-- "full": the whole files
-- false: nothing
shell = "aliases",
shell_files = { "~/.zshrc", "~/.bashrc", "~/.config/fish/config.fish" },
},
})
Every backend runs read-only. Claude Code runs with all tools disabled,
and Codex runs in its read-only sandbox. An answer can never change your
files.
-- Claude Code (default). A cheaper, faster model:
opts = { model = "haiku" }
-- Codex CLI, with your configured model and low reasoning effort
opts = { backend = "codex", effort = "low" }
-- Anything else: ghost writes the prompt to stdin and looks for a JSON
-- object { "kind", "lang", "body" } in stdout. model/effort/args are
-- ignored here; put those flags in cmd yourself.
opts = { backend = { cmd = { "my-llm", "--model", "some-model" } } }
Every question sends this to your backend:
Cargo.toml, package.json, pyproject.toml, …),
so answers match your dependency versionstmux.conf,
and your shell aliases, as set by contextThe default shell mode, "aliases", only sends the lines that define
aliases, functions and key bindings, because rc files are where people tend to
export tokens. Run :Ghost setup to see exactly what's sent, and set any
part of context to false to leave it out.
.ghost/ folderAnswers are stored in one file per project, <git root>/.ghost/answers.json.
It has sorted keys and one field per line, so it diffs cleanly. Committing it
is encouraged: it becomes a notebook of how-tos for the project, readable top
to bottom. If you'd rather not, add .ghost/ to .gitignore. Files outside a
git repo share a store in Neovim's cache directory instead.
Each answer lists the files that ask its question. When you save a file, that
file is taken off the list of every answer whose question it no longer has, and
an answer no file asks anymore is deleted. Projects using the older layout (one
.ghost/<path>.json per source file) are converted automatically the first time
you open them.
bin/ghost asks a question from the terminal. It loads your Neovim config
headless, so answers still use your keybindings and aliases. It prints the
answer and saves nothing.
Put it on your $PATH by linking it from wherever your plugin manager
installed ghost.nvim:
# lazy.nvim
ln -s ~/.local/share/nvim/lazy/ghost.nvim/bin/ghost ~/.local/bin/ghost
# vim.pack
ln -s ~/.local/share/nvim/site/pack/core/opt/ghost.nvim/bin/ghost ~/.local/bin/ghost
ghost "how do I attach to my tmux session"
GHOST_BACKEND=codex GHOST_EFFORT=low ghost "undo the last git commit but keep the changes"
Quote questions that contain ?. Otherwise zsh treats ? as a wildcard.
| Group | Default | Used for |
|---|---|---|
GhostText |
links to Comment |
step answers, the "thinking" line |
GhostCode |
italic |
layered on syntax colors in code answers |
GhostBar |
links to NonText |
the ┆ gutter |
GhostError |
links to DiagnosticError |
failed requests |
Code answers are syntax-highlighted with treesitter whenever a parser for the
language is installed. Otherwise they're shown as GhostText.
Each question is one request. It carries a few thousand tokens of setup context, plus your question and the code around it. That counts against your Claude or ChatGPT plan, or your API usage, the same as any other request from those tools. Cached answers cost nothing.
See CONTRIBUTING.md.