347 lines
12 KiB
Markdown
347 lines
12 KiB
Markdown
# 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](.claude/hooks/aptabase.json)):
|
|
|
|
```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](.claude/settings.json)은 인라인 PowerShell 래퍼로 구성되어 있다:
|
|
|
|
```json
|
|
{
|
|
"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.json` 의 `oauthAccount.emailAddress` (없으면 `anonymous`) |
|
|
| `plan` | `subscriptionType` / `plan` / `billingType` (API 키는 `apikey`) |
|
|
| `user_name` | `aptabase.json` 의 `user_name` |
|
|
| `local_ip` | UDP connect 트릭 (패킷 미전송) |
|
|
| `public_ip` | `api.ipify.org` → `ifconfig.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`
|
|
|
|
```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)
|
|
|
|
```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-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 직접 실행
|
|
|
|
```bash
|
|
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.json` 의 `enabled: 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.json` 의 `enabled: false`. 훅 등록은 유지해도 된다.
|
|
|
|
### 전송 기준점 초기화 (디버깅용)
|
|
|
|
```bash
|
|
jq '.last_sent_commit = ""' .claude/state/aptabase-accum.json > /tmp/_s \
|
|
&& mv /tmp/_s .claude/state/aptabase-accum.json
|
|
```
|
|
|
|
---
|
|
|
|
## 11. 알려진 이슈 — Stop 훅 비활성
|
|
|
|
현재 이 저장소의 `aptabase-accum.json` 은 `last_transcript` 가 빈 문자열인 채로 남아 있다. 이는 **Stop 훅 래퍼가 한 번도 `.ps1` 까지 도달하지 못했음**을 의미한다 (스크립트 자체는 수동 실행 시 정상 동작 확인됨).
|
|
|
|
### 추정 원인
|
|
|
|
`settings.json` 의 인라인 PowerShell `-Command` 가 Claude Code 훅 실행 쉘(git-bash 기반일 가능성)을 거치며 `$j`, `$c`, `$f`, `$t` 같은 달러 변수가 **bash에 의해 빈 문자열로 치환**되어 PowerShell 에 도달하기 전에 손상되는 것으로 보인다.
|
|
|
|
### 권장 수정
|
|
|
|
인라인 래퍼 대신 단순 파일 호출로 대체:
|
|
|
|
```json
|
|
{
|
|
"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. 제거
|
|
|
|
```bash
|
|
# 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
|
|
```
|