며칠 전 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

며칠 전 사내 슬랙에 "내 노트북 메모리가 켜기만 해도 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