commit 과 push test
This commit is contained in:
295
.claude/hooks/usage.md
Normal file
295
.claude/hooks/usage.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# common/.claude/hooks 사용법
|
||||
|
||||
Claude 의 모든 응답 토큰을 프로젝트 단위로 누적하고, git 커밋이 일어나면 **Aptabase** 로 한 번에 전송하는 훅 묶음.
|
||||
|
||||
---
|
||||
|
||||
## 파일 구성
|
||||
|
||||
```
|
||||
.claude/hooks/
|
||||
├── aptabase.json # 설정 (app_key, aptabase_host, user_name, enabled)
|
||||
├── aptabase_common.py # 공통 유틸 (설정/IP/git/state/transcript/POST)
|
||||
├── aptabase-accumulate.sh # Stop 훅 진입점
|
||||
├── aptabase-accumulate.py # 트랜스크립트 → state.accum 누적 (네트워크 없음)
|
||||
├── aptabase-commit.sh # 커밋 flush 진입점 (Claude 훅 + git 훅 공용)
|
||||
├── aptabase-commit.py # HEAD 변화 감지 → POST → 리셋
|
||||
└── install-git-hook.sh # .git/hooks/post-commit 설치 스크립트
|
||||
|
||||
.claude/state/
|
||||
└── aptabase-accum.json # 누적 상태 (자동 생성, git root 기준)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 설치 (프로젝트 1회)
|
||||
|
||||
### 1. 훅 파일 복사
|
||||
|
||||
```bash
|
||||
# 프로젝트 루트에서
|
||||
cp -r path/to/common/.claude/hooks .claude/hooks
|
||||
```
|
||||
|
||||
### 2. aptabase.json 채우기
|
||||
|
||||
`.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 으로 전송할 사용자 식별자 |
|
||||
|
||||
### 3. settings.json 에 Claude 훅 등록
|
||||
|
||||
`.claude/settings.json`:
|
||||
|
||||
```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" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
기존 훅이 있으면 같은 `hooks` 배열에 추가.
|
||||
|
||||
### 4. git post-commit 훅 설치 (필수)
|
||||
|
||||
**Claude 외부**(IDE, 터미널, git GUI)에서 이루어진 커밋도 포착하려면 반드시 실행:
|
||||
|
||||
```bash
|
||||
bash .claude/hooks/install-git-hook.sh
|
||||
```
|
||||
|
||||
옵션:
|
||||
```bash
|
||||
bash .claude/hooks/install-git-hook.sh --force # 기존 post-commit 을 .bak 로 백업 후 덮어쓰기
|
||||
bash .claude/hooks/install-git-hook.sh --uninstall # 제거
|
||||
```
|
||||
|
||||
> `.git/hooks/` 는 버전 관리되지 않으므로 **clone 할 때마다 재설치 필요**.
|
||||
|
||||
---
|
||||
|
||||
## 동작 흐름
|
||||
|
||||
```
|
||||
Claude 응답 완료 ──┐
|
||||
│
|
||||
▼
|
||||
[aptabase-accumulate.sh]
|
||||
└─ 트랜스크립트 JSONL 의 새 byte 영역만 파싱
|
||||
└─ assistant 메시지의 usage 를 state.accum 에 누적
|
||||
└─ 네트워크 호출 없음
|
||||
|
||||
git commit (Claude 의 Bash 툴 또는 외부 도구) ──┐
|
||||
│
|
||||
┌─────────────────────────┴────────┐
|
||||
▼ ▼
|
||||
[PostToolUse(Bash)] [.git/hooks/post-commit]
|
||||
└─ aptabase-commit.sh └─ aptabase-commit.sh
|
||||
│ │
|
||||
└──────────────┬───────────────────┘
|
||||
▼
|
||||
[aptabase-commit.py]
|
||||
├─ git rev-parse HEAD
|
||||
├─ last_sent_commit 과 같으면 return (중복 방지)
|
||||
├─ 다르면:
|
||||
│ ├─ 최신 트랜스크립트 flush (Claude 훅 모드)
|
||||
│ ├─ commit_message / issue_number / repository 수집
|
||||
│ ├─ claude_oauth_id / plan / local_ip / public_ip 수집
|
||||
│ ├─ POST {aptabase_host}/api/v0/event
|
||||
│ ├─ 성공: state.accum = 0, last_sent_commit = HEAD
|
||||
│ └─ 실패: state 유지, 다음 호출에서 재시도
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 전송되는 이벤트
|
||||
|
||||
**이벤트 이름:** `claude_commit` (고정)
|
||||
|
||||
### props 필드
|
||||
|
||||
| 필드 | 출처 |
|
||||
|---|---|
|
||||
| `claude_oauth_id` | `~/.claude/config.json` 의 OAuth 이메일 (없으면 `anonymous`) |
|
||||
| `plan` | 구독 플랜 (`max` / `pro` / `team` / `enterprise` / `apikey` / `unknown`) |
|
||||
| `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 추출. 없으면 `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` | 누적 세부 |
|
||||
|
||||
**이슈 번호 추출 패턴** (우선순위 순):
|
||||
- `FEAT-123`, `BUG-45`, `FIX-7`, `TASK-9`, `PROJ-100`, `ISSUE-12`
|
||||
- `closes #45`, `fixes #12`, `resolves #3`, `GH-8`
|
||||
- `#123`
|
||||
|
||||
---
|
||||
|
||||
## 검증
|
||||
|
||||
### 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}
|
||||
}'
|
||||
```
|
||||
|
||||
`HTTP/1.1 200` + `{}` 이면 성공.
|
||||
|
||||
### 2. Stop 훅 누적 확인
|
||||
|
||||
Claude 와 대화 후:
|
||||
```bash
|
||||
cat .claude/state/aptabase-accum.json
|
||||
```
|
||||
|
||||
`accum.input_tokens`, `output_tokens` 등이 0 이 아니면 정상 동작.
|
||||
|
||||
### 3. 커밋 훅 수동 실행
|
||||
|
||||
```bash
|
||||
bash .claude/hooks/aptabase-commit.sh < /dev/null
|
||||
```
|
||||
|
||||
Aptabase 대시보드에서 이벤트 확인 → `accum` 이 0 으로 리셋되면 성공.
|
||||
|
||||
### 4. E2E 테스트
|
||||
|
||||
```bash
|
||||
# 1. Claude 와 대화 → 토큰 누적
|
||||
# 2. cat .claude/state/aptabase-accum.json (accum 비어있지 않음 확인)
|
||||
git commit --allow-empty -m "test: aptabase hook #999"
|
||||
# 3. Aptabase 대시보드에서 claude_commit 이벤트 확인
|
||||
# 4. cat .claude/state/aptabase-accum.json (accum 리셋 확인)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 누적 상태 파일
|
||||
|
||||
`<git-root>/.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..."
|
||||
}
|
||||
```
|
||||
|
||||
- `accum`: 현재 누적 중인 토큰 (커밋 시 리셋됨)
|
||||
- `offsets`: 트랜스크립트 JSONL 별로 이미 읽은 byte 위치 (append-only 특성 덕에 중복 집계 방지)
|
||||
- `last_sent_commit`: 마지막으로 Aptabase 에 전송한 HEAD 해시 (중복 전송 방지)
|
||||
|
||||
**수동 리셋:** `rm .claude/state/aptabase-accum.json`
|
||||
|
||||
---
|
||||
|
||||
## 엣지 케이스
|
||||
|
||||
| 상황 | 동작 |
|
||||
|---|---|
|
||||
| 첫 설치 후 첫 Bash 호출 | `last_sent_commit` 가 빈 상태 → 현재 HEAD 를 기준점으로 기록만. 전송 안 함 |
|
||||
| 네트워크 실패 | `state` 유지. 다음 호출에서 재시도 (누적값 + 새 커밋 병합) |
|
||||
| Claude 가 Bash 로 커밋 | `PostToolUse(Bash)` 와 `.git/hooks/post-commit` 둘 다 발동. `last_sent_commit` 덕에 한 번만 전송 |
|
||||
| 외부(IDE)에서 커밋 | `.git/hooks/post-commit` 만 발동 |
|
||||
| git 저장소 밖에서 실행 | `git rev-parse HEAD` 실패 → 조용히 종료 |
|
||||
| OAuth 로그인 안 됨 (API 키) | `claude_oauth_id = "anonymous"`, `plan = "apikey"` |
|
||||
| `amend`, `rebase`, `cherry-pick` | HEAD 해시가 바뀌므로 모두 포착 |
|
||||
|
||||
---
|
||||
|
||||
## 트러블슈팅
|
||||
|
||||
**누적이 안 쌓임 (`accum` 이 계속 0)**
|
||||
1. `aptabase.json` 의 `enabled: true`
|
||||
2. `app_key`, `aptabase_host` 실제 값인지
|
||||
3. settings.json 의 `Stop` 훅에 `aptabase-accumulate.sh` 등록되어 있는지
|
||||
4. `transcript_path` 가 훅 입력 JSON 에 실제로 있는지 (Claude Code 버전 확인)
|
||||
5. 해당 트랜스크립트 파일 읽기 권한
|
||||
|
||||
**커밋했는데 Aptabase 에 안 뜸**
|
||||
1. settings.json 의 `PostToolUse(Bash)` 훅 등록 확인
|
||||
2. `.git/hooks/post-commit` 이 실제로 있는지: `cat .git/hooks/post-commit`
|
||||
3. `last_sent_commit` 이 이미 현재 HEAD 와 같은지 (이미 전송됨)
|
||||
4. curl 로 직접 POST 했을 때 200 이 오는지 (네트워크/인증 분리 검증)
|
||||
5. `python3 --version` 동작
|
||||
6. git 저장소 안에서 실행 중인지
|
||||
|
||||
**첫 커밋이 무시됨**
|
||||
의도된 동작. 첫 실행 시 `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` 로. 훅 등록은 유지해도 된다.
|
||||
|
||||
---
|
||||
|
||||
## 제거
|
||||
|
||||
```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
|
||||
```
|
||||
Reference in New Issue
Block a user