Skip to content

Add Project browser and path autocomplete omit symlinked directories #8408

Description

@ShaulLavo

What happened

The Add Project file browser and the path autocomplete do not show symlinks to
directories, so I could not browse into my projects at all.

On the server machine, ~/Projects is a symlink to /work/projects (a mounted
drive). When I open Add Project and browse my home directory, the Projects
symlink never appears as a folder, so none of my projects are reachable through
the UI. The only ways in are typing the fully resolved real path by hand, or
adding the project on the server machine directly.

Surface: desktop app 0.0.35 on macOS, connected via the SSH-launch flow to a
Linux server running t3@0.0.35.

Diagnosis

Single root cause in apps/server/src/workspace/WorkspaceEntries.ts:222, inside
browse():

for (const dirent of dirents) {
  if (
    dirent.isDirectory() &&
    dirent.name.toLowerCase().startsWith(lowerPrefix) &&
    (showHidden || !dirent.name.startsWith("."))
  ) {

readdir(..., { withFileTypes: true }) returns dirents with lstat semantics:
for a symlink, dirent.isDirectory() describes the link itself, not its target,
and is therefore false. A symlink pointing at a directory is classified as
neither a file nor a directory and is filtered out entirely.

The Add Project directory picker and the path autocomplete are the same code
path — both are served by the filesystem.browse RPC
(packages/contracts/src/rpc.ts:224, WsFilesystemBrowseRpc at line 674), and
browse() above is its only implementation. The client
(packages/client-runtime/src/state/filesystem.ts,
filterFilesystemBrowseEntries) filters only on the name prefix, so all
directory classification happens in that one server-side loop. That is why both
surfaces fail identically.

The call succeeds — it does not error. Entries are silently dropped, so nothing
surfaces in the logs and the directory just looks empty of that folder.

Suggested fix, matching the smallest-scope proposal in the earlier report: keep
dirents where isDirectory() is true, and additionally keep
isSymbolicLink() dirents whose stat() target is a directory (guarding
ELOOP/ENOENT for broken or cyclic links, which should be excluded). One
extra stat() per symlink candidate, on an interactive-only path. No contract
change is required — FilesystemBrowseEntry is { name, fullPath } and
fullPath should stay the symlink path the user typed, not the resolved target.

This is still present on main as of 2026-08-27: line 222 is unchanged.

Steps to reproduce

On the machine running the server:

mkdir -p /tmp/real
ln -s /tmp/real ~/link-to-real
  1. Open Add Project.
  2. Browse to the home directory (~/).
  3. link-to-real does not appear in the listing. /tmp/real browsed directly
    does appear.
  4. Same in the path autocomplete: typing ~/link produces no completion.

Reproducible every time. Platform-independent — nothing macOS-specific about it.

Version

0.0.35 (server t3@0.0.35, launched as npx t3); desktop app 0.0.35 on macOS.

Environment

Server: Linux x64 (kernel 7.1.9), Node v26.7.0, server running on
127.0.0.1:3773. Client: desktop app 0.0.35 on macOS, connected over the
SSH-launch flow.

Evidence

# Real symlinks on the server machine:
~/Projects  -> /work/projects
~/Worktrees -> /work/worktrees

# Node dirent semantics that browse() relies on:
link-to-real   dirent.isDirectory=false  dirent.isSymbolicLink=true  | stat(target).isDirectory=true
plaindir       dirent.isDirectory=true   dirent.isSymbolicLink=false | stat(target).isDirectory=true

# Applying the v0.0.35 filter vs. a symlink-aware filter to the home directory:
v0.0.35 browse("~/") returns:  Documents, Downloads, Music, Pictures, Videos
symlink-aware filter returns:  Documents, Downloads, Music, Pictures, Projects, Videos, Worktrees
MISSING:                       Projects, Worktrees

# server.trace.ndjson — the RPC succeeds; entries are dropped silently:
{"type":"effect-span","name":"WorkspaceEntries.browse","durationMs":0.457326,"exit":{"_tag":"Success"}}
{"type":"effect-span","name":"ws.rpc.filesystem.browse","attributes":{"rpc.method":"filesystem.browse",
 "rpc.aggregate":"workspace"},"durationMs":0.549747,"exit":{"_tag":"Success"}}
# (2026-08-27T14:33:04.409Z and 14:33:29.650Z)

Nothing appears in server.log for these calls — there is no failure to log.

Related issues

Fix applied or workaround

Nothing was changed on the machine. Workaround: type the resolved real path
(/work/projects/...) in the picker instead of the ~/Projects/... symlink
path, or add the project on the server machine directly.

Filed by

claude (Opus 5) via t3 triage.

Activity

  1. fMads commented on Sep 4, 2026

    @fMads

    I have kinda the same issue - one of the sub-directories in my project is symlinked in (to support multiple projects having the same work files).
    I cannot open and view those files in the file view

  2. bawisniewski commented on Sep 20, 2026

    @bawisniewski

    Same root cause also hides symlinks in the main Files tab (file tree), not just the Add Project picker.

    projects.listEntries (apps/server/src/workspace/WorkspaceEntries.ts, directory listing branch) uses readdir(..., { withFileTypes: true }) and then keeps only children where isDirectory() || isFile():

    if (child.name === ".git" || (!child.isDirectory() && !child.isFile())) return [];

    A symlinked entry is neither, so it is silently dropped from the tree listing. This affects both web and mobile file browsers, since they share this RPC and the client adds no classification of its own.

    There is also no way to represent a symlink downstream: ProjectEntry in packages/contracts/src/project.ts is { path, kind: "file" | "directory", ignored? } — no isSymlink flag — so even a client-side fix could not label links correctly.

    Repro: add a project containing ln -s . loop or ln -s ~/some-dir linked, open the Files tab (web or mobile) — linked never appears. Expected: symlinked entries show up (ideally resolved to their target kind, with symlink-aware containment retained so links escaping the workspace don't allow traversal).

  3. darknessest commented on Oct 6, 2026

    @darknessest

    Also affected on stable 0.0.45: existing project folders that are valid directory symlinks do not appear in the Add Project/open/clone folder picker, while ordinary directories do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions