
Claude Code에서 Codex로 코드 리뷰를 위임했더니 9분 동안 응답이 없었던 이야기 — codex review
요약
Claude Code에서 Codex로 코드 리뷰를 요청할 때, 특정 파일만 지정하더라도 `--uncommitted` 플래그 사용 시 커밋되지 않은 모든 변경 사항을 리뷰 대상으로 삼는 문제를 다룹니다. 프롬프트 지시사항과 리뷰 대상 지정 플래그가 상호 배타적으로 작동하여 발생하는 현상과 그 회피책을 설명합니다.
핵심 포인트
- codex exec review 시 --uncommitted와 커스텀 프롬프트는 병용 불가
- 리뷰 대상 지정 플래그와 커스텀 지시는 상호 배타적임
- 의도치 않게 커밋되지 않은 모든 변경 사항이 리뷰 대상에 포함될 수 있음
- 회피책으로 순수 codex exec를 사용하여 stdin으로 diff를 전달하는 방식 권장
Claude Code에서 Codex로 코드 리뷰를 위임했더니,
codex exec review --uncommitted
가 약 9분 동안 돌아오지 않았다. 원인을 추적해 보니, "이 3개 파일만 봐줘"라고 작성한 프롬프트가 Codex에 한 번도 전달되지 않았다. codex exec review는 -를 지정하지 않는 한 리다이렉션된 stdin을 리뷰 지시로 읽지 않으며, 게다가 그 -는 --uncommitted와 병용할 수 없다. codex-cli 0.144.5에서 확인한 인자(argument) 사양의 함정과 회피책을 최소 재현 사례와 함께 남긴다.
요약 (결론부터)
codex exec review --uncommitted < prompt.txt로 실행해도, Codex는 기본prompt.txt의 내용을 리뷰 지시로 사용하지 않고current changes만 수신하여, 커밋되지 않은 변경 사항 전체를 리뷰하기 시작한다.[PROMPT]/--uncommitted/--base는 **상호 배타적(mutually exclusive)**으로, 리뷰 대상을 지정하는 플래그와 커스텀 지시는 병용할 수 없다. 이는 현재 사양이며, 변경 요청도 not planned로 닫혀 있다.- 커밋되지 않은 변경 사항을 여러 개 가진 상태에서 실행하면 그 전체가 대상이 되어, 예상보다 시간이 오래 걸릴 수 있다.
- 회피책은 순수한
codex exec에 가깝게 사용하는 것이다. 이 방식은 stdin을 읽는다. 대상 diff를 호출 측에서 선택하여 전달하는 것이 다루기 쉽다.
하고 싶었던 것
Claude Code로 개발하면서, 각 페이즈(phase)의 구현이 끝날 때마다 Codex에 코드 리뷰를 위임하는, 이른바 크로스 리뷰(cross-review) 구성을 돌리고 있었다. AI가 작성한 변경 사항을 다른 AI가 리뷰하게 하는, 최근 흔히 볼 수 있는 방식이다.
페이즈는 3개가 병행 중이었고, 워킹 트리(working tree)는 다음과 같은 상태였다.
- 페이즈 1: C API 변경 (리뷰 완료, commit 대기 중)
- 페이즈 2: C# Interop 변경 (리뷰 완료, commit 대기 중)
- 페이즈 3: WPF 변경 ← 이번에 리뷰하고 싶은 것은 이것뿐
즉, "커밋되지 않았지만 리뷰가 완료된" 변경 사항이 공존하고 있었다. 이 상태에서 Claude Code에게 페이즈 3의 리뷰를 Codex로 요청하게 했다.
Claude Code가 실제로 구성한 커맨드는 다음과 같다.
codex exec review --uncommitted \
< codex_review_phase3.txt \
> codex_review_phase3.log 2>&1
codex_review_phase3.txt의 내용은 리뷰 관점과 함께 "대상은 3개 파일(EditorPanelView.xaml / .cs / MainWindow.xaml.cs)뿐이며, 다른 커밋되지 않은 변경 사항은 무시해 주세요"라고 명시적으로 지시하는 프롬프트다.
지금 돌이켜보면, 이 시점에서 의도대로 작동하지 않을 것이 확정되어 있었다. 이유는 후술한다.
일어난 일
Codex가 침묵했다. 약 9분을 기다린 끝에 exit code 144로 종료되었다. 로그를 살펴보니, 리뷰 대상이어야 할 WPF는 건드리지 않고, 커밋되지 않은 다른 파일(C++의 readback 버퍼)을 시험 실행하기 시작했다.
[ RUN ] FrameBuffer.CreateRejectsZeroWidth
[ OK ] FrameBuffer.CreateRejectsZeroWidth (0 ms)
...
...
마지막 git status --short가 답의 절반을 알려주었다. 커밋되지 않은 readback 버퍼의 변경 사항(WPF 리뷰와는 무관함)이 남아 있었고, Codex는 이것들도 리뷰 대상으로 포함하고 있었다. 페이즈 1·2의 리뷰 완료된 변경 사항이 통째로 스코프(scope)에 들어갔고, 거기서부터 성실하게 코멘트를 달기 시작한 결과가 9분과 6,800행이었다.
남은 절반 — "왜 WPF만 보라는 지시가 먹히지 않았는가" — 에 도달하기까지, 두 번의 오진을 했다.
오진 그 1: 히스토리를 전부 추적하고 있는 것 아닌가?
체감상으로는 "무겁다", "응답이 없다"였기에, 처음에는 master로부터의 히스토리를 전부 추적하고 있는 것 아닌가? 라고 의심했다. 브랜치가 길게 분기되어 있었으므로, 히스토리를 거슬러 올라가고 있다면 이 정도의 무게도 설명이 된다고 생각했다.
이것은 오답이었다. --uncommitted
가 보는 것은 미커밋(uncommitted) 변경 사항뿐이며, 커밋 히스토리를 거슬러 올라가지는 않는다.
오진 그 두 번째: 프롬프트가 무시된 것이 아닌가
다음에 세운 가설은 이러했다. "리뷰 대상의 결정은 프롬프트가 아니라 CLI 플래그의 역할이며, --uncommitted를 전달한 이상 프롬프트에 무엇을 쓰더라도 전체가 대상이 된다".
그럴듯하며, 관측된 동작도 모두 설명할 수 있다. 한동안 이것을 결론이라고 생각했다.
이것도 틀렸다. 프롬프트는 무시된 것이 아니다. 전달되지 않았던 것이다.
실제 원인: 프롬프트가 읽히지 않았다
최소 재현으로 확인한다. 더미 리포지토리에 서로 무관한 미커밋 변경 사항을 2개 만들고, 프롬프트로 한쪽만 보도록 지시한다.
mkdir /tmp/scope-repro && cd /tmp/scope-repro && git init -q
printf 'def add(a, b):\n return a + b\n' > alpha.py
printf 'def greet(name):\n return "hello " + name\n' > beta.py
...
여기서 사고 당시와 완전히 동일한 형태 —— [PROMPT] 인수를 전달하지 않고, 프롬프트 파일을 stdin으로 리다이렉트 —— 하여 실행한다.
codex exec review --uncommitted < prompt.txt
명령어는 정상적으로 실행된다. 에러도 경고도 나오지 않는다. 그런데, Codex가 표준 출력(stdout)으로 그대로 흘려보내는 세션 로그의 user 메시지 —— 즉, Codex가 실제로 받은 지시 —— 를 보면 다음과 같았다.
user
current changes
prompt.txt의 내용이 아니다. current changes는 Codex 측에서 삽입하는 기본 문구다. 대조 실험으로서 [PROMPT]를 인수로 전달하면, user 메시지에는 그것이 그대로 나타난다.
$ codex exec review "MARKER-XYZ: review only alpha.py"
user
MARKER-XYZ: review only alpha.py
즉, < prompt.txt의 내용은 Codex에 전혀 전달되지 않았다.
이유는 --help에 적혀 있다. [PROMPT]는 위치 인수(positional argument)이며, 설명은 다음과 같다.
Arguments:
[PROMPT]
Custom review instructions. If '-' is used, read from stdin
-를 지정하지 않는 한, codex exec review는 리다이렉트된 stdin을 리뷰 지시로 읽어들이지 않는다. < prompt.txt는 셸(shell)이 stdin을 연결하고 있을 뿐, Codex는 그것을 입력 원천으로 취급하지 않는다. 결과적으로 프롬프트 파일은 사용되지 않은 채 처리가 진행된다.
-를 붙이면 되는 것 아닌가
그것도 안 됐다.
$ codex exec review --uncommitted - < prompt.txt
error: the argument '--uncommitted' cannot be used with '[PROMPT]'
인수 조합을 일일이 시도해 보니, 배타적 관계는 다음과 같았다.
| 명령어 | 결과 |
|---|---|
codex exec review --uncommitted "..." | error: '--uncommitted' cannot be used with '[PROMPT]' |
codex exec review --uncommitted - < f | 위와 동일 (- 도 [PROMPT] 취급) |
codex exec review --base main "..." | error: '--base <BRANCH>' cannot be used with '[PROMPT]' |
codex exec review --uncommitted --base main | error: '--uncommitted' cannot be used with '--base <BRANCH>' |
--uncommitted / --base / [PROMPT]
이 세 가지는 **상호 배타적(mutually exclusive)**이다. 리뷰 대상을 지정하는 플래그와 커스텀 리뷰 지시사항은 애초에 병용할 수 없다.
여기까지 오면 상황이 반전된다. Codex가 전체를 리뷰한 것은 지시를 무시했기 때문이 아니다. 오직 그것(current changes)만 전달받았기 때문이다.
current changes라고만 전달받았다면, 커밋되지 않은 변경 사항 전체를 리뷰하는 것은 지극히 정상적인 동작이다. 잘못된 것은 Codex가 아니라, 지시를 제대로 전달하지 못한 쪽이었다.
그리고 가장 질이 나쁜 부분은 여기다. -를 붙였다면 에러가 발생해 알아차릴 수 있었겠지만, -를 붙이지 않은 탓에 아무런 경고 없이 성공해 버린다. 잘못된 사용법 중 더 대충 사용한 쪽이 에러를 발생시키지 않는 것이다.
회피책
codex exec를 사용한다
① 순수한 codex exec review 사용을 중단하고, 순수한 codex exec로 전환한다. 두 명령의 --help를 비교해 보면 차이점이 명확하게 문서화되어 있다.
codex exec review:
[PROMPT]
Custom review instructions. If `-` is used, read from stdin
codex exec:
[PROMPT]
Initial instructions for the agent. If not provided as an argument (or if `-` is used),
instructions are read from stdin. If stdin is piped and a prompt is also provided, stdin
...
review는 -가 없는 한 stdin을 읽지 않는다. 반면 순수한 exec는 [PROMPT]가 생략될 때 stdin을 읽으며, 심지어 [PROMPT]와 stdin이 모두 존재하면 stdin을 <stdin> 블록으로 추가한다. 동일하게 < file 방식을 사용하더라도 결과가 다른 이유는 바로 이 차이 때문이다.
| 구분 | codex exec review | codex exec (순수) |
|---|---|---|
| 리뷰 대상 | CLI 플래그 (--uncommitted 등) | 프롬프트와 입력으로 지정 |
| 출력 형식 | Codex 측에서 구조화 | 프롬프트로 규정 |
< file | 읽지 않음 | 읽음 |
구체적인 전달 방법은 두 가지가 있다. 이를 구분해 두면 실수를 줄일 수 있다.
방식 A: 호출 측에서 diff를 확정하여 전달한다 (권장)
git diff HEAD -- alpha.py | codex exec "다음 diff를 리뷰해줘. 관점은 버그, 예외 처리, 네이밍이야. 일본어로 지적 사항만 나열해줘."
user 메시지를 보면 지시사항과 diff가 모두 전달된 것을 확인할 수 있다.
user
다음 diff를 리뷰해줘. 관점은 버그, 예외 처리, 네이밍이야. 일본어로 지적 사항만 나열해줘.
<stdin>
...
리뷰 대상인 diff를 호출 측에서 명시할 수 있다는 것이 장점이다. 다만 Codex는 셸(shell)을 사용할 수 있으므로, 전달된 diff 이외의 파일을 읽으러 갈 가능성까지 차단할 수는 없다. 어디까지나 "무엇을 리뷰해주길 원하는지"를 확실히 전달하는 수단일 뿐, 대상을 강제하는 메커니즘은 아니다. 후술할 issuekit#12가 채택한 방식도 이 방식이다.
diff를 가져올 때는 주의가 필요하다. --uncommitted가 대상으로 삼는 것은 스테이징된(staged), 스테이징되지 않은(unstaged), 추적되지 않은(untracked) 세 종류이지만, git diff는 이 중 스테이징되지 않은 부분만 가져온다.
$ git status --short
M alpha.py # 스테이징되지 않음
M beta.py # 스테이징됨
...
스테이징된 변경 사항도 포함하고 싶다면 git diff HEAD -- <path>를 사용한다. 추적되지 않은 파일은 둘 다 포함되지 않으므로, --uncommitted와 동일한 범위를 재현하려면 git status --porcelain 등으로 별도로 추출해야 한다.
방식 B: 대상 경로를 지시하여 Codex가 직접 diff를 가져오게 한다 (이번에 시도한 방식)
codex exec \
< codex_review_phase3_v2.txt \
> codex_review_phase3.log 2>&1
프롬프트 파일 측에서 "리뷰 대상 diff는 git diff -- <대상 3개 파일>"
「~를 취득하도록」이라고 명시적으로 지시한다. Codex는 기동 직후 그 지시대로 스코프가 지정된 (scoped) git diff를 실행한 뒤 리뷰에 들어간다. 로그 서두에 그 증적이 남아 있었다.
exec
/bin/bash -lc 'git diff -- csharp/SampleHost/Views/EditorPanelView.xaml csharp/SampleHost/Views/EditorPanelView.xaml.cs csharp/SampleHost/MainWindow.xaml.cs' in /home/<user>/projects/myapp
succeeded in 0ms:
이렇게 하면 무관한 차분(diff)은 보지 않게 되며, 90초 정도 만에 의도한 대로 리뷰 결과가 돌아왔다. 다만 대상 확정을 Codex 측의 실행에 맡기는 만큼, 방식 A보다는 간접적이다. 대상 diff의 선택을 Codex 측에 맡기고 싶지 않다면 방식 A가 적합하다.
② 리뷰가 완료된 변경 사항은 먼저 커밋(commit)한다
애초에 워킹 트리(working tree)에 '리뷰는 완료되었으나 커밋되지 않은' 변경 사항을 쌓아두지 않는 운영 측면의 대책이다. --uncommitted는 커밋된 변경 사항을 대상으로 하지 않으므로, 리뷰가 끝난 부분부터 커밋해 나가면 스코프는 자연스럽게 좁혀진다.
Codex에 위임 → 직접 리뷰 → 커밋 (commit) 순서를 지키면 자연스럽게 이 상태가 유지된다. 역으로 말하면, 이 순서를 깨뜨려 여러 단계의 변경 사항을 미커밋 상태로 안고 있는 순간 문제가 재발한다. codex exec review --uncommitted를 그대로 사용하는 한, 리뷰 대상은 저장소에 남아 있는 미커밋 변경 사항으로 결정된다. AI 리뷰를 돌린다면 커밋의 입도(granularity)와 타이밍이 전제 조건으로 포함되어야 한다는 것이 이번의 배움이다.
보충: 관련 이슈 (Issue)
조사해 보니 리뷰 대상 지정에서 막혔던 사례들을 발견할 수 있었다.
- issuekit#12 — codex-cli 0.125.0에서
--base/--uncommitted/[PROMPT]가 상호 배타적이 되어, 크로스 리뷰용 스킬 호출 예시들이 일제히 에러가 났다는 보고. 실제 기기 검증 결과가 포함되어 있으며, 위의 배타 관계 표와 일치한다. 해결된 PR#20(머지 완료)에서는codex exec review를 버리고 순수한 (raw) 방식으로 전환했다. 위의 방식 A와 동일한 결론에 독립적으로 도달했던 방식인,codex exec에 diff를 파이프(pipe)로 넘기고 리뷰 지시를[PROMPT]로 전달하는 방식이다. - openai/codex#7825 — 「
--base/--commit사용 시에도[PROMPT]를 사용할 수 있게 해달라」는 상위 단계로의 요청. **not planned로 클로즈(closed)**되었으며, 구현을 시도한 PR#11903도 머지되지 않고 클로즈되었다. 적어도 이것이 현행 사양이며, 변경 요청도 닫혀 있는 것은 확실하다. 당분간은 이 전제하에 구성할 수밖에 없다. - openai/codex#8404 — v0.77.0에서 베이스 브랜치 비교의 diff 계산이
git diff {mergeBaseSha}(merge base와 워킹 트리의 비교)로 되어 있어, 미커밋된 무관한 파일이 리뷰에 혼입되는 또 다른 버그. 보고자는 소스상의 근본 원인까지 특정했다. 현재 클로즈되어 있으나, 클로즈 코멘트는 「3개월 동안 upvote도 추가 게시글도 없었으므로 닫는다」일 뿐, 수정되었다고 적혀 있지는 않다. - openai/codex#17160 — 해당 #8404가 「근본 원인은 특정했으나 고쳐지지 않았다」며 재보고된 것으로, 본 기사 집필 시점에서 open 상태이다. 다만 검증 환경은 0.116.0이며, open 상태라는 사실만으로 0.144.5에서도 미수정되었다고 단정할 수는 없다. 적어도 0.116.0 시점에서는 문제가 남아 있었다는 정도까지 확인된다.
--base측의 이야기이므로 이번의--uncommitted와는 조건도 다르다.
「리뷰 대상의 지정은 리뷰 위임의 급소」라는 것은 아무래도 일반 법칙인 듯하다.
교훈
- 위임했다면, 우선 「지시가 전달되었는가」를 확인한다. 이번에 프롬프트는 CLI의 인자(argument) 사양으로 인해 읽히지 않았다. 에러는 발생하지 않는다. 로그의
user
메시지를 실제로 보기 전까지는 알아차릴 수 없었다. 전달되지 않은 지시는 무시된 지시와 구분이 불가능하다 -
리뷰 대상은 프롬프트가 아니라, 플래그(flag)나 입력 데이터로 확정해야 한다. 중간 단계의 AI(이번 경우에는 Claude Code)가 인간의 의도를 커맨드의 플래그 사양으로 정확하게 번역해 준다는 보장은 없다. 게다가 이번 사례처럼, 번역에 실패하더라도 마치 성공한 것처럼 보일 때가 있다 -
AI 리뷰의 운용에는 커밋(commit) 규율이 포함된다. 워킹 트리(working tree)의 청결도가 곧 리뷰 대상의 제어로 이어진다 -
CLI의 사양은 변한다. --base와 --uncommitted의 배타적 관계처럼, 에이전트가 구성하도록 만든 커맨드는 CLI 버전 업데이트 시 조용히 망가진다. 위임 구성을 설계했다면, 호출 부분은 AI에게 맡기지 말고 고정해 두는 것이 좋다.
환경
사고 발생 시:
- 날짜: 2026-07-07
- codex-cli: 0.142.5 (model=gpt-5.5)
- Claude Code: 2.1.198
최소 재현 및 배타 관계 검증 시:
- 날짜: 2026-07-17
- codex-cli: 0.144.5 (model=gpt-5.6-sol)
같은 실수를 저지른 누군가의 검색에 이 글이 걸리기를 바랍니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기