> ## Content Index
> Fetch the complete content index at: https://sanghunkang.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# [App] 하나의 기기에서 Codex(chatgpt) 다계정 사용하기
- URL: https://sanghunkang.com/ko/codex-app-dual-account-ko/
- Published: 2026-07-22T05:56:51.000Z
- Updated: 2026-08-17T13:23:31.000Z
- Author: Sanghun Kang
- Tags: dev, #ko, #multi

빠르게 적용하기: 이 글의 url을 당신의 ai에게 던져서 이대로 해달라고 하세요. (단 앱은 서명이 얽혀서 CLI보다 까다로우니 결과는 꼭 확인하자.)

**TLDR:**

- ChatGPT 데스크톱 앱을 계정별로 두 개(이상) 띄울 수 있게 된다. 동시에.
- 업데이트는 **앱 열 때** 자동으로 진행된다.
- 주의: **passkey 로그인은 불가능.** OTP·비밀번호·SSO로 로그인해야 함.

## 방법

아래를 `~/.local/bin/chatgpt2-rebuild`로 저장한다. (원본 `ChatGPT.app`이 깔려 있어야 하고, stub 컴파일용 `clang`이 필요하다 → `xcode-select --install`) 이 런처는 사이드바 상태를 양방향으로 동기화한다. 실행 전에는 ChatGPT의 상태를 ChatGPT2로 복사하고, ChatGPT2가 종료되면 최종 상태를 다시 ChatGPT로 복사한다.

```bash

#!/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로 마지막 상태를 복사하려면 정상적으로 앱을 종료해야 한다. 강제 종료, 앱 충돌, 갑작스러운 시스템 종료에서는 마지막 복사가 실행되지 않을 수 있다.

그리고 실행:

```bash

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 안에서 업데이트를 시도하면 표시되는 서명 오류](https://sanghunkang.com/content/images/2026/07/chatgpt2-update-signature-error.jpg)

ChatGPT2 안에서 업데이트를 시도하면 표시되는 서명 오류

원본을 업데이트하면 chatgpt2도 자동으로 업데이트되니 복사된 앱에서 업데이트를 시도할 필요가 없다.

## 주의: passkey는 사용 불가능

재서명하는 순간 앱의 **Team ID와 키체인 접근 권한이 사라진다.** passkey는 iCloud 키체인/보안 요소에 있고 그 접근이 딱 그 권한으로 통제되므로, ChatGPT2에선 "Verify your identity → passkey" 화면에서 무한 로딩이다. 이건 못 고친다(OpenAI 실제 인증서로 서명해야 가능한데 그건 우리가 못 한다). 대신 **"Try another method"** 로 **OTP·비밀번호·Google/Apple/MS SSO** 를 쓰면 된다. 이 세션은 쿠키로 저장돼 재시작해도 유지된다.

CLI 편은 여기: [하나의 기기에서 Codex(chatgpt) 다계정 사용하기](https://sanghunkang.com/ko/codex-cli-dual-account-ko/)