Smarter ghost text
ghst is a zsh plugin that puts LLM ghost text on your command line. A dim suggestion appears as you type and Tab accepts it, Ctrl+G takes a request in plain English and hands back a command, Ctrl+R searches your history by what a command did rather than what it contained, and nothing runs until you press Enter yourself. It works in any terminal emulator and talks to OpenAI or Anthropic.
Code editors have had ghost text for years,1 and it has become the kind of feature you only notice once it’s gone. The terminal never got it. You have Ctrl+R for substring search over history and Tab for file paths, and neither has any idea what you’re trying to do. Where this does exist on the command line, in Warp or GitHub’s Copilot CLI, it comes attached to a new terminal or a separate tool, and I did not want to give up the shell, the emulator and the years of muscle memory I already have. I wanted three things added to the terminal as it is: a suggestion as I type, a way to describe a command instead of remembering it, and history search that understands “that ffmpeg command from last week” without me recalling a single flag. ghst is those three things in one plugin.
Getting started
uv tool install ghst
ghst init
exec zsh
init asks for your provider and key, adds the shell integration to .zshrc, and starts the daemon; exec zsh reloads the shell with it active. From there Tab or → accepts a suggestion and Shift+→ takes it a word at a time. Configuration, the list of supported providers and the issue tracker are in the repo.
Two processes
Most of the work went into keeping the prompt responsive. Zsh’s line editor, ZLE, is synchronous, so any call that waits on the network freezes the cursor for as long as it takes. That rules out calling an API from a widget. ghst splits in two instead: the widgets stay in zsh and do nothing but read and draw, and a Python asyncio daemon does the talking, with a Unix domain socket between them.
zsh (ZLE widgets) ←── Unix domain socket ──→ ghstd (Python asyncio daemon)
The shell connects once with zsocket and keeps the connection open. On the other side the daemon holds a connection pool, an LRU cache of responses, a circuit breaker for when the provider is down, and cancellation for requests that are already stale, so when you type another character before the last suggestion arrives, the pending call is dropped rather than drawn late over the wrong text.
Drawing the suggestion turned out to be its own problem. ZLE has a POSTDISPLAY variable meant for exactly this, but on zsh 5.9 on macOS it renders ANSI escapes as literal characters, so the dim styling came out as garbage after the cursor. ghst goes around ZLE and writes the escape codes straight to /dev/tty, using colors from the 256-color range. Colors 16 through 255 are fixed and terminal themes don’t remap them, so the ghost text looks the same in iTerm, Ghostty and the stock Terminal, whatever palette is in use.
Two kinds of prompt
Autocomplete and plain-English commands feel like one feature and turned out to need opposite prompts. Autocomplete is text continuation: the model sees git ch and its job is to produce eckout main. A plain-English command is instruction following: the model sees “list files larger than 10MB” and its job is find . -size +10M. ghst runs them on different models too, a small fast one such as gpt-4o-mini for the continuation that fires on every keystroke and a stronger one such as gpt-4o for commands and history search, where a second of latency is fine and a wrong answer is not.
# FIM-style: "continue this text"
AUTOCOMPLETE_SYSTEM = """Continue the user's shell command from where it left off.
Return ONLY the continuation text, nothing else."""
# Instruction-following: "translate this request"
COMMAND_SYSTEM = """Convert the user's natural language description into a shell command.
Return ONLY the command, nothing else."""
Treating autocomplete as continuation also settled a question I had been handling badly, which is whether a suggestion should begin with a space. If you have typed git ch the answer is no, and if you have typed git checkout it is yes, and any rule I wrote for that had exceptions. Framed as continuation, the model produces the next characters of the text, spaces included, and the rule went away.
Nothing runs on its own
ghst never executes a command. A plain-English request or a history match lands in your buffer as text, where you can read it and edit it, and it runs only when you press Enter; Ctrl+Z puts back whatever you had before. Commands like rm -rf / and curl | sh are flagged on the way through, before the request ever reaches the model.
Everything that leaves your machine is scrubbed first. Shell history and terminal output go through a redaction pass that strips API keys, tokens, passwords and anything else that looks like a credential before it is sent to the provider, and if you would rather not keep your own key in the config file, GHST_API_KEY in the environment works instead.
Notes
-
The look comes from zsh-autosuggestions, which has drawn history matches as dim text after the cursor for years and needs no model to do it. ghst’s suggestions come from an LLM that also sees your directory and git state, so they can be things you have never typed. ↩