diff --git a/docs/custom-hooks.md b/docs/custom-hooks.md index f6d752a9..b338ed01 100644 --- a/docs/custom-hooks.md +++ b/docs/custom-hooks.md @@ -107,7 +107,7 @@ customPolicies.add({ |-------|--------------|----------------------| | `PreToolUse` | Before Claude runs a tool | The tool's input (e.g. `{ command: "..." }` for Bash) | | `PostToolUse` | After a tool completes | The tool's input + `tool_result` (the output) | -| `Notification` | When Claude sends a notification | `{ message: "..." }` | +| `Notification` | When Claude sends a notification | `{ message: "...", notification_type: "idle" \| "permission_prompt" \| ... }` — hooks must always return `allow()`, they cannot block notifications | | `Stop` | When the Claude session ends | Empty | --- diff --git a/examples/policies-advanced/index.js b/examples/policies-advanced/index.js index 49f2af20..25ed7c8a 100644 --- a/examples/policies-advanced/index.js +++ b/examples/policies-advanced/index.js @@ -99,3 +99,48 @@ customPolicies.add({ return allow(); }, }); + +// 6. Notification event: forward Claude's idle notifications to Slack +customPolicies.add({ + name: "slack-on-idle-notification", + description: "Forward Claude idle notifications to Slack (set SLACK_WEBHOOK_URL env var)", + match: { events: ["Notification"] }, + fn: async (ctx) => { + const webhookUrl = process.env.SLACK_WEBHOOK_URL; + if (!webhookUrl) return allow(); // skip if not configured + + const type = String(ctx.payload?.notification_type ?? ""); + if (type !== "idle") return allow(); // only forward idle notifications + + const message = String(ctx.payload?.message ?? "Claude is waiting for input"); + const cwd = ctx.session?.cwd ?? "unknown"; + const sessionId = ctx.session?.sessionId ?? "unknown"; + + // Fire-and-forget — never block Claude if Slack is unreachable + fetch(webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + blocks: [ + { + type: "header", + text: { type: "plain_text", text: "💬 Claude is waiting for you", emoji: true }, + }, + { + type: "section", + text: { type: "mrkdwn", text: message }, + }, + { + type: "section", + fields: [ + { type: "mrkdwn", text: `*Project*\n\`${cwd}\`` }, + { type: "mrkdwn", text: `*Session*\n\`${sessionId}\`` }, + ], + }, + ], + }), + }).catch(() => {}); + + return allow(); // Notification hooks must always return allow + }, +});