D99 — Codex's config.toml: comment markers are not enough, orphaned tables are adopted¶
Decision. Before rewriting one of its managed blocks in ~/.codex/config.toml, Cartographer removes from the rest of the file every table that block owns: the [mcp_servers.<name>] tables declared in the block being written (configurator.Apply, CodexMCPTableOwner) and the [[hooks.<event>]] registrations whose command points inside .codex/hooks/<name>/ (registerHookConfigTOML, codexHookTableOwner). The scan (internal/configurator/codextoml.go) stays purely textual, as D58 requires: it recognizes table headers, folds each header's sub-tables into its span, skips anything inside any Cartographer block, and leaves every other byte — comments, ordering, unrelated tables — untouched. Each adoption is reported as a warning on connect/sync.
Rationale. D58's ownership model (comment markers + internal/blocktext) assumes the markers survive. They do not: Codex CLI re-serializes the whole config.toml whenever it persists its own settings (trusted hook hashes, [tui], [projects.*]), emitting the tables in canonical form and dropping every comment. The tables survive, the markers do not, and blocktext.Write — which appends when it finds no markers — then declares [mcp_servers.cartographer] a second time: a duplicate key, so codex refuses to start. For hooks the file stays valid (an array-of-tables may repeat) and the hook simply fires twice, which is quieter and worse. Parsing the file as TOML would fix it and lose exactly what D58 exists to protect, so ownership is instead resolved by identity: a table we would write, outside every block of ours, is a copy of ours.
Consequences. connect/sync self-heal an already-broken machine on the next run: no hand-editing, and nothing on the client's read path parses config.toml, so a duplicated file never blocks the repair. [hooks.state."…"] entries are deliberately not pruned: they are Codex's own bookkeeping (a trusted hash per hook, keyed by position), a stale one is inert because Codex gates it on a hash we do not compute, and deleting them would mean interpreting Codex's internals — the very format-awareness D58 rules out. EnsureBootstrapHook has no warnings channel and drops its repair message; the same repair on the MCP entry is reported, which is what makes the file visibly change. Table identity is the reason hooks are matched by command path and not by header: [[hooks.PreToolUse]] is shared by every hook on that event.