Recent Posts

All Posts →

Linux 바이너리 역어셈블로 커널 모듈 패치 확인하기

objdump를 사용한 Linux ELF 바이너리 역어셈블 기법과, 소스 없이 두 커널 모듈(.ko)의 차이를 비교한 실제 사례를 정리합니다.

소스 코드를 구할 수 없는 Linux 바이너리를 분석해야 할 때가 있습니다. 이 글에서는 objdump 기반의 역어셈블 기법을 먼저 정리하고, 활용 예시로 Ubuntu 패키지의 intel_cvs.ko 커널 모듈과 DKMS 패치 버전을 비교한 사례의 핵심을 설명합니다.

1. 역어셈블의 기본 개념

1.1. ELF과 .ko

Linux의 실행 파일과 공유 라이브러리, 커널 모듈은 모두 ELF(Executable and Linkable Format) 포맷입니다. 특히 커널 모듈(.ko)은 relocatable object 파일로, 커널에 로드되는 시점에 주소가 확정됩니다. 따라서 역어셈블에서 call 대상이 `0x0000`으로 보이는 것은 비정상이 아니라 재배치가 아직 적용되지 않았기 때문입니다.

ELF는 섹션 단위로 구성됩니다.

  • .text — 코드

  • .rodata — 읽기 전용 데이터, 문자열 리터럴

  • .data — 전역 변수

  • .symtab — 심볼 테이블

최근 Ubuntu OEM 커널은 모듈을 zstd로 압축하므로 `zstd -d`로 먼저 해제해야 합니다.

1.2. 핵심 도구

도구 용도

objdump

ELF 역어셈블. `-d -M intel`이 가장 많이 쓰는 조합

nm

심볼 테이블 조회. 함수 주소와 이름 확인

readelf

ELF 헤더, 섹션, 재배치 정보 확인

strings

바이너리 안의 문자열 추출. 힌트 얻기 용도

modinfo

커널 모듈 메타데이터

pahole

DWARF 디버그 정보에서 구조체 레이아웃 추출

1.3. x86-64 호출 규약 (System V AMD64 ABI)

역어셈블을 읽기 위해 가장 먼저 알아야 하는 것은 호출 규약입니다. Linux는 System V AMD64 ABI를 따르며, 함수 인자는 정해진 순서의 레지스터로 전달됩니다.

인자 순서 레지스터 비고

1번째

rdi / edi

2번째

rsi / esi

3번째

rdx / edx

4번째

rcx

5번째

r8

6번째

r9

반환값

rax

예를 들어 다음 시퀀스는 foo(0x805, NULL, 0) 호출과 정확히 대응됩니다.

xor    edx, edx            ; 3번째 인자: 0
xor    esi, esi            ; 2번째 인자: NULL
mov    edi, 0x805          ; 1번째 인자: 0x805
call   foo

1.4. 자주 보이는 x86-64 명령어

명령어 의미

mov

값 복사. mov edi, 0x805edi = 0x805

movzx

작은 값을 0 확장하여 복사. 1바이트를 32비트로 읽을 때

lea

주소 계산만 수행 (메모리 접근 없음). 포인터 전달 용도

test reg, reg

같은 레지스터로 AND. 0인지 검사

cmp

뺄셈 후 플래그만 설정

je / jne

조건 점프 (equal / not equal)

xor reg, reg

레지스터를 0으로 초기화 (`mov reg, 0`보다 짧음)

call

함수 호출

1.5. 상수로 호출 지점 찾기

소스에서 #define 값을 알고 있으면 역어셈블에서 해당 상수를 검색해 호출 지점을 좁힐 수 있습니다. 다만 같은 값이 다른 맥락(예: 길이, 인덱스)에서 쓰일 수 있으므로, 그 상수가 어느 레지스터에 들어가는지와 주변 call 대상을 함께 확인해야 합니다.

2. 활용 예시: 커널 모듈 바이너리 비교

2.1. 문제 상황

Ubuntu의 linux-modules-vision-*-oem 패키지에 포함된 `intel_cvs.ko`가 Intel 업스트림의 특정 버그 픽스를 반영하고 있는지 확인해야 했습니다. 패키지 changelog에는 어느 커밋까지 포함되었는지 명시되어 있지 않고, DKMS로 설치한 패치 버전과 어떻게 다른지를 확인해야 했습니다.

2.2. 왜 GitHub 소스 비교 대신 역어셈블을 선택했나

Intel의 업스트림 저장소가 GitHub에 공개되어 있으므로 소스 레벨 `diff`가 더 쉬운 선택처럼 보입니다. 그런데도 역어셈블을 택한 이유는 다음과 같습니다.

  • 어떤 커밋에서 빌드되었는지 모른다. Ubuntu OEM 패키지의 changelog에는 Intel 업스트림의 어떤 시점 스냅샷인지가 명시되어 있지 않았습니다. 임의의 커밋에서 찍은 소스와 비교하면 "차이"가 진짜 패치 누락 때문인지 단순한 버전 갭 때문인지 구분되지 않습니다.

  • Ubuntu가 자체 패치를 얹었을 수 있다. 패키지 빌드는 업스트림 tarball에 distro 패치를 덧붙이는 경우가 흔합니다. 업스트림 소스만으로는 실제로 빌드된 결과를 재현할 수 없습니다.

  • 바이너리가 ground truth다. 커널이 실제로 로드하는 것은 .ko 바이너리입니다. "이 시스템에서 지금 돌고 있는 코드에 해당 가드가 있는가"라는 질문의 답은 바이너리 안에만 있습니다.

  • 확인하려는 범위가 작다. 특정 한 가지 패치(특정 상수 호출 앞의 분기 유무)만 확인하면 충분했습니다. 소스 트리 전체를 받아 빌드 환경을 맞추는 수고보다 objdump 한 번이 더 빠른 상황이었습니다.

