문제는 이렇다. 내가 대화하며 쓰는 Claude Code 세션은 이틀 사이 사고를 세 번 냈다. 그런데 같은 설정으로 GitHub 이슈를 혼자 처리하는 auto-issue 헤드리스 워커(headless worker — 화면도 사람도 없이 명령 한 줄로 켜져서 이슈 하나를 끝내면 꺼지는 자동 작업자)는 같은 사고를 한 번도 내지 않았다. 둘은 동일한 user scope 기계(사용자 계정 전체에 적용되는 훅·규칙·스킬 설정 묶음) 위에서 돈다. 그래서 어디서 갈리는지 보려고 hook pipeline(훅 파이프라인 — 세션의 정해진 순간마다 자동으로 끼어드는 검사 스크립트의 줄) · spawn(스폰 — 자식 작업을 새로 띄우는 일) · teammate(팀메이트 — 메시지를 주고받는 동료 에이전트) · subagent(하위 에이전트 — 일을 맡기고 결과만 받는 자식 에이전트) · skill(스킬 — 정해진 절차를 담아 둔 명령 묶음) 다섯 축을 user/project scope 레벨에서 실측 비교했다. 분기는 정확히 세 곳에서 난다.
결론부터 말하면 출발점은 완전히 같다. 사용자 홈 폴더의 ~/.claude 디렉토리에 든 훅·규칙·스킬·플러그인은 프로세스 종류를 가리지 않는다. 대화형 세션도 헤드리스 워커도 같은 기반을 로드한다. 차이는 전부 그 위층에서 시작된다. 바로 project scope(프로젝트 폴더 안의 .claude/ 설정으로, 그 프로젝트에서만 적용되는 층)를 어떤 경로로 손에 넣느냐다.
훅이 걸리는 16개 이벤트(세션 시작·프롬프트 제출·도구 실행 전처럼 훅이 끼어드는 순간의 종류)는 양쪽 모두 존재한다. 훅은 대개 게이트(gate — 조건을 못 맞추면 다음 단계로 못 넘어가게 막는 검문소)로 동작한다. 다른 것은 발화 빈도(훅이 실제로 실행되는 횟수)와 실패 완화 장치의 위치다. 대화형은 사용자가 말을 걸 때마다, 즉 턴마다 주입(훅이 모델의 컨텍스트, 곧 한 번에 읽는 작업 메모리에 밀어 넣는 규칙·기억·안내문)이 반복된다. 워커는 단 한 번의 프롬프트에 모든 주입이 실린다.
| 이벤트 | 대화형 세션 | auto-issue 워커 |
|---|---|---|
| SessionStart ×4 | 세션 시작 시 1회 | 디스패치마다 1회(dispatcher, 곧 보드에서 이슈를 꺼내 워커를 띄우는 조정 프로그램이 이슈를 배정할 때마다). 매번 fresh 컨텍스트(아무것도 남지 않은 새 작업 메모리)로 시작한다 |
| UserPromptSubmit ×9 | 매 사용자 턴 재발화. pending 신호(아직 처리되지 않은 대기 알림)와 memory 주입(과거 대화에서 뽑아 둔 기억)이 턴마다 반복된다 | 정확히 1회. claude -p(프롬프트 한 줄을 넘기고 결과만 받는 비대화 실행 명령) 단발 프롬프트에 전부 실린다 |
| Stop ×10 | 차단이 사용자에게 표면화되고, 대화로 수습한다 | 런(워커 실행 1회) 내부 루프 → attempts cap(재시도 횟수 상한) → dispatcher 산출물 게이트(디스패처가 워커의 결과물을 검사해 통과 여부를 정하는 마지막 검문소)가 최종 방어 |
| Pre/PostCompact | 오래 산 세션에서 실제로 발화한다. 압축(compact — 작업 메모리가 꽉 차면 요약해 줄이는 동작)만 반복하고 일은 못 하는 스래싱 실측 | 사실상 미발화. 한도를 넘기면 BIGCTX 모델 스왑 재기동(더 큰 컨텍스트 창을 가진 모델로 바꿔 워커를 처음부터 다시 띄우는 방식)으로 대응한다. 완화 장치가 프로세스 외부에 있다 |
| PreToolUse ×13 | exit-2 게이트(훅이 종료 코드 2를 돌려주면 그 도구 실행을 막는 차단 장치) + 권한 프롬프트 이중(사용자에게 허용 여부를 묻는 창까지 한 겹 더) | exit-2 게이트만 남는다. --dangerously-skip-permissions(권한 확인 창을 전부 건너뛰는 실행 옵션)로 프롬프트 계층을 제거한다 |
통념과 반대다. 흔히 사람이 붙어 있는 대화형 세션이 더 많은 검사를 받을 것이라 생각한다. 실측은 거꾸로다. 지금 이 대화형 세션(hugh-soft, 이 글을 쓰고 있는 프로젝트)은 project hook 0개로 돌고, bs 워커(bs-hanyang 프로젝트의 이슈를 맡은 auto-issue 워커)의 워크트리에는 hooks와 settings.json(훅 등록과 권한 설정을 담는 Claude Code 설정 파일) 풀셋이 실재한다. 본 repo(저장소)에서 이 파일들은 git 미추적(git이 관리하지 않는 파일)이라 워크트리로 저절로 전파될 수 없다. settings.local.json(개인 로컬 전용 설정 파일로, 커밋되지 않는다)의 존재가 증거다. 부트스트랩이 디스패치 시점에 설치한 것이다. 워커가 project scope 게이트를 더 완전하게 받는다.
핵심은 자식이 무엇을 물려받느냐다. 대화형의 spawn은 같은 하네스(harness — 모델을 감싸고 도구 실행·훅·검증을 맡는 실행 틀로, 여기서는 Claude Code 프로세스 자체를 가리킨다) 프로세스 안에서 일어난다. 그래서 자식은 부모의 컨텍스트와 대형 주입을 그대로 물려받는다. 이것이 in-process(같은 프로세스 안) 상속이다. 워커의 spawn은 별도 OS 프로세스라 아무것도 물려받지 않는다. 이것이 OS-process(운영체제 프로세스 단위) 격리다. 대신 환경도, 제어 수단도 완전히 다르다.
세 번째 분기가 사고를 갈랐다. 대화형의 팀메이트는 주소 지정 가능한 mailbox 실체(이름을 지정해 메시지를 보낼 수 있는 우편함을 가진 에이전트)고, 워커는 팀메이트가 아니다. 그리고 상태 신원(세션 ID·ledger·memory scope, 곧 이 프로세스가 "나"라고 알고 있는 세션 번호·작업 기록·기억의 범위)의 격리 수준이 실제 사고 3건의 발생 여부를 정확히 갈랐다. 여기서 ledger는 원장(ledger — 작업 완료를 주장할 때마다 근거를 줄줄이 적어 두는 기록 파일)이고, attestation(검사를 통과했다는 도장으로, 코드 상태가 바뀌면 효력을 잃는다)은 검증 서명이다.
"말은 통하지만, 상태가 섞인다"
idle_notification(상대 에이전트가 일을 마치고 쉬는 상태가 되면 오는 알림)을 받는다 (실측: 리뷰어 통지 2회 배달)work-recheck(작업 완료 주장을 다시 뜯어보는 재검증 게이트) ledger를 동시에 열린 다른 세션들과 같은 디렉토리에서 공유한다"말은 안 통하지만, 상태가 깨끗하다"
백그라운드 subagent가 세션의 대형 주입을 상속했다. 그 결과 Prompt is too long(프롬프트가 모델 한도보다 길다는 오류)으로 죽었다. 이어서 autocompact(작업 메모리가 차면 자동으로 요약 압축하는 기능) 스래싱으로 자원만 소모했다.
공유 상태 디렉토리에 다른 세션·다른 프로젝트의 경로가 혼입됐다. 이 세션과 무관한 파일 변경이 검증 상태를 오염시켰다.
6시간마다 도는 스케줄러가 공유 경로의 파일 .seen.json(이미 본 항목을 적어 두는 목록)을 갱신했다. 그때마다 검증 서명이 stale(낡아서 효력 없음)이 됐다. 작업을 하지 않았는데도 게이트가 다시 막혔다.
"목록은 양쪽에 똑같이 주입된다"
.claude/skills 폴더 기준이다auto_issue.py(이슈 선택과 워커 프롬프트 조립을 맡는 보조 프로그램)에는 ~/.claude(사용자 홈의 Claude 설정 폴더)를 프로젝트 폴더로 잘못 읽지 않게 막는 가드가 있다 (auto_issue.py:276)"재량이냐, 계약이냐"
build_issue_prompt(이슈 내용으로 워커 프롬프트를 조립하는 함수)가 계약으로 호출을 지정한다/init-project(프로젝트 설정과 계획을 만드는 스킬) → /team(구현과 검증을 맡는 팀 오케스트레이터 스킬)" 순서가 프롬프트에 명문화돼 있다user scope 기계는 완전히 동일하다. 갈리는 곳은 세 곳뿐이다. 이 세션이 이틀간 겪은 사고 3건(스폰 실패·ledger 오염·attestation 무효화)은 전부 대화형 쪽 구조의 약점이 발화한 것이다. 워커 구조에서는 셋 다 설계상 발생하지 않는다. 그렇다고 워커가 늘 답은 아니다. 대화를 주고받는 중에 비로소 과녁(무엇을 만들어야 하는지)이 정해지는 작업은, 동결된(처음 적힌 그대로 고정된) 이슈로는 표현할 수 없다. 스펙을 문장으로 적을 수 있으면 워커에게, 적을 수 없으면 대화형에.