Skip to content

Instantly share code, notes, and snippets.

@Saik0s
Created May 9, 2026 20:58
Show Gist options
  • Select an option

  • Save Saik0s/6d098d9caae564f48e4decfeaea67f1d to your computer and use it in GitHub Desktop.

Select an option

Save Saik0s/6d098d9caae564f48e4decfeaea67f1d to your computer and use it in GitHub Desktop.
Unlock Computer Use plugin in Codex.app for EU / region-locked accounts (in-place asar patch)

Unlock Computer Use in Codex.app (EU / region-locked accounts)

OpenAI's Codex desktop app ships with a Computer Use plugin (computer-use@openai-bundled) but hides it from users in regions where the feature isn't rolled out yet — including most of the EU. The plugin binary is already on your machine (/Applications/Codex.app/Contents/Resources/plugins/openai-bundled/computer-use/); only the UI is gated.

This guide flips two flags so the plugin appears in your plugin picker.

Tested on: macOS 15 (arm64), Codex.app version 26.506.31421 (May 2026 build). Difficulty: Intermediate. You'll edit a TOML config and binary-patch Codex's Electron bundle. Risk: Low. We back up everything we touch and never change file lengths. Time: ~5 minutes.


Why two flags?

The Codex client side gates computer-use behind two checks (both in webview/assets/use-in-app-browser-use-availability-_UMFu9j2.js inside app.asar):

  1. Statsig feature gate 1506311413 — server-evaluated. Returns false for EU/non-rolled-out accounts. This is the region wall.
  2. Local experimental feature flag computer_use — read from ~/.codex/config.toml [features] section. Defaults off.

We enable both: edit the config (easy), and patch one 15-byte string inside app.asar so the Statsig call always returns truthy (less easy, but mechanical).


Step 1 — Add the local feature flag

Open ~/.codex/config.toml and add computer_use = true to your [features] block. If the block doesn't exist, create it.

[features]
computer_use = true

That's it for Step 1. Save the file.


Step 2 — Add the plugin entry to your config

Codex needs to know you actually want this plugin enabled. Append to ~/.codex/config.toml:

[plugins."computer-use@openai-bundled"]
enabled = true

Note: Codex's startup-sync may strip this entry if the Statsig gate still rejects the plugin. That's why Step 3 is required before Step 2 will stick.


Step 3 — Patch the Statsig gate inside app.asar

This is the only invasive step. We do an in-place binary edit (no repacking, no extracting). The patch:

  • Replaces s(\1506311413`)( !0)(15 bytes → 15 bytes, evaluates totrue`)
  • Recomputes the SHA-256 of the patched file and overwrites the matching hash strings in the asar JSON header (same length, since SHA-256 hex is always 64 chars)
  • Recomputes the asar header SHA-256 and writes it to Info.plist ElectronAsarIntegrity
  • Ad-hoc re-signs the .app bundle so macOS hardened runtime accepts it

3a. Install the helper

mkdir -p /tmp/codex-asar-tools && cd /tmp/codex-asar-tools
npm init -y >/dev/null
npm install @electron/asar

3b. Save the patcher script

Save this as /tmp/codex-asar-tools/patch.mjs:

import * as asar from '@electron/asar';
import { readFileSync, writeFileSync, copyFileSync } from 'node:fs';
import { createHash } from 'node:crypto';
import { execSync } from 'node:child_process';

const APP        = '/Applications/Codex.app';
const ASAR       = `${APP}/Contents/Resources/app.asar`;
const PLIST      = `${APP}/Contents/Info.plist`;
const REL        = 'webview/assets/use-in-app-browser-use-availability-_UMFu9j2.js';
const BEFORE     = `s(\`1506311413\`)`;   // 15 bytes
const AFTER      = `(           !0)`;     // 15 bytes, evaluates !0 = true
const STAMP      = Date.now();

// 0. Back up
copyFileSync(ASAR,  `${ASAR}.bak.${STAMP}`);
copyFileSync(PLIST, `${PLIST}.bak.${STAMP}`);
console.log(`backups: *.bak.${STAMP}`);

// 1. Parse asar layout from raw bytes (NOT from getRawHeader's headerSize, which is off by 8)
const fullBuf = readFileSync(ASAR);
const innerPickleTotal = fullBuf.readUInt32LE(4);
const stringLen        = fullBuf.readUInt32LE(12);
const headerJsonStart  = 16;
const headerJsonEnd    = headerJsonStart + stringLen;
const dataStart        = 8 + innerPickleTotal;        // <-- where file content blob begins