즉, 소스 비교는 "업스트림에 그 패치가 존재하는가"에 답하지만, 이번에 필요한 답은 "내 시스템에 설치된 바이너리에 그 패치가 반영되어 있는가"였고, 이 질문에는 바이너리를 직접 보는 것이 가장 확실한 방법이었습니다.

2.3. 모듈 추출과 역어셈블

# 압축 해제
zstd -d -c /lib/modules/$(uname -r)/ubuntu/vision/intel_cvs.ko.zst > /tmp/pkg.ko
zstd -d -c /lib/modules/$(uname -r)/updates/dkms/intel_cvs.ko.zst > /tmp/dkms.ko

# 동일성 확인
md5sum /tmp/pkg.ko /tmp/dkms.ko

# 관심 함수만 Intel 문법으로 역어셈블
objdump -d -M intel --no-show-raw-insn /tmp/pkg.ko \
  | sed -n '/<cvs_common_probe>:/,/^$/p' > /tmp/pkg_probe.asm

objdump -d -M intel --no-show-raw-insn /tmp/dkms.ko \
  | sed -n '/<cvs_common_probe>:/,/^$/p' > /tmp/dkms_probe.asm

-M intel`은 `mov dst, src 순서의 Intel 문법을 사용합니다 (기본은 `mov src, dst`의 AT&T 문법). `--no-show-raw-insn`은 기계어 바이트를 생략해 가독성을 높입니다.

2.4. 핵심 차이 찾기

확인하고 싶은 패치는 아래 형태의 가드였습니다.

if (icvs->magic_num_support) {
    ret = cvs_write_i2c(SET_HOST_IDENTIFIER, NULL, 0);
}

SET_HOST_IDENTIFIER = 0x0805`는 첫 번째 인자이므로 `mov edi, 0x805 패턴으로 역어셈블에서 검색할 수 있습니다.

DKMS 버전 (가드 있음)

movzx  eax, BYTE PTR [r12+0x4c]  ; icvs->magic_num_support 로드
and    ebx, 0x1
test   bl, bl                    ; 0인지 검사
je     12c7                      ; 0이면 호출 건너뜀
xor    edx, edx                  ; len = 0
xor    esi, esi                  ; buf = NULL
mov    edi, 0x805                ; SET_HOST_IDENTIFIER
call   cvs_write_i2c

패키지 버전 (가드 없음)

xor    edx, edx
xor    esi, esi
mov    edi, 0x805
call   cvs_write_i2c             ; 무조건 실행됨

mov edi, 0x805 바로 앞에 test + `je`로 된 분기가 있느냐 없느냐가 두 바이너리의 실질적 차이였습니다. 즉 패키지 버전에는 해당 패치가 반영되어 있지 않다는 결론이 나옵니다.

2.5. 재배치 정보로 call 대상 확인

.ko`의 `call 대상이 0x0000`이거나 어색한 주소로 보일 때는 `-r 옵션으로 재배치 정보를 함께 출력하면 원래 타깃 심볼을 알 수 있습니다.

objdump -d -r -M intel /tmp/pkg.ko
127f: call   1284 <cvs_common_probe+0x104>
      1280: R_X86_64_PLT32  cvs_write_i2c-0x4

3. 주의할 점

컴파일러 최적화로 같은 소스여도 명령어 순서와 레지스터 선택이 달라집니다. 정확히 같은 시퀀스가 아니라 논리적 흐름(분기 구조) 을 비교해야 합니다. DKMS 빌드와 패키지 빌드의 GCC 버전이 같으면 차이가 적고, 다르면 코드 구조 자체가 꽤 달라 보일 수 있습니다.

재배치 주소 차이는 무시해야 합니다. diff`를 돌리면 `je 1553 vs `je 12c7`처럼 점프 대상 주소가 대량으로 다르게 나오는데, 이것은 함수 배치 차이일 뿐이고 주목해야 할 것은 명령어 자체의 추가/삭제입니다.

상수 검색의 false positive: mov edi, 0x805`가 반드시 원하는 호출은 아닐 수 있습니다. 어떤 함수 안에 있는지, 직후의 `call 대상이 무엇인지로 맥락을 확인해야 합니다.

그리고 당연한 이야기지만, 소스를 구할 수 있다면 역어셈블보다 `diff`가 훨씬 빠르고 정확합니다. 역어셈블은 소스를 확보할 수 없을 때의 최후 수단입니다.

4. 참고 자료


이 포스트는 Claude Code가 정상혁의 지시에 따라서 작성했습니다.

Claude Code의 Prompt Cache 버그 (v2.1.69~v2.1.89)

Claude Code v2.1.69~v2.1.89에서 발생한 Prompt Cache 버그의 원인, 영향 범위, 비용 영향을 정리합니다.

변경 이력
  • 2026-04-06: Prompt Cache TTL 기본값(5분)과 1시간 옵션에 대한 설명 추가

