History does not remember file paths that convert to [Image #1] across restarts
Open 💬 4 comments Opened Aug 14, 2026 by nullbio
What version of Codex CLI is running?
codex-cli 0.147.0
What subscription do you have?
Pro
Which model were you using?
GPT-5.6
What platform is your computer?
Linux
What terminal emulator and version are you using (if applicable)?
_No response_
Codex doctor report
What issue are you seeing?
If you restart the CLI, and go up through your history using the up arrow key, the [image #1] links are actually just the text "[image #1]" and dont represent the underlying filepath you pasted.
Furthermore, copy/paste of them also loses the actual filepath.
Also, alt+r (raw mode) does not show the actual filepath.
What steps can reproduce the bug?
Paste an image, restart codex, and get back your previous message using up arrow key.
What is the expected behavior?
It should preserve the underlying filepath for copy/paste, or for repeating messages. It should show the real filepath instead of [image #1] when in raw mode.
Additional information
_No response_
4 Comments
I traced this on
rust-v0.147.0and currentmain(cbe85e1). The behavior comes from two related, but separate, loss points rather than image decoding itself.1. Cross-session Up-arrow history is deliberately text-only
Same-process recall already keeps the complete draft state. The existing
history_navigation_restores_remote_and_local_image_attachmentstest confirms that a local image path and itsTextElementsurvive Up/Down navigation in the same TUI process.The fresh-process path is different:
history_textbefore emittingAppendMessageHistoryEntry(input_submission.rs).codex-message-history::HistoryEntrystores only{ session_id, ts, text }(message-history/src/lib.rs).entry.textback to the TUI (thread_routing.rs).HistoryEntry::new_with_at_mentionsexplicitly rebuilds persistent entries with emptytext_elements,local_image_paths, andremote_image_urls(chat_composer_history.rs).So
[Image #1]survives as literal text, but the ownership span and path needed to turn it back into an attachment do not. This is also consistent with the scope of #9628, which added rich in-process history while explicitly leaving persistent history text-only.Resume/replay is a useful distinction: replayed user inputs can seed a rich
HistoryEntryfrom structured rollout items. The loss described here is the global history used by a new CLI process/thread.2. Raw scrollback has the path, but does not render it
UserHistoryCellalready carrieslocal_image_paths, butraw_lines()renders onlymessageand remote-image labels; it never consumes the local paths (messages.rs). That explains whyAlt+Rand terminal selection still expose only[Image #1]even before a restart.Suggested bounded fix
I would avoid persisting image bytes or remote data URLs. A backward-compatible local-image payload should be enough:
String.text_elementsandlocal_image_pathswhen applying a persistent entry. Missing files can retain their original path and use the existing image-validation/error path on submission.The history-owned descriptor can remain independent of
codex-protocol, avoiding a new dependency from the smallcodex-message-historycrate. The existingSaveAll/Nonesetting and0600history-file permissions would continue to govern whether paths are persisted.Regression coverage
TextElement, and originalPathBufare restored.UserInput::LocalImagewith the original path rather than literal[Image #1]text.UserHistoryCell::raw_lines()substitutes local paths while leaving ordinary literal[Image #N]text untouched.I ran the current focused suites locally: all 12
codex-message-historytests pass, as do the TUI tests for same-session image recall and persistent text reconstruction.If this persistence boundary and raw-mode behavior match the intended privacy/UX direction, would a maintainer be willing to invite a PR? I can implement the bounded local-image version with the regression coverage above.
Confirmed on
main@ 1f41cc5d92 — this is by-construction, per the code's own comment: cross-session history persists only the rendered text (with the literal[image #1]placeholder), and "Persistent history does not store attachment payloads or text-element metadata":https://github.com/openai/codex/blob/1f41cc5d92722748e45cae9cecc6d883a4e7cbb1/codex-rs/tui/src/bottom_pane/chat_composer_history.rs#L55-L70
On restart,
HistoryEntry::new(text)rebuilds entries with emptytext_elements/local_image_paths, so the placeholder is dead text everywhere (recall, copy, alt+r).Notably the same constructor already round-trips mention bindings by encoding them into the persisted text and decoding on load (
decode_history_mentions…). Image paths could ride the identical mechanism: encode[image #1 → /path/to/file.png](or a structured marker) atappend_entrytime, decode on recall to restore the attachment reference. Cheaper interim: persist the real path in place of the bare placeholder so raw mode and copy/paste at least show it.Thanks, this confirms my reading that the behavior is intentional at the persistent-history boundary rather than an image decoding issue. The existing mention-binding encode/decode path is a useful integration point.
My preference would be to use a backward-compatible structured marker or descriptor for local images rather than replace the placeholder with a bare path. That would let us restore both the attachment metadata and its placeholder range, while avoiding collisions with literal
[Image #N]text. Raw scrollback and copy/paste could still project the source path for owned image placeholders.If maintainers are comfortable persisting local file paths under the existing history policy, I can prepare a focused PR covering legacy/new record compatibility, append/lookup round-tripping, composer rehydration, raw scrollback rendering, missing-file behavior, and regression tests.
Would maintainers prefer reusing the existing mention marker format, or introducing a dedicated local-image descriptor?
We need to get some eyes on this from OpenAI or I feel like it'll go unnoticed for months, if not forever. It's one of those bugs that is annoying but most people just ignore because they can't be bothered to open a ticket about it (which was the case for me as well, for the last 6+ months).