codex: add profile config support

Codex 0.134.0 stopped reading profile settings from [profiles.<name>] in config.toml and no longer supports the top-level profile selector.

Add a dedicated programs.codex.profiles option so Home Manager can write CODEX_HOME/<name>.config.toml files that match the current CLI --profile behavior. For Codex 0.134.0 and later, legacy programs.codex.settings.profiles entries are migrated into profile files with a warning, and removed legacy profile keys are omitted from config.toml so strict config loading keeps working. Older Codex versions keep the legacy config shape.
This commit is contained in:
Austin Horstman
2026-06-18 08:19:47 -05:00
parent 6274683ee4
commit d456f483f1
12 changed files with 211 additions and 3 deletions
@@ -0,0 +1,12 @@
{ config, ... }:
{
time = "2026-06-08T12:00:00+00:00";
condition = config.programs.codex.enable;
message = ''
The `programs.codex.profiles` option was added to manage Codex CLI
profile files under `CODEX_HOME`.
These files are selected with `codex --profile <name>`, matching the
profile behavior used by Codex 0.134.0 and later.
'';
}
+65 -3
View File
@@ -13,8 +13,11 @@ let
yamlFormat = pkgs.formats.yaml { }; yamlFormat = pkgs.formats.yaml { };
jsonFormat = pkgs.formats.json { }; jsonFormat = pkgs.formats.json { };
packageVersion = if cfg.package != null then lib.getVersion cfg.package else "0.94.0"; # A null package has no detectable version, so assume the latest Codex and
isTomlConfig = lib.versionAtLeast packageVersion "0.2.0"; # enable version-gated behavior by default.
atLeast = version: cfg.package == null || lib.versionAtLeast (lib.getVersion cfg.package) version;
isTomlConfig = atLeast "0.2.0";
migrateLegacyProfiles = atLeast "0.134.0";
settingsFormat = if isTomlConfig then tomlFormat else yamlFormat; settingsFormat = if isTomlConfig then tomlFormat else yamlFormat;
in in
{ {
@@ -82,6 +85,32 @@ in
} }
''; '';
}; };
profiles = lib.mkOption {
type = lib.types.attrsOf tomlFormat.type;
default = { };
description = ''
Named Codex configuration profiles written to
{file}`CODEX_HOME/<name>.config.toml`.
These profiles are selected with {command}`codex --profile <name>`.
Codex 0.134.0 and later no longer reads profile settings from
{option}`programs.codex.settings.profiles`, and the top-level
{option}`programs.codex.settings.profile` selector is no longer
supported.
'';
example = lib.literalExpression ''
{
deep-review = {
model = "gpt-5.5";
model_reasoning_effort = "xhigh";
approval_policy = "on-request";
sandbox_mode = "workspace-write";
};
}
'';
};
context = lib.mkOption { context = lib.mkOption {
type = lib.types.either lib.types.lines lib.types.path; type = lib.types.either lib.types.lines lib.types.path;
description = '' description = ''
@@ -336,6 +365,12 @@ in
}; };
category = "Productivity"; category = "Productivity";
}; };
mkProfileEntry =
name: settings:
lib.nameValuePair "${configDir}/${name}.config.toml" {
source = tomlFormat.generate "codex-${name}-config" settings;
};
transformedMcpServers = lib.optionalAttrs (cfg.enableMcpIntegration && config.programs.mcp.enable) ( transformedMcpServers = lib.optionalAttrs (cfg.enableMcpIntegration && config.programs.mcp.enable) (
lib.mapAttrs ( lib.mapAttrs (
name: server: name: server:
@@ -358,6 +393,23 @@ in
) config.programs.mcp.servers ) config.programs.mcp.servers
); );
# TODO: remove this migration block in a future stateVersion once the
# Codex 0.134 profile transition window has passed.
hasLegacyProfileSettings =
migrateLegacyProfiles && ((rawSettings ? profile) || (rawSettings ? profiles));
legacyProfiles = lib.optionalAttrs (
hasLegacyProfileSettings && builtins.isAttrs (rawSettings.profiles or null)
) rawSettings.profiles;
mergedProfiles = legacyProfiles // cfg.profiles;
baseSettings =
if hasLegacyProfileSettings then
lib.removeAttrs rawSettings [
"profile"
"profiles"
]
else
rawSettings;
generatedPluginSettings = generatedPluginSettings =
lib.optionalAttrs (cfg.plugins != [ ] || cfg.marketplaces != { }) { lib.optionalAttrs (cfg.plugins != [ ] || cfg.marketplaces != { }) {
features.plugins = true; features.plugins = true;
@@ -368,14 +420,23 @@ in
// lib.optionalAttrs (cfg.marketplaces != { }) { // lib.optionalAttrs (cfg.marketplaces != { }) {
marketplaces = lib.mapAttrs mkMarketplaceConfigEntry cfg.marketplaces; marketplaces = lib.mapAttrs mkMarketplaceConfigEntry cfg.marketplaces;
}; };
mergedSettingsWithoutMcp = lib.recursiveUpdate rawSettings generatedPluginSettings; mergedSettingsWithoutMcp = lib.recursiveUpdate baseSettings generatedPluginSettings;
settingMcpServers = lib.attrByPath [ "mcp_servers" ] { } mergedSettingsWithoutMcp; settingMcpServers = lib.attrByPath [ "mcp_servers" ] { } mergedSettingsWithoutMcp;
mergedMcpServers = transformedMcpServers // settingMcpServers; mergedMcpServers = transformedMcpServers // settingMcpServers;
mergedSettings = mergedSettings =
mergedSettingsWithoutMcp mergedSettingsWithoutMcp
// lib.optionalAttrs (mergedMcpServers != { }) { mcp_servers = mergedMcpServers; }; // lib.optionalAttrs (mergedMcpServers != { }) { mcp_servers = mergedMcpServers; };
in in
mkIf cfg.enable { mkIf cfg.enable {
warnings = lib.optional hasLegacyProfileSettings ''
`programs.codex.settings.profile` and `programs.codex.settings.profiles`
are no longer supported by Codex 0.134.0 and later. Home Manager
now writes entries from `programs.codex.settings.profiles` to
`CODEX_HOME/<name>.config.toml`. Move them to
`programs.codex.profiles` and remove `programs.codex.settings.profile`.
'';
assertions = [ assertions = [
{ {
assertion = (cfg.plugins == [ ] && cfg.marketplaces == { }) || isTomlConfig; assertion = (cfg.plugins == [ ] && cfg.marketplaces == { }) || isTomlConfig;
@@ -448,6 +509,7 @@ in
text = cfg.context; text = cfg.context;
}; };
} }
// lib.mapAttrs' mkProfileEntry mergedProfiles
// lib.mapAttrs' mkSkillEntry skillSources // lib.mapAttrs' mkSkillEntry skillSources
// lib.listToAttrs (map mkPluginFileEntry cfg.plugins) // lib.listToAttrs (map mkPluginFileEntry cfg.plugins)
// lib.mapAttrs' mkRuleEntry cfg.rules; // lib.mapAttrs' mkRuleEntry cfg.rules;
+3
View File
@@ -7,6 +7,9 @@
codex-mcp-integration = ./mcp-integration.nix; codex-mcp-integration = ./mcp-integration.nix;
codex-mcp-integration-with-override = ./mcp-integration-with-override.nix; codex-mcp-integration-with-override = ./mcp-integration-with-override.nix;
codex-plugins = ./plugins.nix; codex-plugins = ./plugins.nix;
codex-profiles-legacy-old-version = ./profiles-legacy-old-version.nix;
codex-profiles-legacy-warning = ./profiles-legacy-warning.nix;
codex-profiles = ./profiles.nix;
codex-rules = ./rules.nix; codex-rules = ./rules.nix;
codex-skills-inline = ./skills-inline.nix; codex-skills-inline = ./skills-inline.nix;
codex-skills-inline-null-package = ./skills-inline-null-package.nix; codex-skills-inline-null-package = ./skills-inline-null-package.nix;
@@ -0,0 +1,2 @@
approval_policy = "never"
sandbox_mode = "workspace-write"
@@ -0,0 +1 @@
model = "gpt-5"
@@ -0,0 +1,4 @@
approval_policy = "on-request"
model = "gpt-5.5"
model_reasoning_effort = "xhigh"
sandbox_mode = "workspace-write"
@@ -0,0 +1,31 @@
{ pkgs, ... }:
let
codexPackage = pkgs.runCommand "codex-0.133.0" { } ''
mkdir -p $out/bin
echo '#!/bin/sh' > $out/bin/codex
chmod +x $out/bin/codex
'';
in
{
programs.codex = {
enable = true;
package = codexPackage;
settings = {
model = "gpt-5";
profile = "deep-review";
profiles.deep-review = {
approval_policy = "on-request";
sandbox_mode = "workspace-write";
};
};
};
test.asserts.warnings.expected = [ ];
nmt.script = ''
assertFileExists home-files/.codex/config.toml
assertFileContent home-files/.codex/config.toml \
${./profiles-legacy-old-version.toml}
assertPathNotExists home-files/.codex/deep-review.config.toml
'';
}
@@ -0,0 +1,6 @@
model = "gpt-5"
profile = "deep-review"
[profiles.deep-review]
approval_policy = "on-request"
sandbox_mode = "workspace-write"
@@ -0,0 +1,2 @@
approval_policy = "on-request"
sandbox_mode = "workspace-write"
@@ -0,0 +1,41 @@
{ pkgs, ... }:
let
codexPackage = pkgs.runCommand "codex-0.134.0" { } ''
mkdir -p $out/bin
echo '#!/bin/sh' > $out/bin/codex
chmod +x $out/bin/codex
'';
in
{
programs.codex = {
enable = true;
package = codexPackage;
settings = {
model = "gpt-5";
profile = "deep-review";
profiles.deep-review = {
approval_policy = "on-request";
sandbox_mode = "workspace-write";
};
};
};
test.asserts.warnings.expected = [
''
`programs.codex.settings.profile` and `programs.codex.settings.profiles`
are no longer supported by Codex 0.134.0 and later. Home Manager
now writes entries from `programs.codex.settings.profiles` to
`CODEX_HOME/<name>.config.toml`. Move them to
`programs.codex.profiles` and remove `programs.codex.settings.profile`.
''
];
nmt.script = ''
assertFileExists home-files/.codex/config.toml
assertFileContent home-files/.codex/config.toml \
${./profiles-legacy-warning.toml}
assertFileExists home-files/.codex/deep-review.config.toml
assertFileContent home-files/.codex/deep-review.config.toml \
${./profiles-legacy-warning-deep-review.toml}
'';
}
@@ -0,0 +1 @@
model = "gpt-5"
+43
View File
@@ -0,0 +1,43 @@
{ pkgs, ... }:
let
codexPackage = pkgs.runCommand "codex-0.134.0" { } ''
mkdir -p $out/bin
echo '#!/bin/sh' > $out/bin/codex
chmod +x $out/bin/codex
'';
in
{
programs.codex = {
enable = true;
package = codexPackage;
settings = {
model = "gpt-5";
};
profiles = {
deep-review = {
approval_policy = "on-request";
model = "gpt-5.5";
model_reasoning_effort = "xhigh";
sandbox_mode = "workspace-write";
};
ci = {
approval_policy = "never";
sandbox_mode = "workspace-write";
};
};
};
test.asserts.warnings.expected = [ ];
nmt.script = ''
assertFileExists home-files/.codex/config.toml
assertFileContent home-files/.codex/config.toml \
${./profiles-config.toml}
assertFileExists home-files/.codex/deep-review.config.toml
assertFileContent home-files/.codex/deep-review.config.toml \
${./profiles-deep-review.toml}
assertFileExists home-files/.codex/ci.config.toml
assertFileContent home-files/.codex/ci.config.toml \
${./profiles-ci.toml}
'';
}