--- name: aptabase description: Aptabase 커밋 기준 토큰 기록 훅을 설치/검증/디버깅한다. 사용자가 "aptabase 설정", "aptabase 테스트", "토큰 기록 확인", "누적 토큰"을 요청할 때 사용. --- # Aptabase 커밋 기준 토큰 기록 Claude 의 모든 응답 토큰을 프로젝트 단위로 누적하고, **git 커밋이 발생하면** Aptabase 로 한 번에 flush 한 뒤 0 으로 리셋한다. ## 구조 ``` .claude/hooks/ ├── aptabase.json # 설정 (app_key, aptabase_host, user_name) ├── aptabase_common.py # 공통 유틸 (Python) ├── aptabase-accumulate.sh # Stop 훅 진입점 ├── aptabase-accumulate.py # Stop 로직 — 토큰 누적만 ├── aptabase-commit.sh # 커밋 flush 진입점 (Claude 훅 + git 훅 공용) ├── aptabase-commit.py # 커밋 감지 + Aptabase POST + 리셋 └── install-git-hook.sh # .git/hooks/post-commit 설치 스크립트 .claude/state/ └── aptabase-accum.json # 누적 상태 (자동 생성, git root 기준) .git/hooks/ └── post-commit # install-git-hook.sh 가 설치 (모든 커밋 포착) ``` ## 동작 방식 | 시점 | 훅 | 하는 일 | |---|---|---| | 응답 완료 | `Stop` → `aptabase-accumulate.sh` | 트랜스크립트 새 영역 파싱 → `state.accum` 에 토큰 더하기 (네트워크 없음) | | Claude 의 Bash 툴 실행 후 | `PostToolUse(Bash)` → `aptabase-commit.sh` | HEAD 변화 감지. 바뀌었으면 flush → POST → 리셋 | | **모든 커밋** (IDE / 터미널 / GUI) | `.git/hooks/post-commit` → `aptabase-commit.sh` | 같은 로직. Claude 밖에서 이루어진 커밋도 포착 | `aptabase-commit.py` 는 stdin 에 JSON 이 있으면 Claude 훅 모드, 비어 있으면 git 훅 모드로 동작한다. 두 경로가 중복 실행돼도 `last_sent_commit` 비교 덕에 이중 전송되지 않는다. - **누적 범위**: `/.claude/state/aptabase-accum.json` — 프로젝트(= git 저장소)마다 독립 - **커밋 감지**: HEAD 해시 변화 기반 — `git commit`, `--amend`, `merge`, `cherry-pick`, `rebase` 모두 감지 - **첫 실행**: 기존 HEAD 를 `last_sent_commit` 에 기준점으로 기록만 하고 전송 안 함 (이전 작업 토큰이 0이라) - **전송 실패**: `state` 유지 → 다음 Bash 호출에서 재시도 (누적값 + 새 커밋 병합) ## 1. 설정 `.claude/hooks/aptabase.json`: ```json { "enabled": true, "app_key": "A-SH-XXXXXXXXXX", "aptabase_host": "https://aptabase.example.com", "user_name": "kim" } ``` | 키 | 의미 | |---|---| | `enabled` | false 로 두면 훅이 즉시 종료 (일시 비활성) | | `app_key` | Aptabase App Key (self-hosted 는 `A-SH-` 접두사) | | `aptabase_host` | Aptabase 인스턴스 base URL | | `user_name` | props.user_name 으로 전송할 사용자 식별자 | ## 2. settings.json 에 훅 등록 `.claude/settings.json` 의 `hooks` 에 두 항목 모두 등록: ```json { "hooks": { "Stop": [ { "matcher": "*", "hooks": [ { "type": "command", "command": "bash .claude/hooks/aptabase-accumulate.sh" } ] } ], "PostToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "bash .claude/hooks/aptabase-commit.sh" } ] } ] } } ``` 이미 다른 Stop / PostToolUse 훅이 있으면 같은 `hooks` 배열에 추가한다. ## 2-1. git post-commit 훅 설치 (필수) Claude 외부에서 이루어진 커밋(IDE, 터미널, git GUI 등)도 포착하려면 `.git/hooks/post-commit` 을 설치해야 한다. 프로젝트 루트에서 한 번만 실행: ```bash bash .claude/hooks/install-git-hook.sh ``` 이미 `.git/hooks/post-commit` 이 존재하면 에러가 나면서 병합 방법을 안내한다. 기존 훅을 덮어쓰려면: ```bash bash .claude/hooks/install-git-hook.sh --force # 기존 훅을 .bak 로 백업하고 덮어쓰기 ``` 제거: ```bash bash .claude/hooks/install-git-hook.sh --uninstall ``` > `.git/hooks/` 는 버전 관리되지 않으므로 **clone 할 때마다 재설치 필요**하다. > CI/자동 설정 스크립트에서 프로젝트 초기화 시 함께 실행하는 것을 권장한다. ## 3. 전송되는 payload Aptabase 이벤트 이름은 **`claude_commit`** (고정). ### systemProps - `osName`, `osVersion`, `locale`, `appVersion`, `sdkVersion`, `isDebug` ### props | 필드 | 출처 | |---|---| | `claude_oauth_id` | `~/.claude/config.json` 의 `oauthAccount.emailAddress` (없으면 `anonymous`) | | `plan` | `oauthAccount.subscriptionType` (max/pro/team/enterprise) — API 키면 `apikey` | | `user_name` | `aptabase.json` 의 `user_name` | | `local_ip` | UDP connect 트릭으로 추출 (패킷 미전송) | | `public_ip` | `api.ipify.org` / `ifconfig.me` (2초 타임아웃) | | `commit_hash` | `git rev-parse HEAD` | | `commit_message` | `git log -1 --pretty=%B` | | `issue_number` | 커밋 메시지에서 regex 추출 (`FEAT-123`, `#123`, `fixes #45` 등). 없으면 `null` | | `repository` | `owner/repo` (remote URL) 또는 디렉터리명 | | `repository_url` | `git config --get remote.origin.url` | | `total_tokens` | 누적 합계 | | `input_tokens`, `cache_creation_tokens`, `cache_read_tokens`, `output_tokens` | 누적 세부 | > Aptabase 최상위 `sessionId` 는 epoch+random 으로 매 이벤트 새로 생성된다. > Claude 세션 UUID 는 사용하지 않는다. ## 4. 연결 테스트 **단계 1 — Aptabase 도달 가능성 확인 (curl):** ```bash 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-07T00:00:00.000Z", "sessionId":"manual-test", "eventName":"claude_commit", "systemProps":{"osName":"Test","sdkVersion":"manual"}, "props":{"commit_message":"manual test","total_tokens":1} }' ``` 200 + `{}` 이면 Aptabase 수신 OK. **단계 2 — 훅 직접 실행 (Stop 누적):** ```bash echo '{"transcript_path":"","cwd":"'"$PWD"'","hook_event_name":"Stop"}' \ | bash .claude/hooks/aptabase-accumulate.sh cat .claude/state/aptabase-accum.json ``` transcript_path 가 비어있으면 accum 은 0 그대로지만, state 파일은 생성돼야 한다. **단계 3 — 실제 동작 확인:** 1. Claude 와 대화해서 토큰 쌓기 2. `cat .claude/state/aptabase-accum.json` 으로 누적 확인 3. `git commit` 실행 4. Aptabase 대시보드에서 `claude_commit` 이벤트 확인 5. `cat .claude/state/aptabase-accum.json` — `accum` 이 0 으로 리셋, `last_sent_commit` 갱신됐는지 확인 ## 5. 누적 상태 파일 포맷 `/.claude/state/aptabase-accum.json`: ```json { "accum": { "input_tokens": 1234, "cache_creation_tokens": 5678, "cache_read_tokens": 9012, "output_tokens": 345 }, "offsets": { "C:\\Users\\...\\projects\\d--foo\\session-uuid.jsonl": 45678 }, "last_sent_commit": "abc123def..." } ``` - `offsets`: 트랜스크립트 JSONL 파일별로 이미 읽은 byte 위치. append-only 특성 덕에 중복 집계 방지 - 수동 리셋이 필요하면 `rm .claude/state/aptabase-accum.json` ## 6. 트러블슈팅 **누적이 안 쌓임 (`accum` 이 계속 0):** 1. `aptabase.json` 의 `enabled: true` 2. `app_key`, `aptabase_host` 실제 값 3. settings.json 의 Stop 훅에 `aptabase-accumulate.sh` 등록 4. `transcript_path` 가 훅 입력 JSON 에 실제로 있는지 5. 해당 파일 읽기 권한 **커밋했는데 Aptabase 에 안 뜸:** 1. settings.json 의 PostToolUse(Bash) 훅에 `aptabase-commit.sh` 등록 2. `last_sent_commit` 가 이미 현재 HEAD 와 같은지 (이미 전송된 상태) 3. curl 로 직접 POST 했을 때 200 이 오는지 (네트워크/인증 분리 검증) 4. `python3 --version` 동작 5. git 저장소 안에서 동작 중인지 (`git rev-parse HEAD` 성공해야 함) **첫 커밋이 무시됨:** 의도된 동작. `last_sent_commit` 가 빈 상태일 때는 현재 HEAD 를 기준점으로 기록만 하고 전송하지 않는다. 다음 커밋부터 전송된다. **전송 기준점 초기화:** ```bash jq '.last_sent_commit = ""' .claude/state/aptabase-accum.json > /tmp/_s && mv /tmp/_s .claude/state/aptabase-accum.json ``` **일시 비활성:** `aptabase.json` 에서 `enabled: false`. 훅 등록은 유지해도 된다.