2026년 3월 초부터 약 한 달간 Claude Code에 Prompt Cache가 제대로 작동하지 않는 버그가 있었습니다. Claude Code 토큰 비용과 프롬프트 캐싱에서 정리했듯이 Prompt Caching은 입력 토큰 비용을 90%까지 절약하는 핵심 수단인데, 이 캐시가 작동하지 않으면 비용이 크게 증가합니다. 이 글에서는 해당 기간에 발생한 캐시 버그의 원인과 영향을 정리합니다.

1. 버그 요약

항목 내용

영향 버전

v2.1.69 (2026-03-05) ~ v2.1.89

수정 버전

v2.1.90 (2026-04-01)

근본 원인

v2.1.69에서 도입된 deferred_tools_delta`로 인해 resume 시 `messages[0] 구조가 새 세션과 달라짐

영향 대상

--resume 또는 `/resume`으로 세션을 재개하는 사용자

GitHub Issue

#34629, #28899

2. 원인: Resume 시 메시지 구조 불일치

2.1. 근본 원인

v2.1.69에서 도입된 deferred_tools_delta 기능이 원인입니다. 이 기능은 deferred tools 목록, MCP 서버 설정, skills 정보를 메시지에 첨부하는 방식을 변경했는데, 새 세션과 재개 세션에서 `messages[0]`의 구조가 달라지는 문제가 생겼습니다.

세션 유형 messages[0] 내용 크기

새 세션

deferred tools 목록, MCP 설정, skills, 날짜 컨텍스트

~13.4KB

재개 세션

날짜 컨텍스트만 포함 (나머지는 messages 끝에 추가)

~352B

Prompt Cache는 prefix의 해시값으로 캐시를 조회합니다. `messages[0]`의 내용이 13.4KB에서 352B로 바뀌면 해시값이 완전히 달라지므로, 이전 세션에서 쌓은 캐시를 전혀 재사용할 수 없었습니다.

2.2. MCP 서버, deferred tools와의 관계

이 버그는 MCP 서버나 deferred tools를 사용하는 환경에서 더 큰 영향을 받았습니다. 다만 이것은 MCP 서버의 tool schema가 불안정해서가 아니라, deferred tools/MCP 설정/skills 정보가 resume 시 `messages[0]`이 아닌 다른 위치에 배치되어 prefix가 달라지기 때문입니다. MCP 서버가 매번 완전히 동일한 schema를 반환하더라도, resume만 하면 메시지 구조가 달라져서 캐시 미스가 발생했습니다.

v2.1.90의 수정 내용도 이를 뒷받침합니다.

Fixed --resume causing a full prompt-cache miss on the first request for users with deferred tools, MCP servers, or custom agents

2.3. 부수적 문제: 매 턴 tool schema 재직렬화

근본 원인과 별개로, Claude Code가 매 턴마다 MCP tool schema를 `JSON.stringify`로 재계산하여 캐시 키를 만드는 비효율도 있었습니다. 이것은 캐시 미스를 유발하는 직접적인 원인은 아니었지만 성능 오버헤드를 발생시켰고, v2.1.90에서 tool schema를 한 번만 계산하고 재사용하도록 개선되었습니다.

3. 영향 범위

이 버그는 --resume (CLI 플래그)과 /resume (슬래시 커맨드) 모두에 해당합니다. 두 방식 모두 세션 재개 시 동일한 메시지 구조 불일치가 발생하기 때문입니다.

단, Prompt Cache의 TTL이 기본 5분 [1] 이므로 실질적인 비용 손해가 발생하는 경우는 제한적입니다.

시나리오 영향

일반 연속 세션 (resume 없이)

영향 없음. `messages[0]`이 세션 내내 안정적으로 유지

5분 이내에 resume

캐시가 살아있어야 하는데 prefix 불일치로 캐시 미스 발생 → 비용 손해

5분 이후에 resume

어차피 TTL 만료로 캐시 소멸 → 버그와 무관하게 비용 동일

--print --resume 자동화

매 호출이 새 프로세스이므로 캐시를 전혀 재활용하지 못함 → 가장 큰 피해

4. 비용 영향

정상 상태에서는 캐시가 쌓이면서 턴이 진행될수록 메시지당 비용이 감소합니다.

정상 (v2.1.68):  메시지 1 → $0.15,  메시지 4 → $0.02 (캐시 적중률 증가)
버그 (v2.1.69+): 모든 메시지 → $0.35~0.40 (매번 전체 히스토리를 Cache Write)

--print --resume 워크플로우에서는 cache_read 토큰이 ~11,000(시스템 프롬프트 분량)에 머물고, cache_creation 토큰이 300,000 이상으로 치솟는 현상이 관찰되었습니다. 메시지당 약 20배의 비용 증가가 보고되었습니다.

5. 수정 내용 (v2.1.90)

v2.1.90 (2026-04-01)에서 다음과 같이 수정되었습니다.

  • 메시지 구조 정규화: 세션 재개 시 `messages[0]`에 deferred tools, MCP 설정, skills를 새 세션과 동일한 위치에 삽입하도록 변경

  • tool schema 재직렬화 제거: MCP tool schema를 한 번만 계산하고 캐시하여, 매 턴 `JSON.stringify`를 호출하지 않도록 성능 개선

현재 버전 확인:

claude --version

v2.1.90 이상이면 수정된 상태입니다.

6. 영향 받았는지 확인하는 방법

