HookStackGitHub
Back to catalogue
NotificationSubagentStopSubagentStopWhen a subagent finishes· non-blocking

Subagent end voice summary

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

Use cases

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

Providers & tags

Claude Code
#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
// Annonce la fin d'un sous-agent par TTS (SubagentStop)
import { readFileSync } from 'fs';
import { execSync } from 'child_process';
import { fileURLToPath } from '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);
}