Claude Code Hooks로 블로그 글 자동 전송하기: n8n Webhook 연동
Claude Code Hooks 기능으로 블로그 글 작성부터 Ghost 전송까지 완전 자동화하는 파이프라인을 구축합니다. /blog 실행만 하면 나머지는 자동!
1. 문제 상황
이전 글에서 n8n + Ghost 연동을 완료했습니다. Webhook으로 마크다운을 보내면 Ghost에 초안이 생성됩니다.
하지만 여전히 수동 작업이 남아있었습니다:
현재 워크플로우:
1. Claude Code에서 /blog 실행
2. docs/blog-drafts/에 마크다운 파일 생성됨
3. 수동으로 curl 명령어 실행 (또는 파일 내용 복사) ← 이 단계가 귀찮음
4. n8n이 Ghost에 전송
5. Ghost에서 확인 및 발행
매번 curl을 치거나 파일을 복사하는 건 자동화의 의미가 없습니다.
목표: /blog 커맨드 실행 → 글 작성 완료 → 자동으로 n8n Webhook 호출
2. 해결 방법: Claude Code Hooks
Claude Code는 Hooks 기능을 제공합니다. 특정 이벤트 발생 시 셸 스크립트를 자동 실행할 수 있습니다.
2.1 Hook 이벤트 종류
| 이벤트 | 실행 시점 | 용도 |
|---|---|---|
| PreToolUse | 도구 실행 전 | 실행 차단, 입력 검증 |
| PostToolUse | 도구 실행 후 | 후처리, 알림, 연동 |
| Notification | 알림 발생 시 | 외부 알림 시스템 연동 |
| Stop | 세션 종료 시 | 정리 작업 |
Write 도구가 실행된 후 PostToolUse hook을 사용하면 됩니다.
2.2 Hook이 받는 데이터
PostToolUse hook은 stdin으로 JSON 데이터를 받습니다:
{
"session_id": "abc123",
"hook_event_name": "PostToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/docs/blog-drafts/my-post.md",
"content": "# 제목\n\n본문 내용..."
},
"tool_response": {
"filePath": "/path/to/docs/blog-drafts/my-post.md",
"success": true
}
}
tool_input.file_path로 어디에 파일이 생성되었는지, tool_input.content로 파일 내용을 알 수 있습니다.
3. 구현
3.1 디렉토리 구조
~/.claude/
├── settings.json # Hook 설정
├── hooks/
│ └── send-blog-to-ghost.sh # Webhook 전송 스크립트
└── ...
3.2 settings.json에 Hook 추가
~/.claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "/Users/username/.claude/hooks/send-blog-to-ghost.sh",
"timeout": 10000
}
]
}
]
}
}
설정 설명:
| 항목 | 값 | 설명 |
|---|---|---|
| matcher | "Write" |
Write 도구 실행 시에만 hook 실행 |
| type | "command" |
셸 명령어 실행 |
| command | 스크립트 경로 | 실행할 스크립트 (절대 경로) |
| timeout | 10000 |
10초 타임아웃 (ms) |
matcher 패턴 예시:
"matcher": "Write" // Write 도구만
"matcher": "Write|Edit" // Write 또는 Edit
"matcher": ".*" // 모든 도구
3.3 Webhook 전송 스크립트
~/.claude/hooks/send-blog-to-ghost.sh:
#!/bin/bash
# /blog 커맨드로 작성된 글을 Ghost webhook으로 전송
# PostToolUse hook에서 호출됨 (Write 도구 사용 후)
# stdin에서 JSON 데이터 읽기
INPUT=$(cat)
# jq로 파일 경로와 내용 추출
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')
# 파일 경로가 없으면 종료
if [ -z "$FILE_PATH" ]; then
exit 0
fi
# docs/blog-drafts/ 경로인 경우에만 처리
if [[ "$FILE_PATH" == *"docs/blog-drafts/"* ]]; then
FILENAME=$(basename "$FILE_PATH")
# JSON payload 생성 및 webhook 전송
PAYLOAD=$(jq -n \
--arg filename "$FILENAME" \
--arg filepath "$FILE_PATH" \
--arg content "$CONTENT" \
'{filename: $filename, filepath: $filepath, content: $content}')
curl -s -X POST \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"https://n8n.example.dev/webhook/ghost-post" \
> /dev/null 2>&1 &
# 로그 (디버깅용, 필요시 주석 해제)
# echo "[$(date)] Sent to Ghost: $FILENAME" >> ~/.claude/hooks/blog-webhook.log
fi
exit 0
스크립트 핵심 로직:
# 1. stdin에서 JSON 읽기 (PostToolUse가 전달)
INPUT=$(cat)
# 2. jq로 필요한 값 추출
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty')
# 3. 경로 필터링: docs/blog-drafts/ 포함 시에만 처리
if [[ "$FILE_PATH" == *"docs/blog-drafts/"* ]]; then
# 4. JSON payload 생성
PAYLOAD=$(jq -n \
--arg filename "$FILENAME" \
--arg content "$CONTENT" \
'{filename: $filename, content: $content}')
# 5. webhook 전송 (백그라운드)
curl -s -X POST -d "$PAYLOAD" "https://..." &
fi
실행 권한 부여:
chmod +x ~/.claude/hooks/send-blog-to-ghost.sh
3.4 Webhook Payload 형식
n8n으로 전송되는 JSON:
{
"filename": "claude-code-hooks-blog-automation.md",
"filepath": "/Users/username/project/docs/blog-drafts/claude-code-hooks-blog-automation.md",
"content": "# Claude Code Hooks로 블로그 글 자동 전송하기\n\n> 요약...\n\n## 1. 문제 상황..."
}
이전 글에서 만든 n8n 워크플로우가 이 형식을 받아서 Ghost API로 전송합니다.
4. 전체 파이프라인
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Claude Code │───▶│ PostToolUse │───▶│ n8n │
│ /blog 커맨드 │ │ Hook 실행 │ │ Webhook 수신 │
└─────────────────┘ └─────────────────┘ └────────┬────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Ghost Admin │◀───│ Ghost API │◀───│ Code 노드 │
│ 초안 확인/발행 │ │ POST /posts │ │ Markdown 파싱 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
자동화된 워크플로우:
Before (수동):
1. /blog 실행
2. 파일 생성됨
3. curl로 webhook 호출 (수동) ← 귀찮음
4. Ghost에서 확인
After (자동):
1. /blog 실행
2. 파일 생성됨 → Hook 자동 실행 → webhook 자동 호출
3. Ghost에서 확인
5. 설정 적용 방법
5.1 전역 vs 프로젝트 설정
| 설정 파일 | 적용 범위 | 우선순위 |
|---|---|---|
~/.claude/settings.json |
모든 프로젝트 | 낮음 |
.claude/settings.json |
해당 프로젝트만 | 높음 |
.claude/settings.local.json |
해당 프로젝트 (git 제외) | 가장 높음 |
전역 설정 (모든 프로젝트에서 /blog 사용 시):
# ~/.claude/settings.json에 hooks 추가
프로젝트 설정 (특정 프로젝트에서만):
mkdir -p .claude
# .claude/settings.json 생성
5.2 기존 설정과 병합
이미 settings.json에 다른 설정이 있다면 hooks 섹션만 추가:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": "/Users/username/.claude/hooks/send-blog-to-ghost.sh",
"timeout": 10000
}
]
}
]
},
"existingKey": "existingValue"
}
5.3 설정 적용 확인
# 현재 hook 설정 확인 (Claude Code 내에서)
/hooks
중요: Hook 설정은 새 Claude Code 세션부터 적용됩니다.
6. 동작 확인
6.1 테스트 순서
# 1. 새 터미널에서 Claude Code 시작
cd ~/your-project
claude
# 2. /blog 실행
/blog 테스트 주제
# 3. 파일 생성 확인
ls docs/blog-drafts/
# 4. n8n 실행 로그 확인
# n8n UI → Executions
# 5. Ghost Admin에서 초안 확인
6.2 로그로 디버깅
스크립트에서 로그를 활성화:
# send-blog-to-ghost.sh에서 주석 해제
echo "[$(date)] Sent to Ghost: $FILENAME" >> ~/.claude/hooks/blog-webhook.log
로그 확인:
tail -f ~/.claude/hooks/blog-webhook.log
6.3 수동 테스트
스크립트를 직접 실행해서 테스트:
echo '{"tool_input":{"file_path":"/test/docs/blog-drafts/test.md","content":"# Test"}}' | \
~/.claude/hooks/send-blog-to-ghost.sh
7. 트러블슈팅
7.1 Hook이 실행되지 않음
원인 1: 설정 파일 위치 오류
해결: ~/.claude/settings.json 또는 .claude/settings.json 확인
원인 2: 새 세션에서 실행하지 않음
해결: Claude Code 재시작
원인 3: matcher 오타
해결: "Write" 대소문자 정확히 확인
원인 4: JSON 문법 오류
해결: jq . ~/.claude/settings.json 으로 검증
7.2 jq 명령어 없음
# macOS
brew install jq
# Ubuntu/Debian
sudo apt install jq
# 확인
jq --version
7.3 Permission denied
# 실행 권한 부여
chmod +x ~/.claude/hooks/send-blog-to-ghost.sh
# 확인
ls -la ~/.claude/hooks/
7.4 Webhook 전송 실패
# 수동 테스트
curl -v -X POST \
-H "Content-Type: application/json" \
-d '{"filename":"test.md","filepath":"/test","content":"# Test"}' \
https://n8n.example.dev/webhook/ghost-post
확인 사항:
- n8n이 실행 중인지
- Webhook 노드가 활성화되어 있는지
- 방화벽/네트워크 문제
7.5 HTTP → HTTPS 리다이렉트 문제
원인: Cloudflare 등에서 HTTP를 HTTPS로 리다이렉트
해결: webhook URL을 https://로 변경
# ❌ 잘못됨 (301 리다이렉트)
curl http://n8n.example.dev/webhook/...
# ✅ 올바름
curl https://n8n.example.dev/webhook/...
7.6 한글 깨짐
# 스크립트 상단에 추가
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
7.7 타임아웃 발생
{
"timeout": 30000 // 30초로 늘림
}
또는 백그라운드 실행 확인:
curl ... & # & 있는지 확인
8. 고급 설정
8.1 특정 파일명 패턴만 전송
_final.md로 끝나는 파일만:
if [[ "$FILE_PATH" == *"docs/blog-drafts/"*"_final.md" ]]; then
# webhook 전송
fi
published/ 폴더 제외:
if [[ "$FILE_PATH" == *"docs/blog-drafts/"* ]] && \
[[ "$FILE_PATH" != *"docs/blog-drafts/published/"* ]]; then
# webhook 전송
fi
8.2 Webhook 인증 추가
curl -s -X POST \
-H "Content-Type: application/json" \
-H "X-API-Key: your-secret-key" \ # ← 인증 헤더
-d "$PAYLOAD" \
"https://n8n.example.dev/webhook/ghost-post"
n8n Webhook 노드에서 Header Auth 설정 필요.
8.3 실패 시 재시도
MAX_RETRIES=3
RETRY_COUNT=0
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
RESPONSE=$(curl -s -w "%{http_code}" -o /dev/null -X POST \
-H "Content-Type: application/json" \
-d "$PAYLOAD" \
"https://n8n.example.dev/webhook/ghost-post")
if [ "$RESPONSE" = "200" ]; then
echo "[$(date)] Success: $FILENAME" >> ~/.claude/hooks/blog-webhook.log
break
fi
RETRY_COUNT=$((RETRY_COUNT + 1))
sleep 2
done
if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
echo "[$(date)] Failed after $MAX_RETRIES retries: $FILENAME" >> ~/.claude/hooks/blog-webhook.log
fi
8.4 여러 Webhook 동시 호출
# Ghost webhook
curl -s -X POST -d "$PAYLOAD" "https://n8n.example.dev/webhook/ghost-post" &
# Slack 알림
curl -s -X POST -d "{\"text\":\"새 블로그 초안: $FILENAME\"}" \
"https://hooks.slack.com/services/xxx" &
# 둘 다 백그라운드로 실행
wait
8.5 Edit 도구에도 적용
{
"matcher": "Write|Edit",
"hooks": [...]
}
주의: Edit은 기존 파일 수정이므로 중복 전송될 수 있습니다.
9. 핵심 개념 정리
| 개념 | 설명 |
|---|---|
| Claude Code Hooks | 도구 실행 전후에 셸 스크립트를 자동 실행하는 기능 |
| PostToolUse | 도구 실행 완료 후 발생하는 이벤트 |
| PreToolUse | 도구 실행 전 발생하는 이벤트 (차단 가능) |
| matcher | 특정 도구에만 hook을 적용하기 위한 정규식 패턴 |
| stdin JSON | hook 스크립트가 받는 입력 데이터 |
| tool_input | 도구에 전달된 입력값 |
| tool_response | 도구 실행 결과 |
10. 베스트 프랙티스
- [ ] Hook 스크립트는 절대 경로로 지정
- [ ] 타임아웃 설정으로 무한 대기 방지
- [ ] 경로 필터링으로 불필요한 실행 방지
- [ ] 백그라운드 실행(
&)으로 Claude Code 응답 지연 방지 - [ ] 에러 시에도
exit 0으로 종료 (Claude Code에 영향 주지 않도록) - [ ] 민감 정보(API Key)는 환경변수로 관리
- [ ] 디버깅용 로그는 필요할 때만 활성화
- [ ] HTTPS URL 사용 (HTTP는 리다이렉트될 수 있음)
11. FAQ
Q: Hook 설정 후 바로 적용되나요?
A: 아니요, 새 Claude Code 세션부터 적용됩니다. 터미널을 새로 열고 claude를 실행하세요.
Q: 전역 설정과 프로젝트 설정 중 어느 것이 우선인가요?
A: 프로젝트 설정(.claude/settings.json)이 전역 설정(~/.claude/settings.json)보다 우선합니다.
Q: 여러 hook을 동시에 실행할 수 있나요?
A: 네, hooks 배열에 여러 개를 추가하면 병렬로 실행됩니다.
Q: Hook 스크립트가 실패하면 Claude Code에 영향이 있나요?
A: 스크립트가 비정상 종료해도 Claude Code는 정상 동작합니다. 다만 타임아웃까지 대기할 수 있으므로 exit 0을 명시하세요.
Q: PreToolUse로 Write를 차단할 수 있나요?
A: 네, PreToolUse hook에서 비정상 종료(exit 1)하면 도구 실행이 차단됩니다.
12. 참고 자료
13. 다음 단계
이제 /blog 커맨드만 실행하면 자동으로 Ghost에 초안이 생성됩니다.
시리즈 목차:
- n8n으로 블로그 자동화 구축하기
- Claude Code Hooks로 블로그 글 자동 전송하기 ← 현재 글
- Ghost 발행 → SNS 자동 공유 (예정)