Subagents inherit Codex App task controls and can create independent user-owned threads

Open 💬 5 comments Opened Aug 15, 2026 by masterkain

What version of the Codex App are you using (From “About Codex” dialog)?

Codex App <code>26.810.41047</code>, build <code>6570</code>. The associated rollout metadata reports Codex <code>0.148.0-alpha.9</code>

The source checkout used for the analysis exactly matched upstream <code>main</code> at <code>2ca575026cef066a58c70f6bdee4feafa6e63d3a</code>

What subscription do you have?

ChatGPT paid plan. The exact tier is omitted from this public report because the failure is a local capability-boundary issue and does not appear plan-dependent

What platform is your computer?

<code>Darwin 25.5.0 arm64 arm</code>, macOS <code>26.5.1</code> build <code>25F80</code>

What issue are you seeing?

A real multi-agent child, after a third-party OMO/LazyCodex <code>SubagentStop</code> continuation hook incorrectly promoted the completed worker into the root plan orchestrator, used inherited Codex App dynamic tools to create two independent, user-owned Desktop tasks

The user had requested ordinary plan execution through subagents, not separate Desktop tasks. The OMO hook explains why the child attempted further delegation; it does not supply explicit user authorization to create user-owned tasks

The source conversation was a normal depth-1 <code>SessionSource::SubAgent(ThreadSpawn)</code> child with an incoming spawn edge and a parent thread. Its persisted <code>SessionMeta.dynamic_tools</code> nevertheless contained the complete <code>codex_app</code> namespace, including:

  • <code>create_thread</code>
  • <code>fork_thread</code>
  • <code>handoff_thread</code>
  • <code>send_message_to_thread</code>
  • <code>set_thread_archived</code>
  • <code>set_thread_pinned</code>
  • <code>set_thread_title</code>
  • the thread listing, reading, waiting, navigation, automation, and project functions

The tool description for <code>create_thread</code> correctly says: <code>Create a separate task only when the user explicitly asks for a new task.</code> However, that rule is only natural-language guidance. There is no hard runtime capability check preventing a subagent from calling it

The immediate orchestration trigger was OMO 4.19.4's <code>start-work-continuation</code> hook. On the worker's <code>SubagentStop</code>, the hook matched the parent/root session id stored in Boulder state, injected the remaining root plan into the completed child, and instructed it to delegate the next checkbox through native multi-agent tools. This is a separate OMO identity/scope bug

During the incident, the child entered Code Mode <code>exec</code> and searched for agent/collaboration capabilities. Native multi-agent functions were not exposed through that Code Mode catalog, but the inherited <code>codex_app</code> namespace was. The child discovered <code>codex_app__create_thread</code>, called it twice in parallel despite the tool's explicit-user-request rule, and both calls returned new thread IDs

The upstream issue is therefore a hard capability-boundary failure, not a claim that Codex independently invented the desire to delegate. A third-party hook and a model/tool-selection error triggered the calls; Codex still exposed and accepted root task-management operations from a known non-root caller

The resulting state rows were not subagents:

  • <code>source = vscode</code>
  • <code>thread_source = user</code>
  • <code>forked_from_id = null</code>
  • no <code>agent_path</code>, <code>agent_role</code>, or <code>agent_nickname</code>
  • no incoming spawn edge

They appeared as normal independent tasks in the Desktop sidebar, continued under separate lifecycles, used independent permission profiles, and spawned their own descendants. This looked like subagents had escaped their parent, but the metadata shows that the child had actually created new root tasks

One generated task used an <code>on-request</code> restricted profile while the other used <code>never</code> with the sandbox disabled. This also explains why permission requests appeared independently and repeatedly instead of following the original subagent lifecycle

Later cross-task messages displayed <code>Sent by ChatGPT from another task</code>. That marker itself is behaving correctly: the installed App renders it for messages carrying <code>codexDelegation</code> and links back to the source thread. The bug is that a non-root child could create and coordinate user-owned tasks in the first place

Full session IDs and prompts are redacted from this public report. I can provide the local metadata through a private channel if useful; no raw prompts, credentials, command bodies, or user content are required to reproduce the capability problem

What steps can reproduce the bug?

OMO is not required for reproduction. It supplied the concrete trigger in this incident, but the upstream defect exists whenever any prompt, hook, orchestration error, or adversarial input causes a non-root session to attempt one of these user-owned task operations

A source-level and runtime reproduction is:

  1. Start a Codex App project task with multi-agent support and the normal <code>codex_app</code> dynamic tool catalog
  2. Spawn a full-history subagent
  3. Inspect the child session metadata or Code Mode <code>ALL_TOOLS</code>
  4. Observe that the child still has <code>codex_app__create_thread</code> and the other user-owned task-management functions
  5. From the child, invoke <code>codex_app__create_thread</code> with a valid project target
  6. Observe that the returned thread is a new <code>source = vscode</code>, <code>thread_source = user</code> root task with no spawn-edge parent and its own permission/lifecycle state

