Plugin cache install silently drops symlinks from local plugin sources
What issue are you seeing?
Summary
Local plugin installation into the Codex plugin cache silently drops symlinks.
This breaks plugin source trees that contain valid symlinked entries. In my case, a local plugin with a Python virtual environment was copied into:
~/.codex/plugins/cache/local-personal/linux-computer-use/...
but the cached .venv was missing .venv/bin/python, because that entry is a symlink in a normal Python venv. The cached tree still contained regular files such as pip, so the result looked like a partially copied venv rather than a clean missing-runtime error.
This does not appear to be caused by Linux archive extraction or unpacking. The affected marketplace source is a local plugin source, and the Codex source path for local plugins does not go through tar, zip, or archive materialization.
Observed failure:
missing venv .../.venv/bin/python
Local evidence from the cached plugin tree:
- source plugin tree contains symlinks
- cached plugin tree contains zero corresponding symlinks
.venv/bin/python*symlinks are missing from the cache- regular files such as
.venv/bin/pipremain in the cache - cached venv metadata/entrypoints can still refer to the original source plugin path, which suggests a partially copied runtime rather than a rebuilt one
The direct source-level cause appears to be codex-rs/core-plugins/src/store.rs: copy_dir_recursive() handles only directories and regular files and has no is_symlink() branch, so symlinks are silently skipped.
What steps can reproduce the bug?
A minimal repro is a local marketplace plugin whose source tree contains a symlink.
- Create or use a local plugin source with a symlinked file:
mkdir -p /tmp/codex-local-plugin/.codex-plugin
printf '{"name":"sample-plugin","version":"local","description":"sample"}\n' \
> /tmp/codex-local-plugin/.codex-plugin/plugin.json
echo "hello" > /tmp/codex-local-plugin/target.txt
ln -s target.txt /tmp/codex-local-plugin/target-link.txt
- Install that plugin through a local marketplace source.
- Inspect the installed cache entry under:
~/.codex/plugins/cache/<marketplace-name>/<plugin-name>/<version-or-local>/
- Check whether the symlink was preserved:
test -L ~/.codex/plugins/cache/<marketplace-name>/<plugin-name>/<version-or-local>/target-link.txt
Actual result: the symlink is missing.
Expected result: target-link.txt should still be a symlink pointing to target.txt.
For local marketplace plugins, this is not an archive extraction issue:
MarketplacePluginSource::Localis resolved as a local path.materialize_marketplace_plugin_source()returns local plugin paths directly.- The cache write is performed by
PluginStoreincodex-rs/core-plugins/src/store.rs.
What is the expected behavior?
Plugin cache installation should preserve symlinked files and symlinked directories from the materialized plugin source tree.
If Codex intentionally does not support some filesystem entry type, the install should fail with an explicit error rather than silently omitting entries.
Additional information
I searched existing issues for symlink plugin cache, plugin cache, local plugin symlink, and copy_dir_recursive, and did not find a duplicate for this specific cache-copy behavior.
The relevant code appears to be:
codex-rs/core-plugins/src/store.rs
copy_dir_recursive() currently handles only directories and regular files:
if file_type.is_dir() {
...
} else if file_type.is_file() {
fs::copy(...)?;
}
There is already a symlink-preserving copy implementation elsewhere in the repository:
codex-rs/exec-server/src/local_file_system.rs
That implementation reads the link target and recreates the symlink, which seems like the expected behavior for plugin cache materialization as well.
A possible fix would be to:
- add an
is_symlink()branch incopy_dir_recursive() - use
fs::read_link()and recreate the symlink in the staged cache directory - on Unix, use
std::os::unix::fs::symlink - on Windows, choose
symlink_dirvssymlink_file - return an explicit error for unsupported entry types instead of silently skipping them
I validated this approach locally with regression coverage for:
- preserving a symlinked file during plugin cache install
- preserving a symlinked directory during plugin cache install
- preserving symlinks through the local marketplace
PluginsManager::install_plugin()path
These passed locally:
cargo test --manifest-path codex-rs/Cargo.toml -p codex-core-plugins
cargo test --manifest-path codex-rs/Cargo.toml -p codex-core install_plugin_supports_git_subdir_marketplace_sources
cargo test --manifest-path codex-rs/Cargo.toml -p codex-core install_plugin_preserves_symlinks_from_local_marketplace_sources