728x90

월말에 Anthropic 사용량 대시보드를 열었다가 "우리가 이걸 이렇게 많이 썼나?" 싶었던 적 있으면 이 글이 도움이 될 거다. Systima에서 Claude Code와 OpenCode를 같은 모델·같은 머신·같은 태스크에 물려놓고, API 경계에서 오가는 요청 페이로드를 전부 뜯어본 실측 리포트를 냈다. 결론부터 말하면 Claude Code는 사용자가 프롬프트를 치기도 전에 약 33,000 토큰을 소비하고, OpenCode는 약 7,000 토큰이었다. 약 4.7배 차이다.

단순히 "Claude Code가 비싸다"로 끝날 얘기가 아니다. 이 오버헤드가 어디서 나오는지, 캐싱은 뭘 살려주고 뭘 못 살려주는지, 그리고 실무에서 어떤 셋업일 때 이게 재앙이 되는지를 알아야 도구를 제대로 고를 수 있다.

1. 왜 지금 이 얘기가 나오는가

AI 코딩 에이전트를 팀에 도입해본 사람이면 알 거다. 처음엔 "토큰? 얼마 안 나오겠지" 하다가, 여러 명이 하루종일 돌리기 시작하면 청구서가 예상 밖으로 튄다. 문제는 토큰 오버헤드가 곧 비용이자 지연시간이자 컨텍스트 예산이라는 점이다.

에이전트가 매 턴마다 보내는 건 여러분의 프롬프트만이 아니다. 시스템 프롬프트, 툴 스키마(도구 정의 JSON), 그리고 각종 주입된 스캐폴딩이 앞에 붙는다. 이 베이스라인은 매 요청마다 다시 전송되거나 캐시에서 다시 읽힌다. 즉 33k 토큰짜리 베이스라인이면, 200k 컨텍스트 윈도우의 약 1/6을 코드 한 줄 넣기도 전에 이미 잡아먹고 시작하는 셈이다.

특히 EU AI Act Article 12처럼 에이전트 동작을 로깅하고 설명할 수 있어야 하는 규제 환경이라면, "내 에이전트가 실제로 뭘 보내는가"를 소문이 아니라 데이터로 답할 수 있어야 한다. 이 리포트가 딱 그걸 한 거다.

2. 33k는 어디서 나오는가 — 시스템 프롬프트 해부

Systima는 하네스(harness, 여기선 Claude Code / OpenCode)와 모델 엔드포인트 사이에 로깅 프록시를 끼워넣었다. 구조는 이렇다.

harness (Claude Code / OpenCode)
    → logging proxy (요청 페이로드 + 응답 usage 캡처)
        → model endpoint

프록시는 두 가지를 기록한다. 하네스가 실제로 내보낸 JSON 페이로드(시스템 블록, 툴 스키마, 메시지)와, API가 반환한 usage 블록(input 토큰, cache write, cache read, output 토큰). 페이로드는 "무엇을 보냈나"의 원본 진실, usage는 "무엇이 과금됐나"의 원본 진실이다.

T1: "OK라고만 답해" — 순수 오버헤드 측정

가장 극단적인 테스트다. 프롬프트는 딱 22자, "Reply with exactly: OK". 여기에 각 하네스가 뭘 얹어 보냈는지 보자.

구성요소 Claude Code OpenCode
시스템 프롬프트 27,344자 (3블록) 9,324자 (1블록)
툴 스키마 27개 툴, 99,778자 10개 툴, 20,856자
첫 메시지 스캐폴딩 7,997자 (system-reminder 블록) 없음
실제 프롬프트 22자 22자
첫 턴 페이로드(보정치) 약 32,800 토큰 약 6,900 토큰

핵심은 두 하네스 모두 툴 스키마가 지배적이라는 점이다. Claude Code의 약 33k 중 약 24,000이 툴 정의고, OpenCode의 약 6,900 중 약 4,800이 툴 정의다.

왜 Claude Code가 이렇게 무거운가? 27개 툴이 코딩 코어만이 아니라 백그라운드 에이전트/오케스트레이션 스위트 전체를 포함하기 때문이다. CronCreate, Monitor, Task 패밀리, worktree 관리, 푸시 알림까지 다 들어있다. 게다가 첫 유저 메시지 앞에 세 개의 리마인더 블록(위임용 에이전트 카탈로그, 사용 가능 스킬 카탈로그, 유저 컨텍스트)이 주입된다.

반면 OpenCode는 거의 미니멀하다. "You are OpenCode, the best coding agent on the planet"으로 시작하는 시스템 블록 하나, 클래식한 코딩 툴 10개, 그리고 여러분의 프롬프트가 유일한 유저 콘텐츠다.

툴을 다 꺼도 여전히 3배

툴을 빼고 시스템 프롬프트 자체만 비교하면, Claude Code는 26,891자(약 6.5k 토큰), OpenCode는 8,811자(약 2.0k 토큰)다. 툴을 전부 제거해도 Claude Code의 명령어 세트가 3배 이상 크다. 나머지는 톤 규칙, 안전 가이드, 태스크 관리 지침, 환경 설명 같은 "행동 강령"이다.

직접 확인하고 싶다면 툴을 끄고 돌려볼 수 있다.

# Claude Code에서 툴 비활성화
claude --tools

# OpenCode에서 툴 비활성화 (설정)
# opencode 설정 JSON에
{ "tools": { "*": false } }

3. 캐싱은 어디까지 살려주나 — 여기가 진짜 함정

"어차피 프롬프트 캐싱 걸리니까 괜찮은 거 아니야?"라고 생각하기 쉽다. 여기가 실무에서 제일 많이 착각하는 지점이다.

OpenCode는 캡처된 모든 실행에서 요청 프리픽스가 바이트 단위로 동일했다. 세션당 한 번 캐시에 쓰고, 이후엔 몇 푼짜리 cache read로 다시 읽는다. 이상적인 캐시 활용이다.

반면 Claude Code는 세션 중간에 수만 토큰의 prompt-cache 토큰을 다시 썼다. 같은 태스크에서 OpenCode보다 최대 54배 많은 cache write를 기록했다. cache write는 프리미엄 가격으로 과금된다. 대시보드 숫자가 치솟는 주범이 바로 이거다.

왜 다시 쓰는가: Claude Code는 대화가 진행되면서 system-reminder 블록을 추가로 주입한다. 첫 턴엔 3개, 첫 툴 라운드트립엔 4개로 늘어난다. 스캐폴딩이 턴 수에 따라 자라니까, 프리픽스가 바뀌고 캐시가 깨지는 것으로 보인다.

