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.

  • Codex Mastery
  • $codex-profiles
  • 160 lines
  • 3 files
  • ~1,500 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- codex-profiles

Installs to ~/.agents/skills/codex-profiles/ for every project on this machine.

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

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.

~/.agents/skills/codex-profiles/SKILL.md

--- name: codex-profiles description: Creates and maintains Codex configuration profiles as separate ~/.codex/NAME.config.toml files selected with --profile, so one machine can switch… license: MIT ---

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 (under CODEX_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>, for codex, codex exec and other commands that take shared options. There is no persistent “default profile” selector: the base config.toml IS the default.
  • Precedence: CLI flags and -c > project .codex/config.toml > profile file > user config.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 in config.toml and the top-level profile = "<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 model in .codex/config.toml wins over --profile; use -m on the command line for that one run.
  • Profile selection keys (profile, profiles) are ignored inside project config files.
  • codex features enable/disable always edits config.toml; the features command does not accept --profile. Put feature toggles for a profile under [features] in its file.
  • sandbox_mode in a profile disables permission profiles for that run, because the two models do not compose.
  • The Codex GitHub Action rejects codex-args that select alternate config profiles in protected runs; configure CI through action inputs or a trusted codex-home instead.
  • 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.yamlgenerated from this page

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.py143 lines, 5.2 KB
#!/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.

esc