openai / openai/codex

send_message_to_thread into a ChatGPT thread arrives with no attribution, unlike Codex-to-Codex

Open
#41,658 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app bug session
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?
  1. From a desktop-hosted thread, call send_message_to_thread targeting another Codex
    thread. Read that thread's rollout: the message is wrapped in <codex_delegation> with
    source_thread_id.
  2. From the same thread, call send_message_to_thread targeting a ChatGPT conversation.
  3. Call read_thread on 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_id field rather than
    reusing bare userMessage;
  • apply the same treatment read_thread already 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.