중요한 사실 하나. 세 가지는 캐시 할인과 무관하게 무조건 스케일한다.

  • 첫 턴의 cache write (프리미엄 과금)
  • 매 턴의 cache read (싸긴 하지만 0은 아님)
  • 컨텍스트 윈도우 소비 — 이건 어떤 캐시 할인으로도 줄지 않는다

세 번째가 핵심이다. 33k 베이스라인은 매 턴이 200k 윈도우의 1/6 지점에서 시작한다는 뜻이고, 캐싱이 아무리 잘 돼도 이 컨텍스트 압박은 절대 안 줄어든다.

모델을 바꾸면 그림이 달라진다

Systima가 T1을 Claude Fable 5로 재실행했더니 격차가 줄었다. 이유가 흥미롭다. Claude Code의 시스템 프롬프트는 모델 조건부다. Sonnet에는 27,787자를 보냈지만 Fable에는 10,526자만 보냈고, 툴 스키마도 99,778자에서 82,283자로 줄었다. 같은 27개 툴인데 doctrine이 훨씬 적다. OpenCode 페이로드는 두 모델에서 바이트 단위로 동일했다.

결과적으로 페이로드 기준 격차가 Sonnet에서 4.7배, Fable에서 약 3.3배로 좁혀졌다. 여전히 훨씬 무겁지만, 배수는 모델에 따라 다르다는 점을 기억하자.

4. 실무 관점 — 멀티플라이어와 흔한 함정

T1은 최선의 시나리오다. 현실 세션은 저렇게 린하게 시작해서 짧게 끝나지 않는다. 실무에서 얹히는 레이어들을 보자.

멀티플라이어 1: 지시 파일 (AGENTS.md / CLAUDE.md)

프로덕션 레포의 72KB 지시 파일을 워크스페이스에 넣고 T1을 재실행하니, 양쪽 다 요청당 20,000 토큰 이상이 추가됐다.

  • OpenCode: 13,152 → 33,336 토큰
  • Claude Code: 39,005 → 59,243 토큰

여기 첫 번째 함정. Claude Code 2.1.207은 AGENTS.md를 완전히 무시했다. CLAUDE.md로 이름을 바꿔야만 읽었고, 그것도 시스템 프롬프트가 아니라 첫 유저 메시지에 주입했다. OpenCode는 두 파일명을 다 읽고 시스템 프롬프트에 넣는다.

이게 왜 무서운가 하면, 무시된 지시 파일은 아무 에러도 안 낸다. 팀 컨벤션을 열심히 적어놨는데 에이전트가 조용히 무시하고 있는 상황을 상상해보라. 파일명을 잘못 쓰면 이런 식으로 조용히 아무 일도 안 일어난다.

# 실무 체크: 하네스가 어떤 파일명을 실제로 먹는지 확인
$ ls -la
-rw-r--r--  1 dev  staff  73728  AGENTS.md   # Claude Code가 무시함!

# 로그를 보면 지시 파일 관련 주입이 아예 없다
# (에러 메시지가 안 나오는 게 오히려 함정)

# 해결: CLAUDE.md로 심볼릭 링크 또는 리네임
$ ln -s AGENTS.md CLAUDE.md
$ ls -la
lrwxr-xr-x  1 dev  staff      9  CLAUDE.md -> AGENTS.md
-rw-r--r--  1 dev  staff  73728  AGENTS.md

멀티플라이어 2: MCP 서버

MCP 서버를 붙이면 스키마가 프롬프트에 추가된다. 스키마는 하네스 간 동일하니 세금도 거의 같다. 작은 서버당 요청당 약 1,000~1,400 토큰. 5개 서버를 붙였더니 Claude Code는 페이로드 기준 4,900 토큰, OpenCode는 metered 6,967 토큰이 늘었고, 툴 개수가 각각 27→69, 10→52로 불었다.

여기 두 번째 조용한 함정. Claude Code는 print 모드에서 프로젝트 스코프 .mcp.json을 조용히 무시했다. --mcp-config 플래그를 명시해야 읽었다. 서버가 붙어있다고 가정했는데 실제론 안 붙어있는 상황이 생긴다.

# .mcp.json이 있는데도 print 모드에서 서버가 안 잡히는 경우
$ claude -p "list available tools"
# → MCP 툴이 목록에 안 보임 (에러도 없음)

# 명시적으로 config를 넘겨야 함
$ claude -p "list available tools" --mcp-config .mcp.json
# → 이제 MCP 서버 툴이 스키마에 포함됨

공교육용 교훈은 하나다. 서버가 붙었다고 가정하지 말고 경계(API boundary)에서 검증하라. 프록시 로그든 툴 목록이든, 실제 페이로드를 눈으로 확인해야 한다.

멀티플라이어 3: 프레임워크 템플릿

BMAD 같은 스토리 기반 워크플로우 프레임워크는 슬래시 커맨드를 페르소나·프로토콜·체크리스트로 가득한 큰 템플릿으로 확장한다. 8,405자짜리 대표 템플릿을 T3 프롬프트로 넣어봤더니, 템플릿 자체는 약 2,100 토큰이지만 대화 히스토리에 들어가서 이후 모든 요청이 다시 실어 나른다. 9-요청 세션이면 9번 재전송된다. 프레임워크 세금 = 템플릿 크기 × 요청 수, 그리고 이건 위의 모든 것 위에 쌓인다.

멀티플라이어 4: 서브에이전트 — 여기서 총액이 폭발

이게 제일 무섭다. 작업을 서브에이전트 2개로 팬아웃했더니, 직접 하면 121,000 토큰이던 작은 태스크가 513,000 토큰으로 뛰었다. 서브에이전트마다 자기 부트스트랩 비용이 있고, 부모가 다시 그 트랜스크립트를 소비하기 때문이다. "병렬로 나누면 빠르겠지"라고 서브에이전트를 남발하면 토큰 청구서가 4배 넘게 튀는 걸 각오해야 한다.

반전: 멀티스텝에선 Claude Code가 더 쌀 수도 있다

모든 게 Claude Code 불리로만 흐르는 건 아니다. T3(write-run-test-fix 루프)에서는 예상이 뒤집혔다.

지표 Claude Code OpenCode
모델 요청 수 3 9 (+타이틀 콜 1)
툴 호출 방식 한 라운드트립에 병렬 배치 턴당 툴 1개
누적 metered input 약 121,000 토큰 약 132,000 토큰

Claude Code는 전체 작업(파일 쓰기 2개 + 스크립트 실행 2개)을 단일 병렬 툴 라운드트립으로 배치했다. OpenCode는 턴당 정확히 툴 하나씩 9번 돌았다. 베이스라인은 매 요청마다 재전송되므로 요청 수가 베이스라인을 곱한다. OpenCode는 약 7k 베이스라인을 9번 냈고, Claude Code는 약 33k를 3번 냈다. 결과적으로 총액이 수렴했다.

공식: 전체 작업 input ≈ 베이스라인 × 요청 수 + 대화 성장분. 큰 베이스라인 + 공격적 배칭 하네스와, 작은 베이스라인 + 직렬 하네스가 같은 지점에 착지할 수 있다. 미터는 높은 데서 시작하지만, 세션이 어떻게 전개되느냐가 누가 더 쓸지를 결정한다.

5. 정리 — 누가 언제 뭘 써야 하나

한 줄 요약: Claude Code는 시작 오버헤드가 크지만 툴 배칭이 강하고, OpenCode는 베이스라인이 가볍지만 턴마다 그 비용을 반복 지불한다.

  • 짧고 단순한 반복 작업이 많다면 (커밋 메시지, 소규모 리팩터, 단발성 질의): OpenCode가 유리하다. 33k를 매번 태울 이유가 없다.
  • 복잡한 멀티스텝 작업을 배치로 처리한다면: Claude Code의 병렬 배칭이 요청 수를 줄여 총액을 낮출 수 있다.
  • 컨텍스트 윈도우가 빡빡한 대형 코드베이스라면: 캐시 할인과 무관하게 33k가 윈도우를 잡아먹는다는 걸 기억하라. 여기선 베이스라인이 가벼운 쪽이 실질적으로 유리하다.
  • 규제 환경(EU AI Act 등)이라면: 로깅 프록시를 경계에 두고 실제 페이로드를 캡처하라. "무엇을 보내는가"를 데이터로 답할 수 있어야 한다.

그리고 도입 전 반드시 확인할 것 세 가지: ① 지시 파일 파일명을 하네스가 실제로 먹는지(AGENTS.md vs CLAUDE.md), ② MCP 서버가 경계에서 진짜 붙었는지, ③ 서브에이전트 팬아웃이 정말 필요한지(4배 폭발 각오). 이 셋만 챙겨도 예상 밖 청구서는 크게 줄어든다.

마지막으로, 이 모든 수치는 특정 버전(Claude Code 2.1.207, OpenCode 1.17.18, 2026년 7월 claude-sonnet-4-5 / claude-fable-5 기준)에서 나온 것이다. 하네스와 모델은 계속 바뀌므로, 여러분 환경에서는 직접 프록시를 끼워 측정하는 게 정답이다. 소문 말고 데이터로.

참고 자료

728x90
728x90

내부 LLM 게이트웨이 하나 굴려본 사람이라면 이 얘기가 남 일 같지 않을 거다. Claude Code가 ANTHROPIC_BASE_URL을 커스텀 엔드포인트로 돌렸을 때, 시스템 프롬프트에 들어가는 "오늘 날짜" 문장 하나를 조용히 바꿔서 요청 안에 분류 신호를 숨긴다는 분석이 나왔다. 눈으로는 거의 구분이 안 되는 유니코드 문자로 말이다.

결론부터 말하면, 대부분의 사용자는 이 경로가 아예 안 켜진다. 하지만 자체 게이트웨이나 프록시, 모델 라우터를 태워서 Claude Code를 쓰는 조직이라면 얘기가 다르다. 이 글에서는 원리를 뜯어보고, 실무에서 내 트래픽에 이런 마커가 붙는지 직접 확인하는 방법까지 정리한다.

1. 왜 지금 화제인가

스테가노그래피(steganography)는 암호화와 다르다. 암호화는 "이건 비밀이다"라는 걸 대놓고 보여주고 내용만 못 읽게 한다. 스테가노그래피는 비밀이 있다는 사실 자체를 숨긴다. 평범해 보이는 이미지의 최하위 비트에 데이터를 심거나, 문장의 공백·문장부호에 정보를 인코딩하는 식이다.

이번 사안이 딱 그 케이스다. Claude Code 2.1.196 바이너리에는 시스템 프롬프트로 들어가는 날짜 문자열을 바꾸는 함수가 있다. 기본형은 이렇다.

Today's date is 2026-06-30.

겉으로는 그냥 오늘 날짜를 알려주는 평범한 안내 문장이다. 그런데 특정 조건에서 이 문장의 두 지점이 바뀐다.

  • Today's의 아포스트로피(')
  • 날짜 구분자(-/)

여기서 "왜 문제냐"가 핵심이다. 파일시스템 접근하고, 셸 명령 실행하고, 패키지 설치하고, 커밋까지 푸시하는 개발자 도구가 사용자에게 알리지 않고 보이지 않는 표식으로 분류 정보를 요청에 심고 있다는 것. 목적이 "중국 연구소의 모델 증류(distillation) 탐지"라 해도, 구현 방식이 조용하고 은밀하다는 게 신뢰를 흔든다는 지적이다.

2. 동작 원리: 마커가 어떻게 심기나

트리거 조건

모든 게 ANTHROPIC_BASE_URL에서 시작한다. 이건 Claude Code의 API 기본 URL을 오버라이드하는 환경 변수다. 로직을 순서대로 보면:

  1. ANTHROPIC_BASE_URL설정 안 됨 → 함수 조기 반환(early return). 평범한 Today's date is 2026-06-30. 유지.
  2. 설정됐지만 호스트가 api.anthropic.com → 마찬가지로 조기 반환.
  3. 그 외의 경우에만 아래 검사를 수행한다.
    • 시스템 시간대가 Asia/Shanghai 또는 Asia/Urumqi인지
    • 호스트명이 디코딩된 도메인 목록과 일치하거나 그 하위 도메인인지
    • 호스트명에 특정 AI 연구소 키워드가 들어있는지

무엇이 인코딩되나

시간대가 상하이/우루무치면 날짜 구분자가 바뀐다.

2026-06-30  →  2026/06/30

호스트명 분류 결과는 아포스트로피에 인코딩된다. 이게 진짜 교묘하다.

일반:                          '        (U+0027)
알려진 도메인:                  ’        (U+2019)
연구소 키워드:                  ʼ        (U+02BC)
알려진 도메인 + 연구소 키워드:  ʹ        (U+02B9)

모노스페이스 폰트에서 이 네 개를 눈으로 구분할 수 있는가? 나는 못 한다. 그게 포인트다. 로그를 흘려봐도 "그냥 아포스트로피네" 하고 넘어간다.

도메인 목록은 왜 안 보였나

도메인·키워드 목록은 그냥 문자열로 박혀있지 않다. base64로 저장한 다음 XOR 키 91로 디코딩한다. 그래서 바이너리를 strings로 훑어도 deepseek, zhipu 같은 게 바로 안 나온다. 디코딩된 연구소 키워드에는 이런 게 있다.

deepseek, moonshot, minimax, zhipu, bigmodel,
baichuan, stepfun, 01ai, dashscope, volces

도메인 목록은 더 크다. 중국 기업 도메인(baidu.com, bytedance.net, alibaba-inc.com), AI 회사 도메인(moonshot.ai, stepfun-inc.com), 그리고 프록시·리셀러·게이트웨이 도메인(proxyai.com, anyrouter.top, claude-code-hub.app, yunwu.ai, zenmux.ai)까지 포함된다.

이 마커는 에이전트 컨텍스트를 구성할 때 currentDate 값으로 들어가고, 같은 컨텍스트에는 조건에 따라 사용자 이메일이나 프로젝트 정보도 포함될 수 있다. 즉 모델로 전송되는 시스템 컨텍스트의 일부가 된다. Anthropic 백엔드가 이 값을 파싱할 가능성도 남아있다는 게 원 분석의 지적이다.

비유하자면 배송 송장에 찍힌 날짜 도장 같은 거다. 대부분 사람에겐 그냥 "2026/06/30"이지만, 도장 잉크 색과 슬래시 각도만 보면 창고 시스템은 "이 물건은 어느 경로로 왔고 어느 창구에서 검수 대상인지"를 안다. 받는 사람은 색 차이를 눈치채지 못한다.

3. 실무 관점: 언제 이걸 만나고 어떻게 확인하나

영향받는 시나리오

정상적이고 합법적인데도 이 경로가 켜지는 경우가 문제다. 실제로 우리가 게이트웨이를 두는 이유들이다.

  • 내부 LLM 게이트웨이로 감사·비용 관리
  • 로컬 프록시로 Claude Code가 Anthropic에 뭘 보내는지 검증(데이터 유출 확인)
  • 프롬프트 난이도에 따라 모델 동적 선택
  • 프로젝트별 여러 Anthropic 계정 전환
  • 자격 증명·PII·회사 기밀 필터링

이런 세팅에서 게이트웨이 호스트명이 우연히 목록의 키워드나 도메인 패턴에 걸리거나, 서버 시간대가 상하이/우루무치면 마커가 붙는다. 원 분석은 "이상하지만 합법적인 설정을 쓰는 일반 개발자가 오히려 더 쉽게 지문 채집 대상이 된다"고 본다. 진짜 작정한 공격자는 호스트명 바꾸고 시간대 바꾸고 바이너리 패치하면 그만이기 때문이다.

내 트래픽에 마커가 붙는지 직접 확인하기

가장 확실한 건 프록시 앞단에서 실제로 나가는 요청 바디를 뜯어보는 거다. mitmproxy를 세워서 system 필드의 날짜 문장을 유니코드 코드포인트 단위로 확인한다.

# 1) mitmproxy 실행
mitmproxy --listen-port 8080 --set flow_detail=3

# 2) Claude Code를 프록시 태워서 실행 (테스트용 커스텀 URL)
export ANTHROPIC_BASE_URL="https://my-gateway.internal.example.com"
export HTTPS_PROXY="http://127.0.0.1:8080"
claude "hello"

캡처한 요청 바디에서 날짜 문장만 뽑아 아포스트로피 문자의 정체를 확인한다. 파이썬 한 줄이면 된다.

python3 - <<'PY'
s = "Today’s date is 2026/06/30."   # 캡처한 실제 문자열을 붙여넣기
for ch in s:
    if not ch.isascii():
        print(f"non-ASCII: {ch!r}  U+{ord(ch):04X}")
PY

출력이 이렇게 나오면 마커가 붙은 거다.

non-ASCII: '’'  U+2019

U+2019면 "알려진 도메인" 분류, U+02BC면 "연구소 키워드", U+02B9면 둘 다 걸린 거다. 아무 출력도 없고(모두 ASCII) 날짜가 - 구분자면 정상 경로다.

흔한 함정

함정 1: 프록시 TLS 인증서를 안 넣어서 캡처가 아예 안 됨. Claude Code는 Node 기반이라 시스템 신뢰 저장소가 아니라 Node의 CA를 본다. mitmproxy CA를 안 걸면 이런 에러로 요청 자체가 실패한다.

Error: unable to get local issuer certificate
    at TLSSocket.onConnectSecure (node:_tls_wrap:1678:34)
    code: 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY'

해결은 mitmproxy CA를 Node에 명시적으로 물려주는 거다.

export NODE_EXTRA_CA_CERTS="$HOME/.mitmproxy/mitmproxy-ca-cert.pem"
claude "hello"

함정 2: 시간대를 컨테이너에서 신경 안 씀. CI/에이전트를 상하이 리전 노드에서 돌리면 TZ=Asia/Shanghai가 잡혀서 날짜 구분자가 슬래시로 바뀔 수 있다. 컨테이너 TZ를 명시적으로 UTC로 고정하는 게 재현성 측면에서도 낫다.

# TZ 확인
$ date +%Z
CST
# 명시적 고정
$ export TZ=UTC
$ date +%Z
UTC

함정 3: 로그 grep으로 못 잡는다. 로그 파이프라인에서 grep "Today's date" 걸어도 아포스트로피가 ASCII가 아니면 매칭이 안 된다. 이런 오탐(false negative)이 진짜 골치다.

# 이렇게 하면 U+2019 버전은 안 걸린다
$ grep "Today's date" request.log
# (매칭 없음)

# 코드포인트 무관하게 잡으려면 패턴을 느슨하게
$ grep -P "Today.s date is \d{4}" request.log

대응 옵션과 트레이드오프

  • 프록시에서 정규화(normalize). 게이트웨이 미들웨어에서 시스템 프롬프트의 날짜 문장을 강제로 ASCII '-로 치환한다. 가장 확실하지만, 남이 만든 요청 바디를 손대는 거라 파싱 로직이 바뀌면 유지보수 비용이 든다.
  • 호스트명·시간대 회피. 원 분석대로 호스트명을 목록에 안 걸리게 바꾸고 시간대를 UTC로 두면 조기 반환된다. 단, 이건 "탐지 신호를 무력화"하는 거라 Anthropic 약관 관점에서 애매할 수 있으니 조직 정책 확인이 필요하다.
  • 바이너리 패치/래핑. 이론상 가능하지만 서명된 바이너리를 건드리는 순간 업데이트마다 재작업이고 지원도 못 받는다. 실무에선 비추천이다.
  • 그냥 오픈소스 클라이언트로 갈아타기. HN 반응 중엔 Codex CLI가 FOSS라 이런 은닉 동작 가능성이 낮다는 의견이 있었다. 다만 "클라이언트가 오픈소스면 서버에서 더 숨긴다는 뜻일 뿐"이라는 반론도 있으니 만능은 아니다.

