Plays a short chime when a long main-agent turn completes; /done-sound off|on|test

Plays a short chime when a long turn finishes, so you can look away while Claude works.
sounds/done.wav) when the turn lasted at least minSeconds.| Command | Effect |
|---|---|
/done-sound off / on | Disable / enable the chime (default on) |
/done-sound test | Plays the chime now; reports if audio is unavailable |
| Field | Type | Default | Meaning |
|---|---|---|---|
minSeconds | number | 30 | Minimum turn length, in seconds, for the chime to play |
claude --plugin-dir /path/to/ModsTools/mods/done-sound
/done-sound test tells you).claude plugin validate mods/done-sound
claude plugin test mods/done-sound # 6 testshooks/register.ts 58 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import { shouldPlay } from './format'
5
6const DEFAULT_MIN_SECONDS = 30
7const isEnabled = atom({ plugin: 'done-sound', key: 'isEnabled' } as const, true)
8
9async function chime($: EngineInterface): Promise<boolean> {
10 try {
11 await $.audio.play({ asset: 'sounds/done.wav' })
12
13 return true
14 } catch {
15 return false
16 }
17}
18
19async function answer($: EngineInterface, arg: string): Promise<string> {
20 if (arg === 'off' || arg === 'on') {
21 await update($, isEnabled, () => arg === 'on')
22
23 return `done-sound ${arg}.`
24 }
25 if (arg === 'test') return (await chime($)) ? 'done-sound: played.' : 'done-sound: audio unavailable.'
26
27 return 'Usage: /done-sound off|on|test'
28}
29
30export const register: Register = (on, options) => {
31 const minSeconds = typeof options?.minSeconds === 'number' ? options.minSeconds : DEFAULT_MIN_SECONDS
32 let startedAt: number | null = null
33
34 on('session.start', async ($, e, next) => {
35 await $.command.register({ name: 'done-sound', description: 'done-sound: off|on|test' })
36
37 return next(e)
38 })
39
40 on('prompt.submit', async ($, e, next) => {
41 startedAt = await $.clock.now()
42
43 return next(e)
44 })
45
46 on('turn.complete', async ($, e, next) => {
47 if (e.agentId === undefined && startedAt !== null) {
48 const ms = (await $.clock.now()) - startedAt
49 startedAt = null
50 if (shouldPlay(ms, minSeconds) && (await read($, isEnabled))) await chime($)
51 }
52
53 return next(e)
54 })
55
56 on('command.run', { command: 'done-sound' }, async ($, e) => ({ text: await answer($, e.args.trim()) }))
57}
58hooks/format.ts 4 lines1export function shouldPlay(ms: number, minSeconds: number): boolean {
2 return ms >= minSeconds * 1000
3}
4types/index.d.ts 8 lines1export type Enabled = boolean
2
3declare module 'claude-code' {
4 interface PluginState {
5 'done-sound': { isEnabled: Enabled }
6 }
7}
8