Summary
[hooks.state.*] entries in codex's config.toml keep getting duplicate keys on Windows, causing codex to fail loading its config with a "duplicate key" / "Cannot declare ... twice" parse error. This recurs every time codex's Rust runtime and the omo-codex TypeScript installer both write hook trust state to the same file using different TOML quoting styles for Windows paths.
Environment
- LazyCodex version: omo-codex 4.12.1 (via oh-my-openagent)
- Codex version: 0.141.0
- OS: Windows 11 (win32)
- Install method:
npx lazycodex-ai install + Orca codex-runtime-home
- Relevant config:
C:\Users\lenovo\AppData\Roaming\orca\codex-runtime-home\home\config.toml
Repository Decision
- Target repository:
code-yeongyu/lazycodex
- Why this belongs there: The defect is in the omo-codex installer's TOML section editor (
findTomlSection()), which is distributed via lazycodex/oh-my-openagent. The installer produces double-quoted [hooks.state.*] keys that collide with codex's own single-quoted keys.
- LazyCodex evidence (runtime + source):
packages/omo-codex/src/install/codex-config-plugins.ts line 28: ensureHookTrusted() builds header as hooks.state.${JSON.stringify(state.key)} — JSON.stringify always produces double-quoted basic strings with escaped backslashes (e.g., "C:\\Users\\lenovo\\...\\hooks.json:session_start:0:0").
packages/omo-codex/src/install/toml-section-editor.ts line 14: findTomlSection() does exact text matching (if (trimmed === headerLine)) — it cannot find a single-quoted section when searching for a double-quoted one (or vice versa).
- When
findTomlSection() returns null, ensureHookTrusted() calls appendBlock() → a new [hooks.state.*] block is appended → duplicate key.
- Upstream Codex source evidence: Codex's Rust runtime uses the
toml_edit crate, which serializes Windows paths as single-quoted literal strings (e.g., 'C:\Users\lenovo\...\hooks.json:session_start:0:0') — no escaping needed, more readable. Both quoting styles resolve to the same TOML key string.
Reproduction
- On Windows, install omo-codex via
npx lazycodex-ai install — this writes [hooks.state."C:\\Users\\...\\hooks.json:session_start:0:0"] (double-quoted) to config.toml.
- Launch codex and approve the Orca hook when prompted — codex's Rust runtime writes
[hooks.state.'C:\Users\...\hooks.json:session_start:0:0'] (single-quoted) to the same config.toml.
- Run
npx lazycodex-ai install again (or the bootstrap setup re-runs on upgrade) — ensureHookTrusted() searches for the double-quoted header, doesn't find it (because the single-quoted version exists), appends a new double-quoted block → duplicate key.
- Next codex launch fails with:
Error loading config.toml: ...:duplicate key or Cannot declare ('hooks', 'state', '...') twice.
Expected Behavior
The omo-codex installer should recognize existing [hooks.state.*] entries regardless of quoting style (single-quoted literal vs double-quoted basic) and update them in place rather than appending duplicate blocks.
Actual Behavior
findTomlSection() does exact string comparison on the header line, so it fails to match entries with different quoting styles. ensureHookTrusted() appends a new block, creating a duplicate key that causes codex to fail loading its config.
Evidence
Backup file timeline (from C:\Users\lenovo\AppData\Roaming\orca\codex-runtime-home\home\):
| Timestamp |
File |
Orca hook quoting |
Duplicate? |
| 2026-06-20 20:03 |
config.toml.bak-lazycodex-20260620-200449 |
Double-quoted only |
No |
| 2026-06-20 20:14 |
config.toml.bak-lazycodex-final-20260620-201452 |
Double-quoted only |
No |
| 2026-06-20 22:11 |
config.toml.bak-lazycodex-demo-20260620-221148 |
Double-quoted only |
No |
| 2026-06-21 09:08 |
config.toml.bak-doctor-fix-20260621-091225 |
Both single + double |
Yes |
| 2026-06-21 13:26 |
config.toml.bak |
Both single + double |
Yes |
The duplicate appeared between June 20 22:11 and June 21 09:08 — after codex ran a session and its Rust runtime added single-quoted entries alongside the installer's double-quoted ones.
Parse error (from tomllib.load() on the duplicated file):
Cannot declare ('hooks', 'state', 'C:\\Users\\lenovo\\AppData\\Roaming\\orca\\codex-runtime-home\\home\\hooks.json:session_start:0:0') twice (at line 353, column 113)
Source code confirming the defect:
packages/omo-codex/src/install/toml-section-editor.ts:
export function findTomlSection(config: string, header: string): TomlSection | null {
const headerLine = `[${header}]`
const lines = config.match(/[^\n]*\n?|$/g) ?? []
let offset = 0
let start = -1
for (const line of lines) {
if (line.length === 0) break
const trimmed = line.trim()
if (start === -1) {
if (trimmed === headerLine) start = offset // EXACT TEXT MATCH — fails on quote-style mismatch
} else if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
return { start, end: offset, text: config.slice(start, offset) }
}
offset += line.length
}
// ...
}
packages/omo-codex/src/install/codex-config-plugins.ts:
export function ensureHookTrusted(config: string, state: TrustedHookState): string {
const header = `hooks.state.${JSON.stringify(state.key)}` // JSON.stringify → double-quoted
const section = findTomlSection(config, header)
if (!section) return appendBlock(config, `[${header}]\ntrusted_hash = ${JSON.stringify(state.trustedHash)}\n`) // appends duplicate
return replaceOrInsertSetting(config, section, "trusted_hash", JSON.stringify(state.trustedHash))
}
Root Cause
findTomlSection() in toml-section-editor.ts compares TOML header lines as exact strings. TOML allows both single-quoted (literal) and double-quoted (basic) strings for table keys — 'C:\Users\...' and "C:\\Users\\..." are semantically identical keys. The function does not normalize quoting before comparing, so when the installer searches for a double-quoted header and the existing entry is single-quoted (written by codex's Rust runtime), it fails to find the section and appends a duplicate block.
Proposed Fix
Normalize TOML quoting in findTomlSection() before comparing header lines. Two approaches:
Option A (minimal, recommended): Parse the quoted key from both the search header and the file line, normalize to the actual string value, and compare values instead of text. This mirrors what a TOML parser does internally:
function normalizeTomlKey(quoted: string): string {
quoted = quoted.trim()
if (quoted[0] === "'" && quoted[quoted.length - 1] === "'") return quoted.slice(1, -1)
if (quoted[0] === '"' && quoted[quoted.length - 1] === '"') {
// unescape basic string: \\ → \, \" → ", etc.
return JSON.parse(quoted)
}
return quoted
}
Then in findTomlSection(), extract the key portion from both headers and compare normalized values.
Option B (broader): Use a real TOML parser (e.g., @iarna/toml) to parse the config into an object, modify the hooks.state map, and re-serialize. This eliminates all text-matching fragility but is a larger change.
Also affected: parseJsonString() in codex-config-toml-sections.ts (if it exists) uses JSON.parse() which rejects single-quoted strings — the cleanup path (isManagedHookStateHeader) would also fail to recognize single-quoted hooks.state entries.
Verification Plan
- Write a unit test that creates a config with single-quoted
[hooks.state.'C:\Users\...\hooks.json:session_start:0:0'], then runs ensureHookTrusted() with the same key — it should update the existing block, not append a duplicate.
- Write a unit test with double-quoted entries and verify the same behavior.
- Run
npx lazycodex-ai install twice on a Windows config that has single-quoted entries — verify no duplicates are created.
- Verify the output config parses cleanly with both Python
tomllib and Rust toml crate.
This issue or PR was generated by LazyCodex.
Tag: lazycodex-generated
Summary
[hooks.state.*]entries in codex'sconfig.tomlkeep getting duplicate keys on Windows, causing codex to fail loading its config with a "duplicate key" / "Cannot declare ... twice" parse error. This recurs every time codex's Rust runtime and the omo-codex TypeScript installer both write hook trust state to the same file using different TOML quoting styles for Windows paths.Environment
npx lazycodex-ai install+ Orca codex-runtime-homeC:\Users\lenovo\AppData\Roaming\orca\codex-runtime-home\home\config.tomlRepository Decision
code-yeongyu/lazycodexfindTomlSection()), which is distributed via lazycodex/oh-my-openagent. The installer produces double-quoted[hooks.state.*]keys that collide with codex's own single-quoted keys.packages/omo-codex/src/install/codex-config-plugins.tsline 28:ensureHookTrusted()builds header ashooks.state.${JSON.stringify(state.key)}—JSON.stringifyalways produces double-quoted basic strings with escaped backslashes (e.g.,"C:\\Users\\lenovo\\...\\hooks.json:session_start:0:0").packages/omo-codex/src/install/toml-section-editor.tsline 14:findTomlSection()does exact text matching (if (trimmed === headerLine)) — it cannot find a single-quoted section when searching for a double-quoted one (or vice versa).findTomlSection()returns null,ensureHookTrusted()callsappendBlock()→ a new[hooks.state.*]block is appended → duplicate key.toml_editcrate, which serializes Windows paths as single-quoted literal strings (e.g.,'C:\Users\lenovo\...\hooks.json:session_start:0:0') — no escaping needed, more readable. Both quoting styles resolve to the same TOML key string.Reproduction
npx lazycodex-ai install— this writes[hooks.state."C:\\Users\\...\\hooks.json:session_start:0:0"](double-quoted) to config.toml.[hooks.state.'C:\Users\...\hooks.json:session_start:0:0'](single-quoted) to the same config.toml.npx lazycodex-ai installagain (or the bootstrap setup re-runs on upgrade) —ensureHookTrusted()searches for the double-quoted header, doesn't find it (because the single-quoted version exists), appends a new double-quoted block → duplicate key.Error loading config.toml: ...:duplicate keyorCannot declare ('hooks', 'state', '...') twice.Expected Behavior
The omo-codex installer should recognize existing
[hooks.state.*]entries regardless of quoting style (single-quoted literal vs double-quoted basic) and update them in place rather than appending duplicate blocks.Actual Behavior
findTomlSection()does exact string comparison on the header line, so it fails to match entries with different quoting styles.ensureHookTrusted()appends a new block, creating a duplicate key that causes codex to fail loading its config.Evidence
Backup file timeline (from
C:\Users\lenovo\AppData\Roaming\orca\codex-runtime-home\home\):config.toml.bak-lazycodex-20260620-200449config.toml.bak-lazycodex-final-20260620-201452config.toml.bak-lazycodex-demo-20260620-221148config.toml.bak-doctor-fix-20260621-091225config.toml.bakThe duplicate appeared between June 20 22:11 and June 21 09:08 — after codex ran a session and its Rust runtime added single-quoted entries alongside the installer's double-quoted ones.
Parse error (from
tomllib.load()on the duplicated file):Source code confirming the defect:
packages/omo-codex/src/install/toml-section-editor.ts:packages/omo-codex/src/install/codex-config-plugins.ts:Root Cause
findTomlSection()intoml-section-editor.tscompares TOML header lines as exact strings. TOML allows both single-quoted (literal) and double-quoted (basic) strings for table keys —'C:\Users\...'and"C:\\Users\\..."are semantically identical keys. The function does not normalize quoting before comparing, so when the installer searches for a double-quoted header and the existing entry is single-quoted (written by codex's Rust runtime), it fails to find the section and appends a duplicate block.Proposed Fix
Normalize TOML quoting in
findTomlSection()before comparing header lines. Two approaches:Option A (minimal, recommended): Parse the quoted key from both the search header and the file line, normalize to the actual string value, and compare values instead of text. This mirrors what a TOML parser does internally:
Then in
findTomlSection(), extract the key portion from both headers and compare normalized values.Option B (broader): Use a real TOML parser (e.g.,
@iarna/toml) to parse the config into an object, modify thehooks.statemap, and re-serialize. This eliminates all text-matching fragility but is a larger change.Also affected:
parseJsonString()incodex-config-toml-sections.ts(if it exists) usesJSON.parse()which rejects single-quoted strings — the cleanup path (isManagedHookStateHeader) would also fail to recognize single-quotedhooks.stateentries.Verification Plan
[hooks.state.'C:\Users\...\hooks.json:session_start:0:0'], then runsensureHookTrusted()with the same key — it should update the existing block, not append a duplicate.npx lazycodex-ai installtwice on a Windows config that has single-quoted entries — verify no duplicates are created.tomlliband Rusttomlcrate.This issue or PR was generated by LazyCodex.
Tag: lazycodex-generated