2026년 3월 5일~4월 1일 사이에 Claude Code를 사용했다면, 다음 방법으로 영향 여부를 확인할 수 있습니다.

6.1. Resume 사용 여부 확인

이 버그는 resume 시에만 발생하므로, 해당 기간에 resume을 사용했는지가 핵심입니다.

6.1.1. /resume (슬래시 커맨드) 확인

대화형 세션 안에서 `/resume`을 입력한 기록은 `~/.claude/history.jsonl`에 남습니다.

Claude Code 프롬프트
~/.claude/history.jsonl에서 2026년 3월 5일~4월 1일 사이의 /resume 기록을 찾아줘.
각 resume 시점에 대해, 같은 프로젝트의 ~/.claude/projects/ 아래 JSONL 세션 로그에서
직전 세션의 마지막 timestamp와의 시간 간격을 계산해줘.
5분 이내에 resume한 경우가 있는지 확인하고 싶어.

6.1.2. --resume (CLI 플래그) 확인

`claude --resume`처럼 CLI 인자로 실행한 경우는 `history.jsonl`에 기록되지 않습니다. 이 경우는 shell history에서 확인합니다.

bash 사용자
grep "claude.*--resume" ~/.bash_history
zsh 사용자
grep "claude.*--resume" ~/.zsh_history

bash는 기본적으로 명령 실행 시각을 기록하지 않으므로 [2], --resume 사용 여부만 확인할 수 있고 5분 간격 분석은 어렵습니다. zsh는 EXTENDED_HISTORY 옵션이 켜져 있으면 `~/.zsh_history`에 타임스탬프가 함께 기록됩니다.

--resume`을 주로 자동화 스크립트(--print --resume`)에서 사용했다면 해당 스크립트의 실행 로그를 확인하는 것이 더 정확합니다.

5분 이내에 resume한 기록이 없다면, Prompt Cache의 TTL(5분)이 만료된 후 재개한 것이므로 이 버그로 인한 추가 비용은 발생하지 않은 것입니다.

6.2. API 사용량 확인

Anthropic Console에서 해당 기간의 토큰 사용량을 확인합니다. 평소 대비 `cache_creation_input_tokens`가 비정상적으로 높고 `cache_read_input_tokens`가 낮다면 이 버그의 영향을 받았을 가능성이 있습니다.

6.3. 캐시 메트릭 모니터링

세션 중 API 응답의 캐시 관련 토큰을 확인합니다.

  • cache_creation_input_tokens: 대화가 진행될수록 감소해야 정상

  • cache_read_input_tokens: 대화가 진행될수록 증가해야 정상

버그 발생 시에는 `cache_read_input_tokens`가 낮은 값에 머물고, `cache_creation_input_tokens`가 계속 높게 유지됩니다.


1. Anthropic API는 기본 5분(ephemeral)과 1시간("ttl": "1h") 두 가지 TTL 옵션을 제공합니다. 1시간 옵션은 cache write 비용이 기본 입력 토큰의 2배(5분은 1.25배)로 더 높습니다. Claude Code에서는 구독 플랜에 따라 자동 결정되며(Max 플랜: 1시간, Pro 플랜/API 키: 5분), 사용자가 직접 설정할 수는 없습니다. 자세한 내용은 Anthropic 공식 문서를 참고하세요.
2. HISTTIMEFORMAT`을 설정하면 `history 명령에서 시각이 표시되지만, ~/.bash_history 파일 자체에는 타임스탬프가 저장되지 않습니다.

Claude Code 비용 최적화를 위한 지표 수집

Claude Code의 사용 패턴을 분석하고 개인별 개선 권고를 만들기 위해, Hook과 OpenTelemetry로 어떤 지표를 수집하고 활용할 수 있는지 정리합니다.

Claude Code를 효율적으로 사용하고 있는지 개인적으로나 팀 차원에서 자주 돌아보고 개선 시도를 한다면, 같은 작업을 더 적은 비용으로 할 수 있습니다. 이 글에서는 Claude Code의 Hook과 OpenTelemetry를 활용하여 사용 지표를 수집하고, 수집된 데이터로 개인별 개선 권고 리포트를 만드는 방법을 정리합니다.

1. 수집해야 할 지표

비용 최적화 관점에서 의미 있는 지표는 크게 네 영역으로 나뉩니다.

1.1. 컨텍스트 관리 효율성

컨텍스트가 가득 차서 자동 압축(autocompact)이 발생하면, 그만큼 한 세션에서 많은 토큰을 소비했다는 신호입니다.

지표 의미

세션 시작 유형 (source)

startup(신규), resume(재개), compact(압축 후), clear(/clear 후) 비율로 사용 습관을 파악

자동 압축 빈도

세션당 autocompact 횟수가 많으면 작업 단위를 더 작게 분리할 필요가 있음

압축 간격

압축 사이 시간이 짧을수록 컨텍스트를 빠르게 소진하고 있다는 의미

1.2. 도구 사용 패턴

Claude Code는 파일 읽기, 편집, 명령 실행 등 도구를 호출할 때마다 API 호출이 발생합니다. 도구 사용 방식에 따라 같은 작업의 토큰 소비량이 크게 달라집니다.

지표 의미

도구별 호출 빈도

어떤 도구를 주로 쓰는지 파악. Bash`로 `cat, grep`을 반복 호출하면 전용 도구(`Read, Grep)보다 토큰을 더 소비

도구 호출 실패율

