Summary
When the OpenAI/Anthropic providers fail to build the HTTP request, the resulting ProviderError interpolates the underlying exception, whose message contains the full Authorization / x-api-key header value. The user's API key is printed in cleartext to the terminal (and therefore to CI logs, screen recordings, and pasted bug reports).
Reproduced on the Python runtime, v1.6.0.
Reproduction
Any API key carrying a character that http.client rejects in a header value will trigger it. The realistic path is a .env file with CRLF line endings (the default when the file is created or edited on Windows/WSL), which leaves a trailing \r on the value:
printf 'OPENAI_API_KEY=sk-proj-REDACTED\r\n' > .env
set -a; source .env; set +a
oa run --spec examples/skill-wrapper/spec.yaml --task summarise \
--input '{"text":"hello"}'
Output:
╭─ ✗ Run error ────────────────────────────────────────────────────────────────╮
│ OpenAI request failed: Invalid header value b'Bearer │
│ sk-proj-<THE USER'S ENTIRE REAL API KEY IS PRINTED HERE>\r' │
╰──────────────────────────────────────────────────────────────────────────────╯
Note the trailing \r — that is the whole trigger. The key itself is valid.
Root cause
Two separate defects compound:
1. The key is never sanitised on read.
oas_cli/providers/openai_http.py:54
api_key: str | None = os.environ.get(api_key_env) if api_key_env else None
No .strip(), so a CRLF .env yields a key ending in \r, which is an illegal header value. Same at oas_cli/providers/anthropic_http.py:87.
2. The error path leaks the credential.
oas_cli/providers/openai_http.py:259-260
except Exception as exc:
raise ProviderError(f"OpenAI request failed: {exc}") from exc
The broad except Exception catches http.client's ValueError: Invalid header value b'Bearer sk-...\r' — an exception whose message embeds the header value — and interpolates it straight into a user-facing error.
Same shape in oas_cli/providers/anthropic_http.py:159-160 and :239. Anthropic sends the key via x-api-key, so it leaks identically.
Impact
- The API key is written to stdout in a bright, copy-pasteable error panel. The failure mode ("my key doesn't work") is exactly the one that prompts users to paste their terminal output into an issue or a chat — publishing a live credential.
- CRLF
.env files are common on Windows/WSL, so this is reachable by accident, not just by malformed input.
- The key is valid at the point it leaks; only its framing is wrong.
Suggested fix
Defence in depth — both halves are worth doing:
- Sanitise on read (fixes the trigger):
.strip() the key when pulled from the environment, in both providers. One line each, and it makes the CRLF .env case just work.
- Redact on error (fixes the leak class): never interpolate a raw exception into a provider error without scrubbing the credential — e.g. replace occurrences of the key with
***, or catch the header-construction failure specifically and raise a message that names the problem (API key contains an invalid character — check for trailing whitespace or CRLF line endings in your .env) without echoing the value.
(2) matters independently of (1): any future exception whose message happens to carry the header would leak the same way.
Scope
- Python runtime: affected — both
openai_http.py and anthropic_http.py.
- npm runtime: not affected by this path.
npm/src/providers/openai.ts and anthropic.ts use fetch and only interpolate res.status plus the response body into OAError — the header value never reaches the message. Worth adding the same key-sanitising .trim() there for parity, but there is no leak to fix.
Summary
When the OpenAI/Anthropic providers fail to build the HTTP request, the resulting
ProviderErrorinterpolates the underlying exception, whose message contains the fullAuthorization/x-api-keyheader value. The user's API key is printed in cleartext to the terminal (and therefore to CI logs, screen recordings, and pasted bug reports).Reproduced on the Python runtime, v1.6.0.
Reproduction
Any API key carrying a character that
http.clientrejects in a header value will trigger it. The realistic path is a.envfile with CRLF line endings (the default when the file is created or edited on Windows/WSL), which leaves a trailing\ron the value:Output:
Note the trailing
\r— that is the whole trigger. The key itself is valid.Root cause
Two separate defects compound:
1. The key is never sanitised on read.
oas_cli/providers/openai_http.py:54No
.strip(), so a CRLF.envyields a key ending in\r, which is an illegal header value. Same atoas_cli/providers/anthropic_http.py:87.2. The error path leaks the credential.
oas_cli/providers/openai_http.py:259-260The broad
except Exceptioncatcheshttp.client'sValueError: Invalid header value b'Bearer sk-...\r'— an exception whose message embeds the header value — and interpolates it straight into a user-facing error.Same shape in
oas_cli/providers/anthropic_http.py:159-160and:239. Anthropic sends the key viax-api-key, so it leaks identically.Impact
.envfiles are common on Windows/WSL, so this is reachable by accident, not just by malformed input.Suggested fix
Defence in depth — both halves are worth doing:
.strip()the key when pulled from the environment, in both providers. One line each, and it makes the CRLF.envcase just work.***, or catch the header-construction failure specifically and raise a message that names the problem (API key contains an invalid character — check for trailing whitespace or CRLF line endings in your .env) without echoing the value.(2) matters independently of (1): any future exception whose message happens to carry the header would leak the same way.
Scope
openai_http.pyandanthropic_http.py.npm/src/providers/openai.tsandanthropic.tsusefetchand only interpolateres.statusplus the response body intoOAError— the header value never reaches the message. Worth adding the same key-sanitising.trim()there for parity, but there is no leak to fix.