[App] Multiple Codex (ChatGPT) accounts on one machine
Fast track: throw this post's URL at your AI and tell it to set this up for you. (The app version is trickier because of signing, so double-check the result.)
TLDR:
- Run two (or more) ChatGPT desktop apps, one per account. At the same time.
- Updates follow automatically, when you open the app.
- Caveat: passkey login won't work. Log in with OTP, password, or SSO.
How
Save this as ~/.local/bin/chatgpt2-rebuild. (You need the original ChatGPT.app installed, and clang to compile the stub → xcode-select --install.)
#!/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; }
# don't clobber a running instance
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) launch script — on every open: compare versions, re-clone + reopen if they differ
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
# original updated → re-clone in a detached helper, then reopen (this launch is handed off)
/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
# up-to-date → normal launch of account-2 instance
export CODEX_HOME="$HOME/.codex2"
exec "$BIN" "--user-data-dir=$HOME/Library/Application Support/Codex2" "$@"
LAUNCH
chmod +x "$DST/Contents/Resources/cg2-launch.sh"
# (b) Mach-O stub — launchd won't run a script as a bundle's main executable (POSIX 162),
# so this tiny stub just hands off to the script above.
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)."
Then run it:
pkill -f "ChatGPT2.app" 2>/dev/null # quit it if running
chatgpt2-rebuild # ~7s, creates /Applications/ChatGPT2.app
env -u CODEX_HOME -u ORCA_CODEX_HOME open -a /Applications/ChatGPT2.app
On first launch, log in to account 2. (Use OTP/password/SSO, not passkey — see the caveat below.)
Note: if you `open` from a shell that exports `CODEX_HOME` (e.g. Orca), that value leaks into the app and crosses up your accounts. Open with `env -u` as shown above.
Why it works
The app's "identity" is its bundle id + signature. After cloning, we change the id to com.openai.codex2, and the launcher injects CODEX_HOME=~/.codex2 + --user-data-dir=…/Codex2. That splits the account (auth.json), the web login, and the Electron single-instance lock, so both run at the same time. (--user-data-dir is the key: Electron's lock is keyed by the user-data-dir, not the bundle id, so changing the id alone won't let two run.)
Why clone, not symlink? If you symlink the frameworks/binary back to the original, the Chromium helper (Codex (Service)) crashes the instant it starts, because its sandbox / framework path resolution needs the real bundle path and a symlink throws it off. The main process then retries the helper forever, pinned at 100% CPU. So "symlink to auto-follow updates" is impossible.
Why --deep re-sign? The inner frameworks/helpers carry OpenAI's team signature (hardened runtime). If only the top bundle is ad-hoc, the team mismatch makes AMFI reject the launch outright (-420). So the whole clone must be re-signed ad-hoc to unify the identity, and then the helper starts. Since the clone is an independent copy, --deep never touches the original. (Conversely, never --deep-sign while the frameworks are symlinks — it would re-sign the original through the links.)
Updates: automatic, on open
The clone is a snapshot from copy time, so it doesn't auto-update on its own. Instead, when you open the app, the launcher compares its version to the original and, if they differ, re-clones automatically (~7s) and relaunches. The original updates as usual via Sparkle, and ChatGPT2 rides on top when you open it. No background daemon, no polling. (Letting ChatGPT2 self-update via Sparkle would be rejected for a signature mismatch, or would wipe the customization, so its self-update is disabled.)
Caveat: passkey doesn't work
The moment you re-sign, the app loses its Team ID and keychain access. Passkeys live in the iCloud Keychain / Secure Enclave, and access is gated by exactly that, so in ChatGPT2 the "Verify your identity → passkey" screen spins forever. It can't be fixed (that would need OpenAI's real signing certificate, which we don't have). Use "Try another method" with OTP, password, or Google/Apple/MS SSO instead. The session is stored as a cookie, so it persists across restarts.
Part 1 (CLI) is here: Multiple Codex (ChatGPT) accounts on one machine