# Unlock Computer Use plugin in Codex.app for EU / region-locked accounts (in-place asar patch)

> Source: <https://gist.github.com/Saik0s/6d098d9caae564f48e4decfeaea67f1d>
> Published: 2026-05-09 20:58:20+00:00

# 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.

```toml
[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`:

```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 to `true`)
- 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

```bash
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`:

```js
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

```bash
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

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

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

---

## How to verify it worked

```bash
# 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:

```bash
# 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(\`<digits>\`)`)

---

## 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](https://gist.github.com/clairernovotny/89587e4932d854b10bbab913b95ecb5c).

Use at your own risk. Not affiliated with OpenAI.

