Linux Features Architecture
June 29, 2026 ยท View on GitHub
linux-features/ is the extension boundary for optional Linux integrations.
Core keeps a small generic loader; feature-specific behavior lives in feature
directories and is disabled by default.
Layout
Repository features live directly under linux-features/<feature-id>/.
User-local features live under linux-features/local/<feature-id>/. The
linux-features/local/ directory is ignored by git, so a user can keep private
or experimental integrations in the checkout without accidentally committing
them.
Every feature needs a feature.json manifest and a neighboring README.md.
The README is required for both repository features and git-ignored local
features, and should describe what the feature does, how to test it, and known
support risks.
{
"id": "my-feature",
"title": "My Feature",
"description": "Optional Linux integration.",
"defaultEnabled": false
}
Feature ids must match ^[a-z0-9][a-z0-9-]*$. Repository and local features
share one id namespace; local features cannot shadow repository features.
defaultEnabled: true is rejected. Enabling always happens through the
git-ignored linux-features/features.json file:
{
"enabled": ["my-feature"]
}
Feature developers can also define user-overridable settings. Keep shipped
defaults in tracked feature.json, and read user-specific overrides from the
git-ignored features.json file under settings.<feature-id>:
{
"enabled": ["my-feature"],
"settings": {
"my-feature": {
"option": "local value"
}
}
}
Patch descriptors receive this object as context.feature.settings. Treat it
as optional, validate the shape inside the feature, warn on invalid values, and
fall back to manifest defaults rather than failing the build.
Lifecycle
The build pipeline loads enabled features in these phases:
- ASAR patching: patch descriptors modify extracted upstream app files.
- App staging: declarative resources and runtime hooks are copied into
codex-app/. - Legacy staging: optional
stage.shhooks run for features that still need custom install-time logic. - Native packaging: optional package hooks can mutate the
.deb,.rpm, or pacman staging root. - Runtime: the launcher consumes staged environment files, prelaunch hooks, Electron args, and cold-start hooks.
Native packages copy the configured feature root into the packaged
update-builder bundle, including linux-features/local/, and write a
sanitized features.json containing the enabled ids plus settings for enabled
features. Local auto-updates therefore rebuild with the same opt-in features
and user-specific feature settings.
Declarative staged files are tracked in
.codex-linux/linux-features-staged.json. On the next install, the framework
removes the previously tracked declarative resources and runtime hooks before
staging the currently enabled set, so disabling a feature removes its
framework-owned runtime hooks. Legacy stage.sh hooks are not tracked by this
manifest and must clean up any feature-owned files themselves.
Manifest Keys
entrypoints keeps the existing patch and staging API:
{
"entrypoints": {
"patchDescriptors": "./patch.js",
"patches": "./patch.js",
"mainBundlePatch": "./patch.js",
"stageHook": "./stage.sh"
}
}
Prefer patchDescriptors for new patches. Feature descriptor ids are reported
as feature:<feature-id>:<descriptor-id> and are optional in CI by default.
mainBundlePatch is the compatibility path for older features that export
applyMainBundlePatch(source, context).
Use requires and conflicts to declare feature relationships:
{
"requires": ["read-aloud"],
"conflicts": ["other-voice-loop"]
}
The setup wizard, installer, patcher, and package builders validate these relationships before applying enabled features.
Declarative App Staging
Use resources to copy files into the generated app directory:
{
"resources": [
{
"source": "assets/tool.json",
"target": ".codex-linux/features/my-feature/tool.json",
"mode": "0644"
}
]
}
source stays inside the feature directory. target is relative to the app
directory and must point to a file or subdirectory, not the app root itself.
File modes are optional, but when present they must be quoted octal strings
such as "0644" or "0755"; numeric JSON modes are rejected. Declared modes
are recorded in the staged manifest and restored after native package
permission normalization, so restrictive resource modes survive .deb, .rpm,
and pacman packaging.
Use runtimeHooks for launcher-visible hooks:
{
"runtimeHooks": {
"env": "env",
"prelaunch": "prelaunch.sh",
"electronArgs": "electron-args",
"launcher": "launcher.sh",
"coldStart": "cold-start.sh",
"afterExit": "after-exit.sh"
}
}
The runtime hook types map to:
env: copied to.codex-linux/env.d/; each non-comment line is exported as literalKEY=VALUEwith no shell evaluation.prelaunch: copied to.codex-linux/prelaunch.d/; executable hooks run synchronously before the packaged runtime prelaunch and webview setup.electronArgs: copied to.codex-linux/electron-args.d/; each non-comment line is appended as one Electron argument.launcher: copied to.codex-linux/launcher.d/; executable hooks run after feature, user, and command-line Electron args are merged, but before final Electron launch args are built. Hooks receive the current Electron args as argv and may printenv KEY=VALUEorelectron-arg VALUElines on stdout. Unknown output lines are ignored; stderr is logged normally.coldStart: copied to.codex-linux/cold-start.d/; executable hooks run in the background during cold start, after bundled plugin cache sync.afterExit: copied to.codex-linux/after-exit.d/; executable hooks run after Electron exits. Hook failures are logged and the launcher preserves Electron's original exit status.
Runtime hooks receive CODEX_HOME, CODEX_LINUX_APP_DIR,
CODEX_LINUX_APP_STATE_DIR, CODEX_LINUX_FEATURES_DIR, and
CODEX_LINUX_LAUNCHER_LOG. Executable hooks also receive
CODEX_LINUX_FEATURE_HOOK_PHASE; afterExit additionally receives
CODEX_LINUX_ELECTRON_EXIT_STATUS. Use this pattern for user-home artifacts
such as Codex skills: stage the source file with resources under
.codex-linux/features/<feature-id>/..., then copy it from
$CODEX_LINUX_FEATURES_DIR/<feature-id>/... to $CODEX_HOME/skills/... in a
runtimeHooks.prelaunch script. Do not write user-home files from stage.sh;
install, package, and updater rebuilds may run outside the real user's session.
Package Hooks
Use packageHooks only when a feature must mutate native package staging:
{
"packageHooks": [
{
"path": "package.sh",
"formats": ["deb", "rpm", "pacman"]
}
]
}
Hooks run with:
PACKAGE_FORMATPACKAGE_NAMEPACKAGE_VERSIONPACKAGE_ROOT/PACKAGE_STAGING_ROOTAPP_DIR/PACKAGE_APP_DIRREPO_DIR
Package hooks should be idempotent and narrowly scoped.
Local Feature Example
Create a private feature without touching tracked files:
mkdir -p linux-features/local/my-feature
$EDITOR linux-features/local/my-feature/feature.json
Then enable it:
cp linux-features/features.example.json linux-features/features.json
$EDITOR linux-features/features.json
make install-native
make setup-native also discovers local features, marks them as [local],
and can enable them by id or list number.
Design Rule
If a change is required for the basic Linux app to launch and behave correctly
for most users, it belongs in core patches under scripts/patches/.
If a change is optional, distro-specific, editor-specific, browser-specific,
workflow-specific, or likely to add future support burden for a minority of
users, put it in linux-features/ and keep it disabled by default.