repeated "file update:" in non-interactive mode

Open 💬 6 comments Opened Nov 11, 2025 by lopuhin
💡 Likely answer: A maintainer (etraut-openai, contributor) responded on this thread — see the highlighted reply below.

What version of Codex is running?

codex-cli 0.57.0

What subscription do you have?

OpenAI API

Which model were you using?

gpt-5-codex

What platform is your computer?

issue happens on both Mac OS and Linux (in Docker)

What issue are you seeing?

When running codex in non-interactive mode in a git repo, the "file update:" outputs the full diff again and again, even for files which are not being changed currently, but which were changed previously.

What steps can reproduce the bug?

Create a git repo with two small files, e.g. like this:

konstantin@MacBookPro codex-file-update-issue % git diff
konstantin@MacBookPro codex-file-update-issue % git log
commit b073d54f4b5c409497fb1bebd64e89aaf760fe7c (HEAD -> main)
Author: Konstantin Lopuhin <kostia.lopuhin@gmail.com>
Date:   Tue Nov 11 16:50:02 2025 +0000

    initial commit
konstantin@MacBookPro codex-file-update-issue % git show
commit b073d54f4b5c409497fb1bebd64e89aaf760fe7c (HEAD -> main)
Author: Konstantin Lopuhin <kostia.lopuhin@gmail.com>
Date:   Tue Nov 11 16:50:02 2025 +0000

    initial commit

diff --git a/a.py b/a.py
new file mode 100644
index 0000000..9332a27
--- /dev/null
+++ b/a.py
@@ -0,0 +1,2 @@
+def foo():
+    pass
diff --git a/b.py b/b.py
new file mode 100644
index 0000000..4371a5a
--- /dev/null
+++ b/b.py
@@ -0,0 +1,2 @@
+def bar():
+    pass

Now run codex on a task which would require edits to two files separately

codex exec --yolo "add a docstring to a.py, then check if we can still import the file, then add a docstring to b.py, and check if we can still import both files"

this results in a lot of repeated outputs of the diff, e.g. see this fragment, where the actual edit was done only to file b.py:

thinking
**Adding docstring with apply_patch**
file update
M /Users/konstantin/dev/codex-file-update-issue/b.py
@@ -1 +1,4 @@
+"""Module b providing bar function."""
+
+
 def bar():
apply_patch(auto_approved=true) exited 0 in 47ms:
Success. Updated the following files:
M b.py
file update:
diff --git a/a.py b/a.py
index 9332a2735b6ab5670bc4dc21c1049e5227ab1e7e..5e2f4434d1be7dcb2092e3a5de45ddc2dd2dab33
--- a/a.py
+++ b/a.py
@@ -1,2 +1,5 @@
+"""Module a providing foo function."""
+
+
 def foo():
     pass
diff --git a/b.py b/b.py
index 4371a5a1f6ed7f53dc5a62ffef65bbb5f4607c38..7d600ffd23fac7e4fb508efa14fcd38342e0e38b
--- a/b.py
+++ b/b.py
@@ -1,2 +1,5 @@
+"""Module b providing bar function."""
+
+
 def bar():
     pass

file update:
diff --git a/a.py b/a.py
index 9332a2735b6ab5670bc4dc21c1049e5227ab1e7e..5e2f4434d1be7dcb2092e3a5de45ddc2dd2dab33
--- a/a.py
+++ b/a.py
@@ -1,2 +1,5 @@
+"""Module a providing foo function."""
+
+
 def foo():
     pass
diff --git a/b.py b/b.py
index 4371a5a1f6ed7f53dc5a62ffef65bbb5f4607c38..7d600ffd23fac7e4fb508efa14fcd38342e0e38b
--- a/b.py
+++ b/b.py
@@ -1,2 +1,5 @@
+"""Module b providing bar function."""
+
+
 def bar():
     pass


thinking
**Verifying module imports**

full log attached: log.txt

In real-life tasks it becomes even more complicated to analyze the logs.

What is the expected behavior?

I expect to see only the diff from the changes done on a particular step, like in interactive mode, e.g. for above quoted example I'd expect to see only

 thinking
