anthropics / anthropics/claude-agent-sdk-python

fork_session() write is non-atomic; crash mid-write leaves orphan transcript

Open
#906 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
8.1k
Forks
1.3k
Avg merge
2d 31m
Merged PRs (30d)
1

Description

## 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 `.jsonl` in the project directory.

## Source

In `claude_agent_sdk/_internal/session_mutations.py` (verified against 0.1.72 on PyPI):

```python
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 `.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:

```python
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).

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.