Files
workhistory/HOOKS.md
minsung 68d4012f11 docs: add project analysis and hooks documentation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:25:46 +09:00

12 KiB

Aptabase 훅 시스템 문서

작성: 2026-04-08 대상: workhistory 저장소의 .claude/hooks/ 기반 Aptabase 토큰 기록 파이프라인


1. 개요

Claude Code 세션에서 발생한 응답 토큰을 git 커밋 단위로 묶어 자체 호스팅 Aptabase(https://aptabase.hmac.kr)로 자동 전송하는 시스템. 토큰은 세션 동안 로컬에 누적되다가 git 커밋이 감지되면 claude_commit 이벤트로 flush된다.

핵심 아이디어

  • 커밋을 "작업 단위"의 자연스러운 경계로 사용
  • 외부(IDE/터미널/GUI)에서 일어난 커밋도 포착 (.git/hooks/post-commit 설치)
  • 전송 실패 시 재시도, 이중 전송 방지(last_sent_commit 해시 비교)

2. 구성 파일

.claude/
├── settings.json                  # 훅 등록 (Stop / PostToolUse(Bash))
├── hooks/
│   ├── aptabase.json              # app_key, host, user_name
│   ├── aptabase-accumulate.ps1    # Stop 훅 진입점 (Windows)
│   ├── aptabase-accumulate.sh     # Stop 훅 진입점 (Unix)
│   ├── aptabase-commit.ps1        # 커밋 flush (Windows)
│   ├── aptabase-commit.sh         # 커밋 flush (Unix)
│   ├── install-git-hook.sh        # .git/hooks/post-commit 설치
│   └── usage.md                   # 상세 사용 설명
├── skills/aptabase/SKILL.md       # /aptabase 슬래시 스킬 정의
└── state/
    ├── .gitignore
    └── aptabase-accum.json        # 누적 상태 (자동 생성)

3. 동작 흐름

시점 훅 이벤트 실행 스크립트 역할
Claude 응답 완료 Stop aptabase-accumulate.ps1 트랜스크립트 JSONL 증분 파싱 → state.accum에 토큰 누적. 네트워크 호출 없음.
Claude Bash 툴 호출 후 PostToolUse(Bash) aptabase-commit.ps1 HEAD 해시 변화 감지 → 변했으면 Aptabase POST → 누적 리셋
모든 git 커밋 (외부 포함) .git/hooks/post-commit aptabase-commit.ps1 동일 로직. Claude 밖 커밋도 포착
Claude 응답 완료
    │
    ▼
[Stop: accumulate.ps1]
  ├─ 트랜스크립트 새 byte 구간만 읽어 assistant usage 누적
  └─ state.accum 저장 (네트워크 없음)

git commit (Claude Bash 또는 외부)
    │
    ├─ PostToolUse(Bash)  ┐
    │                     │
    └─ post-commit 훅     ▼
                 [commit.ps1]
                 ├─ HEAD 가 last_sent_commit 과 같으면 exit
                 ├─ 다르면:
                 │   ├─ 남은 트랜스크립트 flush
                 │   ├─ consumed_tokens 계산 (0이면 전송 생략)
                 │   ├─ POST /api/v0/event (event: claude_commit)
                 │   ├─ 성공: 누적 리셋, last_sent_commit = HEAD
                 │   └─ 실패: state 유지 → 다음 호출에서 재시도
                 └─ exit 0 (실패해도 git/Claude 흐름 비차단)

4. 설정 — aptabase.json

현재값 (.claude/hooks/aptabase.json):

{
  "enabled": true,
  "app_key": "A-SH-7756143445",
  "aptabase_host": "https://aptabase.hmac.kr",
  "user_name": "김민성(b16213)",
  "git_repositories": ["D:/MYCLAUDE_PROJECT/workhistory"]
}
의미
enabled false 면 훅이 즉시 종료 (일시 비활성)
app_key Aptabase App Key (self-hosted 는 A-SH- 접두사)
aptabase_host Aptabase 인스턴스 base URL
user_name props.user_name 으로 전송할 사용자 식별자
git_repositories (참고용) 대상 저장소 목록

⚠️ app_key가 평문으로 커밋되어 있다. 공개 저장소로 전환할 경우 반드시 gitignore 또는 .example 템플릿으로 분리할 것.


5. 훅 등록 — settings.json

.claude/settings.json은 인라인 PowerShell 래퍼로 구성되어 있다:

{
  "hooks": {
    "Stop": [{
      "hooks": [{
        "type": "command",
        "command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \"$j=[Console]::In.ReadToEnd();$c=try{($j|ConvertFrom-Json).cwd}catch{$null};if(-not $c){exit 0};$f=Join-Path $c '.claude\\hooks\\aptabase-accumulate.ps1';if(Test-Path $f){$t=[IO.Path]::GetTempFileName();[IO.File]::WriteAllText($t,$j,[Text.Encoding]::UTF8);try{&powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $f $t}finally{Remove-Item $t -EA 0}}\""
      }]
    }],
    "PostToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "powershell ... (같은 패턴, commit.ps1 호출)"
      }]
    }]
  }
}

