Summary
fork_session() writes the new transcript with a single os.write() call without a tmp+rename, so a crash between os.open and os.close leaves a partial or empty <forked_session_id>.jsonl in the project directory.
Source
In claude_agent_sdk/_internal/session_mutations.py (verified against 0.1.72 on PyPI):
fork_path = project_dir / f"{forked_session_id}.jsonl"
fd = os.open(fork_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
try:
os.write(fd, ("\n".join(lines) + "\n").encode("utf-8"))
finally:
os.close(fd)
There is no temp file + os.replace() step, so a process kill between os.open and the completed os.write produces a half-written file. O_EXCL guarantees the file did not exist beforehand, but does not provide atomicity for the contents.
Impact
Low severity:
- Original session file is untouched (no data loss).
- The crash window is short —
os.write() of typical transcript sizes (KB to ~100KB) completes in microseconds — but it is not zero.
- The orphan
<uuid>.jsonl will appear in any directory listing and may surface in tooling that enumerates sessions, until manually cleaned.
- Forked-session UUIDs are random v4, so collisions with a later successful fork are not a practical concern, but the orphan file occupies its UUID slot until removed.
Suggested fix
Standard tmp + atomic rename:
fork_path = project_dir / f"{forked_session_id}.jsonl"
tmp_path = fork_path.with_suffix(".jsonl.tmp")
fd = os.open(tmp_path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
try:
os.write(fd, ("\n".join(lines) + "\n").encode("utf-8"))
finally:
os.close(fd)
os.replace(tmp_path, fork_path)
os.replace() is atomic on POSIX and on Windows (when source and destination are on the same volume, which they are here since both paths are inside project_dir).
Reproduction
The behavior is visible from source inspection alone; a deterministic mid-write crash repro would require injecting a fault into os.write. The same code shape that makes it non-atomic also makes a fault-injection repro straightforward in a test (mock os.write to raise mid-call).
Version
claude-agent-sdk 0.1.72 (latest on PyPI as of filing).
Summary
fork_session()writes the new transcript with a singleos.write()call without a tmp+rename, so a crash betweenos.openandos.closeleaves a partial or empty<forked_session_id>.jsonlin the project directory.Source
In
claude_agent_sdk/_internal/session_mutations.py(verified against 0.1.72 on PyPI):There is no temp file +
os.replace()step, so a process kill betweenos.openand the completedos.writeproduces a half-written file.O_EXCLguarantees the file did not exist beforehand, but does not provide atomicity for the contents.Impact
Low severity:
os.write()of typical transcript sizes (KB to ~100KB) completes in microseconds — but it is not zero.<uuid>.jsonlwill appear in any directory listing and may surface in tooling that enumerates sessions, until manually cleaned.Suggested fix
Standard tmp + atomic rename:
os.replace()is atomic on POSIX and on Windows (when source and destination are on the same volume, which they are here since both paths are insideproject_dir).Reproduction
The behavior is visible from source inspection alone; a deterministic mid-write crash repro would require injecting a fault into
os.write. The same code shape that makes it non-atomic also makes a fault-injection repro straightforward in a test (mockos.writeto raise mid-call).Version
claude-agent-sdk 0.1.72 (latest on PyPI as of filing).