Skip to content

Instantly share code, notes, and snippets.

@wickedev
Last active October 24, 2025 00:13
Show Gist options
  • Save wickedev/7c4b4ab3de983c23913c935828d2dd01 to your computer and use it in GitHub Desktop.
Save wickedev/7c4b4ab3de983c23913c935828d2dd01 to your computer and use it in GitHub Desktop.
stop-slack-dm.sh

stop-slack-dm.sh 사용 문서

Claude Code 대화 내용을 Slack DM으로 자동 전송하는 훅 스크립트입니다.

기능

  • Claude Code 세션의 마지막 어시스턴트 응답을 추출
  • Slack API를 통해 지정된 사용자에게 DM 전송
  • 이메일 주소 또는 사용자 ID를 통한 사용자 식별
  • 수동 메시지 전송 기능

설치 및 설정

1. Slack 앱 설정

  1. Slack API에서 새 앱 생성
  2. OAuth & Permissions에서 다음 스코프 추가:
    • chat:write
    • users:read
    • users:read.email
    • im:write
  3. 봇 토큰 복사 (xoxb-로 시작)

2. 스크립트 권한 설정

chmod +x ~/.claude/hooks/stop-slack-dm.sh

사용법

기본 형식

./stop-slack-dm.sh [옵션]

옵션

  • -t TOKEN: Slack 봇 토큰 (또는 SLACK_TOKEN 환경변수 설정)
  • -u USER_ID: Slack 사용자 ID (예: U1234567890)
  • -e EMAIL: 사용자 이메일 주소 (사용자 ID 대신 사용)
  • -m MESSAGE: 전송할 메시지 (미제공시 자동 추출)
  • -h: 도움말 표시

사용 예시

1. 수동 메시지 전송 (사용자 ID)

./stop-slack-dm.sh -t xoxb-your-token -u U1234567890 -m "안녕하세요!"

2. 수동 메시지 전송 (이메일)

./stop-slack-dm.sh -t xoxb-your-token -e [email protected] -m "작업이 완료되었습니다."

3. Claude Code 세션 내용 자동 전송

echo '{"transcript_path": "path/to/file.jsonl", "session_id": "session123"}' | ./stop-slack-dm.sh -t xoxb-your-token -u U1234567890

4. 환경변수 사용

export SLACK_TOKEN="xoxb-your-token"
./stop-slack-dm.sh -u U1234567890 -m "테스트 메시지"

Claude Code 훅으로 사용

설정 방법

Claude Code 설정에서 훅으로 등록하여 세션 완료시 자동으로 실행되도록 설정할 수 있습니다.

훅 설정 예시

{
  "hooks": {
    "session-end": "~/.claude/hooks/stop-slack-dm.sh -t $SLACK_TOKEN -e [email protected]"
  }
}

동작 원리

  1. 입력 처리: stdin에서 JSON 데이터를 읽어 transcript 경로와 session ID 추출
  2. 내용 추출: jq를 사용하여 마지막 어시스턴트 응답의 텍스트 내용 추출
  3. 사용자 조회: 이메일이 제공된 경우 Slack API로 사용자 ID 조회
  4. 대화 개시: 사용자와의 DM 채널 생성
  5. 메시지 전송: 추출된 내용 또는 지정된 메시지를 DM으로 전송

의존성

필수

  • bash (스크립트 실행)
  • curl (Slack API 호출)

선택적

  • jq (JSON 파싱, 없으면 기본 텍스트 처리 사용)

오류 해결

토큰 관련 오류

  • Slack 토큰이 올바른지 확인
  • 봇에게 필요한 권한이 부여되었는지 확인

사용자 조회 실패

  • 이메일 주소가 정확한지 확인
  • 봇이 해당 워크스페이스의 사용자 정보에 접근할 수 있는지 확인

메시지 전송 실패

  • 봇이 DM을 보낼 권한이 있는지 확인
  • 대상 사용자가 봇을 차단하지 않았는지 확인