The relevant source path is deterministic:

  1. A fork loads the parent rollout, including session metadata, into the child history:
  1. When no explicit dynamic tool list is supplied, session startup restores dynamic tools from the inherited conversation history without consulting <code>SessionSource</code>:
  1. Tool planning registers every dynamic tool unconditionally even though it already has the turn context:
  1. Code Mode intentionally makes deferred or hidden dynamic tools discoverable through <code>ALL_TOOLS</code> and callable:

This means <code>deferLoading</code> changes presentation, not authorization

Codex already provides <code>SessionSource::is_non_root_agent()</code>, which could support the missing restriction:

There is also precedent for hard source-aware capability enforcement in the App Server: direct input is explicitly rejected for multi-agent v2 child sources

What is the expected behavior?

A non-root agent should not be able to create, fork, hand off to, mutate, archive, or directly message user-owned Codex App tasks merely because those dynamic tools were persisted in the root conversation and inherited through a history fork

This must be enforced even when a third-party hook incorrectly broadens the child's scope, the model selects the wrong tool, or an adversarial prompt attempts the operation. Those circumstances are exactly why a host-side authorization boundary is needed

Subtasks should remain on the multi-agent control plane. A separate user-owned task should be creatable only from a root or user turn with explicit user intent

I suggest enforcing this in both layers:

  1. Filter or reject user-owned task mutation tools for <code>SessionSource::SubAgent</code> and other internal non-root sessions
  2. Apply the policy to ordinary tool specs, tool search, Code Mode <code>ALL_TOOLS</code>, and direct or forged dynamic calls
  3. Add defense in depth in the App dynamic-tool handler so <code>create_thread</code>, <code>fork_thread</code>, <code>handoff_thread</code>, <code>send_message_to_thread</code>, and <code>set_thread_*</code> reject non-root callers unless a host-created explicit-user-intent capability is present
  4. Keep safe read-only App utilities available separately where intended instead of dropping the whole namespace
  5. Keep legitimate root-to-task delegation and the <code>Sent by ChatGPT from another task</code> marker working

A regression test should start a root App thread with dynamic tools, fork both v1 and v2 children using full-history and last-N inheritance, and assert that forbidden task-management functions are absent or rejected across every exposure path. It should also verify that an explicitly user-authorized root call still works

Additional information

A follow-up transcript audit established the complete causal chain:

  1. The scoped LazyCodex worker completed its assigned task
  2. OMO's <code>SubagentStop</code> continuation hook incorrectly injected the root plan's next checkbox into that child
  3. The hook instructed native subagent delegation, not creation of separate Desktop tasks
  4. The child searched Code Mode <code>ALL_TOOLS</code>, found inherited <code>codex_app</code> task controls, and called <code>create_thread</code>
  5. Codex accepted both calls and created user-owned root tasks with independent permission and lifecycle state

OMO's documented isolated Codex CLI reviewer was not invoked, and its Codex App team-mode transport was not activated. The accidental reviewer was one of the two Desktop tasks created by the child

This narrows responsibility without removing the upstream defect: OMO needs to stop resuming completed children as root orchestrators, while Codex must reject root-only task operations from non-root sessions regardless of what prompted the call

The incident occurred while model traffic was routed through Codex Pooler, so I audited its recently completed Agents v2 compatibility work before attributing the App capability failure to Codex

Codex Pooler only preserves canonical encrypted <code>agent_message</code> handoffs between agent paths. Its public implementation validates the <code>NEW_TASK</code> and <code>MESSAGE</code> envelopes here:

That gateway behavior cannot invoke a local App dynamic tool, insert a <code>source = vscode</code> user thread, create a sidebar conversation, select a local permission profile, or attach <code>codexDelegation</code> metadata. The source and target state records rule out the proxy handoff path, and the missing capability boundary is present in current upstream Codex source independently of Codex Pooler

Both accidentally created tasks eventually emitted explicit <code>task_complete</code>. This is not a report about permanently live threads. It is about unauthorized root-task creation, independent permissions, and a lifecycle/resource multiplier from a non-root agent

I searched open and closed issues and open pull requests for <code>codex_app</code>, <code>create_thread</code>, inherited dynamic tools, user-owned threads, and the delegation marker and did not find an existing report

View original on GitHub ↗

5 Comments

SankeerthNara · 13 days ago

Could you assign this to me

shleder · 11 days ago

This is a useful session-graph/control case for codex-rescue. Rescue does not fix the codex_app authorization boundary; the test is whether it distinguishes actual subagent descendants from the independently created user-owned root tasks and avoids folding or replaying those roots as child recovery state.

