A thread admits one writer at a time
The app-server tracks who is writing and turns away anyone else. This is a lock rather than a fault: the alternative is two turns appending to one history, which ruins the thing both of them were trying to use.
Codex CLI fix: Codex refuses a thread that already has a writer
thread already has an active writer means another Codex client is attached to this conversation and writing to it, so Codex refused to let a second one in. The full line reads thread <id> already has an active writer, and in the terminal it often appears as already has an active writer (code -32600). Finish or close the other client, or fork the thread and keep working. One conversation, one writer: Codex refuses the second rather than interleaving two streams into a history that would then belong to neither of them.
already has an active writer
Printed by Codex CLI.
Diagnosis
The app-server tracks who is writing and turns away anyone else. This is a lock rather than a fault: the alternative is two turns appending to one history, which ruins the thing both of them were trying to use.
The refusal comes back from the app-server when a client tries to resume or continue a thread another client already holds. codex resume on a session that is still open in another window is the most common way to see it.
A terminal in another window, the Codex IDE extension, Codex Desktop, a codex exec script, or a browser tool driving Codex on the same machine all hold it. The lock belongs to a process, not to a person.
Current Codex runs sessions on a shared local app-server daemon, which codex agents browses. A session there can outlive the terminal window that started it, so closing the window you remember does not always release the thread.
Codex talks to its app-server over JSON-RPC, and -32600 is the standard JSON-RPC Invalid Request code, shared by many unrelated refusals. The words already has an active writer are what identify this one; searching the number alone finds other problems.
The lock releases when the process ends. A killed process is normally noticed at once, and the moment in between is exactly when the refusal looks wrong.
Check
| Run this | What a good answer looks like |
|---|---|
codex agents | Lists the sessions on the shared local app-server daemon, which is where a forgotten writer turns up. |
ps -eo pid,etime,cmd | grep "[c]odex" | Every Codex process with its age. More of them than you expected is the answer, and the oldest is usually the holder. An editor extension shows up here too. |
Every command here has been run on a real machine. Termdeck answers the same questions for every machine you have connected, without opening a terminal on any of them.
Fix
The lock lifts when the process holding it ends its turn or exits. Nothing else releases it. Then codex resume <id>, or codex resume --last, picks the thread up in this window.
codex fork --last forks the most recent session, and codex fork <id> a specific one; with no argument it opens a picker. The fork continues from the same history in a separate thread, so neither writer has to yield.
codex --no-daemon runs without the shared background server even if it is already running. Use it when you want a session that no other client attaches to, not to get around a lock on a thread someone is still writing.
A new session cannot conflict, and it costs nothing when the task matters more than the transcript.
Termdeck applies the same rule from the browser: a Codex chat that another client is writing opens view-only, you watch it live, and the composer unlocks when the other process lets go. When you need it now, Take over closes an idle terminal client and Force take over reaches a busy one. See Chats and sessions and resuming a Codex session.
Not this
The lock is held by a running process rather than by the file. Deleting it loses the history and changes nothing about the refusal.
Every attempt meets the same writer. The refusal stops only when that process finishes its turn or exits, so find it rather than hammering the thread.
FAQ
Another Codex client, such as a second terminal, the IDE extension or Codex Desktop, is attached to the same conversation. Codex lets one client write to a thread at a time and refuses the second.
The same refusal as printed in the terminal. -32600 is the generic JSON-RPC Invalid Request code that the app-server attaches; the words already has an active writer are the actual reason.
Close or finish the other client holding the thread, then resume it. If you cannot find it or do not want to wait, codex fork --last continues the same history in a new thread.
Both would append to one history. Refusing the second is what keeps the transcript coherent enough to resume.
A fork continues from the same history and diverges from that point rather than duplicating a file.
The idea is the same, one writer per conversation. Which process enforces it is what differs.
Claude Code, Codex and Grok in one browser console