보안 고려사항

  • Slack 토큰을 환경변수로 관리하여 코드에 하드코딩하지 않기
  • 스크립트 파일 권한을 적절히 설정하여 다른 사용자가 접근할 수 없도록 하기
  • 민감한 정보가 포함된 메시지 전송시 주의

제한사항

  • Slack API 호출 제한에 따라 대량 전송시 지연 가능
  • transcript 파일이 존재하지 않거나 형식이 잘못된 경우 메시지 추출 실패
  • jq가 없는 환경에서는 복잡한 JSON 파싱 제한
#!/bin/bash
# Stop Slack DM Sender Script
# Extract last assistant message content text and send to Slack
#
# Usage:
# ./stop-slack-dm.sh -t TOKEN -u USER_ID -m "Your message"
# ./stop-slack-dm.sh -t TOKEN -e [email protected] -m "Your message"
# echo '{"transcript_path": "path/to/file.jsonl", "session_id": "session123"}' | ./stop-slack-dm.sh -t TOKEN -u USER_ID
set -euo pipefail
# Read stdin with timeout to avoid hanging
INPUT=""
if read -t 1 -N 1 first_char 2>/dev/null; then
INPUT="${first_char}$(cat)"
fi
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Default values
TOKEN=""
USER_ID=""
EMAIL=""
MESSAGE=""
# Function to display usage
usage() {
cat <<EOF
Usage: $0 [OPTIONS]
OPTIONS:
-t TOKEN Slack Bot User OAuth Token (or set SLACK_TOKEN env var)
-u USER_ID Slack User ID (e.g., U1234567890)
-e EMAIL User email address (alternative to user ID)
-m MESSAGE Message to send
-h Show this help message
Note: If no -m option is provided, the script will extract text from the transcript file.
Examples:
$0 -t xoxb-your-token -u U1234567890 -m "Hello!"
echo '{"transcript_path": "path/to/file.jsonl", "session_id": "session123"}' | $0 -t xoxb-your-token -u U1234567890
EOF
exit 1
}
# Function to extract content text from transcript
extract_content_text() {
local input_data="$1"
# Extract transcript_path and session_id from INPUT
local transcript_path=$(echo "$input_data" | grep -o '"transcript_path":"[^"]*"' | cut -d'"' -f4)
local session_id=$(echo "$input_data" | grep -o '"session_id":"[^"]*"' | cut -d'"' -f4)
if [ -n "$transcript_path" ] && [ -f "$transcript_path" ] && [ -n "$session_id" ] && command -v jq >/dev/null 2>&1; then
echo -e "${YELLOW}Extracting content text from: $transcript_path (session: $session_id)${NC}" >&2
# Get last assistant message and extract content[].text
local content_text=$(jq -s --arg sid "$session_id" '
map(select(.sessionId == $sid and .type == "assistant"))
| last
| .message.content[]?.text // empty
' "$transcript_path" 2>/dev/null | jq -r '. // empty' | tr '\n' ' ')
if [ -n "$content_text" ] && [ "$content_text" != "empty" ]; then
echo -e "${GREEN}Successfully extracted content text${NC}" >&2
echo "$content_text"
else
echo -e "${YELLOW}No content text found${NC}" >&2
echo ""
fi
else
echo -e "${YELLOW}Cannot extract content text (missing file/session/jq)${NC}" >&2
echo ""
fi
}
# Function to find user by email
find_user_by_email() {
local email=$1
local response=$(curl -s -X GET \
-H "Authorization: Bearer $TOKEN" \
-H "Content-type: application/json" \
"https://slack.com/api/users.lookupByEmail?email=$email")
local ok=$(echo "$response" | grep -o '"ok":[^,}]*' | cut -d: -f2)
if [ "$ok" = "true" ]; then
echo "$response" | grep -o '"id":"[^"]*' | cut -d'"' -f4
else
local error=$(echo "$response" | grep -o '"error":"[^"]*' | cut -d'"' -f4)
echo -e "${RED}❌ Error finding user by email: $error${NC}" >&2
return 1
fi
}
# Function to open conversation and get channel ID
open_conversation() {
local user_id=$1
local response=$(curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-type: application/json" \
-d "{\"users\": \"$user_id\"}" \
https://slack.com/api/conversations.open)
local ok=$(echo "$response" | grep -o '"ok":[^,}]*' | cut -d: -f2)
if [ "$ok" = "true" ]; then
echo "$response" | grep -o '"id":"[^"]*' | head -1 | cut -d'"' -f4
else
local error=$(echo "$response" | grep -o '"error":"[^"]*' | cut -d'"' -f4)
echo -e "${RED}❌ Error opening conversation: $error${NC}" >&2
return 1
fi
}
# Function to send message
send_message() {
local channel_id=$1
local message=$2
# Create JSON payload with jq if available
if command -v jq >/dev/null 2>&1; then
local json_payload=$(jq -n --arg channel "$channel_id" --arg text "$message" '{channel: $channel, text: $text}')
else
local escaped_message=$(echo "$message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g; s/$/\\n/g' | tr -d '\n' | sed 's/\\n$//')
local json_payload="{\"channel\": \"$channel_id\", \"text\": \"$escaped_message\"}"
fi
local response=$(curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-type: application/json" \
-d "$json_payload" \
https://slack.com/api/chat.postMessage)
local ok=$(echo "$response" | grep -o '"ok":[^,}]*' | cut -d: -f2)
if [ "$ok" = "true" ]; then
echo -e "${GREEN}✅ Message sent successfully!${NC}"
return 0
else
local error=$(echo "$response" | grep -o '"error":"[^"]*' | cut -d'"' -f4)
echo -e "${RED}❌ Error sending message: $error${NC}" >&2
return 1
fi
}
# Parse command line arguments
while getopts "t:u:e:m:h" opt; do
case $opt in
t)
TOKEN="$OPTARG"
;;
u)
USER_ID="$OPTARG"
;;
e)
EMAIL="$OPTARG"
;;
m)
MESSAGE="$OPTARG"
;;
h)
usage
;;
\?)
echo "Invalid option: -$OPTARG" >&2
usage
;;
esac
done
# Check for token (from argument or environment variable)
if [ -z "$TOKEN" ]; then
if [ -n "${SLACK_TOKEN:-}" ]; then
TOKEN="$SLACK_TOKEN"
else
echo -e "${RED}Error: Slack token not provided. Use -t option or set SLACK_TOKEN environment variable${NC}" >&2
usage
fi
fi
# Check for user identification
if [ -z "$USER_ID" ] && [ -z "$EMAIL" ]; then
echo -e "${RED}Error: Either user ID (-u) or email (-e) must be provided${NC}" >&2
usage
fi
# Get message - if no -m provided, extract from transcript
if [ -z "$MESSAGE" ]; then
if [ -n "$INPUT" ]; then
MESSAGE=$(extract_content_text "$INPUT")
if [ -z "$MESSAGE" ]; then
echo -e "${RED}Error: No content text extracted and no message provided${NC}" >&2
exit 1
fi
else
echo -e "${RED}Error: Message not provided and no input available${NC}" >&2
usage
fi
fi
# If email provided, convert to user ID
if [ -n "$EMAIL" ]; then
echo -e "${YELLOW}Finding user by email: $EMAIL${NC}"
if USER_ID=$(find_user_by_email "$EMAIL"); then
echo -e "${GREEN}Found user ID: $USER_ID${NC}"
else
echo "Error: Failed to find user by email" >&2
exit 1
fi
fi
# Open conversation with user
echo -e "${YELLOW}Opening conversation with user: $USER_ID${NC}"
if CHANNEL_ID=$(open_conversation "$USER_ID"); then
echo -e "${GREEN}Successfully opened conversation${NC}" >&2
else
echo "Error: Failed to open conversation" >&2
exit 1
fi
# Send the message
echo -e "${YELLOW}Sending message...${NC}"
if send_message "$CHANNEL_ID" "$MESSAGE"; then
echo -e "${GREEN}Script completed successfully${NC}" >&2
exit 0
else
echo "Error: Failed to send message" >&2
exit 1
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment