invariant
WebUI 피파한테는 정체성이 들어올 수 있는 길이 둘 있고, 아키텍처는 둘이 절대 섞이면 안 된다고 해.
- ContextEngine 이 vault 를
system_prompt로 지어서, Claude adapter 가 그걸 SDK 에 넘겨. - filesystem 컨텍스트 — Claude Code 의 원래 channel 이야.
~/.claude/settings.json, 프로젝트.claude/settings.json,CLAUDE.md계층, SessionStart hook, 프로젝트마다 쌓이는 auto-memory 인덱스. terminal 피파 가claudeCLI 로 직접 불릴 때 쓰는 길이지.
WebUI subprocess 는 channel 1 만, terminal 은 channel 2 만 봐야 해. 이중 주입 없음, 토큰 새는 것 없음, 동작 표류 없음. 이 설계는 맞고, 바뀐 적도 없어.
첫날부터 틀렸던 문장
2026-04-30 에 쓴 아키텍처 문서는 그 장치를 이렇게 적었어. adapter 는 setting_sources 를 일부러 비워 두고, None 이면 SDK 가 CLI 호출에서 --setting-sources 를 빼는데, 문서 말로는 그러면 subprocess 가 filesystem 설정을 하나도 안 읽는다고. 그 기본값이 기둥이니까 미래의 나는 손대지 말라는 말까지 붙어 있었어.
앞 절반은 맞았어. 뒤 절반은 한 번도 시험해 본 적이 없었고, 그땐 이미 틀린 말이었어. 초기 SDK 는 정말로 기본이 격리였어. None 이면 빈 --setting-sources 를 보냈거든. 그런데 2026-03-31 에 나온 0.1.53 부터는 값이 없으면 flag 자체를 안 보내. changelog 에는 flag 파싱 버그를 고쳤다고만 적혀 있고, 뭘 읽게 되는지는 한마디도 없어. 하지만 flag 를 못 받은 CLI 는 자기 기본값대로 가고, 그 기본값이 '전부 읽기' 야. 문서보다 한 달, cwkPippa 첫 커밋보다 여드레 앞선 일이야. 문서를 쓸 때 고정돼 있던 0.1.56 엔 버그가 하나 더 있었어. [] 까지 falsy 로 보고 버렸거든. 그래서 0.1.60 전까진 어느 값을 넣어도 격리가 안 됐어. WebUI 는 격리된 채로 돈 적이 한 번도 없어. 지금 SDK 문서는 아예 대놓고 적어. None 은 CLI 처럼 모든 소스를 읽고, 격리는 [] 라고.
그러니까 WebUI 첫날부터 2026-09-18 까지, spawn 할 때마다 terminal 피파의 channel 이 통째로 vault 위에 얹혀 있었던 거야. 한 단어짜리 호출로 재 보니 입력이 43,867 토큰이었어. 사용자 CLAUDE.md, cwkPippa 자기 CLAUDE.md, terminal 피파의 auto-memory 인덱스, 그리고 그날 아침부터는 새로 생긴 전역 SessionStart hook 이 받아 온 기억 머리까지. subprocess 는 terminal 피파의 settings 환경변수로 돌았고, 모델을 안 정한 호출은 아빠 terminal 의 기본 모델을 탔어. terminal 세션 보라고 써 둔 집 안내문을 WebUI 가 지시문으로 읽고 있었던 셈이지.
스위치는 하나가 아니라 둘
None 을 [] 로 바꾸는 걸로는 반만 막혀. [] 는 settings 파일, CLAUDE.md 계층, hook, plugin 을 막아. 그런데 auto-memory, 그러니까 Claude Code 가 프로젝트마다 들고 있는 MEMORY.md 인덱스는 setting source 가 아니야. CLI 가 따로 관리해서, [] 에서도 그대로 읽혔어. 같은 한 단어 호출이 [] 로는 43,867 에서 8,018 토큰까지만 내려갔고, auto-memory 까지 꺼야 442 가 됐어.
2026-09-18 에 나간 수정은 두 스위치를 작은 helper 하나에 같이 넣었고, WebUI 쪽 spawn 은 전부 그걸 거쳐. 채팅 adapter, Ollama 프로토콜 재생성기, 모델 probe 셋 다. adapter 는 이제 호출마다 모델도 꼭 적어. 격리된 CLI 는 terminal 기본값을 물려받지 않으니까, 모자라면 Admin 에서 고정한 모델로 채워.
setting_sources 에 'user' 나 'project' 를 넣으면, terminal 세션 보라고 만든 SessionStart hook 이 WebUI 턴마다 터지고, terminal 의 규칙이랑 환경변수랑 plugin 까지 딸려 와서 정체성이 두 번 들어가. 옛 경고는 어느 방향이 틀렸는지는 늘 맞혔어. 어느 값이 안전한지는 틀렸고, 두 번째 스위치는 아예 몰랐지. WebUI 피파가 자기 코드 만지다가 CLAUDE.md 가 필요하면 Read 로 읽으면 돼. 주입이 아니라 선택으로.
두 channel 을 나눈 건 정체성을 둘로 나눈 게 아니야. 같은 vault 를 서로 다른 host 가 어떻게 주입하는지만 갈라 놓은 거야. WebUI 에선 ContextEngine 이 명시적으로 조립하고, terminal 에선 host 가 시작할 때 bootstrap 과 프로젝트 overlay 를 읽어. 어느 쪽도 상대 channel 이 우연히 따라올 거라 기대하면 안 돼.
flag 말고 동작을 시험해
flag 는 처음부터 없었어. 없는 게 설계였고, 설치된 CLI 가 없는 flag 를 받아서 뭘 하는지는 코드를 아무리 읽어도 안 보여. 그래서 새 guard 는 flag 값을 아예 안 봐. adapter 가 실제로 만드는 옵션을 그대로 가져와서, 카나리아 다섯 개를 심어 둔 임시 프로젝트를 가리키게 한 다음 진짜 CLI 에 넘기고, 뭘 읽었는지 물어. 사용자와 프로젝트 CLAUDE.md, 사용자와 프로젝트 SessionStart hook, auto-memory MEMORY.md 가 그 다섯이야. 이 질문엔 모델 호출이 필요 없어서 quota 도 안 들어. 대조군도 있어. 같은 카나리아를 SDK 기본값으로 돌려서 다섯 개를 전부 봐야 해. 못 보면 카나리아가 안 걸려 있다는 뜻이니까, 테스트는 통과하는 대신 '눈먼 테스트' 라고 실패해. setting_sources is None 을 확인하는 테스트였다면, 문서가 버틴 딱 그 시간만큼 이 버그를 제자리에 붙들어 뒀을 거야.