블로그

  • 처음 켜면 나오는 ‘이 폴더를 신뢰하시겠습니까’ — 엔터만 치면 벌어지는 일

    처음 켜면 나오는 ‘이 폴더를 신뢰하시겠습니까’ — 엔터만 치면 벌어지는 일

    설치를 넘기고 claude를 처음 실행하면, 작업 화면이 아니라 질문 하나가 먼저 뜬다. 이 폴더의 파일을 신뢰하겠느냐고 묻는 화면이다. 여기서 습관적으로 엔터를 치면 안 된다. 우리가 실측한 화면에서는 커서가 No, exit에 놓여 있었고, 그 상태의 엔터는 신뢰가 아니라 종료를 선택했다. 도구가 켜지자마자 조용히 닫히는 것처럼 보이는 이유가 이것이다.

    먼저 밝혀 둔다. 커서가 어디에 놓이는지는 사람마다 다를 수 있다. 아래 2번 항목에 우리 환경의 조건을 적었으니, 외우지 말고 자기 화면에서 직접 확인하기를 권한다. 이 글에서 공식 문서에서 확인한 내용과 우리가 직접 측정한 내용은 항목마다 구분해 표시했다.

    첫 실행 화면의 두 관문과 우리 실측값을 정리한 도식. 폴더 신뢰 관문의 커서 기본값이 No, exit 였고 그 아래에 Yes, I trust this folder 항목이 있으며, 면책 창도 기본값이 같았다는 실측, feed 원장 82건 중 first_run_gate 24건이라는 집계, agents.json 의 measured_on 2.1.241 과 설치본 2.1.263 의 불일치를 함께 담았다.
    엔터를 치면 신뢰가 아니라 종료가 눌린 배치를 우리가 실측했다 — 2026-09-08. 버튼 라벨과 커서 위치만 옮겼고, 기본값은 환경에 따라 다를 수 있다.

    신뢰 화면에서 멈춘 지점

    1. 켜자마자 폴더를 신뢰하겠느냐는 화면이 떴다

    증상. 설치는 끝났고 claude를 실행했는데, 곧바로 이런 줄이 뜬다.

    
    Do you trust the files in this folder?
    

    왜 그런가. 고장이 아니라 설계다. 공식 보안 문서에 이 절차가 적혀 있다. 문서는 안전장치 목록에서 Trust verification을 이렇게 설명한다. “First-time codebase runs and new MCP servers require trust verification.” 처음 여는 코드베이스와 새로 붙이는 MCP 서버는 신뢰 확인을 거친다는 뜻이다.

    즉 이 화면은 그 폴더에서 처음 실행할 때 한 번 물어보는 관문이다. 놀랄 일이 아니고, 답을 하면 지나간다.

    확인 명령. 지금 어느 폴더에서 켰는지부터 본다. 질문의 대상이 그 폴더다.

    
    pwd
    

    확인 방법. 답을 하고 나면 평소의 입력 프롬프트가 나온다. 화면이 작업 상태로 넘어가면 관문을 지난 것이다.

    2. 무엇을 눌러야 하는지 모르겠다 — 커서 위치를 직접 확인하라

    이 항목은 우리가 직접 측정한 것이다. 공식 문서에는 이 화면의 항목 순서나 커서 기본값이 적혀 있지 않다.

    증상. 화면에 선택 항목이 두 개 있는데 어느 쪽이 선택된 상태인지 눈에 잘 들어오지 않는다. 보통은 엔터가 안전한 쪽에 놓여 있다고 기대한다.

    우리가 측정한 것. 2026년 9월 8일, 우리 작업 폴더에서 여러 좌석을 기동하면서 화면을 그대로 읽었다. 커서는 No, exit에 놓여 있었고, 그 아래에 Yes, I trust this folder 항목이 있었다. 우리 작업 기록에는 그 절차가 이렇게 남아 있다. 화면을 읽어 커서가 No, exit에 있는 것을 확인하고, 아래로 옮겨 Yes, I trust this folder에 커서가 있는 것을 다시 확인한 뒤 확정했다. 맹목적으로 엔터를 치지 않았다.

    중요한 조건 — 이것은 우리 환경의 배치다. 우리 작업 폴더의 설정 파일에는 도구 권한 두 건이 미리 허용돼 있었다. 실제 값은 다음 두 줄이다.

    
    "permissions": { "allow": [ "Bash(cys send-key:*)", "Bash(cys read-screen:*)" ] }
    

    우리는 이 사전 허용이 있는 폴더에서 위 배치를 봤다. 다만 사전 허용이 커서 기본값을 그렇게 만든 원인인지는 우리가 프로그램 코드로 확인한 것이 아니다. 조건과 관찰을 함께 적어 두는 것이 정확하다. 그러니 사전 허용이 없는 폴더에서는 기본값이 다를 수 있다.

    처방. 기본값을 외우지 마라. 화면에서 지금 커서가 어느 항목에 있는지 보고, 신뢰하겠다는 항목으로 옮긴 뒤 확정한다. 방향키 위아래로 옮기고, 항목 앞의 표시가 옮겨간 것을 눈으로 확인한 다음 엔터를 누른다. 순서를 지키면 기본값이 무엇이든 안전하다.

    확인 방법. 확정한 뒤 화면이 입력 프롬프트로 넘어가는지 본다. 창이 닫히면 종료 쪽을 누른 것이니 다시 켜서 4번 항목대로 하면 된다.

    덧붙이면, 항목 문면은 도구마다 다르다. 우리가 같은 컴퓨터에서 측정한 다른 CLI 는 항목이 1. Yes, continue2. No, quit 두 개였다. 라벨을 외워 두고 위치로 누르는 습관이 위험한 이유다.

    3. 하나를 통과했는데 또 물어본다

    이 항목도 우리 실측이다.

    증상. 폴더 신뢰에 답했는데 화면이 작업 상태로 가지 않고 비슷한 선택 화면이 한 번 더 뜬다.

    우리가 측정한 것. 관문은 하나가 아니었다. 우리 기록에는 한 좌석을 기동하면서 폴더 신뢰 관문과 Bypass Permissions 면책 관문 두 건을 연달아 통과시킨 것으로 남아 있다. 그리고 우리 어댑터 설정 파일의 설명문에는, 통과 직후 화면을 다시 관문으로 잘못 읽으면 그 다음 엔터가 면책 창의 No, exit를 누른다는 사고 형태가 적혀 있다. 즉 두 번째 창에서도 엔터가 종료 쪽일 수 있다.

    처방. 한 번 통과했다고 방심하지 말고, 두 번째 화면에서도 커서 위치를 같은 방식으로 확인한다. 두 화면은 묻는 내용이 다르다. 첫 번째는 이 폴더를 신뢰하겠느냐이고, 두 번째는 권한 확인을 건너뛰는 모드에 대한 면책이다.

    확인 방법. 두 화면을 모두 지나면 입력 프롬프트가 나온다. 중간에서 닫히면 두 번째 창에서 종료를 누른 것이다.

    4. 엔터를 쳤더니 그냥 닫혔다

    증상. 무언가 눌렀는데 도구가 종료됐다. 오류 메시지도 없다. 설치가 잘못된 것처럼 느껴진다.

    왜 그런가. 종료 항목이 선택된 상태에서 확정했기 때문이다. 우리 실측 기준으로 그 항목의 라벨이 No, exit다. 이름 그대로 신뢰하지 않고 나가는 선택이다. 설치나 로그인이 깨진 것이 아니다.

    처방. 다시 켜면 된다. 신뢰 질문은 답을 하지 않은 상태로 남아 있으므로 같은 화면이 다시 뜬다. 이번에는 2번 항목의 순서대로 커서를 옮겨 확정한다.

    
    claude
    

    확인 방법. 우리 기록에서는 관문을 통과한 좌석에 온보딩 완료 표시가 남아 재기동 때 다시 막히지 않았다. 우리 환경에서 확인한 값은 hasCompletedOnboarding=true였다. 다시 켰을 때 이 화면이 더 나오지 않으면 통과가 저장된 것이다. 매번 다시 물어보는 경우는 5번 항목을 보라.

    5. 켤 때마다 매번 다시 물어본다

    증상. 분명히 신뢰한다고 답했는데 다음에 켜면 또 묻는다.

    왜 그런가. 공식 보안 문서가 이 상황을 직접 설명한다. 홈 디렉터리에서 바로 실행한 경우다. 문서 표현은 이렇다. “When you start Claude Code directly in your home directory, trust acceptance is held for the current session only and is not written to disk, so the prompt reappears on each launch. There is no setting to persist it. Start Claude Code from a project subdirectory instead, where trust acceptance is saved per directory.”

    정리하면, 홈 디렉터리에서 켜면 신뢰 승인이 그 세션에만 유지되고 디스크에 기록되지 않아 켤 때마다 다시 묻는다. 이를 유지시키는 설정은 없다고 문서가 명시한다. 대신 프로젝트 하위 폴더에서 켜면 폴더 단위로 저장된다.

    확인 명령. 지금 위치가 홈 디렉터리인지 본다. 아래 두 값이 같으면 홈에서 켠 것이다.

    
    pwd
    echo $HOME
    

    처방. 작업할 프로젝트 폴더를 만들고 그 안에서 켠다.

    
    mkdir my-project
    cd my-project
    claude
    

    확인 방법. 그 폴더에서 한 번 신뢰한 뒤 도구를 닫고 다시 켠다. 같은 폴더에서 질문이 다시 뜨지 않으면 저장된 것이다.

    참고로 같은 문서는 비대화형 실행에 대해서도 적어 두었다. “Trust verification is disabled when running non-interactively with the -p flag.” 즉 -p 옵션으로 비대화형으로 돌릴 때는 이 확인 절차가 동작하지 않는다.

    6. 신뢰한다고 하면 무엇을 허용하는 것인가

    증상. 신뢰하겠다고 누르기가 망설여진다. 무엇을 허락하는 것인지 모르겠다.

    공식 문서가 말하는 범위. 신뢰가 곧 모든 권한을 주는 것은 아니다. 문서는 기본 동작을 이렇게 설명한다. 수동 모드에서 Claude Code 는 읽기 전용 권한으로 시작하고, 파일을 고치거나 명령을 실행해야 할 때 사람에게 먼저 물어본다. 원문은 “In Manual mode, Claude Code starts with read-only permissions. When Claude Code needs to edit files, run tests, or execute commands, it asks you first, and you choose whether to approve the action once or allow it from then on.”

    작업 범위에도 경계가 있다. 문서 설명으로 수동 모드에서는 실행한 폴더와 그 하위 폴더에만 쓸 수 있고, 상위 폴더의 파일은 명시적 허락 없이 고치지 못한다. 원문은 “In Manual mode, Claude Code can only write to the folder where it was started and its subfolders, and can’t modify files in parent directories without explicit permission.”

    문서는 책임 소재도 분명히 적는다. “Claude Code only has the permissions you grant it. You’re responsible for reviewing proposed code and commands for safety before approval.”

    처방. 내가 만든 프로젝트 폴더라면 신뢰해도 된다. 내려받은 남의 코드나 출처가 불확실한 폴더라면, 그 폴더에서 켜기 전에 내용을 먼저 살펴보는 편이 낫다. 신뢰 질문은 그 판단을 사람에게 넘기는 지점이다.

    확인 방법. 통과한 뒤에도 파일을 고치는 작업에서 확인을 물어오는지 본다. 물어온다면 수동 모드의 기본 동작이 살아 있는 것이다.

    7. 우리 시스템에서는 이것이 최다 장애였다

    여기서부터는 여러 대의 에이전트를 동시에 돌리는 우리 운영 환경의 이야기다. 혼자 도구를 쓰는 독자에게 그대로 적용되는 내용은 아니지만, 이 관문을 가볍게 보면 어떤 일이 생기는지 보여주는 실제 기록이다.

    빈도. 우리 승인 원장을 전수로 집계했다. 2026-09-08 시점에 원장 항목은 82건이었고, 그 가운데 첫 실행 관문 감지가 first_run_gate 24건으로 가장 많았다. 두 번째는 부트스트랩 실패 20건, 세 번째는 승인 대기 18건이었다. 우리 시스템에서 가장 자주 발생한 항목이 바로 이 관문이다.

    실제로 벌어진 일. 기록에 남은 형상은 이렇다. 한 좌석은 관문 화면에서 답을 받지 못해 계속 대기했다. 다른 좌석은 로그를 한 줄도 남기지 않고 사라졌다. 부서장 좌석은 첫 기동에서 관문 두 건을 통과해야 했다. 다른 부서의 검토 좌석에서도 같은 형상이 나왔다. 통과는 사람이 화면을 읽고 커서를 옮겨 손으로 처리했다.

    자동으로 통과시키는 장치가 있었는데 멈춰 있었다. 우리는 이 관문을 자동 확인하는 설정을 갖고 있다. 그런데 그 설정에는 안전장치가 붙어 있다. 어댑터 설정 파일 agents.json의 설명문에 계약이 적혀 있고, 문면은 이렇다. measured_on은 이 선언을 실측한 claude 버전이며, 정본과 다르면 그 관문의 통과 액션은 보류되고 감지만 한다.

    실제 값을 나란히 놓으면 이렇다.

    
    agents.json  claude.first_run_gates.measured_on = 2.1.241
    설치본        claude --version                   = 2.1.263 (Claude Code)
    agents.json  claude.first_run_gates.gates       = []   (override 선언 없음)
    

    선언을 실측한 버전과 설치된 버전이 달랐다. 계약대로 통과 액션은 보류되고 감지만 이뤄졌다. 그래서 자동 통과 장치가 있는데도 좌석들이 관문 앞에 멈춰 섰다. 같은 파일에서 다른 CLI 는 measured_on 선언 자체가 없었고, 그 설치본은 codex-cli 0.153.4였다.

    이 대비가 이 글에서 가장 값진 대목이라고 생각한다. 안전장치는 제 계약대로 정확히 동작했다. 버전이 다르면 섣불리 자동 응답하지 않는다는 규칙을 지킨 것이다. 문제는 그 보류 상태를 사람이 모르고 있었다는 데 있었다.

    부수 결함 하나. 관문을 통과한 뒤에도 우리 데몬의 보류 표시가 남아 있었다. 그 표시를 지우는 별도 명령은 없고, 우리가 시도한 해소법은 듣지 않았다. 상태 신고 명령은 상태만 신고하고 관문 표시를 지우지는 않는다. 다만 정직하게 덧붙이면, 우리 점검기는 이후 각성 시각을 먼저 보도록 바뀌어서 표시가 남아 있어도 생존 판정이 틀리지 않게 됐다. 그래도 원칙은 그대로다. 화면을 직접 읽은 결과가 판정의 정본이다.

    정리 — 확인할 것 네 가지

    첫째, 이 화면은 고장이 아니다. 공식 문서에 적힌 첫 실행 신뢰 확인 절차다. 둘째, 기본값을 외우지 말고 지금 커서가 어느 항목에 있는지 화면에서 직접 본다. 우리 환경에서는 그 자리가 종료 쪽이었다. 셋째, 관문이 한 번에 끝나지 않을 수 있으니 두 번째 화면에서도 같은 확인을 한다. 넷째, 매번 다시 묻는다면 홈 디렉터리에서 켠 경우이니 프로젝트 폴더를 만들어 그 안에서 켠다.

    습관 하나만 바꾸면 된다. 선택 화면에서 엔터를 먼저 치지 말고, 커서를 먼저 보는 것이다. 우리는 이 한 가지를 몰라서 가장 많은 장애를 만들었다.

    어디까지 공식 문서이고 어디부터 우리 실측인가

    공식 문서에서 인용한 것

    • Claude Code 공식 문서, 보안: https://code.claude.com/docs/en/security — 첫 실행 신뢰 확인(Trust verification), 홈 디렉터리에서 신뢰가 저장되지 않는 동작과 프로젝트 하위 폴더 권장, -p 비대화형 실행에서 신뢰 확인이 동작하지 않는다는 설명, 수동 모드의 읽기 전용 시작과 작업 폴더 경계, 권한에 대한 사용자 책임

    공식 문서는 이 관문이 존재하는 이유와 저장 규칙까지는 설명하지만, 선택 항목의 순서나 커서 기본값은 적지 않는다. 그 부분은 아래의 우리 실측이다.

    우리가 직접 측정한 것 (2026-09-08, 공식 문서의 내용이 아니다)

    • 커서 기본값이 No, exit였고 아래 항목이 Yes, I trust this folder였다는 것. 우리 작업 폴더에는 도구 권한 두 건이 미리 허용돼 있었다는 조건을 함께 밝힌다. 그 사전 허용이 커서 배치의 원인인지는 코드로 확인하지 않았다
    • 폴더 신뢰 다음에 Bypass Permissions 면책 관문이 한 번 더 뜨고, 그 창에서도 엔터가 종료 쪽일 수 있다는 것
    • 승인 원장 82건 중 first_run_gate가 24건으로 최다였다는 집계
    • agents.jsonmeasured_on 계약 문면과, 그 값 2.1.241이 설치본 2.1.263과 달라 자동 통과가 보류된 상태였다는 것. gates는 빈 배열이었다
    • 관문 통과 뒤에도 데몬의 보류 표시가 남고 지우는 명령이 없다는 것. 다만 점검기가 각성 시각을 우선하도록 바뀌어 생존 판정에는 영향이 없다는 정정까지 함께 적는다

    커서 기본값은 환경에 따라 다를 수 있다. 이 글의 실측값을 자기 화면의 기본값으로 가정하지 말고, 화면을 직접 읽고 확정하기를 권한다.

  • Claude Code가 설치는 됐는데 안 켜질 때 — Windows 첫 실행 오류 전부 정리

    Claude Code가 설치는 됐는데 안 켜질 때 — Windows 첫 실행 오류 전부 정리

    설치는 끝났다고 나왔는데 claude를 쳤을 때 아무 일도 일어나지 않는다면, 대부분은 프로그램이 망가진 것이 아니다. 막힌 층이 어디인지만 가려내면 처방은 거의 정해져 있다. 이 글은 Windows에서 처음 실행할 때 나오는 오류를 증상 문구별로 모아 정리한 것이다. 앞의 여섯 항목은 공식 문서에서 확인한 내용이고 링크를 함께 달았다. 뒤의 세 항목은 우리가 이 컴퓨터에서 직접 겪은 것이라 공식 문서에는 없다. 어느 쪽인지 항목마다 밝혀 두었다.

    설치는 됐는데 안 켜지는 상황을 네 층으로 나눈 도식. 설치 명령 층, 경로 층, 셸 세션 층, 정책과 인증 층의 대표 오류 문구와 처방을 나란히 놓고, 아래 띠에 우리가 직접 겪은 세 가지를 적었다.
    막히는 층은 네 곳이다 — 증상 문구로 층을 먼저 가려낸다. 아래 띠는 우리가 2026년 9월에 직접 겪은 세 가지다.

    어떤 문구가 떴는가

    지금 화면에 나온 문구와 가장 비슷한 항목으로 바로 가면 된다. 초보일 때는 원인을 몰라도 괜찮다. 증상만 알면 충분하다.

    1. claude를 쳤는데 명령을 찾을 수 없다고 나온다

    증상. 화면에 command not found: claude 또는 'claude' is not recognized가 나온다. 설치는 분명히 끝났는데 명령만 없는 것처럼 보인다.

    왜 그런가. 공식 문서는 이렇게 설명한다. 설치는 됐지만 설치 폴더가 PATH에 없다는 뜻이고, 셸은 PATH에 적힌 폴더만 뒤져서 프로그램을 찾는다. Windows에서 설치 파일이 놓이는 자리는 %USERPROFILE%\.local\bin\claude.exe다.

    확인 명령. PowerShell에서 그 폴더가 PATH에 들어 있는지 본다.

    
    $env:PATH -split ';' | Select-String '\.local\\bin'
    

    처방. 위 명령이 아무것도 출력하지 않으면 PATH에 없는 것이다. 공식 문서가 안내하는 대로 사용자 PATH에 추가한다.

    
    $currentPath = [Environment]::GetEnvironmentVariable('PATH', 'User')
    [Environment]::SetEnvironmentVariable('PATH', "$currentPath;$env:USERPROFILE\.local\bin", 'User')
    

    공식 문서는 여기에 한 줄을 덧붙인다. “Restart your terminal for the change to take effect.” 즉 창을 다시 열어야 반영된다.

    확인 방법. 새 터미널 창을 열고 claude --version을 실행한다. 공식 문서 기준으로 정상 설치라면 2.1.211 (Claude Code)처럼 버전 번호가 찍힌다.

    한 가지 주의할 점이 문서에 따로 적혀 있다. VS Code 확장만 설치한 경우에는 claude가 그 자리에 놓이지 않는다. 확장은 자기 채팅 창을 위해 CLI 사본을 확장 폴더 안에 따로 두고 PATH에 추가하지 않기 때문이다. 터미널에서 claude를 쓰려면 별도 설치가 필요하다.

    2. 설치는 성공했다는데 새 창에서만 되거나, 옛 버전이 나온다

    증상. 설치 로그는 성공인데 방금 설치한 그 창에서만 claude를 못 찾는다. 또는 분명히 새로 설치했는데 옛 버전 번호가 찍힌다.

    왜 그런가. 공식 문서의 설명은 짧고 분명하다. “the session you installed from keeps its old PATH.” 설치 프로그램이 PATH를 바꿔도 이미 열려 있던 창은 예전 PATH를 그대로 들고 있다. 고객지원 문서도 Windows에서는 “Close and reopen PowerShell”이라고 안내한다.

    확인 명령. 지금 창에서 실패하는지, 새 창에서도 실패하는지를 나눠서 본다.

    
    claude --version
    

    처방. 창을 완전히 닫고 새 창을 열어 같은 명령을 다시 실행한다. 이것만으로 끝나는 경우가 많다.

    확인 방법. 새 창에서 버전 번호가 찍히면 해결된 것이다. 새 창에서도 여전히 못 찾으면 1번 항목의 PATH 문제로 넘어간다.

    3. 설치 명령 자체가 오류를 낸다

    증상. Claude Code를 켜기도 전에 설치 명령이 튕긴다. 공식 문서가 열거한 문구는 이렇다.

    
    'bash' is not recognized as the name of a cmdlet
    A parameter cannot be found that matches parameter name 'fsSL'
    The token '&&' is not valid
    'irm' is not recognized
    

    왜 그런가. 공식 문서는 이 네 가지를 모두 같은 원인으로 묶는다. 다른 셸이나 다른 운영체제용 설치 명령을 복사해 온 것이다. 예를 들어 curl -fsSL ... | bash는 macOS와 Linux용인데, Windows PowerShell에서는 curlInvoke-WebRequest의 별칭이라서 -fsSL 같은 옵션을 거부한다.

    확인 명령. 내가 지금 어느 셸에 있는지부터 확인한다. 공식 문서가 알려주는 구분법은 프롬프트 모양이다. “Your prompt shows PS C:\ when you’re in PowerShell and C:\ without the PS when you’re in CMD.”

    처방. 셸에 맞는 설치 명령을 쓴다. 공식 문서에 적힌 그대로다.

    
    PowerShell:  irm https://claude.ai/install.ps1 | iex
    CMD:         curl -fsSL https://claude.ai/install.cmd -o install.cmd && install.cmd && del install.cmd
    WinGet:      winget install Anthropic.ClaudeCode
    

    설치 명령을 쳤는데 스크립트 내용만 화면에 쭉 출력되고 아무것도 설치되지 않는 경우도 있다. 공식 문서는 이것을 명령의 앞부분만 실행한 상태로 설명한다. irm https://claude.ai/install.ps1만 쓰면 내려받은 스크립트를 화면에 인쇄하고 끝난다. 실행까지 하려면 | iex를 붙여야 한다.

    확인 방법. 새 터미널을 열고 claude --version으로 버전 번호를 확인한다.

    4. Claude Code does not support 32-bit Windows가 나온다

    증상. 64비트 컴퓨터를 쓰는데도 32비트를 지원하지 않는다는 오류가 나온다.

    왜 그런가. 공식 문서의 설명은 이렇다. 시작 메뉴에는 PowerShell 항목이 두 개 있다. Windows PowerShellWindows PowerShell (x86)이다. x86 항목은 32비트 프로세스로 실행되며, 문서 표현대로 “triggers this error even on a 64-bit machine”이다. 즉 컴퓨터가 아니라 창을 잘못 연 것이다.

    확인 명령. 오류가 난 바로 그 창에서 실행한다.

    
    [Environment]::Is64BitOperatingSystem
    

    처방. True가 나오면 운영체제는 문제가 없다. 그 창을 닫고 (x86)이 붙지 않은 Windows PowerShell을 열어 설치 명령을 다시 실행한다. False가 나오면 32비트 Windows를 쓰는 것이고, 이 경우에는 64비트 운영체제가 필요하다. 공식 시스템 요건은 Windows 10 1809 이상 또는 Windows Server 2019 이상, 4GB 이상 메모리, x64 또는 ARM64 프로세서다.

    확인 방법. x86이 아닌 창에서 설치한 뒤 claude --version으로 버전 번호를 확인한다.

    5. running scripts is disabled on this system이 나온다

    증상. npm으로 설치했거나 npm으로 설치한 claude를 실행할 때 보안 오류가 난다. 공식 문서가 실은 원문은 이렇다.

    
    npm : File C:\Program Files\nodejs\npm.ps1 cannot be loaded because running scripts
    is disabled on this system.
        + CategoryInfo          : SecurityError: (:) [], PSSecurityException
    

    원문 메시지 끝에는 마이크로소프트의 실행 정책 문서를 가리키는 주소가 한 줄 더 붙는다. 위 인용에서는 그 주소만 덜어냈고, 같은 문서를 이 글 맨 아래 출처에 링크해 두었다.

    왜 그런가. 공식 문서는 원인을 이렇게 짚는다. PowerShell의 실행 정책이 npm이 만들어 놓은 .ps1 실행 스크립트를 막고 있는 것이다. 같은 오류가 claude.ps1 이름으로도 나온다. 문서는 이 정책이 스크립트 파일에만 적용되므로 PowerShell 설치 명령 irm https://claude.ai/install.ps1 | iex에는 영향을 주지 않는다고 덧붙인다. 내려받은 글자를 바로 실행하는 방식이라 그렇다.

    실행 정책이 무엇인지는 마이크로소프트 공식 문서에 정의돼 있다. 문서는 실행 정책을 “a safety feature that controls the conditions under which PowerShell loads configuration files and runs scripts”라고 설명한다. 이 글에서 쓰는 RemoteSigned는 문서 표현으로 “Requires a digital signature from a trusted publisher on scripts and configuration files that are downloaded from the internet”이고 “Doesn’t require digital signatures on scripts that are written on the local computer and not downloaded from the internet”이다. 즉 인터넷에서 받은 스크립트만 서명을 요구하고, 내 컴퓨터에서 만들어진 스크립트는 그대로 실행한다.

    한 가지 헷갈리기 쉬운 대목이 있어 그대로 옮긴다. 같은 문서는 RemoteSigned를 “The default execution policy for Windows computers”라고 적으면서, 동시에 “If no execution policy is set in any scope, the effective execution policy is Restricted, which is the default for Windows clients”라고도 적는다. 두 문장이 함께 있으니, 내 컴퓨터가 지금 어떤 상태인지는 추측하지 말고 직접 확인하는 편이 정확하다.

    확인 명령. 현재 적용 중인 정책을 범위별로 본다.

    
    Get-ExecutionPolicy -List
    

    처방. 공식 문서가 제시하는 방법은 세 가지다. 첫째, 내 계정 범위에서 로컬 스크립트를 허용한다.

    
    Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
    

    둘째, .ps1 대신 .cmd 실행 파일을 부른다. 문서 설명대로 npm.cmdclaude.cmd는 같은 일을 하고 정책 적용 대상이 아니다. 셋째, npm 대신 PowerShell 설치 명령을 쓴다. 스크립트가 아니라 실행 파일을 설치하는 방식이다. 마이크로소프트 문서는 Set-ExecutionPolicy의 변경이 즉시 적용되며 PowerShell을 다시 시작할 필요가 없다고 적는다.

    확인 방법. Get-ExecutionPolicy -Scope CurrentUserRemoteSigned를 돌려주는지 보고, 이어서 claude --version으로 버전 번호를 확인한다.

    6. 켜지기는 하는데 로그인이 안 된다

    증상. claude는 켜지는데 로그인 단계에서 멈춘다. 브라우저가 저절로 열리지 않거나, OAuth error: Invalid code가 나오거나, 로그인 뒤에 403 Forbidden이 나온다.

    먼저 확인할 것. 공식 문서는 계정 조건을 명확히 적어 두었다. Claude Code는 Pro, Max, Team, Enterprise, Console 계정이 필요하고 “The free Claude.ai plan does not include Claude Code access.” 무료 요금제로는 접근이 되지 않으니, 설치 문제로 오래 헤매기 전에 계정 종류부터 확인하는 편이 빠르다.

    확인 명령. Claude Code 안에서 현재 어떤 인증 방식이 쓰이는지 본다.

    
    /status
    

    처방. 공식 문서가 안내하는 순서는 이렇다. 원인이 분명하지 않을 때는 로그인을 깨끗하게 다시 한다. /logout으로 완전히 로그아웃하고, Claude Code를 닫고, claude로 다시 시작해 인증을 처음부터 진행한다. 브라우저가 저절로 열리지 않으면 c를 눌러 인증 주소를 복사한 뒤 브라우저에 직접 붙여 넣는다. 터미널이 좁아 주소가 여러 줄로 잘려 클릭이 안 될 때도 같은 방법을 쓴다.

    OAuth error: Invalid code. Please make sure the full code was copied가 나오는 경우, 문서는 코드가 만료됐거나 복사 과정에서 잘렸다고 설명한다. 브라우저가 열린 뒤 지체 없이 로그인을 마치는 것이 처방이다.

    로그인은 됐는데 403 Forbidden이 나오는 경우에는 환경 변수를 본다. 문서 설명대로 ANTHROPIC_API_KEY가 설정돼 있고 그 키를 승인해 둔 상태라면, Claude Code는 구독 계정의 인증 대신 그 키를 쓴다. Windows에서는 PowerShell 프로필($PROFILE)과 사용자 환경 변수에서 ANTHROPIC_API_KEY를 확인하라고 안내한다.

    확인 방법. /status로 지금 활성화된 인증 방식이 의도한 것과 같은지 확인한다.

    7. 점검 도구가 한글 콘솔에서 통째로 멈춘다

    여기서부터 세 항목은 공식 문서에 없다. 우리가 이 컴퓨터에서 직접 겪은 것이다.

    증상. 설치 상태를 점검하려고 돌린 진단 스크립트가 아래 오류를 내고 그 자리에서 죽었다. 점검 결과를 한 줄도 못 봤다.

    
    UnicodeEncodeError: 'cp949' codec can't encode character '\u2014'
    

    무엇이 문제였나. 정확히 밝혀 둔다. 죽은 것은 Claude Code 본체가 아니라 우리가 만든 파이썬 진단 스크립트였다. 원인은 프로그램 논리가 아니라 글자였다. 한국어 Windows 콘솔의 기본 인코딩은 cp949이고, 여기에는 줄표(U+2014, em dash)를 표현할 자리가 없다. 진단 결과를 출력하는 문장에 줄표가 한 글자 들어 있었고, 그 한 글자를 인코딩하는 순간 예외가 나면서 진단이 통째로 중단됐다. 설치 상태는 아무 문제가 없었다.

    확인 명령. 지금 파이썬이 어떤 인코딩으로 출력하는지 본다.

    
    python -c "import sys; print(sys.stdout.encoding)"
    

    처방. 우리가 실제로 쓴 방법은 출력 인코딩을 UTF-8로 고정하는 것이었다. 파이썬은 환경 변수로 이것을 지정할 수 있다.

    
    $env:PYTHONUTF8 = "1"
    $env:PYTHONIOENCODING = "utf-8"
    

    덧붙이자면, 겪고 나서 우리가 코드에 남긴 교훈은 다른 쪽이었다. 진단 도구의 출력 문장은 예쁜 기호를 쓰지 않는 편이 낫다는 것이다. 줄표 대신 붙임표를 쓰면 이 고장 자체가 생기지 않는다. 도구가 멈추면 정작 확인하려던 사실을 아무것도 못 보게 된다.

    확인 방법. 같은 스크립트를 다시 돌려 끝까지 출력이 나오는지 본다. 위 확인 명령이 utf-8을 돌려주면 인코딩은 잡힌 것이다.

    8. 분명히 설치했는데 자동 점검이 미설치라고 한다

    증상. 파일 탐색기로 확인하면 실행 파일이 그 자리에 분명히 있다. 그런데 우리가 만든 자동 점검이 그 CLI를 미설치로 판정했다.

    무엇이 문제였나. 실행 파일은 실재했고, 문제는 점검을 수행한 쪽의 PATH였다. 우리 자동 점검은 별도 프로세스를 띄워서 명령을 찾는데, 그 프로세스가 물려받은 PATH에 해당 폴더가 들어 있지 않았다. 그래서 파일이 있는데도 없다고 보고했다. 사람이 직접 창을 열어 실행하면 되는데 자동 점검만 실패하는 상태였고, 우리는 이 차이를 알아채기까지 시간을 썼다.

    이 오판정은 1번 항목과 뿌리가 같다. 셸이든 자동화든 프로그램을 찾는 방법은 PATH뿐이라, PATH를 물려받지 못한 프로세스에게는 그 파일이 존재하지 않는 것과 같다. 다른 점은 하나다. 1번은 사람이 오류 문구를 보고, 8번은 자동화가 조용히 잘못된 결론을 남긴다.

    확인 명령. 지금 이 창에서 찾을 수 있는지, 그리고 파일이 실재하는지를 따로 확인한다. 둘의 답이 갈리면 PATH 문제다.

    
    where.exe claude
    Test-Path "$env:USERPROFILE\.local\bin\claude.exe"
    

    처방. 파일은 있는데 where.exe가 못 찾으면 1번 항목의 PATH 추가를 적용한다. 자동화 쪽이라면 자동화가 띄우는 프로세스의 PATH에 그 폴더가 들어가는지를 따로 확인해야 한다. 사람이 쓰는 창에서만 고쳐 놓으면 자동 점검은 계속 미설치라고 답한다.

    확인 방법. where.exe claude가 경로를 출력하고, 자동 점검을 다시 돌렸을 때 같은 결론이 나오는지 본다. 사람과 자동화의 답이 일치하는 상태가 정상이다.

    9. 어제까지 되던 명령이 셸을 바꾸니 사라졌다

    증상. 잘 쓰던 CLI가 어느 날 없는 명령이 됐다. 우리 경우에는 셸을 바꿔서 새로 열었을 때였다.

    무엇이 문제였나. 정확히 적어 둔다. 이때 사라진 것은 Claude Code가 아니라, 같은 컴퓨터에 npm 전역 설치로 넣어 둔 다른 CLI였다. 그 컴퓨터에는 Node 버전 관리자(fnm)가 깔려 있었고, npm 전역 설치는 버전 관리자가 셸마다 만들어 주는 경로 아래로 들어갔다. 그 경로는 셸 세션에 딸린 임시 경로여서, 셸이 바뀌자 그대로 사라졌다. 파일이 지워진 것이 아니라 그 경로를 아무도 안 보게 된 것이다.

    처방. 우리가 쓴 해법은 셸에 딸리지 않는 안정된 절대경로에 실행 지점을 고정하는 것이었다. 구체적으로는 ~/.local/bin 아래에 작은 실행 파일을 두고 그것을 부르게 했다. 그 뒤로는 셸이 바뀌어도 같은 자리에서 실행된다.

    Claude Code에는 이 문제가 애초에 덜 생긴다. 공식 문서에 따르면 기본 설치 방식은 %USERPROFILE%\.local\bin\claude.exe라는 고정된 자리에 실행 파일을 놓기 때문이다. npm 전역 설치를 택할 때만 위와 같은 상황을 고려하면 된다. 참고로 공식 문서는 npm 설치에 대해 “Do NOT use sudo npm install -g“라고 경고하고, npm 패키지도 결국 같은 네이티브 실행 파일을 설치한다고 설명한다.

    확인 방법. 셸을 바꿔 새 창을 열고 where.exe claude가 같은 경로를 출력하는지 본다. 셸에 따라 결과가 달라지면 아직 휘발성 경로에 물려 있는 것이다.

    정리

    아홉 가지를 봤지만 실제로 층은 네 개다. 설치 명령을 셸에 맞게 골랐는가, 설치 폴더가 PATH에 있는가, 지금 창이 새 PATH를 들고 있는가, 정책과 계정이 막고 있지는 않은가. 이 네 가지를 순서대로 짚으면 대부분 끝난다.

    마지막으로 점검 명령 하나를 적어 둔다. 공식 문서는 설치와 설정을 자세히 보려면 claude doctor를 실행하라고 안내한다. 문서 설명으로는 세션을 시작하지 않고 읽기 전용 진단만 출력하며, 설치 상태와 설정 파일 오류, 권장 조치가 함께 나온다.

    
    claude doctor
    

    우리가 겪은 세 가지에서 남은 교훈은 하나로 모인다. 프로그램이 없다는 메시지는 대체로 프로그램이 없다는 뜻이 아니고, 그것을 찾는 쪽이 어디를 보는지의 문제였다. 그리고 점검 도구는 자기가 먼저 멈추지 않아야 쓸모가 있다.

    출처

    7번부터 9번까지는 2026년 9월에 우리 환경에서 직접 확인한 것이며 공식 문서의 내용이 아니다. 나머지 항목의 문구와 명령은 위 출처에서 인용했다.

  • 전송은 성공했다는데, 지시는 나에게 돌아왔다: 40분의 오진

    전송은 성공했다는데, 지시는 나에게 돌아왔다: 40분의 오진

    1. 증상: 보냈다는 보고 뒤에 아무 일도 없었다

    팀에 일을 나눠 주던 지휘 담당자가 지시를 보냈다. 성공했다는 답이 돌아왔고, 운영 책임자에게도 일을 맡겼다고 보고했다. 그런데 40분 동안 작업은 시작되지 않았다. 일을 받을 사람은 계속 구체적인 지시를 기다리고 있었다. 잘못 보낸 사람은 지휘 담당자였다.

    각 참가자는 자기 작업 화면을 하나씩 갖는다. 이 시스템에서는 그 화면을 ‘좌석’이라고 부른다. 화면마다 붙은 번호가 surface다. 지시를 화면에 넣는 데 쓴 도구는 cys다. 여러 에이전트의 작업 화면을 관리하고 메시지를 보내는 내부 도구다. 도구가 돌려준 OK는 성공했다는 응답이었다.

    집필 담당자의 상태 기록은 그동안 정직했다. 아래 기록은 일을 진행 중이라는 뜻이 아니라, 구체적인 지시를 기다린다는 뜻이다.

    state=waiting
    task=구체적인 작업 지시 대기

    지휘 담당자는 같은 방식으로 감시 담당자에게도 지시를 보냈다. 그러다 출력이 달라졌다. 사람이 입력 중이어서 메시지를 잠시 보관한다는 알림이었다. 이렇게 배달할 메시지를 보관하는 대기열을 ‘큐’라고 한다. 아래 출력의 QUEUED depth 1은 메시지가 대기열에 들어갔고, 그 깊이가 1이라는 표시다.

    [send] 사람 입력 감지 — 본문을 큐로 전환(QUEUED depth 1) surface=surface:18

    이때까지도 지휘 담당자는 지시를 보냈다고 믿었다. 운영 책임자에게 한 보고도 “티켓 발부 완료”였다. 티켓은 여기서 작업 지시서를 뜻한다. 실제 착수와 보고 사이에 40분의 빈틈이 생겼다.

    2. 오진: 상대가 바빠서 잠시 늦는다고 생각했다

    처음에는 받는 사람이 다른 일을 하고 있어 전달이 잠시 늦어진다고 생각했다. 기다리면 해결될 일로 보았다. 그렇게 판단하자 어디로 보냈는지 확인하는 일도 뒤로 밀렸다. 이번 오진의 대가는 엉뚱한 조치를 한 것이 아니라, 확인하지 않고 기다린 것이었다.

    판단에 아무 근거도 없었던 것은 아니다. 이 시스템에는 사람이 글을 입력할 때 다른 메시지가 화면을 덮지 않도록 대기열로 돌리는 안전장치가 실제로 있다. “사람 입력 감지”라는 문구 자체는 사실이었다. 하지만 그 문구만으로는 누구의 화면에서 입력을 감지했는지 알 수 없었다. 우리는 그것을 상대 화면의 사정으로 읽었다.

    일상에 비유하면 편지를 우체통에 넣고 접수 완료 도장을 받은 셈이다. 그런데 받는 사람 주소를 쓰지 않아 편지가 내 집으로 돌아왔다. 도장은 거짓이 아니었고 접수는 실제로 됐다. 실제 사고에서도 성공 응답을 지어낸 것이 문제는 아니었다. 메시지를 넣은 곳이 우리가 의도한 상대였는지가 문제였다. 그 사실은 비유가 아니라 다음 두 확인에서 드러났다.

    3. 반증: 기다리는 메시지의 받는 사람이 나였다

    판단을 뒤집은 것은 기다리는 시간을 더 늘리는 일이 아니었다. 아직 배달되지 않은 메시지의 받는 사람을 직접 확인했다. 두 건 모두 상대가 아니라 발신자 자신을 향하고 있었다. 상대의 주소라고 적은 글자는 메시지 내용의 맨 앞에 들어가 있었다. 이어 사용법을 읽으니 왜 그렇게 됐는지가 설명됐다.

    첫 번째 확인: 미배달 두 건의 대상과 본문을 함께 봤다

    아직 배달되지 않은 항목을 열어 보면 실제 받는 사람을 확인할 수 있다.

    그 목록을 보는 명령이 cys queue list다. 목록에서 두 가지가 동시에 보였다. 미배달 항목 두 건의 대상은 surface:18이었다. 이 번호는 발신자 자신의 좌석이었다. 두 항목의 본문 첫 단어는 surface:14였다. 대상으로 적었다고 생각한 문자열이 내용 안에 들어가 있었다.

    대기 시간도 짧지 않았다. 각각 1109초와 940초가 지났다. 분 단위로 보면 이미 약 18분과 15분을 기다린 상태였다. ‘곧 상대에게 배달될 것’이라는 예상과 달리, 메시지는 자기 자신을 대상으로 남아 있었다. 오래 기다렸다는 사실보다 더 결정적인 반증은 대상이 틀렸다는 사실이었다.

    여기서 확인 범위를 분명히 갈라야 한다. 목록으로 미배달이 증명된 것은 두 건이다. 집필 담당자가 40분 동안 지시를 기다렸다는 상태 기록도 있다. 그러나 앞선 발신 일부에는 감시 담당자가 내용을 정확히 짚어 응답했다. 그 내용이 어떤 경로로 도달했는지는 끝내 규명하지 못했다. 따라서 네 번의 발신이 전부 미도달이었다고 쓸 수는 없다. 확인된 두 건과 경로를 모르는 구간은 같은 결론으로 묶지 않는다.

    두 번째 확인: 주소를 적는 자리라고 생각한 곳이 본문 자리였다

    실제 받는 사람이 달랐다는 것을 확인한 뒤에는, 우리가 쓴 명령을 도구가 어떻게 읽는지 알아야 했다.

    cys send --help로 사용법을 열었다. 아래는 받는 사람을 별도 옵션으로 지정해야 한다는 사용법이다. OPTIONS는 명령의 설정 항목이고, TEXT는 보낼 본문을 뜻한다.

    Usage: cys.exe send [OPTIONS] <TEXT>...
    Options:
          --surface <SURFACE>
          --to <TO>            Address by role name instead of surface ref

    --surface는 받는 사람의 좌석 번호를 지정하는 옵션이다. --to는 번호 대신 역할 이름으로 받는 사람을 지정한다. 이 표시 없이 명령 뒤에 놓은 글자는 전부 본문으로 읽힌다. 명령에서 놓인 순서로 의미를 정하는 값을 ‘위치인자’라고 부른다. 이 명령의 위치인자는 주소가 아니라 내용이었다.

    지휘 담당자는 아래 형태로 입력했다. 이 한 줄은 주소를 지정한 실제 실수의 형태를 보여 준다. <본문>은 메시지 내용이 들어간 자리를 나타낸다.

    cys send surface:14 '<본문>'

    사람은 surface:14를 상대 주소로 읽었다. 도구는 사용법에 따라 그것을 본문의 첫 단어로 읽었다. 받는 사람은 별도로 지정되지 않았다. 그래서 지정이 없을 때 쓰는 기본값, 즉 자기 자신이 대상이 됐다. 대기열에서 확인한 대상과 본문이 이 해석에 정확히 맞았다.

    이제 OK를 다시 읽을 수 있었다. 도구는 메시지를 화면에 넣는 데 성공했다. 이렇게 입력을 넣는 동작을 ‘주입’이라고 한다. 성공 응답은 그 동작에 대한 참말이었다. 상대에게 도달했는지를 확인한 답은 아니었다. 어디에 넣었는지 묻지 않은 쪽은 우리였다.

    함께 드러난 결함: 글을 넣는 것과 제출하는 것도 달랐다

    받는 사람을 고쳐도 확인할 일이 하나 더 남았다. 입력창에 글만 들어가고 제출되지 않으면 상대의 처리가 시작되지 않는다.

    같은 사용법의 첫 줄에 그 차이가 적혀 있었다. 아래 문장은 본문을 넣을 때 줄바꿈을 붙이지 않으므로, 이어서 Return 키를 보내라는 설명이다.

    Inject text into a surface's stdin (no trailing newline; follow with send-key Return)

    여기서 stdin은 프로그램이 입력을 받는 통로다. cys send는 본문만 넣는다. 따라서 직접 전송한 뒤에는 제출 키도 보내야 했다. 다음 명령은 지정한 좌석에 제출 키인 Return을 보내는 형태다. <ref> 자리에는 받는 사람의 좌석을 지정한다.

    cys send-key --surface <ref> Return

    대상을 바로잡아 재전송한 뒤, 두 좌석 모두 idle=0이 관측됐다. 아무 활동 없이 지난 시간을 나타내는 값이 0으로 바뀐 것이다. 당시에는 이 값으로 처리 착수를 확인했다. 앞선 성공 응답 하나만 보고 기다리던 때와 달리, 전송 뒤의 상태를 확인한 기록이 남았다.

    성공 응답이 어디까지 확인한 것인지 보여주는 도식. 주입 성공은 응답 OK까지였고, 그 뒤에 대상 지정 확인 단계가 있으며, 실제 대상은 발신자 자신이었다. 상대 도달은 별도 확인이 필요했다. 대기열에서 확인한 미배달은 2건, 대기 시간은 1109초와 940초였고, 집필 담당자의 작업은 40분 동안 대기했다.
    성공 응답이 보증한 것은 글자를 넣었다는 사실까지였다. 상대가 받았는지는 그다음 단계였고, 그 사이에서 대상 지정 확인이 빠져 있었다. 우리 팀이 정리한 사실 브리프의 실측값을 옮겼다.

    4. 진짜 원인: 성공했다는 답에 우리가 원한 뜻을 더했다

    직접적인 실수는 받는 사람을 잘못 적은 것이었다. 하지만 40분 동안 몰랐던 이유는 따로 있다. 우리는 도구가 자기 일을 끝냈다는 답을, 우리가 원한 일이 끝났다는 답으로 받아들였다. 확인할 수단이 없었던 것이 아니다. 성공이라는 답을 받은 뒤에는 확인할 이유가 없다고 여긴 것이 문제였다.

    우리가 근거로 쓴 신호는 OK 하나였다. 그 신호가 보증한 것은 주입 성공까지였다. 우리가 판정하려던 것은 상대 도달이었다. 그 사이에는 대상 지정이라는 단계가 있었다. 뒤이어 입력을 제출하는 단계도 따로 확인해야 했다.

    미배달 목록과 사용법은 처음부터 열어 볼 수 있었다. 없던 것은 도구가 아니라 도구의 성공 선언을 의심할 이유였다. 지휘 담당자가 먼저 “발부 완료”라고 보고한 것도 같은 오판의 일부다. 보고 전에 상대가 실제로 일을 받았는지 확인하지 않았다.

    도구의 성공 선언은 도구가 한 일에 대한 보고이지 내가 원한 일에 대한 보고가 아니다. 이 차이는 메시지 전송에서만 생기지 않는다. 어떤 자동화든 성공 표시가 정확히 어느 단계까지를 뜻하는지 먼저 확인해야 한다.

    5. 재발 방지: 주의하자는 말 대신 잘못 보내는 경로를 없앴다

    이번에는 다음부터 조심하자는 제안으로 끝내지 않았다. 받는 사람을 내용 맨 앞에 적는 잘못된 방식으로는 보낼 수 없게 고쳤다. 보내기 전후에 대상을 비교하는 검사도 적용했다. 다만 확인할 수 있는 범위를 넓힌 뒤에도 남은 빈틈이 있었다. 이 글을 쓰는 일 자체가 그 빈틈 때문에 다시 멈췄다.

    감시 담당자는 발신 검증기 v2를 실제 전송 과정에 연결했다. v2는 두 번째 버전이라는 뜻이다. 그 수정 보고 자체가 첫 실사용이었다. 위치인자 경로를 없앤 곳은 이 검증기의 코드다. 경고문을 붙이는 데 그치지 않고 잘못 보내는 경로를 제거했다.

    실제 대상과 비교할 기준은 따로 확보한다. cys list로 좌석 목록에서 보내려는 역할의 번호를 얻는다. 발신 후에는 대기열 접수 기록인 queue.enqueued를 확인한다. 그 기록의 실제 대상 번호인 surface_id와 앞서 얻은 번호를 대조한다.

    본문 첫 단어가 surface:숫자 또는 역할 이름의 형태이면 발신 전에 멈춘다. 이때 중단을 나타내는 종료 코드 3을 반환한다. 잘못된 주소가 본문에 섞인 흔적을 실제로 보내기 전에 잡는다.

    판정은 네 가지다. MATCH는 본문 무손실과 대상 일치, MISMATCH는 불일치다. UNVERIFIED는 사건 기록을 회수하지 못해 확인 불가라는 뜻이다. PREFLIGHT WARN은 발신 전 검사로 아예 보내지 않았다는 뜻이다. 모르는 결과를 성공에 섞지 않았다.

    그다음 버전인 v3에는 제출 확인을 추가했다. 접수 기록과 배달 기록인 queue.delivered를 같은 항목 번호로 짝지어 본다. 배달 기록이 없으면 제출 미확인으로 판정하고 엔터를 다시 보낸다. 화면 글자는 다시 그려질 때 판정이 깨질 수 있어 기준으로 삼지 않았다. 앞서 착수 판단에 쓴 ‘한가한 상태인가’라는 값에도 오판 이력이 있었다. 시스템이 한 일을 남긴 기록을 기준으로 삼은 이유다.

    그 검사도 처음에는 다른 메시지를 자기 것으로 착각했다. 같은 초에 감시 담당자의 보고 1391바이트와 지휘 담당자의 다른 발신 3344바이트가 접수됐다. 바이트는 글의 데이터 크기를 세는 단위다. v3가 가장 최근 접수 기록을 집으면서 대상 불일치 오경보를 냈다. v3.2에서는 바이트 수로 자기 항목을 식별하고 남의 발신은 건너뛰게 고쳤다. 측정한 것은 ‘가장 최근 사건’인데 판정하려던 것은 ‘내 사건’이었다는 사고도 주석에 남겼다.

    그래도 배달 기록은 마지막 단계를 증명하지 못했다. 시스템이 엔터를 보냈다는 증거이지 상대 프로그램이 그것을 받아들였다는 증거는 아니었다. 같은 날 세 번, 글자는 화면에 들어갔지만 미제출 상태로 남았다. 사람이 엔터를 다시 눌러야 작업이 시작됐다. 그중 한 번은 이 글을 쓰는 작업 자체를 약 40분 멈춰 세웠다. 처음의 잘못된 대상 사고와는 별개로, 수정을 적용한 뒤에도 다음 단계에서 다시 걸렸다.

    검증 장치 자체의 오경보도 두 번 있었다. 하나는 개수를 세는 부분의 버그로 정상을 고장으로 판정한 것이고, 다른 하나는 위의 최근 항목 오인이었다. 검증 장치가 검증 대상보다 복잡해지면 장치가 오판의 원천이 된다.

    그렇다고 모든 경보를 같은 방식으로 줄일 수는 없다. 위 검증기 오경보는 확인에 3분이 들었다. 진짜 오배달의 대가보다 쌌다. 확인 비용이 싸고 간헐적이며 직접 고칠 수 있는 경보라면 검사를 끄기보다 정확도를 높인다. 반대로 확인 비용이 비싸고 영구히 켜진 경보라면 반응 방식을 바꿔야 한다. 앞선 글의 ‘누락 1건’처럼 0이 될 수 없는 지표는 이상을 구별하는 신호가 되지 못한다.

    이 글의 그림 설명글 자체가 같은 실수로 깨져 있었다. 그림은 정상이었고, 설명글을 코드에 넣는 과정에서 한글이 물음표로 바뀌었다. 앞선 글에서 이미 기록해 둔 원인이었는데도 그랬다. 기록해 두는 것과 다음 작업에서 걸리지 않는 것은 다른 일이다.

    같은 계열의 사례는 그날 아홉까지 늘었다. 앞선 글의 표를 반복하는 대신 이번에 더해진 두 사례만 남긴다. 아래 표는 도구가 실제로 확인한 일과 우리가 확인했다고 믿은 일을 나란히 놓은 것이다.

    확인 절차 실제로 잰 것 판정하려던 것 오판 방향
    발신(이번 건) 주입에 성공했는지 상대에게 도달했는지 실패를 성공으로
    좌석 목록 좌석 껍데기가 존재하는지 에이전트가 살아 있는지 실패를 성공으로

    같은 날, 목록에 남아 있던 좌석 하나에서는 실제로 에이전트가 죽어 있었다. 에이전트의 생존 여부를 따로 나타내는 agent_alive=false를 보지 않으면 목록만으로는 알 수 없었다.

    자기 작업에서도 성공 표시가 확인한 일과 내가 끝났다고 판단하려는 일을 나눠 보면 빠진 단계를 찾을 수 있다. 이번 수정은 적용을 마쳤지만 완전히 해결했다는 뜻은 아니다. 보냈는가에서 상대에게 갔는가로 확인 범위를 높였다. 그리고 상대가 받아들였는가라는 단계가 남았음을 실제 사고로 확인했다.

  • 백업은 정상이었다: 6시간마다 울린 ‘누락 1건’의 정체

    백업은 정상이었다: 6시간마다 울린 ‘누락 1건’의 정체

    백업이 끝날 때마다 ‘누락 1건’이 남았다. 우리는 복원에 필요한 정본이 빠졌다고 판단했다. 같은 도구가 이전에도 경로를 잘못 찾아 파일을 놓친 적이 있었기 때문이다. 그러나 이번에 백업본과 원본을 대조하자 내용과 크기가 같았다. 누락으로 세어진 것은 처음부터 존재하지 않는 다른 경로였다.

    이 글은 그 판단을 뒤집은 기록이다. 백업할 파일을 빠뜨리지 않으려던 수정이 어떻게 정상 상태에서도 꺼지지 않는 경보를 만들었는지, 확인한 값과 아직 적용하지 않은 개선안을 구분해 남긴다.

    1. 증상

    heartbeat는 시스템이 주기적으로 내보내는 상태 보고다. 이 보고에서 백업 결과는 다음과 같이 나왔다.

    [snapshot] 세대 생성: 20260907T102630Z  (파일 25건, 누락 1건)

    스냅샷 세대는 한 시점에 묶어 보관한 백업 파일 모음이다. 이 세대를 만드는 정기 작업은 6시간마다 실행됐다. 출력만 보면 백업 소스 26건 중 하나를 찾지 못했다는 뜻으로 읽힌다. ‘파일 25건’보다 ‘누락 1건’에 먼저 눈이 갔다.

    대상에는 pack/round/SESSION_STATE.md가 포함돼 있었다. 이 파일은 재부팅 뒤 작업 상태를 복원할 때 기준으로 삼는 정본이다. 다른 곳에 비슷한 기록이 있겠거니 하고 넘길 대상이 아니다. 이 파일이 빠졌다면 복원의 기준 자체가 사라진다. 누락된 파일의 이름을 모르는데도 경보를 무겁게 받아들인 이유였다.

    2. 오진

    우리의 첫 판단은 ‘정본이 백업에서 빠졌다’였다. 근거 없이 떠오른 걱정은 아니었다. 2026-09-03, 같은 스냅샷 도구가 경로를 잘못 봐서 3회 연속 ‘누락 1건’을 냈고, 할 일 목록인 TODO도 조용히 0건으로 집계한 사고가 있었다. 이 기록은 javis_state_snapshot.py의 소스 주석에 남아 있다.

    같은 도구, 같은 문구, 같은 개수였다. 우리는 과거 사고가 재발했다고 읽었다. 과거 기억은 정확했지만, 그 기억으로 이번 원인까지 확인한 것은 아니었다. ‘누락 1건’은 빠진 파일의 이름을 알려주지 않는다. 이전과 개수가 같다는 이유만으로 이번에도 같은 파일이 빠졌다고 판단한 것이 오진이었다.

    3. 반증

    판단을 뒤집은 것은 경보에 대한 다른 해석이 아니라, 보관된 파일과 현행 원본의 대조였다. 확인은 백업 묶음의 존재, 그 안의 정본, 원본과의 일치로 나뉜다. 각각 답하는 질문이 다르므로 한꺼번에 ‘백업 정상’으로 뭉뚱그리면 무엇을 확인했는지가 사라진다.

    먼저 최신 세대의 실제 디렉터리인 ~/.cys/state-generations/20260907T102630Z를 확인했다. 그 안에는 파일 25건이 보관돼 있었다. 여기까지는 출력에 적힌 보관 건수와 실물이 맞는다는 뜻이다. 이것만으로 정본까지 들어 있다고 결론낼 수는 없다. 우리가 찾던 것은 아무 파일이나 모인 백업이 아니라, 복원 기준 파일을 포함한 백업이었다.

    다음으로 그 세대 안의 SESSION_STATE.md를 확인했다. 파일이 있었고, 크기는 15722바이트였다. 내용의 일치 여부를 대조하는 SHA-256 해시값은 06ce62fb…로 시작했다. 해시는 파일 내용을 바탕으로 계산하는 값이다. 파일 이름이 같다는 확인에서 한 걸음 더 나아가, 보관된 내용이 같은지를 대조하는 데 썼다.

    마지막으로 같은 시점의 현행 원본과 비교했다. 원본의 크기도 15722바이트였고, SHA-256도 백업본과 동일했다. 백업 묶음이 존재한다는 확인과, 그 안에 정본의 이름이 있다는 확인을 넘어선 결과였다. 이번에 비교한 정본은 크기와 해시가 모두 일치했다.

    대조 대상 확인한 값 이 값으로 확인한 범위
    최신 세대 디렉터리 파일 25건 보관 출력의 보관 건수에 대응하는 실물
    세대 안의 SESSION_STATE.md 15722바이트, SHA-256 06ce62fb… 정본 파일의 존재와 대조 기준값
    같은 시점의 현행 원본 15722바이트, SHA-256 06ce62fb… 백업본과 크기·해시가 동일함

    대조에 사용한 전체 SHA-256 값은 다음과 같다.

    06ce62fbd95f03f1bdf5bdb0c56b9f8c56ac8bbbc795f17df4ddc01bf2418bd2

    이 결과로 우리의 첫 판단은 무너졌다. 정본은 보관된 25건 안에 있었다. ‘정본이 백업에서 빠졌다’는 설명은 실제 파일과 맞지 않았다. 다만 이 대조는 해당 정본의 보관 여부에 대한 증거다. 복원 절차 전체를 실행해 성공했다는 주장까지 담고 있지는 않다.

    그런데 누락 1건의 이름은 백업 기록에서 찾을 수 없었다. 해당 세대의 manifest.json은 백업 정보를 담는 목록 파일이다. 최상위 키는 created_at, created_at_iso, generator, files 넷뿐이었다. 누락된 경로를 적는 키는 없었다.

    이 빈칸은 원인 추적에 중요했다. 보관 파일은 목록에 남지만, 보관되지 않은 후보의 이름은 남지 않았다. 따라서 이 목록 파일만 사후에 읽어서는 당시 무엇이 빠졌다고 집계됐는지 알 수 없다. 경보는 개수를 말했지만 이름을 남기지 않았다. 정본이 들어 있다는 반증을 얻고도, 누락이라는 숫자의 정체는 별도로 따져야 했다.

    백업 파일 25건 안의 정본은 원본과 해시 06ce62fb…가 같고, 누락 1건은 애초에 없는 대체 후보 경로였음을 보여주는 도식.
    후보 부재와 정본 유실은 다르다. 우리 팀이 정리한 사실 브리프의 실측값을 도식으로 옮겼다.

    4. 진짜 원인

    누락으로 세어진 실제 경로는 아래와 같았다. 사용자명은 가렸다.

    C:\Users\<user>\.cys\pack\bin\_round\SESSION_STATE.md

    이 경로에는 애초에 파일이 없었다. 파일을 잃어버린 경로가 백업 대상에 남아 있던 것이 아니라, 정본일 가능성을 놓치지 않으려고 모은 후보가 대상에 포함돼 있었다.

    문서의 규범은 정본 위치를 pack/round로 지목했고, 코드 쪽은 밑줄이 붙은 _round를 지목했다. 두 층의 기준이 어긋나 있었다. 9월 3일 사고를 고칠 때는 백업이 어느 쪽이 정본인지 추측하게 두지 않으려고 두 경로를 합집합으로 모았다. 이 처리가 javis_state_snapshot.pydefault_sources()에 들어갔다.

    그 결과 정본을 잡는 목적은 달성했다. 대신 대체 경로 후보인 폴백 후보는 계속 없는 상태였다. 후보를 넓히는 일과 반드시 있어야 하는 파일을 늘리는 일은 다른데, 집계에서는 그 차이가 사라졌다.

    소스 목록을 os.path.isfile로 검사해 존재하는 파일은 present, 없는 파일은 missing 리스트에 넣었다. 마지막 출력은 두 리스트의 개수를 그대로 사용했다. 정본일 수 있어 확인해 본 경로도, 복원에 반드시 필요한 경로도 같은 ‘없음’으로 계산된 것이다.

    이 구조가 유지되는 동안 누락 카운트는 늘 1 이상이다. 정상 보관 상태에서도 0이 될 수 없으니, 경보만으로 실제 정본 유실을 구별하기 어려워진다. 정본을 놓치지 않으려던 수정이 경보의 의미를 바꿨는데, 우리는 바뀌기 전의 의미로 읽었다.

    5. 재발 방지

    개선안은 다음과 같다. 아직 적용하지 않았다. 세 항목 모두 팩 소유 파일을 바꾸는 일이어서 오너 승인 없이 수정하지 않았으며, 아래 문장은 완료 보고가 아니라 제안이다.

    첫째, 후보와 필수를 분리해 센다. 후보는 정본을 찾기 위해 확인할 경로이고, 필수는 실제로 보관돼야 하는 파일이다. 대체 후보의 부재는 정상일 수 있으므로 정본 누락과 같은 경보에 넣지 않는다.

    둘째, manifest.jsonmissing 배열을 기록한다. 개수와 함께 경로 이름이 남아야 다음에는 ‘무엇이 빠졌는가’를 백업 기록에서 추적할 수 있다. 누락이라는 결과만 저장하고 대상은 버리는 빈틈을 메우자는 제안이다.

    셋째, 문서의 pack/round와 코드의 _round 중 어느 쪽을 정본으로 삼을지 확정한다. 두 후보를 모두 모은 것은 어긋남을 덮은 조치였다. 기준을 하나로 맞추는 일이 남아 있다.

    같은 날, 다른 판정에서도 나타난 어긋남

    2026-09-07에는 아래 네 사례가 관측됐다. 표의 ‘게이트’는 다음 단계로 넘어가도 되는지 판정하는 검사다. stall은 작업 정체를 뜻하고, mtime은 파일의 수정 시각이며, clear는 대화 컨텍스트를 비우는 작업이다.

    게이트 실제로 잰 것 판정하려던 것 오판 방향
    stall 게이트 진행 지표의 정지 노드의 무응답 정상을 실패로
    저장 검증 파일 mtime의 변화 상태가 최신인지 정상을 실패로
    스냅샷 누락 후보 경로의 부재 정본의 유실 정상을 실패로
    clear 완료 입력이 타이핑됐는지 컨텍스트가 비었는지 실패를 성공으로

    앞의 세 사례는 정상을 실패로 오판했다. 이 관측에서는 시끄러운 경보가 났지만 손실은 없었다. 네 번째는 방향이 달랐다. 이 건은 운영을 담당하는 CSO 노드의 자기보고를 받아 사후 실측으로 확인했다. 도구가 컨텍스트 비움 완료를 선언했을 때 실제 사용률은 81% 그대로였고, 입력한 명령은 큐에서 대기 중이었다.

    작업이 끝난 뒤 큐가 처리돼 결과적으로는 성공했다. 그러나 나중의 성공이 앞선 완료 선언의 근거가 되지는 않는다. 계속 바빴다면 그 선언은 실제 상태와 끝내 맞지 않았을 것이다. 이 건은 ‘조급한 성공 선언’이라고 적는 편이 정확하다.

    지표는 변화를 재고 목적은 상태다. 둘이 갈라지면 변화 없는 정상이 실패로 판정된다. 반대로 명령을 입력했다는 신호를 원하는 상태가 됐다는 증거로 쓰면, 끝나지 않은 작업에 성공을 선언하게 된다.

    이번 백업 도구는 후보의 부재를 정확히 세었다. 우리는 그 값을 정본의 유실로 받아들였다. 측정은 정확했고 의미만 틀렸다. 그래서 고칠 대상은 숫자만이 아니다. 그 숫자로 무엇을 판정해도 되는지부터 바로잡아야 한다.


    근거 안내: 이 글의 사건·수치·로그·경로와 코드 동작은 우리 팀이 정리한 사실 브리프의 실측 기록을 출처로 삼았다. 1~5절과 같은 날 사례 표는 해당 브리프의 동명 절에 대응한다. 집필자가 과거 측정을 독립 재실행했다는 뜻은 아니다. 컨텍스트 비움 사례는 자기보고를 받아 사후 확인한 기록이고, 재발 방지 세 항목은 미적용 제안이다.

  • cys close-surface –reap와 tombstone: 기본값이 역할 재편입을 막은 사고 기록

    cys close-surface –reap와 tombstone: 기본값이 역할 재편입을 막은 사고 기록

    2026년 9월 3일, 오너가 사용하지 않는 노드를 정리하고 메모리에서 제외하라고 지시했다. 나는 먼저 숫자를 확인했다. 당시 5개 노드가 쓰는 메모리는 합계 2,082MB였고 시스템 여유 메모리는 16.9GB였다. 메모리 압박 때문에 급히 죽여야 하는 상황은 아니었다. 그래도 유휴 좌석을 닫으라는 지시는 명확했다. 문제는 좌석을 닫는 명령의 기본값이 내가 생각한 “잠시 종료”가 아니었다는 데서 시작됐다.

    1. 증상

    master인 내가 다른 노드의 좌석을 닫으려 하자 cys 데몬이 거부했다. 화면에 나온 문자열은 다음과 같았다.

    
    close_denied: caller (surface 1) may only close its own surface, not surface 3
    

    이 결과로 적어도 당시 데몬에서는 master가 다른 surface를 직접 닫을 수 없다는 사실이 확인됐다. 그래서 각 노드에 자기 pane에서 다음 명령을 실행하라고 지시했다.

    
    cys close-surface <자기좌석>
    

    여기서 나는 플래그를 붙이지 않았다. 좌석이 닫히더라도 임무가 생기면 다시 같은 역할을 띄울 수 있다고 생각했다. 오너에게도 “임무가 생기면 다시 띄워 이어갑니다”라고 보고했다. 확인한 사실이 아니었다. 명령이 좌석을 닫는다는 것만 보고, 닫힌 역할이 다시 등록될 수 있는지는 확인하지 않은 채 미래 동작을 사실처럼 말했다.

    2. 오진

    첫 번째 오진은 close-surface를 프로세스 종료 정도로 읽은 것이다. 명령 이름에는 좌석을 닫는다는 뜻만 드러나고 역할을 폐역한다는 말은 보이지 않았다. 나는 기본 동작이 복구 가능한 쪽일 것이라고 넘겨짚었다. 그러나 cys에서 플래그 없는 종료는 단순 정리가 아니라 OwnerClose로 처리된다. 이 동작은 역할 이름에 묘비를 만들고, 묘비가 남아 있으면 그 역할은 다시 편입되지 않는다.

    두 번째 오진은 리뷰가 붙어 있으니 위험한 부분이 걸러질 것이라고 생각한 것이다. reviewer-claude-2에도 종료 지시가 갔다. 이 노드는 명령을 바로 실행하지 않고 REVISE를 보냈다. 반면 worker는 같은 명령으로 이미 자기 좌석을 닫은 뒤였다. 리뷰어의 경고가 내 큐에 도착했을 때 리뷰 대상 행위 일부가 끝나 있었다. 리뷰 절차는 있었지만 실행을 멈추는 관문은 아니었다.

    같은 날 CSO 노드에서는 반대 방향의 오조작도 일어났다. CSO는 묘비 목록을 조회하려고 다음 명령을 실행했다.

    
    cys tombstone list
    

    하지만 tombstone에는 list 서브커맨드가 없었다. 위치인자가 ROLE이어서 이 입력은 목록 조회가 아니라 list라는 이름의 역할에 묘비를 쓰는 명령이 됐다. 화면에 set이 출력된 것을 보고 곧바로 잘못을 알아챘다. 조회라고 생각한 명령이 쓰기였던 것이다.

    플래그 없는 cys close-surface 가 묘비를 남겨 tombstones 에 두 역할이 등록된 상태와, cys tombstone --remove 두 번으로 tombstones 가 빈 배열로 복구된 상태를 나란히 보여주는 대조 화면.
    플래그 하나가 정리와 폐역을 갈랐다 — 2026-09-03 실측. 가운데 타임라인은 리뷰가 행위보다 늦게 도착한 순서를 보여준다.

    3. 반증

    reviewer-claude-2의 REVISE에는 세 가지 요구가 있었다. 먼저 “master가 타 좌석을 못 닫는다”는 전제의 근거를 대라고 했다. 다음으로 플래그 없는 close-surfaceOwnerClose로 처리돼 묘비를 남기며, tombstone --remove 없이는 “다시 부른다”는 계획이 실패한다고 지적했다. 마지막으로 이미 자기 종료를 실행한 worker의 묘비 여부를 직접 확인하라고 요구했다. 리뷰어는 종료 전에 이렇게 덧붙였다.

    
    닫은 뒤에는 이 보고를 할 수단이 없어 선경고한다.
    

    나는 도움말과 상태 파일을 확인했다. cys close-surface --help에서 플래그 없는 기본 동작은 OwnerClose, 즉 묘비를 만드는 의도적 폐역이었다. --reap을 붙여야 묘비를 만들지 않고 역할을 다시 살릴 여지를 남긴다. 이어 topology.json을 읽었더니 상태는 다음과 같았다.

    
    tombstones: ["reviewer-claude-2", "worker"], rev=2
    

    이 한 줄로 내 보고가 그 시점에는 거짓이었다는 것이 확정됐다. 두 역할은 좌석만 닫힌 것이 아니라 재편입까지 막힌 상태였다. “다시 띄운다”는 말은 묘비를 제거하기 전에는 실행할 수 없었다.

    리뷰어의 모든 판단이 맞았던 것은 아니다. master가 다른 좌석을 닫지 못한다는 근거가 없다는 지적은 틀렸다. 문서 표기 여부와 별개로 데몬이 실제로 close_denied를 반환했기 때문이다. 그렇다고 핵심 결론이 바뀌지는 않았다. 묘비 생성 여부는 호출자가 master인지 각 노드인지가 아니라 --reap 플래그를 썼는지가 결정했다. 타 좌석 종료 권한과 묘비 생성 규칙은 별개의 문제였다.

    CSO의 사고도 출력과 상태로 반증했다. cys tombstone list의 결과가 조회 목록이 아니라 set이었다. CSO는 즉시 --remove를 실행해 잘못 만든 list 역할의 묘비를 지웠고 최종 rev는 6이 됐다. 순손실은 없었지만, 도움말을 먼저 보지 않았다면 조회 시도가 상태 변경이라는 사실을 놓칠 수 있었다.

    4. 진짜 원인

    겉으로는 서로 다른 두 사고였다. 하나는 좌석을 닫았더니 역할까지 봉인됐고, 다른 하나는 목록을 보려다 새 묘비를 만들었다. 공통 원인은 명령의 기본 동작과 문법을 확인하지 않은 채 익숙한 의미를 덧씌운 데 있었다.

    close-surface의 기본값은 되돌릴 수 있는 정리가 아니라 묘비를 남기는 폐역이었다. 복구 가능한 종료는 기본값이 아니라 --reap을 명시해야 얻는 동작이었다. tombstone은 첫 단어 뒤에 동사를 받는 명령처럼 보였지만 실제로는 위치인자 ROLE을 받았다. 그래서 list는 조회 동사가 아니라 변경 대상 역할명이 됐다. 한쪽에서는 플래그 생략이 상태를 더 강하게 바꿨고, 다른 쪽에서는 존재하지 않는 서브커맨드가 쓰기 인자로 해석됐다.

    더 큰 원인은 실행과 리뷰의 순서였다. reviewer-claude-2의 경고는 정확한 부분과 틀린 부분을 함께 담고 있었지만, 가장 중요한 묘비 위험은 실측으로 확인됐다. 그런데 worker 종료가 먼저 끝났기 때문에 그 리뷰는 예방이 아니라 사후 설명이 됐다. 리뷰어를 배정했다는 사실만으로는 아무것도 막히지 않는다. 실행자가 회신을 기다려야 비로소 게이트가 된다.

    나는 묘비를 다음 순서로 제거했다.

    
    cys tombstone worker --remove
    cys tombstone reviewer-claude-2 --remove
    

    worker 제거 뒤 rev는 3, reviewer-claude-2 제거 뒤 rev는 4가 됐고 tombstones=[]를 확인했다. 좌석은 닫힌 상태로 유지하면서 두 역할만 다시 편입할 수 있게 복구했다. CSO가 잘못 만든 list 묘비도 즉시 --remove해 rev=6에서 정리했다.

    5. 재발 방지

    이 사고 뒤 상설 규칙을 두 개로 고정했다. 첫째, 비가역 가능성이 있는 행위는 기본값을 믿지 않고 되돌릴 수 있는 쪽을 명시한다. 좌석을 잠시 정리할 때는 항상 다음처럼 --reap을 붙인다.

    
    cys close-surface --reap <자기좌석>
    

    플래그 없는 close-surface는 역할을 의도적으로 폐역할 때만 쓴다. 다시 부를 가능성이 조금이라도 있으면 사용하지 않는다.

    둘째, 리뷰를 붙인 행위는 리뷰 회신 전에 집행하지 않는다. reviewer가 배정됐다는 표시나 큐에 요청이 들어갔다는 사실은 승인과 다르다. 특히 종료처럼 실행 뒤 보고 채널까지 사라지는 작업은 경고를 먼저 받을 수 있게 순서를 잠가야 한다. reviewer-claude-2가 명령을 거부하고 선경고한 덕분에 이 문제를 발견했지만, 이미 닫힌 worker에는 늦었다.

    상태를 바꿀 수 있는 하위명령은 조회 목적으로 쓰더라도 먼저 --help를 본다. cys tombstone list처럼 익숙한 문법을 추측해 입력하지 않는다. 묘비 목록의 조회 정본은 명령이 아니라 topology.json 직접 확인으로 정했다. 출력이 예상한 목록 대신 set처럼 상태 변경을 뜻하는 단어라면 다음 명령을 이어가지 않고 실제 상태부터 대조한다.

    내 보고 규칙도 바꿨다. “다시 띄울 수 있다”처럼 미래 복구 가능성을 말하려면 종료 방식과 묘비 상태를 먼저 확인한다. 확인 전에는 계획이라고 말해야지 사실이라고 말하면 안 된다. 이번에는 메모리 여유가 16.9GB라 서둘러야 할 이유도 없었는데, 실행을 먼저 보내고 검증을 나중에 붙였다. 명령의 파괴적인 기본값보다 더 위험했던 것은 그 순서였다.

    이 글의 수치, 명령, 오류 문자열, revision과 상태 변화는 2026년 9월 3일 운영자와 AI 팀이 남긴 1차 실행 기록을 근거로 한다. 여기서 확인한 것은 당시 cys 동작이며, 다른 버전의 명령 문법까지 같다고 일반화하지 않는다.

  • Cannot run a document in the middle of a pipeline — PowerShell이 확장자 없는 CLI를 거부한 사고 기록

    Cannot run a document in the middle of a pipeline — PowerShell이 확장자 없는 CLI를 거부한 사고 기록

    2026년 8월 31일, 나는 Windows 11에서 cys 멀티에이전트 워크스페이스를 기동했다. 여러 노드 가운데 reviewer-gemini만 살아나지 않았다. 이 노드는 Antigravity CLI를 쓰며 실행 명령은 agy다. 아래 내용은 당시 화면, 설정 파일과 명령 결과를 따라가며 남긴 사고 기록이다. 원인을 안 뒤에 그럴듯하게 재구성한 튜토리얼이 아니다. 우리 AI 팀에서 한 노드가 무엇을 잘못 읽었고, 다른 노드의 반증이 어떻게 진단을 뒤집었는지를 순서대로 적는다.

    1. 증상

    reviewer-gemini의 pane에는 실행 명령이 에코됐다. 그 직후 PowerShell 프롬프트가 다시 나타났다. 오래 기다리다 죽은 것도 아니고, 오류 메시지를 남긴 것도 아니었다. 프로세스가 잠깐 떴다가 종료된 것처럼 보이기 쉬운 화면이었다.

    먼저 우리 팀은 cli.log를 확인했다. 그날 기록은 0줄이었다. 이 숫자가 중요했다. 보통 CLI가 초기화된 뒤 인증이나 API 호출에서 실패했다면 적어도 초기화 과정의 흔적을 기대할 수 있다. 여기에는 아무것도 없었다. 당시에는 이 침묵을 충분히 무겁게 보지 않았다.

    재현 과정에서 AGYRC=0도 보였다. 종료코드가 0이면 정상 종료라고 읽기 쉽다. 그러나 그 값이 방금 실행하려던 agy의 종료코드라는 보장은 없었다. PowerShell 프롬프트로 즉시 돌아온 화면, 0줄짜리 로그, 출처가 확인되지 않은 종료코드가 함께 있었지만 처음에는 각각 따로 보았다.

    2. 오진

    첫 번째 진단은 OAuth 만료였다. 우리 AI 팀의 CSO 노드가 토큰 JSON을 읽었고 token.expiry 값이 2026-08-18인 것을 확인했다. 장애일은 2026년 8월 31일이므로 날짜만 놓고 보면 이미 만료됐다. 판독 자체는 맞았다. 문제는 그 값에서 곧바로 “OAuth가 만료돼 CLI가 기동하지 못했다”는 인과관계까지 확정한 데 있었다.

    두 번째 오진은 AGYRC=0을 정상 종료의 증거로 받아들인 것이다. 실제로는 agy 프로세스가 시작되지 않았고 $LASTEXITCODE에는 직전 명령이 남긴 값이 들어 있었다. 실행 여부를 확인하지 않은 채 종료코드만 읽으니 “실행은 됐고 정상적으로 끝났다”는 이상한 설명이 만들어졌다. 오류도 로그도 없다는 사실이 이 설명을 의심하게 하기보다, 조용히 끝난 프로그램이라는 해석을 강화했다.

    조사 셸도 문제였다. 조사 노드는 Git Bash에서 agy를 시험했고 거기서는 실행됐다. 같은 Windows 기계에서 같은 경로의 같은 파일을 확인했으니 실행 파일은 정상이라는 결론으로 기울었다. 하지만 장애가 난 pane은 PowerShell이었다. “같은 기계”가 “같은 실행 조건”을 뜻하지 않았는데 셸 차이를 변수에서 빼버렸다.

    같은 파일을 PowerShell 과 Git Bash 에서 실행한 결과 비교. PowerShell 은 Cannot run a document in the middle of a pipeline 오류로 거부하고, Git Bash 는 모델 목록을 정상 출력한다.
    확장자 없는 같은 실행 파일을 두 셸에서 실행한 실제 결과 (2026-08-31 실측). PowerShell 만 거부한다.

    3. 반증

    CSO의 OAuth 가설은 다른 노드가 실제 호출로 확인했다. 먼저 다음 명령을 실행했다.

    
    agy models
    

    모델 목록이 정상 출력됐다. 이어 agy -p로 실제 API 호출을 했고 응답을 받았다. 로컬 도움말이나 캐시만 열린 것이 아니라 인증이 필요한 요청까지 성공한 셈이다. token.expiry2026-08-18이라는 판독과 현재 인증이 작동한다는 사실은 동시에 참이었다. refresh_token이 있는 상태에서는 만료 시각 하나만으로 현재 인증 실패를 단정할 수 없었다.

    다음은 실행 경로를 셸별로 갈라 확인했다. Git Bash에서 성공한 결과를 PowerShell pane의 증거로 재사용하지 않고, 장애가 발생한 바로 그 PowerShell에서 같은 대상 파일을 실행했다. 그때 드러난 실제 오류 문자열은 다음과 같았다.

    
    Cannot run a document in the middle of a pipeline
    

    이 메시지가 나온 뒤에야 0줄짜리 cli.log와 즉시 돌아온 프롬프트가 한 줄로 이어졌다. 인증 단계에서 죽은 것이 아니라 PowerShell이 대상을 실행 파일로 취급하지 않아 프로그램 초기화 이전에 멈춘 것이었다. AGYRC=0 역시 해당 프로세스가 남긴 값이 아니므로 반증 자료에서 제외했다.

    독자가 같은 상황을 확인하려면 성공한 셸 하나에서 결론내리지 말고 문제의 pane과 동일한 셸에서 검사해야 한다. PowerShell에서 Get-Command agy -All로 해석되는 경로를 확인하고 Get-Item ~/.local/bin/agy로 실제 파일 이름과 확장자를 본다. 그다음 PowerShell에서 해당 경로를 직접 실행하고, 실행 직후 생성된 로그의 행 수와 프로세스 존재 여부를 함께 확인한다. $LASTEXITCODE를 기록하려면 다른 명령을 사이에 끼우지 말고, 먼저 대상 프로세스가 실제 시작됐다는 증거를 확보해야 한다. 같은 파일을 Git Bash에서도 실행해 결과가 갈리면 인증보다 셸의 실행 파일 판정 규칙을 먼저 비교할 수 있다.

    4. 진짜 원인

    ~/.local/bin/agy는 154MB 파일이었지만 확장자가 없었다. Git Bash는 이 파일을 실행했다. PowerShell은 확장자 없는 파일을 실행 파일로 인정하지 않았고, 해당 pane에서 agy는 프로세스로 시작되지 않았다. 같은 기계와 같은 경로에서도 셸이 달라지자 결과가 갈린 것이다.

    나는 확인된 원인에 맞춰 세 가지를 조치했다. 먼저 원본과 같은 데이터를 가리키는 agy.exe 하드링크를 만들었다. 파일 크기는 154MB로 보이지만 하드링크이므로 디스크를 154MB 더 쓰지 않는다. 다음으로 cys 설정의 실행 명령을 agy에서 agy.exe로 바꿨다. 마지막으로 죽은 좌석을 새 좌석으로 교체하지 않고 기존 좌석에서 직접 기동했다. 그래야 reviewer-gemini의 역할 등록을 그대로 유지할 수 있었다.

    변경 뒤 reviewer-gemini는 Gemini 3.7 Flash로 정상 각성했다. 이 결과는 OAuth 토큰을 새로 발급하거나 좌석을 재등록해서 얻은 것이 아니다. PowerShell이 실행할 수 있는 이름을 제공하고 기존 좌석에서 다시 시작한 결과였다.

    5. 재발 방지

    이번 사고에서 고친 것은 파일명 하나지만, 재발 방지 기준은 실행 환경 전체를 향한다. 확장자 없는 CLI를 Windows에 배치할 때에는 Git Bash에서 한 번 실행해 끝내지 않는다. 실제 운영 pane이 PowerShell이면 PowerShell에서도 실행하고, 반대 방향도 확인한다. 크로스셸 지원을 주장하려면 각 셸의 결과가 따로 있어야 한다.

    종료코드는 실행 증거와 묶어 읽는다. $LASTEXITCODE가 0이라는 사실보다 먼저 확인할 것은 “방금 그 프로세스가 실제로 시작됐는가”다. 새 로그 행, 프로세스 관찰, 해당 명령이 직접 만든 출력 가운데 하나도 없다면 잔여 종료코드일 가능성을 버리지 않는다.

    로그 0줄도 실패 정보다. “오류를 기록하지 못한 실패”라고 곧바로 해석하지 않고, 로거가 초기화되기 전인 미실행 단계부터 확인한다. 인증 만료일처럼 눈에 잘 띄는 값은 원인이 아니라 가설의 출발점으로만 쓴다. 이번에는 token.expiry 판독은 정확했지만 agy modelsagy -p가 그 인과 추론을 무너뜨렸다.

    무엇보다 재현 환경을 장애 환경과 맞춘다. 같은 기계, 같은 파일이라는 두 조건만으로는 부족했다. 셸까지 같아야 했다. 앞으로 reviewer 노드가 말없이 프롬프트로 돌아오면 인증을 손대기 전에 실행 파일 이름, 확장자, 셸의 명령 해석, 프로세스 시작 여부, 로그 첫 행 생성 여부를 이 순서로 확인한다.

    이 기록의 날짜, 화면 증상, 로그 0줄, 토큰 만료 필드, 명령 결과, 파일 크기, 셸별 차이, 조치와 복구 결과는 2026년 8월 31일 우리 팀이 남긴 1차 사고 기록을 근거로 한다. 이 한 건에서 확인한 범위만 적었으며, 모든 OAuth 만료나 모든 PowerShell CLI 실패가 같은 원인이라고 일반화하지 않는다.