Skip to content

Instantly share code, notes, and snippets.

@hqman
Created August 1, 2026 09:09
Show Gist options
  • Select an option

  • Save hqman/8055375bd40a8885d4038e8ba4409074 to your computer and use it in GitHub Desktop.

Select an option

Save hqman/8055375bd40a8885d4038e8ba4409074 to your computer and use it in GitHub Desktop.
codex-model-filter-patch.sh
#!/bin/bash
# ============================================================================
# Codex Desktop (ChatGPT.app) model-picker patch for macOS
# Strategy: build in /tmp, then swap back into /Applications
#
# Problem
# After switching Codex to a third-party provider (DeepSeek, Kimi, GLM, ...),
# the model picker only shows "Custom" instead of the real model name.
#
# Cause
# The bundled UI filters models against a remote OpenAI whitelist
# (useHiddenModels / available_models). Third-party slugs are dropped, so the
# composer falls back to the "Custom" label.
#
# What this script does
# Disables that whitelist filter inside the packaged webview JS, then repacks
# app.asar, updates ElectronAsarIntegrity, and ad-hoc re-signs the app.
#
# Why build under /tmp
# /Applications/ChatGPT.app is Developer ID signed and protected by macOS
# App Management (TCC). In-place asar edits / codesign often fail with
# "Operation not permitted" (commonly on CodexDockTilePlugin.plugin).
# sudo does NOT bypass TCC.
#
# Flow:
# 1) Copy the whole app to /tmp (reads are allowed)
# 2) Patch asar + recompute integrity hash + ad-hoc codesign under /tmp
# 3) Swap the finished app back into /Applications
#
# Step 3 still needs one of:
# (a) Your terminal app is allowed under
# System Settings → Privacy & Security → App Management
# (b) Manual Finder replace if the automated swap fails (no extra grant)
#
# Requirements
# - macOS with Codex Desktop at /Applications/ChatGPT.app
# - Node.js (npx) for @electron/asar
# - codesign, PlistBuddy (ship with macOS)
#
# Usage
# bash codex-model-filter-patch.sh # apply patch
# bash codex-model-filter-patch.sh restore # restore latest backup
#
# Risks
# - Modifies the local Codex / ChatGPT.app bundle
# - App updates overwrite the patch; re-run after each update
# - Ad-hoc signing may require re-approving Accessibility / Screen Recording
# - Optional: everything still works without this; only the label stays Custom
#
# License: use at your own risk. Not affiliated with OpenAI.
# ============================================================================
set -euo pipefail
APP="/Applications/ChatGPT.app"
STAGE="/tmp/ChatGPT.app" # patched + signed build output
WORK="/tmp/codex-patch-$$"
TS="$(date +%Y%m%d-%H%M%S)"
BAK_DIR="${CODEX_PATCH_BACKUP_DIR:-$HOME/codex-patch-backups}"
log() { printf '\033[1;36m>>> %s\033[0m\n' "$*"; }
err() { printf '\033[1;31m!!! %s\033[0m\n' "$*" >&2; }
# npx cache path for @electron/asar (integrity hash)
NPX_ASAR="$(find "$HOME/.npm/_npx" -type d -path '*/node_modules/@electron/asar' 2>/dev/null \
| head -1 | xargs -I{} dirname {} | xargs -I{} dirname {} 2>/dev/null || true)"
# ---------------------------------------------------------------------------
# restore mode
# ---------------------------------------------------------------------------
if [[ "${1:-}" == "restore" ]]; then
LAST_ASAR="$(ls -t "$BAK_DIR"/app.asar.bak.* 2>/dev/null | head -1 || true)"
LAST_PLIST="$(ls -t "$BAK_DIR"/Info.plist.bak.* 2>/dev/null | head -1 || true)"
[[ -z "$LAST_ASAR" ]] && { err "No backup found under $BAK_DIR"; exit 1; }
log "Quitting ChatGPT..."
osascript -e 'quit app "ChatGPT"' 2>/dev/null || true; sleep 2
pkill -f "ChatGPT.app/Contents/MacOS/ChatGPT" 2>/dev/null || true; sleep 2
log "Restoring app.asar <- $LAST_ASAR"
cp "$LAST_ASAR" "$APP/Contents/Resources/app.asar" || {
err "Write failed (App Management permission?). Use Finder or grant access, then retry."
exit 1
}
[[ -n "$LAST_PLIST" ]] && cp "$LAST_PLIST" "$APP/Contents/Info.plist"
log "Re-signing..."
xattr -cr "$APP" 2>/dev/null || true
codesign --force --deep --sign - --timestamp=none "$APP"
codesign -v "$APP" && log "Restore complete"
exit 0
fi
# ---------------------------------------------------------------------------
# 0. Preconditions
# ---------------------------------------------------------------------------
[[ -d "$APP" ]] || { err "App not found: $APP"; exit 1; }
command -v npx >/dev/null || { err "npx (Node.js) is required"; exit 1; }
if [[ -z "$NPX_ASAR" ]]; then
log "Warming up @electron/asar..."
npx -y @electron/asar --version >/dev/null 2>&1
NPX_ASAR="$(find "$HOME/.npm/_npx" -type d -path '*/node_modules/@electron/asar' 2>/dev/null \
| head -1 | xargs -I{} dirname {} | xargs -I{} dirname {})"
fi
# ---------------------------------------------------------------------------
# 1. Quit ChatGPT / Codex
# ---------------------------------------------------------------------------
log "Step 1: quitting ChatGPT..."
osascript -e 'quit app "ChatGPT"' 2>/dev/null || true; sleep 2
pkill -f "ChatGPT.app/Contents/MacOS/ChatGPT" 2>/dev/null || true; sleep 2
# ---------------------------------------------------------------------------
# 2. Copy app to /tmp (read-only w.r.t. App Management)
# ---------------------------------------------------------------------------
log "Step 2: copying app to $STAGE ..."
rm -rf "$STAGE"
cp -R "$APP" "$STAGE"
log " Backing up original asar/plist to $BAK_DIR ..."
mkdir -p "$BAK_DIR"
cp "$APP/Contents/Resources/app.asar" "$BAK_DIR/app.asar.bak.$TS"
cp "$APP/Contents/Info.plist" "$BAK_DIR/Info.plist.bak.$TS"
RES="$STAGE/Contents/Resources"
ASAR="$RES/app.asar"
UNPACKED="$RES/app.asar.unpacked"
PLIST="$STAGE/Contents/Info.plist"
# ---------------------------------------------------------------------------
# 3. Extract asar under /tmp
# ---------------------------------------------------------------------------
log "Step 3: extracting app.asar ..."
rm -rf "$WORK"; mkdir -p "$WORK/extract"
npx -y @electron/asar extract "$ASAR" "$WORK/extract"
# ---------------------------------------------------------------------------
# 4. Locate and patch filter code (chunk hashes change every build)
# Older builds:
# l=o&&e!==`amazonBedrock` -> l=!1
# Newer builds (app-initial):
# B1 useHiddenModels:n.success?n.data:X.useHiddenModels -> useHiddenModels:!1
# B2 i&&t!==`amazonBedrock`?n.has(r.model):!r.hidden -> !r.hidden
# B3 i.useHiddenModels&&r!==`amazonBedrock`?i.availableModels.has(e.model):!e.hidden
# -> !e.hidden
# ---------------------------------------------------------------------------
log "Step 4: locating model filter code..."
ASSETS="$WORK/extract/webview/assets"
PATCHED=0
# --- Path A: older minified assignment ---
TARGET_OLD="$(grep -rl 'l=o&&e!==`amazonBedrock`' "$ASSETS/" 2>/dev/null | head -1 || true)"
if [[ -n "$TARGET_OLD" ]]; then
log " [old] target: ${TARGET_OLD#$WORK/extract/}"
perl -pi -e 's/l=o&&e!==`amazonBedrock`/l=!1/' "$TARGET_OLD"
grep -q 'l=o&&e!==`amazonBedrock`' "$TARGET_OLD" && { err "Old-pattern replace failed"; exit 1; }
log " [old] l=o&&e!==\`amazonBedrock\` -> l=!1"
PATCHED=$((PATCHED + 1))
fi
# --- Path B: useHiddenModels / availableModels whitelist ---
TARGET_NEW=""
while IFS= read -r f; do
if grep -q 'amazonBedrock' "$f" && grep -q 'availableModels' "$f"; then
TARGET_NEW="$f"
break
fi
done < <(grep -rl 'useHiddenModels' "$ASSETS/" --include='*.js' 2>/dev/null || true)
if [[ -n "$TARGET_NEW" ]]; then
log " [new] target: ${TARGET_NEW#$WORK/extract/}"
BEFORE_HASH="$(cksum "$TARGET_NEW" | awk '{print $1" "$2}')"
NEW_HITS=0
# B1: force useHiddenModels off
# Note: Perl s/// interpolates; escape $ inside the character class as \$
if grep -qE 'useHiddenModels:n\.success\?n\.data:[A-Za-z0-9_$]+\.useHiddenModels' "$TARGET_NEW"; then
perl -pi -e 's/useHiddenModels:n\.success\?n\.data:[A-Za-z0-9_\$]+\.useHiddenModels/useHiddenModels:!1/g' "$TARGET_NEW"
log " [new] useHiddenModels forced to !1"
NEW_HITS=$((NEW_HITS + 1)); PATCHED=$((PATCHED + 1))
fi
# B2: single-model visibility
if grep -q 'i&&t!==`amazonBedrock`?n.has(r.model):!r.hidden' "$TARGET_NEW"; then
perl -pi -e 's/i&&t!==`amazonBedrock`\?n\.has\(r\.model\):!r\.hidden/!r.hidden/g' "$TARGET_NEW"
log " [new] item filter -> !r.hidden"
NEW_HITS=$((NEW_HITS + 1)); PATCHED=$((PATCHED + 1))
fi
# B3: list filter
if grep -q 'i.useHiddenModels&&r!==`amazonBedrock`?i.availableModels.has(e.model):!e.hidden' "$TARGET_NEW"; then
perl -pi -e 's/i\.useHiddenModels&&r!==`amazonBedrock`\?i\.availableModels\.has\(e\.model\):!e\.hidden/!e.hidden/g' "$TARGET_NEW"
log " [new] list filter -> !e.hidden"
NEW_HITS=$((NEW_HITS + 1)); PATCHED=$((PATCHED + 1))
fi
AFTER_HASH="$(cksum "$TARGET_NEW" | awk '{print $1" "$2}')"
if [[ "$NEW_HITS" -eq 0 || "$BEFORE_HASH" == "$AFTER_HASH" ]]; then
err "Found a new-pattern target file but no replacements applied."
err "Inspect useHiddenModels near amazonBedrock in the extracted assets."
exit 1
fi
fi
if [[ "$PATCHED" -eq 0 ]]; then
err "Neither old nor new filter patterns were found."
err "Codex may have changed minified code. Search for useHiddenModels / amazonBedrock."
grep -rl 'useHiddenModels' "$ASSETS/" --include='*.js' 2>/dev/null | head || true
exit 1
fi
log " Applied $PATCHED replacement(s)"
# ---------------------------------------------------------------------------
# 5. Native modules that must stay unpacked
# ---------------------------------------------------------------------------
log "Step 5: computing unpack list for native modules..."
UNPACK_MODS="$(find "$UNPACKED/node_modules" -maxdepth 1 -mindepth 1 -type d 2>/dev/null \
| sed 's|.*/||' | sort -u | paste -sd'|' -)"
if [[ -z "$UNPACK_MODS" ]]; then
err "No modules found under app.asar.unpacked/node_modules; aborting to avoid a broken pack."
exit 1
fi
log " unpack modules: $UNPACK_MODS"
# ---------------------------------------------------------------------------
# 6. Repack (keep native modules unpacked)
# ---------------------------------------------------------------------------
log "Step 6: repacking asar..."
npx -y @electron/asar pack "$WORK/extract" "$WORK/app.asar.new" \
--unpack-dir "node_modules/@($UNPACK_MODS)"
log " Copying any missing unpacked files..."
( cd "$UNPACKED" && find . -type f ) | while read -r f; do
if [[ ! -f "$WORK/app.asar.new.unpacked/$f" ]]; then
mkdir -p "$WORK/app.asar.new.unpacked/$(dirname "$f")"
cp "$UNPACKED/$f" "$WORK/app.asar.new.unpacked/$f"
fi
done
# ---------------------------------------------------------------------------
# 7. Integrity hash = SHA256 of asar headerString (not the whole file)
# ---------------------------------------------------------------------------
log "Step 7: computing integrity hash..."
NEW_HASH="$(NODE_PATH="$NPX_ASAR" node -e "
const a=require('@electron/asar'),c=require('crypto');
process.stdout.write(c.createHash('sha256').update(a.getRawHeader(process.argv[1]).headerString).digest('hex'));
" "$WORK/app.asar.new")"
log " hash: $NEW_HASH"
# ---------------------------------------------------------------------------
# 8. Install into the staged app under /tmp
# ---------------------------------------------------------------------------
log "Step 8: applying patch inside $STAGE ..."
cp "$WORK/app.asar.new" "$ASAR"
rm -rf "$UNPACKED"
cp -R "$WORK/app.asar.new.unpacked" "$UNPACKED"
/usr/libexec/PlistBuddy -c \
"Set :ElectronAsarIntegrity:Resources/app.asar:hash $NEW_HASH" "$PLIST"
# ---------------------------------------------------------------------------
# 9. Ad-hoc re-sign under /tmp (no App Management restriction)
# ---------------------------------------------------------------------------
log "Step 9: clearing xattrs and ad-hoc signing..."
xattr -cr "$STAGE" 2>/dev/null || true
codesign --force --deep --sign - --timestamp=none "$STAGE"
codesign -v --verbose=2 "$STAGE" && log " Signature OK (ad-hoc)"
# ---------------------------------------------------------------------------
# 10. Swap into /Applications (needs App Management; Finder fallback)
# ---------------------------------------------------------------------------
log "Step 10: swapping into /Applications ..."
if rm -rf "$APP" 2>/dev/null && cp -R "$STAGE" "$APP" 2>/dev/null; then
xattr -cr "$APP" 2>/dev/null || true
codesign -v "$APP" && log " Installed to /Applications and verified"
rm -rf "$WORK"
log "Done. Open Codex; the model picker should show your third-party model name."
log "Backup dir: $BAK_DIR"
log "Restore: bash $0 restore"
else
rm -rf "$WORK"
err "Could not write /Applications: this terminal lacks App Management permission."
err "sudo will not help; this is TCC, not Unix permissions."
err "Staged build is ready at: $STAGE"
err ""
err "Finish install with one of:"
err " (a) System Settings → Privacy & Security → App Management"
err " enable this terminal, fully quit and reopen it, then re-run,"
err " or run: bash $(dirname "$0")/install-patched-codex.sh"
err " (b) Finder: quit ChatGPT → move /Applications/ChatGPT.app to Trash"
err " → drag /tmp/ChatGPT.app into /Applications → open and verify."
exit 2
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment