카테고리 없음

Codex CLI 사용법(코덱스 CLI): codex exec 를 백그라운드로 돌릴 때 걸리는 함정 3가지

코드짜는아빠 2026. 10. 3. 04:56

끝난 줄 알았는데 끝나지 않은 실행들

백그라운드로 넘긴 codex exec 가 50분 동안 파일을 하나도 고치지 않고 멈춰 있다가 시간 초과로 끝난 적이 있다. 9월 14일 저녁 일.

Claude Code 세션이 구현 작업을 Codex CLI 에 넘길 때는 codex exec 를 백그라운드로 돌린다. 둘을 어떻게 나눠 쓰는지는 다음 글 몫이고, 여기서는 사람 없이 돌린 codex exec 가 실제로 걸린 함정만 적는다.

9월 한 달 동안 내 노트북에서 돈 codex exec 는 705회(매일 도는 이미지 생성 자동화가 부른 것 포함). 이 글은 그중 원인을 찾아 기록해 둔 세 날짜, 9월 14일 · 18일 · 21일의 사례를 다룬다. 한도에 걸려 거부된 실행이 종료 코드 0으로 끝난 일은 3편에 적었다.

0. Codex CLI 설치 방법과 기본 사용법 (2026-09-30 공식 문서 기준)

공식 문서의 리눅스 · macOS 설치 명령은 설치 스크립트 한 줄이다.

curl -fsSL https://chatgpt.com/codex/install.sh | sh

npm 으로도 깔 수 있다. 내 노트북에 깔린 건 npm 전역 패키지.

npm i -g @openai/codex
codex --version   # codex-cli 0.159.2 (2026-09-30 기준)

프로젝트 폴더에서 codex 를 처음 실행하면 로그인 방법을 고르는 화면이 나오고, 「Sign in with ChatGPT」 로 로그인하면 된다.

대화창 없이 한 번 돌리고 끝낼 때 쓰는 게 codex exec(아래 동작은 2026-09-30 공식 문서 기준).

항목 동작
출력 진행 상황은 stderr, 마지막 답만 stdout
-o <파일> 마지막 답을 파일에도 저장
--json stdout 이 이벤트 단위 JSONL 이 됨
샌드박스 기본은 읽기 전용. 파일을 고치게 하려면 --sandbox workspace-write
stdin 프롬프트 인자를 주고 stdin 도 넘기면, stdin 내용이 추가 맥락으로 붙음

함정 1. stdin 을 닫지 않으면 조용히 멈춘다

9월 14일 저녁에 멈춘 그 수정 작업이 로그에 남긴 건 이 한 줄뿐이었다.

Reading additional input from stdin...

그 상태로 50분 제한 시간을 다 채우고 exit 124(시간 초과)로 끝. 같은 날 오후에 먼저 돌린 백그라운드 실행은 문제없이 끝났으니, 실패가 무작위처럼 보였다.

codex exec --help 의 프롬프트 설명에는 「stdin 이 파이프로 들어오고 프롬프트도 주어지면, stdin 은 <stdin> 블록으로 덧붙는다」는 문장이 있다. 덧붙이려면 stdin 을 끝까지 읽어야 하는 셈이다. 백그라운드 실행기가 stdin 을 열어 둔 채 아무것도 보내지 않았다면, 그 끝이 오지 않았을 것으로 본다. 도움말과 로그 문구에서 짐작한 거고, 공식 문서에는 이 멈춤 얘기가 없다.

원리만 확인하려고 codex 자리에 cat 을 넣어 재현했다. sleep 10 | 은 10초 동안 파이프를 열어 두기만 하고 아무것도 보내지 않는다.

# 파이프가 열려 있는 동안 cat 은 기다린다 → 5초 제한에 걸림
sleep 10 | timeout 5 cat
# exit=124 — cat 이 입력을 기다리다 5초 제한에 걸림

# stdin 을 /dev/null 로 닫으면 바로 끝난다
sleep 10 | timeout 5 cat < /dev/null
# exit=0 — cat 은 바로 끝남 (명령 전체는 sleep 10 이 끝나야 돌아옴)

셸은 파이프라인의 명령이 모두 끝나야 돌아오므로, 두 줄 다 10초 뒤에 프롬프트가 돌아온다. 차이는 종료 코드, 그리고 cat 이 걸린 시간. 내 노트북에서 cat 만 따로 재 보니 첫 줄 5,003ms, 둘째 줄 3ms.