실패 후 재시도는 토큰 낭비. 반복 실패 패턴은 사용자 교육이 필요한 영역

서브에이전트 생성 빈도

Agent 도구로 서브에이전트를 생성하면 추가 API 호출이 발생. 불필요한 서브에이전트 생성은 비용 증가 요인

1.3. 세션 수명 주기

지표 의미

세션 길이 (시작~종료 시간)

세션이 길수록 컨텍스트가 누적되어 턴당 입력 토큰이 증가 [1]

세션 종료 사유

정상 종료, 비정상 종료, rate limit 등의 비율

1.4. API 호출과 비용

가장 직접적인 비용 지표입니다.

지표 의미

API 호출 횟수

세션당, 프롬프트당 API 호출 수

호출당 토큰 수

입력/출력/캐시 읽기/캐시 쓰기 토큰

호출당 비용 (USD)

모델별 단가 적용된 실제 비용

모델별 사용 비율

Opus vs Sonnet vs Haiku. Opus는 Sonnet 대비 5배 비용

캐시 히트율

캐시 읽기 토큰 비율이 높을수록 비용 효율적

2. 지표별 수집 방법

수집 방법은 크게 두 가지입니다. Hook은 사용 행동 패턴을, OpenTelemetry는 정확한 비용 데이터를 수집합니다.

2.1. Hook으로 수집하는 지표

Claude Code의 Hook은 특정 이벤트 발생 시 셸 스크립트를 실행하는 기능입니다. 비용 데이터에는 직접 접근할 수 없지만, 사용 행동 패턴을 기록하는 데 적합합니다.

Hook 이벤트 수집 가능한 지표 비고

SessionStart

세션 시작 유형(source), 사용 모델(model)

source 값: startup, resume, compact, clear

SessionEnd

세션 종료 시각, 종료 사유

세션 시작 시각과 조합하면 세션 길이 산출

PreCompact

압축 발생 시각, 자동/수동 여부

`matcher: auto`로 자동 압축만 필터링 가능

PreToolUse

도구 이름, 호출 시각

고빈도 이벤트이므로 성능 영향 고려 필요

PostToolUseFailure

실패한 도구, 에러 내용

반복 실패 패턴 분석용

SubagentStart

서브에이전트 생성 시각

불필요한 서브에이전트 생성 패턴 분석

2.1.1. 수집 스크립트 예시

하나의 스크립트에서 이벤트 유형별로 분기하여 JSONL 형식으로 기록합니다.

~/.claude/scripts/log-usage.sh
#!/bin/bash
INPUT=$(cat)
EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
SESSION=$(echo "$INPUT" | jq -r '.session_id')
TIMESTAMP=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
USER=$(whoami)

LOG_DIR="$HOME/.claude/usage-logs"
mkdir -p "$LOG_DIR"

case "$EVENT" in
  SessionStart)
    SOURCE=$(echo "$INPUT" | jq -r '.source')
    MODEL=$(echo "$INPUT" | jq -r '.model')
    echo "{\"ts\":\"$TIMESTAMP\",\"user\":\"$USER\",\"event\":\"session_start\",\"session\":\"$SESSION\",\"source\":\"$SOURCE\",\"model\":\"$MODEL\"}" \
      >> "$LOG_DIR/events.jsonl"
    ;;
  SessionEnd)
    echo "{\"ts\":\"$TIMESTAMP\",\"user\":\"$USER\",\"event\":\"session_end\",\"session\":\"$SESSION\"}" \
      >> "$LOG_DIR/events.jsonl"
    ;;
  PreCompact)
    echo "{\"ts\":\"$TIMESTAMP\",\"user\":\"$USER\",\"event\":\"compact\",\"session\":\"$SESSION\"}" \
      >> "$LOG_DIR/events.jsonl"
    ;;
  PreToolUse)
    TOOL=$(echo "$INPUT" | jq -r '.tool_name')
    echo "{\"ts\":\"$TIMESTAMP\",\"user\":\"$USER\",\"event\":\"tool_use\",\"session\":\"$SESSION\",\"tool\":\"$TOOL\"}" \
      >> "$LOG_DIR/events.jsonl"
    ;;
  PostToolUseFailure)
    TOOL=$(echo "$INPUT" | jq -r '.tool_name')
    echo "{\"ts\":\"$TIMESTAMP\",\"user\":\"$USER\",\"event\":\"tool_fail\",\"session\":\"$SESSION\",\"tool\":\"$TOOL\"}" \
      >> "$LOG_DIR/events.jsonl"
    ;;
  SubagentStart)
    echo "{\"ts\":\"$TIMESTAMP\",\"user\":\"$USER\",\"event\":\"subagent\",\"session\":\"$SESSION\"}" \
      >> "$LOG_DIR/events.jsonl"
    ;;
esac

2.1.2. settings.json 설정

~/.claude/settings.json
{
  "hooks": {
    "SessionStart": [
      { "hooks": [{ "type": "command", "command": "~/.claude/scripts/log-usage.sh" }] }
    ],
    "SessionEnd": [
      { "hooks": [{ "type": "command", "command": "~/.claude/scripts/log-usage.sh" }] }
    ],
    "PreCompact": [
      { "matcher": "auto",
        "hooks": [{ "type": "command", "command": "~/.claude/scripts/log-usage.sh" }] }
    ],
    "PreToolUse": [
      { "hooks": [{ "type": "command", "command": "~/.claude/scripts/log-usage.sh" }] }
    ],
    "PostToolUseFailure": [
      { "hooks": [{ "type": "command", "command": "~/.claude/scripts/log-usage.sh" }] }
    ],
    "SubagentStart": [
      { "hooks": [{ "type": "command", "command": "~/.claude/scripts/log-usage.sh" }] }
    ]
  }
}

