@dabit3: Open sourced a smol plugin for Ghostty. It auto-generates simple tab titles for you, something I've wanted for a while.…
摘要
Nader Dabit open sourced Ghostwriter, a zsh plugin that auto-generates context-aware tab titles for Ghostty and other terminals, with an optional AI fallback for ambiguous names.
查看缓存全文
缓存时间: 2026/07/31 02:46
Open sourced a smol plugin for Ghostty. It auto-generates simple tab titles for you, something I’ve wanted for a while.
https://t.co/0LghCp0R1o https://t.co/RiRI3BnPjr
dabit3/ghostwriter
Source: https://github.com/dabit3/ghostwriter
ghostwriter
Context-aware tab titles for any terminal that honors OSC 2 title sequences
(which is nearly all of them — Ghostty, iTerm2,
kitty, Alacritty, WezTerm, Terminal.app, …). Instead of tabs named after
file paths or zsh, your tabs name themselves after the project you’re in:
before: ~/opensource/experiments/ghostty-plugin zsh ~/blog
after: Ghostty plugin Api auth debugging blog
Titles come from the folder (or git repo) name, split into words
(naders-portfolio becomes Naders portfolio) — no network, no latency. A
one-word name stays lowercase; longer ones capitalize only the first word.
Only when the name alone says nothing (src, tmp, ~, the middle tab
above) is an AI fallback consulted to describe the work from recent
activity.
It’s implemented as a zsh plugin: it watches your shell activity and sets the tab title through standard terminal escape sequences (OSC 2), so it works in any terminal that renders those — no terminal-specific hooks. The rare fallback calls run asynchronously in the background, so your prompt is never blocked.
Requirements
- Any terminal that honors OSC 2 titles (nearly all of them; the plugin sits out only in tmux, dumb terminals, and the Linux console)
- zsh (your login shell)
- Optional, for the AI fallback — an API key for one supported backend
(without one, titles come purely from folder names):
- OpenAI (
OPENAI_API_KEY) - Anthropic (
ANTHROPIC_API_KEY) - OpenRouter (
OPENROUTER_API_KEY)
- OpenAI (
curlandperl(preinstalled on macOS and most Linux distros)- macOS or Linux
Quick start
-
Clone this repo somewhere permanent:
git clone https://github.com/dabit3/ghostwriter.git ~/ghostwriter -
Optionally, export an API key in
~/.zshrcto enable the AI fallback (skip if it’s already there, or skip entirely for folder-name-only titles). The plugin picks the backend from whichever key it finds:export OPENAI_API_KEY=sk-... # or ANTHROPIC_API_KEY / OPENROUTER_API_KEY -
Add the plugin to the end of your
~/.zshrcso it loads in every new shell from now on:echo 'source ~/ghostwriter/ghostwriter.plugin.zsh' >> ~/.zshrc(Or open
~/.zshrcin an editor and paste thesourceline yourself. Runningsourcedirectly in a terminal only lasts for that session.)Plugin managers work too — clone into
$ZSH_CUSTOM/plugins/ghostwriterand addghostwritertoplugins=(...)for Oh My Zsh, orzinit light dabit3/ghostwriter/antidote bundle dabit3/ghostwriter. -
Ghostty users: tell Ghostty’s shell integration to stop overwriting titles by adding
shell-integration-features = no-titleto your Ghostty config. On macOS:echo 'shell-integration-features = no-title' >> "$HOME/Library/Application Support/com.mitchellh.ghostty/config"On Linux:
echo 'shell-integration-features = no-title' >> ~/.config/ghostty/config(The macOS path contains a space, so keep the quotes if you type it yourself.) This only disables Ghostty’s built-in cwd-as-title behavior; cursor, sudo, and path integration stay on. Other terminals: if yours auto-sets titles from the cwd, turn that setting off so the two don’t fight (in iTerm2, for example, uncheck the title components under Profiles > General > Title).
-
Oh My Zsh users only: OMZ has its own title auto-setter that will fight the plugin. Disable it in
~/.zshrc(beforesource $ZSH/oh-my-zsh.sh):DISABLE_AUTO_TITLE="true" -
Reload your terminal’s config (
cmd+shift+,in Ghostty on macOS) and open a new tab. Tabs opened before the config change keep the old behavior until closed.
Try it out
Open a new tab, cd into one of your projects, and run a few commands:
cd ~/my-api && git status && npm test
The folder name appears as the title instantly, tidied up (my-api becomes
My api) — and that’s the title. Only in a directory whose name says
nothing (src, work, tmp, …) does the AI fallback rename the tab from
recent activity a few seconds later. Check what the plugin thinks it’s doing
at any time:
$ tabname
title: My api
mode: auto
Usage
Mostly: just use your terminal, tabs name themselves. Manual control:
tabname show current title & mode
tabname <name> pin a manual title (automatic renaming stops)
tabname --auto unpin and resume automatic naming (renames immediately)
tabname --now unpin and force a rename right now (uses the AI fallback
when available)
When renames happen
Most titles are computed locally and instantly: opening a tab or cd-ing
into a repo/directory names it after its folder, on the spot. The AI
fallback only fires for directories whose name carries no signal — ~,
src, tmp, work, code, and the like — and even then only when:
- a tab opens there, or you
cdin (the plain folder name appears immediately; the AI refines it a few seconds later) - enough new commands have run there (default 6, at most once per minute)
Navigation and filler (cd, ls, clear, pwd, …) and immediately
repeated commands are ignored: they neither count toward that threshold nor
get sent to the AI. Commands are remembered per directory, so moving to a
new directory starts its description from scratch instead of describing it
with the last one’s commands — and returning to an earlier directory picks
its history back up.
A no-signal directory you haven’t run anything in keeps its plain folder name (or its cached name): with no activity there is nothing meaningful for the AI to say, and guessed titles are worse than honest ones.
Keeping it fast and cheap
- Directories with meaningful names never touch the network at all — for most tabs the title is pure local string-mangling.
- New tabs in a repo the AI has already named reuse a cached title (7-day TTL), so opening five tabs in one project costs at most one AI call.
- Returning to a directory you already named this session restores its title instantly from a per-tab map, no AI call.
- A generation barrier discards in-flight renames for directories you’ve since left, so fast navigation never stamps a stale title on a tab.
- Tabs sitting in a no-signal directory with no activity (including
$HOME) skip the AI entirely. - Pinned and ignored tabs are never touched.
- On the gpt-5 family, reasoning effort is capped at
low: a three-word tab title needs little deliberation, and the model’s default (medium) spends reasoning tokens and seconds of latency to reach the same answer.
Configuration
Set in ~/.zshrc before sourcing the plugin. Everything except
GHOSTWRITER_DISABLE, GHOSTWRITER_FORCE, GHOSTWRITER_MAX_LEN, and
GHOSTWRITER_IGNORE only affects the AI fallback.
| Variable | Default | Description |
|---|---|---|
GHOSTWRITER_BACKEND | auto | openai, anthropic, or openrouter (default: first backend with an API key set, in that order) |
GHOSTWRITER_API_KEY | unset | API key override; by default the backend’s own env var is used |
GHOSTWRITER_MODEL | per backend | Model id (see below) |
GHOSTWRITER_BASE_URL | per backend | API base URL override (proxies, OpenAI-compatible servers) |
GHOSTWRITER_REASONING | low | Reasoning effort for gpt-5 models; off omits the parameter |
GHOSTWRITER_IGNORE | unset | Colon-separated path globs to never send to the AI |
GHOSTWRITER_DISABLE | unset | 1 skips loading the plugin entirely |
GHOSTWRITER_FORCE | unset | 1 loads even where the plugin would sit out (dumb terminals, Linux console) |
GHOSTWRITER_CURL | curl | Path to the curl binary |
GHOSTWRITER_CMD_THRESHOLD | 6 | Commands before a re-name |
GHOSTWRITER_MIN_INTERVAL | 60 | Min seconds between renames |
GHOSTWRITER_MAX_LEN | 32 | Max title length |
GHOSTWRITER_HISTORY | 10 | Commands kept per directory as AI context |
GHOSTWRITER_TIMEOUT | 45 | AI call timeout (seconds) |
GHOSTWRITER_DEBUG | unset | 1 logs to ~/.cache/ghostwriter/debug.log |
Backends and models
For the AI fallback, the plugin calls the backend’s HTTP API directly with
curl; nothing else needs to be installed. Each backend reads its standard
API key env var and defaults to a small, cheap model suited to naming tabs:
| Backend | API key env var | Default model | API |
|---|---|---|---|
openai | OPENAI_API_KEY | gpt-5-nano | https://api.openai.com/v1/chat/completions |
anthropic | ANTHROPIC_API_KEY | claude-haiku-4-5 | https://api.anthropic.com/v1/messages |
openrouter | OPENROUTER_API_KEY | anthropic/claude-haiku-4.5 | https://openrouter.ai/api/v1/chat/completions |
If exactly one key is exported, no further setup is needed. With several
keys, or to pick a specific model, set the backend explicitly before the
plugin’s source line:
export GHOSTWRITER_BACKEND=openrouter
export GHOSTWRITER_MODEL=meta-llama/llama-3.3-70b-instruct
source ~/ghostwriter/ghostwriter.plugin.zsh
GHOSTWRITER_BASE_URL points the openai backend at any
OpenAI-compatible server (e.g. a local proxy or gateway):
export GHOSTWRITER_BACKEND=openai
export GHOSTWRITER_BASE_URL=http://localhost:4000/v1
export GHOSTWRITER_API_KEY=whatever-your-proxy-expects
The backend is read once when the plugin loads, so existing tabs keep
whatever backend they started with; open a new tab after changing it. Run
tabname --now to force a rename and confirm titles are coming from the
right place (with GHOSTWRITER_DEBUG=1, each call is logged to
~/.cache/ghostwriter/debug.log).
Troubleshooting
Tabs aren’t renaming at all
-
Are you in a new tab? Tabs opened before a config change keep the old title behavior.
-
Does
tabnameprint anything? If “command not found”, the plugin didn’t load; check thesourceline in~/.zshrc(the plugin deactivates inside tmux, in dumb terminals, and whenGHOSTWRITER_DISABLE=1is set). -
Does your terminal honor OSC 2 titles at all? Test with
printf '\033]2;hello\007'— the tab/window title should read “hello”. -
Folder-name titles work with no API key; if the AI fallback specifically isn’t firing, the plugin prints a one-line warning when a prerequisite is missing (API key,
curl,perl), so open a new tab and look for that message. -
Does
tabnamereportmode: off (GHOSTWRITER_IGNORE)? This directory is on your exclusion list. -
Does the API key work on its own? For OpenAI:
curl -sS https://api.openai.com/v1/chat/completions \ -H "Authorization: Bearer $OPENAI_API_KEY" -H "content-type: application/json" \ -d '{"model":"gpt-5-nano","messages":[{"role":"user","content":"say hi"}]}'With
GHOSTWRITER_DEBUG=1, failed API calls (bad key, quota, network) land in~/.cache/ghostwriter/debug.log.
Titles keep getting overwritten with the directory path
- Your terminal’s own set-title-from-cwd feature is fighting the plugin:
in Ghostty confirm the
no-titlesetting is active in this tab (reload config and open a fresh tab); in other terminals turn off their title/cwd integration. - Oh My Zsh: confirm
DISABLE_AUTO_TITLE="true"is set.
Titles are weird or wrong
- Turn on debug logging (
export GHOSTWRITER_DEBUG=1in~/.zshrc, new tab) and watch~/.cache/ghostwriter/debug.log; every decision, AI call, and applied title is logged. - Force a redo with
tabname --now, or take over withtabname My Name.
A rename lags a long-running command
That’s by design: the rename fires when you launch a long command (dev server, build), so the tab is named while it runs.
Privacy & cost
Directories with meaningful names are titled entirely on your machine;
nothing is sent anywhere. When the AI fallback fires (no-signal directory
names only), recent command lines (plus cwd, repo name, and the branch when
it isn’t the default one) are sent directly to the selected backend’s API
with your API key. Common secret patterns in those command lines (API_KEY=...,
Authorization: Bearer ..., long tokens) are redacted before leaving the
machine. Directory paths and branch names are sent as-is, since they carry
most of the signal — treat the whole context as something you’d paste into
that provider’s chat.
To keep certain projects off the wire entirely, list them in
GHOSTWRITER_IGNORE; matching directories (and everything under them) keep a
plain folder title and never trigger a request:
export GHOSTWRITER_IGNORE="~/work/acme:~/private/*"
Your API key is piped into curl --config, so it is never passed as a
command-line argument, never written to disk, and never written to the
debug log.
Cost: with no API key exported there is none — the fallback is simply off.
With one, the defaults are the cheapest current models (gpt-5-nano,
claude-haiku-4-5) at low reasoning effort. The prompt is only a few
hundred tokens, and since most directories never trigger the fallback,
calls are rare. Usage and billing follow the API key’s account.
Limitations
- Inside tmux the plugin disables itself (tmux owns titles there).
- TUI apps that set their own titles (e.g. Claude Code) will win while they run; the next rename trigger takes the title back.
- zsh only for now.
- A terminal that ignores OSC 2 titles simply never shows them; the plugin
does no harm there, but
GHOSTWRITER_DISABLE=1skips it cleanly.
Uninstall
- Remove the
source ...ghostwriter.plugin.zshline from~/.zshrc. - Undo any terminal config changes (e.g. remove
shell-integration-features = no-titlefrom your Ghostty config, and re-enable OMZ auto-title if you disabled it). rm -rf ~/.cache/ghostwriterto clear state, then delete the repo.
Files
ghostwriter.plugin.zsh: zsh hooks, local naming, rename triggers,tabnamecommandbin/ghostwriter-namer: detached worker for the AI fallback (cache, redaction, API call, OSC 2 title write)tests/: regression suite (zsh tests/<name>.zsh, no network required)- State/cache:
~/.cache/ghostwriter/(sessions auto-cleaned after 7 days)
Development
zsh -n ghostwriter.plugin.zsh bin/ghostwriter-namer tests/*.zsh # syntax
for t in tests/*.zsh; do zsh "$t" || break; done # tests
The suite stubs curl, so it never makes a real API call.
License
MIT — see LICENSE.
相似文章
Libghostty 即将到来
Mitchell Hashimoto 宣布了 libghostty 的计划,这是一个可嵌入的终端模拟库,首先推出 libghostty-vt,这是一个从 Ghostty 中提取的零依赖终端序列解析器。
ghostel.el - 由 libghostty 驱动的终端模拟器
ghostel.el 是一个 Emacs 包,提供由 libghostty 驱动的终端模拟器,具有 shell 集成、输入模式和内联图像支持等功能。
Ghostty: 反思1.0版本的发布
Mitchell Hashimoto 反思了他用 Zig 构建的终端模拟器 Ghostty 达到1.0版本的过程,讨论了项目的起源、成功但富有争议的内测版,以及他对这款终端的愿景。
@hellokillian:受 @karpathy 启发,打造本地 Markdown 编辑器 + 终端代理工作区
一位开发者发布了一款集成终端代理工作区的本地 Markdown 编辑器,灵感源自 Andrej Karpathy。
Lobsters 专访 mitchellh
对 Vagrant、Terraform 和 Ghostty 的创建者 Mitchell Hashimoto 的深度专访,探讨他对终端的热爱、使用 Zig 构建 Ghostty 的过程以及基于终端的应用程序的未来。