const header = JSON.parse(fullBuf.subarray(headerJsonStart, headerJsonEnd).toString('utf8'));

function get(node, parts) {
  for (const p of parts) {
    if (!node.files || !node.files[p]) throw new Error('missing entry: ' + parts.join('/'));
    node = node.files[p];
  }
  return node;
}
const entry          = get(header, REL.split('/'));
const fileSize       = Number(entry.size);
const fileAbsOffset  = dataStart + Number(entry.offset);

// 2. Read the file, swap the pattern, verify size
const orig = fullBuf.subarray(fileAbsOffset, fileAbsOffset + fileSize);
const text = orig.toString('utf8');
if (!text.includes(BEFORE)) throw new Error('pattern not found — Codex version may have changed');
if (BEFORE.length !== AFTER.length) throw new Error('replacement length mismatch');
const newBuf = Buffer.from(text.replace(BEFORE, AFTER), 'utf8');

// 3. Sanity: pre-patch hash must match header
const preHash = createHash('sha256').update(orig).digest('hex');
if (preHash !== entry.integrity.hash) throw new Error('pre-patch hash mismatch — file may already be patched or asar is corrupt');

// 4. Compute new file integrity (single block since file < 4 MiB)
const newOverall = createHash('sha256').update(newBuf).digest('hex');
const newBlocks  = [];
for (let off = 0; off < newBuf.length; off += entry.integrity.blockSize) {
  newBlocks.push(createHash('sha256').update(newBuf.subarray(off, off + entry.integrity.blockSize)).digest('hex'));
}

// 5. In-place edit: replace hash strings inside the JSON header
function replaceInRange(big, [lo, hi], oldStr, newStr) {
  const oldB = Buffer.from(oldStr, 'utf8');
  const newB = Buffer.from(newStr, 'utf8');
  if (oldB.length !== newB.length) throw new Error('hash length changed');
  let pos = lo, count = 0;
  while (pos < hi) {
    const found = big.indexOf(oldB, pos);
    if (found < 0 || found >= hi) break;
    newB.copy(big, found);
    count++;
    pos = found + 1;
  }
  return count;
}
replaceInRange(fullBuf, [headerJsonStart, headerJsonEnd], entry.integrity.hash, newOverall);
for (let i = 0; i < entry.integrity.blocks.length; i++) {
  replaceInRange(fullBuf, [headerJsonStart, headerJsonEnd], entry.integrity.blocks[i], newBlocks[i]);
}

// 6. Patch file content at the correct offset
newBuf.copy(fullBuf, fileAbsOffset);
writeFileSync(ASAR, fullBuf);

// 7. Recompute asar header hash for Info.plist
const newHeaderJson = readFileSync(ASAR).subarray(headerJsonStart, headerJsonEnd).toString('utf8');
const newHeaderHash = createHash('sha256').update(newHeaderJson).digest('hex');
console.log('new asar header hash:', newHeaderHash);

// 8. Update Info.plist
execSync(`/usr/libexec/PlistBuddy -c "Set :ElectronAsarIntegrity:Resources/app.asar:hash ${newHeaderHash}" "${PLIST}"`);

// 9. Ad-hoc re-sign (Info.plist change invalidates Apple's signature)
execSync(`codesign --force --deep --sign - "${APP}"`, { stdio: 'inherit' });

console.log('Done. Restart Codex.app.');

3c. Run it

cd /tmp/codex-asar-tools && node patch.mjs

Expected output (last lines):

new asar header hash: e5bf56229175a64fea86dd21519c19b7b54a0014ac752016975c193bd9737385
/Applications/Codex.app: replacing existing signature
Done. Restart Codex.app.

3d. Restart Codex

pkill -f "/Applications/Codex.app"
open -a Codex

Open Settings → Plugins. Computer Use now appears next to Browser Use.


How to verify it worked

# 1. Codex.app process is alive (no FATAL on launch)
ps aux | grep "/Applications/Codex.app/Contents/MacOS/Codex" | grep -v grep

# 2. Check the patched bytes are present in app.asar
grep -ao '(           !0)' /Applications/Codex.app/Contents/Resources/app.asar | head -1

# 3. Confirm Info.plist integrity hash matches the new header
/usr/libexec/PlistBuddy -c "Print :ElectronAsarIntegrity" /Applications/Codex.app/Contents/Info.plist

If Codex starts and the plugin tile is visible, you're done.


Rollback

