name: statusline-vocab
description: >
Manage the statusline-vocab feature — a Stop hook picks one English word worth learning from each
conversation and renders {emoji} {word} /IPA/ pos. {translation} on the Claude Code statusline
(translation language configurable; Chinese by default).
Trigger on "/statusline-vocab" or when the user asks to install / configure / check / uninstall /
switch language / debug the vocab statusline. Do NOT auto-trigger — explicit invocation only.
statusline-vocab
Put a "word of the conversation" segment on the Claude Code statusline so the user passively learns English vocabulary from their own work. Three moving parts:
~/.claude/hooks/vocab/extract.sh—Stophook. Each time the assistant finishes a turn, this hook reads the last30 transcript entries, calls/.claude/vocab/current.json`. A 3-minute cooldown prevents the word from churning every single turn within one conversation.claude -p --model claude-haiku-4-5to pick one English word worth learning (lemma + US IPA + POS + translation + emoji; target language configurable), and writes JSON to `~/.claude/vocab/current.json+~/.claude/vocab/history.jsonl— current word plus an append-only wordbook history of everything that's been picked.~/.claude/statusline-command.sh— readscurrent.jsonand appends a vocab segment to the statusline output.
Rendered example (with default Chinese; swap to any language via --lang):
➜ skills git:(develop) · Opus 4.7 ctx:12% · 📝 transcript /ˈtrænskrɪpt/ n. 誊本,逐字记录
When to use this skill
The user has typed /statusline-vocab or otherwise referenced this feature. Likely intents:
- Install on a fresh machine — set everything up (optionally with
--lang) - Status / diagnose — "why isn't a word showing up?", "what's installed?", "what language am I on?"
- Switch language — "translate to Spanish", "use Japanese definitions" →
install.py config --lang … - Reconfigure other knobs — cooldown, model, selection criteria (edit
extract.shdirectly) - Uninstall — remove the hook and tell the user how to clean the statusline
Pick the right subcommand below based on which of these the user wants. If unclear, run
status first so the conversation has a shared baseline.
Workflow
Step 1 — Detect environment
Before doing anything destructive, verify the prerequisites. The installer script does this too, but it's worth knowing what it checks so you can give a useful error if something is missing:
claudeCLI onPATH(the hook shells out toclaude -p)jqonPATH(both the hook and the statusline parse JSON with it)~/.claude/exists (true on any machine where Claude Code has been launched)
Step 2 — Run the installer
python <skill-dir>/scripts/install.py install
The installer is idempotent — running it twice does nothing the second time. It:
- Creates
~/.claude/hooks/vocab/and~/.claude/vocab/if missing. - Copies the canonical
extract.shfrom the skill into~/.claude/hooks/vocab/extract.sh(overwrites the file each run — this is how the user picks up skill updates). - Registers the hook under
hooks.Stopin~/.claude/settings.json. Skips if the exact command path is already present. Makes a.bak-vocabbackup before writing. - Handles the statusline based on its current state — see Step 3.
Step 3 — Statusline integration
The installer does not auto-edit a custom statusline. It branches into three cases and prints which one it took:
Case A — no statusline exists (~/.claude/statusline-command.sh missing or empty)
→ installer writes the bundled default-statusline.sh verbatim. The default is intentionally
plain (dir · model · ctx · vocab); users who want a richer look can edit freely after.
Case B — already integrated (statusline contains the # vocab:start marker)
→ installer skips, prints "already integrated".
Case C — existing custom statusline without markers
→ installer prints the snippet below and refuses to auto-modify. Your job in the
conversation: read the user's ~/.claude/statusline-command.sh, understand how it builds
its output line, and insert the snippet so the vocab segment lands at the end of the line
with a · separator. Always show the diff before writing so the user can sanity-check.
Insertion snippet (always surround with the markers so a future uninstall can find it):
# vocab:start — statusline-vocab skill
vocab_file="$HOME/.claude/vocab/current.json"
vocab_part=""
if [ -f "$vocab_file" ]; then
v_word=$(jq -r '.word // empty' "$vocab_file" 2>/dev/null)
if [ -n "$v_word" ] && [ "$v_word" != "null" ]; then
v_emoji=$(jq -r '.emoji // "📖"' "$vocab_file" 2>/dev/null)
v_ipa=$(jq -r '.ipa // ""' "$vocab_file" 2>/dev/null)
v_pos=$(jq -r '.pos // ""' "$vocab_file" 2>/dev/null)
v_meaning=$(jq -r '.meaning // ""' "$vocab_file" 2>/dev/null)
vocab_part=" · ${v_emoji} \033[1;94m${v_word}\033[0m \033[2m${v_ipa}\033[0m \033[0;33m${v_pos}\033[0m ${v_meaning}"
fi
fi
# vocab:end
How to wire ${vocab_part} into the final output depends on how the statusline is structured:
- Statusline builds a single
linevariable and prints once at the end (most common pattern, matches the bundled default): append${vocab_part}to that variable just before the finalprintf/echo. - Statusline calls
printfmultiple times inline: refactor lightly so the lastprintfgets${vocab_part}concatenated onto its argument. Don't add a separateprintfcall after, because Claude Code's statusline keeps trailing newlines literal. - Statusline outputs multiple lines on purpose (e.g. uses
printf '%b\n'twice): pick the line you want vocab on, and append there.
After inserting, run Step 4 to confirm.
Step 4 — Verify
python <skill-dir>/scripts/install.py status
Reports: hook script present? hook registered? statusline integrated (markers found)? what word is current? how many history entries?
To preview rendering without waiting for a real Stop, pipe a stub event:
echo '{"workspace":{"current_dir":"'"$PWD"'"},"cwd":"'"$PWD"'","model":{"display_name":"Test"},"context_window":{"used_percentage":0}}' \
| bash ~/.claude/statusline-command.sh
The vocab segment only appears once a word has been extracted — that happens after the next real
Stop hook fires in a Claude Code session. To force an extract right now, point the hook script
at an existing transcript:
latest=$(ls -t ~/.claude/projects/*/*.jsonl | head -1)
printf '{"transcript_path":"%s","hook_event_name":"Stop"}' "$latest" \
| ~/.claude/hooks/vocab/extract.sh
cat ~/.claude/vocab/current.json
Configuration
Translation language
The meaning field is translated into a target language — Chinese by default, but switchable. The
language is stored in ~/.claude/vocab/config so it survives across upgrades and doesn't require
editing extract.sh. Two ways to set it:
# At install time:
python <skill-dir>/scripts/install.py install --lang Spanish
# Anytime after, without reinstalling:
python <skill-dir>/scripts/install.py config --lang Japanese
# Inspect current:
python <skill-dir>/scripts/install.py config
The value is passed verbatim to the prompt ("…tutor for a {LANG}-speaking learner. … concise translation in {LANG}, under ~15 characters or 3-4 words…"), so any language the model knows
works: Chinese, Japanese, Korean, Spanish, French, German, Portuguese, Italian,
Russian, Vietnamese, Thai, …
The change takes effect on the next Stop hook fire. The currently-displayed word
(current.json) is not retroactively re-translated — wait for the cooldown to expire or
rm ~/.claude/vocab/current.json to force a fresh extract.
Other knobs
For knobs that don't change often, edit ~/.claude/hooks/vocab/extract.sh directly. They live
at the top under # --- Knobs ---:
| Knob | Default | Effect |
|---|---|---|
COOLDOWN |
180 (seconds) |
Skip re-extraction if current.json is fresher than this. Higher = more stable word; lower = updates more often. Raising to 600 gives roughly one word per "real" conversation. |
MODEL |
claude-haiku-4-5 |
Swap for claude-sonnet-4-6 if you want richer choices. Cost goes up; latency too (still async, so doesn't block). |
DEFAULT_LANG |
Chinese |
Only used when no config file exists. The config file always wins. |
| The prompt body | see file | Edit the skip-list ("the", "use", "make"…), the difficulty bias, or the JSON schema. After editing, the next Stop picks up the new prompt — no restart needed. |
If the user asks for a tunable that requires more than a one-line edit (e.g. "only extract on weekdays", "rotate through three difficulty levels"), edit the script in place.
Uninstall
python <skill-dir>/scripts/install.py uninstall
This:
- Removes the hook entry from
~/.claude/settings.json(backup at.json.bak-vocab-uninstall). - Deletes
~/.claude/hooks/vocab/extract.sh. - Leaves
~/.claude/vocab/intact so the user'shistory.jsonlsurvives. - Leaves the statusline alone, but prints the marker lines so the user (or Claude) can strip
them. If the statusline was the bundled default and the user wants a clean slate, they can
just
rm ~/.claude/statusline-command.sh.
Troubleshooting
- No word ever appears. Check
~/.claude/vocab/extract.logfor errors. The most common cause isclaudenot found from the hook's non-interactive shell — Claude Code hooks inherit a minimal PATH. Fix by symlinkingclaudeinto/usr/local/bin/or by editing the hook to use the absolute path. Verify manually with the "force an extract" snippet above. - Same word keeps showing. Cooldown is doing its job; either wait it out or
rm ~/.claude/vocab/current.jsonto force a re-extract on the nextStop. - Statusline renders broken / garbled.
jqis probably missing. The snippet swallows individualjqerrors but ends up with an empty vocab segment, which usually looks fine. If color codes are leaking as raw\033[...text, the user's terminal doesn't renderprintf '%b'— switch the insertion toecho -eor drop the colors. - Model picks bad words ("the", "use", "make"). Strengthen the skip-list in the prompt inside
extract.sh. Haiku follows the skip-list reliably. - Hook fires twice on every turn. Look for duplicate entries in
~/.claude/settings.jsonunderhooks.Stop— possible if the file was hand-edited between installer runs. Either re-run the installer (it dedupes) or remove the duplicate manually.
Mental model
Three boundaries, each swappable without touching the others:
- Trigger — when to think about updating the word. Currently
Stop(every assistant turn, rate-limited byCOOLDOWN). Could be moved toSessionEndfor "one word per session" semantics. - Storage —
current.json(the visible word) plushistory.jsonl(the running wordbook). - Render — how the statusline displays the word. The snippet only reads JSON; you can rewrite the format without ever touching the hook.
If the user wants a richer experience later (Anki export, daily review prompt, frequency-based
spaced repetition over history.jsonl), build it as a fourth piece that consumes history.jsonl —
don't entangle it with the trigger or render layers.