SLOPSHOPPER

notify

Raises a desktop notification when Claude finishes a reply

newprocess
v0.1.0no licenseupdated 2026-10-11kdaisho/claude-skills/notify
A shopper browsing a rack in a slop shop
README

claude-skills

Reusable Claude Code skills, and a few mods.

Skills

Copy (or symlink) a skill folder into a project's .claude/skills/ to use it.

  • implement
  • skeleton-ui

Mods

book-pager

Shows Claude's finished replies in a side pane as fixed pages with short lines, so they are easier to read in a wide terminal. Each session keeps its own book, and a restart or /resume brings it back.

To load it in every session, add this to ~/.claude/settings.json (inside the "env" block if there already is one), then start a new session:

"env": { "CLAUDE_CODE_PLUGIN_DIRS": "~/code/claude-skills/book-pager" }
  • /pager shows or hides the pane.
  • p / n turn the page when the pane has focus. With the prompt empty, , and . turn it from the prompt.
  • The text fills the pane, so drag the pane's edge to change the page width. A table is drawn with borders when it fits, with long cells wrapped inside their column, and as one card per row when even its narrowest columns do not fit.
  • The pane opens at the width setting, in terminal columns of text (default 76, 20 to 200). Change "Page width" in /config, or set it in ~/.claude/settings.json and restart:
  "pluginConfigs": { "book-pager": { "options": { "width": 90 } } }

defer

Saves a question for later without sending it to Claude.

  • Save: /defer <question>, or end a prompt with /defer. Both work while Claude is busy. The prompt form never reaches Claude, and the command form keeps the question out of the conversation.
  • Pick: /defer with no text opens a list. Press 1, 2, and so on, or click. The question goes into the prompt box and leaves the list.
  • A line above the prompt shows "2 saved questions · /defer to pick". /differ does the same as /defer.
  • Each session has its own list. It survives compaction, restarts and /resume, and /clear starts an empty one. A list is saved in $.store under items:<session id>, and is removed when it becomes empty, when it has not changed for 30 days, or when more than 50 lists are saved (the one changed longest ago goes first).

notify

Raises a desktop notification, "<session> is done", when Claude finishes a reply. The name is the /rename title, else the folder's name. It goes through Claude Code's notification channel: set "preferredNotifChannel": "ghostty" in ~/.claude/settings.json (or auto in Ghostty). Claude Code raises its own notification when it waits for an answer.

A Notification hook also hears this notification, with notification_type plugin_notification, so a voice hook can skip it.

Loading the mods

Separate the folders with ::

"env": { "CLAUDE_CODE_PLUGIN_DIRS": "~/code/claude-skills/book-pager:~/code/claude-skills/defer:~/code/claude-skills/notify" }

The mod API is early access. book-pager was built on Claude Code 2.1.289; defer and notify on 2.1.296. Setup steps, the design and what was tested are in the design note.

Source 1 files
hooks/register.ts 40 lines
1import type { EngineInterface, Register } from "claude-code";
2
3const TITLE = "Claude Code";
4
5// The name the voice notification says: the /rename title, else the folder's name.
6async function sessionName(
7  $: EngineInterface,
8  transcriptPath: string,
9  cwd: string,
10): Promise<string> {
11  const found = await $.process
12    .run(["grep", '"type":"custom-title"', transcriptPath])
13    .catch(() => undefined);
14  const last = found?.stdout.trim().split("\n").pop() ?? "";
15  try {
16    const title = (JSON.parse(last) as { customTitle?: unknown }).customTitle;
17    if (typeof title === "string" && title !== "") {
18      return title;
19    }
20  } catch {
21    // No /rename in this session.
22  }
23
24  return cwd.split("/").filter(Boolean).pop() ?? "Claude";
25}
26
27// A banner when Claude finishes, beside the voice from ~/.claude/hooks/say-notify.sh. Claude Code raises its own banner
28// when it waits for an answer, through the same `preferredNotifChannel`.
29export const register: Register = (on) => {
30  on("classic.Stop", async ($, e, next) => {
31    const result = await next(e);
32    if (e.agent_id === undefined) {
33      const name = await sessionName($, e.transcript_path, e.cwd);
34      await $.ui.notify(`${name} is done`, { title: TITLE }).catch(() => undefined);
35    }
36
37    return result;
38  });
39};
40