Background: what happened
On October 7, 2026, I noticed that my existing local project groups had disappeared from the Windows desktop app after an update. These were established projects that had previously appeared normally in the sidebar.
The local project section showed “No projects.” Existing conversations were still available in Recents, so it initially wasn’t clear whether the underlying data had been lost or whether the application had stopped displaying the project organization.
Switching between Chat, Work and Codex did not restore the groups. I specifically checked the Codex view, so changing views alone was not a solution.
The phone’s Remote view still displayed familiar local project groups and nested conversations while the desktop project list was empty. That was an important clue, although it did not establish why the two views differed or prove that every conversation was intact.
The original project folders and their contents were still present on disk. Inspection of a preserved profile copy also found existing project definitions and roots in the database. The investigation therefore focused on the metadata used to display local project groups rather than recreating the original folders or repositories.
Environment
- Windows desktop package: OpenAI.Codex 26.1002.7124.0
- Standard profile location:
%USERPROFILE%\.codex - CODEX_HOME was not set in the environment checked
- If you use a custom CODEX_HOME, verify your actual profile location before following any of these steps
Expected versus actual behavior
Expected: the desktop sidebar should continue displaying existing local project groups and provide access to their conversations.
Actual: the local project section was empty, despite conversations remaining in Recents, source folders remaining on disk, and project records remaining in the local database.
I do not have a reliable reproduction sequence. “After an update” describes when I noticed the issue; it does not prove the update caused it.
What the investigation found
The current .codex-global-state.json and its .bak both parsed as valid JSON when inspected. However, local-projects was an empty object and project-order was an empty array.
The two files were identical, so the automatic .bak did not contain a different earlier project registry that could simply be restored.
The database still contained ordinary local projects and ChatGPT-backed projects. These needed different handling. Existing legacy mappings, orphan mappings, incomplete migration state and an explicit projectless marker were also present. None was automatically deleted, recreated or reset.
The logs showed repeated failures to load global state because of an unexpected NUL character, followed by a project-migration completion message, followed by an updater-completion message.
That ordering matters: the loading errors preceded the recorded update-completion entry. The evidence supports a global-state loading problem around that period, but does not establish which operation caused the empty registry. A valid JSON file when inspected later does not rule out an earlier loading failure.
Recovery result
A narrowly scoped change to local-projects and project-order restored the local groups. After one controlled launch, I opened historical conversations from the recovered groups and confirmed that they loaded.
No direct database updates, bulk thread reassignment or migration-flag reset was used.
This is a detailed account of one recovery, not an official or universal fix. If your files, schema or application behavior differ, stop rather than forcing this procedure onto your profile.
1. Understand what is being repaired
The “project registry” in this post means the project map inside .codex-global-state.json. This procedure does not edit the Windows Registry.
A missing sidebar group does not, by itself, tell you whether the underlying folder, project record or conversation has been deleted. Those are separate things to check.
This approach is relevant when the local project map is empty but the existing project definitions and roots remain available. It is not a recovery method for deleted source files, missing database records or every possible form of database corruption.
2. Preserve the profile before investigating
Save your work and close ChatGPT/Codex normally. Establish whether relevant background writers are still active. A process-name check alone does not prove that nothing can write to the profile.
Create a separate preservation backup and then a disposable inspection copy. Leave the preserved backup untouched.
Important precautions:
- Keep SQLite databases together with their existing -wal and -shm sidecars
- Do not delete sidecars to make copying or opening a database easier
- Record copied paths, file sizes, full SHA-256 hashes, exclusions and errors
- Document junctions and other reparse points rather than blindly following them into unrelated folders
- Stop on access-denied paths and document the limitation; don’t take ownership, change permissions or switch identities to bypass it
- Don’t describe a backup as complete if some contents could not be copied
- Don’t describe a backup as cold unless relevant writers were actually quiescent
My preservation pass had documented inaccessible-directory exclusions. Matching hashes for the included files did not turn it into a complete filesystem-image backup. The proposed repair still required a narrow, independently verified pre-apply snapshot.
If you cannot obtain a trustworthy backup of the files a proposed repair could affect, do not proceed with that repair.
3. Independently verify the inspection inputs
Before trusting a diagnosis, verify the selected files against the preserved manifest.
For each relevant file, record:
- Its exact resolved path
- Expected size and full SHA-256 from the manifest
- Actual filesystem size
- Actual number of bytes read successfully
- Full computed SHA-256
Fingerprint the manifest itself so it is clear which baseline was used.
Stop on a denied read, incomplete read, unexpected path or mismatch. A read failure must raise an error rather than being treated as normal end-of-file.
Our focused verification covered the global-state JSON, its .bak, and the state database with its sidecars. Credentials and conversation contents were not needed.
Perform SQLite inspection on the disposable copy. Even read-only database access can involve auxiliary-file activity, so don’t use the sole preserved backup as your working inspection copy.
4. Inspect the existing project definitions
Use a read-only SQLite connection pointing to the disposable copy. Check the actual schema first; table names and behavior may differ between versions.
The relevant tables in this case were:
- projects: existing project IDs, names, positions and timestamps
- project_roots: all roots belonging to each project, including multi-root projects
- project_idempotency_keys: existing key-to-project mappings, including mappings whose target project record was absent
Inspect only the relevant global-state fields as well, including local-projects, project-order, legacy mappings, migration state and explicit projectless markers.
Distinguish ordinary local projects from ChatGPT-backed projects. An orphan mapping is something to understand, not an instruction to recreate or delete a project.
Also, NULL values in threads.project_id do not automatically mean membership was lost. The installed app has feature-gated assignment behavior. Matching a thread’s working directory to a project root is only a possible association, not proof of its intended original membership.
We did not bulk-assign chats from cwd or remove explicit projectless markers.
5. Verify the correct ID handling for your build
This was essential before constructing a candidate.
We reviewed the installed application’s project-cache and startup code, including storeProject and initializeProjects.
In the relevant code path, an existing legacy-to-server mapping takes precedence. Otherwise, the canonical server project ID is used. The ordinary local projects being reconstructed did not have applicable legacy mappings in that map, so their canonical database IDs matched the existing-project lookup in the reviewed code.
That supported the ID choice for this candidate. It does not mean everyone should replace legacy IDs with canonical IDs.
Do not copy another person’s IDs, generate replacements or assume the same mapping rules apply to another version. If you cannot establish the expected record structure and ID handling for your installed build, stop before editing.
The ChatGPT-backed projects were excluded from this reconstruction. Their backing metadata and account associations were preserved rather than converting them into plain local records.
6. Prepare an offline candidate
Create a separate candidate based on the verified global-state file. Do not edit the live profile at this stage.
Only two top-level fields changed in this recovery:
local-projects
We added the intended ordinary local entries using existing, verified values:
- id: the appropriate existing ID for the application’s cache
- name: the existing project name
- rootPaths: every registered root, in its stored order
- createdAt: the database’s created_at_ms
- updatedAt: the database’s updated_at_ms
project-order
We added the same IDs in their existing database position order.
Do not invent timestamps, type paths from memory or use values from someone else’s example.
Every other global-state field was preserved. There were no migration resets, legacy-map changes, cloud-backed-state changes, thread assignments or removals of projectless markers.
7. Validate the exact candidate change
Check that:
- Only the intended ordinary local projects are included
- Project IDs are unique
- Each project-map key matches its entry’s id
- project-order contains each intended ID exactly once
- Names, ordered roots and timestamps match the preserved source records
- Only local-projects and project-order differ from the baseline
- All unrelated fields are unchanged
In this case, reversing those two candidate additions reproduced the original global-state file’s exact SHA-256 hash. That independently corroborated preservation of the remaining state.
The code review supported an existing-project lookup rather than creation of new projects. That was still a source-based expectation, not a guarantee about every startup path. Application startup remained untested until the controlled launch.
Likewise, bundle-default feature flags were not treated as proof of effective runtime settings.
8. Apply only with fresh checks
Before replacing a live file:
- Establish writer quiescence again
- Verify that the current global-state file still matches the reviewed baseline
- Recheck the relevant project/database state
- Stop if anything material has changed; don’t overwrite newer state or automatically regenerate a candidate
- Create a fresh, independently verified pre-apply snapshot of the relevant JSON and database files with their existing sidecars
- Verify the exact candidate hash
- Preserve the original file’s appropriate security permissions
- Replace only the primary global-state JSON atomically
- Verify the resulting hash and exact two-field semantic change
Do not rely solely on the automatic .bak file for rollback. Also, copying basic file mode bits is not proof that Windows ACLs were preserved.
The only intended live change in this recovery was the primary global-state JSON. We did not directly edit SQLite records or migration flags.
9. Test the result separately from the assumptions
After one controlled launch, the local project groups returned. I opened historical conversations from recovered groups and confirmed that they loaded.
Check sidebar visibility and conversation loading separately. If the groups return but conversations are missing or misplaced, do not immediately run a bulk reassignment script. That is a separate issue requiring its own diagnosis.
I did not exhaustively verify every conversation or prove zero data loss. The verified outcome was the return of the project groups and successful loading of the older conversations I checked.
10. Keep the snapshots and respect the rollback boundary
Keep both the preservation backup and the fresh pre-apply snapshot.
Before application startup, restoring the original primary JSON can undo that specific JSON change. After startup, the application may also have written database or other state. Restoring only JSON afterward does not guarantee a complete return to the pre-launch state.
If unexpected behavior occurs, close the application normally, preserve the resulting evidence and determine what changed before attempting broader rollback. Do not blindly overwrite a current database with an older copy.
Privacy and sharing evidence
Do not publicly upload auth.json, tokens, raw profile or database files, full global-state dumps, personal filesystem paths or conversation history.
Sanitized field summaries and targeted error messages are safer for comparing symptoms. This post intentionally omits project names, project categories, account identifiers and private evidence files.