2.2. OpenTelemetry로 수집하는 지표

Hook에서는 API 호출 횟수, 토큰 수, 비용 같은 핵심 비용 데이터에 접근할 수 없습니다. 이 데이터는 Claude Code의 OpenTelemetry 연동을 통해 수집할 수 있습니다.

Claude Code는 claude_code.api_request 이벤트를 OTLP로 내보냅니다. 이 이벤트에는 다음 필드가 포함됩니다.

필드 설명

model

사용된 모델 (예: claude-sonnet-4-6)

cost_usd

해당 API 호출의 비용 (USD)

input_tokens

입력 토큰 수

output_tokens

출력 토큰 수

cache_read_tokens

캐시에서 읽은 토큰 수

cache_creation_tokens

캐시에 새로 쓴 토큰 수

duration_ms

API 응답 시간

event.sequence

세션 내 API 호출 순번 (순서 보장용)

prompt.id

사용자 프롬프트 단위 UUID (한 프롬프트에서 여러 API 호출이 발생할 수 있음)

2.2.1. 활성화 방법

환경 변수를 설정하면 Claude Code가 OTLP 프로토콜로 텔레메트리를 전송합니다.

export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_LOGS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317

개발자 PC의 셸 프로파일(~/.bashrc, ~/.zshrc 등)에 추가하거나, 전사 dotfiles 관리 도구로 배포할 수 있습니다.

2.3. 수집 방법 요약

지표 Hook OpenTelemetry

세션 시작/종료, 압축 빈도

O

도구 사용 패턴, 실패율

O

API 호출 횟수

O

토큰 수 (입력/출력/캐시)

O

호출당 비용 (USD)

O

모델별 사용 비율

O

O

3. 개인 분석: 로컬 로그로 셀프 리포트 만들기

전사 수집 인프라가 없더라도, 앞서 설명한 Hook 스크립트로 로컬에 쌓인 ~/.claude/usage-logs/events.jsonl 파일만으로 개인 사용 패턴을 분석할 수 있습니다.

3.1. 분석 스크립트 예시

`jq`와 셸 명령만으로 주요 지표를 산출하는 스크립트입니다.

~/.claude/scripts/self-report.sh
#!/bin/bash
LOG="$HOME/.claude/usage-logs/events.jsonl"

if [ ! -f "$LOG" ]; then
  echo "로그 파일이 없습니다: $LOG"
  exit 1
fi

# 분석 기간 (기본: 최근 7일)
SINCE=$(date -u -d '7 days ago' '+%Y-%m-%dT00:00:00Z' 2>/dev/null \
     || date -u -v-7d '+%Y-%m-%dT00:00:00Z')  # macOS 호환
DATA=$(jq -c --arg since "$SINCE" 'select(.ts >= $since)' "$LOG")

echo "=== 셀프 사용 리포트 (최근 7일) ==="
echo ""

# 세션 수
SESSIONS=$(echo "$DATA" | jq -r 'select(.event=="session_start") | .session' | sort -u | wc -l)
echo "[세션 요약]"
echo "  세션 수: $SESSIONS"

# 세션 시작 유형 비율
echo "  시작 유형:"
echo "$DATA" | jq -r 'select(.event=="session_start") | .source' \
  | sort | uniq -c | sort -rn | while read count source; do
    echo "    $source: $count"
  done

# autocompact 횟수
COMPACTS=$(echo "$DATA" | jq -r 'select(.event=="compact")' | wc -l)
echo "  자동 압축 횟수: $COMPACTS (세션당 평균: $(echo "scale=1; $COMPACTS / $SESSIONS" | bc 2>/dev/null || echo "N/A"))"
echo ""

# 도구 사용 패턴
echo "[도구 사용]"
TOTAL_TOOLS=$(echo "$DATA" | jq -r 'select(.event=="tool_use")' | wc -l)
echo "  총 도구 호출: $TOTAL_TOOLS"
echo "  상위 도구:"
echo "$DATA" | jq -r 'select(.event=="tool_use") | .tool' \
  | sort | uniq -c | sort -rn | head -10 | while read count tool; do
    pct=$(echo "scale=0; $count * 100 / $TOTAL_TOOLS" | bc 2>/dev/null || echo "?")
    echo "    $tool: $count (${pct}%)"
  done

# Bash로 cat/grep/find 호출 비율 (도구 이름만으로는 Bash 내부 명령을 알 수 없으므로 Bash 비율만 표시)
BASH_COUNT=$(echo "$DATA" | jq -r 'select(.event=="tool_use" and .tool=="Bash")' | wc -l)
if [ "$TOTAL_TOOLS" -gt 0 ]; then
  BASH_PCT=$(echo "scale=0; $BASH_COUNT * 100 / $TOTAL_TOOLS" | bc 2>/dev/null || echo "?")
  echo "  Bash 도구 비율: ${BASH_PCT}% (30% 이상이면 전용 도구 활용 권장)"
fi

