A Teacher in the Terminal
I built a little setup where Claude sits next to my Anki sessions and explains anything I'm fuzzy on — then adds it to my deck in one shot.

学如逆水行舟,不进则退。
Learning is like rowing upstream — not to advance is to fall back.
I've been studying Mandarin for a while now. Anki every day, a mix of characters I've seen before and new ones. The thing I kept running into: I'd hit something I didn't fully understand mid-session, tell myself I'd look it up later, and then not do it.
The fix turned out to be pretty simple — Claude Code and a small Anki CLI.
The setup
The left panel in that screenshot is review.py — a terminal TUI I wrote that pulls due cards from Anki via AnkiConnect and lets me review them without ever opening the Anki GUI. Arrow keys to reveal and rate, audio plays automatically, progress bar at the top. 750 lines of Python using Textual.
The right panel is Claude Code.
The loop
When I hit something I don't fully understand — not just recognize, but understand — I paste it into the terminal and run /chinese-breakdown.
What comes back is dense. Character-by-character decomposition. How the radicals combine into meaning. Etymology when there's a good story. A mnemonic. Two example sentences in natural usage. For 成语, I get the original historical anecdote — the dynasty, the text, what actually happened — which is the only way I've found to make them stick.
This isn't a dictionary entry. It's closer to what a knowledgeable teacher would say if you walked up mid-session and asked: okay but what does this actually mean, and why?
At the end of every breakdown, Claude asks if I want to add the word to Anki. One confirmation, and it calls AnkiConnect directly — writes the card to my deck with the right fields, confirms with the note ID. The card shows up in tomorrow's reviews. No tab-switching, no copy-pasting into the Anki GUI, no breaking focus.
Anki is good at drilling recognition, less good at building actual understanding. I can recognize a card and still have no feel for when to use the word. The breakdown fills that in.
How to set it up
0. The TUI (optional but recommended)
review.py is on GitHub here. Drop it anywhere, pip install textual requests, run it with python review.py. Anki needs to be open — it talks to AnkiConnect under the hood.
1. Install AnkiConnect
Tools → Add-ons → Get Add-ons, code 2055492159. Restart Anki. It runs a local server on port 8765 whenever Anki is open.
2. The card script
add_card.py — the only custom code in the setup:
#!/usr/bin/env python3
"""
Add a card to Anki via AnkiConnect.
Usage: python add_card.py --deck DECK --model MODEL --fields '{"Hanzi": "...", "Pinyin": "...", "English": "..."}'
Exit codes: 0 success, 1 AnkiConnect unreachable, 2 duplicate skipped, 3 error
"""
import json, sys, argparse, urllib.request, urllib.error
ANKICONNECT_URL = "http://localhost:8765"
DEFAULT_DECK = "中文课"
DEFAULT_MODEL = "Chinese"
def ankiconnect(action, **params):
payload = json.dumps({"action": action, "version": 6, "params": params}).encode()
req = urllib.request.Request(ANKICONNECT_URL, data=payload, method="POST")
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=3) as resp:
result = json.loads(resp.read())
if result.get("error"):
raise RuntimeError(f"AnkiConnect error: {result['error']}")
return result["result"]
except (urllib.error.URLError, ConnectionRefusedError):
print("ERROR: Cannot connect to AnkiConnect. Is Anki open?", file=sys.stderr)
sys.exit(1)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--deck", default=DEFAULT_DECK)
parser.add_argument("--model", default=DEFAULT_MODEL)
parser.add_argument("--fields", type=str)
parser.add_argument("--front", type=str)
parser.add_argument("--back", type=str)
parser.add_argument("--tags", nargs="*", default=[])
parser.add_argument("--duplicate", action="store_true")
args = parser.parse_args()
if args.fields:
fields = json.loads(args.fields)
elif args.front or args.back:
fields = {k: v for k, v in [("Front", args.front), ("Back", args.back)] if v}
else:
print("ERROR: Provide --fields or --front/--back", file=sys.stderr)
sys.exit(3)
note = {
"deckName": args.deck, "modelName": args.model, "fields": fields,
"tags": args.tags, "options": {"allowDuplicate": args.duplicate, "duplicateScope": "deck"},
}
try:
note_id = ankiconnect("addNote", note=note)
if note_id is None:
print(f"SKIPPED: Duplicate in '{args.deck}'", file=sys.stderr)
sys.exit(2)
print(note_id)
except RuntimeError as e:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(3)
if __name__ == "__main__":
main()
3. The Claude Code skills
Two files in ~/.claude/skills/. Claude Code picks them up automatically.
~/.claude/skills/anki/SKILL.md
---
name: anki
description: Manage Anki cards via AnkiConnect. Trigger on "add to anki", "add this to my deck", etc.
---
# Anki — Deck Management
Default deck: 中文课 | Default note type: Chinese (fields: Hanzi, Pinyin, English)
## Adding a card
python3 ~/.claude/skills/anki/scripts/add_card.py \
--deck "中文课" \
--model "Chinese" \
--fields '{"Hanzi": "WORD", "Pinyin": "PINYIN", "English": "DEFINITION"}'
Confirm to the user with the word, pinyin, and definition after adding.
If AnkiConnect fails (exit 1): ask them to open Anki and retry.
If duplicate (exit 2): say the card already exists, skipping.
~/.claude/skills/chinese-breakdown/SKILL.md
---
name: chinese-breakdown
description: Deep-dive breakdown of a Chinese word, phrase, 成语, or sentence.
---
# Chinese Breakdown
Give a rich, structured breakdown: character-by-character meaning, how parts combine,
etymology/history (always for 成语 — tell the original story), one sharp mnemonic,
1–2 example sentences (hanzi + pinyin + English).
Pinyin always with tone marks. No filler.
After the breakdown, always ask: "Want me to add this to Anki?"
If yes, use the `anki` skill — default deck 中文课, note type Chinese.
Anki open in the background, skills in place. During a session: /chinese-breakdown 乘胜追击, read the breakdown, say yes, keep going. The card shows up tomorrow.
See you in the next one, Caden