fix: repair trapped non-boolean keys under [features] on Codex re-install

Pre-#1346 GSD installs prepended [features] before bare top-level keys
in ~/.codex/config.toml, trapping keys like model="gpt-5.3-codex" under
[features] where Codex expects only booleans. The #1346 fix prevented
NEW corruption but did not repair EXISTING corrupted configs. Re-installing
GSD left the trapped keys in place, causing "invalid type: string, expected
a boolean" on every Codex launch.

repairTrappedFeaturesKeys() now detects non-boolean key-value lines inside
[features] and relocates them before the [features] header during
ensureCodexHooksFeature(), so re-installs heal previously corrupted configs.

Fixes #1379

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-03-25 14:50:02 -04:00
parent 4ef309e0e4
commit f4d8858188
2 changed files with 125 additions and 2 deletions

View File

@@ -2050,6 +2050,84 @@ function mergeCodexConfig(configPath, gsdBlock) {
fs.writeFileSync(configPath, content);
}
/**
* Repair config.toml files corrupted by pre-#1346 GSD installs.
* Non-boolean keys (e.g. model = "gpt-5.3-codex") that ended up under [features]
* are relocated before the [features] header so Codex can parse them correctly.
* Returns the content unchanged if no trapped keys are found.
*/
function repairTrappedFeaturesKeys(content) {
const eol = detectLineEnding(content);
const lineRecords = getTomlLineRecords(content);
const featuresSection = getTomlTableSections(content)
.find((section) => !section.array && section.path === 'features');
if (!featuresSection) {
return content;
}
// Find non-boolean key-value lines inside [features] that don't belong there.
// Boolean keys (codex_hooks, multi_agent, etc.) are legitimate feature flags.
const trappedLines = lineRecords.filter((record) => {
if (record.tableHeader || record.startsInMultilineString) return false;
if (record.tablePath !== 'features') return false;
if (record.start < featuresSection.headerEnd) return false;
if (record.end + record.eol.length > featuresSection.end) return false;
if (!record.keySegments || record.keySegments.length === 0) return false;
// Check if the value is a boolean — if so, it belongs under [features]
const equalsIndex = findTomlAssignmentEquals(record.text);
if (equalsIndex === -1) return false;
const commentStart = findTomlCommentStart(record.text);
const valueText = record.text
.slice(equalsIndex + 1, commentStart === -1 ? record.text.length : commentStart)
.trim();
if (valueText === 'true' || valueText === 'false') return false;
// Skip values that start a multiline string — they may legitimately live
// under [features] and spanning multiple lines makes relocation unsafe.
if (valueText.startsWith("'''") || valueText.startsWith('"""')) return false;
// Non-boolean value — this key is trapped
return true;
});
if (trappedLines.length === 0) {
return content;
}
// Build the relocated text block from trapped lines
const relocatedText = trappedLines.map((r) => r.text).join(eol) + eol;
// Remove trapped lines from their current positions (with their EOLs)
const removalRanges = trappedLines.map((r) => ({
start: r.start,
end: r.end + r.eol.length,
}));
let cleaned = removeContentRanges(content, removalRanges);
// Collapse any runs of 3+ blank lines left behind
cleaned = collapseTomlBlankLines(cleaned);
// Re-locate the [features] header in the cleaned content
const cleanedRecords = getTomlLineRecords(cleaned);
const cleanedFeaturesHeader = cleanedRecords.find(
(r) => r.tableHeader && r.tableHeader.path === 'features' && !r.tableHeader.array
);
if (!cleanedFeaturesHeader) {
return cleaned;
}
// Insert relocated keys before [features]
const before = cleaned.slice(0, cleanedFeaturesHeader.start);
const after = cleaned.slice(cleanedFeaturesHeader.start);
const needsGap = before.length > 0 && !before.endsWith(eol + eol);
const trailingGap = after.length > 0 && !relocatedText.endsWith(eol + eol) ? eol : '';
return before + (needsGap ? eol : '') + relocatedText + trailingGap + after;
}
function ensureCodexHooksFeature(configContent) {
const eol = detectLineEnding(configContent);
const lineRecords = getTomlLineRecords(configContent);
@@ -2071,8 +2149,9 @@ function ensureCodexHooksFeature(configContent) {
);
if (sectionLines.length > 0) {
const rewritten = rewriteTomlKeyLines(configContent, sectionLines, 'codex_hooks');
return {
content: rewriteTomlKeyLines(configContent, sectionLines, 'codex_hooks'),
content: repairTrappedFeaturesKeys(rewritten),
ownership: null,
};
}
@@ -2081,8 +2160,9 @@ function ensureCodexHooksFeature(configContent) {
const needsSeparator = sectionBody.length > 0 && !sectionBody.endsWith('\n') && !sectionBody.endsWith('\r\n');
const insertPrefix = sectionBody.length === 0 && featuresSection.headerEnd === configContent.length ? eol : '';
const insertText = `${insertPrefix}${needsSeparator ? eol : ''}codex_hooks = true${eol}`;
const merged = configContent.slice(0, featuresSection.end) + insertText + configContent.slice(featuresSection.end);
return {
content: configContent.slice(0, featuresSection.end) + insertText + configContent.slice(featuresSection.end),
content: repairTrappedFeaturesKeys(merged),
ownership: 'section',
};
}