Claude Code·Codex 세션 로그 읽을 때의 4가지 함정

Claude Code는 대화형 요청이 끝날 때 세션 로그에 turn_duration 줄을 씁니다. claude -p는 그렇지 않습니다. 저는 2026년 9월 27일의 실제 로그(Claude Code 2.1.283)에서 이를 측정했으며, 이것은 이 파일들을 읽는 방식이 첫인상과 달라지는 네 군데 중 하나입니다.
이 글은 지난 글의 기술적인 후속편입니다. 그 글에서 저는 혼자 만든 데스크톱 앱 Refmade가 AI 세션을 사무실처럼 그려 주는데, 에이전트 보고서를 믿는 대신 로컬 로그를 읽어야 한다고 결정했습니다. Refmade는 독립적인 프로젝트이며 Anthropic이나 OpenAI와는 관련이 없습니다. Claude Code와 Codex 에이전트가 제 지시에 따라 리더의 대부분을 작성했고, 아래 규칙들은 제 실제 로그를 기준으로 조정했습니다. 이것들은 버전이 올라가면 바뀔 수 있는 내부 로그 형식이므로, 이 내용은 무엇이든 깨질 수 있습니다.
디스크에 무엇이 있는가
Claude Code의 경우 하나의 주요 세션은 ~/.claude/projects/ / .jsonl 같은 파일입니다. 하위 에이전트는 /subagents/ 아래에 있으며, 각각 agent- .jsonl 파일과 그 옆의 작은 agent- .meta.json 파일을 가집니다. 메타 파일에는 에이전트 유형, 한 줄 설명, parentAgentId, workflowPhase, 그리고 fork 여부가 들어 있습니다. 워크플로 실행은 실행마다 journal.jsonl을 추가합니다. Codex는 자체 세션 폴더 아래에 rollout-*.jsonl을 쓰며, 하위 에이전트는 parent_thread_id를 통해 부모를 가리킵니다.
리더는 각 파일을 마지막으로 읽은 지점부터 이어서 읽고, 완전한 줄만 처리하며, 파싱하기 전에 바이트 기준 검색어로 줄을 고릅니다. 도구 결과를 담는 큰 사용자 줄은 트랜스크립트 바이트의 대부분을 차지하므로 아예 파싱하지 않습니다. 대신 타임스탬프와 tool_use_id만 잘라 냅니다. 프롬프트, 답변, 도구 인자, 경로는 규칙을 위해 메모리에서만 살펴보고 보관하지 않습니다. 보관하는 것은 다음과 같습니다: 시간, 각 도구 이름에 대한 행위, 에이전트 유형과 한 줄 라벨, 세션 제목과 폴더 이름, 그리고 끝부분에 설명되는 한 문장짜리 결과이며, 모두 로컬 페이지용입니다.
함정 1: 대부분의 "user" 줄은 요청이 아니다
요청은 user 레코드에서 시작하지만, 도구 결과도 user 레코드이고, 압축 요약, 작업 알림, 인터럽트 표식, 그리고 ! 셸 명령의 출력도 마찬가지입니다. 제가 정리한 시작 규칙은 다음과 같습니다: 메타가 아님, 사이드체인이 아님, 도구 결과가 아님, 자동 압축이 쓴 요약이 아님, 그리고 짧은 접두사 목록 중 하나로 시작하지 않음 (<task-notification, <local-command, <system-reminder, [Request interrupted 등).
슬래시 명령과 ! 명령은 어시스턴트가 답할 때까지 보류됩니다. 로컬 명령인 /model은 답변을 받지 못하므로 요청이 되지 않습니다. 일반적으로 입력한 프롬프트는 보류되지 않습니다. 실제 로그에서는 첫 답변이 중앙값 7초 뒤에 도착해, 사무실을 눈먼 상태로 두기에는 너무 길었습니다.
요청이 열려 있지 않은 상태에서 백그라운드 작업이 끝나면 자동 턴이 시작됩니다. 팀은 작업 중으로 보이지만, 제가 요청하지 않았기 때문에 그 턴은 번호가 매겨지지 않고 카드도 생기지 않습니다.
함정 2: fork는 부모의 이력으로 시작한다
fork의 트랜스크립트는 부모 대화의 복사본으로 시작합니다. 이를 순진하게 읽으면 fork가 부모가 했던 모든 일을 부모의 타임스탬프로 한 것처럼 보입니다. 메타 파일에는 그것을 생성한 호출의 toolUseId가 들어 있으므로, 리더는 복사본 안에서 그 호출을 찾아 그 뒤부터 카운트를 시작합니다.
// excerpt: cli/lib/office/claude-source.mjs
const needle = Buffer.from(`"id":"${agent.forkTool}"`);
let found = false;
scanFdLines(fd, [needle], () => { found = true; }, {start: 0, completeOnly: true});
agent.copying = found ? needle : null;이 검사는 생성 호출이 실제로 있을 때만 건너뜁니다. 복사본이 없는 fork에는 건너뛸 것도 없기 때문입니다. 이것이 없으면 팀원들이 두 번씩 집계됩니다.
함정 3: 요청은 어디에서 끝나는가?
다음은 신뢰도가 높은 것부터 낮은 것 순서의 규칙입니다.
| 세션 종류 | 요청이 끝나는 방식 |
|---|---|
| 대화형 | 다음 system turn_duration 줄. 그것이 없으면 [Request interrupted 마커. 그것도 없으면, 다음 요청의 시작 시점에서 그 직전의 마지막 답변을 사용함. |
| claude -p (entrypoint sdk-cli ) | turn_duration 없음, 그리고 사용자가 Stop hooks를 가진 경우에만 Stop-hook 요약이 있음. 따라서 리드의 마지막 assistant 레코드(end_turn, 도구 호출 없음) 또는 성공한 StructuredOutput 결과입니다. 이후에 리드 레코드가 다시 나타나면 요청이 다시 열린 것이므로 잠정적입니다. |
| 아무것도 끝내지 않음 | 30분 동안 조용하면 닫힘. 리드의 마지막 행위가 질문이라면 6시간 후에 닫힘. |
claude -p 분기가 존재하는 이유는, 그렇지 않으면 일회성 실행이 30분 유휴 규칙이 닫을 때까지 열린 채로 남아 있기 때문입니다. 스키마 실행은 성공한 StructuredOutput 호출에서 끝나며, 워크플로 에이전트도 같은 방식으로 끝납니다. 그래서 끝이 잠정적인 것입니다. 차단적인 Stop hook이나 StructuredOutput 알림은 턴을 계속 진행하게 만들 수 있고, 끝났다고 생각한 뒤에 기록이 하나 더 나오면 요청이 다시 열립니다.
서브 에이전트는 각자 고유한 규칙을 따릅니다. 에이전트는 assistant stop에서 끝나거나, 성공한 StructuredOutput 결과에서 끝납니다. 10분 동안 기록이 없으면 마지막 기록이 종료 시점입니다. 정지 후 30초보다 늦게 나온 기록은 새 방문, 새 오피스 에이전트로 간주되므로, 유휴 간격은 작업으로 그려지지 않습니다.
거부된 호출은 작은 쪽입니다. 승인 카드에서 도구 호출을 거부하면, CLI는 제 거부를 오류 결과로 기록합니다. 리더는 다음 기록이 나올 때까지 "파일을 편집 중"이라고 보여 주지 않고, 그 호출의 동작을 즉시 끝냅니다.
함정 4: 워크플로 저널에는 시계가 없다
워크플로 실행의 journal.jsonl에는 {"type":"launched", "started", "result" 또는 "failed"로 시작하는 줄이 있습니다. 파싱되는 것은 launched와 started뿐입니다. result 줄에는 에이전트의 전체 출력이 들어 있으므로, 에이전트가 끝났다는 신호로 맨 앞의 agentId만 잘라 냅니다. 저널에는 타임스탬프가 없습니다. 실행은 저널 파일 생성 시점(macOS와 Windows에서는 실제 생성 시점) 또는 첫 에이전트가 시작된 시점 중 더 이른 때에 시작됩니다.
실행이 끝나면 세션 폴더의 workflows/ .json 파일에서 이름, 단계 제목, 지속 시간을 알 수 있습니다. 실행 중에는 계획된 이름과 단계가 메인 트랜스크립트의 Workflow 호출 안에 있는 export const meta = {...} 리터럴에서 나오며, 처음 16 KB를 스캔해 최대 4 KB까지만 읽습니다. 스크립트는 보관되지 않습니다.
도구 이름만으로 판단하는 아홉 가지 작업
관찰된 모든 단계는 도구 이름에 따라 아홉 가지 동작 중 하나가 됩니다.
| 작업 | Claude Code 도구(예시) |
|---|---|
| read | Read, Grep, Glob, Skill, ToolSearch |
| write | Edit, Write, MultiEdit, design 및 media-generator MCP 도구 |
| run | Bash, Monitor, 알 수 없는 모든 도구 |
| web | WebSearch, WebFetch, browser 및 computer control |
| delegate | Agent, Task, Workflow |
| talk | SendMessage |
| ask | AskUserQuestion |
| plan | TaskCreate, TodoWrite, plan mode |
| think | 도구 없음, 텍스트만 있음 |
알 수 없는 도구는 run으로 처리됩니다. Codex 도구는 별도의 표에서 같은 아홉 범주에 매핑되며, exec 호출은 입력 안에서 알려진 도구 식별자를 찾아 분류합니다. 이 검색은 처음 64 KB에서 수행되며 저장되지는 않습니다.
무엇이 잘못되는가
완료된 요청이 곧 성공한 요청은 아닙니다. 오피스는 Claude Code가 턴을 끝낼 때 카드를 한 장 그릴 뿐, 그 이상은 아닙니다. ask 상태의 팀은 리더의 마지막 동작이 질문이었다는 뜻일 뿐입니다. 앱의 작업 목록도 같은 내용을 평이한 말로 보여 주며, 아래 데모 작업이 바로 내 답변을 기다리고 있습니다.

앱 작업 목록의 데모 데이터입니다. 한국어 문구는 "응답을 기다리는 중"과 "Codex가 제 답변을 기다리고 있습니다"이며, 단계 표시는 "둘러보기 > 빌드 및 수정 > 제 답변을 기다리는 중 > 완료"입니다. "고객 문의 답변 정렬" 작업은 가짜입니다.
완료된 요청마다 한 문장짜리 result도 붙는데, 이는 리더의 마지막 답변에서 가져오며 이 컴퓨터의 페이지에만 표시됩니다. 답변이 한국어일 때만 유지되고, 그렇지 않으면 페이지가 자체 문장을 씁니다. AI가 생성한 식별자가 때때로 그 문장에 섞여 들어갑니다. 그리고 claude -p 또는 codex exec 실행은 충분히 짧아서, 오피스는 그 뒤를 휴식 중인 팀으로 표시합니다.
다음 글에서는 빌드 과정을 다룹니다. 하나의 계약 문서, 병렬로 작업하는 다섯 빌더, 그리고 하나의 통합기입니다. 앱 링크는 시리즈의 마지막 글에 나옵니다.
이 파일들을 직접 파싱한다면, 네 가지 함정 중 어느 것에 가장 먼저 걸렸을까요? 그리고 제가 처리해야 할 다섯 번째 함정을 찾으셨나요?
출처
- 원문: https://dev.to/ji_ai/reading-claude-code-and-codex-session-logs-four-traps-3f57 (2026-10-18 · jidonglab)