Codex Profiles skill for Codex
Switch Codex presets with one flag. Codex designs a small set of presets as standalone profile files that hold only the keys differing from your base config, explains where profiles sit in config precedence, and confirms each one with /status and --strict-config. A bundled script moves legacy [profiles.<name>] tables, ignored since Codex 0.134.0, into the new file layout with comments intact.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- codex-profiles Installs to ~/.agents/skills/codex-profiles/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install codex-profiles The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project codex-profiles Installs to .agents/skills/codex-profiles/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/codex-profiles.zipunzip -o codex-profiles.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) codex-profiles PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $codex-profiles, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Creates and maintains Codex configuration profiles as separate ~/.codex/NAME.config.toml files selected with --profile, so one machine can switch between presets for model and reasoning effort, sandbox and approval policy, network access, MCP servers and CI runs. Includes a script that migrates legacy [profiles.NAME] tables, which Codex 0.134.0+ ignores. Use when a user wants quick versus deep modes, a locked-down profile for untrusted code, a CI profile for codex exec, or when --profile stopped working after an upgrade.
Use it when
- You switch between fast and thorough modes several times a day
- You need a locked-down preset for unfamiliar repositories
- A pipeline should run codex exec with fixed settings
- Legacy [profiles] tables stopped taking effect after an upgrade
Use something else when
- You only need to understand or fix the base config file: use codex-config-toml
- Choosing a model and effort level is the real question: use codex-model-selection
How to check the result
Launch each profile and check /status and /debug-config show the profile layer supplying the expected values; run codex exec --profile ci --strict-config once.
The mistake it prevents
Expecting --profile to override a value that a trusted project's .codex/config.toml also sets; the project layer has higher precedence.
A good profile set is small (three to five presets), each file holds only the keys that differ from the base config, every profile has a name that says when to use it, and each one has been launched once to confirm the values actually take effect.
How profiles work now
- A profile is a standalone file:
~/.codex/<name>.config.toml(underCODEX_HOME). Names may contain letters, digits,-and_. - Keys in the file are top-level, exactly like
config.toml. Do not wrap them in[profiles.<name>]. - Select with
--profile <name>or-p <name>, forcodex,codex execand other commands that take shared options. There is no persistent “default profile” selector: the baseconfig.tomlIS the default. - Precedence: CLI flags and
-c> project.codex/config.toml> profile file > userconfig.toml> managed and system layers. A profile overrides your user defaults but NOT a trusted project’s config or explicit flags. - Since Codex 0.134.0,
[profiles.<name>]tables inconfig.tomland the top-levelprofile = "<name>"key are no longer read.
“Config profiles” (this skill) are different from beta “permission profiles”
(default_permissions and [permissions.<name>]), which only define sandbox
filesystem and network rules. A config profile may select a permission profile.
Workflow
- [ ] 1. Migrate legacy profile tables if present
- [ ] 2. Keep shared defaults in config.toml
- [ ] 3. Write one file per preset with only the differing keys
- [ ] 4. Launch each profile and confirm effective values
- [ ] 5. Document the set (name, purpose, command)
1. Migrate legacy tables
grep -nE '^\s*\[profiles\.|^\s*profile\s*=' "${CODEX_HOME:-$HOME/.codex}/config.toml"
python3 scripts/migrate_profiles.py # dry run: prints each new file
python3 scripts/migrate_profiles.py --write # writes <name>.config.toml, keeps config.toml.bak
The script moves each [profiles.<name>] table (and nested [profiles.<name>.x]
tables, re-headed as [x]) into its own file with comments intact, removes the
top-level profile = ... selector, and refuses to overwrite existing profile files or
handle inline [profiles] definitions; migrate those by hand.
2. Base config holds the defaults
Put everything common in ~/.codex/config.toml (model, everyday effort,
approval_policy = "on-request", sandbox_mode = "workspace-write", MCP servers). A
profile that repeats base values only creates drift.
3. A starter set
# ~/.codex/quick.config.toml - small edits, renames, one-file fixes
model_reasoning_effort = "low"
model_verbosity = "low"
# ~/.codex/deep.config.toml - design work, hard bugs, large refactors
model_reasoning_effort = "high"
plan_mode_reasoning_effort = "xhigh"
# ~/.codex/readonly.config.toml - unfamiliar or untrusted code, audits
sandbox_mode = "read-only"
approval_policy = "on-request"
web_search = "disabled"
[shell_environment_policy]
inherit = "core"
ignore_default_excludes = false
# ~/.codex/deps.config.toml - dependency upgrades that need registries
[sandbox_workspace_write]
network_access = true
[features.network_proxy]
enabled = true
domains = { "registry.npmjs.org" = "allow", "pypi.org" = "allow", "files.pythonhosted.org" = "allow" }
# ~/.codex/ci.config.toml - used as codex exec --profile ci in pipelines
approval_policy = "never"
sandbox_mode = "workspace-write"
model_reasoning_summary = "none"
[history]
persistence = "none"
Effort levels available depend on the model (low, medium, high, xhigh, max,
ultra are the documented names). Check what the current model offers with /model
before writing a level into a profile; levels are not portable across models.
Use a profile, not a -c flag, when the same override is typed more than twice a week.
Use -c for a single run.
4. Confirm each profile
codex --profile deep # then /status: model, effort, approval policy, sandbox
codex --profile readonly # then /debug-config: profile layer listed above the user layer
codex exec --profile ci --strict-config --ephemeral "Reply with OK"
If a profile value does not show up, the usual cause is a trusted project
.codex/config.toml or a flag setting the same key with higher precedence.
5. Document the set
Add a short table to your personal notes or the team’s onboarding doc:
| Profile | Use for | Command |
|---|---|---|
| (none) | everyday work | `codex` |
| quick | one-file fixes | `codex -p quick` |
| deep | design and hard bugs | `codex -p deep` |
| readonly | unfamiliar repos | `codex -p readonly` |
| ci | pipelines | `codex exec -p ci "..."` |
Gotchas
- Project config outranks profiles. A repo that pins
modelin.codex/config.tomlwins over--profile; use-mon the command line for that one run. - Profile selection keys (
profile,profiles) are ignored inside project config files. codex features enable/disablealways editsconfig.toml; thefeaturescommand does not accept--profile. Put feature toggles for a profile under[features]in its file.sandbox_modein a profile disables permission profiles for that run, because the two models do not compose.- The Codex GitHub Action rejects
codex-argsthat select alternate config profiles in protected runs; configure CI through action inputs or a trustedcodex-homeinstead. - Profiles can set
model_catalog_json; the profile’s value wins over the base file. - Do not store secrets in profile files. Reference environment variables
(
bearer_token_env_var,env_key) instead.
Output contract
**Profiles:** quick, deep, readonly, deps, ci (files in ~/.codex/)
**Migrated:** [profiles.deep], [profiles.ci] from config.toml (backup config.toml.bak)
**Verified:** each launched; /status shows expected effort and sandbox; ci passes --strict-config
**Precedence notes:** repo X pins model in .codex/config.toml, profile model ignored there What the zip contains
Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Codex Profiles"
short_description: "Switch Codex presets with one flag"
default_prompt: "$codex-profiles Set up quick, deep and read-only Codex profiles for me" scripts/migrate_profiles.py
#!/usr/bin/env python3
"""Split legacy [profiles.<name>] tables out of a Codex config.toml.
Codex 0.134.0 and later ignore [profiles.<name>] tables and the top-level
`profile = "<name>"` selector. Each profile must live in its own file,
$CODEX_HOME/<name>.config.toml, with top-level keys. This script moves the
text of each legacy table into that file (comments preserved) and removes
the legacy tables and selector from config.toml.
Usage:
python3 scripts/migrate_profiles.py [CONFIG] # dry run, prints the plan
python3 scripts/migrate_profiles.py [CONFIG] --write # apply; keeps CONFIG.bak
CONFIG defaults to $CODEX_HOME/config.toml (CODEX_HOME defaults to ~/.codex).
Exit codes: 0 success or nothing to do, 1 refused (conflict or unsupported form), 2 usage error.
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
import sys
from pathlib import Path
HEADER = re.compile(r"^\s*(\[\[?)\s*([^\]]+?)\s*(\]\]?)\s*(#.*)?$")
NAME_OK = re.compile(r"^[A-Za-z0-9_-]+$")
def split_key(key: str) -> list[str]:
"""Split a dotted TOML key, honouring simple quoted segments."""
parts, buf, quote = [], "", None
for ch in key:
if quote:
if ch == quote:
quote = None
else:
buf += ch
elif ch in "\"'":
quote = ch
elif ch == ".":
parts.append(buf.strip())
buf = ""
else:
buf += ch
parts.append(buf.strip())
return parts
def join_key(parts: list[str]) -> str:
return ".".join(p if re.fullmatch(r"[A-Za-z0-9_-]+", p) else f'"{p}"' for p in parts)
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
home = Path(os.environ.get("CODEX_HOME", Path.home() / ".codex")).expanduser()
ap.add_argument("config", nargs="?", default=str(home / "config.toml"))
ap.add_argument("--write", action="store_true", help="apply the migration")
args = ap.parse_args()
cfg = Path(args.config).expanduser()
if not cfg.is_file():
print(f"error: {cfg} not found", file=sys.stderr)
return 2
lines = cfg.read_text(encoding="utf-8").splitlines(keepends=True)
kept: list[str] = []
profiles: dict[str, list[str]] = {}
current: str | None = None
selector = None
problems = []
seen_header = False # top-level keys only exist before the first table header
for number, line in enumerate(lines, 1):
m = HEADER.match(line)
if m:
seen_header = True
parts = split_key(m.group(2))
if parts[0] == "profiles":
if len(parts) == 1:
problems.append(f"line {number}: bare [profiles] table (inline profile definitions) must be migrated by hand")
current = None
kept.append(line)
continue
name = parts[1]
current = name
body = profiles.setdefault(name, [])
rest = parts[2:]
if rest:
body.append(f"{m.group(1)}{join_key(rest)}{m.group(3)}\n")
elif m.group(1) == "[[":
problems.append(f"line {number}: [[profiles.{name}]] array form is not supported")
continue
current = None
kept.append(line)
continue
if current is not None:
profiles[current].append(line)
continue
if not seen_header and re.match(r"^\s*profile\s*=", line):
selector = line.strip()
continue
if not seen_header and re.match(r"^\s*profiles\s*\.", line):
problems.append(f"line {number}: dotted profiles.* key outside a table must be migrated by hand")
kept.append(line)
if not profiles and not selector:
print(f"nothing to migrate in {cfg}")
return 0 if not problems else 1
for name in profiles:
if not NAME_OK.match(name):
problems.append(f"profile name {name!r} has characters other than letters, digits, '-' and '_'")
target = cfg.parent / f"{name}.config.toml"
if target.exists():
problems.append(f"{target} already exists; merge it by hand")
print(f"config: {cfg}")
for name, body in profiles.items():
print(f"\n--- {cfg.parent / (name + '.config.toml')} ---")
print("".join(body).rstrip() or "(empty profile)")
if selector:
print(f"\nremove selector: {selector} (pass --profile on the command line instead)")
if problems:
print("\nrefusing to write:", file=sys.stderr)
for p in problems:
print(f" - {p}", file=sys.stderr)
return 1
if not args.write:
print("\ndry run only; re-run with --write to apply")
return 0
shutil.copy2(cfg, cfg.with_suffix(cfg.suffix + ".bak"))
for name, body in profiles.items():
text = "".join(body).strip("\n") + "\n"
(cfg.parent / f"{name}.config.toml").write_text(f"# migrated from [profiles.{name}] in {cfg.name}\n" + text, encoding="utf-8")
cfg.write_text("".join(kept), encoding="utf-8")
print(f"\nwrote {len(profiles)} profile file(s); backup at {cfg}.bak")
return 0
if __name__ == "__main__":
sys.exit(main())
About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.