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.
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):
- Statsig feature gate
1506311413— server-evaluated. Returnsfalsefor EU/non-rolled-out accounts. This is the region wall. - 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).
Open ~/.codex/config.toml and add computer_use = true to your [features] block. If the block doesn't exist, create it.
[features]
computer_use = trueThat's it for Step 1. Save the file.
Codex needs to know you actually want this plugin enabled. Append to ~/.codex/config.toml:
[plugins."computer-use@openai-bundled"]
enabled = trueNote: 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.
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.plistElectronAsarIntegrity - Ad-hoc re-signs the
.appbundle so macOS hardened runtime accepts it
mkdir -p /tmp/codex-asar-tools && cd /tmp/codex-asar-tools
npm init -y >/dev/null
npm install @electron/asarSave 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.');cd /tmp/codex-asar-tools && node patch.mjsExpected output (last lines):
new asar header hash: e5bf56229175a64fea86dd21519c19b7b54a0014ac752016975c193bd9737385
/Applications/Codex.app: replacing existing signature
Done. Restart Codex.app.
pkill -f "/Applications/Codex.app"
open -a CodexOpen Settings → Plugins. Computer Use now appears next to Browser Use.
# 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.plistIf Codex starts and the plugin tile is visible, you're done.
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.appYou 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.
These are the dead ends I hit before finding the working approach. Skip them.
@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.
@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.
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 intoInfo.plist.
Skip either and you get the FATAL above.
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.
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:
- Extracting
app.asarto a temp dir (npx @electron/asar extract /Applications/Codex.app/Contents/Resources/app.asar /tmp/extracted) - Grepping for
featureNameandcomputer_useinwebview/assets/*.js - Identifying the gate ID literal passed to the Statsig hook (e.g.
s(\`)`)
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.
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.