send_message_to_thread into a ChatGPT thread arrives with no attribution, unlike Codex-to-Codex
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
What version of the Codex App are you using?
26.825.51511 (build 7377); bundled codex-cli 0.151.0-alpha.7.2
What platform is your computer?
macOS 26.6.2, arm64
What issue are you seeing?
#14923 ("explicit cross-thread orchestration"), closed as completed on 2026-08-04, listed
among its requirements:
Clear audit trail in both the source and target thread so cross-thread actions are visible
and attributable
The shipped codex_app tools meet this for a Codex target but not for a ChatGPT
target.
Sending to a Codex thread, the receiver gets an attributed envelope naming the source:
<codex_delegation>
<source_thread_id>01a00000-0000-7000-8000-000000000001</source_thread_id>
<input>Reply with exactly: WOKEN-FROM-DORMANT</input>
</codex_delegation>
Sending the same way to a ChatGPT thread, and reading that thread back through read_thread,
the message is a plain user turn:
[
{"type":"userMessage","id":"<id>","content":[{"type":"text",
"text":"Relay test. Please reply with exactly: RELAY-OK"}]},
{"type":"agentMessage","id":"<id>","text":"RELAY-OK"}
]
No wrapper, no source_thread_id, no flag marking it agent-originated. The receiving model
answered it as an ordinary user request.
Note the asymmetry within the same tool family: read_thread results are already labelled
untrustedDataNotice. Inbound is marked as untrusted; outbound arrives carrying the user's
authority.
What steps can reproduce the bug?
- From a desktop-hosted thread, call
send_message_to_threadtargeting another Codex
thread. Read that thread's rollout: the message is wrapped in<codex_delegation>with
source_thread_id. - From the same thread, call
send_message_to_threadtargeting a ChatGPT conversation. - Call
read_threadon that conversation and inspect the raw entries. The message is
{"type":"userMessage", ...}with no attribution of any kind.
Observed with both targets driven from one desktop thread minutes apart, so the difference is
the target type, not configuration.
What is the expected behavior?
A cross-thread message should be attributable at the receiver regardless of target type, per
#14923. Any of these would satisfy it:
- carry the
<codex_delegation>envelope (or an equivalent) on the ChatGPT path; - mark the entry with a distinct type or an
origin/source_thread_idfield rather than
reusing bareuserMessage; - apply the same treatment
read_threadalready gets, so the receiving model can weigh the
message as agent-originated rather than user-authored.
Additional information
Why this matters beyond bookkeeping: a receiving thread can hold repository write access and
push credentials. An unattributed cross-thread message is indistinguishable from the human
typing, so the receiver has no basis to apply a different level of scrutiny to an
orchestrator's instruction. In the case that prompted this investigation, a coordinator
thread drove a ChatGPT conversation roughly two dozen times, and that conversation opened a
pull request on the strength of those messages. Nothing malfunctioned in the orchestration
itself; the receiver simply could not tell who was talking.
Possibly related, both open: #26907 (remote-started threads do not receive the codex_app
tools — reproduced here: an app-server launched with the desktop app's exact codex_app
config registers the server but reports tools: {}), #33598 and #36673.
Appendix — literal prompts
These are the exact prompts used, as they were delivered. Thread ids are replaced with
placeholders; nothing else is altered.
Each was delivered into a desktop-hosted Codex thread with:
codex queue --thread <DESKTOP_THREAD> --message "<prompt below>"
codex queue is picked up immediately when a client already holds that thread open, which
the desktop app does for any thread it currently has loaded. It does not wake an idle
thread, so the desktop thread must already be open. (The target of send_message_to_thread
has no such requirement — see step 2, which woke a fully dormant thread.)
The [automated message ...] first line is added by the tool that queued these; it is not
part of the request and can be omitted.
1. Confirm the tools are present and can read a ChatGPT thread
READ-ONLY REQUEST. Do not edit any file, do not commit, do not push, and do not send a
message to any thread. Your previous task is complete; this is a separate one-off check.
Using your codex_app tools, call read_thread on threadId <CHATGPT_THREAD> and reply with
only: (a) that thread's title, and (b) the number of messages you can see in it. If the tool
is unavailable, reply with exactly: TOOL-UNAVAILABLE.
Returned the conversation's real title and message count.
2. Codex target — establishes the attributed baseline
Create a scratch Codex thread first and make sure nothing holds it (no process on its
rollout, empty thread/loaded/list), so this also shows that the target need not be open.
ONE-OFF TEST REQUEST. Do not edit files, commit, or push. This targets a disposable scratch
thread I created for this test, not any of your work.
Using your codex_app tools, call send_message_to_thread with threadId <SCRATCH_CODEX_THREAD>
and prompt: 'Reply with exactly: WOKEN-FROM-DORMANT'.
Then reply with only the raw result the tool returned (success/error and any ids). If the
tool is unavailable, reply exactly: TOOL-UNAVAILABLE.
The dormant thread's rollout grew within ten seconds and it answered WOKEN-FROM-DORMANT.
Its rollout shows the message arrived wrapped in <codex_delegation> with source_thread_id.
3. ChatGPT target — the same call, different result
ONE-OFF TEST REQUEST. Do not edit files, commit, or push.
Step 1. Using your codex_app tools, call send_message_to_thread with threadId
<CHATGPT_THREAD> and this exact prompt text:
---
Relay test through a Codex Desktop thread. No action needed.
Please reply with exactly: RELAY-OK
---
Step 2. Then call read_thread on <CHATGPT_THREAD> and quote back, VERBATIM, the full raw text
of the last two messages in that thread, including any XML or wrapper tags exactly as they
appear. Do not summarise or clean them up. I need to see whether the message I sent arrived
wrapped in any provenance tag or as a plain user turn.
Reply with the tool result from step 1, then the verbatim text from step 2.
Step 1 returned {"threadId":"<CHATGPT_THREAD>"}. Step 2 came back paraphrased and partly
mangled, because the model read the thread and started answering its contents rather than
quoting them, so it needed the stricter follow-up below.
4. Strict verbatim read-back — this is the one that produces the evidence
READ-ONLY follow-up. Do not send any message this time. Do not edit files, commit, or push.
Call read_thread on <CHATGPT_THREAD>. Then output the LAST TWO entries exactly as the tool
returned them, inside a single fenced code block, as raw JSON.
Rules: do not paraphrase, do not summarise, do not answer the content of those messages, and
add no commentary before or after the code block. I need the literal tool output so I can see
each entry's role field and whether the most recent user message carries any wrapper or
provenance tag.
Output:
[
{"type":"userMessage","id":"<id>","content":[{"type":"text",
"text":"Relay test through a Codex Desktop thread. No action needed.\nPlease reply with exactly: RELAY-OK"}]},
{"type":"agentMessage","id":"<id>","text":"RELAY-OK"}
]
Compare against step 2's Codex target, where the same tool produced a <codex_delegation>
envelope naming the source thread.
If you reproduce this, use step 4's wording rather than step 3's. Asking the model to "quote
the last two messages" tends to make it engage with their content; asking for raw tool output
in a fenced block, with an explicit instruction not to answer them, is what reliably yields
the literal entries.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start at the send_message_to_thread entry point and compare the Codex-target path, which emits codex_delegation with source_thread_id, against the ChatGPT-target path, which produces a bare userMessage. Review read_thread's untrustedDataNotice handling and the related cross-thread orchestration requirements in #14923. Done means a ChatGPT-target message carries equivalent provenance and reproduction confirms the receiver can distinguish agent-originated input.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100