-
켰는데 왜 안 먹지 — 하나의 모드를 Claude Code와 Codex 두 호스트에 (Scrooge 4편)AI 엔지니어링 2026. 6. 15. 17:50728x90반응형
🔗 scrooge
1. 도입부 — "같은 기능"이 호스트마다 다른 표면을 갖는다
압축 모드 하나를 Claude Code에서 잘 돌렸다고 하자. 이걸 Codex에도 태우려 한다. 기능은 "똑같다" — 사용자 입력에 압축 지시를 끼워 LLM이 짧게 답하게 만든다. 그런데 막상 옮기려 보면, 두 호스트가 개입하는 메커니즘 자체가 다르다. 같은 기능인데 표면이 갈린다.
여기에 더 음험한 문제가 겹친다. "scrooge가 켜졌다"는 말이 실은 세 가지 다른 의미로 쓰이고 있었다는 점이다. 설치됐다는 뜻인지, 지금 활성이라는 뜻인지, 이 세션에만 거는 지시라는 뜻인지. 이 셋을 섞으면 "분명 켰는데 왜 안 먹지" 같은 혼란이 반드시 생긴다.
이 글에서 당신이 얻어갈 것은 두 가지다. 하나는 같은 기능을 다른 hook 메커니즘에 이식하는 패턴, 다른 하나는 "설치 범위 ≠ 활성 상태 ≠ 세션 지시"라는 세 개념을 분리하는 멘탈 모델과, 그걸 어겼을 때 생기는 구체적 함정이다. 워크플로 글인 만큼 멱등성·가드레일·검증을 특히 강조한다.
2. 핵심 개념 — 섞이기 쉬운 세 가지
용어 1줄 정의 — hook: 에이전트의 동작 흐름 중간에 끼어들어 입력·출력을 가로채거나 변형하는 확장 지점. scrooge는 hook으로 사용자 입력에 압축 지시를 주입한다.
scrooge를 멀티 호스트로 옮기며 가장 중요했던 통찰은, "scrooge 켜짐"을 세 층으로 분리한 것이다.

설치 범위·활성 상태·세션 지시 3개념 분리와, 단일 전역 상태 파일이 만드는 함정 - 설치 범위(install scope): hook이 어디에 설치됐는가. user-level(전역)에 깔렸는가, 특정 위치에만 깔렸는가. → 핵심: 설치됐다고 켜진 게 아니다.
- 활성 상태(active state): 지금 켜져 있는가. scrooge는 이걸 단일 전역 파일
~/.claude/.scrooge-active하나로 표현한다. 파일이 있으면 켜짐, 없으면 꺼짐. - 세션 지시(session instruction): 이 세션에만 거는 일회성 지시. 전역 상태를 건드리지 않고 현재 대화에만 압축을 적용하는 경로.
이 셋은 서로 독립이다. 설치돼 있어도 비활성일 수 있고, 전역은 꺼져 있는데 한 세션만 켤 수도 있다. 이 독립성을 인지하지 못하면 디버깅이 미궁에 빠진다.
3. 동작 원리 — 다른 메커니즘, 같은 기능
호스트별로 갈리는 hook 표면
같은 압축 모드인데 두 호스트의 개입 방식이 다르다.
- Claude Code:
UserPromptSubmithook으로 사용자 입력 제출 시점에 개입하고, 가시 출력으로 압축 지시를 드러낸다. - Codex: hook intercept로 흐름을 가로채는 방식. 같은 목적이지만 경로가 다르다.
핵심은 "기능 명세는 하나, 호스트 어댑터는 둘"로 본 것이다. 압축 모드라는 추상은 공유하되, 각 호스트의 hook 메커니즘에 맞는 어댑터를 따로 둔다. FE에서 같은 컴포넌트를 다른 렌더 타깃에 태우는 것과 같은 구조다 — 로직은 공유, 어댑터는 분리.
활성 상태는 왜 "전역 파일 하나"인가, 그리고 그 함정
활성 상태를 단일 전역 파일(
~/.claude/.scrooge-active)로 둔 건 의도적 단순화다. 상태 관리 코드는 이 파일을 가리키는getStatePath()한 군데로 모인다(hooks/scrooge-config.js의 해당 로직). 프로젝트별·세션별 키가 없으니 구현이 단순하고, "지금 켜졌나?"의 답이 항상 명확하다.대가는 켜고 끄기가 전역이라는 점이다. 그리고 여기서 이 글의 핵심 함정이 나온다.
텍스트 명령
/scrooge는 전역 상태 파일을 켜고 끈다 → 모든 세션에 영향.
반면 skill 단독 호출은 그 세션에만 적용되고 전역 상태는 건드리지 않는다.즉 "scrooge를 켜는" 두 경로가 서로 다른 층을 건드린다. 사용자가 한 세션에서 skill로 켜고 "켰다"고 믿은 뒤 다른 세션을 열면, 전역 상태는 여전히 꺼져 있어 "어, 껐는데 왜 안 먹지(혹은 켰는데 왜 안 먹지)"가 된다. 이건 버그가 아니라 세 개념(②와 ③)을 섞었을 때 필연적으로 생기는 혼란이다. 그래서 이 함정을 코드 주석이나 버그가 아니라 설계 문서/멘탈 모델 레벨에서 명시하는 게 중요하다(이 동작이 왜 전역인지 코드로 추적한 작업이 한 세션에 기록돼 있다).
멱등한 설치기 — 재설치해도 같은 결과
멀티 호스트 + 전역 상태는 설치/재설치를 까다롭게 만든다. 이전 버전의 hook 블록이 설정 파일에 남아 중첩되거나(중첩 레거시 hook), 더는 안 쓰는 상태가 남는(stale state) 문제다. scrooge 설치기는 이걸 멱등(idempotent)하게 처리한다 — 몇 번을 재설치해도 결과가 같도록, 설치 전에 중첩 레거시 hook 블록과 stale state를 청소한다. 설치가 멱등하지 않으면, 재설치할 때마다 hook이 쌓여 같은 입력에 압축 지시가 두 번 끼는 식의 부작용이 난다.
4. 실무 적용 — 멀티 호스트 어댑터 + 멱등 설치
이 시리즈에서 "코드 예제" 자리는 hook 설정·설치기 구조가 채운다.
✅ Good Practice — 어댑터 분리 + 멱등 설치 + 상태 단일화
// 1) 활성 상태는 단일 전역 파일 한 군데로 (getStatePath 하나) function getStatePath() { return path.join(os.homedir(), ".claude", ".scrooge-active"); } const isActive = () => fs.existsSync(getStatePath()); // 2) 호스트별 어댑터 분리 — 기능 명세는 공유, 메커니즘만 다름 const adapters = { "claude-code": injectViaUserPromptSubmitHook, // 가시 출력 "codex": injectViaHookIntercept, // intercept }; // 3) 멱등 설치: 항상 "청소 → 설치" 순서 function install(host) { removeNestedLegacyHooks(host); // 중첩 레거시 hook 제거 clearStaleState(host); // stale state 청소 adapters[host].register(); // 그다음 설치 → 몇 번 돌려도 같은 결과 }핵심은 (a) 상태가 한 군데(
getStatePath)로 모이고, (b) 호스트 차이는 어댑터로 격리되며, (c) 설치가 항상 "청소 먼저"라 멱등하다는 점이다.❌ Anti-Pattern — 세 개념을 섞고, 설치가 비멱등
// 잘못된 설계 function enableScrooge() { // 설치하면서 동시에 켜고, 세션 지시랑도 안 구분 appendHookBlock(config); // ← 재설치마다 블록이 쌓임 (비멱등) // 활성 상태를 세션마다 따로? 전역으로? 기준 없음 // skill 호출과 /scrooge가 같은 걸 켠다고 가정 ← 실제론 다른 층 }왜 문제인가.
appendHookBlock은 재설치할 때마다 hook을 누적시켜 압축 지시가 중복 주입된다(비멱등). 그리고 "설치=활성=세션"을 한 함수에 뭉치면, skill로 켠 것과/scrooge로 켠 것이 같다고 착각하게 만들어 3화에서 본 종류의 "재현 안 되는" 혼란을 낳는다.🔍 실행 결과 — 분리가 사주는 것
세 개념을 분리하고 설치를 멱등하게 만들면, "왜 안 먹지"의 답이 명확해진다. 설치는 됐는데 전역이 꺼졌나(②)? 이 세션만 skill로 켠 거였나(③)? 를 각각 확인하면 된다. 그리고 재설치를 몇 번 하든 hook이 한 벌만 남으므로, 압축 지시 중복 같은 부작용이 구조적으로 사라진다.
5. 장단점 및 고려사항
장점 단점·비용 ✓ 호스트가 늘어도 어댑터만 추가하면 됨 (로직 공유) ✗ 어댑터 레이어라는 추상 비용이 선행 ✓ 단일 전역 상태 파일로 "켜졌나?"가 항상 명확 ✗ 켜고 끄기가 전역 — 프로젝트별 분리 불가 ✓ 멱등 설치로 재설치 부작용(중복 hook) 차단 ✗ "청소 먼저" 로직을 호스트별로 유지해야 함 실무 팁 — 멀티 호스트 툴링 체크리스트
- "켜짐"을 세 층으로 분리한다: 설치 범위 / 활성 상태 / 세션 지시. 한 단어로 뭉치지 않는다.
- 기능 명세와 호스트 어댑터를 분리한다. 호스트가 늘 때 건드리는 건 어댑터뿐이어야 한다.
- 활성 상태의 단위를 명시적으로 정한다. 전역이면 "전역임"을 문서에 박고,
/scrooge와 skill 단독 호출의 차이를 사용자에게 분명히 알린다. - 설치를 멱등하게: 항상 "청소(중첩 레거시 hook·stale state) → 설치" 순서. 재설치가 결과를 바꾸면 안 된다.
- 검증 게이트를 둔다. 설치 후 "hook이 한 벌만 등록됐는가 / 상태 파일 경로가 단일한가"를 자동 확인한다.
핵심 3줄 요약
- 같은 압축 모드라도 호스트마다 hook 메커니즘이 다르다(Claude=
UserPromptSubmit+가시 출력, Codex=hook intercept). 기능은 공유, 어댑터는 분리로 푼다. - "scrooge 켜짐"은 설치 범위 ≠ 활성 상태 ≠ 세션 지시 세 개념이며, 섞으면 "켰는데 왜 안 먹지" 혼란이 난다. 특히
/scrooge(전역)와 skill 단독 호출(세션 한정)이 다른 층을 건드린다. - 멀티 호스트 + 전역 상태에서는 멱등 설치(청소 먼저)가 필수다.
저장소는 github.com/Kir93/scrooge-mode에 공개돼 있고, 이슈·PR·새 언어 rule 기여 모두 환영한다.
반응형'AI 엔지니어링' 카테고리의 다른 글
재현 안 되는 버그의 정답은 버그가 아니었다 — 비자명한 디버깅 기록 (Scrooge 3편) (0) 2026.06.14 안 깎는 것이 실력이다 — 압축 도구가 거부해야 할 출력 (Scrooge 2편) (0) 2026.06.13 측정하지 않으면 압축이 아니다 — 출력 압축 도구의 효과를 재는 법 (Scrooge 1편) (0) 2026.06.12 토큰은 돈이다 — 한국어를 위한 LLM 출력 압축 도구 (Scrooge 0편) (0) 2026.06.10