**Adding docstring with apply_patch**
file update
M /Users/konstantin/dev/codex-file-update-issue/b.py
@@ -1 +1,4 @@
+"""Module b providing bar function."""
+
+
 def bar():
apply_patch(auto_approved=true) exited 0 in 47ms:
Success. Updated the following files:
M b.py

thinking
**Verifying module imports**

Or at least provide an option to avoid repeated "file update:" output.

Additional information

_No response_

View original on GitHub ↗

6 Comments

etraut-openai contributor · 9 months ago

Thanks for the bug report. What you're seeing here is model behavior. We've shared the feedback with the team members who are responsible for training our codex models. We're tracking this feedback and similar reports so we can make the model more robust in future iterations.

If you see other model behavior that you'd like to report, please use the recently-added /feedback command in the CLI. That will provide us with additional details including logs and conversation text.

lopuhin · 9 months ago

@etraut-openai thanks for looking into this, and you probably know better, but it does not look like model behavior to me, for two reasons:

  • I can replicate the same issue with gpt-5 too -- and can check other non-OpenAI models if this can make you reconsider
  • it does not happen in the interactive mode

So it looks more like an issue with how non-interactive mode is logging what is happening.

etraut-openai contributor · 9 months ago

Reopening for further investigation.

etraut-openai contributor · 4 months ago

@lopuhin, have you seen this behavior with recent versions of the CLI (0.118.0)?

marcindulak · 2 months ago

I see repeated diff with

OpenAI Codex v0.137.0
--------
workdir: /mnt
model: gpt-5.4-mini
provider: openai
approval: never
sandbox: danger-full-access
reasoning effort: none
reasoning summaries: none

This diff gets printed 2-4 times in various places of the output, depending on the approach the agent takes for solving the task.

diff --git a/test.py b/test.py
index ca034ad0e4bff84aeacf8e7350a78c7f2a8de613..4897b9ed845fe9ecba61b14de7bf2a75954c9fb7
--- a/test.py
+++ b/test.py
@@ -1 +1 @@
-print("Claude fails to follow CLAUDE.md instructions)
+print("Claude fails to follow CLAUDE.md instructions")

I replicate this behavior using https://github.com/marcindulak/claude-fails-to-follow-claude-md/tree/cab663a9b0e242cf27f5d370c4010e0443a3ae2a#usage, when I allow the agent to make changes to the git-controlled test.py file by mounting the file read-write (-v ./test.py:/mnt/test.py), instead of read-only (-v ./test.py:/mnt/test.py:ro) .

SDGLBL · 16 days ago

Source-level root cause and a narrow fix proposal

I reproduced this on Codex CLI 0.144.6 on Linux. In one run, a single successful apply_patch created one file, but the exact same unified diff (including identical before/after blob IDs) was appended 16 times while later model/tool cycles completed.

This appears to be deterministic non-interactive renderer behavior rather than model behavior, and the same path is still present on current main:

  1. Every completed sampling response sets should_emit_turn_diff = true, including responses that continue with another tool/model cycle: turn.rs#L2542.
  2. At the end of each such sampling request, Codex reads tracker.get_unified_diff() and emits the complete cumulative turn diff: turn.rs#L2733-L2740.
  3. The human-output codex exec renderer prints every TurnDiffUpdated notification unconditionally when non-empty; it does not compare it with the previous snapshot: event_processor_with_human_output.rs#L334-L338.
  4. The JSONL renderer already ignores this cumulative notification and represents file changes through structured item events: event_processor_with_jsonl_output.rs#L555.

A narrow fix would be to deduplicate only in EventProcessorWithHumanOutput:

  • Keep the last received turn-diff snapshot.
  • On TurnDiffUpdated, update the saved snapshot even when it is empty, but print only a changed, non-empty snapshot.
  • Clear the saved snapshot on TurnStarted.

Updating the cache on empty snapshots preserves the correct A -> empty -> A behavior, and resetting it per turn ensures the same diff can be printed again in a later turn. This leaves the protocol/app-server snapshot semantics and --json output unchanged while preventing log amplification in non-interactive human output.

Suggested regression cases:

  • A, A in one turn prints A once.
  • A, empty, A prints A twice.
  • A, new turn, A prints once per turn.