Runtime-generated secrets in nested MCP calls are stored unredacted in Codex session transcripts
What version of the Codex App are you using (From “About Codex” dialog)?
26.721.81911
What subscription do you have?
Business
What platform is your computer?
Darwin 25.5.0 arm64 arm
What issue are you seeing?
When functions.exec generates a sensitive value at runtime and passes it directly to a nested MCP tool, Codex persists the fully resolved MCP arguments in the local session transcript.
The value does not need to be:
- Printed with
text(). - Returned to the model.
- Included in the original JavaScript source.
- Written to a repository file.
- Shown in normal tool output.
It is nevertheless retained unredacted under an event resembling:
mcp_tool_call_end.payload.invocation
This was observed while using Supabase MCP’s execute_sql. A temporary PostgreSQL password was generated inside the orchestration isolate, passed to an ALTER ROLE ... PASSWORD statement, and then supplied directly to a local test process. Although it was never emitted to the model, the resolved password appeared inside the retained MCP invocation.
The affected passwords have since been disabled and the disposable database branches deleted.
What steps can reproduce the bug?
Use only a harmless marker—never a real credential:
- Connect an MCP server with a tool accepting a string argument, such as Supabase
execute_sql. - Start a fresh Codex task against a disposable test project.
- Inside one
functions.execcall, generate a unique marker at runtime:
``javascript``
const marker =
"CODEX_SECRET_RETENTION_TEST_" +
Date.now() +
"_" +
Math.random().toString(36).slice(2);
- Pass the marker only to the nested MCP call, for example as a benign SQL literal:
``javascriptselect '${marker}' as harmless_marker`,
await tools.mcp__codex_apps__supabase_execute_sql({
project_id: "<disposable-project-ref>",
query:
});
text("Test completed without emitting the generated marker.");
```
- Do not print or otherwise return
marker. - After the task finishes, inspect the corresponding transcript under:
``text``
$CODEX_HOME/sessions
The default location is:
``text``
~/.codex/sessions
- Search the transcript for
CODEX_SECRET_RETENTION_TEST_. - Observe that the resolved random marker is retained in the nested MCP invocation metadata, even though only the prefix—not the completed marker—existed in the submitted JavaScript.
What is the expected behavior?
Codex should provide a supported sensitive-value boundary for nested tool composition.
Ideally:
- Runtime values explicitly marked sensitive are redacted before session, telemetry, replay, or diagnostic persistence.
- Nested MCP invocations store a redacted placeholder instead of the resolved value.
- Sensitive values can be passed directly between tools or into a child process without becoming model-visible or transcript-visible.
- If complete argument retention is required for replay, Codex should provide a documented opt-out or per-field sensitivity mechanism.
- The documentation should clearly warn that ordinary nested-tool arguments are retained.
For example, a secret wrapper or sensitive parameter annotation could result in:
ALTER ROLE example PASSWORD '<redacted>'
rather than the resolved value.
Additional information
_No response_
3 Comments
Source tracing on current
mainshows agent-turn MCP results pass throughhandle_approved_mcp_tool_callbefore their live events, telemetry preview, post-use hooks, nested code-mode result, model history, and rollout storage. A narrow fix can reusecodex-secretsat that ingress and add synthetic direct-MCP and nested-code-mode regression coverage. It would not rewrite past rollouts or change the separate out-of-band MCP RPC.I have this bounded change validated locally without incident or credential data. If the approach matches the maintainers' intended solution, I can submit the focused PR by invitation.
We hit the same missing sensitive-value boundary in a ChatGPT Sites publication workflow on Windows.
The Sites connector returned a short-lived, repository-scoped write credential, while the supported release sequence required pushing the exact source commit before saving and deploying a version. In the model-facing tool surface available to the task,
exec_commandhad noenv,secret_env, secret-reference, or initial-stdin field.write_stdincontent was still an ordinary tool argument. We therefore could not hand the credential to the Git child while proving that it would stay out of the rollout/session transcript, nested-tool invocation metadata, process command line, terminal output, and temporary Git configuration.We failed closed: no authenticated Git child was started, and no source push, version save, or deployment occurred. No credential value, repository URL, project identifier, domain, path, or commit is included here.
Git already supports the downstream half through
--config-env=http.extraHeader=<ENV_VAR>, specifically to keep transient sensitive configuration out of command-line arguments: https://git-scm.com/docs/git#Documentation/git.txt---config-envltnamegtltenvvargtA first-class fix for this workflow could be either:
SecretRef/secret-field contract that can pass an MCP result into a child process environment or stdin while redacting session, telemetry, replay, diagnostics, and tool metadata; orpush_sourceoperation that consumes the short-lived credential internally and returns only non-sensitive push evidence.This is not solved by Git Credential Manager, askpass, PTY input, or a temporary config file, because those approaches still need the raw MCP result to cross the unsupported boundary first.
We have another fail-closed automation that hits the intersection of this issue and #18578.
Inside one
functions.execisolate, a connector can return private message content that must be converted into a bounded JSON batch and passed to a local Python child process. The workflow cannot use ordinary nested-tool arguments or ordinarywrite_stdin, because resolved values may be retained in session or execution records as described here, while non-TTY children currently have no writable stdin pipe as described in #18578. PTY input, temporary files, FIFOs, argv, environment variables, or cross-call storage do not satisfy the confidentiality boundary.The missing primitive is a host-owned, one-shot sensitive stdin path with semantics equivalent to:
The opaque value would need to be recognized before ordinary argument serialization; remain non-stringifiable, non-inspectable, non-storable, and isolate-local; be written byte-exactly through an anonymous pipe; and be replaced by a fixed placeholder in session, telemetry, replay, diagnostics, crash dumps, and retry records. The call should return no reusable session and should never downgrade to PTY, files, argv, environment variables, or ordinary
write_stdin.A synthetic acceptance test can generate a unique harmless sentinel, pass it through the sensitive path, verify exact UTF-8 bytes and EOF in the child, and then require zero sentinel occurrences outside the controlled child fixture. No real mailbox content, customer data, credential, production identifier, or production session is needed.
Could the maintainers confirm whether this combined sensitive-value-to-child-process requirement belongs in this issue, or whether you prefer a separate feature request linked to both #36079 and #18578?