Claude Code 로그에서 숨겨진 리마인더(Reminders) 및 컨텍스트 사용량 감사하는 방법
요약
Claude Code의 세션 로그를 분석하여 숨겨진 리마인더(Reminders)와 토큰 사용량 메타데이터를 감사하는 방법을 설명합니다. JSONL 형식의 로그에서 ip_reminder 마커와 캐시 관련 토큰 데이터를 추출하여 정확한 사용량을 파악하는 워크플로우를 제공합니다.
핵심 포인트
- Claude Code의 세션 로그(JSONL)를 통한 내부 상태 감사 방법 안내
- ip_reminder 마커 및 토큰(입력, 출력, 캐시) 데이터 추출 및 해석
- 단순 합산 시 발생할 수 있는 토큰 계산 오류 및 한계점 주의
- 구조적 점검을 통한 로컬 보고서(CSV, JSON) 구축 프로세스
Agent Lab Journal
Guides
...
고급 필드 가이드
Claude Code 로그에서 숨겨된 리마인더(Reminders) 및 컨텍스트 사용량 감사하는 방법
Advanced · 45분 읽기 · 로컬 분석 (Local analysis) · 2026년 8월 1일 업데이트
Claude Code의 눈에 보이는 트랜스크립트(transcript)가 요청 주변에 기록된 모든 것을 반드시 완벽하게 나타내는 것은 아닙니다. 서비스 메시지(Service messages), 내부 리마인더 마커(internal reminder markers), 도구 페이로드(tool payloads), 그리고 사용량 메타데이터(usage metadata)는 일반적인 채팅 턴(chat turns)으로 나타나지 않고 세션 로그(session logs)에 존재할 수 있습니다. ip_reminder가 얼마나 자주 발생하는지, 또는 입력(input), 출력(output), 캐시 생성(cache creation), 캐시 읽기(cache read) 토큰이 어떻게 분배되는지 알고 싶다면, 저장된 기록을 직접 조사하고 잘못된 합계가 나오지 않도록 충분한 구조를 보존해야 합니다.
이 가이드의 내용
-
이 감사를 통해 확립할 수 있는 것
-
구체적인 조사 사례
-
세션 하나를 찾아 선택하기
-
감사 가능한 사본 보존하기
-
빠른 구조적 점검 실행하기
-
전체 로컬 보고서 구축하기
-
리마인더(reminder) 및 토큰 데이터 해석하기
-
보고서를 독립적으로 검증하기
-
실패 사례 및 복구
-
한계점
이 감사를 통해 확립할 수 있는 것과 없는 것
이 워크플로우는 JSON Lines (JSONL) 형식으로 저장된 하나의 로컬 세션을 조사합니다: JSONL은 각 줄이 일반적으로 독립적인 JSON 값인 텍스트 형식입니다. 이 과정은 다음과 같은 보고서를 생성합니다:
-
선택된 파일의 경로, 크기, 수정 시간 및 SHA-256 다이제스트 (digest);
-
물리적 라인 수, 파싱된 레코드 (parsed records) 수, 빈 줄 및 형식이 잘못된 라인 수;
-
대소문자를 구분하는 정확한 문자열
ip_reminder를 포함하는 모든 레코드; -
해당 마커가 발견된 JSON 경로;
-
해당 필드들을 사용할 수 있는 경우의 타임스탬프 (timestamps) 및 레코드 유형;
-
레코드별 및 합계 입력 (input), 출력 (output), 캐시 생성 (cache creation), 캐시 읽기 (cache read) 토큰 값;
-
스프레드시트나 노트북에 적합한 연대순 CSV;
-
추후 비교를 위한 기계 판독 가능한 (machine-readable) JSON 보고서.
이 보고서는 선택된 파일에 무엇이 존재하는지를 보여줍니다. 이것이 리마인더 (reminder)가 왜 삽입되었는지, 저장된 그대로 모델에 전송되었는지, 또는 클라이언트의 문서화되지 않은 내부 상태 (internal state)가 어떻게 동작했는지를 증명하지는 않습니다. `ip_reminder`를 요청 파이프라인 (request pipeline)에 대한 완전한 설명이 아닌, 관찰 가능한 마커 (observable marker)로 취급하십시오. 동일한 구분이 컨텍스트 윈도우 (context window)에도 적용됩니다. 로그에 기록된 토큰 카운터는 요청 활동의 특성을 파악하는 데 도움이 될 수 있지만, 매 순간 모델에 보이는 정확한 컨텍스트를 재구성하지는 못합니다. 캐시 읽기 (cache reads), 재시도 (retries), 브랜칭 (branching), 압축 (compaction), 그리고 중복된 사용 객체 (usage objects)는 모두 단순 합산 (naive sum)을 무효화할 수 있습니다.
구체적인 사례: 긴 코딩 세션이 예상보다 비싸게 느껴질 때
긴 리팩터링 (refactoring) 세션을 상상해 보십시오. 눈에 보이는 대화에는 수십 개의 사용자 및 어시스턴트 턴 (turns)이 포함되어 있지만, 세션 로그는 예상보다 훨씬 큽니다. 당신은 두 가지를 의심할 수 있습니다:
-
클라이언트가 인터페이스에서는 명확히 드러나지 않는 반복적인
ip_reminder삽입을 기록하고 있음; -
어시스턴트의 출력이 아니라, 대규모 캐시 읽기 (cache reads) 또는 반복적인 입력 처리 (input processing)가 기록된 사용량의 대부분을 차지함;
유용한 질문은 단순히 "마커가 존재하는가?"가 아닙니다. 다음과 같습니다:
마커가 어디에서 발생하는가, 세션 전반에 걸쳐 발생 빈도가 어떻게 분포되어 있는가, 그리고 동일한 지점 주변에 어떤 토큰 사용 (token-usage) 레코드가 나타나는가?
방어 가능한 답변을 위해서는 레코드 수준의 증거가 필요합니다. 단순히 원문 텍스트 일치 횟수만 세는 것은 불충분합니다. 하나의 레코드에 해당 문자열이 여러 번 포함될 수 있고, 특정 필드가 이전 페이로드 (payload)를 인용할 수 있으며, 동일한 API 사용 (API usage) 객체가 여러 래퍼 레코드 (wrapper records)에서 반복될 수 있기 때문입니다.
전제 조건 및 안전 사항 (Prerequisites and safety)
셸 (shell), Python 3.9 이상, 그리고 로컬 Claude Code 데이터에 대한 읽기 권한이 필요합니다. 선택 사항인 스팟 체크 (spot checks)를 위해서는 rg, jq, find, sort, sha256sum, wc가 사용됩니다. macOS에서는 sha256sum 대신 shasum -a 256을 사용할 수 있습니다.
세션 파일에는 소스 코드, 프롬프트 (prompts), 도구 결과 (tool results), 로컬 경로, 환경 세부 정보 및 기타 민감한 자료가 포함될 수 있습니다. 보고서는 로컬에 보관하십시오. 검토 및 비식별화 (redacting) 과정을 거치지 않고 원본 레코드를 이슈 (issue), 공개 리포지토리 (public repository) 또는 공유 채팅에 붙여넣지 마십시오.
복사본으로 작업하기 (Work on a copy)
실제 세션 파일을 수정하지 마십시오. Claude Code가 여전히 해당 파일에 기록 중일 수 있으며, 무해한 포맷터 (formatter)라 할지라도 신뢰할 수 있는 감사를 위해 필요한 '레코드당 한 줄 (one-record-per-line)' 레이아웃을 파괴할 수 있습니다.
1단계: 정확히 하나의 세션 위치 파악 및 선택 (Step 1: locate and select exactly one session)
설치 방식과 버전에 따라 로컬 데이터 구성이 다를 수 있으므로, 고정된 경로를 가정하기보다는 탐색부터 시작하십시오. 다음 명령어는 일반적인 사용자 수준의 Claude 디렉토리에서 JSONL 파일을 검색하고 수정 시간과 경로를 출력합니다:
find "$HOME/.claude" -type f -name '*.jsonl' -printf '%T@ %p\n' 2>/dev/null \
| sort -nr \
| head -n 30
macOS에서 흔히 사용되는 BSD find는 -printf를 지원하지 않습니다. 다음을 사용하십시오:
find "$HOME/.claude" -type f -name '*.jsonl' -print0 2>/dev/null \
| xargs -0 stat -f '%m %N' \
| sort -nr \
...
디렉토리가 존재하지 않는 경우, 전체 파일 시스템 대신 홈 디렉토리의 제한된 영역을 검색하십시오:
find "$HOME" -maxdepth 5 -type f \\
\( -name '*.jsonl' -o -name '*.json' \) \\
2>/dev/null \\
...
증거를 바탕으로 파일을 선택하십시오:
-
수정 시간이 원하는 세션과 겹치는지;
-
프로젝트 경로(project path) 또는 작업 디렉토리(working-directory) 필드가 해당 프로젝트와 일치하는지;
-
초기 사용자 텍스트(early user text)가 기억나는 프롬프트와 일치하는지;
-
파일 전체에서 세션 식별자(session identifier)가 일관되게 유지되는지 확인하십시오.
명시적인 변수를 설정하십시오. 모든 명령에서 이를 인용하십시오:
```
SESSION_FILE="/absolute/path/to/the/selected-session.jsonl"
test -f "$SESSION_FILE" || {
...
단순히 가장 최신 파일이라는 이유만으로 파일을 선택하지 마십시오. 백그라운드 프로세스, 두 번째 터미널, 또는 나중에 시작된 짧은 세션이 더 최근의 타임스탬프를 가질 수 있습니다.
### 내용을 덤프하지 않고 구조 미리보기
먼저 필드 이름과 공통 메타데이터만 검사하십시오:
head -n 5 "$SESSION_FILE"
| jq -c '{
keys: (keys | sort),
...
만약 이 단계가 실패한다면, 파일에 잘못된 형식의 라인(malformed lines)이 포함되어 있거나, JSONL 대신 JSON 배열이 있거나, JSON이 아닌 접두사(non-JSON prefixes)가 있을 수 있습니다. 아직 소스 파일을 "수정"하지 마십시오. 아래의 보고 스크립트는 증거를 변경하지 않고 잘못된 형식의 라인을 기록합니다.
## 2단계: 감사 가능한 스냅샷(auditable snapshot) 보존
계속 변하는 소스 파일은 재현 불가능한 수치를 생성합니다. 선택한 세션의 활동을 종료하거나 일시 중지한 다음, 개인 작업 디렉토리를 생성하고 파일을 복사하십시오:
umask 077
AUDIT_DIR="$PWD/claude-session-audit"
mkdir -p "$AUDIT_DIR"
...
macOS의 경우:
shasum -a 256 "$AUDIT_DIR/session.jsonl"
| tee "$AUDIT_DIR/session.sha256"
이 시점부터는 $AUDIT_DIR/session.jsonl을 분석하십시오. 해시(hash)는 보고서를 생성하는 데 사용된 정확한 바이트를 식별합니다. 나중에 라이브 파일가 변경되더라도, 원래의 결과는 재현 가능하게 유지됩니다.
## 3단계: 빠른 마커 및 스키마 확인 실행
대소문자를 구분하는 원시 검색(raw, case-sensitive search)으로 시작하십시오:
rg -n -F -- 'ip_reminder' "$AUDIT_DIR/session.jsonl"
일치하는 물리적 라인 수를 세십시오:
rg -c -F -- 'ip_reminder' "$AUDIT_DIR/session.jsonl" || true
한 줄에 반복되는 발생을 포함하여 모든 리터럴 발생 횟수를 계산합니다:
rg -o -F -- 'ip_reminder' "$AUDIT_DIR/session.jsonl"
| wc -l
이 숫자들은 서로 다른 질문에 답합니다. 라인 수(line count)는 모든 물리적 라인이 하나의 레코드(record)일 때만 매칭되는 레코드 수를 근사치로 나타냅니다. 발생 횟수(occurrence count)는 레코드 내부의 반복된 언급을 포함하며, 따옴표로 묶이거나 이스케이프(escaped)된 과거 콘텐츠를 포함할 수 있습니다.
### 사용량 필드 후보 찾기
토큰(Tokens)은 모델과 API 과금 계층에서 사용하는 단위이며, 문자(characters)나 단어(words)와 동일하지 않습니다. 단일 스키마(schema)를 가정하기보다는 필드 이름을 검색하십시오:
rg -o --no-filename
'"[^"]*(input|output|cache)[^" ]tokens?[^" ]"'
"$AUDIT_DIR/session.jsonl"
...
일반적인 사용량 객체(usage objects)에는 input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens와 같은 이름이 포함될 수 있습니다. 다른 버전에서는 camelCase를 사용하거나 중첩된 구조(nested structures)를 사용할 수도 있습니다. 이 스크립트는 모든 소스 경로를 유지하면서 여러 일반적인 변형들을 정규화(normalize)합니다.
### 왜 하나의 jq 표현식으로 모든 것을 합산하지 않나요?
input_tokens라는 이름의 모든 숫자 필드를 재귀적으로 수집하는 명령은, 로그가 요청 래퍼(request wrapper)와 응답 이벤트(response event) 모두에 동일한 사용량 객체를 저장할 경우 동일한 객체를 중복 계산할 수 있습니다. 합계를 산출하기 전에 반드시 다음을 구분해야 합니다:
-
레코드 내 어디에서나 발견되는 가공되지 않은(raw) 사용량 객체;
-
서로 다른 경로에서 반복되는 동일한 사용량 객체;
-
안정적인 메시지 또는 요청 식별자(identifier)를 공유하는 레코드;
-
우연히 동일한 토큰 값을 가지게 된 별개의 API 호출.
## 4단계: 스키마 허용적(schema-tolerant) 로컬 보고서 생성
다음 내용을 audit_claude_session.py로 저장하십시오. 이 스크립트는 Python 표준 라이브러리만을 사용합니다. 데이터를 네트워크를 통해 전송하지 않으며, 출력물에 전체 레코드 본문을 포함하지 않습니다.
#!/usr/bin/env python3
import argparse
import csv
import hashlib
import json
import os
import re
import sys
from collections import Counter, defaultdict
from datetime import datetime, timezone
from pathlib import Path
MARKER = "ip_reminder"
TOKEN_ALIASES = {
"input_tokens": "input_tokens",
"inputTokens": "input_tokens",
"output_tokens": "output_tokens",
"outputTokens": "output_tokens",
"cache_creation_input_tokens": "cache_creation_tokens",
"cacheCreationInputTokens": "cache_creation_tokens",
"cache_creation_tokens": "cache_creation_tokens",
"cacheCreationTokens": "cache_creation_tokens",
"cache_read_input_tokens": "cache_read_tokens",
"cacheReadInputTokens": "cache_read_tokens",
"cache_read_tokens": "cache_read_tokens",
"cacheReadTokens": "cache_read_tokens",
}
ID_KEYS = (
"request_id", "requestId",
"message_id", "messageId",
"event_id", "eventId",
"uuid", "id",
)
TIME_KEYS = (
"timestamp", "created_at", "createdAt",
"time", "date",
)
TYPE_KEYS = (
"type", "kind", "event", "event_type", "eventType",
)
def sha256_file(path):
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def json_path(parts):
out = "$"
for part in parts:
if isinstance(part, int):
out += f"[{part}]"
elif re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", str(part)):
out += "." + str(part)
else:
out += "[" + json.dumps(str(part), ensure_ascii=False) + "]"
return out
def walk(value, path=()):
yield path, value
if isinstance(value, dict):
for key, child in value.items():
yield from walk(child, path + (key,))
elif isinstance(value, list):
for index, child in enumerate(value):
yield from walk(child, path + (index,))
def scalar_string(value):
if isinstance(value, (str, int, float)) and not isinstance(value, bool):
return str(value)
return None
def find_first_scalar(record, keys):
if isinstance(record, dict):
for key in keys:
if key in record:
value = scalar_string(record[key])
if value is not None:
return value
for _, value in walk(record):
if isinstance(value, dict):
for key in keys:
if key in value:
found = scalar_string(value[key])
if found is not None:
return found
return None
def marker_hits(record):
hits = []
for path, value in walk(record):
if isinstance(value, str):
count = value.count(MARKER)
if count:
hits.append({
"path": json_path(path),
"occurrences": count,
"value_length": len(value),
})
return hits
def numeric_token(value):
if isinstance(value, bool):
return None
if isinstance(value, int) and value >= 0:
return value
if isinstance(value, float) and value >= 0 and value.is_integer():
return int(value)
return None
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기