브라우저 없는 원격 서버에서 Codex CLI 로그인 — device authorization
ssh 전용 헤드리스 서버에서 codex login 은 로컬 콜백 서버를 띄우고 브라우저를 기다리다 끝나지 않는다. --device-auth 로 코드만 옮겨 로그인하고, auth.json 과 login status 로 실제 완료를 검증한다.
브라우저가 없는 원격 서버에 Codex CLI 를 깔았다. ssh 로만 붙는 클라우드 인스턴스라 데스크톱 세션도, 웹 브라우저도 없다. 여기서 codex login 을 치면 로그인이 시작만 되고 영원히 끝나지 않는다.
콜백은 localhost:1455 로 온다 — 헤드리스 서버엔 그게 없다
기본 codex login 은 OAuth authorization code 플로우를 쓴다. CLI 가 로컬에 콜백 서버(포트 1455)를 띄우고, 브라우저를 열어 인증을 마친 뒤 그 브라우저가 localhost:1455 로 리다이렉트되기를 기다린다. 데스크톱에서는 이 과정이 자동으로 돌아가서 눈에 안 띈다.
문제는 헤드리스 서버다. 리다이렉트를 받을 브라우저가 서버에 없다. 콜백 서버는 떠 있지만 아무도 그 포트로 돌아오지 않으니, 프로세스는 그냥 계속 기다린다.
재현 — 로그인이 영원히 안 끝난다
재현은 간단하다. GUI 도 브라우저도 없는 서버에 ssh 로 붙어서 로그인만 시도하면 된다.
codex login
CLI 는 인증 URL 을 출력하고 콜백을 기다리는 상태로 멈춘다. 다른 기기 브라우저에서 그 URL 을 열어 인증을 끝내도, 그 브라우저는 서버의 localhost:1455 로 리다이렉트되지 못한다(자기 로컬의 1455 로 갈 뿐이다). 서버 쪽 프로세스는 콜백을 못 받으니 로그인이 완료되지 않고, ~/.codex/auth.json 도 생기지 않는다.
포트 포워딩으로 우회했지만
처음엔 포트 포워딩으로 우회하려 했다. 로컬에서 서버의 콜백 포트를 터널링하면 로컬 브라우저가 콜백을 대신 받는다.
# 로컬 터미널에서
ssh -L 1455:localhost:1455 <host>
# 그 세션 안에서 서버의 codex login 실행
동작은 한다. 하지만 로그인할 때마다 별도 ssh 터널을 열어야 하고, 로컬에 브라우저가 있어야 한다는 전제가 깔린다. 순수 CLI 환경이나 에이전트가 대신 로그인해야 하는 상황에서는 매번 사람이 터널을 걸어줘야 해서 불편하다.
원인 — 두 프로세스가 로컬호스트를 공유한다는 전제
authorization code 플로우 자체가 “인증한 브라우저”와 “토큰을 받는 프로세스”가 같은 네트워크 로컬호스트를 공유한다고 가정한다. 이 가정이 헤드리스 서버에서 깨진다. 인증은 다른 기기 브라우저에서 하고, 토큰은 서버 프로세스가 받아야 하는데, 둘을 잇는 게 localhost:1455 리다이렉트뿐이기 때문이다.
해법은 이 리다이렉트 의존을 끊는 것이다. 즉 device authorization(device code) 플로우를 쓰면 된다. 인증한 브라우저와 토큰을 받는 프로세스가 서로 로컬호스트를 공유하지 않아도 된다 — 브라우저는 코드만 입력하고, 서버 프로세스는 자기가 인증 서버를 폴링해서 토큰을 가져온다.
–device-auth — 코드만 옮기면 된다
codex-cli 0.146.0 기준으로 --device-auth 플래그가 있다. 포트 포워딩 없이 코드 문자열만 옮기면 된다.
codex login --device-auth
출력은 이렇게 나온다.
1. Open this link in your browser and sign in to your account
https://auth.openai.com/codex/device
2. Enter this one-time code (expires in 15 minutes)
XXXX-XXXXX
폰이든 PC든 아무 기기의 브라우저에서 위 URL 을 열고 이 일회용 코드를 입력한다. 그러면 서버 쪽 프로세스가 인증 서버를 폴링하다가 Successfully logged in 을 찍고 ~/.codex/auth.json 을 만든다. 코드 만료는 15분이라, 넘기면 프로세스를 죽이고 다시 띄우면 새 코드가 나온다.
에이전트 세션 / 비대화형에서
TTY 없이도 동작한다. 세션 턴이나 대화 컨텍스트를 낭비하지 않으려면, 로그인을 백그라운드로 띄워 코드만 뽑고 완료는 파일 감시로 잡는다.
# 1) 로그인 시작 + 코드 확보
setsid codex login --device-auth > codex-login.log 2>&1 < /dev/null &
sleep 8; cat codex-login.log
# 2) 완료 대기
until [ -f ~/.codex/auth.json ]; do sleep 5; done
codex login status
setsid 로 세션에서 떼어내고 stdin 을 /dev/null 로 막아, 프로세스가 TTY 입력을 기다리지 않게 한다. 로그 파일에서 코드를 읽어 브라우저에 입력하고, auth.json 이 생기면 완료다.
이미 자격증명이 있을 때
브라우저 인증조차 건너뛰고 싶으면, 토큰이나 API 키를 표준입력으로 직접 넘기는 방법도 있다.
# 종량제 API 키 (ChatGPT 플랜이 아니라 사용량 과금)
printenv OPENAI_API_KEY | codex login --with-api-key
# 이미 확보한 액세스 토큰
printenv CODEX_ACCESS_TOKEN | codex login --with-access-token
로그인된 다른 기기의 ~/.codex/auth.json 을 그대로 복사해 오는 방법도 되긴 한다. 다만 자격증명 파일을 통째로 옮기는 거라 되도록 피한다.
완료 판정 — auth.json 과 login status 둘 다
로그인이 “실제로” 끝났는지는 두 단계로 확인한다.
첫째, CLI 자체 상태.
codex login status
둘째, ~/.codex/auth.json 파일 존재. device auth 는 이 파일이 생겨야 완료다. 위 until 루프가 이걸로 완료를 판정한다.
Codex 를 다른 도구의 백엔드로 붙여 쓴다면(예: 에이전트 플러그인의 런타임), 그 도구가 제공하는 준비 상태 점검 커맨드까지 돌려 보는 게 확실하다. 대개 아래 같은 JSON 을 돌려준다.
{
"ready": true,
"codex": { "available": true, "detail": "codex-cli 0.146.0; advanced runtime available" },
"auth": { "loggedIn": true, "authMethod": "chatgpt", "verified": true }
}
auth.loggedIn 과 auth.verified 가 둘 다 true 면, 파일만 생긴 게 아니라 토큰이 실제로 유효하다는 뜻이다.
타 환경에서 걸리는 것들
- 계정명과 홈 디렉터리가 어긋나는 인스턴스 — 클라우드 이미지에 따라
whoami결과와$HOME의 마지막 경로 조각이 다를 수 있다.~/.codex는$HOME기준으로 잡히므로, 파일을 직접 찾을 땐 로그인 계정명이 아니라$HOME을 봐야 한다. 소유자가 로그인 계정과 일치하면 권한 문제는 없다. - 코드 만료 15분 — 백그라운드로 띄워 두고 다른 일 하다 돌아오면 이미 만료돼 있을 수 있다. 만료됐으면 새로 띄워 새 코드를 받는다.
- 자격증명을 managed 계층에 두는 래퍼 — Codex 를 감싸서 자격증명을 자체 저장소에 관리하는 도구도 있다. 그런 경우는
~/.codex/auth.json을 아예 안 쓰므로, 이 문서의 파일 기반 검증이 적용되지 않는다. 도구가 정한 경로를 따른다.