
Claude Code의 compact로 인해 작업 문맥이 손실되는 문제와 hook을 통한 대책
요약
Claude Code의 context 압축(compact) 과정에서 발생하는 작업 문맥 및 판단 근거 손실 문제를 분석합니다. 요약 방식의 한계로 인해 발생하는 에이전트의 오작동 패턴을 설명하고, 이를 보완하기 위한 설계적 접근법을 다룹니다.
핵심 포인트
- Claude Code의 compact는 대화 이력을 요약하여 context를 관리함
- 요약 과정에서 '왜'라는 판단 근거와 기각된 안이 소실됨
- 압축 후 에이전트가 이전의 실패를 반복하거나 원칙을 어기는 현상 발생
- hook과 상태 파일을 활용한 에이전트 기억의 영속화 설계 필요
Claude Code로 조금 긴 작업을 하다 보면, 어느 타이밍부터 에이전트의 거동이 갑자기 변할 때가 있습니다. 방금 전까지 실기(実機)에서 확인한 후 배치하기로 합의했을 텐데, 다음 턴에서는 검증을 건너뛰고 배치 위치를 덮어쓰러 갑니다. 한 번 실패해서 철회했던 숏컷(shortcut)을 다시 같은 절차로 시도하려고 합니다. plan mode를 벗어난 상태에서 멋대로 구현을 계속합니다.
이것은 모델의 능력이 떨어진 것이 아닙니다. 대화 이력의 압축, 이른바 compact가 사이에 끼어들었기 때문입니다. 에이전트에게 긴 일을 맡길수록 이 현상은 피할 수 없게 됩니다. 여기서는 compact로 인해 에이전트가 무엇을 잃게 되는지, 그리고 그것을 hook과 상태 파일로 어떻게 보완할 수 있는지를 실제로 운용하고 있는 구성에 따라 써 내려가겠습니다. 프롬프트를 궁리하는 이야기가 아니라, 에이전트의 기억을 어떻게 영속화하여 복구할 것인가라는 환경 측면의 설계 이야기입니다.
Claude Code의 compact는 두 가지 경로로 실행됩니다. 하나는 수동으로, /compact를 명시적으로 호출하면 임의의 타이밍에 발화합니다. 다른 하나는 자동으로, 세션의 context 사용률이 상한선 근처에 도달하면 Claude Code 측의 판단에 따라 턴의 구분 시점에 끼어들어 실행됩니다. 자동 방식은 선언 없이 움직이기 때문에, 체감상으로는 어느샌가 압축되어 있었다는 형태가 됩니다. 발화점은 대체로 상한의 90% 전후이며, 환경 변수(CLAUDE_AUTOCOMPACT_PCT_OVERRIDE로 비율 지정)를 통해 전후로 조절할 수 있습니다.
어느 경로든 내용은 같습니다. 그때까지의 대화 이력을 LLM에 전달하여 자연문 요약을 만들게 하고, 요약과 직근의 턴, 그리고 system prompt를 조합하여 새로운 context를 다시 구성합니다. 즉, compact 이후의 에이전트가 보고 있는 것은 가공되지 않은 이력이 아니라 압축된 이야기입니다. 게다가 한 번 압축이 실행되면, 압축 전의 생(raw) 로그로는 돌아갈 수 없습니다. 비가역적인 작업이라는 점이 나중에 중요해집니다.
메커니즘은 이것뿐이지만, 까다로운 것은 요약이라는 방식 그 자체입니다.
compact의 요약은 무엇을 했는지를 이야기처럼 정리한 것이 됩니다. 파일을 이렇게 변경했다, 이 안을 검토했다, 테스트를 실행했다와 같은 행동의 기록은 비교적 잘 남습니다. 하지만 왜 그 선택을 했는지, 어떤 안을 기각했는지, 지금 어느 페이즈에 있는지와 같은 판단 부분은 요약 과정에서 희미해집니다. 요약은 어디까지나 과거의 작업 기록이지, 다음에 무엇을 해야 하는지에 대한 지시가 아닙니다. 이 차이가 압축 후의 오작동으로 이어집니다.
제가 실제로 겪었던 패턴은 대략 다음 네 가지입니다. 추상론이 아니라, 최근 세션에서 일어난 구체적인 사고로서 나열합니다.
- 채택안과 기각안의 혼동. 세션 도중에 안 A를 버리고 안 B로 전환했는데, 요약에는 안 A를 검토했다는 사실만 남는다. 압축 후의 에이전트는 그 안 A를 구현하기 시작한다.
- 페이즈의 혼동. 실기에서 확인한 후 배치하기로 합의했는데, 요약에서 그 순서 제약이 빠져나가 압축 후에는 완성 이미지(image)만 보고 검증을 건너뛴 채 배치 위치를 그대로 덮어쓴다.
- 기각 이유의 소실. 한 번 시도했다가 실패하여 철회한 숏컷을, 요약은 시도한 절차로서만 남긴다. 왜 안 되었는지가 전달되지 않기 때문에, 압축 후에 같은 절차를 재실행하여 같은 지뢰를 밟는다.
- 원칙과 세션 상태의 상실. 배치 위치를 직접 편집하지 않고 관리 원천인 repository를 편집하여 배포한다와 같이 세션 중에 확립한 원칙이 누락되어, 단기적으로는 동작하지만 재현할 수 없는 직접 편집으로 달려든다. plan mode를 벗어난 상태에서 작업을 계속하거나, tmux로 병행 중인 worker의 존재를 잊고 스스로 손을 움직이기 시작하거나, 고정해 두었던 테스트의 목적을 놓치고 주변 조사로 탈선하기도 한다.
모두 원인은 같습니다. 판단과 상태가 요약에서 빠져나가는 것에 있습니다. 어쩌다 일어난 사고가 아니라, 표준 compact 메커니즘상 어쩔 수 없이 발생하는 실패입니다. 그렇다면 대책도 프롬프트의 궁리가 아니라, 메커니즘으로 억제할 수밖에 없습니다.
할 일은 심플합니다. 요약에는 남기기 어려운 판단이나 세션의 상태만을 압축이 실행되기 전에 외부 파일로 써둡니다. 그리고 압축 직후에 그 파일을 반드시 다시 읽게 합니다. 이 두 가지가 맞물리면 요약에서 희미해진 정보를 압축의 외부에서 보완할 수 있습니다.
제 환경에서는 이것을 세 가지 파츠로 구성하고 있습니다. 압축 전에 상태를 써내는 skill, 압축 직후에 저장된 상태를 다시 읽게 하는 hook, 그리고 자동 compact에 선수를 빼앗기지 않기 위한 알림입니다. 차례대로 살펴보겠습니다.
/compact
/compact를 실행하기 전에, 사용자(user) 측에서 명시적으로 실행할 수 있는 slash command로서 하나의 skill을 준비해 두었습니다. 이름은 /pre-compact-save입니다. 하는 일은, 현재 세션에서 압축 요약(summary)으로 남기기 어려운 정보만을 추출하여, 고정된 경로의 상태 파일(${TMPDIR}/claude-session-state/<session_id>.md)에 정해진 포맷으로 저장하는 것입니다.
저장하는 항목은 요약에 포함되기 어려운 것들을 우선합니다.
- 현재 plan (plan 파일의 절대 경로와 현재 단계)
- 현재 단계 (지금 어느 단계에 있는지)
- 태스크 상황 (진행 중(in-progress)인 태스크와 보충 설명)
- 판단 로그 (채택한 안, 기각한 안, 그리고 기각한 이유)
- 제약 사항·블로커 (검증 후 배포와 같은 순서 제약 포함)
- worker 구성 (tmux 등으로 병행 실행 중인 pane, 역할, 담당)
- 편집 중인 파일 (저장되지 않았거나 검증되지 않은 파일과 주의 사항)
- 복구 메모 (압축 후의 자신에게 쓰는 편지. 다음 수와 밟아서는 안 될 지뢰)
설계에서 가장 주의를 기울이는 점은, 썼다고 생각했지만 내용이 망가져 있는 상황을 방지하는 것입니다. 이 부분은 인간의 선의에 의존하지 않고 기계적으로 강제합니다. session_id를 가져올 수 없으면 추측된 이름으로 파일을 만들지 않고 중단시키는 hard gate. 헤더(heading)의 순서를 고정하고, 작성을 마친 뒤 파일을 다시 읽어 필수 헤더가 누락되지 않았는지 감지하게 하는 forcing function. 부작용을 줄이기 위해 allowed-tools를 최소한으로 제한하는 것. 이 세 가지 점을 통해 상태 파일이 어중간한 상태로 남는 경로를 차단하고 있습니다.
요약을 AI에게 전적으로 맡기는 것이 아니라, 정해진 형식으로 기록을 쓰게 합니다. 이 한 끗 차이로 압축 후에 어디까지 복구할 수 있는지가 달라집니다.
skill의 실체는 다음과 같습니다. ~/.claude/skills/pre-compact-save/SKILL.md에 위치합니다. frontmatter의 strict_procedure와 allowed_tools가 강제의 핵심이며, 절차의 각 단계는 판단과 상태를 빠짐없이 기록하는 데 집중되어 있습니다.
---
name: pre-compact-save
description: |
...
get-session-id.sh는 세션 고유의 ID를 반환하는 작은 보조 스크립트로, 상태 파일의 이름을 세션마다 고유하게 만들기 위해 사용합니다. hard gate가 여기에 걸려 있는 이유는, ID를 가져오지 못한 채 추측된 이름으로 파일을 만들면 압축 후에 다른 세션의 상태를 읽어들이는 사고로 이어지기 때문입니다.
상태 파일을 작성하더라도, 압축 후의 에이전트가 이를 읽지 않는다면 의미가 없습니다. 이 부분이 가장 까다로운 지점이며, Claude Code의 hook 사양이 그대로 설계의 제약이 됩니다.
압축은 PostCompact라는 이벤트로 포착할 수 있습니다. 하지만 PostCompact hook는 additionalContext, 즉 컨텍스트(context)에 삽입할 텍스트를 반환할 수 없는 사양입니다. 압축 직후의 에이전트에게 지시를 직접 주입할 경로가 여기에는 없습니다. 압축 전에 실행되는 PreCompact hook는 additionalContext를 반환할 수 있지만, 이는 요약이 아직 존재하지 않는 단계에서 동작하므로 압축 후의 복구 지시를 전달하는 용도로는 적합하지 않습니다. 결국 압축 후의 에이전트에게 지시를 삽입할 수 있는 것은, 다음 사용자의 발화를 받는 UserPromptSubmit 이벤트뿐입니다.
그래서 2단계로 나눕니다.
- PostCompact hook는 session_id로 marker 파일을 하나 작성하기만 한다. 지시는 넣지 않고, 압축이 발생했다는 사실만을 기록한다.
- 다음 UserPromptSubmit hook가 해당 marker를 감지하면, additionalContext로 복구 지시를 주입하고 marker를 삭제한다 (단 한 번만 실행).
주입하는 복구 지시는 'plan 파일을 다시 읽어 단계와 제약을 확인하라, 상태 파일을 읽어 판단 로그와 복구 메모를 최우선으로 복원하라, TaskList에서 현재 태스크를 확인하라, 그리고 압축 요약의 다음 단계는 가설로 취급하고 plan과 상태 파일과 rules를 정답으로 삼아라'는 내용입니다. additionalContext에는 1만 자라는 상한선이 있으므로, 생(raw) 로그를 통째로 되돌리는 것이 아니라, 다시 읽어야 할 위치를 가리키는 포인터로 사용하는 것이 현실적입니다.
여기서 기교가 필요한 부분이 바로 hook 간의 연결 방식입니다. Claude Code에는 hook끼리 상태를 공유하는 공통 메커니즘이 없습니다. 따라서 파일 시스템상의 marker가 유일한 통신 경로가 됩니다. PostCompact는 압축이 발생했는지 여부만을 알고, UserPromptSubmit은 복구 지시를 주입할지 여부만을 압니다. 각각의 책임은 단일하며, 상태는 marker의 존재 여부만으로 표현됩니다. 압축이 일어나지 않는 일반적인 턴에서는 UserPromptSubmit hook이 파일의 유무를 한 번 확인하고 즉시 종료되므로 실질적인 비용은 없습니다. 전체를 fail-open 구조로 설계했기 때문에, hook이 고장 나더라도 Claude Code 본체는 멈추지 않습니다.
1단계인 PostCompact hook은 marker를 작성하기만 하는 가벼운 구현입니다. ~/.claude/hooks/mark-compaction.sh에 배치합니다.
#!/bin/bash
# PostCompact hook: 압축이 발생했다는 사실만을 marker로 기록하는 가벼운 hook.
# PostCompact는 additionalContext를 반환할 수 없으므로, 여기서 지시를 주입하지는 않는다.
...
2단계인 UserPromptSubmit hook이 해당 marker를 감지했을 때만 복구 지시를 주입합니다. ~/.claude/hooks/restore-after-compaction.sh에 배치합니다. plan 파일과 상태 파일의 존재를 확인하여, 존재하는 것들만 다시 읽어야 할 대상으로 지시합니다.
#!/bin/bash
# UserPromptSubmit hook: PostCompact가 남긴 압축 marker를 감지하면,
# additionalContext를 통해 복구 절차를 단 한 번 주입한다.
...
이렇게 하면 압축 직후의 첫 번째 턴부터, '나는 지금 plan의 이 단계 중간에 있으며, 이 worker에게 이것을 위임했다'라는 상태로 돌아갈 수 있습니다.
지금까지의 두 가지는 수동 /compact 전후로 무언가를 수행하도록 설계된 것입니다. 이는 사용자가 직접 상태를 기록한 뒤 /compact를 실행한다는 순서를 전제로 합니다. 하지만 자동 compact는 선언 없이 실행되므로 이 순서를 지킬 수 없습니다. 자동 compact가 먼저 실행되어 버리면, 상태 파일이 저장되지 않은 채 생(raw) 로그가 요약되어 압축되면서 복구 재료 자체가 사라지게 됩니다.
대책은 자동 compact가 실행되기 전에, 사용자가 수동으로 상태를 기록한 후 압축 단계로 들어갈 수 있는 환경을 만드는 것입니다. 이를 위해 context 사용률이 일정 수준을 넘으면 알림을 보내도록 설정했습니다.
임계값은 60%로 설정했습니다. 자동 compact의 발화점인 90% 전후보다 충분히 앞선 시점입니다. 알림이 발화점에 너무 가까우면, 사용자가 해당 턴에 인지하더라도 다음 작업 턴에서 자동 compact가 먼저 실행되어 버릴 여지가 남습니다. 안전하게 30% 정도의 마진을 두는 것입니다. 또한, 60%에서 알림이 왔을 때 아직 30%의 작업 여력이 남아 있으므로, 어중간한 상태에서 즉시 압축으로 넘어가지 않고 적절한 지점까지 진행한 뒤 상태를 기록할 수 있습니다. 집중하다 보면 사용률을 인지하기 어렵기 때문에, 주관적인 판단 기준이 아닌 타이머로서 기계적으로 개입시키려는 목적도 있습니다.
이 60%라는 숫자는 1M context를 전제로 할 때 비로소 의미를 갖습니다. 표준인 200K context라면 60%는 120K token에 불과하여, 알림이 온 시점에 남은 프레임이 너무 적어 즉시 압축할 수밖에 없습니다. 1M context라면 60%는 약 600K token에 해당하므로, 알림을 받은 후 적절한 지점까지 작업을 계속하더라도 여유 있게 상태 기록과 압축 단계로 넘어갈 수 있습니다. 1M context는 현재 Opus 4.7이나 4.8, Sonnet 5, Fable 5에서 유효하게 사용할 수 있으므로, 장시간 에이전트 운용을 본격적으로 하려면 우선 프레임을 확장할 준비를 하는 것이 선행되어야 합니다.
구현은 기존 부품에 추가하는 것만으로 충분합니다. 매 턴(turn)의 statusLine 업데이트 시에 컨텍스트(context) 사용률을 계산하고 있으므로(statusLine에는 입력 토큰(input token) 기준의 사용률이 전달됩니다), 임계값을 초과했을 때 경고 마커(warn marker)를 작성하는 분기를 추가합니다. 또 다른 UserPromptSubmit 훅(hook)이 해당 경고 마커를 감지하면, 적절한 시점에서 상태를 기록하도록 제안하라는 지시를 주입(inject)하도록 합니다. 사용하는 마커는 앞으로 알리고 싶다는 '경고 마커(warn marker)', 이미 알렸다는 '쿨다운 마커(cooldown marker)', 그리고 압축 직후를 나타내는 '마커'의 3종류입니다. 각각의 훅은 자신의 책임에 대응하는 한 종류의 마커만을 읽고 씁니다.
statusLine 측은 사용률을 계산하는 기존 처리의 끝에, 임계값 초과 시 경고 마커를 작성하는 분기를 추가하기만 하면 됩니다. 컨텍스트 사용률은 입력 토큰 기준(total_input_tokens / context_window_size)으로 산출합니다. 쿨다운 마커(claude-ctxwarn-sent)가 있을 때는 작성하지 않으므로, 한 사이클 내에서 여러 번 알림이 발생하는 일은 없습니다.
# --- context window 사용률: 바 표시 + pre-compact-save 60% warn marker ---
pct_ctx = None
try:
...
경고 마커를 포착하여 제안을 주입하는 것이 또 다른 UserPromptSubmit 훅입니다. 이는 ~/.claude/hooks/context-budget-warn.sh에 위치합니다. 주입 후에는 경고 마커를 지우고 쿨다운 마커를 세워, 다음 압축 전까지 재알림을 중단합니다.
#!/bin/bash
# UserPromptSubmit 훅: statusline이 배치한 context 경고 marker를 감지하면,
# 적절한 시점에 /pre-compact-save를 촉구하는 메시지를 한 번만 주입한다.
...
마지막으로, 이러한 훅들을 ~/.claude/settings.json에 등록합니다. PostCompact에 하나, UserPromptSubmit에 두 개(압축 직후의 복구와 컨텍스트 잔량 알림)를 나열합니다. UserPromptSubmit은 여러 개를 등록할 수 있으며 매 턴 양쪽 모두 실행되지만, 마커가 없으면 즉시 종료되므로 통상적인 비용은 무시할 수 있습니다.
{
"hooks": {
"PostCompact": [
...
이 구성으로 정착한 이후, 한 세션에서 10회 가까이 compact가 끼어들어도 논리적 파탄이 거의 일어나지 않게 되었습니다. 압축으로 인한 작업 손실은 거의 제로가 되었고, 거절했을 터인 안을 다시 제안하는 현상은 사라졌으며, 플랜 모드(plan mode)를 유지할 수도 있게 되었습니다. 여러 워커(worker)를 병행 운용할 때 발생하는 토폴로지(topology) 망각 현상도 없어졌습니다.
파생적인 효과도 있습니다. 컨텍스트 잔량 알림은 현재 어느 단계에 있고 무엇을 남겨두어야 하는지를 강제적으로 점검하게 하므로, 끊어가는 판단 그 자체가 정리됩니다. 에이전트를 위해 상태를 기록하는 작업이 곧 자신의 생각을 정리하는 작업이 되는, 일종의 부산물입니다.
compact 문제는 흔히 작성 방식의 묘수로 해결하려 하기 쉽습니다. 하지만 요약이 판단력을 흐리게 하는 것은 구조적인 특성이므로, 프롬프트를 바꾼다고 해결되지 않습니다. 실제로 도움이 되는 것은 판단을 압축의 외부에 저장해 두었다가, 압축 후에 반드시 다시 읽게 만드는 방식입니다. 에이전트의 컨텍스트를 방치하면 사라지는 대화 로그가 아니라, 제대로 저장하고 되돌려야 할 작업 상태로 취급하는 것. 우선은 이러한 관점의 전환부터 시작해야 한다고 생각합니다.
코드를 작성하는 비중을 AI로 옮기면 옮길수록, 인간의 업무는 현장의 세세한 제약과 판단을 AI가 잊지 않는 형태로 남기는 설계로 향하게 됩니다. 안을 거절한 이유, 검증 후 배포한다는 순서, 직접 건드려서는 안 되는 배치 위치. 이러한 판단이야말로 인간이 그 자리에 가져오는 가치입니다. 그것을 어떻게 계승할지를 설계하는 것이 앞으로의 엔지니어링의 핵심이 되지 않을까요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기