[App] 하나의 기기에서 Codex(chatgpt) 다계정 사용하기
빠르게 적용하기: 이 글의 url을 당신의 ai에게 던져서 이대로 해달라고 하세요. (단 앱은 서명이 얽혀서 CLI보다 까다로우니 결과는 꼭 확인하자.)
TLDR:
- ChatGPT 데스크톱 앱을 계정별로 두 개(이상) 띄울 수 있게 된다. 동시에.
- 업데이트는 앱 열 때 자동으로 진행된다.
- 주의: passkey 로그인은 불가능. OTP·비밀번호·SSO로 로그인해야 함.
방법
아래를 ~/.local/bin/chatgpt2-rebuild로 저장한다. (원본 ChatGPT.app이 깔려 있어야 하고, stub 컴파일용 clang이 필요하다 → xcode-select --install) 이 런처는 사이드바 상태를 양방향으로 동기화한다. 실행 전에는 ChatGPT의 상태를 ChatGPT2로 복사하고, ChatGPT2가 종료되면 최종 상태를 다시 ChatGPT로 복사한다.
#!/usr/bin/env bash
set -euo pipefail
SRC="/Applications/ChatGPT.app"
DST="/Applications/ChatGPT2.app"
NEW_ID="com.openai.codex2"
NEW_NAME="ChatGPT2"
CODEX_HOME2="$HOME/.codex2"
USERDATA2="$HOME/Library/Application Support/Codex2"
P=/usr/libexec/PlistBuddy
[ -d "$SRC" ] || { echo "ERROR: $SRC not found"; exit 1; }
command -v clang >/dev/null || { echo "ERROR: clang needed (xcode-select --install)"; exit 1; }
# 실행 중이면 덮어쓰지 않는다
if pgrep -f "ChatGPT2.app/Contents/MacOS/ChatGPT.bin" >/dev/null; then
echo "ChatGPT2 is running — quit it first, then re-run."; exit 1
fi
EXE="$($P -c "Print :CFBundleExecutable" "$SRC/Contents/Info.plist")"
echo "[1/5] APFS clone of $SRC → $DST …"
rm -rf "$DST"
cp -Rc "$SRC" "$DST" # clonefile: copy-on-write, ~0 extra disk
echo "[2/5] patch Info.plist…"
PL="$DST/Contents/Info.plist"
$P -c "Set :CFBundleIdentifier $NEW_ID" "$PL"
$P -c "Set :CFBundleName $NEW_NAME" "$PL" 2>/dev/null || $P -c "Add :CFBundleName string $NEW_NAME" "$PL"
$P -c "Set :CFBundleDisplayName $NEW_NAME" "$PL" 2>/dev/null || $P -c "Add :CFBundleDisplayName string $NEW_NAME" "$PL"
$P -c "Add :LSEnvironment:CODEX_HOME string $CODEX_HOME2" "$PL" 2>/dev/null || $P -c "Set :LSEnvironment:CODEX_HOME $CODEX_HOME2" "$PL"
$P -c "Add :SUEnableAutomaticChecks bool false" "$PL" 2>/dev/null || $P -c "Set :SUEnableAutomaticChecks false" "$PL"
echo "[3/5] install lazy launcher (update-on-use script + tiny Mach-O stub; main exe → *.bin)…"
mv "$DST/Contents/MacOS/$EXE" "$DST/Contents/MacOS/$EXE.bin"
# (a) 실행 스크립트 — 열 때마다 원본 버전과 비교, 다르면 재clone 후 재실행
cat > "$DST/Contents/Resources/cg2-launch.sh" <<'LAUNCH'
#!/bin/bash
set -u
SDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # …/ChatGPT2.app/Contents/Resources
APP="$(cd "$SDIR/../.." && pwd)" # …/ChatGPT2.app
BIN="$SDIR/../MacOS/ChatGPT.bin"
SRC="/Applications/ChatGPT.app"
P=/usr/libexec/PlistBuddy
ver(){ "$P" -c "Print :CFBundleShortVersionString" "$1/Contents/Info.plist" 2>/dev/null; }
cur="$(ver "$SRC")"; mine="$(ver "$APP")"
if [ -n "$cur" ] && [ -n "$mine" ] && [ "$cur" != "$mine" ]; then
# 원본이 업데이트됨 → 분리된 헬퍼로 재clone 후 재실행 (이 실행은 넘긴다)
/usr/bin/nohup /bin/bash -c '
"$HOME/.local/bin/chatgpt2-rebuild" >/tmp/chatgpt2-autoupdate.log 2>&1 \
&& /usr/bin/env -u CODEX_HOME -u ORCA_CODEX_HOME open -a /Applications/ChatGPT2.app
' >/dev/null 2>&1 &
exit 0
fi
# 최신이면 계정 2로 정상 실행
export CODEX_HOME="$HOME/.codex2"
PRIMARY_STATE="$HOME/.codex/.codex-global-state.json"
SECONDARY_STATE="$HOME/.codex2/.codex-global-state.json"
sync_state() {
local from="$1"
local to="$2"
local tmp="${to}.sync.$$"
[ -f "$from" ] || return 0
if /bin/cp -p "$from" "$tmp"; then
/bin/mv -f "$tmp" "$to"
else
/bin/rm -f "$tmp"
return 1
fi
}
# 사이드바를 최신으로 유지한다: 실행 전 ChatGPT → ChatGPT2
sync_state "$PRIMARY_STATE" "$SECONDARY_STATE" || \
echo "WARNING: ChatGPT 사이드바 상태를 가져오지 못했다" >&2
# 사이드바를 최신으로 유지한다: 정상 종료 후 ChatGPT2 → ChatGPT
trap 'sync_state "$SECONDARY_STATE" "$PRIMARY_STATE" || true' EXIT
"$BIN" "--user-data-dir=$HOME/Library/Application Support/Codex2" "$@"
status=$?
exit "$status"
LAUNCH
chmod +x "$DST/Contents/Resources/cg2-launch.sh"
# (b) Mach-O stub — launchd는 스크립트를 메인 실행파일로 못 띄운다(POSIX 162).
# 그래서 작은 stub가 위 스크립트로 넘긴다.
SRC_C="$(mktemp /tmp/cg2launcher.XXXX.c)"
cat > "$SRC_C" <<EOF
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <libgen.h>
#include <unistd.h>
#include <mach-o/dyld.h>
int main(int argc, char **argv){
char exe[PATH_MAX]; uint32_t sz=sizeof(exe);
if(_NSGetExecutablePath(exe,&sz)!=0) return 71;
char dir[PATH_MAX]; strncpy(dir,dirname(exe),sizeof(dir));
char sh[PATH_MAX];
snprintf(sh,sizeof(sh),"%s/../Resources/cg2-launch.sh",dir);
char **n=calloc(argc+3,sizeof(char*)); int k=0;
n[k++]="/bin/bash"; n[k++]=sh;
for(int i=1;i<argc;i++) n[k++]=argv[i];
n[k]=NULL;
execv("/bin/bash",n); perror("execv"); return 72;
}
EOF
clang -O2 -arch arm64 -o "$DST/Contents/MacOS/$EXE" "$SRC_C"; rm -f "$SRC_C"
echo "[4/5] sign — DEEP ad-hoc reseal of the whole clone…"
codesign --force --deep --sign - "$DST"
echo "[5/5] clear quarantine + register…"
xattr -dr com.apple.quarantine "$DST" 2>/dev/null || true
mkdir -p "$USERDATA2"
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f "$DST"
echo "Done (APFS clone)."
이 방식은 두 앱을 동시에 사용하지 않는다는 전제에서 동작한다. ChatGPT2에서 ChatGPT로 마지막 상태를 복사하려면 정상적으로 앱을 종료해야 한다. 강제 종료, 앱 충돌, 갑작스러운 시스템 종료에서는 마지막 복사가 실행되지 않을 수 있다.
그리고 실행:
pkill -f "ChatGPT2.app" 2>/dev/null # 실행 중이면 종료
chatgpt2-rebuild # ~7초, /Applications/ChatGPT2.app 생성
env -u CODEX_HOME -u ORCA_CODEX_HOME open -a /Applications/ChatGPT2.app
첫 실행 때 계정 2로 로그인한다. (단 passkey 말고 OTP/비밀번호/SSO. 아래 주의 참고)
참조: `CODEX_HOME`을 export하는 셸(예: Orca)에서 그냥 `open`하면 그 값이 앱에 새어 들어가서 계정이 꼬인다. 위처럼 `env -u`로 열자.
원리
앱의 "정체성"은 번들 id + 서명이다. clone 후 id를 com.openai.codex2로 바꾸고, 런처가 CODEX_HOME=~/.codex2 + --user-data-dir=…/Codex2를 주입한다. 그러면 계정(auth.json)·웹 로그인·Electron 단일 인스턴스 잠금이 갈려서 둘이 동시에 뜬다. (--user-data-dir이 핵심이다. Electron 잠금은 번들 id가 아니라 user-data-dir로 잡히기 때문에, id만 바꿔선 두 개가 안 뜬다.)
왜 심볼릭이 아니라 clone인가? 프레임워크·바이너리를 원본으로 심볼릭 링크하면 Chromium 헬퍼(Codex (Service))가 시작하자마자 죽는다. 헬퍼의 샌드박스·프레임워크 경로 해석이 실제 번들 경로를 요구하는데 심볼릭이면 어긋나기 때문이다. 그러면 메인이 헬퍼를 무한 재시도하며 100% CPU로 헛돈다. 그래서 "심볼릭으로 업데이트를 자동 추종"하는 건 불가능하다.
왜 --deep 재서명인가? 내부 프레임워크·헬퍼는 OpenAI 팀 서명(hardened runtime)이다. 최상위 번들만 ad-hoc로 바꾸면 팀 불일치로 AMFI가 실행 자체를 거부한다(-420). 그래서 clone 전체를 ad-hoc로 통일 재서명해야 헬퍼가 정상 기동한다. clone은 독립 복사본이라 --deep을 해도 원본은 안 건드린다. (반대로, 프레임워크가 심볼릭인 상태에서 --deep을 하면 링크를 타고 원본까지 재서명되니 절대 금지.)
업데이트: 열 때 자동
clone은 복사 시점의 스냅샷이라 업데이트 기능이 작동하지 않는다. 대신 복사한 앱을 열 때 런처가 원본과 버전을 비교해서, 다르면 자동으로 다시 clone(~7초)하고 재실행한다. 원본은 평소대로 Sparkle로 업데이트되고, ChatGPT2는 열 때 그걸 얹어 받는 구조다. 백그라운드 데몬도, 폴링도 없다. (ChatGPT2가 스스로 Sparkle로 업데이트하게 두면 서명 불일치로 거부되거나 커스터마이징이 통째로 날아가서, 자체 업데이트는 꺼둔다.)
ChatGPT2에서 업데이트를 실행하면 아래 스크린샷처럼 서명 불일치로 업데이트가 되지 않는다.

원본을 업데이트하면 chatgpt2도 자동으로 업데이트되니 복사된 앱에서 업데이트를 시도할 필요가 없다.
주의: passkey는 사용 불가능
재서명하는 순간 앱의 Team ID와 키체인 접근 권한이 사라진다. passkey는 iCloud 키체인/보안 요소에 있고 그 접근이 딱 그 권한으로 통제되므로, ChatGPT2에선 "Verify your identity → passkey" 화면에서 무한 로딩이다. 이건 못 고친다(OpenAI 실제 인증서로 서명해야 가능한데 그건 우리가 못 한다). 대신 "Try another method" 로 OTP·비밀번호·Google/Apple/MS SSO 를 쓰면 된다. 이 세션은 쿠키로 저장돼 재시작해도 유지된다.
CLI 편은 여기: 하나의 기기에서 Codex(chatgpt) 다계정 사용하기