codex - 💡(How to fix) Fix Plugin cache install silently drops symlinks from local plugin sources [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openai/codex#18863Fetched 2026-04-22 07:51:16
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Author
Participants
Timeline (top)
labeled ×3unlabeled ×1

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/pip remain 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.

Error Message

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. If Codex intentionally does not support some filesystem entry type, the install should fail with an explicit error rather than silently omitting entries.

  • return an explicit error for unsupported entry types instead of silently skipping them

Root Cause

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.

Code Example

~/.codex/plugins/cache/local-personal/linux-computer-use/...

---

missing venv .../.venv/bin/python

---

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

---

~/.codex/plugins/cache/<marketplace-name>/<plugin-name>/<version-or-local>/

---

test -L ~/.codex/plugins/cache/<marketplace-name>/<plugin-name>/<version-or-local>/target-link.txt

---

codex-rs/core-plugins/src/store.rs

---

if file_type.is_dir() {
    ...
} else if file_type.is_file() {
    fs::copy(...)?;
}

---

codex-rs/exec-server/src/local_file_system.rs

---

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
RAW_BUFFERClick to expand / collapse

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/pip remain 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.

  1. 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
  1. Install that plugin through a local marketplace source.

  2. Inspect the installed cache entry under:

~/.codex/plugins/cache/<marketplace-name>/<plugin-name>/<version-or-local>/
  1. 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::Local is resolved as a local path.
  • materialize_marketplace_plugin_source() returns local plugin paths directly.
  • The cache write is performed by PluginStore in codex-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 in copy_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_dir vs symlink_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

extent analysis

TL;DR

The issue can be fixed by modifying the copy_dir_recursive() function in codex-rs/core-plugins/src/store.rs to handle symlinks by adding an is_symlink() branch and recreating the symlink in the staged cache directory.

Guidance

  • Identify the copy_dir_recursive() function in codex-rs/core-plugins/src/store.rs and add a new branch to handle symlinks using is_symlink().
  • Use fs::read_link() to read the link target and recreate the symlink in the staged cache directory.
  • Handle platform-specific symlink creation using std::os::unix::fs::symlink on Unix and symlink_dir vs symlink_file on Windows.
  • Return an explicit error for unsupported entry types instead of silently skipping them.

Example

if file_type.is_dir() {
    // ...
} else if file_type.is_file() {
    fs::copy(...)?;
} else if file_type.is_symlink() {
    let link_target = fs::read_link(...)?;
    std::os::unix::fs::symlink(link_target, ...)?;
}

Notes

The provided code snippet is a simplified example and may require additional error handling and platform-specific adjustments. The fix should be thoroughly tested to ensure it works correctly for both file and directory symlinks.

Recommendation

Apply the workaround by modifying the copy_dir_recursive() function to handle symlinks, as this will provide a more robust and explicit solution for preserving symlinks during plugin cache installation.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

codex - 💡(How to fix) Fix Plugin cache install silently drops symlinks from local plugin sources [1 participants]