# Claude Code 테스트 로그가 너무 길 때: Context Diet는 오류를 얼마나 남길까?

> Canonical: https://vxi.me/ko/posts/claude-code-context-diet-log-compression/
> Locale: ko-kr
> Published: 2026-10-11
> Updated: 2026-10-11
> Markdown: https://vxi.me/ko/posts/claude-code-context-diet-log-compression.md

Claude Code에서 `npm test`나 `pytest`를 실행하면 수백 줄의 `PASS` 메시지가 쏟아지고 정작 중요한 실패 한 줄은 그 사이에 묻힐 때가 있다. 에이전트는 실패 지점을 찾기 위해 도구를 다시 호출하거나 로그를 재조회한다. 마지막 몇십 줄만 보는 것도 해결책은 아니다. 예외가 중간에 발생했다면 마지막 부분엔 아무것도 없을 수 있다.

[Hacker News의 Show HN](https://news.ycombinator.com/item?id=50024608)에서 [Context Diet](https://github.com/drkokorev/context-diet)를 발견했다. Claude Code의 Mod Hook으로 긴 도구 결과를 요약하고, 필요할 때 전체 로그를 다시 읽을 수 있는 파일 경로를 남기는 프로젝트다. 이런 접근은 마음에 들었다. 모델에게 무작정 줄여서 보여주는 대신, 원문으로 돌아갈 수 있는 색인을 주는 셈이니까.

다만 README에 적힌 "every failure"가 걸렸다. 문자 수 예산이 정해져 있는데 서로 다른 오류가 수십 개라면 정말 하나도 빠뜨리지 않을 수 있을까? 그래서 [v0.3.0, commit `689284a`](https://github.com/drkokorev/context-diet/tree/689284a893f1aa509b9c6c1c06b91ef1ca52279c)로 버전을 고정한 뒤 Hook과 압축기, 테스트 코드를 읽고 범용 로그 축약 함수의 핵심 부분을 JavaScript로 옮겨 인위적인 로그에 적용했다.

먼저 범위를 분명히 하자. **실제 Claude Code에 Context Diet Mod를 설치하거나 실행하지 않았다. Claude 모델 A/B 테스트도 하지 않았다.** 아래 숫자는 인공 입력에 대한 소스 기반 알고리즘 실험 결과이지, 실제 플러그인의 오류 누락률이나 토큰 절감률이 아니다.

## 모델이 보는 도구 결과를 줄이는 방식

고정 버전의 [`session.append` Hook](https://github.com/drkokorev/context-diet/blob/689284a893f1aa509b9c6c1c06b91ef1ca52279c/plugins/context-diet/hooks/register.tsx)은 일정 길이를 넘는 Bash, Grep, Glob, JSON 형태 MCP 결과 등을 대상으로 한다. Read, Edit, Write, WebFetch와 JSON이 아닌 MCP 설명문은 이 축약 경로에 포함되지 않는다. 기본 트리거 임계값은 **8,000자**다. 8,000 토큰이 아니다.

무조건 `tail`처럼 끝부분만 남기지도 않는다. [도구별 파서](https://github.com/drkokorev/context-diet/blob/689284a893f1aa509b9c6c1c06b91ef1ca52279c/plugins/context-diet/hooks/tools.ts)는 Jest, Vitest, pytest, Go test, Cargo, TypeScript 검사기 출력을 식별하여 성공한 테스트보다 오류 위치와 주변 문맥을 우선한다. JSON은 뼈대만 남기고 Diff, Grep, 파일 목록도 서로 다른 방식으로 줄인다. 이 내용은 **소스에서 확인한 처리 전략**이며, 내가 실제 Claude Code에서 각 프레임워크를 실행한 호환성 결과는 아니다.

더 중요한 건 원본을 되찾는 방식이다. Hook은 요약으로 대체하기 전에 전체 결과를 `.context-diet/out-NNN.txt`에 저장하려고 한다. 파일 이름은 **300개 슬롯**을 순환한다. 저장에 실패하면 존재하지 않는 경로를 만들어 알려주는 대신 원본 출력을 그대로 통과시킨다.

하지만 디스크에 저장됐다는 것과 영구 보관은 다르다. 이전 슬롯은 재사용되고, 로그 안의 인증 정보·환경 변수·프로젝트 경로가 파일에 남을 수 있다. `.gitignore`는 Git에 실수로 추가할 위험을 줄일 뿐, 암호화나 백업을 보장하지 않는다. 운영 사고에 중요한 로그라면 별도 보관이 필요하다.

## 오류를 먼저 고른다고 모든 오류가 보이는 것은 아니다

[범용 로그를 처리하는 `diet.ts`](https://github.com/drkokorev/context-diet/blob/689284a893f1aa509b9c6c1c06b91ef1ca52279c/plugins/context-diet/hooks/diet.ts)를 살펴봤다. 반복되는 줄을 묶고, 앞뒤 출력에 일정한 문자 예산을 할당하고, 뒤쪽 오류부터 찾아 주변 줄과 함께 보존하려고 한다.

결정적인 부분은 다음 조건문이다.

```ts
const cost = window.reduce((n, i) => n + (folded[i]?.text.length ?? 0) + 1, 0)
if (used + cost > target) continue
```

오류 구간이 남은 예산에 들어가지 않으면 그 구간을 건너뛴다. 오류가 한 건이면 꽤 효율적일 수 있지만, 서로 다른 실패가 여러 개 나오고 스택 트레이스까지 길면 일부 오류는 요약에 들어가지 못한다.

Jest/pytest처럼 식별 가능한 프레임워크 출력은 별도 파서를 사용하며 그쪽도 길이 제한이 있다. **아래 실험은 범용 로그의 fallback 경로를 대상으로 한다. Jest나 pytest 전용 파서에서 오류가 얼마나 남는지는 시험하지 않았다.** 145개 합성 오류의 비율을 실제 Jest 오류 누락률로 해석하면 안 된다.

README의 "every failure"를 기술적으로 해석한다면 "실패를 우선 보여주고 전체 원문으로 돌아갈 수 있게 한다"에 가깝다. "어떤 입력에서도 모든 오류 줄을 유한한 길이의 요약에 보여준다"는 보증으로 볼 수 없다.

## 오류 하나와 서로 다른 오류 145개

이 문자 예산 문제를 확인하려고 Claude Code를 새로 구성하지는 않았다. 고정된 원본에서 범용 `dietLog`와 텍스트 헬퍼를 수동으로 JavaScript에 옮겨, 외부 의존성 없는 Node.js v22.16.0 격리 환경에서 돌렸다. 상대적으로 완전한 합성 테스트는 **4/4 통과, 종료 코드 0**이다. 공식 Mod 통합 테스트와는 별개다.

| 인공으로 만든 입력 | 원본 길이 | 요약 길이 | 관찰 |
| --- | ---: | ---: | --- |
| 긴 로그 중앙에 핵심 오류 하나 | 32,087자 | 1,175자 | 오류 표식 1/1개 보임 |
| 서로 다른 오류 145줄, 예산 1,450자 | 23,146자 | 1,419자 | 원본 오류 줄 7/145개만 보임 |
| 짧은 출력 | 45자 | 45자 | 변경 없음 |
| `PASS errorHandler.test.ts` 분류 대조 | 52자 | 52자 | PASS를 오류로 오판하지 않음 |

두 번째 합성 사례에서는 원본 오류 **138줄이 요약에 나타나지 않았다**. 다음은 실제로 저장한 *인공 로그 요약*의 일부다. 실제 운영 프로젝트에서 발생한 오류가 아니다.

```text
… [225 lines cut] …
ERROR CASEei unhandled category_ts in /app/modules/amy/workflow.ts:40 expected stable result bci
ERROR CASEej unhandled category_tt in /app/modules/amz/workflow.ts:40 expected stable result bcj
ERROR CASEek unhandled category_tu in /app/modules/ana/workflow.ts:40 expected stable result bck
…
… [79 lines cut] …
```

여기서 한 가지 계산상의 함정이 있었다. 압축기가 남긴 설명은 "5 of 145 error lines with context"였지만, 최종 요약에서 보이는 원본 오류 줄을 직접 세면 **7개**였다. 앞부분 또는 뒷부분에 남겨둔 출력에도 오류가 포함될 수 있기 때문이다. "선택된 오류 문맥 묶음 수"와 "최종적으로 눈에 보이는 오류 줄 수"는 같은 지표가 아니다.

**실험의 1,450자 예산은 의도적으로 제한한 값이지 플러그인의 기본값이 아니다.** 고정된 v0.3.0에서 압축 트리거는 기본 **8,000자**, 일반 긴 출력의 요약 목표는 `Math.max(2_000, Math.round(threshold / 2))`에 따라 **4,000자**다. Claude Code가 이미 파일에 저장한 출력은 **3,000자** 목표를 사용한다. 예산이 커지면 더 많은 오류를 담을 수 있지만 합성 실험만으로 실제 환경의 오류 누락률을 산출할 수는 없다.

독자가 직접 확인할 수 있게 문자 예산 선택 부분만 재현한 **2개짜리 Node 표준 라이브러리 테스트**도 실행했다. 이 작은 대조군에서는 합성 오류 줄이 좀 더 길어서 **8/145줄**이 남았고 **2/2 통과**했다. 앞선 7/145 사례와 입력·구현 범위가 달라 성능 비교처럼 섞어서는 안 된다.

아래는 검증한 최소 재현 코드 전체다. `budget-replay.test.mjs`로 저장한 다음 `node --test budget-replay.test.mjs`를 실행하면 된다.

```js
/**
 * First-party negative control for Context Diet v0.3.0's generic dietLog budget path.
 * Pin: drkokorev/context-diet @ 689284a893f1aa509b9c6c1c06b91ef1ca52279c.
 * Only the error-window budget logic is mirrored, for controlled ASCII log lines.
 * This is NOT an upstream Mod invocation, a benchmark, or a production loss rate.
 * The full adapted dietLog fixture and observed digests are in the separate evidence bundle.
 */
import assert from 'node:assert/strict';
import test from 'node:test';

const id = i => { let s='',n=i+1; while(n){ n--;s=String.fromCharCode(97+n%26)+s;n=Math.floor(n/26); } return s; };
const noise = n => Array.from({length:n},(_,i)=>'step '+id(i)+' completed '+id(i+1200)+' '+i);
const critical = line => /^ERROR\b|^\s+(Expected|Received):/.test(line);
function select(raw,target){
  const folded=raw.split('\n').map(text=>({text,level:critical(text)?2:0}));
  const errorHits=folded.map((l,i)=>l.level===2?i:-1).filter(i=>i>=0);
  const keep=new Set();let used=0;
  const headBudget=target*(errorHits.length?0.1:0.2);
  const tailBudget=target*(errorHits.length?0.25:0.35);
  for(let i=0;i<folded.length&&used<headBudget;i++){keep.add(i);used+=folded[i].text.length+1;}
  let tailUsed=0;
  for(let i=folded.length-1;i>=0&&tailUsed<tailBudget;i--){
    if(!keep.has(i)){keep.add(i);tailUsed+=folded[i].text.length+1;}
  }
  used+=tailUsed;
  let windowsShown=0;
  for(let h=errorHits.length-1;h>=0;h--){
    const at=errorHits[h];const window=[];
    for(let i=at-2;i<=at+3;i++)if(i>=0&&i<folded.length&&!keep.has(i))window.push(i);
    const cost=window.reduce((n,i)=>n+folded[i].text.length+1,0);
    if(used+cost>target)continue; // EXACT budget guard from upstream dietLog.take()
    window.forEach(i=>keep.add(i));used+=cost;windowsShown++;
  }
  return {visible:folded.filter((_,i)=>keep.has(i)).map(l=>l.text).join('\n'),totalCritical:errorHits.length,windowsShown};
}
test('single late/central failure remains visible',()=>{
  const text=[...noise(310),'ERROR ECASEONE impossible transaction transition','  Expected: completed','  Received: pending',...noise(310)].join('\n');
  const out=select(text,2000);
  assert.ok(out.visible.includes('ECASEONE'));
});
test('many distinct failures cannot all fit finite digest budget',()=>{
  const failures=Array.from({length:145},(_,i)=>'ERROR CASE'+id(i)+' unhandled category_'+id(i+400)+' in /app/modules/'+id(i+900)+'/workflow.ts:40 expected stable result '+id(i+1300));
  const raw=[...noise(90),...failures,...noise(90)].join('\n');
  const out=select(raw,1450);
  const retained=failures.filter(line=>out.visible.includes(line)).length;
  assert.ok(retained>0&&retained<failures.length);
  process.stdout.write(JSON.stringify({failure_lines:failures.length,retained,missing:failures.length-retained,digest_budget_chars:1450,mode:'source-budget-window-control-not-plugin-runtime'})+'\n');
});
```

이 코드는 범용 로그의 문자 예산 선택 로직을 단순 재현한다. Claude Code Mod API를 호출하지 않는다. 테스트가 통과했다고 해서 실제 Agent의 문제 해결이나 외부 플러그인 동작까지 확인한 건 아니다.

내가 얻은 결론은 압축을 버리자는 게 아니다. **요약은 원본 증거를 찾아가기 위한 색인이지, 원본 증거 자체가 아니다.** 이 구분을 놓치지 않아야 한다.

## dry부터 확인하고 capture는 별도로 판단하자

Mod를 지원하는 Claude Code 환경이라면 [고정 버전의 README](https://github.com/drkokorev/context-diet/blob/689284a893f1aa509b9c6c1c06b91ef1ca52279c/README.md)와 현재 클라이언트 문서를 함께 확인하길 권한다. 당시 요구 버전은 Claude Code **2.1.287+**다. 나는 설치 과정을 실사용 환경에서 검증하지 않았다.

```text
/plugin marketplace add drkokorev/context-diet
/plugin install context-diet@context-diet
```

시작은 `/diet dry`와 `/diet report`가 적절하다. dry 모드는 원문을 바꾸지 않은 채 얼마나 잘릴지 계산하는 용도다. 반드시 모든 로그를 읽어야 하는 명령은 `/diet keep <명령 문자열 일부>`로 제외하거나 `/diet off`로 기능을 끌 수 있다.

`/diet capture on`은 다른 문제를 다룬다. Claude Code 자체가 지나치게 긴 결과를 먼저 잘라 버릴 때, 특정 단일 테스트·빌드 Bash 명령을 감싸서 stdout과 stderr를 `.context-diet/run-NN.log`에 함께 기록하고 다시 출력하며 종료 코드를 보존하려고 한다. **기본값은 꺼짐**이다.

명령을 감싸는 과정에서 `Bash(pytest:*)` 같은 기존 권한 허용 규칙과 일치하지 않아 추가 확인이 나올 수 있다. 두 스트림을 하나로 합치면 로그의 보이는 순서도 달라질 수 있다. 파이프, 여러 명령, 리다이렉션, watch/백그라운드 실행은 자동 capture 대상이 아니다. 권한 관리나 실시간 출력을 중시하는 환경이라면 무작정 켜지 않는 편이 좋다. 되돌릴 때는 `/diet capture off`를 사용한다.

[원저자가 공개한 결과](https://github.com/drkokorev/context-diet/blob/689284a893f1aa509b9c6c1c06b91ef1ca52279c/README.md)에 따르면 7개 Q&A 과제는 두 조건 모두 정답, 총 도구 호출은 **13회에서 8회**로 줄었다. 별도의 **18회 Agent 수정 실행**에서는 평균 호출이 미사용 **8.0**, 사용 **6.0**, capture 포함 **5.8**로 보고됐다. 이는 원저자의 소규모 집계이며 내가 모델 실행 로그나 비용을 독립적으로 재검증한 결과가 아니다.

테스트 통과 로그가 컨텍스트를 계속 차지한다면 Context Diet를 평가해 볼 만하다. 반면 사고 분석, 복잡한 동시 오류, 자동 PR 병합에서는 요약만으로 실패 목록이 완전하다고 판단하면 안 된다. Agent가 실제로 전체 파일을 다시 읽는지, 몇 개 보이는 실패만 고쳐서 PR을 만드는지는 따로 연구할 행동 문제다.

이전에 정리한 [CLAUDE.md / AGENTS.md / Skills 지침 분리](https://aitokenhacks.com/posts/claude-md-agents-md-skills-ci/)와 [Agent 상태 인계](https://aitokenhacks.com/posts/ai-agent-orchestration-after-assistants/)는 무엇을 로드하고 누가 맡는지를 다뤘다. 이번 문제는 더 작고 직접적이다. 모델이 보고 있는 도구 결과가 완전한 증거인가, 아니면 증거로 돌아가는 경로가 적힌 메모인가?
