
Claude Code의 compact 문제점과 대책
요약
Claude Code의 context 압축(compact) 기능이 작업 지시와 로그를 구분하지 못해 발생하는 문제점과 이를 해결하기 위한 플러그인 개발 내용을 다룹니다. 압축 과정에서 판단 근거가 누락되어 발생하는 오작동 사례를 분석합니다.
핵심 포인트
- Claude Code의 compact는 대화 이력을 요약하여 context를 재구축함
- 요약 과정에서 '왜 기각했는가'와 같은 판단 구조가 희석되는 문제 발생
- 압축 후 에이전트가 작업 지시와 로그를 혼동하여 잘못된 동작 수행 가능
- 검증 전 배포, 기각된 방식 재실행, 설계 원칙 망각 등의 사고 사례 관측
이 기사는 X의 원문을 Zenn용으로 재게재한 것입니다.
추후, 다음과 같은 내용 + α를 자동으로 삽입하여 투명하게 compact를 강화할 수 있는 플러그인을 만들었습니다.
애초에 Claude Code의 compact는 어떻게 작동하는가
이야기의 전제로, Claude Code의 compact는 다음 두 가지 경로로 실행됩니다:
수동 compact (/compact): 사용자가 명시적으로 호출합니다. 원하는 타이밍에 실행할 수 있습니다.
자동 compact: 세션의 context 사용률이 상한선 근처(실운용 시 대략 90~95% 부근)에 도달하면, Claude Code 측의 판단으로 자동 실행됩니다. 사용자가 의도한 순간에 실행되는 것이 아니라, 턴(turn)의 구분점에서 "어느샌가 압축되어 있었다"는 형태가 됩니다.
어느 경로든 내용은 동일하며, 그때까지의 대화 이력을 LLM에 던져 자연문 요약을 만들게 하고, 요약 + 직전 턴 + system prompt를 새로운 context로 재구축합니다. 요약은 "무엇을 했는가"에 대한 이야기 기반의 기술이 되며, "왜 그 선택을 했는가", "어떤 안을 기각했는가", "현재 어느 단계인가"와 같은 판단 구조는 결과적으로 희석됩니다. 게다가 한 번 압축이 실행되면 압축 전의 raw 로그로 돌아갈 수 없는 비가역적인 작업이라는 점도 큽니다. hook 관점에서 보면, 압축은 PostCompact 이벤트로 포착할 수 있습니다. 다만 PostCompact hook은 additionalContext를 반환할 수 없는 사양이라, 압축 직후의 에이전트에게 지시를 직접 주입하는 경로가 없습니다. 지시를 끼워 넣으려면 다음 UserPromptSubmit 이벤트를 경유할 수밖에 없습니다. 이 제약이 본 기사의 hook 설계에 영향을 미칩니다.
표준 compact의 무엇이 문제인가
Claude Code의 /compact는 대화 이력을 LLM에게 요약하게 하는 메커니즘으로, 요약 결과를 새 세션의 초기 context로 설정합니다. 요약 자체는 타당하지만, 요약은 "과거의 작업 기록"이지 "다음에 무엇을 해야 하는가"라는 지시가 아닙니다. 결과적으로 압축 후의 에이전트는 다음과 같은 오인식을 일으키기 쉽습니다:
"작업 지시"와 "작업 로그"의 경계를 상실합니다. 압축 후의 요약에 "안 A를 검토했다"라고 남아 있으면, 거기서 채택된 안 B가 아니라 안 A를 구현하기 시작합니다.
검증 단계와 구현 단계를 혼동합니다. "검증한 뒤에 배포한다"라고 결정했던 전제가 요약에서 누락되어, 다음 턴에서 갑자기 배포에 해당하는 파괴적인 조작에 들어갑니다.
압축 전에 한 번 폐기했던 잘못된 접근 방식을 재제안합니다. 요약은 "시도했다"는 사실은 남기더라도 "왜 기각했는가"에 대한 근거까지는 가져가지 않습니다.
plan mode / worker 위임 / task tree와 같은 "세션 상태"를 상실합니다. plan mode를 벗어난 상태로 작업을 계속하거나, tmux로 병행 중인 worker의 존재를 잊고 스스로 작업을 시작하기도 합니다.
실제로 어떤 사고가 발생하고 있었는가
최근 1주일간의 세션을 되돌아보니, compact 직후에 적어도 다음과 같은 4가지 패턴이 관측되었습니다. 모두 "작업 지시"와 "작업 로그"의 분리 실패라는 하나의 뿌리에서 기인한 것으로 보입니다:
검증 전 배포: 압축 전에 "실기에서 동작을 확인한 뒤 배치한다"라고 합의했음에도, 압축 후에는 요약에 남은 완성 이미지(image)만 보고 검증을 건너뛴 채 배치 위치를 덮어씌웠습니다.
기각된 지름길의 재실행: 압축 전에 한 번 실패하여 철회했던 지름길식 접근 방식을, 압축 후의 요약이 "시도한 절차"로 남겨두고 있기 때문에 그대로 재실행하여 동일한 실패를 반복합니다.
설계 원칙 망각: "배치 위치를 직접 편집하지 않고, 관리처인 repository를 편집하여 배포한다"와 같이 세션 중에 확립한 원칙이 요약에서 빠져나가, 압축 후에는 배치 위치를 직접 건드리는 "단기적으로는 동작하지만 재현할 수 없는" 변경을 수행합니다.
태스크 목적 오인: 압축 전에 고정해 두었던 테스트 목적(예: sandbox 외 실행 시의 승인 판단 검증)이 요약에서 누락되어, 압축 후에는 관련 주변 조사로 탈선하고 최종적으로 태스크와는 별개의 라인의 작업까지 시작합니다.
이러한 현상은 동일 세션 내에서 여러 번 발생했으며, compact를 할 때마다 종류가 다른 오인식이 일어나고 있었습니다. 단발적인 우연한 사고가 아니라, 표준 compact의 사양에 대해 구조적으로 발생하는 실패라고 보고 있습니다.
만든 것
(1) compact-prep skill
/compact를 입력하기 전에 user가 명시적으로 실행하는 slash command로서 구현한 skill. 현재 세션에서 압축 요약에 다 담기지 않을 것으로 예상되는 「판단 구조(judgment structure)」와 「세션 상태(session state)」만을 추출하여, 고정 경로의 state file (${TMPDIR}/claude-compact-state/<session_id>.md)에 정해진 포맷으로 저장한다. 저장하는 항목은 다음과 같으며, 요약에 담기 어려운 정보를 우선시한다:
- Active Plan (plan file의 경로와 현재 페이즈)
- TaskList Summary (진행 중인(in-progress) 태스크와 보충 설명)
- Session Decisions (채택한 안 / 기각한 안 / 기각한 이유)
- Constraints and Blockers
- Worker Topology (tmux-bridge의 pane / role / 담당)
- Editing Files (저장되지 않았거나 검증되지 않은 주의사항)
- Recovery Notes (압축 후의 자신에게 쓰는 편지)
설계의 핵심은 「기계적으로 강제하는 것」이다. session_id를 가져올 수 없으면 추측된 이름으로 file을 만들지 못하게 하는 Hard gate, 헤더 순서를 고정하여 작성을 마친 후 다시 읽어보며 누락을 감지하는 Forcing function, 부작용을 제한하는 allowed-tools 등 세 가지를 통해, 「썼다고 생각했지만」 실제로는 state가 망가지는 경로를 차단하고 있다. 실제 구현은 끝부분에 둔다.
(2) PostCompact + UserPromptSubmit 2단계 hook
(1)에서 state file을 작성하더라도, 압축 후의 에이전트가 이를 읽지 않으면 의미가 없다. 이 부분이 까다로운데, Claude Code의 PostCompact hook는 additionalContext를 반환할 수 없는 사양이라 압축 직후의 에이전트에게 지시를 직접 주입할 경로가 없다. 따라서 2단계 구성으로 만든다:
- PostCompact hook가 session_id로 marker file을 작성한다 (지시는 넣지 않고, 압축이 발생했다는 사실만 기록한다).
- 다음 UserPromptSubmit hook가 marker를 감지하면, additionalContext를 통해 「plan file을 Read 해라 / state file을 Read 해라 / TaskList를 확인해라 / 압축 요약의 next step은 가설로 취급해라 / plan mode가 해제되어 있다면 재진입을 확인하라」를 주입하고, marker를 삭제한다 (one-shot).
Claude Code에는 hook 간에 state를 공유하는 공통 메커니즘이 없으므로, file system 상의 marker가 유일한 통신로가 된다. 각각의 hook은 「단일 책임 + marker의 읽기/쓰기」만을 갖는 매우 얇은 분업 구조다. 압축하지 않는 일반적인 턴에서는 UserPromptSubmit hook가 test -f 한 번으로 즉시 exit 하므로 실질적인 비용은 0이다. 전체를 fail-open (항상 exit 0)으로 설계했기에 hook이 고장 나더라도 Claude Code 본체는 멈추지 않는다. 이를 통해 압축 직후의 첫 번째 턴부터 「나는 plan의 Phase 3 도중이며, worker A에게 X를 위임했다」라는 상태로 복구된다. 실제 구현은 끝부분에 둔다.
(3) 60% 알림 (자동 compact를 회피하기 위해)
지금까지의 (1) skill + (2) 2단계 hook는 「수동 /compact 전후에 무언가를 수행한다」는 설계로, user 스스로가 /compact-prep → /compact 순으로 실행하는 것을 전제로 한다. 하지만 Claude Code의 자동 compact는 예고 없이 실행되므로 이 경로를 사용할 수 없다. 자동 compact가 먼저 실행되어 버리면 state file이 저장되지 않은 채 raw 로그가 요약되어 버리고, 이후의 복구 재료가 사라지게 된다. 대책은 「자동 compact가 실행되기 전에, user 스스로 수동으로 /compact-prep → /compact를 실행할 수 있는 상태를 만드는 것」이다. 이것이 60% 알림의 목적이다. 왜 60%인가?
자동 compact 발화점으로부터 충분한 여유를 두기 위해: 자동 compact는 대략 90~95% 부근에서 발화한다. 알림이 발화점에 너무 가깝게 발생하면, user가 해당 발화 턴에서 인지하더라도 다음 작업 턴에서 자동 compact가 먼저 실행될 가능성이 남는다. 안전하게 30%의 마진을 확보하고 있다.
구분 지점까지 작업을 진행할 여유를 남김: 60%에서 알림이 온 시점에 아직 30% 분량의 작업 여력이 있다. 어중간한 상태에서 즉시 /compact로 넘어가지 않고, 적절한 구분 지점까지 진행한 후 state file을 작성할 수 있다.
구분이 '주관'이 아닌 '타이머'로 이루어짐: 집중해서 작업하다 보면 context 소비를 인지하지 못한다. 60%에서 기계적으로 개입하는 forcing function으로서, hook이 능동적으로 지시를 주입한다.
부수적 효과: 60% 시점에서 "현재 어느 페이즈에 있는지 / 무엇을 남겨두었는지"를 정리하게 되므로, 구분 지점을 판단하는 것 자체에도 효과가 있다.
1M context 전제임: 60%에서 "아직 충분히 작업할 수 있는" 상태를 성립시키려면 절대적인 토큰(token) 양이 필요하다. 표준인 200K context라면 60%는 120K token이므로, 알림이 온 시점에 남은 프레임이 너무 적어 즉시 compact를 할 수밖에 없다. 본인은 Opus 4.7 (4.6이 아닌 4.7)의 1M context를 활성화하여 사용하고 있으며, 60% = 약 600K token 분량의 여유가 있다. 이 정도 프레임이 있으면 알림을 받은 후 구분 지점까지 작업을 계속하더라도 여유 있게 /compact-prep → /compact로 이어갈 수 있다. 60%라는 숫자는 1M context를 전제로 할 때 비로소 의미를 갖는 설정이며, 200K context인 상태라면 임계값(threshold)을 80%대까지 올려야 답답하지 않다. 구현의 분업: 구현은 2개 파트로 나뉜다. 기존의 statusline hook (매 턴 statusline 업데이트 시 context 사용률을 계산함)에 임계값 초과 시 warn marker를 작성하는 분기를 추가하고, UserPromptSubmit hook 측에서 warn marker를 감지하면 "/compact-prep를 제안하라"를 주입한다. 사용 중인 marker는 3종류다:
claude-compact-warn: "이제부터 알림을 하고 싶다"는warn marker.statusline이 작성하고UserPromptSubmit hook이 읽어서 삭제한다.claude-compact-warned: "이미 알림 완료"된cooldown marker.UserPromptSubmit hook이 작성하고PostCompact hook이 삭제한다. 이를 통해 중복 알림을 방지한다.claude-compacted: (2)에서 소개한 "압축 직후"marker.PostCompact hook이 작성하고UserPromptSubmit hook(recovery 측)이 읽어서 삭제한다.
statusline은 "사용률"만 알고, UserPromptSubmit hook은 "알림 여부"만 알고, PostCompact hook은 "압축이 일어났는지"만 안다. 각각의 책임이 단일하며, 상태는 marker의 존재 여부만으로 표현된다. 실제 구현은 말미의 참고 C에 둔다. 사용자 측의 모습으로는, 60%를 초과한 턴에서 Claude가 자연문으로 "context 사용률이 높아지고 있으므로, 적절한 구분 지점에서 /compact-prep → /compact를 실행하는 것을 권장합니다"라고 제안하게 된다. "멋대로 자동 compact가 실행되어 미궁에 빠지는" 패턴이 구조적으로 줄어든다.
효과
한 세션에서 10번 정도 compact를 해도 특별히 논리적 파탄이 일어나지 않을 정도로 안정되었으므로, 당초 기대했던 효과는 달성되었을 것이다!
- 압축으로 인한 작업 손실이 거의 제로
- 거절된 안건의 재제안이 사라짐
plan mode를 유지할 수 있게 됨tmux에서 여러worker를 돌릴 때의topology망각이 없어짐
참고: 구현 코드
이하는 본문에서 언급한 skill / hook의 실체다. 그대로 복사해서 작동한다. 배치는 Claude Code의 표준(~/.claude/)을 전제로 한다.
A: compact-prep skill 본문
~/.claude/skills/compact-prep/SKILL.md에 둔다.
---
name: compact-prep
description: |
...
B: 압축 직후의 복구 hook (2단계)
B-1: PostCompact hook
~/.claude/hooks/compaction-recovery.sh:
#!/bin/bash
# PostCompact hook (matcher: ""): 압축 발생을 marker file로 기록한다.
# PostCompact는 additionalContext 출력을 지원하지 않으므로,
...
B-2: UserPromptSubmit hook (복구 지시 주입)
~/.claude/hooks/userpromptsubmit-compaction-recovery.sh:
#!/bin/bash
# UserPromptSubmit hook: PostCompact가 남긴 marker file을 감지하고,
# additionalContext를 통해 압축 복구 지시를 context에 주입한다 (one-shot).
...
C: 60% 알림
C-1: statusline hook의 임계값 초과 분기
~/.claude/hooks/statusline.sh 내부에서, 기존의 context 사용률 계산 다음에 다음 블록을 추가한다:
# 임계값 초과 시 compact-prep 경고 marker를 작성한다 (cooldown 중이 아니라면)
COMPACT_WARN_THRESHOLD=60
if [ -n "$session_id" ] && [ "$int_pct" -ge "$COMPACT_WARN_THRESHOLD" ] 2>/dev/null; then
...
C-2: UserPromptSubmit hook (60% 알림 주입)
~/.claude/hooks/userpromptsubmit-compact-prep-reminder.sh:
#!/bin/bash
# UserPromptSubmit hook: statusline이 작성한 compact-warn marker를 감지하고,
# additionalContext를 통해 compact-prep 실행을 권고한다 (one-shot).
...
D: settings.json의 hook 등록
~/.claude/settings.json에 다음을 추가한다 (기존의 hook 설정에 추가하는 방식):
{
"hooks": {
"PostCompact": [
...
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기