#3564: fix(plugins): follow symlinks in plugin and hook discovery
Cluster:
Plugin Management Enhancements
Symlinked extension and hook directories weren't being discovered. `readdir` with `withFileTypes` returns `Dirent` objects where `isDirectory()` is false for symlinks, even when the target is a directory — so they get silently skipped.
Added `isSymbolicLink()` checks in three places:
- `src/plugins/discovery.ts` — plugin/extension discovery
- `src/hooks/workspace.ts` — hook discovery
- `src/security/audit-extra.ts` — extension security audit
Other `isDirectory()` calls in `src/` either use `fs.statSync` (follows symlinks already) or operate on internal state dirs that wouldn't be symlinked. Dangling or non-directory symlinks are handled gracefully by existing null checks downstream (`readPackageManifest` returns null on any failure).
Related: #2961
- [x] AI-assisted
- [x] Tested locally with symlinked extensions
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
This PR updates plugin/extension and hook discovery to treat symlinked entries returned by `readdir({ withFileTypes: true })` as candidates, since `Dirent.isDirectory()` is `false` for symlinks even when they point at directories. The same adjustment is applied to the security audit’s scan of the extensions directory so symlinked extension directories are counted.
Overall this brings discovery behavior closer to codepaths that already rely on `stat` (which follows symlinks), improving support for monorepos and setups that symlink extension/hook directories into the expected locations.
<h3>Confidence Score: 4/5</h3>
- This PR is likely safe to merge and should fix the reported symlinked-directory discovery gap.
- Changes are small and localized (additional `isSymbolicLink()` checks) and align with the stated root cause. The main remaining risk is behavioral edge cases (symlink-to-file vs symlink-to-dir) and a possible false positive in the security audit count, but no obvious runtime-breakers were introduced.
- src/plugins/discovery.ts and src/security/audit-extra.ts
<!-- greptile_other_comments_section -->
<sub>(2/5) Greptile learns from your feedback when you react with thumbs up/down!</sub>
**Context used:**
- Context from `dashboard` - CLAUDE.md ([source](https://app.greptile.com/review/custom-context?memory=fd949e91-5c3a-4ab5-90a1-cbe184fd6ce8))
- Context from `dashboard` - AGENTS.md ([source](https://app.greptile.com/review/custom-context?memory=0d0c8278-ef8e-4d6c-ab21-f5527e322f13))
<!-- /greptile_comment -->
Most Similar PRs
#21245: fix(plugins): harden symlinked extension discovery
by victorGPT · 2026-02-19
84.2%
#23473: Hooks/Plugins: enforce discovery root containment
by bmendonca3 · 2026-02-22
78.6%
#20424: Fix plugin extension path traversal in discovery/install
by markmusson · 2026-02-18
77.7%
#7007: Fix security audit false-positive for symlinked state dir
by MohammadErfan-Jabbari · 2026-02-02
75.0%
#11032: fix(security): block plugin install/load on critical source scan fi...
by coygeek · 2026-02-07
73.7%
#22910: fix(browser): resolve symlinks in upload path validation
by erdinccurebal · 2026-02-21
73.5%
#7090: fix: plugin install uses manifest ID for folder name (#2796)
by dial481 · 2026-02-02
73.5%
#23620: fix(memory): follow symlinks in memory indexer, reader, and watcher
by Diaspar4u · 2026-02-22
73.2%
#20499: test(plugins): add bundled+config duplicate discovery regression
by dcol91863 · 2026-02-19
73.0%
#2556: fix(plugin-install): handle existing plugins and filter workspace deps
by longmaba · 2026-01-27
73.0%