SLOPSHOPPER

sounds

Звуки: ответ дописан, сессия ждёт ответа; тревогу предохранителя играет fuse

newguardpromptaudio
★ 15v0.2.2MITupdated 2026-10-09tsergeytovarov/ai-settings/plugins/sounds
A shopper browsing a rack in a slop shop
README

sounds

Плагин Claude Code: короткий звук, когда сессии нужен человек. Можно уйти в другое окно и вернуться на звук.

ЗвукКогда
done.wav, звонкая капляход закончился ответом: ответ дописан
ask.wav, «кря-кря» живой кряквымодель задала вопрос (AskUserQuestion), принесла план на одобрение (ExitPlanMode) или открылся диалог разрешения
alarm.wav в плагине fuse, короткая сиренасработал предохранитель

Молчит:

  • когда ход остановил сам человек: он и так смотрит;
  • на ходах субагентов: это часть работы, а не ответ;
  • в автозадачах: слушать некому;
  • на вопросе предохранителя: тот играет свою тревогу.

Звук играет движок через afplay, поэтому работает на macOS. Ход и диалог звука не ждут.

Звуки

Каплю и сирену синтезирует скрипт:

cd ~/work/ai-settings/plugins
python3 sounds/scripts/make_sounds.py

Он пишет sounds/sounds/done.wav и fuse/sounds/alarm.wav. Чтобы поменять звук, правь рецепт в скрипте и запусти его заново.

ask.wav — запись кряквы: Jonathon Jongsma, xeno-canto XC62258, лицензия CC BY-SA 3.0, взята с Wikimedia Commons. Из записи вырезан отрезок 1,16–2,02 с, срезан гул ниже 150 Гц, края сглажены, громкость выровнена. Этот файл распространяется под той же лицензией CC BY-SA 3.0:

ffmpeg -i "Anas_platyrhynchos_-_Mallard_-_XC62258.ogg" -ac 1 -ar 44100 mallard.wav
ffmpeg -i mallard.wav -af "atrim=1.16:2.02,asetpts=PTS-STARTPTS,highpass=f=150,afade=t=in:d=0.01,afade=t=out:st=0.82:d=0.04,loudnorm=I=-16:TP=-2:LRA=7" -ar 44100 -ac 1 sounds/ask.wav

Установка

claude plugin marketplace update popovs-plugins
claude plugin install sounds@popovs-plugins

Разработка

  • hooks/register.ts — хуки turn.complete, tool.call, classic.PermissionRequest, prompt.submit.
  • Проверка: claude plugin validate . и claude plugin test .
Source 1 files
hooks/register.ts 53 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3export const DONE = 'sounds/done.wav'
4export const ASK = 'sounds/ask.wav'
5
6// Tools whose whole point is a question to the person.
7const QUESTION_TOOLS = new Set(['AskUserQuestion', 'ExitPlanMode'])
8// The fuse asks with this header and plays its own alarm; one sound is enough.
9const FUSE_HEADER = '⛔ Опасно'
10
11// Module state: a scheduled task runs with nobody to hear it.
12let isUnattended = false
13
14// Not awaited: the clip plays alongside, the turn and the dialog do not wait for it.
15function chime($: EngineInterface, asset: string): void {
16  if (isUnattended) return
17  $.audio.play({ asset }).catch(error => {
18    $.ui.log(`sounds: ${asset} did not play: ${error instanceof Error ? error.message : String(error)}`, { to: 'debug' })
19  })
20}
21
22function isFuseQuestion(e: unknown): boolean {
23  const questions = (e as { questions?: Array<{ header?: unknown }> }).questions ?? []
24  return questions.some(question => question.header === FUSE_HEADER)
25}
26
27export const register: Register = on => {
28  on('prompt.submit', async ($, e, next) => {
29    if (e.text.startsWith('<scheduled-task')) isUnattended = true
30    return next(e)
31  })
32
33  // The answer is written: the main loop's turn ended on its own, not by a stop
34  // the person pressed while watching. A subagent's turn is part of the work.
35  on('turn.complete', async ($, e, next) => {
36    const answer = await next(e)
37    if (e.agentId === undefined && e.reason !== 'aborted') chime($, DONE)
38    return answer
39  })
40
41  // The model asks a question or brings a plan to approve.
42  on('tool.call', async ($, e, next) => {
43    if (QUESTION_TOOLS.has(e.tool) && !isFuseQuestion(e)) chime($, ASK)
44    return next(e)
45  })
46
47  // A permission dialog. Questions and plans come through here too, and already rang above.
48  on('classic.PermissionRequest', async ($, e, next) => {
49    if (!QUESTION_TOOLS.has(e.tool_name)) chime($, ASK)
50    return next(e)
51  })
52}
53