래퍼의 동작

  1. stdin으로 들어온 훅 JSON을 읽는다
  2. cwd 필드를 추출해 프로젝트의 .ps1 경로를 찾는다
  3. 훅 JSON 전체를 UTF-8 임시 파일에 쓴다
  4. .ps1-File 인자로 실행하며 임시 파일 경로를 넘긴다
  5. 스크립트는 $args[0] 또는 stdin 양쪽에서 JSON을 읽을 수 있도록 작성되어 있음

6. 전송 이벤트 — claude_commit

엔드포인트: POST {aptabase_host}/api/v0/event 헤더: App-Key: <app_key>, Content-Type: application/json

systemProps

osName, osVersion, locale, appVersion, sdkVersion, isDebug

props

필드 출처
claude_oauth_id ~/.claude.jsonoauthAccount.emailAddress (없으면 anonymous)
plan subscriptionType / plan / billingType (API 키는 apikey)
user_name aptabase.jsonuser_name
local_ip UDP connect 트릭 (패킷 미전송)
public_ip api.ipify.orgifconfig.me fallback (2초 타임아웃)
commit_hash git rev-parse HEAD
commit_message git log -1 --pretty=%B
issue_number 커밋 메시지 regex 추출 (#123, fixes #45 등). 없으면 null
repository remote URL 또는 git root 디렉터리
consumed_tokens 가중 합계 (아래 참조)
total_tokens 단순 합계
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens 세부

consumed_tokens 가중치

consumed = input * 1.0
         + cache_creation * 1.25
         + cache_read * 0.10
         + output * 5.0

Anthropic 청구 단가 비율과 맞춘 "실질 비용 단위".

이슈 번호 추출 regex

(?:fix(?:e[sd])?|close[sd]?|resolve[sd]?)?[\s]*#(\d+)

예: fixes #45, closes#12, #123 모두 매칭.


7. 누적 상태 파일 — aptabase-accum.json

경로: <git-root>/.claude/state/aptabase-accum.json

{
  "input_tokens": 1234,
  "cache_creation_tokens": 5678,
  "cache_read_tokens": 9012,
  "output_tokens": 345,
  "last_sent_commit": "abc123def...",
  "last_transcript": "C:\\Users\\...\\session-uuid.jsonl",
  "last_transcript_offset": 45678
}
필드 의미
*_tokens 현재 누적 중인 토큰 (flush 시 0으로 리셋)
last_sent_commit 마지막 성공 전송한 HEAD 해시 (중복 전송 방지)
last_transcript 마지막으로 읽은 트랜스크립트 JSONL 경로
last_transcript_offset 해당 파일에서 이미 읽은 byte 위치 (JSONL append-only 특성 활용)

수동 리셋: rm .claude/state/aptabase-accum.json 전송 기준점만 초기화: last_sent_commit 필드를 "" 로 편집


8. 첫 실행 / 엣지 케이스

상황 동작
첫 설치 후 첫 커밋 last_sent_commit 이 비어 있으면 현재 HEAD 를 기준점으로 기록만 하고 전송 안 함
consumed_tokens == 0 전송 생략, last_sent_commit 만 갱신
네트워크 실패 state 유지 → 다음 Bash 호출에서 재시도 (누적 + 새 커밋 병합)
Claude가 Bash로 커밋 PostToolUse(Bash) + post-commit 둘 다 발동. last_sent_commit 비교로 한 번만 전송
외부(IDE)에서 커밋 post-commit 만 발동
git 저장소 밖 실행 git rev-parse HEAD 실패 → 조용히 종료
OAuth 미로그인 (API 키) claude_oauth_id = "anonymous", plan = "apikey"
amend / rebase / cherry-pick HEAD 해시가 바뀌므로 모두 포착
last_sent_commit 이 삭제된 커밋 탐지 후 현재 HEAD 를 새 기준점으로 재설정 (누적 리셋)

9. 검증 절차

9.1 Aptabase 도달성 (curl)

APP_KEY=$(jq -r .app_key .claude/hooks/aptabase.json)
HOST=$(jq -r .aptabase_host .claude/hooks/aptabase.json)
curl -i -X POST "$HOST/api/v0/event" \
  -H "Content-Type: application/json" \
  -H "App-Key: $APP_KEY" \
  -H "User-Agent: ClaudeCodeHook/test" \
  -d '{
    "timestamp":"2026-04-08T00:00:00.000Z",
    "sessionId":"manual-test",
    "eventName":"claude_commit",
    "systemProps":{"osName":"Test","sdkVersion":"manual"},
    "props":{"commit_message":"manual test","total_tokens":1}
  }'

