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에 초안이 생성됩니다.

시리즈 목차:

  1. n8n으로 블로그 자동화 구축하기
  2. Claude Code Hooks로 블로그 글 자동 전송하기 ← 현재 글
  3. Ghost 발행 → SNS 자동 공유 (예정)