참고로 이게 무조건 "악성"이냐에 대해선 의견이 갈린다. "약관 위반 사용자를 걸러내면서 정상 사용자엔 방해 안 되는 방법"이라는 옹호론과, "PII를 안 수집한다는 보장이 없고, 접근 패턴을 공개하지 않고 지문 채취하는 것 자체가 부정직"이라는 비판론이 팽팽하다. 판단은 각자의 몫이지만, 내 게이트웨이 트래픽에 뭐가 붙는지는 내가 아는 게 맞다는 데는 이견이 없을 거다.

4. 정리

한 줄 요약: Claude Code는 ANTHROPIC_BASE_URL이 공식 엔드포인트가 아닐 때, 호스트명·시간대를 검사해 시스템 프롬프트 날짜 문장의 아포스트로피와 구분자를 보이지 않게 바꿔 분류 신호를 심는다. 대부분은 비활성이지만, 커스텀 엔드포인트를 쓰면 켜진다.

  • 확인해야 할 사람: 자체 LLM 게이트웨이·프록시·모델 라우터를 통해 Claude Code를 운영하는 인프라/백엔드 엔지니어.
  • 언제: 커스텀 ANTHROPIC_BASE_URL을 쓰거나, 상하이/우루무치 시간대 노드에서 에이전트를 돌리는 순간부터.
  • 바로 할 것: mitmproxy로 요청 바디의 날짜 문장을 캡처해 유니코드 코드포인트를 확인하고, 필요하면 게이트웨이에서 정규화. 로그 grep은 코드포인트 무관 패턴으로 바꿀 것.

단정할 수 없는 부분(백엔드가 실제로 이 값을 파싱하는지, PII 수집 여부 등)은 여전히 남아있다. 확실한 건 "내 도구가 내가 모르는 뭔가를 요청에 심고 있다"는 사실이고, 이건 신뢰 문제로 직결된다. 프록시 뒤에서 운영한다면 오늘 한 번 캡처 떠서 내 눈으로 확인하는 걸 권한다.

참고 자료

728x90
728x90

며칠 전 Claude Code 세션 로그를 감사 추적용으로 긁어 모으는 작업을 하다가 좀 황당한 걸 발견했다. thinking block에 분명히 추론 내용이 들어 있을 줄 알았는데, 까보니 사람이 읽을 수 없는 600자짜리 base64스러운 문자열만 덩그러니 박혀 있더라. "ctrl+o로 보던 그 사고 과정은 어디 갔지?" 싶었는데, 알고 보니 그건 진짜 추론이 아니라 요약이었다.

이게 단순한 트리비아가 아닌 이유는, AI 코딩 도구를 컴플라이언스 환경에서 굴리는 사람한테는 "에이전트가 왜 이 코드를 짰는지 재현 가능한가?"라는 질문에 직결되기 때문이다. 오늘은 그 얘기를 실무 관점에서 풀어본다.

1. 왜 지금 이게 화제인가

Claude Code는 세션을 디스크에 JSON 형태로 차곡차곡 기록한다. 그래서 많은 사람들이 자연스럽게 "이걸 감사 로그로 쓰면 되겠네"라고 생각한다. 코드 리뷰에서 "이 변경 왜 했어?"라는 질문이 나왔을 때, 에이전트의 사고 과정을 그대로 보여주면 깔끔하니까.

그런데 발단이 된 글(patrickmccanna.net)의 저자가 로컬 로그를 직접 까봤더니, thinking block 안에 있던 건 실제 추론 텍스트가 아니라 600자 길이의 signature였다. Anthropic 공식 문서를 따라가 보면 구조가 이렇다:

  • Claude는 추론을 signature 안에 암호화한다.
  • 복호화 키는 Anthropic이 보유한다.
  • 사용자 기기는 키를 받지 못한다.
  • API가 돌려주는 건 추론 원문이 아니라 reasoning summary(요약)다.
  • 전체 thinking output을 받으려면 enterprise agreement가 필요하다.

즉, ctrl+o로 보던 그 예쁜 사고 과정은 모델이 실제로 행동을 구동한 추론 그 자체가 아니라, 그걸 압축·요약한 결과물이다. 그리고 압축 과정에서 정보 손실이 일어난다.

2. 동작 원리 — 직접 까보자

백문이 불여일견이니 실제 세션 로그를 한번 들여다보자. Claude Code 세션은 보통 홈 디렉터리 아래 프로젝트별로 떨어진다.

# 세션 로그 위치 확인 (환경에 따라 경로 다를 수 있음, 확인 필요)
$ ls -la ~/.claude/projects/

drwxr-xr-x  3 user staff   96 Feb 10 14:22 -Users-user-work-myapp

해당 디렉터리의 jsonl 파일에서 thinking block만 추려보자. jq로 type이 thinking인 것만 필터링한다.

