Created
August 1, 2026 09:09
-
-
Save hqman/8055375bd40a8885d4038e8ba4409074 to your computer and use it in GitHub Desktop.
codex-model-filter-patch.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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