#!/usr/bin/env bash set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" STAMP="$(date +%Y%m%d-%H%M%S)" BACKUP_DIR="$CODEX_HOME/backups/$STAMP" MANAGED_CONFIG="$REPO_ROOT/templates/global/config-managed.toml" mkdir -p "$CODEX_HOME" "$BACKUP_DIR" "$CODEX_HOME/memories" if [[ -f "$CODEX_HOME/config.toml" ]]; then cp "$CODEX_HOME/config.toml" "$BACKUP_DIR/config.toml.bak" fi if [[ -f "$CODEX_HOME/AGENTS.md" ]]; then cp "$CODEX_HOME/AGENTS.md" "$BACKUP_DIR/AGENTS.md.bak" fi cp "$REPO_ROOT/templates/global/AGENTS.md" "$CODEX_HOME/AGENTS.md" python3 - "$CODEX_HOME/config.toml" "$MANAGED_CONFIG" <<'PY' from pathlib import Path import sys config_path = Path(sys.argv[1]) managed_path = Path(sys.argv[2]) managed_start = "# >>> codex-ops-kit managed block >>>" managed_end = "# <<< codex-ops-kit managed block <<<" # "profile" and the "profiles.*" tables are legacy: Codex 0.144+ refuses to load # a config containing them. They stay in these strip lists (despite no longer # being emitted) so an upgrade from an older kit removes them from a live config. managed_root_keys = { "model", "model_reasoning_effort", "profile", "project_doc_max_bytes", "web_search", } managed_sections = { "history", "profiles.economy", "profiles.balanced", "profiles.deep", } # utf-8-sig drops a leading BOM if one is present. Preserving it risks stranding # the BOM mid-file once content is merged around the block, which is invalid TOML. text = config_path.read_text(encoding="utf-8-sig") if config_path.exists() else "" managed = managed_path.read_text(encoding="utf-8-sig").rstrip() def strip_managed(chunk): """Drop kit-owned keys/tables so only machine-specific settings remain.""" filtered_lines = [] current_section = None for line in chunk.splitlines(): stripped = line.strip() if stripped.startswith("[") and stripped.endswith("]"): current_section = stripped[1:-1].strip() if current_section in managed_sections: continue filtered_lines.append(line) continue if current_section in managed_sections: continue if "=" in line and not line.lstrip().startswith("#"): key = line.split("=", 1)[0].strip() if current_section is None and key in managed_root_keys: continue filtered_lines.append(line) return "\n".join(filtered_lines).strip() def split_root(chunk): """Separate bare root keys from [table] sections, preserving order within each.""" root, tables, seen_table = [], [], False for line in chunk.splitlines(): stripped = line.strip() if stripped.startswith("[") and stripped.endswith("]"): seen_table = True (tables if seen_table else root).append(line) return "\n".join(root).strip(), "\n".join(tables).strip() # Settings may sit on either side of the managed block -- notably `codex mcp add` # appends [mcp_servers.*] to the end of the file. Keep both sides; rewriting only # what precedes the block silently discards everything after it. # # The block's *interior* is filtered too, never dropped. Where an older kit let the # end marker drift below content Codex had written, real [plugins.*]/[projects.*] # sections ended up inside the block; discarding it wholesale would delete them. # Filtering keeps whatever the kit does not own and heals the layout in one run. if managed_start in text and managed_end in text: before, rest = text.split(managed_start, 1) interior, after = rest.split(managed_end, 1) else: before = text interior = "" after = "" preserved = "\n\n".join( part for part in (strip_managed(before), strip_managed(interior), strip_managed(after)) if part ) preserved_root, preserved_tables = split_root(preserved) block = f"{managed_start}\n{managed}\n{managed_end}\n" # Bare root keys must precede every [table] header, or TOML binds them to the table # above them rather than to the document root -- appending the block last quietly # filed model/web_search under whichever table happened to come before it. parts = [ part for part in (preserved_root, block.rstrip(), preserved_tables) if part ] config_path.write_text("\n\n".join(parts) + "\n", encoding="utf-8") PY # Profile files. Codex 0.144+ reads per-profile settings from .config.toml # next to config.toml rather than from [profiles.] tables. These files are # wholly kit-managed, so they are replaced outright (after backup). for profile in economy balanced deep; do profile_src="$REPO_ROOT/templates/global/$profile.config.toml" profile_dest="$CODEX_HOME/$profile.config.toml" if [[ -f "$profile_dest" ]]; then cp "$profile_dest" "$BACKUP_DIR/$profile.config.toml.bak" fi cp "$profile_src" "$profile_dest" done mkdir -p "$CODEX_HOME/memories/portable-kit" cp "$REPO_ROOT"/memory/*.md "$CODEX_HOME/memories/portable-kit/" echo "Installed Codex kit into: $CODEX_HOME" echo "Backup created at: $BACKUP_DIR" echo echo "Next steps:" echo "- Defaults match the balanced profile; plain 'codex' needs no flag." echo "- Use 'codex --profile economy' for lighter work and '--profile deep' for hard tasks." echo "- Run bash scripts/audit_codex_home.sh periodically."