TUI: /goal 4k-char validation skipped when entire `/goal …` command is pasted as one >1000-char chunk

Resolved 💬 3 comments Opened May 14, 2026 by vishaltandale00 Closed May 14, 2026
💡 Likely answer: A maintainer (etraut-openai, contributor) responded on this thread — see the highlighted reply below.

Summary

When a user pastes a single clipboard string of the form "/goal <large objective>" into an empty composer and the whole paste exceeds LARGE_PASTE_CHAR_THRESHOLD (1000 chars), the TUI never recognizes the input as a /goal slash command. The 4,000-character MAX_THREAD_GOAL_OBJECTIVE_CHARS validation is silently skipped: no error appears, and the literal "/goal …" text is submitted to the model as a regular user message. The model then frequently follows up by calling the create_goal function tool with its own summarized objective, so a goal ends up being created anyway — from the user's perspective the 4k limit looks bypassed.

Observed on codex CLI 0.128.0; reproducible against current main (6a225e400).

Reproduction

  1. Start a TUI session with the goals feature enabled (empty thread).
  2. Copy a single string like /goal followed by ~4,001 x's to the clipboard (anything that pushes the whole paste over 1000 chars and the objective over 4000 chars).
  3. Paste into an empty composer (the textarea now shows [Pasted Content N chars]) and press Enter.

Expected: the same Goal objective is too long: 4,001 characters. Limit: 4,000 characters. Put longer instructions in a file … error that fires when the user types /goal first and then pastes the long objective. That path is already covered by goal_slash_command_rejects_large_paste_using_expanded_length at codex-rs/tui/src/chatwidget/tests/goal_validation.rs:133.

Actual:

  • No validation error is shown.
  • The full "/goal …" text is submitted as a normal user_message.
  • The model often calls the create_goal function tool with a summarized objective, so a goal is created without ever passing through the TUI length guard.

I reproduced this with a one-off test in codex-rs/tui/src/chatwidget/tests/goal_validation.rs that does chat.handle_paste(format!("/goal {}", "x".repeat(MAX_THREAD_GOAL_OBJECTIVE_CHARS + 1))) on an empty composer and submits. SetThreadGoalObjective is never emitted, no Goal objective is too long error is rendered, and a plain Op::UserTurn is sent.

Root cause

In codex-rs/tui/src/bottom_pane/chat_composer.rs:

  • handle_paste (line 949): any paste with char_count > LARGE_PASTE_CHAR_THRESHOLD (1000) is replaced by a single [Pasted Content N chars] placeholder element in the textarea, with the full text stashed in pending_pastes.
  • try_dispatch_bare_slash_command (line 3034) and try_dispatch_slash_command_with_args (line 3070) both inspect self.textarea.text() before expansion and call parse_slash_name, which requires a leading /. When the entire /goal … blob was pasted, the textarea contains only [Pasted Content N chars], so both return None.
  • Execution falls through to prepare_submission_text_with_options (line 2778), which expands the placeholder. At that point the only length check is MAX_USER_INPUT_TEXT_CHARS (~1 MiB), not MAX_THREAD_GOAL_OBJECTIVE_CHARS.
  • handle_submission_with_time then emits InputResult::Submitted with the expanded "/goal …" text. The slash-dispatch path that calls goal_objective_with_pending_pastes_is_allowed (codex-rs/tui/src/chatwidget/goal_validation.rs:17) is never entered.

Existing paste tests miss this case because they pre-fill "/goal " into the composer before pasting, so the textarea already starts with /:

  • goal_slash_command_rejects_large_paste_using_expanded_length (line 133)
  • goal_slash_command_giant_paste_uses_goal_specific_error (line 166)

Note: the protocol-side validate_thread_goal_objective (codex-rs/protocol/src/protocol.rs:3551) does enforce 4k on create_goal arguments, but the model may summarize the objective before calling the tool, so the protocol guard often does not trip for this path. It is also not user-visible feedback at the moment of submission.

Why it matters

  • Silent failure mode: the user pastes a long /goal, sees no error, and may not realize the goal that gets set (if any) was paraphrased by the model rather than created from their literal text.
  • Token waste: the full pasted text is round-tripped through the model even though the TUI guard was meant to short-circuit it.
  • Inconsistent with the existing pasted-after-/goal behavior, which was the whole point of #20746.

Proposed fix direction

Make the slash-command detector consider the expanded text when the composer leads with a paste placeholder. Two reasonable shapes:

  1. In try_dispatch_bare_slash_command / try_dispatch_slash_command_with_args, when the raw textarea starts with a [Pasted Content …] placeholder, parse parse_slash_name against the expansion (or against the first pending-paste content) and, if it resolves to a known builtin, route through the normal slash-dispatch path so goal_objective_with_pending_pastes_is_allowed runs.
  2. Alternatively, in prepare_submission_text_with_options, after expansion, if the resulting text starts with a known slash command, redirect into the slash-dispatch path instead of returning InputResult::Submitted.

Either path should also pick up a regression test mirroring goal_slash_command_rejects_large_paste_using_expanded_length but with the entire "/goal " + objective pasted into an empty composer.

Happy to send a PR if a maintainer agrees with one of the directions.

View original on GitHub ↗

3 Comments

vishaltandale00 · 3 months ago

@etraut-openai tagging you here because of this comment in another thread https://github.com/openai/codex/issues/20598#issuecomment-4367177939

It seems that pasting the /goal command bypasses the limit

etraut-openai contributor · 3 months ago

Yes, this is expected behavior. When you paste text into the composer, it will not be treated as a slash command or go through normal slash command processing. You are instead sending the full pasted text to the model. In this case, the model is interpreting this as a request to create a new goal, and it does to using the create_goal tool. It knows about the 4K limit so it will limit the objective size (typically through summarization) to avoid going over.

vishaltandale00 · 3 months ago

Thanks for explaining.

I noticed this issue because I asked codex to write the goal prompt for before pasting it in.

What is better, pasting the goal in triggering summarization or putting it in an md file?

If it's the latter, it would be nice if codex indicated to put the goal in an md file like it does when the user doesn't paste anything.