Skip to content

fix(cli): escape env values in the codex config.toml writer - #34

Open
stephanbrez wants to merge 1 commit into
Intelligent-Internet:mainfrom
stephanbrez:upstream/toml-env-escaping
Open

fix(cli): escape env values in the codex config.toml writer#34
stephanbrez wants to merge 1 commit into
Intelligent-Internet:mainfrom
stephanbrez:upstream/toml-env-escaping

Conversation

@stephanbrez

Copy link
Copy Markdown

Problem

zenith init --agent codex writes [mcp_servers.zenith.env] into
.codex/config.toml by interpolating each value into a TOML basic string
with a raw f-string (cli.py, no escaping of ", \, or newlines):

env_lines = "\n".join(f'{k} = "{v}"' for k, v in env.items())

-c key="value" is the supported way to splice codex config through
ZENITH_*_ACP_COMMAND, so those values routinely contain double quotes:

$ zenith init --agent codex \
    --validator-acp-command 'codex-acp -c model="gpt-5.6-terra"'

emits invalid TOML — the string terminates at model=, leaving a dangling
gpt-5.6-terra"":

ZENITH_VALIDATOR_ACP_COMMAND = "codex-acp -c model="gpt-5.6-terra""

Codex then fails to parse config.toml or drops the whole managed block, and
the role silently falls back to defaults. Because the line loops over the
entire env dict, the same hole affects every role's ACP command and any
forwarded allowlisted value (ANTHROPIC_MODEL, ZAI_API_KEY, base URLs)
that contains a quote or backslash.

Fix

Serialize with json.dumps. TOML basic strings share JSON's escape syntax,
and json.dumps emits only escapes TOML accepts (\", \\, \n, \t,
\uXXXX; never \/ by default). This mirrors the
args = {json.dumps(server_args)} line directly below it, and fixes every
role at once since the loop spans the whole env dict. Env var names are valid
TOML bare keys, so keys are unchanged.

Only the codex_config branch was affected — the mcp_json branch hands the
same env dict to json.dumps and has always been correct.

Why this surfaced now

Related: #31. Before that change, a custom -c model="..." inside
ZENITH_*_ACP_COMMAND was silently discarded by the npm codex-acp adapter,
so there was little reason to pass a quoted -c value to
--worker-acp-command. #31 makes those overrides functional by routing them
through CODEX_CONFIG, which turns this latent escaping hole into reachable
config corruption.

This PR has no code dependency on #31. It applies cleanly to main as-is,
does not touch acp_runner.py, and can merge before, after, or independently
of that PR. (_augment_acp_command has always appended
-c sandbox_mode="danger-full-access", but that happens at dispatch and never
reaches config.toml; only user-supplied commands are written by
zenith init.)

Testing

New test_codex_init_escapes_quoted_acp_commands drives zenith init --agent codex with a distinct quoted command for worker and validator, asserts
config.toml parses via tomllib, and asserts each value round-trips
byte-identical. It raises TOMLDecodeError without the fix.

Existing codex config tests are unaffected — json.dumps on quote-free values
produces byte-identical output to the current code.

  • uv run pytest -q — 213 passed, 7 skipped
  • uv run ruff check . — passed
  • uv run mypy src — passed
  • Verified end-to-end: before the fix tomllib.loads on the generated
    config.toml raises; after, all commands parse with quotes intact.

🤖 Generated with Claude Code

`_write_bootstrap_config` interpolated every `[mcp_servers.zenith.env]`
value into a TOML basic string with a raw f-string:

    env_lines = "\n".join(f'{k} = "{v}"' for k, v in env.items())

ACP commands splice codex config via `-c key="value"`, so a role's
command can carry double quotes. Interpolated raw, the TOML string
terminates at the first inner quote:

    ZENITH_VALIDATOR_ACP_COMMAND = "codex-acp -c model="gpt-5.6-terra""

which is invalid TOML — codex fails to parse config.toml or drops the
managed block, and the role silently falls back to defaults. The same
hole affects any forwarded allowlisted env value containing a quote or
backslash.

Serialize with json.dumps instead: TOML basic strings share JSON's
escape syntax, and json.dumps emits only escapes TOML accepts. This
mirrors the `args = {json.dumps(server_args)}` line directly below, and
covers every role in one place since the loop spans the whole env dict.
Env var names are valid TOML bare keys, so keys are unchanged.

Only the `codex_config` branch was affected. The `mcp_json` branch hands
the same env dict to json.dumps and has always been correct.

Regression test drives `zenith init --agent codex` with a distinct
quoted command for worker and validator and asserts config.toml parses
and each value round-trips byte-identical; it raises TOMLDecodeError
without the fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
stephanbrez added a commit to stephanbrez/zenith that referenced this pull request Jul 28, 2026
Filed as PR Intelligent-Internet#34 from upstream/toml-env-escaping. Records that the Intelligent-Internet#31
relationship is motivational only, so the row is not misread as
depends-on-unmerged-work like 02aaf73.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant