Claude Code — Discord Notification Hook
Sends a Discord message when Claude Code finishes a task, using Claude’s own final message as the summary. Only fires for tasks longer than 2 minutes.
Final working script
Save as ~/claude-discord-notify.sh and chmod +x.
#!/bin/bash
INPUT=$(cat)
SESSION=$(echo "$INPUT" | python3 -c "
import sys, json
d = json.load(sys.stdin)
print(d.get('session_id', 'unknown'))
" 2>/dev/null || echo "unknown")
TRANSCRIPT=$(echo "$INPUT" | python3 -c "
import sys, json
d = json.load(sys.stdin)
print(d.get('transcript_path', ''))
" 2>/dev/null || echo "")
# Get last_assistant_message directly from hook JSON (no API call needed)
SUMMARY=$(echo "$INPUT" | python3 -c "
import sys, json
d = json.load(sys.stdin)
print(d.get('last_assistant_message', '(no message)')[:4096])
" 2>/dev/null || echo "(no message)")
# Calculate duration from transcript timestamps (last user msg → now)
DURATION=0
if [ -f "$TRANSCRIPT" ]; then
DURATION=$(python3 -c "
import json
from datetime import datetime, timezone
last_user_ts = None
with open('$TRANSCRIPT') as f:
for line in f:
try:
d = json.loads(line)
ts = d.get('timestamp')
if ts and d.get('type') == 'user':
last_user_ts = datetime.fromisoformat(ts.replace('Z', '+00:00'))
except:
pass
if last_user_ts:
now = datetime.now(timezone.utc)
print(int((now - last_user_ts).total_seconds()))
else:
print(0)
")
fi
# Skip tasks under 2 minutes
if [ "$DURATION" -lt 120 ]; then
exit 0
fi
DURATION_STR=$(python3 -c "
m, s = divmod($DURATION, 60)
print(f'{m}m {s}s' if m else f'{s}s')
")
curl -s -X POST "YOUR_DISCORD_WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg summary "$SUMMARY" \
--arg duration "$DURATION_STR" \
--arg session "$SESSION" \
'{
username: "Claude Code",
embeds: [{
title: "✅ Task Completed",
description: $summary,
color: 2664261,
fields: [{ name: "Duration", value: $duration, inline: true }],
footer: { text: ("Session: " + $session) },
timestamp: (now | todate)
}]
}')"settings.json hook config
~/.claude/settings.json:
{
"hooks": {
"Stop": [
{
"hooks": [{
"type": "command",
"command": "~/claude-discord-notify.sh"
}]
}
]
}
}Key facts about the hook JSON
Hook stdin contains:
{
"session_id": "...",
"transcript_path": "/home/user/.claude/projects/.../session.jsonl",
"cwd": "/home/user/project",
"hook_event_name": "Stop",
"last_assistant_message": "Here's what I did: ..."
}⚠️ There is no
start_timefield. Duration must be calculated from timestamps inside the.jsonltranscript file.
Known bug (open — 2026-05-12)
Problem: After switching to “last user message → now” duration calculation, the notification stopped firing entirely.
Suspected root cause: The transcript type field for user messages may be
"human" not "user". The script filters d.get('type') == 'user' but if the
actual value is "human", last_user_ts never gets set and DURATION stays 0,
so the script always exits early.
Next debug step:
# Check what type values actually appear in the transcript
head -20 /path/to/session.jsonl | python3 -m json.tool | grep '"type"'If the output shows "human" instead of "user", change the filter line to:
if ts and d.get('type') in ('user', 'human'):Dependencies
# Required
pip install python3 # usually pre-installed
apt install jq curl # usually pre-installedGet Discord webhook URL
Discord → Channel Settings → Integrations → Webhooks → New Webhook → Copy URL.
⚠️ Never commit the webhook URL to git. Store in
~/.bashrcor a secrets file.