Chimes and notifies when a long turn finishes or Claude waits on you

Seven mods for Claude Code: a pet that grows while Claude works, live panes for agents and files, a boss fight against failing tests, a chime for long turns, and a model router. They work in any project and any language.
| Mod | What it does | Where | Command |
|---|---|---|---|
| cute-feed | A pet that hatches, grows and reacts to what Claude does, over a live emoji feed | pane | /feed, /pat |
| dashboard | Project, git, session cost, context, rate limits, subagents and recent activity | pane | /dashboard |
| swarm | Live tree of subagents: who spawned whom, what each is running right now | pane | /swarm |
| repo-radar | Map of the repo that lights up as files are read, edited or fail | pane | /radar |
| boss-fight | Failing lint, tests or typechecks spawn a boss; each fix damages it | above the prompt | /boss |
| chime | Sound + notification when a long turn finishes or Claude waits on you | sound | /chime |
| model-router | Sends routine prompts and read-only subagents to a cheaper model | status line | /route |
Requires a Claude Code build with mods (function hooks). In a terminal session, install any mod by name:
/plugin install cute-feed --marketplace rabanderlipe/claude-code-mods
Answer y to add the marketplace, then pick a scope (user scope = every project). Repeat with dashboard, swarm, repo-radar, boss-fight, chime or model-router.
Panes open as tabs beside the transcript. One the mod opens on its own needs a terminal at least 144 columns wide; its slash command opens it at any width.
Every setting is a row in /config, or under pluginConfigs.<mod> in your settings.
cute-feed
autoOpen (true) — open the pane when a session startspetName ("Mochi"), species ("cat") — for a brand-new pet: cat, chick, dog, slime, bunny, hamster, penguin, frogdangerPatterns ("") — extra ;-separated regexes for commands that make the pet panic (it never blocks them)dashboard
autoOpen (false), refreshSeconds (5)swarm
autoOpen (true) — open when the first subagent startsrepo-radar
autoOpen (false), depth (2) — folder levels always shownboss-fight
enabledSuites ("lint,unit,int,db,e2e,typecheck")extraPatterns ("") — your own commands, suite=regex;suite=regex, e.g. unit=\bjust test\b;e2e=\bmake smoke\bRecognised out of the box: npm / pnpm / yarn / bun / make scripts (test, test:int, e2e, lint, typecheck, …), eslint, oxlint, biome, ruff, flake8, pylint, rubocop, golangci-lint, clippy, vitest, jest, mocha, ava, pytest, go test, cargo test, rspec, phpunit, dotnet test, mix test, pg_prove, playwright, cypress, tsc, vue-tsc, mypy, pyright, flow.
chime
thresholdSeconds (45), notify (true), waitingSound (true)model-router
lightModel ("claude-haiku-5-5"), standardModel ("claude-sonnet-5-5")maxMessages (40) — past this, switching models costs a prompt-cache rebuild, so it stops routinglightAgents ("Explore,claude-code-guide,statusline-setup")routeMainThread (true), routeSubagents (true)Prefix a prompt with !haiku, !sonnet or !opus to force a model for that turn.
/pat, /feed name <name>, /feed species <name>, /feed feed.claude --plugin-dir plugins/cute-feed # load one mod from disk, hot-reloaded on save
claude plugin validate plugins/cute-feed
claude plugin test plugins/cute-feed
Mods are not sandboxed: read the source before installing any mod, these included.
MIT
hooks/register.ts 113 lines1import type { EngineInterface, PluginOptions, Register } from "claude-code";
2
3import type { ChimeSettings } from "../types";
4
5const DEFAULT_THRESHOLD_SEC = 45;
6const DONE = "assets/done.wav";
7const WAITING = "assets/waiting.wav";
8
9// Waits the engine announces through the Notification hook that mean "Claude needs you".
10const WAIT_TYPES = /permission|idle|elicitation|input/i;
11
12// The configured defaults; `/chime` choices in the store override them per user.
13type Defaults = Omit<ChimeSettings, "isOn">;
14
15export const defaultsFrom = (options: PluginOptions | undefined): Defaults => {
16 const o = options ?? {};
17 const threshold = o.thresholdSeconds;
18 return {
19 thresholdSec:
20 typeof threshold === "number" && threshold > 0
21 ? threshold
22 : DEFAULT_THRESHOLD_SEC,
23 notify: o.notify !== false,
24 waitingSound: o.waitingSound !== false,
25 };
26};
27
28async function settings(
29 $: EngineInterface,
30 defaults: Defaults,
31): Promise<ChimeSettings> {
32 const isOn = (await $.store.get("isOn")) !== false;
33 const raw = await $.store.get("thresholdSec");
34 return {
35 ...defaults,
36 isOn,
37 thresholdSec:
38 typeof raw === "number" && raw > 0 ? raw : defaults.thresholdSec,
39 };
40}
41
42async function ring(
43 $: EngineInterface,
44 asset: string,
45 gain: number,
46): Promise<void> {
47 try {
48 await $.audio.play({ asset }, { gain });
49 } catch {
50 // No player on this machine: the notification still goes out.
51 }
52}
53
54export const register: Register = (on, options) => {
55 const defaults = defaultsFrom(options);
56
57 on("session.start", async ($, e, next) => {
58 await $.command.register({
59 name: "chime",
60 description:
61 "Chime: /chime on | off | test | <seconds> (long-turn threshold)",
62 });
63 return next(e);
64 });
65
66 on("command.run", { command: "chime" }, async ($, e) => {
67 const arg = e.args.trim().toLowerCase();
68 if (arg === "on" || arg === "off") {
69 await $.store.set("isOn", arg === "on");
70 return { text: `Chime ${arg}.` };
71 }
72 if (arg === "test") {
73 await ring($, DONE, 1);
74 await $.clock.sleep(700);
75 await ring($, WAITING, 0.6);
76 return { text: "🔔 done chime, then the waiting chime." };
77 }
78 const seconds = Number(arg);
79 if (arg !== "" && Number.isFinite(seconds) && seconds > 0) {
80 await $.store.set("thresholdSec", Math.round(seconds));
81 return {
82 text: `Chime after turns of ${Math.round(seconds)}s or longer.`,
83 };
84 }
85 const s = await settings($, defaults);
86 return {
87 text: `Chime is ${s.isOn ? "on" : "off"}; rings after turns of ${s.thresholdSec}s+${s.notify ? " with a notification" : ""}${s.waitingSound ? " and when Claude waits on you" : ""}.`,
88 };
89 });
90
91 on("turn.complete", async ($, e, next) => {
92 if (e.agentId === undefined && !e.isAborted) {
93 const s = await settings($, defaults);
94 if (s.isOn && e.durationMs >= s.thresholdSec * 1000) {
95 await ring($, DONE, 1);
96 if (s.notify) {
97 const secs = Math.round(e.durationMs / 1000);
98 await $.ui.notify(`Done in ${secs}s`, { title: "Claude Code" });
99 }
100 }
101 }
102 return next(e);
103 });
104
105 on("classic.Notification", async ($, e, next) => {
106 if (WAIT_TYPES.test(e.notification_type)) {
107 const s = await settings($, defaults);
108 if (s.isOn && s.waitingSound) await ring($, WAITING, 0.6);
109 }
110 return next(e);
111 }).catch(($, e, next) => next(e));
112};
113types/index.d.ts 7 lines1export type ChimeSettings = {
2 isOn: boolean;
3 thresholdSec: number;
4 notify: boolean;
5 waitingSound: boolean;
6};
7