Claude Code와 Codex 사이에서 고민하는 것을 그만두었습니다. 두 가지를 하나의 채팅창에 넣었습니다.
요약
사용자는 Claude Code와 Codex 중 하나를 선택하는 대신, 두 도구를 하나의 인터페이스에서 전환하며 사용할 수 있는 통합 워크플로우를 제안합니다. 이를 위해 CliGate를 통해 직접적인 런타임 모드를 기본값으로 설정하여, 에이전트의 불필요한 중재 없이 사용자의 명령이 즉각적으로 실행되도록 설계했습니다.
핵심 포인트
- Claude Code와 Codex의 장점을 모두 활용하기 위해 인터페이스 전환 비용을 줄이는 것이 핵심임
- 직접적인 런타임 작업(direct runtime work)과 어시스턴트 중재 작업(assistant-mediated work)을 명확히 구분해야 함
- 코딩 에이전트가 모든 메시지를 가로채는 것을 방지하기 위해 직접 런타임 모드를 기본값으로 설정함
- 명령어(/cligate, /runtime)를 통한 간결한 모드 전환 방식을 통해 제어력을 높임
모든 "Claude Code vs Codex" 비교는 결국 똑같이 지루한 진실에 부딪힙니다. 저는 영원히 하나만을 선택하고 싶지 않다는 것입니다. 어떤 작업은 Claude Code에서 더 잘 수행되는 것처럼 느껴지고, 어떤 작업은 Codex에서 더 잘 느껴집니다. 어떤 날은 한 계정이 속도 제한 (rate-limited)에 걸리기도 하고, 어떤 모델이 더 저렴하거나, 혹은 특정 런타임 (runtime)이 이미 내가 필요한 컨텍스트 (context)를 보유하고 있기도 합니다. 짜증 나는 부분은 더 나은 에이전트 (agent)를 선택하는 것이 아닙니다. 짜증 나는 부분은 마음을 바꿀 때마다 인터페이스 (surface)를 전환해야 한다는 것입니다.
제가 원했던 워크플로우 (workflow)는 다음과 같은 일을 할 수 있는 하나의 로컬 채팅창이었습니다: 이 작업에는 Codex를 사용한다. 동일한 런타임을 계속 유지한다. 다음 작업을 위해 Claude Code로 전환한다. 어시스턴트 (assistant)에게 먼저 계획을 세우라고 요청한다. 다시 직접적인 런타임 모드로 돌아간다. 이것은 UI 문제처럼 들리지만, 실제로는 제어 (control)의 문제입니다.
두 가지 서로 다른 일이 일어나고 있습니다: 다음 메시지가 Claude Code나 Codex로 바로 전달되어야 하는 직접적인 런타임 작업 (direct runtime work), 그리고 감독관 (supervisor)이 답변할지, 질문할지, 아니면 런타임에 위임할지를 결정하는 어시스턴트 중재 작업 (assistant-mediated work)입니다. 만약 이 두 모드가 명시적이지 않다면, 채팅창은 함정이 됩니다. "더 작게 만들어줘"와 같은 짧은 후속 질문은 다음과 같은 의미를 가질 수 있습니다:
- 활성화된 Codex 런타임을 계속한다
- 제품 어시스턴트에게 요청한다
- 새로운 Claude Code 작업을 시작한다
- 대기 중인 승인에 응답한다
여기서 잘못 추측하는 것이 바로 코딩 에이전트 (coding agents)가 좌절감을 주는 방식입니다. 그래서 저는 직접적인 런타임을 기본값으로 만들었습니다. CliGate에서 채팅 UI 대화는 이제 기본적으로 직접적인 런타임 모드로 설정됩니다. 이것은 의도적인 선택이었습니다. 제가 코딩 에이전트를 사용할 때 대부분의 경우, 저는 어시스턴트가 모든 메시지를 가로채서 "내가 의도한 것이 무엇인지 생각"하기를 원하지 않습니다. 저는 제가 명시적으로 다른 것을 요청할 때까지 현재의 런타임이 계속되기를 원합니다.
이 동작을 고정하는 테스트가 있습니다:
test ( ' ChatUiConversationStore는 새로운 chat-ui 대화를 direct-runtime 제어 모드로 기본 설정한다 ' , () => {
const conversation = conversationStore.findOrCreateBySessionId ( ' chat-ui-default-direct-runtime-1 ' );
assert.equal ( conversation.metadata?.assistantCore?.mode , ' direct-runtime ' );
assert .
equal ( conversation . metadata ?. assistantCore ?. controlMode , ' direct-runtime ' ); }); 이는 일반적인 채팅 메시지가 자동으로 "assistant work(어시스턴트 작업)"가 되지 않음을 의미합니다. 메시지는 런타임 (runtime) 경로에 머뭅니다. UI를 사용 가능하게 만든 두 가지 명령어를 저는 또 다른 복잡한 설정 패널 대신 작은 모드 전환 방식으로 구현했습니다: /cligate / /runtime. 모드 파서 (parser)는 의도적으로 매우 작게 설계되었습니다:
const cligateMatch = trimmed . match ( /^ / cligate (?:\s + ( .+ ))? $/i s );
if ( cligateMatch ) {
return { command : ' cligate ' , args : String ( cligateMatch [ 1 ] || '' ). trim () };
}
if ( /^ / runtime$/i . test ( trimmed )) {
return { command : ' runtime ' , args : '' };
}
동작 방식은 다음과 같습니다:
/cligate: 어시스턴트 모드 (assistant mode) 진입/cligate <task>: 어시스턴트가 중재하는 하나의 작업 실행/runtime: 어시스턴트 모드를 종료하고 직접 런타임 라우팅 (direct runtime routing)으로 복귀
이 하나의 탈출구 (escape hatch)가 매우 중요합니다. 어시스턴트에게 계획이나 조율을 요청하는 일이 끝나면, 다음 메시지는 번거로운 절차 없이 활성화된 Claude Code 또는 Codex 세션으로 바로 돌아가기를 원하기 때문입니다.
이제 라우트 (route)는 런타임에 닿기 전에 결정을 내립니다. 채팅 라우트는 먼저 어시스턴트 모드 서비스 (assistant mode service)가 메시지를 처리할 기회를 줍니다. 만약 어시스턴트 모드가 활성화되어 있지 않고 /cligate 명령어가 없다면, null을 반환하며 메시지는 일반적인 런타임 경로로 내려갑니다:
const assistantResult = await this . assistantModeService . maybeHandleMessage ({
conversation ,
text ,
defaultRuntimeProvider ,
cwd ,
model ,
executionMode : assistantExecutionMode ,
onBackgroundResult
});
if ( assistantResult ) {
return {
... assistantResult ,
previousSessionId : conversation . activeRuntimeSessionId || null ,
conversation : assistantResult . conversation || this . conversationStore . get ( conversation . id )
};
}
그 이후에야 서비스는 런타임으로 직접 라우팅합니다:
const result = await this . messageService .
routeUserMessage ({ message : { text }, conversation , defaultRuntimeProvider , cwd , model , metadata : { assistantMode : getAssistantControlMode ( conversation ), source : { kind : ' chat-ui ' , sessionId : String ( sessionId || '' ), conversationId : conversation . id } } }); 그러한 분리가 핵심입니다. 어시스턴트 (Assistant)가 존재한다는 이유만으로 직접적인 런타임 (Runtime) 메시지를 가로챌 수는 없습니다. 이것이 왜 제가 시도했던 "스마트한" 기본값보다 더 나은가 하면, 저는 어시스턴트를 도움이 되도록 만들려고 노력했습니다. 그러다 "도움이 된다"는 것이 코딩 워크플로우 (Coding workflow)에서는 위험할 수 있다는 것을 깨달았습니다. 만약 런타임이 입력을 기다리고 있다면, 가장 놀랍지 않은(예측 가능한) 행동은 해당 런타임에 입력을 보내는 것입니다. 만약 작업에 승인 대기 중인 항목이 있다면, 가장 놀랍지 않은 행동은 그 승인을 해결하는 것입니다. 사용자가 명시적으로 /cligate를 입력한다면, 그때 어시스턴트가 개입할 수 있습니다. 그 결과는 덜 마법처럼 느껴지지만, 훨씬 더 신뢰하기 쉬워집니다. 예를 들어:
"실패하는 유닛 테스트를 수정해줘"는 Codex 런타임을 시작할 수 있습니다. 그다음: "더 간단한 패치를 시도해봐"는 해당 런타임을 계속 이어갑니다. 그다음: /cligate 이 실패를 마지막 실행과 비교하고 계속 진행해줘는 어시스턴트가 상황을 추론할 수 있게 합니다. 그다음: /runtime은 대화를 다시 직접적인 런타임 경로로 되돌려 놓습니다. 이것이 제가 원했던 루프 (Loop)입니다.
백그라운드 실행에는 자체적인 가드레일 (Guardrail)이 필요했습니다. 다른 버그는 채팅 UI (Chat UI)에서 어시스턴트 실행을 비동기 (Asynchronous)로 만든 후에 나타났습니다. 만약 어시스턴트가 중재하는 작업이 런타임을 시작하고 나중에 반환된다면, UI는 백그라운드 결과를 유지해야 합니다. 하지만 사용자가 이미 새로운 실행을 시작했다면, 이전 어시스턴트 실행의 오래된 출력을 추가해서는 안 됩니다. 그래서 라우트 (Route)는 대기 중인 어시스턴트 실행 ID를 기록합니다:
if ( result ?. type === ' assistant_run_accepted ' && result ?. assistantRun ?. id ) {
chatUiConversationStore . patch ( conversation . id , {
metadata : { ...( conversation . metadata || {}),
uiChatPendingAssistantRunId : String ( result . assistantRun . id || '' ).
trim() } }); } 그리고 백그라운드 콜백은 오래된 결과(stale results)를 거부합니다:
if ( getPendingUiAssistantRunId ( conversation ) !== backgroundRunId ) { return ; }
이것이 화려한 코드는 아니지만, 매우 실질적인 UI 버그를 방지합니다: 어시스턴트 작업(assistant task)을 시작한 후 첫 번째 작업이 끝나기 전에 다른 작업을 시작하면, 새로운 작업 아래에 이전 답변이 나타나는 현상을 말합니다. 사양하겠습니다. 모델 오버라이드(model override) 버그 또한 또 다른 작은 실수 유발 요인(footgun)이었습니다.
Claude Code와 Codex가 혼합된 채팅 인터페이스에서 중요했던 세부 사항이 하나 더 있었습니다. 일반적인 채팅 UI에는 로컬 모델 선택기(local model selector)가 있습니다. 런타임 라우팅(Runtime routing)은 자체적인 프로바이더(provider) 의미론을 가집니다. 만약 로컬 채팅 모델 오버라이드가 런타임 라우팅으로 새어 나가게 둔다면, 사용자의 의도와 달리 Claude Code 런타임 경로에 gpt-5.4와 같은 값을 실수로 보낼 수도 있습니다. 따라서 로컬 채팅 UI 런타임 메시지의 경우, 라우팅은 의도적으로 UI 채팅 모델 오버라이드를 무시합니다:
const runtimeModelOverride = isExternalConversation ? String ( model || '' ) : '' ;
이에 대한 테스트도 존재합니다:
assert . equal ( captured [ 0 ]. model , '' );
assert . equal ( captured [ 0 ]. defaultRuntimeProvider , ' claude-code ' );
이 작은 규칙 덕분에 UI가 "선택된 채팅 모델"과 "런타임 프로바이더"를 동일한 개념으로 착각하는 상황을 방지할 수 있었습니다. 두 개념은 서로 다릅니다.
설정 방법
CliGate 시작: npx cligate@latest start
대시보드 열기: http://localhost:8081
그 다음 Chat 페이지를 제어 인터페이스(control surface)로 사용합니다:
- 런타임 프로바이더(runtime provider)로 Codex 또는 Claude Code를 선택합니다.
- 직접적인 런타임 작업을 시작하기 위해 일반적인 작업을 보냅니다.
- 해당 런타임을 계속 사용하려면 후속 질문(follow-ups)을 계속 보냅니다.
- 어시스턴트가 중재하는 계획(planning)이나 위임(delegation)이 필요할 때는 /cligate를 사용합니다.
- 직접적인 런타임 경로로 돌아가려면 /runtime을 사용합니다.
이것이 제가 처음부터 원했던 워크플로우입니다. "어떤 터미널 에이전트가 승리하는가?"가 아니라, "어느 한쪽에 맞춰 워크플로우를 재구축하지 않고도 둘 다 계속 사용할 수 있는가?"에 가깝습니다.
교훈
현재의 AI 코딩 도구 열풍은 비교를 유혹합니다. Claude Code 대 Codex. Codex 대 Gemini CLI. 터미널 에이전트 대 IDE 에이전트.
그러한 비교는 유용하지만, 일상적인 문제는 놓치고 있습니다. 개발자들은 단순히 도구를 선택하는 것이 아니라, 도구 사이를 이동하기 때문입니다. 저에게 유용한 추상화는 더 똑똑한 챗봇이 아니었습니다. 그것은 명확한 소유권(ownership)을 가진 채팅 제어 인터페이스(chat control surface)였습니다. 즉, 기본적으로는 직접적인 런타임(direct runtime)을 제공하되, 요청 시에만 어시스턴트 모드(assistant mode)로 작동하며, 스티키 런타임 지속성(sticky runtime continuation)을 유지하고, 오래된 백그라운드 결과(stale background result)로부터 보호하며, 모델의 의도치 않은 오버라이드(model override)가 런타임 작업으로 유출되지 않도록 하는 방식입니다. 이를 통해 Claude Code와 Codex는 서로 경쟁하는 터미널이라기보다, 같은 로컬 책상 뒤에 있는 두 명의 작업자처럼 느껴지게 되었습니다. 구현 내용을 살펴보고 싶다면, 프로젝트는 GitHub의 CliGate에 있습니다. 다른 사람들은 이 문제를 어떻게 다루고 있는지 궁금합니다. 여러분은 하나의 코딩 에이전트(coding agent)를 선택하시나요, 아니면 여러 에이전트가 공존할 수 있는 워크플로(workflow)를 구축하시나요?
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기