A one-line TL;DR under every long answer, written by Haiku, so a session you switch back to reads at a glance

Claude Code mods (function-hook plugins) for working across many repos and many sessions at once — plus a pet.
| Mod | What it does |
|---|---|
| where-am-i | A band above the prompt: 📁 repo 🌿 branch ↑2 ± 3 changed 🎯 task. /task <text> names what this session is doing. |
| done-ping | A macOS notification (repo, duration, your prompt) with a sound per kind: 🔔 Glass when a turn over 30 s is done, Basso when it failed, Submarine when Claude is waiting on you. Change them in SOUNDS. |
| repo-guard | Blocks push --force, pushes to main/master, reset --hard, clean -f, checkout ., branch -D, stash clear, rm -rf ~. Asks before Claude first edits a repo other than the session's own. |
| claude-pet | A pet that earns xp from your work, grows through five stages and unlocks achievements. Pick a species (/pet species: chick, cat, dog, dragon, dino, ocean, bug, plant, robot, moon) or your own emojis (/pet emoji 🦊, /pet emoji 🥚 🐣 🦊 🐺 🐉). Lives in the status line (🦮 xixi (・_・)📖 Lv7 ▰▱▱▱▱▱) with 18 moods that follow Claude: 💭 thinking, 📖 reading, ✍️ writing, ⚡ running, 🧪 testing, 🌐 browsing, 📣 delegating, ✋ waiting for you, 🔥 on a 25-call streak, 🌙 past 1am, 🎉 tests pass, 📦 commit/push, (×_×) error, (╬ಠ益ಠ) three in a row, bored after 5 idle minutes, asleep after 15. /pet for stats, /pet name <name>. |
| claude-mood | Claude's mood in the status line: 🤔 reading, ✍️ writing, 🧪 testing, 😤 after a few errors, 😌 when done — with 📖 ✏️ ⚡ 💥 counters. |
| control-tower | A row under where-am-i's, shown only while another session waits for you (a permission or a question): ✋ backend-ng 在等你 2m [tower]. /tower (or the button) opens a pane listing every Claude session on this machine, named repo · branch for worktrees. |
| tldr | After every long answer, a dim 💡 TL;DR line (Haiku) in the transcript, so a session you switch back to reads at a glance. |
| sdk-sync | Release audit for the SpatialReal SDKs. Never acts while you work: when a new v* tag appears it reminds you once; /release-check <repo> [tag] gathers the facts (public API files changed, CHANGELOG entry, commits in each sibling SDK / docs / examples since the release, version pins) and asks Claude for a read-only audit table. /release-check status lists unchecked releases; --facts skips the audit. Graph: plugins/sdk-sync/hooks/sdk-map.ts. |
| fortune | Programmer jokes after the working spinner (Sauteing… 🎲 删库跑路前,记得先 git push。); the day's first prompt shows 今日运势 (/fortune). Fridays never deploy. |
At a Claude Code terminal prompt:
/plugin marketplace add YuehengHan/my-claude-mods
/plugin install where-am-i@my-claude-mods
/plugin install done-ping@my-claude-mods
/plugin install repo-guard@my-claude-mods
/plugin install claude-pet@my-claude-mods
/plugin install claude-mood@my-claude-mods
Pick the user scope so they load in every session. Install only the ones you want.
Add a local clone as the marketplace instead, and Claude Code reads the plugins straight from the folder:
claude plugin marketplace add ~/Desktop/my-claude-mods
claude plugin install where-am-i@my-claude-mods --scope user
Claude Code runs an installed copy, so after editing a mod bump its version in plugin.json, then:
claude plugin marketplace update my-claude-mods
claude plugin update <mod>@my-claude-mods
and run /reload-plugins (or restart) in each open session.
Check and test a mod:
claude plugin validate plugins/<mod>
claude plugin test plugins/<mod>hooks/register.ts 48 lines1import type { Register } from 'claude-code'
2
3/** Answers shorter than this are their own TL;DR. */
4export const MIN_CHARS = 400
5const MODEL = 'haiku'
6
7const SYSTEM = [
8 'You write the one-line TL;DR shown under a coding assistant\'s reply.',
9 'Say what was done and how it ended (fixed? tests green? waiting on the user? blocked?).',
10 'Write in the same language as the reply. At most 40 Chinese characters or 25 English words.',
11 'Output only the line: no prefix, no quotes, no markdown.',
12].join(' ')
13
14export function needsTldr(answer: string): boolean {
15 return answer.trim().length >= MIN_CHARS
16}
17
18export function clean(text: string): string {
19 return text
20 .replace(/^\s*(tl;?dr[::]?\s*)/i, '')
21 .replace(/^["'“”「」]+|["'“”「」]+$/g, '')
22 .replace(/\s+/g, ' ')
23 .trim()
24}
25
26export const register: Register = on => {
27 on('turn.complete', async ($, e, next) => {
28 const done = await next(e)
29 if (e.agentId !== undefined || e.isAborted || e.reason !== 'answer' || !needsTldr(e.answer)) return done
30
31 // The answer shows right away; the line follows a moment later as a dim
32 // transcript row, which the terminal and the desktop both draw.
33 const reply = e.answer.length > 8000 ? `${e.answer.slice(0, 4000)}\n…\n${e.answer.slice(-4000)}` : e.answer
34 void $.model
35 .complete({ model: MODEL, system: SYSTEM, prompt: reply, maxTokens: 120, timeoutMs: 15_000 })
36 .then(r => {
37 if (!r.isAnswered) {
38 $.ui.log(`tldr: no summary (${r.reason})`, { to: 'debug' })
39 return
40 }
41 const line = clean(r.text)
42 if (line) $.ui.log(`💡 TL;DR:${line}`)
43 })
44 .catch(err => $.ui.log(`tldr: ${String(err)}`, { to: 'debug' }))
45 return done
46 })
47}
48