HTTP/1.1 200 + {} 이면 수신 OK.

9.2 accumulate.ps1 직접 실행

echo '{"transcript_path":"<jsonl 경로>","cwd":"D:/MYCLAUDE_PROJECT/workhistory","hook_event_name":"Stop"}' \
  | powershell -NoProfile -ExecutionPolicy Bypass -File .claude/hooks/aptabase-accumulate.ps1
cat .claude/state/aptabase-accum.json

last_transcript, last_transcript_offset, 각 *_tokens 가 채워지면 스크립트는 정상.

9.3 E2E

  1. Claude 와 대화 → 토큰 누적
  2. cat .claude/state/aptabase-accum.json — accum 이 0 이 아님을 확인
  3. git commit --allow-empty -m "test: aptabase #999"
  4. Aptabase 대시보드에서 claude_commit 이벤트 확인
  5. state 가 0 으로 리셋되고 last_sent_commit 이 새 HEAD 로 갱신됐는지 확인

10. 트러블슈팅

증상 A — accum 이 계속 0 (현재 이 저장소의 증상)

확인 순서

  1. .claude/hooks/aptabase.jsonenabled: true 여부
  2. 훅을 수동 실행했을 때 (9.2) 정상 누적되는지 → YES면 settings.json 래퍼 문제, NO면 스크립트/경로 문제
  3. last_transcript 가 비어 있으면 Stop 훅이 실제로는 스크립트까지 도달하지 않은 것
  4. settings.json 의 인라인 PowerShell 래퍼를 단순화해서 재시도 (§11 참조)

증상 B — 커밋했는데 대시보드에 안 뜸

  1. curl 직접 테스트 (9.1)로 네트워크/인증 분리 검증
  2. last_sent_commit 이 이미 HEAD 와 같은지 (이미 전송됨)
  3. consumed_tokens == 0 이라 전송이 생략됐는지 (accum 확인)
  4. .git/hooks/post-commit 이 실제로 존재하는지: cat .git/hooks/post-commit
  5. 커밋이 git 저장소 내부에서 이루어졌는지 (git rev-parse HEAD 성공)

증상 C — 첫 커밋이 무시됨

의도된 동작. last_sent_commit 가 빈 상태에서는 현재 HEAD 를 기준점으로 기록만 하고 전송하지 않는다. 두 번째 커밋부터 전송된다.

일시 비활성

aptabase.jsonenabled: false. 훅 등록은 유지해도 된다.

전송 기준점 초기화 (디버깅용)

jq '.last_sent_commit = ""' .claude/state/aptabase-accum.json > /tmp/_s \
  && mv /tmp/_s .claude/state/aptabase-accum.json

11. 알려진 이슈 — Stop 훅 비활성

현재 이 저장소의 aptabase-accum.jsonlast_transcript 가 빈 문자열인 채로 남아 있다. 이는 Stop 훅 래퍼가 한 번도 .ps1 까지 도달하지 못했음을 의미한다 (스크립트 자체는 수동 실행 시 정상 동작 확인됨).

추정 원인

settings.json 의 인라인 PowerShell -Command 가 Claude Code 훅 실행 쉘(git-bash 기반일 가능성)을 거치며 $j, $c, $f, $t 같은 달러 변수가 bash에 의해 빈 문자열로 치환되어 PowerShell 에 도달하기 전에 손상되는 것으로 보인다.

권장 수정

인라인 래퍼 대신 단순 파일 호출로 대체:

{
  "hooks": {
    "Stop": [{
      "hooks": [{
        "type": "command",
        "command": "powershell -NoProfile -ExecutionPolicy Bypass -File .claude/hooks/aptabase-accumulate.ps1"
      }]
    }],
    "PostToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "powershell -NoProfile -ExecutionPolicy Bypass -File .claude/hooks/aptabase-commit.ps1"
      }]
    }]
  }
}

.ps1 들은 이미 stdin 에서 훅 JSON 을 읽도록 작성되어 있으므로 래퍼에서 cwd 추출·임시파일 생성 과정은 불필요하다.


12. 제거

# 1. git post-commit 훅 제거
bash .claude/hooks/install-git-hook.sh --uninstall

# 2. settings.json 에서 Stop / PostToolUse(Bash) 훅 엔트리 제거

# 3. 파일 삭제 (선택)
rm -rf .claude/hooks .claude/state/aptabase-accum.json