# 실패율
FAILS=$(echo "$DATA" | jq -r 'select(.event=="tool_fail")' | wc -l)
if [ "$TOTAL_TOOLS" -gt 0 ]; then
  FAIL_PCT=$(echo "scale=1; $FAILS * 100 / $TOTAL_TOOLS" | bc 2>/dev/null || echo "?")
  echo "  도구 호출 실패: $FAILS (실패율: ${FAIL_PCT}%)"
fi

# 서브에이전트
SUBAGENTS=$(echo "$DATA" | jq -r 'select(.event=="subagent")' | wc -l)
echo "  서브에이전트 생성: $SUBAGENTS (세션당 평균: $(echo "scale=1; $SUBAGENTS / $SESSIONS" | bc 2>/dev/null || echo "N/A"))"
echo ""

# 간단한 권고
echo "[자동 권고]"
COMPACT_AVG=$(echo "scale=1; $COMPACTS / $SESSIONS" | bc 2>/dev/null || echo "0")
if [ "$(echo "$COMPACT_AVG >= 3" | bc 2>/dev/null)" = "1" ]; then
  echo "  ⚠ 세션당 autocompact가 ${COMPACT_AVG}회입니다. 작업을 더 작은 단위로 분리하세요."
fi
if [ "$(echo "$BASH_PCT >= 30" | bc 2>/dev/null)" = "1" ]; then
  echo "  ⚠ Bash 도구 비율이 ${BASH_PCT}%입니다. Read, Grep, Glob 전용 도구를 활용하세요."
fi
if [ "$(echo "$FAIL_PCT > 15" | bc 2>/dev/null)" = "1" ]; then
  echo "  ⚠ 도구 호출 실패율이 ${FAIL_PCT}%입니다. 반복 실패하는 도구의 사용법을 점검하세요."
fi
SUBAGENT_AVG=$(echo "scale=1; $SUBAGENTS / $SESSIONS" | bc 2>/dev/null || echo "0")
if [ "$(echo "$SUBAGENT_AVG > 5" | bc 2>/dev/null)" = "1" ]; then
  echo "  ⚠ 세션당 서브에이전트가 평균 ${SUBAGENT_AVG}회입니다. 간단한 검색은 직접 도구를 호출하세요."
fi
echo "  (비용 관련 권고는 OpenTelemetry 데이터가 필요합니다)"

스크립트 실행 결과 예시:

=== 셀프 사용 리포트 (최근 7일) ===

[세션 요약]
  세션 수: 15
  시작 유형:
    startup: 10
    resume: 3
    compact: 2
  자동 압축 횟수: 5 (세션당 평균: 0.3)

[도구 사용]
  총 도구 호출: 312
  상위 도구:
    Read: 89 (28%)
    Edit: 67 (21%)
    Bash: 58 (18%)
    Grep: 45 (14%)
    Glob: 32 (10%)
    Write: 12 (3%)
    Agent: 9 (2%)
  Bash 도구 비율: 18% (30% 이상이면 전용 도구 활용 권장)
  도구 호출 실패: 8 (실패율: 2.5%)
  서브에이전트 생성: 9 (세션당 평균: 0.6)

[자동 권고]
  (비용 관련 권고는 OpenTelemetry 데이터가 필요합니다)

이 스크립트는 별도의 인프라 없이도 개인이 즉시 실행하여 자신의 사용 습관을 점검할 수 있습니다. 팀 평균과의 비교가 필요 없는 개인 수준의 개선에는 이것만으로 충분합니다.

4. 수집 데이터 저장소

다수 사용자의 데이터를 분석하려면 중앙 저장소가 필요합니다. 조직 규모와 기존 인프라에 따라 선택지가 달라집니다.

4.1. 저장소 구성

개발자 PC                         중앙 수집                    분석/리포트
┌──────────────┐                ┌─────────────────┐         ┌──────────────┐
│ Claude Code  │                │                 │         │              │
│  ├ Hook      │── JSONL 전송 ─▶│  저장소          │────────▶│  분석 파이프  │
│  └ OTel      │── OTLP ──────▶│                 │         │  라인        │
└──────────────┘                └─────────────────┘         └──────────────┘

4.2. 저장소 선택지

방식 장점 단점 적합한 규모

파일 기반 (JSONL + S3)

구축 비용 최소, 별도 인프라 불필요

실시간 쿼리 어려움, 분석 시 별도 로딩 필요

소규모 (10명 이하)

OpenTelemetry Collector + Clickhouse

OTLP 네이티브 수집, 대량 시계열 데이터에 강점

Clickhouse 운영 부담

중규모 (10~100명)

Grafana + Loki/Tempo

기존 모니터링 인프라 활용 가능, 대시보드 내장

학습 곡선, 커스텀 리포트 생성이 번거로움

모니터링 인프라가 이미 있는 조직

BigQuery / Athena

SQL로 자유로운 분석, 대규모 데이터 처리

클라우드 비용, 초기 파이프라인 구축

대규모 (100명 이상)

4.3. Hook 데이터 중앙 수집

Hook은 기본적으로 개발자 PC의 로컬 파일에 기록합니다. 중앙으로 모으려면 추가 처리가 필요합니다. 몇 가지 방법이 있습니다.

  • cron + rsync/scp: 주기적으로 로컬 JSONL을 중앙 서버나 S3로 전송

  • Hook 스크립트에서 직접 전송: `curl`로 수집 API에 POST (단, Hook 실행 시간이 세션 성능에 영향을 줄 수 있으므로 백그라운드 전송 권장)

  • Fluentd/Fluent Bit: 로컬 JSONL 파일을 tail하여 중앙 저장소로 전송