The patcher leaves backups at /Applications/Codex.app/Contents/Resources/app.asar.bak.<timestamp> and /Applications/Codex.app/Contents/Info.plist.bak.<timestamp>. To revert:

# Find the backup timestamps
ls /Applications/Codex.app/Contents/Resources/app.asar.bak.*
ls /Applications/Codex.app/Contents/Info.plist.bak.*

# Restore (replace TS with the timestamp from above)
TS=1778355270
cp "/Applications/Codex.app/Contents/Resources/app.asar.bak.$TS"   /Applications/Codex.app/Contents/Resources/app.asar
cp "/Applications/Codex.app/Contents/Info.plist.bak.$TS"           /Applications/Codex.app/Contents/Info.plist
codesign --force --deep --sign - /Applications/Codex.app

You can also just remove [plugins."computer-use@openai-bundled"] and computer_use = true from ~/.codex/config.toml if you want to disable the feature without unpatching.


Pitfalls (what NOT to do)

These are the dead ends I hit before finding the working approach. Skip them.

Don't repack the asar

@electron/asar pack looks like the obvious tool, but Codex marks node_modules/{better-sqlite3,node-pty,objc-js} as unpacked (their native .node binaries live in app.asar.unpacked/, not inside the asar). A naive repack pulls those files back into the asar; Codex then exits at startup with:

Codex failed to start.
better-sqlite3 is only bundled with the Electron app

In-place editing avoids this entirely.

Don't trust getRawHeader().headerSize as the data offset

@electron/asar's getRawHeader() returns headerSize equal to the inner pickle size (e.g. 371956). The actual file content blob starts 8 bytes later (after the outer pickle preamble), at byte 8 + innerPickleTotal (e.g. 371964).

If you use headerSize as the data start, your read/write offsets are off by 8. The content modification still lands correctly (read and write both use the same wrong base, so they cancel), but the SHA-256 you compute is over the wrong bytes — Electron then fails block validation:

FATAL:asar_file_validator.cc:129] Failed to validate block while ending ASAR file stream: 0

The patcher above parses the pickle preamble manually to avoid this.

Don't skip the asar integrity update

Modern Electron embeds per-file SHA-256 hashes in the asar JSON header (integrity.hash and integrity.blocks[]) and a top-level header hash in Info.plist:ElectronAsarIntegrity. Both must be updated:

  • File hash lives inside the JSON header. Updating it does not change header byte length (SHA-256 hex is always 64 chars).
  • Header hash is sha256(headerJsonBytes) and goes into Info.plist.

Skip either and you get the FATAL above.

Don't skip the ad-hoc resign

Editing Info.plist invalidates Apple's code signature. Without codesign --force --deep --sign -, the app may refuse to launch under hardened runtime. Ad-hoc signing (-) is enough — you don't need a Developer ID.


Auto-update warning

Codex auto-updates via Sparkle. A new version will overwrite app.asar and undo the patch. Re-run node /tmp/codex-asar-tools/patch.mjs after each update.

To make this less annoying, save patch.mjs somewhere durable (your dotfiles repo, ~/bin/, etc.) and consider wrapping it in a launchd agent that watches the version string in Info.plist and re-runs on change.

If a future Codex release renames or rewrites use-in-app-browser-use-availability-_UMFu9j2.js, the patcher will exit with pattern not found — Codex version may have changed. You'll need to re-locate the new bundle filename and the new Statsig gate ID by:

  1. Extracting app.asar to a temp dir (npx @electron/asar extract /Applications/Codex.app/Contents/Resources/app.asar /tmp/extracted)
  2. Grepping for featureName and computer_use in webview/assets/*.js
  3. Identifying the gate ID literal passed to the Statsig hook (e.g. s(\`)`)

Why this works

The Codex Electron client's plugin picker filters out computer-use@openai-bundled when either the Statsig gate 1506311413 returns false or the local experimental feature computer_use is unset. Both paths are inside webview/assets/use-in-app-browser-use-availability-_UMFu9j2.js in a function that returns { available, isFetching, isLoading }. By forcing the Statsig call to evaluate truthy and setting the local flag, available becomes true and the plugin tile renders.

The plugin binary itself ships with the app already — no download needed, no API key, no auth bypass. The server-side plugin/list JSON-RPC method already returns computer-use correctly even on EU accounts (verified by directly driving the app-server stdio interface). The gating is purely client-side.


Credits

Reverse-engineered from a fresh Codex.app extraction in May 2026, building on the Codex plugin/marketplace architecture documented in this gist.

Use at your own risk. Not affiliated with OpenAI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment