HookStack
Back to catalogue
NotificationSubagentStop

Subagent end voice summary

Hear each subagent finish, no audio clash

Announces the end of a subagent with a voice message. Uses a file lock to avoid TTS overlap when several subagents finish in parallel.

What does the Subagent end voice summary hook do?

Subagent end voice summary is a Claude Code SubagentStop hook. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Hear each subagent finish, no audio clash.

As a SubagentStop 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

  • Audio tracking of completed parallel tasks
  • Ordered subagent-end announcements without sound collisions

Tags

#tts#subagent#voice#parallel-agents#locking#audio

settings.json fragment

{
  "hooks": {
    "SubagentStop": [
      {
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/subagent-stop-tts.mjs",
            "type": "command"
          }
        ],
        "matcher": ""
      }
    ]
  }
}

Script · .claude/hooks/subagent-stop-tts.mjs

#!/usr/bin/env node
// @hookstack subagent-stop-tts-summary
import { execSync } from "node:child_process";
// @hookstack subagent-stop-tts-summary
// Annonce la fin d'un sous-agent par TTS (SubagentStop)
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

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

export function run(
	input,
	{ exec = defaultExec, platform = process.platform } = {},
) {
	const summary = input?.summary ?? "";
	const text = summary
		? `Sous-agent terminé : ${summary.slice(0, 100).replace(/[`*_#]/g, "")}`
		: "Sous-agent terminé";
	const safe = text.replace(/"/g, '\\"');

	try {
		if (platform === "darwin") exec(`say "${safe}"`);
		else exec(`espeak "${safe}" 2>/dev/null`);
	} catch {}
	return text;
}

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

Learn more

Related hooks