OpenTelemetry 데이터는 OTLP 엔드포인트를 중앙 Collector로 지정하면 별도 처리 없이 수집됩니다.

5. 권고 가이드 생성 로직

수집된 데이터를 분석하여 개인별 개선 권고를 생성하는 로직입니다.

5.1. 컨텍스트 관리 권고

조건 판정 권고

세션당 autocompact >= 3회

컨텍스트 과다 사용

"작업을 더 작은 단위로 분리하세요. 한 세션에서 하나의 목적만 달성하는 것이 비용 효율적입니다."

compact 간격 < 10분

빠른 컨텍스트 소진

"프롬프트를 더 간결하게 작성하거나, 불필요한 파일 읽기를 줄여보세요."

source=resume 비율 < 10%

세션 재개를 활용하지 않음

"이전 세션을 재개(claude --resume)하면 컨텍스트를 처음부터 다시 구성하지 않아도 됩니다."

5.2. 도구 사용 권고

조건 판정 권고

Bash로 cat/grep/find 호출 비율 > 30%

전용 도구 미활용

"Bash 대신 Read, Grep, Glob 등 전용 도구를 사용하면 토큰을 절약할 수 있습니다."

도구 호출 실패율 > 15%

비효율적 도구 사용

"도구 호출 실패가 잦습니다. 에러 메시지를 확인하고 사용법을 점검해보세요."

세션당 서브에이전트 > 5회

서브에이전트 남용 가능성

"서브에이전트 생성을 줄이고, 간단한 검색은 직접 도구를 호출하는 것이 비용 효율적입니다."

5.3. 모델 선택 권고

조건 판정 권고

Opus 사용 비율 > 50%

고비용 모델 과다 사용

"단순 편집이나 검색 작업에는 Sonnet을 사용하세요. Opus는 Sonnet 대비 5배 비용입니다."

서브에이전트에서 Opus 사용

서브에이전트 비용 과다

"서브에이전트는 Sonnet이나 Haiku로 충분한 경우가 많습니다."

5.4. 비용 효율 권고

이 항목은 OpenTelemetry 데이터가 있어야 산출 가능합니다.

조건 판정 권고

캐시 히트율 < 50%

캐시 활용 부족

"세션 중간에 모델을 전환하거나 CLAUDE.md를 수정하면 캐시가 무효화됩니다. 한 세션에서는 하나의 모델을 유지하세요." [2]

프롬프트당 API 호출 > 20회

한 프롬프트에 과도한 작업 요청

"하나의 프롬프트에 여러 작업을 한꺼번에 요청하면 API 호출이 급증합니다. 작업을 나누어 요청하세요."

일일 비용이 팀 평균의 2배 이상

비용 이상치

개별 상담 대상. 사용 패턴 상세 분석 필요

5.5. 리포트 예시

위의 로직을 조합하면 다음과 같은 개인별 주간 리포트를 생성할 수 있습니다.

=== 주간 사용 리포트 (2026-03-24 ~ 2026-03-30) ===
사용자: hong

[요약]
  세션 수: 23
  총 API 호출: 847
  총 비용: $42.30
  주요 모델: Sonnet 68%, Opus 32%

[개선 권고]
  1. 컨텍스트 관리
     - 세션당 평균 autocompact: 3.2회 (팀 평균: 1.1회)
     → 작업을 더 작은 단위로 분리하세요.

  2. 도구 사용
     - Bash로 grep/cat 호출: 전체 도구 사용의 41%
     → Read, Grep 전용 도구를 직접 사용하면 토큰을 절약할 수 있습니다.

  3. 모델 선택
     - Opus 사용 비율: 32% (팀 평균: 15%)
     → 단순 작업에서는 Sonnet으로 전환을 권장합니다.
     → 예상 절감: 주당 ~$8.50

[잘하고 있는 점]
  - 캐시 히트율 87% (팀 평균: 72%)
  - 도구 호출 실패율 3% (팀 평균: 8%)

6. 주의할 점

  • PreToolUse는 고빈도 이벤트입니다. Hook 스크립트의 실행 시간이 길면 세션 응답 속도에 영향을 줄 수 있습니다. 스크립트를 가볍게 유지하거나, 샘플링을 적용하거나, 백그라운드로 기록하는 방식을 고려해야 합니다.

  • 전사 배포가 필요합니다. Hook 설정과 환경 변수를 각 개발자 PC에 배포해야 합니다. Enterprise managed config, dotfiles 저장소, 또는 온보딩 스크립트를 활용할 수 있습니다.

  • Hook으로는 정확한 토큰 수와 비용을 알 수 없습니다. 비용 분석이 목적이라면 OpenTelemetry는 필수입니다. Hook만으로는 사용 행동 패턴 분석에 한정됩니다.

  • 개인정보 고려가 필요합니다. 프롬프트 내용이나 코드를 수집하는 것이 아니라 메타데이터(이벤트 유형, 도구 이름, 타임스탬프)만 수집하지만, 수집 목적과 범위를 사전에 공유하고 동의를 구하는 것이 바람직합니다.


1. 턴당 입력 토큰의 이차 증가에 대해서는 Claude Code 토큰 비용과 프롬프트 캐싱을 참고
2. Prompt Caching의 상세 원리는 Claude Code 토큰 비용과 프롬프트 캐싱을 참고