diff --git a/.claude/commands/new-hook.md b/.claude/commands/new-hook.md
index 4b86ad9..453b447 100644
--- a/.claude/commands/new-hook.md
+++ b/.claude/commands/new-hook.md
@@ -255,6 +255,18 @@ export default defineHooks({
- Always check the hook structure before calling
- Test with realistic hook inputs matching actual Claude Code events
+### 10. Before Publishing
+
+**Git workflow before pushing:**
+- If not on the master branch, rebase from master to ensure your changes are up to date:
+ ```bash
+ git checkout master
+ git pull origin master
+ git checkout feature/your-branch
+ git rebase master
+ ```
+- This ensures your hook is based on the latest code and prevents merge conflicts
+
## Let me create the hook for you
Based on your requirements, I'll create the appropriate hook structure and implementation.
\ No newline at end of file
diff --git a/README.md b/README.md
index 217aeef..300c397 100644
--- a/README.md
+++ b/README.md
@@ -100,6 +100,11 @@ The library includes several predefined hook utilities for common logging scenar
| **`logSubagentStopEvents`**
Logs subagent stop events | • `maxEventsStored` (default: 100)
• `logFileName` (default: 'hook-log.stop.json') |
| **`logNotificationEvents`**
Logs notification messages | • `maxEventsStored` (default: 100)
• `logFileName` (default: 'hook-log.notification.json') |
| **`blockEnvFiles`**
Blocks access to .env files | No options - blocks all .env file variants except example files |
+| **`announceStop`**
Announces task completion via TTS | • `message` (default: 'Task completed')
• `voice` (system-specific voice name)
• `rate` (speech rate in WPM)
• `customCommand` (custom TTS command)
• `suppressOutput` (default: false) |
+| **`announceSubagentStop`**
Announces subagent completion via TTS | Same options as `announceStop` |
+| **`announcePreToolUse`**
Announces before tool execution | • First param: `matcher` (regex pattern, defaults to '.*')
• `message` (default: 'Using {toolName}')
• `voice`, `rate`, `customCommand`, `suppressOutput` |
+| **`announcePostToolUse`**
Announces after tool execution | • First param: `matcher` (regex pattern, defaults to '.*')
• `message` (default: '{toolName} completed')
• `voice`, `rate`, `customCommand`, `suppressOutput` |
+| **`announceNotification`**
Speaks notification messages | • `message` (default: '{message}')
• `voice`, `rate`, `customCommand`, `suppressOutput` |
All predefined hooks:
@@ -356,6 +361,82 @@ The `blockEnvFiles` hook:
- Works with `Read`, `Write`, `Edit`, and `MultiEdit` tools
- Provides clear error messages when access is blocked
+### Text-to-Speech Announcements
+
+```typescript
+import {
+ defineHooks,
+ announceStop,
+ announceSubagentStop,
+ announcePreToolUse,
+ announcePostToolUse,
+ announceNotification,
+} from "@timoaus/define-claude-code-hooks";
+
+// Basic usage - announce all events
+export default defineHooks({
+ Stop: [announceStop()],
+ SubagentStop: [announceSubagentStop()],
+ PreToolUse: [announcePreToolUse()], // Announces all tools
+ PostToolUse: [announcePostToolUse()], // Announces all tools
+ Notification: [announceNotification()],
+});
+
+// Announce specific tools only
+export default defineHooks({
+ PreToolUse: [
+ announcePreToolUse('Bash|Write|Edit', {
+ message: "Running {toolName}"
+ })
+ ],
+ PostToolUse: [
+ announcePostToolUse('Bash|Write|Edit', {
+ message: "{toolName} finished"
+ })
+ ],
+});
+
+// With custom voices and messages
+export default defineHooks({
+ Stop: [
+ announceStop({
+ message: "Claude has finished the task for session {sessionId}",
+ voice: "Samantha", // macOS voice
+ rate: 200,
+ })
+ ],
+ Notification: [
+ announceNotification({
+ message: "Claude says: {message}",
+ voice: "Daniel"
+ })
+ ],
+});
+
+// With custom TTS command (for Linux/Windows)
+export default defineHooks({
+ Stop: [
+ announceStop({
+ customCommand: "espeak -s 150 '{message}'", // Linux
+ // or for Windows PowerShell:
+ // customCommand: "powershell -Command \"(New-Object -ComObject SAPI.SpVoice).Speak('{message}')\""
+ })
+ ],
+});
+```
+
+The announcement hooks:
+- Use text-to-speech to announce various Claude Code events
+- Support macOS (say), Linux (espeak), and Windows (PowerShell SAPI)
+- Allow custom messages with template variables:
+ - `{sessionId}` - The session ID
+ - `{timestamp}` - Current timestamp
+ - `{toolName}` - Tool name (for PreToolUse/PostToolUse)
+ - `{message}` - Notification message (for Notification hook)
+- Support voice selection and speech rate customization
+- Can use custom TTS commands for other systems
+- Run asynchronously without blocking Claude Code
+
### Combining Multiple Hooks
```typescript
@@ -365,6 +446,7 @@ import {
logPreToolUseEvents,
logPostToolUseEvents,
blockEnvFiles,
+ announceStop,
} from "@timoaus/define-claude-code-hooks";
export default defineHooks({
@@ -382,7 +464,10 @@ export default defineHooks({
PostToolUse: logPostToolUseEvents({ logFileName: "hook-log.tool-use.json" }),
- Stop: [logStopEvents("hook-log.stop.json")],
+ Stop: [
+ logStopEvents("hook-log.stop.json"),
+ announceStop({ message: "Task completed successfully!" }),
+ ],
});
```
diff --git a/src/hooks/announceHooks.ts b/src/hooks/announceHooks.ts
new file mode 100644
index 0000000..d2b9000
--- /dev/null
+++ b/src/hooks/announceHooks.ts
@@ -0,0 +1,288 @@
+import { defineHook } from '../index';
+import { StopInput, SubagentStopInput, PreToolUseInput, PostToolUseInput, NotificationInput, AnyHookDefinition } from '../types';
+import { spawn } from 'child_process';
+
+export interface AnnouncementOptions {
+ /**
+ * The message to speak.
+ * Supports template variables:
+ * - {sessionId} - The session ID
+ * - {timestamp} - Current timestamp
+ * - {toolName} - Tool name (for tool hooks)
+ * - {message} - Notification message (for notification hook)
+ * @default Varies by hook type
+ */
+ message?: string;
+
+ /**
+ * The voice to use for TTS (system-specific).
+ * On macOS: Use 'say -v ?' to list available voices
+ * On Linux: Depends on the TTS engine installed
+ * @default undefined (uses system default)
+ */
+ voice?: string;
+
+ /**
+ * The speech rate (words per minute).
+ * @default undefined (uses system default)
+ */
+ rate?: number;
+
+ /**
+ * Whether to suppress the hook's console output
+ * @default false
+ */
+ suppressOutput?: boolean;
+
+ /**
+ * Custom TTS command to use instead of the default.
+ * Should include {message} placeholder for the text to speak.
+ * Example: "espeak '{message}'" or "say -v Daniel '{message}'"
+ * @default undefined (auto-detects based on platform)
+ */
+ customCommand?: string;
+}
+
+// Helper function to speak text
+async function speak(text: string, options: AnnouncementOptions): Promise<{ suppressOutput?: boolean }> {
+ try {
+ const { voice, rate, suppressOutput = false, customCommand } = options;
+
+ // Determine the TTS command based on platform
+ let command: string = '';
+ let args: string[] = [];
+
+ if (customCommand) {
+ // Use custom command
+ const fullCommand = customCommand.replace('{message}', text);
+ // Split command and args (simple split, doesn't handle quoted args perfectly)
+ const parts = fullCommand.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
+ command = parts[0] || '';
+ args = parts.slice(1).map(arg => arg.replace(/^"|"$/g, ''));
+ } else if (process.platform === 'darwin') {
+ // macOS: use 'say' command
+ command = 'say';
+ args = [];
+ if (voice) args.push('-v', voice);
+ if (rate) args.push('-r', rate.toString());
+ args.push(text);
+ } else if (process.platform === 'linux') {
+ // Linux: try espeak (most common)
+ command = 'espeak';
+ args = [];
+ if (rate) args.push('-s', rate.toString());
+ args.push(text);
+ } else if (process.platform === 'win32') {
+ // Windows: use PowerShell with SAPI
+ command = 'powershell';
+ args = [
+ '-Command',
+ `Add-Type -AssemblyName System.Speech; ` +
+ `$synth = New-Object System.Speech.Synthesis.SpeechSynthesizer; ` +
+ (voice ? `$synth.SelectVoice("${voice}"); ` : '') +
+ (rate ? `$synth.Rate = ${Math.round((rate - 150) / 15)}; ` : '') + // Convert WPM to -10 to 10 scale
+ `$synth.Speak("${text.replace(/"/g, '`"')}")`
+ ];
+ } else {
+ if (!suppressOutput) {
+ console.log('[TTS] Unsupported platform for text-to-speech');
+ }
+ return { suppressOutput };
+ }
+
+ // Spawn the TTS process
+ const ttsProcess = spawn(command, args, {
+ detached: false,
+ stdio: 'ignore'
+ });
+
+ // Don't wait for the process to complete - let it run in background
+ ttsProcess.unref();
+
+ if (!suppressOutput) {
+ console.log(`[TTS] Speaking: "${text}"`);
+ }
+
+ return { suppressOutput };
+ } catch (error) {
+ if (!options.suppressOutput) {
+ console.error('[TTS] Error:', error instanceof Error ? error.message : String(error));
+ }
+ return { suppressOutput: options.suppressOutput };
+ }
+}
+
+/**
+ * Creates a Stop hook that announces when tasks complete using text-to-speech.
+ *
+ * @example
+ * ```typescript
+ * import { defineHooks, announceStop } from 'define-claude-code-hooks';
+ *
+ * export default defineHooks({
+ * Stop: [announceStop()]
+ * });
+ * ```
+ *
+ * @example
+ * ```typescript
+ * // With custom message and voice
+ * export default defineHooks({
+ * Stop: [announceStop({
+ * message: "Claude has finished the task for session {sessionId}",
+ * voice: "Samantha", // macOS voice
+ * rate: 200
+ * })]
+ * });
+ * ```
+ */
+export const announceStop = (options: AnnouncementOptions = {}): AnyHookDefinition<'Stop'> => {
+ const defaultMessage = "Task completed";
+
+ return defineHook('Stop', async (input: StopInput) => {
+ const message = (options.message || defaultMessage)
+ .replace('{sessionId}', input.session_id)
+ .replace('{timestamp}', new Date().toLocaleString());
+
+ return speak(message, options);
+ });
+};
+
+/**
+ * Creates a SubagentStop hook that announces subagent task completion using text-to-speech.
+ *
+ * @example
+ * ```typescript
+ * import { defineHooks, announceSubagentStop } from 'define-claude-code-hooks';
+ *
+ * export default defineHooks({
+ * SubagentStop: [announceSubagentStop()]
+ * });
+ * ```
+ */
+export const announceSubagentStop = (options: AnnouncementOptions = {}): AnyHookDefinition<'SubagentStop'> => {
+ const defaultMessage = "Subagent task completed";
+
+ return defineHook('SubagentStop', async (input: SubagentStopInput) => {
+ const message = (options.message || defaultMessage)
+ .replace('{sessionId}', input.session_id)
+ .replace('{timestamp}', new Date().toLocaleString());
+
+ return speak(message, options);
+ });
+};
+
+/**
+ * Creates a PreToolUse hook that announces tool usage before execution.
+ *
+ * @example
+ * ```typescript
+ * import { defineHooks, announcePreToolUse } from 'define-claude-code-hooks';
+ *
+ * export default defineHooks({
+ * PreToolUse: [
+ * announcePreToolUse('.*') // Announce all tools
+ * ]
+ * });
+ * ```
+ *
+ * @example
+ * ```typescript
+ * // Only announce specific tools
+ * export default defineHooks({
+ * PreToolUse: [
+ * announcePreToolUse('Bash|Write|Edit', {
+ * message: "Running {toolName} tool"
+ * })
+ * ]
+ * });
+ * ```
+ */
+export const announcePreToolUse = (matcher: string = '.*', options: AnnouncementOptions = {}): AnyHookDefinition<'PreToolUse'> => {
+ const defaultMessage = "Using {toolName}";
+
+ return defineHook('PreToolUse', {
+ matcher,
+ handler: async (input: PreToolUseInput) => {
+ const message = (options.message || defaultMessage)
+ .replace('{toolName}', input.tool_name)
+ .replace('{sessionId}', input.session_id)
+ .replace('{timestamp}', new Date().toLocaleString());
+
+ return speak(message, options);
+ }
+ });
+};
+
+/**
+ * Creates a PostToolUse hook that announces tool usage after execution.
+ *
+ * @example
+ * ```typescript
+ * import { defineHooks, announcePostToolUse } from 'define-claude-code-hooks';
+ *
+ * export default defineHooks({
+ * PostToolUse: [
+ * announcePostToolUse('.*', {
+ * message: "{toolName} completed"
+ * })
+ * ]
+ * });
+ * ```
+ */
+export const announcePostToolUse = (matcher: string = '.*', options: AnnouncementOptions = {}): AnyHookDefinition<'PostToolUse'> => {
+ const defaultMessage = "{toolName} completed";
+
+ return defineHook('PostToolUse', {
+ matcher,
+ handler: async (input: PostToolUseInput) => {
+ const message = (options.message || defaultMessage)
+ .replace('{toolName}', input.tool_name)
+ .replace('{sessionId}', input.session_id)
+ .replace('{timestamp}', new Date().toLocaleString());
+
+ return speak(message, options);
+ }
+ });
+};
+
+/**
+ * Creates a Notification hook that speaks notification messages aloud.
+ *
+ * @example
+ * ```typescript
+ * import { defineHooks, announceNotification } from 'define-claude-code-hooks';
+ *
+ * export default defineHooks({
+ * Notification: [announceNotification()]
+ * });
+ * ```
+ *
+ * @example
+ * ```typescript
+ * // With custom prefix
+ * export default defineHooks({
+ * Notification: [
+ * announceNotification({
+ * message: "Claude says: {message}"
+ * })
+ * ]
+ * });
+ * ```
+ */
+export const announceNotification = (options: AnnouncementOptions = {}): AnyHookDefinition<'Notification'> => {
+ const defaultMessage = "{message}";
+
+ return defineHook('Notification', async (input: NotificationInput) => {
+ const message = (options.message || defaultMessage)
+ .replace('{message}', input.message)
+ .replace('{sessionId}', input.session_id)
+ .replace('{timestamp}', new Date().toLocaleString());
+
+ return speak(message, options);
+ });
+};
+
+// Legacy exports for backwards compatibility
+export const announceTaskCompletion = announceStop;
+export const announceSubagentTaskCompletion = announceSubagentStop;
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index 2230ab4..76e2f0a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -10,6 +10,16 @@ export {
logPostToolUseEvents
} from './hooks/logToolUseEvents';
export { blockEnvFiles } from './hooks/blockEnvFiles';
+export {
+ announceStop,
+ announceSubagentStop,
+ announcePreToolUse,
+ announcePostToolUse,
+ announceNotification,
+ // Legacy exports
+ announceTaskCompletion,
+ announceSubagentTaskCompletion
+} from './hooks/announceHooks';
/**
* Define a typed hook handler for Claude Code