대화 로그에서 Claude 스킬을 자동 생성하는 방법 (새벽 3시 30분)
요약
대화 로그에서 재사용 가능한 절차를 추출하여 SKILL.md 파일로 자동 생성하는 배치 프로세스의 구조를 설명합니다. 워터마크 방식과 예산 상한선 설정을 통해 LLM 토큰 소모와 비용을 효율적으로 제어하는 방법을 다룹니다.
핵심 포인트
- 워터마크 방식을 사용하여 이미 처리된 로그를 제외하고 최신 로그만 추출
- --max-budget-usd 플래그로 LLM 쿼터 소모 및 비용 폭주 방지
- 대량의 로그를 처리할 때 발생하는 토큰 비용 문제를 효율적으로 해결
- 사람의 개입 없이 100개 이상의 스킬을 자동 생성하는 파이프라인 구축
매일 밤 제가 잠든 동안, 배치 작업이 전날의 대화 로그를 읽고, 그 안에서 재사용 가능한 절차들을 추출하여 SKILL.md 파일로 디스크에 작성합니다. 이전 게시물(my previous post)에서는 이러한 자동 생성된 스킬들의 라이프사이클 관리(두 단계의 stale → archive 은퇴)를 다루었습니다. 이번에는 그보다 앞에 오는 단계, 즉 매일 밤 3시 30분에 실행되어 대화 로그를 읽고, 재사용 가능한 절차만 추출하여 SKILL.md로 작성하는 수확(harvest) 배치의 전체 내부 구조를 공개합니다.
현재 ~/.claude/skills/auto/에는 104개의 스킬이 쌓여 있습니다. 이들 대부분은 skill-harvest.sh에 의해 사람이 개입하지 않고 생성되었습니다.
문제점: 매일 3,349개 로그를 모두 읽는 것은 할당량을 소진시킬 것임
~/Documents/my-knowledge-base/raw/conversations/에는 오늘 기준으로 3,349개의 대화 로그가 쌓였습니다. 만약 제가 매일 이 모든 로그를 LLM에 공급한다면, 확실히 스킬 후보들을 수확할 수 있을 것입니다. 하지만 그 비용은 얼마나 될까요?
3,349개 로그 × 10k 토큰/로그 (평균 추정) = 3,300만 토큰. 매일 이 양을 Max 할당량으로 소모한다면 다른 모든 작업을 마비시킬 것입니다.
저는 두 가지 메커니즘으로 이를 해결했습니다.
- 워터마크 방식 (Watermark method): 이미 처리된 파일은 다시 읽지 않음
- 예산 상한선 (Budget cap): 실행당 LLM 지출을
--max-budget-usd 1.20으로 고정
이 두 가지를 결합함으로써 매일의 수확량을
~/.claude/skills/auto/.harvest-watermark는 이전 수확(harvest)이 완료된 시점의 타임스탬프를 기록합니다.
# 前回以降に更新されたログを収集(初回は最新 MAX_LOGS 件)
if [[ -f "$WM" ]]; then
newlogs=("${(@f)$(find "$LOGS" -name '*.md' -newer "$WM" 2>/dev/null)}")
...
find -newer "$WM"는 "워터마크(watermark)보다 최신인" 파일만 추출합니다. 3,349개의 로그 중, 보통 어젯밤과 오늘 아침 사이에 업데이트된 로그는 3~5개뿐이었습니다. 따라서 처리해야 할 집합의 크기가 수십 배(orders of magnitude) 더 작아집니다.
종료 직전에 워터마크는 항상 업데이트됩니다.
touch "$WM"
exit 0
실제 로그에서 이를 확인할 수 있습니다.
[2026-07-05 03:30:05] harvest start: 3 log(s), digest= 18255B
[2026-07-05 03:35:54] harvest done (exit 0, created=0)
매일 밤 약 3개의 로그와 18KB를 처리합니다.
$1.20 예산 제한(budget cap)으로 폭주하는 비용 제어하기
상한선은 claude -p의 --max-budget-usd 플래그로 전달됩니다.
"$CLAUDE" -p "$PROMPT" \
--model sonnet \
--permission-mode acceptEdits \
...
$BUDGET_USD=1.20을 초과하는 즉시 Claude가 자동으로 중단됩니다. 이는 정액제인 Claude Max 쿼터(quota)에서 실행되므로, $1.20이 실제 지출 비용으로 직접 연결되지는 않습니다. 하지만 이는 쿼터가 소비되는 양을 고정하고, autopilot과 같은 다른 작업에 미치는 영향을 제한하는 역할을 합니다.
예산 제한(budget cap)만으로는 실제 실행 시간(wall-clock time)을 제한할 수 없습니다. TIMEOUT_SEC=600 실제 실행 시간 타임아웃은 perl -e 'alarm N; exec @ARGV'를 사용하여 구현되었습니다.
( cd "$STAGING" && perl -e 'alarm shift @ARGV; exec @ARGV' "$TIMEOUT_SEC" \
"$CLAUDE" -p "$PROMPT" ... )
타임아웃이 발생하면 exit 142 (SIGALRM = 128+14)와 함께 종료됩니다. 이는 실제 로그에서도 확인할 수 있습니다.
[2026-07-08 03:30:05] harvest start: 3 log(s), digest= 35159B
[2026-07-08 03:40:05] harvest done (exit 142, created=0)
[2026-07-09 03:30:06] harvest start: 3 log(s), digest= 35159B
...
10분 후에 강제 종료됩니다. 타임아웃이 발생하더라도 created=0으로 기록되기 때문에, 다음 날의 수확(harvest)은 정상적으로 계속 실행됩니다.
참고: macOS의 기본
coreutils에는 GNU의timeout명령어가 포함되어 있지 않습니다.perl -e 'alarm N; exec @ARGV'는 macOS에서 작동하는 이식 가능한 대안입니다.gtimeout(Homebrew coreutils)도 작동하지만, 이는 plist의 PATH에 의존하므로 더 취약할 수 있습니다.
프롬프트를 가볍게 만들기 위한 시스템 리마인더(system-reminder) 노이즈 사전 필터링
대화 로그에는 Claude Code의 <system-reminder> 태그와 기술/도구 목록이 그대로 기록된 라인들이 포함되어 있습니다. 이러한 내용들을 프롬프트에 그대로 밀어 넣으면, 절차적 지식 (procedural knowledge)과 관련 없는 노이즈가 토큰의 대부분을 차지하게 됩니다.
grep -v -e 'system-reminder' -e '^- [a-z0-9].*:' "$f" 2>/dev/null | head -c $PER_LOG_BYTES
제외 방식에는 두 가지가 있습니다.
system-reminder:<system-reminder>를 포함하는 라인 전체를 제거합니다.'^- [a-z0-9].*:':- skill-name: description스타일의 기술/도구 목록 라인을 제거합니다.
이 과정을 통해 단일 로그에서 최대 $PER_LOG_BYTES=15000 바이트를 가져옵니다. MAX_LOGS=3과 결합하면, 한 번의 수확 (harvest) 시 LLM에 전달되는 양은 최대 **45,000 바이트 (약 45KB)**로 고정됩니다. 로그가 아무리 많이 쌓이더라도 프롬프트 크기는 변하지 않습니다.
스테이징 디렉토리를 통한 안전한 파일 쓰기
Claude Code가 실행되는 동안 ~/.claude/ 하위의 모든 항목은 쓰기 보호 (write-protected)될 수 있습니다. 만약 claude -p가 ~/.claude/skills/auto/에 직접 쓰도록 허용하면, 권한 거부 (permission denied)로 인해 종료됩니다.
STAGING=$(mktemp -d -t skill-harvest-stg)
( cd "$STAGING" && perl -e 'alarm shift @ARGV; exec @ARGV' "$TIMEOUT_SEC" \
"$CLAUDE" -p "$PROMPT" \
...
mktemp -d는 /tmp 하위에 임시 디렉토리를 생성하며, claude -p는 cd "$STAGING" 상태에서 실행됩니다. 프롬프트에는 "현재 디렉토리 바로 아래에 ./kebab-name/SKILL.md를 생성하세요 (절대 경로 금지)."라고 명시되어 있습니다.
- ファイル: ./<kebab-name>/SKILL.md (カレントディレクトリ直下に作る。絶対パス禁止)
Claude가 쓰기를 마치면, 셸(shell) 측에서 $STAGING을 스캔하여 AUTO 디렉토리로 복사합니다.
for sd in "$STAGING"/*(/N); do
[[ -f "$sd/SKILL.md" ]] || continue
name="${sd:t}"
...
프론트매터 (frontmatter)에 author: auto가 포함되어 있지 않은 경우, 전송하기 전에 python3 원라이너 (one-liner)를 사용하여 이를 삽입합니다. 이 author: auto는 무엇을 큐레이션 대상으로 결정할지 판단하는 핵심 키이므로 필수적입니다.
launchd 등록
plist (~/Library/LaunchAgents/com.lily.skill-harvest.plist)의 스케줄 부분입니다.
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key><integer>3</integer>
...
매일 3시 30분에 실행됩니다. ProcessType: Background로 실행되며, 로그는 ~/.claude/logs/com.lily.skill-harvest.log로 출력됩니다.
등록 및 즉시 테스트:
launchctl load ~/Library/LaunchAgents/com.lily.skill-harvest.plist
launchctl start com.lily.skill-harvest
내가 겪은 함정들
- launchd의 PATH는 최소화되어 있음: 순수한 launchd 환경에서는
claude를 찾을 수 없습니다. plist의EnvironmentVariables/PATH에 nvm의 bin 디렉토리와~/.local/bin을 명시적으로 추가하지 않으면 즉시 종료됩니다. ~/.claude에 대한 직접 쓰기는 권한 거부(permission denied) 발생: Claude Code가 실행 중인 동안에는 보호됩니다. 스테이징 (staging) + 셸 (shell) 복사 방식으로 해결했습니다.timeout명령어가 macOS에는 표준이 아님:perl -e 'alarm N; exec @ARGV'로 대체했습니다.- 워터마크 (watermark)를
touch하는 것을 잊으면 매번 모든 로그를 재스캔함: 종료 직전의touch "$WM"은 반드시exit 0바로 앞에 위치해야 합니다. exit 142가created=0과 동일하게 기록됨: 타임아웃 (timeout)을 정상 종료와 구분할 수 없으므로, 로그에서 시작과 종료 사이의 시간 간격으로 구분하는 습관을 들여야 합니다 (정상: 몇 분, 타임아웃: 정확히 10분).- 이미 동일한 이름의 스킬이 존재하는 경우 덮어쓸 수 없음:
[[ -e "$AUTO/$name" ]]가 복사를 건너뛰기 때문입니다. 기존 스킬을 업데이트 (패치, patch)하는 것은 프롬프트 (prompt) 측에서 "중복된 경우, 기존 것을 읽고 패치하라"고 지시하여 처리합니다.
"실패는 정상입니다," 실제 로그에서 확인된 결과
이번 주의 harvest 로그입니다.
[2026-07-05 03:30:05] harvest start: 3 log(s), digest= 18255B
この会話ログ抜粋を確認しました。INDEX.md はファイル一覧のみで
手順的知識なし。tsuzuke-ios の 2 件は既存の
...
5일 중 4일은 created=0입니다. 실패(miss)는 이상 현상이 아닙니다. 이는 추출 기준 (extraction criteria)이 올바르게 작동하고 있다는 증거입니다. 기존 스킬의 중복, 일회성 개인 사정, 그리고 무의미한 대화는 모두 거절됩니다. LLM이 자율적으로 거절 여부를 결정하기 때문에 쓰레기 데이터 (junk)를 양산하지 않습니다.
참고:
created=0인 harvest는 비용 (Max quota)을 거의 소모하지 않습니다.claude -p를 실행하고, 프롬프트 (prompt)를 읽고, "해당 사항 없음"을 반환하는 데 필요한 수준이면 충분합니다. "실패 비용이 저렴하다"는 점은 제가 이 작업을 매일 밤 배치 (batch)로 실행할 수 있는 이유 중 하나입니다.
요약
- 워터마크 방식 (Watermark method):
.harvest-watermark의 수정 시간 (mtime)을 기준으로find -newer를 사용하여 차이점 (diff)이 있는 로그만 추출합니다. 3,349개의 로그가 있더라도 매일 밤 단 3개만 처리합니다. - 이중 예산 제한 (Double budget cap):
--max-budget-usd 1.20와perl alarm 600을 사용하여 LLM 지출과 실행 시간 모두에 상한선을 둡니다. - 사전 노이즈 필터링 (Noise filter up front): 전처리 단계에서
grep -v 'system-reminder'를 추가하여 프롬프트를 고정된 45KB로 압축합니다. - 스테이징을 통한 쓰기 (Writing via staging):
mktemp -d를 사용하여 셸 (shell)이 중재하게 함으로써~/.claude와의 쓰기 경합 (write contention)을 방지합니다. - 실패는 정상입니다: 엄격한 추출 기준을 적용하면
created=0인 날이 지속되는 것은 건강한 동작입니다.
스킬이 100개를 넘어가면서 큐레이션 (curate) 측면의 중요성이 높아진 것에 대해서는 이전 글에서 다루었습니다. 다음에는 스킬과 메모리를 쌓아가는 이 두 가지 배치 (batch) 작업이 어떻게 Claude Code의 실행 속도를 느리게 만들기 시작했는지 — 컨텍스트 주입 (context injection) 양을 감사 (auditing)하고 줄이는 방법에 대해 써볼 계획입니다.
작성자: Lily — iOS 앱을 출시하며 Claude Code로 콘텐츠 스택을 자동화합니다.
팔로우하기: Portfolio · X · GitHub
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기