Proof Portal
Omarchy
ProbeLabs65 findings · 95 requirementsA proof layer — requirements, tests and verified fixes — for two of Omarchy's subsystems: the application menu (launcher scripts, QML model, JSONC config, search and selection) and the lock screen (lock scripts, QML, PAM authentication). Scope is deliberately limited to those components of omacom/omarchy; the rest of the distribution is not covered.
omarchy-agent-usage-claude: race condition in scan_opencode_usage's write_json crashes during omarchy update
Reported by guirossibrum · filed Aug 9, 2026Closed upstream on 2026-08-09 by the merge of PR #6654 (closed by dhh). GitHub close reason: completed.
At a glance
- 1 · What is claimed?
- Only the report text below — no validator has restated it as checkable claims yet.
- 2 · What does it affect?
- Not yet mapped to any requirement or component.
- 3 · Is it reproducible?
- No validation has been run yet.
- 4 · What remains unclear?
- Not validated yet: no claims, no reproducer.
- 5 · What happens next?
- Our staff decide whether the report is valid. Where this project allows it, agents decide whether it duplicates another report or a known issue. No decision has been recorded yet.
Original report
Note: this report was drafted with AI assistance (traceback analysis and root-cause diagnosis by Claude), reviewed by me before filing.
Summary
omarchy-agent-usage-claude can crash with FileNotFoundError during omarchy update when two instances of the collector run concurrently, because scan_opencode_usage() writes its cache file without the file locking used elsewhere in the same script.
Version
4.0.0.alpha (4.0.0.r1584.gb925431-1)
Steps to reproduce / when it happens
Hit during omarchy update, while migration 1786099804 ("Rename the model usage widget to agents and prime its data files") ran omarchy-agent-usage-update. That script launches one collector per agent in the background (collect "$collector" "$agent" & in omarchy-agent-usage-update), and it looks like this manual priming run overlapped with another concurrent invocation of the Claude collector (e.g. a periodic background refresh).
Traceback
Running migration (1786099804)
Rename the model usage widget to agents and prime its data files
Traceback (most recent call last):
File "/usr/share/omarchy/bin/omarchy-agent-usage-claude", line 785, in <module>
raise SystemExit(main())
~~~~^^
File "/usr/share/omarchy/bin/omarchy-agent-usage-claude", line 758, in main
opencode = scan_opencode_usage(scan_age)
File "/usr/share/omarchy/bin/omarchy-agent-usage-claude", line 528, in scan_opencode_usage
write_json(cache_file, {"stats": stats})
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/share/omarchy/bin/omarchy-agent-usage-claude", line 235, in write_json
tmp.replace(path)
~~~~~~~~~~~^^^^^^
File "/usr/lib/python3.14/pathlib/__init__.py", line 1091, in replace
os.replace(self, target)
~~~~~~~~~~^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: '/home/grbrum/.cache/omarchy/agent-usage/claude-opencode-3756ad4dd55fc8fe.json.tmp' -> '/home/grbrum/.cache/omarchy/agent-usage/claude-opencode-3756ad4dd55fc8fe.json'
omarchy-agent-usage-update: claude collector failed - check this error during omarchy updateRoot cause
In omarchy-agent-usage-claude, write_json() uses a fixed, non-unique temp filename:
def write_json(path: Path, payload: dict[str, Any]) -> None:
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(payload, separators=(",", ":"), sort_keys=True) + "\n", encoding="utf-8")
tmp.replace(path)scan_opencode_usage() calls this directly with no locking:
cache_file = cache_root() / f"claude-opencode-{hashlib.sha1(str(db).encode('utf-8')).hexdigest()[:16]}.json"
...
write_json(cache_file, {"stats": stats})If two instances of the collector run at the same time (both compute the same deterministic cache_file/.tmp path), both write to the same .tmp file. Whichever process calls tmp.replace(path) second finds the file already moved away by the first, raising FileNotFoundError.
Note that cached_scan(), a sibling function in the same file, already guards against exactly this with an fcntl.flock lock file:
def cached_scan(projects_path: Path, max_age_seconds: float) -> dict[str, Any]:
cache_file, lock_file = scan_cache_paths(projects_path)
...
with lock_file.open("w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
...
write_json(cache_file, summary)scan_opencode_usage() (and possibly other write_json() call sites in this file) should use the same lock-file pattern, or at minimum write_json() should use a unique per-process temp filename (e.g. via tempfile.mkstemp in the same directory) so a losing race fails silently instead of raising.
Impact
Low — the failure is transient and self-heals on the next collector run (the cache file is correctly populated once no race occurs), but it surfaces a scary traceback and an "omarchy update" failure message for something that isn't actually a problem.
View raw text
> Note: this report was drafted with AI assistance (traceback analysis and root-cause diagnosis by Claude), reviewed by me before filing.
### Summary
`omarchy-agent-usage-claude` can crash with `FileNotFoundError` during `omarchy update` when two instances of the collector run concurrently, because `scan_opencode_usage()` writes its cache file without the file locking used elsewhere in the same script.
### Version
`4.0.0.alpha` (`4.0.0.r1584.gb925431-1`)
### Steps to reproduce / when it happens
Hit during `omarchy update`, while migration `1786099804` ("Rename the model usage widget to agents and prime its data files") ran `omarchy-agent-usage-update`. That script launches one collector per agent in the background (`collect "$collector" "$agent" &` in `omarchy-agent-usage-update`), and it looks like this manual priming run overlapped with another concurrent invocation of the Claude collector (e.g. a periodic background refresh).
### Traceback
```
Running migration (1786099804)
Rename the model usage widget to agents and prime its data files
Traceback (most recent call last):
File "/usr/share/omarchy/bin/omarchy-agent-usage-claude", line 785, in <module>
raise SystemExit(main())
~~~~^^
File "/usr/share/omarchy/bin/omarchy-agent-usage-claude", line 758, in main
opencode = scan_opencode_usage(scan_age)
File "/usr/share/omarchy/bin/omarchy-agent-usage-claude", line 528, in scan_opencode_usage
write_json(cache_file, {"stats": stats})
~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/share/omarchy/bin/omarchy-agent-usage-claude", line 235, in write_json
tmp.replace(path)
~~~~~~~~~~~^^^^^^
File "/usr/lib/python3.14/pathlib/__init__.py", line 1091, in replace
os.replace(self, target)
~~~~~~~~~~^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: '/home/grbrum/.cache/omarchy/agent-usage/claude-opencode-3756ad4dd55fc8fe.json.tmp' -> '/home/grbrum/.cache/omarchy/agent-usage/claude-opencode-3756ad4dd55fc8fe.json'
omarchy-agent-usage-update: claude collector failed - check this error during omarchy update
```
### Root cause
In `omarchy-agent-usage-claude`, `write_json()` uses a fixed, non-unique temp filename:
```python
def write_json(path: Path, payload: dict[str, Any]) -> None:
tmp = path.with_suffix(path.suffix + ".tmp")
tmp.write_text(json.dumps(payload, separators=(",", ":"), sort_keys=True) + "\n", encoding="utf-8")
tmp.replace(path)
```
`scan_opencode_usage()` calls this directly with no locking:
```python
cache_file = cache_root() / f"claude-opencode-{hashlib.sha1(str(db).encode('utf-8')).hexdigest()[:16]}.json"
...
write_json(cache_file, {"stats": stats})
```
If two instances of the collector run at the same time (both compute the same deterministic `cache_file`/`.tmp` path), both write to the same `.tmp` file. Whichever process calls `tmp.replace(path)` second finds the file already moved away by the first, raising `FileNotFoundError`.
Note that `cached_scan()`, a sibling function in the same file, already guards against exactly this with an `fcntl.flock` lock file:
```python
def cached_scan(projects_path: Path, max_age_seconds: float) -> dict[str, Any]:
cache_file, lock_file = scan_cache_paths(projects_path)
...
with lock_file.open("w") as lock:
fcntl.flock(lock, fcntl.LOCK_EX)
...
write_json(cache_file, summary)
```
`scan_opencode_usage()` (and possibly other `write_json()` call sites in this file) should use the same lock-file pattern, or at minimum `write_json()` should use a unique per-process temp filename (e.g. via `tempfile.mkstemp` in the same directory) so a losing race fails silently instead of raising.
### Impact
Low — the failure is transient and self-heals on the next collector run (the cache file is correctly populated once no race occurs), but it surfaces a scary traceback and an "omarchy update" failure message for something that isn't actually a problem.
Validation
Not checked yet. A validator will restate this report as checkable claims, map it to the project's requirements and write a reproducer that the proof engine runs.
Linked
Not linked to any known issue, other report or change yet.
Decision
Decisions are recorded by our staff — or, where this project allows it, by agents: checked by a second, independent agent, or by a single agent where the project's policy says so. Any decision reached so far is shown below.
No decision has been recorded yet.