connection-catalog.json is written non-atomically every ~3s, and a corrupt document bricks the app permanently
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
Description
Summary
~/.t3/userdata/connection-catalog.json appears to be written in place, non-atomically, on a fixed ~3 second cadence. If the process dies mid-write (power loss, hard reset, OOM kill), NTFS commits the file's new size but not its data, leaving the file filled entirely with NUL bytes.
On the next launch, desktop.connectionCatalogStore.get throws a decode error and there is no fallback path — the app comes up with an empty project list and a connection that spins on "Connecting…" forever. It never self-heals, because the renderer only calls connectionCatalog.set after a successful get, so no subsequent write ever repairs the file.
The result is a completely unusable app from a single unlucky 3-second window, with no in-app indication of what's wrong.
Impact
After an unclean shutdown, the app launches into this state permanently:
- Sidebar shows "No projects yet" despite projects existing in
state.sqlite - The connection (in my case WSL/Ubuntu) is stuck on "Connecting…" indefinitely
- Agents (Claude, Codex) can't be used, since no connection ever resolves
- Nothing in the UI surfaces the underlying error — it just looks like data loss
Critically, no data is actually lost. state.sqlite is completely intact. Only a small piece of connection metadata is destroyed, but it takes the entire app down with it.
Reproduction
- Have T3 Code open with at least one project and one connection
- Kill power to the machine (or hard-kill the process) — the window is ~3s wide, so it lands fairly often
- On reboot, check
~/.t3/userdata/connection-catalog.json - Relaunch T3 Code
Expected: app recovers, falls back to a default catalog, rediscovers local/WSL connections Actual: empty project list, connection hangs forever, no error shown
Evidence
The file is 100% NUL bytes
$ xxd ~/.t3/userdata/connection-catalog.json | head -3 00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000010: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000020: 0000 0000 0000 0000 0000 0000 0000 0000 ................
size=1408 nul_bytes=1408
Every other JSON file under .t3/ parsed cleanly. This was the only corrupt file on disk.
Timeline from desktop.trace.ndjson
Time (UTC) | Event
-- | --
20:27:18 | last successful desktop.connectionCatalogStore.writeDocument
20:27:22 | file mtime — the write that got truncated by power loss
20:35:35 | first DesktopConnectionCatalogStoreDocumentDecodeError on relaunch
every launch since | same error, 16 occurrences per session, zero recovery attempts
The error
DesktopConnectionCatalogStoreDocumentDecodeError: Failed to decode the desktop connection catalog document at C:\Users\<user>\.t3\userdata\connection-catalog.json. at ...\resources\app.asar\apps\desktop\dist-electron\main.cjs:12811:227 at desktop.connectionCatalogStore.get (...main.cjs:12...)
Caused by: SyntaxError: Unexpected token '\u0000'
It propagates all the way up the IPC chain — every one of these fails together:
desktop.connectionCatalogStore.get Failure
desktop.ipc.connectionCatalog.get Failure
desktop.ipc.method Failure
desktop.ipc.invoke Failure
The write cadence is a constant 3.01s
Gap histogram between consecutive writeDocument spans in a single session:
3.01s → 124 3.02s → 47 3.00s → 8 3.06s → 2 3.05s → 1
That's ~1,200 writes/hour of what is essentially static connection metadata. The spans carry no payload, so I can't confirm the content is unchanged between writes, but a perfectly constant 3.01s interval strongly suggests a polling timer that writes unconditionally rather than on change.
An atomic writer already exists elsewhere in the codebase
I found this sitting next to the corrupt file:
client-settings.json 803 bytes (valid)
client-settings.json.59004.e6b320f650d14d91a69bd7885ebb0da5.tmp 663 bytes (valid)
That <name>.<pid>.<hash>.tmp pattern is a write-to-temp-then-rename. The settings store was also interrupted by the same power loss — and its real file survived perfectly intact, leaving only an orphaned temp file behind. The connection catalog store, with no such sibling, was destroyed in place.
So the fix pattern is already present in the codebase; the catalog store just isn't using it.
Suggested fixes
In rough priority order:
- Write atomically. Reuse whatever the settings store does: write to
connection-catalog.json.<pid>.<rand>.tmp,fsync, thenrename()over the target. On POSIX and NTFS alike, rename is atomic — a torn write can then only ever leave an orphaned temp file, never a destroyed catalog. - Never let a decode failure be fatal. Treat a malformed document exactly like a missing one: log a warning, move the bad file aside to
connection-catalog.json.corrupt-<timestamp>, and fall back to the default catalog so local/WSL connections get rediscovered. A config file that can't be parsed should degrade, not brick. - Only write when the content actually changed. Compare the serialized document to the last-written value and skip the write if identical. This collapses ~1,200 writes/hour to near zero, which shrinks the corruption window by orders of magnitude on its own — and is worth doing regardless of (1) for disk-wear and battery reasons.
- Surface the failure in the UI. Right now "No projects yet" + an infinite connection spinner is indistinguishable from real data loss. Even a toast saying "couldn't read connection settings" would have saved a lot of guesswork.
(1) and (2) are independent and both worth having: (1) prevents the corruption, (2) makes the app survive it if it happens anyway through some other route.
Workaround for anyone hitting this now
Quit T3 Code completely (including any lingering backend child process), then:
powershellmv "$HOME\.t3\userdata\connection-catalog.json" "$HOME\.t3\userdata\connection-catalog.json.bak"
Relaunch. The catalog is regenerated, projects reappear from state.sqlite, and local/WSL connections are rediscovered. Nothing is lost — the corrupt file contained no recoverable bytes anyway. Any manually configured remote connections may need to be re-added.
Environment
- OS: Windows 11, x64 (NTFS)
- T3 Code: Alpha channel,
t3@0.0.29 - Backend: WSL backend enabled, distro Ubuntu (
wslBackendEnabled: true) - Trigger: power surge / unclean shutdown
Possibly unrelated, but noticed while reading the logs
desktop.trace.ndjson records SSH remote pairing tokens in plaintext inside span events:
Connection string: http://127.0.0.1:3774
Token: <REDACTED>
Pairing URL: http://127.0.0.1:3774/pair#token=<REDACTED>
Since these trace files are exactly what users get asked to attach to bug reports, it may be worth redacting tokens at the log sink. Happy to file this separately if you'd prefer.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by tracing desktop.connectionCatalogStore.get and writeDocument, using the connection-catalog.json failure and desktop.trace.ndjson timeline as references. Check the IPC chain through desktop.ipc.connectionCatalog.get and desktop.ipc.invoke, then reproduce the hard-shutdown case. Done means interrupted writes no longer leave an unusable catalog and a corrupt catalog recovers with a usable default or rediscovered connections.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- electron, typescript
- Domain
- desktop
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 58/100