linux-features/ is the only extension boundary for optional integrations.
Repository features live in linux-features/<id>/; private local features live
in the gitignored linux-features/local/<id>/. Every feature requires adjacent
feature.json and README.md files.
The manifest id must equal the declaring feature directory's final name:
<id> in both repository and local/<id> paths, including alternate feature
roots. Discovery checks the declaration path, not a symlink target's name, and
reports mismatches without renaming directories or migrating private features.
Features are always disabled by default. Enable them only in the gitignored configuration:
{
"enabled": ["read-aloud"],
"settings": {
"read-aloud": { "example": "value" }
}
}make setup-native is the interactive editor for this configuration;
make install-native is the separate build/package/install step. A feature
remains enabled for updater rebuilds because its validated snapshot is included
in the custom package's minimal update-builder.
Known retired IDs and their aliases are discarded during config loading. Other
unknown enabled IDs, default-enabled manifests, unmet requirements, and
conflicts are errors. Strict configuration reads (strictConfig: true),
including native package plans, also reject duplicate enabled IDs and malformed
settings. Non-strict reads warn and ignore malformed settings entries (or the
whole malformed settings container) and deduplicate enabled IDs.
The entire settings field may be omitted or null. Otherwise it must be an
object keyed by syntactically valid feature IDs, with non-null, non-array
objects as values. Nested business fields remain feature-owned. Retired IDs
are filtered before validating their values. Settings for disabled features
are retained; the package/update-builder snapshot exports only enabled feature
settings.
{
"id": "my-feature",
"title": "My Feature",
"description": "Optional Linux integration.",
"defaultEnabled": false,
"entrypoints": {
"patchDescriptors": "./patch.js",
"stageHook": "./stage.sh"
},
"resources": [],
"runtimeHooks": {},
"packageResources": [],
"packageDependencies": {},
"packageHooks": [],
"requires": [],
"conflicts": []
}Use stageHook only when the operation cannot be represented declaratively.
Feature patching supports only entrypoints.patchDescriptors; removed legacy
entrypoint aliases are rejected. entrypoints may be omitted, null, or an
empty object. Otherwise it must be an object containing only patchDescriptors,
stageHook, and cleanupHook; unknown keys fail during manifest discovery in
both the build loader and setup picker, without executing modules or hooks.
Entrypoint paths remain relative to and inside the owning feature directory.
Manifest fields:
| Field | Purpose |
|---|---|
id |
Stable configuration ID matching the directory name |
title, description |
User-facing wizard and documentation text |
defaultEnabled |
Omitted means false; an explicit value must be boolean false, including local and internal features |
internal |
Optional boolean for build-owned plumbing hidden from public feature selection; omission means false, and explicit null or non-boolean values are rejected |
entrypoints.patchDescriptors |
Optional ASAR descriptor module |
entrypoints.stageHook |
Last-resort app staging script |
entrypoints.cleanupHook |
Cleanup script discovered for disabled features |
resources |
Declarative files copied into the app tree |
runtimeHooks |
Launcher environment and lifecycle extensions |
packageResources |
Declarative files outside the app tree in native packages |
packageDependencies |
deb/RPM/pacman/ebuild runtime dependency mapping |
packageHooks |
Narrow native-package staging operations |
gentoo |
Audited Gentoo support and phase-specific dependency mapping |
requires |
Other feature IDs that must be enabled |
conflicts |
Feature IDs that cannot be enabled together |
The table describes framework fields, not a whitelist of top-level keys. Manifest normalization preserves feature-owned top-level extensions for both repository and private features. Their consumers own semantic validation when using them; retaining a field does not mean its value has been validated. Existing examples include:
| Extension field | Consumer and handling |
|---|---|
presets |
model-picker-default-presets/patch.js validates model/effort preset entries |
petOverlay |
pet-overlay/patch.js normalizes overlay defaults and user overrides |
catalogSubfeatures |
community-features-in-gui-settings/catalog.js validates subfeature metadata |
officialBundleAuditScenarios |
scripts/ci/verify-official-linux-features.js validates audit scenario names and settings containers |
Framework-defined structures have their own constrained keys: entrypoints
rejects unknown entrypoint types during discovery, runtimeHooks rejects
unsupported hook names when planning app resources, and packageDependencies
rejects unsupported package formats when planning dependencies. A top-level
extension does not register an executable entrypoint or make a misspelled key
inside entrypoints valid. Unsafe paths fail their corresponding resource or
entrypoint checks.
A manifest title or description does not replace the adjacent README; document setup, settings, side effects, cleanup, supported sessions/architectures, and tests there.
- The installer validates enabled manifests and relationships.
- If required core or enabled feature ASAR descriptors exist, a temporary ASAR copy is extracted and patched. The patch report explains both sets of outcomes; only reported changes trigger deterministic repacking. With no descriptors in either set, ASAR is never extracted and stays byte-identical to the verified official input. Empty feature selection alone is insufficient.
- Declarative app resources and launcher hooks are staged.
- Remaining legacy stage hooks run.
- Native package resources/dependencies/hooks are applied to package staging.
- The launcher loads env, prelaunch, Electron-argument, launcher, cold-start, and after-exit hooks.
The enabled feature snapshot is recorded in build metadata and must match at package time. The update-builder includes only enabled descriptors/resources and repeats the same validation. Drift in an enabled feature rejects the candidate; disabled features are not probed.
User-private modules can be placed under the gitignored
linux-features/local/<id>/ directory:
linux-features/local/my-feature/
├── feature.json
├── README.md
├── patch.js
└── test.js
They use the same validation and disabled-by-default contract as repository
features. Keep source and resources inside the feature directory, use a unique
ID, and do not rely on generated codex-app/ paths. Local features are included
in the installed package/update-builder only when enabled.
Descriptor modules export an array or { descriptors: [] }. IDs are reported
as feature:<feature-id>:<descriptor-id>. Supported phases are
main-bundle, extracted-app:pre-webview, webview-asset, and
extracted-app:post-webview. Descriptors must be idempotent. Execution policy
and candidate acceptance are separate: ciPolicy defaults to optional, so
ordinary execution errors may be recorded as warnings or optional skips;
enforceWhenEnabled defaults to true, so those outcomes still reject a
candidate when the owning feature is explicitly enabled. The standard build
runs patch-linux-window-ui.js --enforce-critical, which checks both
criticalFailuresFromReport and enabledFeatureFailuresFromReport.
| Report status | Default acceptance for an enabled feature |
|---|---|
applied, already-applied |
Successful |
applied-with-warnings, skipped-optional |
Rejected; execution continued but the enabled surface was not validated |
skipped-target, skipped-disabled |
Not applicable to this target or descriptor enable gate; not an acceptance failure |
failed-required, failed-integrity |
Rejected by the independent critical-failure check |
A descriptor may explicitly set enforceWhenEnabled: false only with
ciPolicy: "optional". This exempts that descriptor from enabled-feature
acceptance, but cannot bypass required core or integrity failures. Use this
only for intentionally best-effort behavior, never to conceal enabled-feature
drift. Required core policy remains independent of feature selection.
An enabled patch entrypoint with an invalid export or descriptor aborts loading
before patch execution: every descriptor must be an object with a nonempty
string id (or name) and an apply function. Diagnostics identify the
feature, module path, and offending item. These input errors cannot become an
empty successful patch report. Empty descriptor arrays and resource-only
features are valid; disabled feature modules are not loaded. Execution warnings
from valid descriptors are reported separately and remain subject to candidate
acceptance checks.
Features may share ordinary patch libraries in scripts/patches/lib/. For
example, global-dictation and wayland-global-shortcuts each install an
idempotent portal backend from that directory and select their own command IDs.
Either feature stages the same prebuilt helper through its staging hook. The
update-builder carries the shared patch libraries and staging utility together
with the selected feature directories.
The core registry contains only required compatibility patches. Features must be self-contained and must not compose with deleted core IDs. A generic core extension point may be added only when unavoidable and must remain feature-agnostic.
{
"resources": [{
"source": "assets/tool.json",
"target": ".codex-linux/features/my-feature/tool.json",
"mode": "0644"
}]
}Sources stay inside the feature. Targets stay inside the app and cannot be the app root. Modes are quoted octal strings. Staged files are tracked so disabling a feature removes framework-owned files on the next rebuild.
{
"runtimeHooks": {
"env": "env",
"prelaunch": "prelaunch.sh",
"electronArgs": "electron-args",
"launcher": "launcher.sh",
"coldStart": "cold-start.sh",
"afterExit": "after-exit.sh"
}
}env: sourced as environment assignments.prelaunch: synchronous executable before runtime start.electronArgs: one argument per non-comment line.launcher: may emitenv KEY=VALUEorelectron-arg VALUE.coldStart: background hook at launch.afterExit: requires the wrapper to wait, then runs after process exit.
Launcher hooks receive the Electron arguments already loaded from user and
feature configuration followed by the original launcher arguments. Other
executable hooks receive the original arguments and use
CODEX_LINUX_APP_DIR, CODEX_LINUX_APP_STATE_DIR, and
CODEX_LINUX_APP_CACHE_DIR for lifecycle paths. Feature-owned runtime resources
are available under CODEX_LINUX_FEATURES_DIR, and
CODEX_LINUX_LAUNCHER_LOG names the canonical launcher log path. After-exit
hooks receive the process status in CODEX_LINUX_ELECTRON_EXIT_STATUS. Keep
hooks bounded; the compact launcher does not supervise helper processes or
provide a second application lifecycle. The launcher exports lifecycle paths
but does not create their directories; hooks must create a directory before
writing into it.
packageResources place feature-owned files outside the app directory;
packageDependencies map runtime dependencies for deb/RPM/pacman/ebuild; package hooks
perform the remaining narrowly scoped staging work. Targets must stay inside
the package root and cannot overlap the packaged app tree. Special permission
bits are rejected.
Gentoo support is opt-in per feature and separate from the disabled-by-default
user configuration. Do not mark a feature supported without its adjacent tests,
an official-bundle build, and acceptance for the environments it claims.
ui-tweaks and remote-mobile-control currently declare Gentoo support; other
repository features require their own audit and declaration. This framework adds
no systemd-specific integration. Native validation uses amd64 OpenRC; Gentoo
ARM64 and systemd runtime environments have not been tested.
{
"gentoo": {
"supported": true,
"dependencies": {
"bootstrap": ["dev-lang/rust"],
"BDEPEND": [],
"DEPEND": [],
"RDEPEND": [">=net-libs/nodejs-22.12.0[npm]"],
"IDEPEND": []
}
},
"packageResources": [{
"source": "assets/70-example.rules",
"target": "usr/lib/udev/rules.d/70-example.rules",
"mode": "0644",
"formats": ["ebuild"]
}],
"packageHooks": [{"source": "package.sh", "formats": ["ebuild"]}]
}| Phase | Consumer |
|---|---|
bootstrap |
make bootstrap-native, before regular-user helper/app/package builds; omitted from ebuild metadata |
BDEPEND |
Portage build-host tools needed for the local payload's src_* phases |
DEPEND |
Portage target build libraries/headers for src_* phases |
RDEPEND |
Installed application/feature runtime dependencies |
IDEPEND |
Portage merge/unmerge tools, not persistent runtime dependencies |
Helpers are compiled outside Portage. Put their build tools in bootstrap, not
only in BDEPEND; the binary-payload ebuild does not rebuild helpers. Do not
invent an install dependency for a build-time staging hook. Extra Portage
phases are available for audited packaging needs, not an implicit compile or
root hook extension.
Each phase is an array of individual EAPI 8 atoms. Supported constraints include
version comparisons, = version prefixes, slot/subslot and slot operators, and
fixed positive/negative USE requirements with defaults. Bootstrap atoms do not
accept slot rebuild operators. Dependency expression groups, blockers,
repository selectors, conditional USE, whitespace and shell expansion are
not accepted. Unknown phases and malformed atoms fail closed; the Gentoo host
also checks atoms with Portage's parser. Lists are enabled-only, sorted and
deduplicated within each phase. The array shorthand gentoo.dependencies: [...]
and packageDependencies.ebuild: [...] both contribute to RDEPEND only.
The existing package resource/hook engine handles ebuild alongside other
native formats. Gentoo external resources must be below usr/ or etc/;
app-internal files belong in resources. Hook scripts run under the builder's
user with PACKAGE_FORMAT=ebuild, PACKAGE_VERSION, PACKAGE_ROOT,
PACKAGE_STAGING_ROOT, PACKAGE_NAME, APP_DIR and PACKAGE_APP_DIR. They run
after declarative resources and are never installed as Portage lifecycle code.
Files in the app snapshot must match the enabled resources/hooks and declared
modes; changed selections, stale ownership and failed enabled patches require
a rebuild. Disabling a feature removes its external resources from the next
payload, leaving Portage to unmerge the obsolete owned files.
Native Rust helpers are built once as project release components. They must not be rebuilt merely because a new official application package appeared. Delete an orphan helper crate when its last feature consumer is removed.
A retained feature should have:
- manifest validation tests;
- idempotent descriptor/resource staging tests;
- byte-identical failure tests for missing or ambiguous semantic anchors;
- a build with that feature enabled alone against the current official ASAR;
- runtime acceptance for the Linux sessions, compositors, services, or devices it claims to support.
Run the framework and all adjacent Node tests with:
node --test scripts/lib/linux-features.test.js linux-features/*/test.jsAn applied-with-warnings or optional skip is not evidence that a feature
works. Enabled descriptors must apply cleanly before the candidate is accepted,
unless they carry the explicit optional exemption described above; target and
descriptor-gate skips retain their not-applicable semantics.
Remove a feature only when the official Linux runtime demonstrably replaces its behavior or the project intentionally drops the product surface. Delete its descriptors, helpers, tests, package/Nix/watchdog references, and documentation together. Add the exact old ID to the retired registry so existing local configs migrate silently; do not make arbitrary unknown IDs valid.
The official Linux application is the baseline. A default core patch is allowed only for a reproduced mandatory launch/work failure with a regression test. Everything optional, distro/editor/browser/workflow-specific, experimental, or minority-use belongs here and stays disabled by default.