Codex Desktop is still silently burning your SSD — here's how to check and stop it
On June 30th I came across a post on social media about a Codex bug that was silently burning through SSD write endurance. The original GitHub issue (#28224) had been filed about two weeks earlier by a developer who noticed 37 TB of writes over 21 days of uptime — extrapolating to roughly 640 TB/year, enough to exhaust a consumer SSD's rated lifetime in under twelve months.
By the time I saw it, the situation was confusing. OpenAI had merged three PRs, the issue author had commented saying the fixes cut about 85% of CLI writes, and the issue was closed. But further down the thread, and in separate issues filed the same day (#29463, #29674), Desktop users were reporting the exact same TRACE-dominated pattern on fully restarted machines. "Fixed" and "not fixed" were both true simultaneously, depending on which version of Codex you were running.
I'd been using Codex Desktop heavily that week, so I decided to check for myself. I ran the diagnostics using Claude Code in a separate terminal while keeping Codex Desktop open for real development work, so the measurements reflect actual concurrent usage. What I found confirmed what the Desktop users in those threads were reporting — the fix hasn't shipped to Desktop at all.
Here's the full picture: what's happening, why the "fix" doesn't apply to most Desktop users, how to check your machine, and the workaround I tested with before/after measurements.
First, check how much damage your SSD has already taken
Before digging into Codex specifically, it's worth checking the current state of your drive. On macOS, smartctl reads the NVMe SMART data directly:
brew install smartmontools
smartctl -a disk0The field to look for is Percentage Used. This represents how much of your SSD's rated write endurance has been consumed. A new machine should show 0%. Normal use for a year typically puts you somewhere around 1–3%. If you're seeing a number that looks higher than your machine's age would suggest, that's worth investigating — and Codex might be part of the story.
What's actually happening
Codex maintains a local feedback/diagnostics database at ~/.codex/logs_2.sqlite. The logging sink is hardcoded to TRACE level — the noisiest verbosity tier available — and ignores the RUST_LOG environment variable entirely (#17320). It captures raw WebSocket payloads, FSEvents noise, internal hyper_util HTTP-client tracing, and mirrored OpenTelemetry events. Most of it has zero diagnostic value for end users.
The insidious part is the insert-prune cycle. Codex inserts rows continuously, then deletes old ones to keep the retained count flat. The file size looks stable. du and Finder show nothing unusual. But every single one of those writes goes through a WAL (write-ahead log) checkpoint before deletion, and each checkpoint is a real write to NAND flash. The SSD absorbs every cycle, and those cycles don't come back.
The original report (#28224) measured approximately 37 TB written in 21 days of uptime. Extrapolated to a year, that's roughly 640 TB — enough to exhaust a consumer SSD rated at 600 TBW in under twelve months.
The fix exists — but it hasn't shipped to Desktop
OpenAI responded relatively quickly on the CLI side. Three PRs were merged into the codex-rs Rust codebase by June 23:
- #29432 — stop logging every WebSocket event (CLI 0.142.0)
- #29457 — filter noisy targets from persistent logs (CLI 0.142.0)
- #29599 — stop persisting bridged
target=logevents (CLI 0.143.0)
The original issue reporter confirmed these cut roughly 85% of CLI writes. The issue was closed.
But here's what everyone missed: those version numbers (0.142.0, 0.143.0) are CLI releases shipped via npm. The Desktop app bundles its own copy of the app-server binary built from codex-rs, on a completely separate release cadence with its own version scheme (e.g. 26.623.70822). The fix code exists in codex-rs/state/src/log_db.rs, but as of June 30 2026, the Desktop app hasn't shipped a build that includes it.
This isn't just my machine. Issue #29463, filed the same day #28224 was closed, reports TRACE still dominating at 839/1000 rows on a fully restarted Windows Codex Desktop. Issue #29674 documents the same pattern. Both are Desktop.
There's a further wrinkle that makes this worse: CLI and Desktop share the same SQLite file. Both write to ~/.codex/logs_2.sqlite under the same $CODEX_HOME. If you run both, an unpatched Desktop process will keep churning TRACE rows into the database even if your CLI is on a patched version. Updating the CLI alone doesn't protect you.
What I measured on the latest Desktop build
I ran these diagnostics on macOS Desktop 26.623.70822 on the evening of June 30 2026. I had Codex Desktop open and was actively developing, while using Claude Code in a separate terminal to run the diagnostic queries and scripts — so the measurements reflect a real concurrent workload, not idle or synthetic conditions.
Metric | Value |
|---|---|
TRACE share (last 500 rows) | 94.8% |
Autoincrement sequence | 13,827,825 |
Retained rows | 23,525 |
Churn ratio | ~600x |
TRACE write rate | ~23/sec (1,375 in 60s) |
WAL size | 14.4 MB |
The churn ratio is the number that tells the real story. The database's autoincrement counter had reached 13.8 million, but only 23,525 rows were actually retained. That means for every row sitting in the database, roughly 600 had been written and deleted. Each of those deleted rows still completed a full WAL checkpoint write before being pruned. The SSD absorbed all of it.
The top TRACE sources over the preceding 24 hours were the log crate (the log → tracing shim that catches every dependency's log output) and hyper_util pool/client noise — the exact same sources reported in #29674. This confirmed it's the same unpatched code path, not something specific to my environment.
Check your own machine
Keep Codex running — you want to see live behaviour, not a cold database. The single most useful command:
sqlite3 ~/.codex/logs_2.sqlite \
"SELECT level, COUNT(*) FROM (SELECT level FROM logs ORDER BY id DESC LIMIT 500) GROUP BY level;"If you're affected, TRACE will dominate — typically over 90% of the last 500 rows. On a patched build, TRACE should be absent entirely, with DEBUG making up the majority.
For the churn ratio, which tells you how much invisible writing has already happened:
sqlite3 ~/.codex/logs_2.sqlite "SELECT seq FROM sqlite_sequence WHERE name='logs';"
sqlite3 ~/.codex/logs_2.sqlite "SELECT COUNT(*) FROM logs;"If the sequence number is orders of magnitude higher than the row count, the insert-prune cycle has been churning hard.
And for file sizes:
ls -la ~/.codex/logs_2.sqlite*A large, actively-growing WAL file while Codex is running is the direct indicator of write pressure.
The workaround: block TRACE inserts at the SQLite level
This uses the trigger approach from #29674. A BEFORE INSERT trigger intercepts every INSERT on the logs table and silently discards it if the level is TRACE. RAISE(IGNORE) aborts only the individual insert — it doesn't roll back the transaction or surface an error to the application. Codex won't crash or report anything. Non-TRACE diagnostic rows (INFO, DEBUG, WARN) still get written normally.
Because the trigger lives in the database itself, not in any specific process, it covers everything writing to the same file: CLI, Desktop app-server, renderer, all of them.
Quit Codex first, then back up and apply:
# Confirm no handles are open
lsof ~/.codex/logs_2.sqlite*
# Back up
sqlite3 ~/.codex/logs_2.sqlite ".backup 'logs_2.backup.sqlite'"
# Apply the trigger
sqlite3 ~/.codex/logs_2.sqlite "
CREATE TRIGGER IF NOT EXISTS codex_suppress_trace_logs
BEFORE INSERT ON logs
WHEN NEW.level = 'TRACE'
BEGIN
SELECT RAISE(IGNORE);
END;
"
# Verify it's in place
sqlite3 ~/.codex/logs_2.sqlite \
"SELECT name, sql FROM sqlite_master WHERE type='trigger';"Restart Codex. The trigger takes effect immediately.
Before and after
After applying the trigger and restarting Codex, I used it actively for about six minutes, then re-ran the same diagnostics:
Metric | Before | After |
|---|---|---|
TRACE / 60s | 1,375 | 0 |
Total insert rate | ~1,482/min | ~46/min |
TRACE share (last 500 rows) | 94.8% | 0% |
WAL size | 14.4 MB | 1.79 MB |
The WAL collapsing from 14.4 MB to 1.79 MB is the direct SSD-write-pressure win. Total insert rate dropped by about 97%. Non-TRACE logs (INFO, DEBUG, WARN) continued flowing normally at ~46/min, which is a perfectly reasonable rate for diagnostic logging.
Keeping it alive after updates
The trigger is fragile across Codex updates. A migration that touches the logs table will wipe it silently and TRACE writes resume with no warning. I had Claude Code write a probe script that checks whether the trigger is still in place, reapplies it if it's been wiped, and prints a status snapshot. It's safe to run any time without quitting Codex:
#!/usr/bin/env bash
set -euo pipefail
DB="$HOME/.codex/logs_2.sqlite"
LOG="codex_trace_trigger_check.log"
TRIGGER_NAME="codex_suppress_trace_logs"
now() { date '+%Y-%m-%d %H:%M:%S %Z'; }
if [[ ! -f "$DB" ]]; then
echo "ERROR: $DB does not exist."
exit 1
fi
present=$(sqlite3 "$DB" \
"SELECT COUNT(*) FROM sqlite_master WHERE type='trigger' AND name='$TRIGGER_NAME';")
if [[ "$present" -eq 0 ]]; then
sqlite3 "$DB" <<'SQL'
CREATE TRIGGER IF NOT EXISTS codex_suppress_trace_logs
BEFORE INSERT ON logs
WHEN NEW.level = 'TRACE'
BEGIN
SELECT RAISE(IGNORE);
END;
SQL
state="REAPPLIED (was missing — recreated)"
echo "$(now) REAPPLIED trigger was missing, recreated." >> "$LOG"
else
state="present"
echo "$(now) ok trigger present." >> "$LOG"
fi
echo "=== codex_suppress_trace_logs probe @ $(now) ==="
echo "Trigger: $state"
echo
echo "--- File sizes ---"
ls -la "$DB" "${DB}-wal" "${DB}-shm" 2>/dev/null \
| awk '{printf "%12s %s\n", $5, $9}'
echo
echo "--- Last 60s write rate ---"
sqlite3 -header -column "$DB" \
"SELECT level, COUNT(*) AS n FROM logs
WHERE ts > strftime('%s','now','-60 seconds')
GROUP BY level ORDER BY n DESC;"
echo
echo "--- Last 500 rows by level ---"
sqlite3 -header -column "$DB" \
"SELECT level, COUNT(*) AS n FROM (
SELECT level FROM logs ORDER BY id DESC LIMIT 500
) GROUP BY level ORDER BY n DESC;"Save it, chmod +x it, and run it after any Codex update. What "healthy" looks like: Trigger: present, zero TRACE rows in the last-60s table, WAL well under 10 MB. If you see REAPPLIED, a Codex update wiped the trigger and the script already recreated it.
To revert at any time, just drop the trigger:
sqlite3 ~/.codex/logs_2.sqlite "DROP TRIGGER IF EXISTS codex_suppress_trace_logs;"Update: still unpatched on the latest build (3 July)
After writing the initial investigation, Codex Desktop pushed a new release — Version 26.623.81905, released July 1, 2026. I re-ran the same diagnostics. Same result. TRACE rows still dominating, the insert-prune cycle still churning, the WAL still bloated. The trigger I applied earlier had survived the update and was still suppressing TRACE writes, but after dropping it temporarily to test the new build clean, the unpatched behaviour came right back.
That's two consecutive Desktop releases since the CLI fix was merged, neither containing it. The fix code is sitting in codex-rs/state/src/log_db.rs in the repository. It's just not making it into the Desktop binary.
If you're a Desktop user, apply the trigger and run the probe script after every update until this lands. And if you're on a MacBook or ultrabook where the SSD is soldered to the logic board — there's no drive swap, the endurance loss is permanent. Worth keeping in mind.
