HookStack
Back to catalogue
NotificationNotification

System sound notification

A chime to catch your attention, no voice needed

Plays a system sound (Glass.aiff on macOS, paplay on Linux, PowerShell beep on Windows) when the agent needs the user's attention. A silent alternative to TTS that works offline and requires no voice engine.

What does the System sound notification hook do?

System sound notification is a Claude Code Notification hook. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. A chime to catch your attention, no voice needed.

As a Notification hook it runs after the action, reacting to what just happened rather than blocking it. Because it is a deterministic Node.js script, it executes on every matching event without relying on the model to remember — the guarantee that makes agentic workflows safe to automate.

Use cases

  • Alert the user during long autonomous runs
  • Works in silent environments where TTS is unwanted
  • Minimal-distraction attention signal

Tags

#notification#sound#audio#macos#cross-platform

settings.json fragment

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/notification-sound.mjs",
            "type": "command"
          }
        ],
        "matcher": ""
      }
    ]
  }
}

Script · .claude/hooks/notification-sound.mjs

#!/usr/bin/env node
// @hookstack notification-sound
// Joue un son système quand Claude attend l'utilisateur (Notification)
// Sur macOS, clic sur la notif ramène le bon contexte (terminal ou Claude app)
import { execSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

// Bundle IDs des terminaux courants — clic ramène le bon contexte
const TERMINAL_BUNDLE_IDS = {
	"iTerm.app": "com.googlecode.iterm2",
	Apple_Terminal: "com.apple.Terminal",
	WezTerm: "com.github.wez.wezterm",
	ghostty: "com.mitchellh.ghostty",
	vscode: "com.microsoft.VSCode",
	cursor: "com.todesktop.230313mzl4w4u92",
};
const CLAUDE_APP_BUNDLE_ID = "com.anthropic.claudefordesktop";

export function resolveActivateBundle(termProgram) {
	return TERMINAL_BUNDLE_IDS[termProgram] ?? CLAUDE_APP_BUNDLE_ID;
}

function defaultExec(cmd) {
	execSync(cmd, { timeout: 5_000, stdio: "ignore", shell: true });
}

function defaultHasTerminalNotifier() {
	try {
		execSync("which terminal-notifier", {
			timeout: 2_000,
			stdio: "pipe",
			shell: true,
		});
		return true;
	} catch {
		return false;
	}
}

export function run(
	_input,
	{
		exec = defaultExec,
		hasTerminalNotifier = defaultHasTerminalNotifier,
		platform = process.platform,
		termProgram = process.env.TERM_PROGRAM,
	} = {},
) {
	try {
		if (platform === "darwin") {
			const bundleId = resolveActivateBundle(termProgram);

			if (hasTerminalNotifier()) {
				// Clic sur la notif → ramène automatiquement le bon contexte
				exec(
					`terminal-notifier -title "Claude Code" -message "Claude needs your input" -activate ${bundleId} -sound Glass`,
				);
			} else {
				// Fallback : son + notification sans action de clic (brew install terminal-notifier pour activer)
				exec("afplay /System/Library/Sounds/Glass.aiff");
				exec(
					'osascript -e \'display notification "Claude needs your input" with title "Claude Code"\'',
				);
			}
		} else if (platform === "linux") {
			exec(
				"paplay /usr/share/sounds/freedesktop/stereo/message.oga 2>/dev/null || aplay /usr/share/sounds/alsa/Front_Center.wav 2>/dev/null || true",
			);
		} else if (platform === "win32") {
			exec('powershell -c "[console]::beep(660, 300)"');
		}
	} catch {
		// Son absent ou erreur — non bloquant
	}
	return null;
}

/* v8 ignore next 4 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
	const input = JSON.parse(readFileSync(0, "utf8"));
	run(input);
}

Learn more

Related hooks