If you still have the affected local session graph, could you run:

pipx install codex-rescue
codex-rescue sessions
codex-rescue doctor --latest

A healthy/unknown result can be correct here because both accidental root tasks eventually completed. Please don’t salvage solely to test the capability bug; I’m interested in discovery/classification and whether Rescue stays conservative around the parent/child/root boundaries.

Sanitized output + versions/exit codes only—no raw rollouts/SQLite, prompts, task IDs, credentials, or private paths. Repo: https://github.com/shleder/codex-rescue

UriPomer · 3 days ago

Confirmed: this boundary now caused a second real-world incident on Windows Codex Desktop / rollout CLI 0.149.0-alpha.4.1.

This incident did not require a child to create a new task. A normal depth-1 luna_worker child already had the native parent-result channel, but it could also invoke codex_app__send_message_to_thread.

Sanitized causal chain from the persisted source and target transcripts:

  1. The child attempted to send its result to the pseudo target /root through the App thread API and received Invalid conversation root.
  2. Instead of ending and returning through native multi-agent FINAL_ANSWER, it reused an unrelated durable thread ID available in inherited history.
  3. It called send_message_to_thread five times against that unrelated old task.
  4. The target task was about ten days old and idle. The first delivery automatically woke it and started a turn.
  5. Each delivery was persisted in the target as a user message wrapped in <codex_delegation> and displayed as “Sent by ChatGPT from another task”.
  6. Later deliveries arrived while the target turn was still running, steering/interleaving with unfinished work.

This confirms the capability problem is not limited to create_thread: a non-root child can also mutate and execute an unrelated user-owned task through send_message_to_thread. It also connects the permission boundary here with the stale-thread wake behavior in #32547 and active-turn interruption in #34933.

No raw prompts, business content, paths, or full thread IDs are included. The source metadata identifies the caller as SessionSource::SubAgent(ThreadSpawn), depth 1, role luna_worker.

Expected host-side behavior:

  • Reject send_message_to_thread, create_thread, handoff_thread, and other user-task mutation tools for non-root callers unless backed by an explicit user-intent capability.
  • Never treat a durable historical thread ID as a live child handle.
  • Native child results should return only through the parent multi-agent control plane.
  • If cross-task delivery is intentionally authorized, queue it by default and require explicit interrupt/resume permission before waking or steering a target.
shleder · 22 hours ago

To add an operator-level perspective on containing the luna_worker / send_message_to_thread escalation pattern described by @UriPomer:

Until host-side task authorization is patched upstream, relying solely on agent self-restraint is insufficient because spawned child processes inherit ambient local sockets and storage descriptors.

We addressed this capability leak at the OS perimeter level in Vetto using zero-daemon Landlock/Seatbelt boundaries:

  1. Control Socket Masking: Child subagents and tool processes are blocked from accessing parent control plane sockets ($AGENT/app_server.sock, *.sock, *.ipc) and local SQLite state catalogs (state_*.sqlite).
  2. IPC & Debug Port Boundary: Network requests targeting local inspector/devtools ports (9222, 9229, 5678) and raw socket multiplexers (socat, ncat) trigger immediate fail-closed classification.
  3. Immutable Thread Identity: Historical rollout graphs are preserved read-only so detached subagents cannot wake or steer idle sessions.

For anyone needing immediate protection against subagent privilege escalation while running long-running Codex sessions, wrapping the invocation with vetto --agent codex --profile strict -- codex exec ... enforces these kernel boundaries pre-execution without requiring background daemons.

seasoninlife512-blip · 1 hour ago

Additional reproduction: nested delegation without an active custom hook

I can reproduce the same capability-boundary problem on Windows with Codex App 26.820.7780.0.

Observed setup

  1. A root task delegates an implementation task (B).
  2. B delegates a validation task (C).
  3. C is created through the Codex App create_thread path from a non-root task instead of remaining a native multi-agent child.
  4. C appears as a normal user-visible task with its own lifecycle, while its initial prompt is persisted with a <codex_delegation> wrapper identifying B as the source.
  5. The child can continue independently and can see app-level task-management tools such as create_thread / send_message_to_thread.

In this run, the user-level custom hook configuration had already been reduced to an empty hook set before C was created. The parent conversation itself had started earlier, so stale inherited context cannot be ruled out, but the host still accepted root-task operations from a non-root delegation path.

Expected behavior

A non-root agent should not be able to create, fork, hand off, mutate, or directly message user-owned Codex App tasks. Native subagent results should return only through the direct parent multi-agent channel.

This appears to be the same root-task capability-boundary issue reported here, with an additional Windows/current-build reproduction. No project source, credentials, prompts, or private thread IDs are required to reproduce it.