Skip to content

Instantly share code, notes, and snippets.

@Yubo-Cao
Created June 10, 2026 04:21
Show Gist options
  • Select an option

  • Save Yubo-Cao/8b21ac616db0199f98676b41e49d3a8e to your computer and use it in GitHub Desktop.

Select an option

Save Yubo-Cao/8b21ac616db0199f98676b41e49d3a8e to your computer and use it in GitHub Desktop.
Codex Patch
diff --git a/.SRCINFO b/.SRCINFO
index fb17634..d996c01 100644
--- a/.SRCINFO
+++ b/.SRCINFO
@@ -5,6 +5,7 @@ pkgbase = codex-app-unofficial
url = https://github.com/better-slop/codex-app-linux
arch = x86_64
license = custom
+ makedepends = asar
depends = alsa-lib
depends = gtk3
depends = libnotify
@@ -18,7 +19,9 @@ pkgbase = codex-app-unofficial
install = codex-app-unofficial.install
source = codex-app-linux-26.608.12217-launcher.27-x64-linux-unpacked.tar.gz::https://github.com/better-slop/codex-app-linux/releases/download/v26.608.12217-launcher.27/codex-app-linux-26.608.12217-launcher.27-x64-linux-unpacked.tar.gz
source = codex-app-linux-26.608.12217-launcher.27-x64.png::https://github.com/better-slop/codex-app-linux/releases/download/v26.608.12217-launcher.27/codex-app-linux-26.608.12217-launcher.27-x64.png
+ source = patch-settings-overlap.mjs
sha256sums = 066d104a85c87cf5058b1ae24e04f4dbe63460f0be264c5b8caf8b90162b60ae
sha256sums = 1c926e380bfe6a50f40648dd9bc5de88da7271546491adf99ec72172e17df6a0
+ sha256sums = 074a9f4c874172786bc465f95f5a41c7130e33cb5585177517bb7bd7584ff7d4
pkgname = codex-app-unofficial
diff --git a/PKGBUILD b/PKGBUILD
index 3704bfa..73db752 100644
--- a/PKGBUILD
+++ b/PKGBUILD
@@ -5,6 +5,7 @@ pkgdesc='Unofficial Linux build of Codex from OpenAI'\''s Codex appcast feed.'
arch=('x86_64')
url='https://github.com/better-slop/codex-app-linux'
license=('custom')
+makedepends=('asar')
depends=('alsa-lib' 'gtk3' 'libnotify' 'libsecret' 'libxss' 'nss' 'xdg-utils')
provides=('codex-app-linux-bin')
conflicts=('codex-app-linux-bin')
@@ -13,12 +14,23 @@ install='codex-app-unofficial.install'
source=(
'codex-app-linux-26.608.12217-launcher.27-x64-linux-unpacked.tar.gz::https://github.com/better-slop/codex-app-linux/releases/download/v26.608.12217-launcher.27/codex-app-linux-26.608.12217-launcher.27-x64-linux-unpacked.tar.gz'
'codex-app-linux-26.608.12217-launcher.27-x64.png::https://github.com/better-slop/codex-app-linux/releases/download/v26.608.12217-launcher.27/codex-app-linux-26.608.12217-launcher.27-x64.png'
+ 'patch-settings-overlap.mjs'
)
sha256sums=(
'066d104a85c87cf5058b1ae24e04f4dbe63460f0be264c5b8caf8b90162b60ae'
'1c926e380bfe6a50f40648dd9bc5de88da7271546491adf99ec72172e17df6a0'
+ '074a9f4c874172786bc465f95f5a41c7130e33cb5585177517bb7bd7584ff7d4'
)
+# 2026-06-09 Yubo-Cao
+# Purpose: patch Codex's packaged Electron app before installation so Linux
+# windows and renderer surfaces no longer render transparent flickering panes.
+# Parameters: none; makepkg provides srcdir and the extracted app bundle.
+# Returns: exits non-zero if the expected Electron/CSS/settings anchors are missing.
+prepare() {
+ node "${srcdir}/patch-settings-overlap.mjs" "${srcdir}/linux-unpacked/resources/app.asar"
+}
+
package() {
install -dm755 "${pkgdir}/opt/codex-app-linux"
cp -a "${srcdir}/linux-unpacked/." "${pkgdir}/opt/codex-app-linux/"
diff --git a/patch-settings-overlap.mjs b/patch-settings-overlap.mjs
new file mode 100644
index 0000000..571632f
--- /dev/null
+++ b/patch-settings-overlap.mjs
@@ -0,0 +1,247 @@
+#!/usr/bin/env node
+
+import { execFileSync } from "node:child_process";
+import { createHash } from "node:crypto";
+import { existsSync, mkdtempSync, readFileSync, rmSync, renameSync, writeFileSync } from "node:fs";
+import { readdir } from "node:fs/promises";
+import { basename, dirname, join } from "node:path";
+
+const SETTINGS_BRANCH_PATTERN = /if\([A-Za-z_$][A-Za-z0-9_$]*\)\{let t;/g;
+const DISABLED_SETTINGS_BRANCH = "if(0){let t;";
+const TRANSPARENT_BACKDROP_PROBE = "isSystemBackdropSupported():!0}";
+const OPAQUE_BACKDROP_PROBE = "isSystemBackdropSupported():!1}";
+const TRANSPARENT_TOKEN_CHAIN = "--color-token-side-bar-background:var(--vscode-sideBar-background);--color-token-bg-primary:var(--color-token-side-bar-background);--color-token-diff-surface:var(--color-token-main-surface-primary)}";
+const OPAQUE_TOKEN_CHAIN = "--color-token-side-bar-background:var(--color-background-surface-under);--color-token-bg-primary:var(--color-background-surface);--color-token-diff-surface:var(--color-token-main-surface-primary)}";
+const ROOT_BACKGROUND_MARKER = "#root{height:100vh}";
+const ROOT_OPAQUE_BACKGROUND = "html,body{background:var(--color-background-surface);min-height:100%}#root{height:100vh;background:var(--color-background-surface)}";
+const UNPACK_DIRS = "node_modules/{better-sqlite3,node-pty}";
+
+/**
+ * 2026-06-06 苍朮
+ * Purpose: count exact substring occurrences while avoiding regular-expression escaping mistakes.
+ * @param {string} text - Source text to inspect.
+ * @param {string} needle - Exact substring to count.
+ * @returns {number} Number of exact occurrences in the source text.
+ */
+function countOccurrences(text, needle) {
+ return text.split(needle).length - 1;
+}
+
+/**
+ * 2026-06-06 苍朮
+ * Purpose: recursively list files below a directory so hashed bundle names can change across Codex releases.
+ * @param {string} root - Directory to scan recursively.
+ * @returns {Promise<string[]>} Absolute file paths contained in the directory tree.
+ */
+async function listFiles(root) {
+ const entries = await readdir(root, { withFileTypes: true });
+ const files = await Promise.all(
+ entries.map((entry) => {
+ const filePath = join(root, entry.name);
+ return entry.isDirectory() ? listFiles(filePath) : [filePath];
+ }),
+ );
+ return files.flat();
+}
+
+/**
+ * 2026-06-06 苍朮
+ * Purpose: find one bundle by filename and source anchors, failing loudly when upstream layout changes.
+ * @param {string} root - Directory to scan recursively.
+ * @param {RegExp} fileNamePattern - Regular expression matched against each basename.
+ * @param {(source: string) => boolean} hasAnchor - Predicate that confirms the file owns the target anchor.
+ * @returns {Promise<string>} Absolute path to the only matching bundle.
+ */
+async function findBundle(root, fileNamePattern, hasAnchor) {
+ const candidates = (await listFiles(root)).filter((filePath) => fileNamePattern.test(basename(filePath)));
+ const matches = candidates.filter((filePath) => hasAnchor(readFileSync(filePath, "utf8")));
+
+ if (matches.length !== 1) {
+ throw new Error(`Expected exactly one bundle for ${fileNamePattern}, found ${matches.length}: ${matches.join(", ")}`);
+ }
+
+ return matches[0];
+}
+
+/**
+ * 2026-06-06 苍朮
+ * Purpose: replace a string exactly once and fail loudly if the bundle layout changed.
+ * @param {string} text - Source text to patch.
+ * @param {string} oldValue - Exact text expected before patching.
+ * @param {string} newValue - Exact text to write into the source.
+ * @returns {string} Patched text with one occurrence replaced.
+ */
+function replaceExactlyOnce(text, oldValue, newValue) {
+ const count = countOccurrences(text, oldValue);
+ if (count !== 1) {
+ throw new Error(`Expected exactly one occurrence of ${JSON.stringify(oldValue)}, found ${count}`);
+ }
+ return text.replace(oldValue, newValue);
+}
+
+/**
+ * 2026-06-06 苍朮
+ * Purpose: replace an exact anchor once while allowing a previously patched archive to validate cleanly.
+ * @param {string} text - Source text to patch or validate.
+ * @param {string} oldValue - Exact text expected before patching.
+ * @param {string} newValue - Exact patched text expected after patching.
+ * @returns {string} Source text with the patched value present exactly once.
+ */
+function replaceOnceOrValidate(text, oldValue, newValue) {
+ const oldCount = countOccurrences(text, oldValue);
+ const newCount = countOccurrences(text, newValue);
+
+ if (oldCount === 1 && newCount === 0) {
+ return text.replace(oldValue, newValue);
+ }
+ if (oldCount === 0 && newCount === 1) {
+ return text;
+ }
+
+ throw new Error(`Expected old=1/new=0 or old=0/new=1, found old=${oldCount}, new=${newCount}`);
+}
+
+/**
+ * 2026-06-09 Yubo-Cao
+ * Purpose: report the system-backdrop probe as unsupported so the app's own opaque-window-surface
+ * fallback engages on Linux. Stock Electron lacks BrowserWindow.isSystemBackdropSupported and the
+ * upstream default of "supported" leaves main windows with a fully transparent background color,
+ * which flickers under Linux compositors. With the probe reporting "unsupported", every non-overlay
+ * window is created with an opaque theme background and the main process notifies renderers, which
+ * apply the built-in `electron-opaque` stylesheet path; overlay windows keep their transparency
+ * because the fallback already excludes overlay appearances.
+ * @param {string} bundlePath - Absolute path to the Electron main-process JavaScript bundle.
+ * @returns {{bundlePath: string, hash: string}} Patched bundle path and SHA256 hash.
+ */
+function patchWindowManagerBundle(bundlePath) {
+ const source = replaceOnceOrValidate(readFileSync(bundlePath, "utf8"), TRANSPARENT_BACKDROP_PROBE, OPAQUE_BACKDROP_PROBE);
+ writeFileSync(bundlePath, source);
+ return {
+ bundlePath,
+ hash: createHash("sha256").update(source).digest("hex"),
+ };
+}
+
+/**
+ * 2026-06-09 Yubo-Cao
+ * Purpose: make the renderer root and shared surface tokens opaque so full-page app chrome cannot show
+ * through itself even if the opaque-surface notification from the main process is missed at startup.
+ * @param {string} bundlePath - Absolute path to the shared app CSS bundle.
+ * @returns {{bundlePath: string, hash: string}} Patched bundle path and SHA256 hash.
+ */
+function patchRendererCssBundle(bundlePath) {
+ let source = readFileSync(bundlePath, "utf8");
+ source = replaceOnceOrValidate(source, TRANSPARENT_TOKEN_CHAIN, OPAQUE_TOKEN_CHAIN);
+ source = replaceOnceOrValidate(source, ROOT_BACKGROUND_MARKER, ROOT_OPAQUE_BACKGROUND);
+ writeFileSync(bundlePath, source);
+ return {
+ bundlePath,
+ hash: createHash("sha256").update(source).digest("hex"),
+ };
+}
+
+/**
+ * 2026-06-06 苍朮
+ * Purpose: keep the settings page from taking the dynamic host-navigation branch that previously exposed translucent panes.
+ * @param {string} bundlePath - Absolute path to the settings-page JavaScript bundle.
+ * @returns {{bundlePath: string, hash: string}} Patched bundle path and SHA256 hash.
+ */
+function patchSettingsBundle(bundlePath) {
+ let source = readFileSync(bundlePath, "utf8");
+ const activeBranches = source.match(SETTINGS_BRANCH_PATTERN) ?? [];
+ const disabledBranchCount = countOccurrences(source, DISABLED_SETTINGS_BRANCH);
+
+ if (activeBranches.length === 1 && disabledBranchCount === 0) {
+ source = replaceExactlyOnce(source, activeBranches[0], DISABLED_SETTINGS_BRANCH);
+ } else if (activeBranches.length !== 0 || disabledBranchCount !== 1) {
+ throw new Error(
+ `Expected one active settings branch or one disabled settings branch, found active=${activeBranches.length}, disabled=${disabledBranchCount}`,
+ );
+ }
+
+ if ((source.match(SETTINGS_BRANCH_PATTERN) ?? []).length !== 0) {
+ throw new Error("Settings host branch is still active after patching");
+ }
+ if (countOccurrences(source, DISABLED_SETTINGS_BRANCH) !== 1) {
+ throw new Error("Disabled settings host branch was not written exactly once");
+ }
+
+ writeFileSync(bundlePath, source);
+ return {
+ bundlePath,
+ hash: createHash("sha256").update(source).digest("hex"),
+ };
+}
+
+/**
+ * 2026-06-06 苍朮
+ * Purpose: run a command with inherited output so makepkg logs show the asar operation that failed.
+ * @param {string} command - Executable name to run.
+ * @param {string[]} args - Arguments passed to the executable.
+ * @returns {void}
+ */
+function run(command, args) {
+ execFileSync(command, args, { stdio: "inherit" });
+}
+
+/**
+ * 2026-06-06 苍朮
+ * Purpose: extract, patch, and repack Codex's app.asar while preserving native module unpacking.
+ * @param {string} appAsar - Path to the app.asar archive inside the extracted Linux bundle.
+ * @returns {Promise<void>} Resolves after the archive has been replaced with the patched version.
+ */
+async function main(appAsar) {
+ if (!appAsar || !existsSync(appAsar)) {
+ throw new Error(`app.asar not found: ${appAsar ?? "<missing argument>"}`);
+ }
+
+ const appUnpacked = `${appAsar}.unpacked`;
+ if (!existsSync(appUnpacked)) {
+ throw new Error(`app.asar.unpacked not found: ${appUnpacked}`);
+ }
+
+ const tempRoot = mkdtempSync(join(dirname(appAsar), ".codex-app-unofficial-asar-"));
+ const extractedRoot = join(tempRoot, "app");
+ const patchedAsar = join(tempRoot, "app.asar");
+ const patchedUnpacked = `${patchedAsar}.unpacked`;
+
+ try {
+ run("asar", ["extract", appAsar, extractedRoot]);
+ const mainResult = patchWindowManagerBundle(
+ await findBundle(join(extractedRoot, ".vite", "build"), /^main-[A-Za-z0-9_-]+\.js$/, (source) =>
+ source.includes(TRANSPARENT_BACKDROP_PROBE) || source.includes(OPAQUE_BACKDROP_PROBE),
+ ),
+ );
+ const cssResult = patchRendererCssBundle(
+ await findBundle(join(extractedRoot, "webview", "assets"), /^app-[A-Za-z0-9_-]*\.css$/, (source) =>
+ source.includes(TRANSPARENT_TOKEN_CHAIN) || source.includes(OPAQUE_TOKEN_CHAIN),
+ ),
+ );
+ const settingsResult = patchSettingsBundle(
+ await findBundle(join(extractedRoot, "webview", "assets"), /^settings-page-[A-Za-z0-9_-]+\.js$/, (source) =>
+ (source.match(SETTINGS_BRANCH_PATTERN) ?? []).length > 0 || source.includes(DISABLED_SETTINGS_BRANCH),
+ ),
+ );
+
+ run("asar", ["pack", "--unpack-dir", UNPACK_DIRS, extractedRoot, patchedAsar]);
+ renameSync(patchedAsar, appAsar);
+ if (existsSync(patchedUnpacked)) {
+ rmSync(appUnpacked, { recursive: true, force: true });
+ renameSync(patchedUnpacked, appUnpacked);
+ }
+
+ console.log(`Patched ${mainResult.bundlePath}`);
+ console.log(`Patched main-process bundle sha256=${mainResult.hash}`);
+ console.log(`Patched ${cssResult.bundlePath}`);
+ console.log(`Patched renderer CSS bundle sha256=${cssResult.hash}`);
+ console.log(`Patched ${settingsResult.bundlePath}`);
+ console.log(`Patched settings bundle sha256=${settingsResult.hash}`);
+ } finally {
+ rmSync(tempRoot, { recursive: true, force: true });
+ }
+}
+
+main(process.argv[2]).catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment