Marketplace upgrade/add leaks uncleaned `.staging/` and `marketplace-backup-*` dirs in `$CODEX_HOME/.tmp/marketplaces` (unbounded disk growth)
Marketplace upgrade/add leaks uncleaned .staging/ and marketplace-backup-* dirs in $CODEX_HOME/.tmp/marketplaces (unbounded disk growth)
Summary
Plugin-marketplace upgrade and add operations can leave scratch directories under $CODEX_HOME/.tmp/marketplaces/ that are never garbage-collected. Over time these grow without bound — in one environment they reached tens of GB (100+ leftover directories) and filled the disk.
Two categories leak:
$CODEX_HOME/.tmp/marketplaces/.staging/marketplace-upgrade-*and.../.staging/marketplace-add-*— per-operation git clones (each can be hundreds of MB to >1 GB).$CODEX_HOME/.tmp/marketplaces/marketplace-backup-*— pre-upgrade rollback snapshots.
Environment
@openai/codex0.144.0- Code references below are against
mainas of 2026-07-10 (line numbers may drift).
Root cause (from reading codex-rs/core-plugins/src/)
- add path leaks even on ordinary failures.
.staging/marketplace-add-*is created viatempfileand thenTempDir::keep()(marketplace_add.rs:156-174), which disables theDrop-based cleanup. As a result any subsequent failure — clone, validation, destination move, or rollback — leaves the directory behind (not only abnormal process exit). The add-path git clone also has no timeout, so a stuck clone can be interrupted and stranded. - upgrade path leaks on abnormal exit.
.staging/marketplace-upgrade-*is created every upgrade viatempfile::Builder::…prefix("marketplace-upgrade-").tempdir_in(&staging_parent)(marketplace_upgrade.rs:226-241) and relies onTempDir'sDrop. If the process is killed/interrupted before the guard drops, the directory is left behind. - backups are intentionally retained on rollback failure.
marketplace-backup-*is created inactivate_marketplace_root()(marketplace_upgrade/activation.rs:76-129): the live dir is renamed intobackup_dir/rootas rollback insurance. On success theTempDirdrops; but the double-failure paths intentionally keep the backup (it may be the only surviving copy of the live root), and abnormal exit also leaves it. - No sweep exists for either category. A repo-wide search for
marketplace-backupmatches only the creator. There is an age-based cleanup —remove_stale_curated_repo_temp_dirs(startup_sync.rs:402-485, 10-minute max age, called at:388) — but it targets a different temp category (.tmp/plugins-clone-*) and never touches.staging/*ormarketplace-backup-*.
Why it accumulates in practice
Hosts that spawn codex frequently as short-lived subprocesses and terminate them (before the Rust TempDir Drop runs, where Drop is even still in effect) never get the implicit cleanup. Combined with the add path's keep() disabling Drop entirely, each interrupted or failed upgrade/add can strand a full clone, and nothing sweeps them later, so they pile up indefinitely.
Does external deletion trigger a re-download?
The upgrade skip-vs-clone decision (marketplace_upgrade.rs:202-282) decides to skip cloning based only on: (1) the live destination dir existing and validating, (2) last_revision from config.toml matching the remote HEAD, and (3) <destination>/.codex-marketplace-install.json matching. It never reads .staging/* or marketplace-backup-*. So removing an abandoned scratch/backup directory does not by itself cause a re-clone.
Two caveats make blind external deletion unsafe, though:
- Deleting an in-flight
.staging/*directory breaks the operation that owns it. - A
marketplace-backup-*left behind by a rollback failure may be the only recoverable copy of the live root; deleting it before the live root is restored can lose that state.
Proposed fix
Primary (structural, in codex-rs/core-plugins):
- Drop the add-path
TempDir::keep()(marketplace_add.rs:172) and keep theTempDirguard alive through the rename/activation, so ordinary failures clean up automatically. - Serialize add / upgrade / cleanup for a given marketplace with a cross-process lock, and sweep
.staging/*only while holding that lock (so a sweep can never race an in-flight operation — a directory's top-level mtime does not reliably prove inactivity). - For
marketplace-backup-*, reconcile rather than blindly age-delete: restore or retain the backup when the live root is absent/invalid, and delete only once a healthy live root is established and no operation is active.
Interim external workaround (for affected users, until the above lands): a periodic job that deletes $CODEX_HOME/.tmp/marketplaces/.staging/marketplace-{upgrade,add}-* and .../marketplace-backup-*, filtering by name prefix so live marketplace dirs are never touched, and gating each candidate on an age threshold plus an open-file-handle check so an in-flight operation is skipped (since mtime alone is not sufficient). For git-backed marketplaces a deleted backup only costs a re-clone.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