# thinking 타입 메시지만 추출해서 signature 길이 확인
$ cat ~/.claude/projects/-Users-user-work-myapp/*.jsonl \
  | jq -r 'select(.message.content[]?.type == "thinking")
           | .message.content[]
           | select(.type == "thinking")
           | {has_thinking: (.thinking | length), sig_len: (.signature | length)}'

{
  "has_thinking": 142,
  "sig_len": 612
}

여기서 핵심은 thinking 필드에 든 142자는 요약된 사고 과정이고, signature 필드의 612자가 실제 암호화된 원문이라는 점이다. signature를 그대로 출력해보면 이런 식이다:

$ cat *.jsonl | jq -r 'select(.message.content[]?.type=="thinking")
  | .message.content[] | select(.type=="thinking") | .signature' | head -c 200

EuYBCkYIBxgCKkBxJ9... (이하 사람이 읽을 수 없는 인코딩 문자열)

이걸 base64로 디코딩해봐야 의미 있는 텍스트가 안 나온다. Anthropic의 키로 암호화돼 있으니 당연한 결과다.

jpeg 비유는 사실 거꾸로다

원문에서는 이 과정을 "jpeg를 bmp로 저장했다가 다시 jpeg로 내보내는 것 같은 데이터 손실"에 비유했는데, HN 댓글에서 정확히 지적했듯 비유 방향이 반대다. bmp는 무손실, jpeg가 손실 포맷이니까. 핵심만 가져가자면 "요약 과정에서 비가역적인 정보 손실이 일어난다"는 것이다. 요약본을 아무리 들여다봐도 원본 추론을 복원할 수 없다.

그리고 이건 Anthropic만의 문제가 아니다. HN 댓글에서 여러 사람이 짚었듯 OpenAI, Google도 동일하게 원시 추론(raw CoT)을 숨긴다. 이유는 대체로 두 가지로 정리된다: (1) 증류(distillation) 방지 — 경쟁사가 사고 사슬을 학습 데이터로 가져가 모델을 복제하는 걸 막으려는 것, (2) 정렬되지 않은 중간 출력 노출 방지. 한 댓글은 "1~2월쯤 변경됐고 이유가 명시적으로 증류 방지였다"고 회고했다(버전·시점은 댓글 회고라 공식 확인 필요).

3. 실무 관점 — 도입 전에 반드시 짚을 것

감사 로그로 쓸 거면 전제부터 바꿔라

가장 중요한 결론부터. Claude Code 세션 파일만으로는 에이전트의 실제 논리를 재현할 수 없다. 감사 추적이 필요한 환경(금융, 의료, 공공)에서 "AI가 이 결정을 내린 근거"를 제출해야 한다면, 로컬 로그에 있는 thinking 요약은 법적/규제적으로 "근거"가 되기 어렵다. 실제 추론은 암호화돼 있고 키는 당신에게 없으니까.

그래서 감사가 필요하면 추론 자체가 아니라 관찰 가능한 행동을 별도로 로깅해야 한다. 입력, 출력, 실제 파일 변경, 실행된 명령어 같은 것들. 예를 들어 git을 활용해 에이전트가 만든 모든 변경을 강제로 추적하는 식이다:

# 에이전트 작업 전후를 별도 브랜치/커밋으로 강제 스냅샷
$ git add -A && git commit -m "pre-agent snapshot" --allow-empty

# Claude Code 실행 후
$ git diff HEAD~1 --stat

 src/auth/login.ts | 23 +++++++++++++----------
 src/config/db.ts  |  8 ++++----
 2 files changed, 18 insertions(+), 13 deletions(-)

추론은 못 가져와도 "무엇을 바꿨는가"는 이렇게 100% 재현 가능하게 남길 수 있다. 감사 관점에서는 사실 이게 추론 텍스트보다 더 확실한 증거다.

흔한 함정 — signature 재사용 에러

API를 직접 다루면서 thinking block을 멀티턴 대화에 그대로 끼워 넣을 때 자주 만나는 함정이 있다. signature 검증에 실패하는 케이스다. 모델 버전이 바뀌었거나, thinking 텍스트를 임의로 잘라서 보내거나, signature를 누락하면 이런 에러가 난다:

{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "messages.0.content.0.thinking: Input tag 'thinking' found using 'type'
                does not match any of the expected signatures, or the thinking block
                signature could not be verified."
  }
}

이게 뜨는 전형적인 이유:

  • thinking block의 text만 보내고 signature 필드를 빼먹은 경우. 둘은 세트라 같이 보내야 한다.
  • thinking 내용을 후처리(트리밍, 마스킹)해서 보내면 signature와 불일치해 검증 실패.
  • 이전 턴을 다른 모델 버전으로 생성한 뒤 현재 턴에서 다른 버전으로 이어붙인 경우.

핵심 교훈: thinking block은 당신이 편집할 수 있는 데이터가 아니다. 받은 그대로 보존해서 다음 턴에 돌려줘야만 한다. 로그를 가공·익명화하려다 이 에러를 만나는 경우가 의외로 많다.

보안 관점 — 숨겨진 추론의 리스크

HN 댓글에서 나온 우려 중 실무적으로 새겨들을 만한 게 있다. 프롬프트 주입으로 추론 사슬에 비밀 목표를 심으면, 그게 요약과 최종 출력에서는 가려질 수 있다는 점이다. 사용자에게 보이는 요약은 멀쩡한데 내부 추론에선 다른 의도가 작동할 가능성을 검증할 방법이 없다.

다만 같은 댓글 스레드에서 반론도 나왔다. "도구 호출(tool call)은 어차피 클라이언트가 실행해야 하니 숨겨질 수 없다"는 것. 즉 추론은 가려져도 실제 실행되는 명령은 클라이언트에 노출된다. 그래서 도구 호출 레벨에서 화이트리스트/감사를 거는 게 현실적인 방어선이다. 추론을 못 보는 대신, 실행 권한을 좁히는 쪽으로 가는 게 맞다.

대안 — 신뢰 모델이 다른 선택지

  • 요약 제한이 없는 모델 선택: 원문 댓글에 따르면 Sonnet 계열은 이 제한이 없다는 언급이 있다(공식 확인 필요). 워크로드에 따라 모델을 나눠 쓰는 것도 방법이다.
  • 명시적 CoT 프롬프트로 우회: thinking 기능을 끄고 일반 프롬프트 안에 "답하기 전에 단계별로 생각해라"를 넣으면 GPT-3 시절처럼 사고 과정이 본문에 그대로 노출된다는 회고가 있다. 추론을 본문 텍스트로 받으니 로깅·감사가 쉬워진다. 대신 모델 품질과 토큰 비용은 트레이드오프다.
  • 오픈 가중치 모델: DeepSeek, GLM 등은 사고 사슬이 그대로 노출된다(읽으면 "이게 무슨 소리야" 싶은 경우도 많지만). 데이터 주권이 절대 우선인 온프레미스 환경이면 고려 대상이다.
  • enterprise agreement: 전체 thinking output이 정말 필요하면 결국 이 길이다. 비용·계약 협상이 필요하다.

4. 정리

한 줄 요약: Claude Code의 thinking block에 보이는 건 암호화된 추론의 "요약본"일 뿐, 모델 행동을 실제로 구동한 추론 원문이 아니다. 키는 Anthropic에 있고 당신에겐 없다.

누가 언제 신경 써야 하나:

  • 컴플라이언스/감사가 필요한 팀: 추론 텍스트를 증거로 쓸 생각 버리고, git diff·명령 실행 로그 같은 관찰 가능한 행동을 별도로 강제 로깅하라.
  • API를 직접 통합하는 백엔드: thinking block은 절대 가공하지 말고 받은 그대로 보존·재전송하라. 안 그러면 signature 검증 에러를 만난다.
  • 온프레미스/데이터 주권 우선 환경: 추론이 외부 키로 암호화돼 재현 불가라는 점을 도입 결정 전에 명확히 인지하고, 필요하면 오픈 가중치 모델을 저울질하라.

일반적인 사이드 프로젝트나 사내 도구 수준이면 솔직히 크게 문제될 일은 아니다. 하지만 "AI가 왜 그렇게 했는지를 증명해야 하는" 순간이 오는 조직이라면, 도입 전에 이 신뢰 모델을 팀과 공유해두는 게 나중에 곤란해지지 않는 길이다.

참고 자료

※ 본문의 경로, 버전, 시점 관련 일부 내용은 원문 댓글 회고나 환경별 차이가 있을 수 있어 실제 적용 전 공식 문서 확인을 권장한다.

728x90
728x90

며칠 전 사내 슬랙에 "내 노트북 메모리가 켜기만 해도 60% 넘게 먹는데 뭐가 문제냐"는 질문이 올라왔다. 범인은 Claude Desktop이었다. 채팅 한 줄 안 쳤는데도 Task Manager에 Vmmem 프로세스가 1.8GB를 잡아먹고 있었다. 이게 단순 버그가 아니라 데스크톱 AI 툴의 샌드박스 아키텍처가 어떻게 굴러가는지 보여주는 좋은 케이스라서, 실무 관점에서 한번 정리해본다.

1. 왜 지금 화제인가 — 채팅만 쓰는데 VM이 뜬다

이슈의 핵심은 단순하다. Claude Desktop Windows 앱이 Cowork(agent mode)를 한 번이라도 쓴 뒤로는, 그냥 채팅만 하려고 앱을 열어도 매번 Hyper-V VM을 띄운다는 거다. 재현 환경은 다음과 같이 보고됐다.

  • Windows 11 Pro 25H2 (Build 26200.7840)
  • Hyper-V, WSL, Docker, Windows Sandbox 전부 비활성화 상태
  • 단, VirtualMachinePlatform 기능은 켜져 있음
  • Core Isolation / Memory Integrity도 꺼져 있는 상태

여기서 재밌는 포인트는, 보고자가 wsl --shutdown을 쳐도 "not installed"가 나오고 Get-VM은 실패하는데도 VM이 뜬다는 점이다. 즉 사용자가 흔히 아는 Hyper-V 관리 인터페이스로는 잡히지 않는 경로로 VM을 생성한다는 뜻이다.

왜 VM을 띄우느냐 자체는 명확하다. Cowork/agent mode는 Claude가 사용자 머신에서 실제로 명령을 실행하고 파일을 건드리는 기능인데, 호스트를 직접 건드리게 하면 위험하니까 격리된 샌드박스 안에서 돌린다. MCP(Model Context Protocol) 기반 툴이 코드를 실행할 때 호스트 오염을 막으려는 설계다. 이 방향성 자체는 맞다. 문제는 채팅 전용으로 쓸 때도 VM을 미리 띄우고, 끌 방법을 안 준다는 것이다.

2. 동작 원리 — VirtualMachinePlatform과 Vmmem의 정체

먼저 용어 정리부터 하자. 현장에서 Hyper-V랑 VirtualMachinePlatform을 같은 거라고 착각하는 경우가 많은데 다르다.

  • Hyper-V Platform: 전통적인 Type-1 하이퍼바이저. Get-VM, Hyper-V Manager로 관리하는 그 풀스택 가상화 기능이다.
  • VirtualMachinePlatform (VMP): WSL2, Windows Sandbox, Docker Desktop 등이 쓰는 경량 유틸리티 VM 기반 기능. Hyper-V Manager에는 안 잡히지만 내부적으로는 같은 가상화 스택(vmcompute)을 쓴다.

이게 핵심이다. 보고자가 Hyper-V를 껐는데도 VM이 뜬 이유는, Claude가 Hyper-V가 아니라 VMP의 vmcompute(Host Compute Service)를 직접 트리거하기 때문이다. 보고 내용을 보면 프로세스 트리가 이렇게 나온다.

Claude Desktop
  └─ RPC interface event → vmcompute (Host Compute Service)
       └─ vmwp.exe (VM Worker Process, 부모: services.exe)
            └─ Vmmem (게스트 메모리 영역, ~1,796~1,846MB)

Vmmem은 별도 프로그램이 아니라 VM 게스트에 할당된 메모리를 호스트 Task Manager에서 보여주기 위한 가상 프로세스다. WSL2를 써본 사람이면 익숙할 거다. WSL2 게스트가 메모리를 먹으면 그게 Vmmem으로 잡힌다. 즉 Claude가 띄운 유틸리티 VM의 메모리 풋프린트가 Vmmem 1.8GB로 나타나는 것이다.

비유하자면, 컨테이너 하나만 돌리려고 Docker Desktop을 켰는데 컨테이너를 다 지워도 백그라운드 LinuxKit VM은 계속 메모리를 잡고 있는 상황과 똑같다. 다른 점은 Docker는 "내가 VM 띄웠다"고 명시적으로 알려주는데, Claude는 채팅만 쓰는 사용자한테 아무 안내 없이 조용히 띄운다는 점이다.

3. 실무 관점 — 측정, 흔한 함정, 대응 전략

실제로 뭐가 도는지 확인하기

먼저 본인 머신에서 진짜 VMP VM이 떠 있는지 확인하는 명령어다. 관리자 PowerShell에서 실행한다.

PS C:\> Get-Process vmwp, vmcompute -ErrorAction SilentlyContinue |
>>   Select-Object Name, Id, @{N='RAM(MB)';E={[math]::Round($_.WorkingSet64/1MB,0)}}

Name        Id  RAM(MB)
----        --  -------
vmcompute  4820       18
vmwp       9132     1812

vmwp가 1800MB 근처를 잡고 있으면 Claude가 띄운 유틸리티 VM이 살아있다는 신호다. Get-VM이 실패하는 환경이라도 이 프로세스는 잡힌다. Hyper-V 관리 cmdlet과는 다른 레이어라는 점을 다시 강조해둔다.

VMP 기능 자체가 켜져 있는지는 이렇게 본다.

PS C:\> Get-WindowsOptionalFeature -Online -FeatureName VirtualMachinePlatform |
>>   Select-Object FeatureName, State

FeatureName             State
-----------             -----
VirtualMachinePlatform Enabled

흔한 함정 — 세션 파일 누적과 JSON 에러

보고에서 주목할 부분이 두 가지 있다. 첫째, %APPDATA%\Claude\local-agent-mode-sessions\ 안에 이전 Cowork 세션 파일이 2,689개 쌓여 있었다는 점이다. 세션 종료 후 정리 로직이 없다는 뜻이다. 더 황당한 건 이 파일들을 다 지우고 VM 프로세스를 죽여도, 앱을 다시 켜면 VM과 1.8GB Vmmem이 즉시 다시 생긴다는 점이다. 세션 파일이 원인이 아니라 앱 자체가 시작 시 무조건 VM을 띄우는 구조라는 거다.

둘째, Hyper-V Compute Admin 로그(이벤트 뷰어 → Applications and Services Logs → Microsoft → Windows → Hyper-V-Compute)에 부팅·앱 실행 때마다 이런 에러가 반복된다.

The specified property query is invalid:
The virtual machine or container JSON document is invalid.
(0xC037010D, 'Invalid JSON document '$'')

0xC037010D 에러는 vmcompute에 넘긴 VM 정의 JSON이 깨졌을 때 나는 거다. 빈 $ 문자열을 JSON으로 던진 흔적인데, 이게 매 실행마다 찍히는 걸 보면 VM 초기화 로직이 정상 경로를 안 타고 있을 가능성이 높다. 이벤트 로그가 이런 에러로 도배되면 모니터링 알림 노이즈가 늘어나니, 사내 SIEM에 Hyper-V-Compute 로그를 물려둔 곳이라면 미리 필터링 룰을 잡아두는 게 좋다.

리소스 영향

보고 기준으로 16GB 시스템에서 유휴 메모리 사용량이 약 50%에서 62%로 올라갔고, 일반 앱 부하가 겹치면 70~75%까지 치솟았다. 16GB 노트북에서 1.8GB를 상시 떼이는 건 무시 못 할 수준이다. 특히 VDI나 회의실 공용 PC처럼 메모리가 빠듯한 환경에서는 체감이 크다. (디스크의 경우 macOS에서는 약 10GB VM 번들을 만든다는 별도 보고가 있는데, Windows 쪽 디스크 사용량 수치는 본 보고에 명시돼 있지 않다. 환경별 확인이 필요하다.)

대응 전략

(1) Cowork를 안 쓴다면 — VMP 자체를 끈다

PS C:\> Disable-WindowsOptionalFeature -Online `
>>   -FeatureName VirtualMachinePlatform -NoRestart

Path          :
Online        : True
RestartNeeded : Possible

가장 확실한 방법이지만 부작용이 있다. WSL2, Docker Desktop, Windows Sandbox도 같이 못 쓰게 된다. 개발 머신에서 WSL2를 쓰고 있다면 이 옵션은 못 쓴다. 그리고 당연히 Cowork 기능도 비활성화된다.

(2) VMP는 살리되 VM만 매번 죽이기

PS C:\> Stop-Process -Name vmwp -Force
PS C:\> Stop-Process -Name vmcompute -Force

이렇게 죽여도 채팅 기능은 정상 동작한다. 다만 앱 재실행 때마다 다시 떠서 반복 작업이 된다. 매번 손으로 치기 귀찮으면 작업 스케줄러에 등록하거나, Claude 실행을 감싸는 래퍼 스크립트로 후처리하는 식으로 자동화할 수 있다. 단 vmcompute는 다른 가상화 기능도 공유하는 서비스라, WSL2 등을 같이 쓰는 머신에서는 vmcompute까지 죽이면 다른 VM도 영향을 받을 수 있으니 주의해야 한다.

(3) 격리 VM 안에서 Claude를 돌린다

HN 댓글에서 가장 깔끔한 해법으로 언급된 방식이다. Claude Desktop을 Windows Sandbox나 별도 Hyper-V VM 안에서 돌리고, 그 게스트 VM 안에는 VirtualMachinePlatform을 설치하지 않는 것이다. 그러면 Claude가 VMP가 없는 걸 감지하고 Cowork 탭을 그냥 비활성화한다. 실제로 "VMP가 전혀 설치 안 된 VM에서 돌리니 앱이 이를 받아들이고 Cowork를 비활성화하더라"는 보고가 있다.

다만 이건 트레이드오프가 있다. 기업 환경에서 VM 안에서 도구를 돌리면 관측 가능성(observability)이 떨어진다. 플랫폼 담당자나 보안팀 입장에선 사용자 수준 텔레메트리와 샌드박스 내부 로그가 분리되는 게 골치 아픈 지점이다. EDR(Defender, CrowdStrike 등)이 게스트 내부를 어떻게 볼지도 따로 정책을 잡아야 한다.

(4) 그냥 웹/PWA를 쓴다

냉정하게 말하면, 컴퓨터에 아무것도 접근시키지 않고 채팅만 할 거라면 데스크톱 앱을 쓸 이유가 별로 없다. HN에서도 "빠른 질문은 Claude 웹 앱을 PWA로 고정해서 쓰고, 프로젝트 작업은 CLI를 쓴다"는 운영 패턴이 여러 번 언급됐다. 채팅 전용 사용자라면 PWA가 메모리 풋프린트 측면에서 압도적으로 가볍다.

4. 정리 — 누가 언제 신경 써야 하나

한 줄 요약: Claude Desktop은 Cowork를 한 번 쓰면 그 뒤로는 채팅만 해도 시작 시 VMP 기반 유틸리티 VM을 띄워 Vmmem으로 ~1.8GB를 상시 점유하며, 현재 공식적으로 이걸 끌 토글은 없다.

  • WSL2/Docker를 안 쓰고 채팅 위주라면: VMP를 끄거나 그냥 PWA로 갈아타라. 제일 깔끔하다.
  • WSL2/Docker를 같이 쓰는 개발 머신이라면: VMP를 못 끄니, vmwp 종료 자동화나 격리 VM 방식을 고려하라.
  • VDI·공용 PC·메모리 빠듯한 환경을 운영한다면: 배포 정책에서 Claude Desktop을 통제하거나, 표준 이미지에서 Cowork 사용을 막는 가이드를 미리 만들어둬라. 1.8GB 상시 점유는 동시 세션 밀도에 직접 영향을 준다.

방향성 자체는 데스크톱 AI 툴의 자연스러운 흐름이다. 에이전트가 호스트에서 명령을 실행하려면 샌드박스가 필수고, 앞으로 나올 도구들도 대부분 비슷한 구조를 갖게 될 거다. 다만 이번 케이스의 진짜 교훈은 "기능을 안 쓰는 사용자에게는 비용을 물리지 말라"는 기본 원칙이다. 요청된 동작도 결국 "Cowork가 실제로 요청될 때만 VM을 초기화하고, 세션 종료 후 정리하고, 필요 없으면 채팅 전용 모드로 가라"는 지극히 상식적인 내용이다. 향후 버전에서 lazy initialization이 들어가는지 릴리스 노트를 지켜볼 필요가 있다.

참고 자료

728x90

+ Recent posts