fix(env_config): refuse entries that would split into extra .env lines by Lesereingrape · Pull Request #3344 · zhayujie/CowAgent · GitHub
Skip to content

fix(env_config): refuse entries that would split into extra .env lines - #3344

Merged
zhayujie merged 1 commit into
zhayujie:masterfrom
Lesereingrape:fix/env-config-single-line
Oct 1, 2026
Merged

zhayujie merged 1 commit into
zhayujie:masterfrom
Lesereingrape:fix/env-config-single-line

Conversation

@Lesereingrape

Copy link
Copy Markdown
Contributor

Problem

EnvConfig.execute({"action": "set"}) accepts a value containing a line break
and writes it verbatim into ~/.cow/.env, which is a one-entry-per-line file. The
entry that reads back is not the entry that was written:

  • the key is truncated at the line break, so the credential that was just stored
    is no longer the credential (list still reports it as configured, and the next
    restart loads the truncated one through load_dotenv);
  • every remaining line is parsed as its own assignment, so setting one variable
    silently creates additional ones. They show up in list, reach os.environ via
    _reload_env, and reach every bash subprocess through the dotenv_values call
    in agent/tools/bash/bash.py.

Reproducer in #3343. Measured on this tree:

set(key="OPENAI_API_KEY", value="sk-live-secret\nPROVIDER=evil")  -> status: success
OPENAI_API_KEY=sk-live-secret
PROVIDER=evil

The same asymmetry covers a key containing the field separator:
set(key="A=INJECTED", value="1") writes A=INJECTED=1, which reads back as
{"A": "INJECTED=1"}. On the platform measured that call even reports failure
(ValueError: illegal environment variable name from os.environ[key] = value),
because _write_env_file has already rewritten the file by the time it raises.

Fix

Guard the two shapes the format cannot hold, at the one place a caller-supplied
key or value enters, and return the reason instead of writing broken state. The
check is on line breaks (and = in a key) rather than a name whitelist, so
existing single-line keys and values are untouched — including = inside a value
(base64 padding) and # inside a value.

Quoting on write was the alternative, and it is the larger change: it would also
have to replace _read_env_file's own parser, since that one does not understand
quotes, and it would newly depend on exactly how python-dotenv unescapes a value.
Refusing what cannot be represented keeps both readers agreeing.

Tests

tests/test_env_config_single_line_entry.py — 5 cases: the multi-line value is
refused and the file is left as it was; neither reader sees the injected variable;
an unrelated key already stored is not disturbed; a key containing = is refused;
and a control that ordinary values (with =, #, trailing space) still round-trip
byte-for-byte, so the guard cannot over-refuse.

pristine agent/tools/env_config/env_config.py: 4 failed, 1 passed
with the fix:                                  5 passed

Whole suite, Python 3.11 on this branch:

failed passed skipped
08f64f70 (base) 32 2332 39
this branch 32 2337 39

grep '^FAILED' | sort on both runs diffs empty — no test that passed before
fails now, and the 5 additions are exactly the new file. The 32 leftovers are
pre-existing on this machine (no symlink privilege, WinError 1314; missing
tzdata; rg not installed) and unrelated to these files.

ruff check agent/tools/env_config/env_config.py tests/test_env_config_single_line_entry.py --select F
→ All checks passed!

closes #3343

@zhayujie
zhayujie merged commit ea5bb81 into zhayujie:master Oct 1, 2026
3 checks passed
@zhayujie

zhayujie commented Oct 1, 2026

Copy link
Copy Markdown
Owner

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.

env_config writes a multi-line value as several .env entries, so the key is truncated and extra variables appear

2 participants