그래서 백그라운드로 돌릴 때는 stdin 을 항상 닫는다.

codex exec -C "$HOME/my-project" "$(cat prompt.md)" < /dev/null > codex.log 2>&1

시작하고 1분 안에는 로그 첫 줄을 본다. Reading additional input from stdin 이 보이면 멈춘 거다.

head -5 codex.log

공식 문서에는 프롬프트 자체를 stdin 으로 넘기는 방법도 나와 있다. 프롬프트 인자를 빼거나 - 를 주면 stdin 에서 프롬프트를 읽는다. codex exec - < prompt.md 처럼 파일을 넘기면 파일 끝이 곧 입력의 끝. 내 쪽 자동화는 인자 방식에 < /dev/null 을 붙여 쓴다.

함정 2. 종료 코드를 엉뚱한 자리에서 받는다

9월 18일에는 Codex 가 아무것도 하지 않았는데 「완료」로 보고됐다. 그때 쓰던 버전(0.154.0)에 없는 --full-auto 플래그를 붙여서, codex exec 는 사용법 에러만 내고 바로 끝났다. 명령 뒤에는 다른 명령이 이어 붙어 있었고.

codex exec ... ; echo ...; tail ...

셸이 돌려주는 종료 코드는 마지막 명령(tail)의 것이다. tail 은 성공했으니 전체가 exit 0 이었다. git status 로 바뀐 파일이 0개인 걸 보고서야 잡았다. --full-auto 는 지금 버전(0.159.2)의 codex exec --help 에도 없다. 공식 문서는 --full-auto 를 「폐기 예정, 경고 출력」으로 적고 있지만, 0.159.2 에서 붙여 보면 unexpected argument '--full-auto' 에러로 끝난다(2026-09-30). 예전 글의 명령을 옮겨 쓸 때 조심할 부분.

codex 자리에 false(항상 실패하는 명령)를 넣어 재현했다.

false; echo "바로 뒤: $?"                  # 1
false; echo done; echo "한 줄 건너: $?"     # 0
false | tail -1; echo "파이프 뒤: $?"        # 0
false | tail -1; echo "${PIPESTATUS[0]}"   # 1
set -o pipefail; false | tail -1; echo $?  # 1

파이프 뒤에서 받은 $? 도 같은 함정. 내 운영 기록에도 node ... | tail -5; echo $? 로 잰 값을 믿고 「안전장치가 깨졌다」고 잘못 읽은 날이 남아 있다.

그 뒤로 종료 코드는 codex 바로 다음 줄에서 받는다.

codex exec ... < /dev/null > codex.log 2>&1
echo "CODEX_EXIT=$?"

비슷한 일이 백그라운드 실행기에서도 났다. 9월 21일, nohup codex ... & 를 Claude Code 의 백그라운드 실행 기능 위에 한 번 더 건 날. 실행기는 nohup ... & 를 띄운 셸이 끝난 것(0)을 작업 완료로 보고했고, 그때 Codex 는 막 시작한 상태였다. 그래서 백그라운드는 한 겹으로만 건다.

함정 3. 백그라운드 시간 제한에 잘린다

Claude Code 에서 백그라운드로 돌린다면 시간 제한도 봐야 한다. Claude Code 2.1.285 변경 기록(2026-09-30 조회)에 따르면 백그라운드 Bash 명령은 timeout 이 되면 멈춘다. 기본 30분, 최대 2시간. 긴 Codex 작업을 기본값으로 걸어 두면 30분에 파일을 고치던 도중 끊길 수 있다.

얼마로 잡을지는 실제 실행 시간을 재서 정했다.

집계 실행 횟수 중앙값 p95 최대 30분 초과
9월 1~30일(30일 오후 집계) 705회 1.9분 7.3분 22.0분 0회

세는 법: 9월 세션 로그 중 첫 줄의 originator 가 codex_exec 인 파일마다 첫 기록과 마지막 기록의 시각 차를 쟀다.

기본 30분과 최대값 22분의 차이는 8분. 그래서 Claude Code 가 Bash 도구를 백그라운드로 부를 때 timeout 을 60분(3600000, 밀리초)으로 적게 한다. 최대값의 약 2.7배. 이 규칙은 작업 안내 문서에 넣어 뒀다. 2시간을 주지 않는 건 멈춘 실행을 그만큼 늦게 알게 되기 때문이다. 함정 1처럼 멈춘 실행은 제한 시간이 다 찰 때까지 아무 말이 없으니까. 60분을 넘길 것 같은 작업은 나눠서 넘기거나 systemd-run --user 로 돌린다.

시간 제한으로 끊긴 실행은 파일을 반쯤 고친 채 남을 수 있다. 멈췄다는 알림이 오면 먼저 git diff 로 어디까지 고쳤는지 확인.

exit 0 을 받은 다음: 완료는 직접 확인한다

종료 코드를 제대로 받아도 exit 0 은 「명령이 에러 없이 끝났다」까지만 말해 준다. 일이 맞게 됐는지는 따로 봐야 한다. 9월 21일 한 세션에서는 exit 0 인데 실제로는 미완인 경우가 세 번 연속 나왔다. 첫 번째는 함정 2의 이중 백그라운드. 나머지 두 건은 고친 코드 자체의 문제였고, 둘 다 QA 에서 잡혔다.

  • 새 가드를 함수 맨 위에 넣어서, 가드가 필요 없는 경로까지 막았다. 기존 테스트 7건 파손.
  • 테스트 이름은 「run 이 실제 발행기를 막는다」인데, 단언은 헬퍼만 부르고 run() 은 부르지 않았다. 가드 호출문을 지워도 통과하는 테스트였던 거다.

그래서 「완료」 보고(Codex 가 -o 로 남긴 마지막 메시지 포함)는 주장으로만 읽고, 아래를 직접 돌린다.

echo "CODEX_EXIT=$?"                # ① 종료 코드 (codex 바로 다음 줄)
git status --short                  # ② 대상 파일이 실제로 바뀌었나
git diff --numstat                  # ③ 건드리면 안 되는 파일의 diff 가 0줄인가
npm test                            # ④ 전체 테스트를 직접 (프로젝트의 테스트 명령)

테스트 개수가 늘었다면 새 테스트의 이름과 단언 대상이 맞는지도 읽어 본다.

정리: 백그라운드 codex exec 를 돌릴 때 지키는 네 가지와 승인 요청을 자동 검토로 넘기는 --approve-for-me

codex exec -C "$HOME/my-project" --approve-for-me \
  -o last_message.txt "$(cat prompt.md)" \
  < /dev/null > codex.log 2>&1
echo "CODEX_EXIT=$?"

codex exec --help 설명을 보면 --approve-for-me 는 승인 요청을 workspace-write 샌드박스에서 자동 검토로 돌리는 옵션. 공식 문서 예시처럼 --sandbox workspace-write 만 줘도 파일을 고칠 수 있다. 다만 둘을 같이 주지는 않는다. 9월 18일, 그때 버전(0.154.0)에서 같이 줬다가 실행되지 않았다.

  • stdin 은 < /dev/null 로 닫기, 1분 안에 로그 첫 줄 확인.
  • 종료 코드는 codex 바로 다음 줄에서 받는다. 파이프 뒤나 여러 명령 뒤에서는 받지 않는다.
  • 백그라운드는 한 겹만. Claude Code 라면 timeout 을 실행 시간 실측에 맞춰 명시한다.
  • exit 0 과 「완료」 보고 뒤에는 git status · git diff · 테스트를 직접 돌린다.

다음 글은 Claude Code 와 Codex 를 둘 다 매일 쓰면서 일을 어떻게 나누는지에 대한 이야기다. 설계와 검토, 구현을 누구에게 맡기는지, 그리고 그렇게 정한 이유.

이 블로그에 쓴 글들을 바탕으로 전자책과 템플릿 묶음을 만들어 볼까 고민하고 있어요. 낼지, 낸다면 언제일지는 아직 미정.

나오면 알림을 받고 싶은 분은 제목을 [출시 알림]으로 해서 codenara.contact@gmail.com 으로 메일 주세요.

개인정보 안내: 받은 메일 주소는 출시 알림 한 번 보내는 데만 쓰고, 보내고 나면 지워요. 내지 않기로 하면 그때 지우고, 요청하시면 그 전에라도 바로 지웁니다.

이런 자동화를 직접 세팅하고 싶거나 팀 환경에 맞게 설계해 보고 싶다면 codenara.contact@gmail.com 으로 연락 주세요. 교육·세팅 문의는 따로 받고 있어요.