libnyanpasu/clash-nyanpasu · error

runtime candidate directory is a symlink or reparse point: {

Error message

runtime candidate directory is a symlink or reparse point: {path}

What it means

Raised by prepare_private_dir (backend/tauri/src/client/runtime.rs:270) when symlink_metadata of the runtime candidate directory reports it is a symlink (or Windows reparse point). The store refuses to write private candidate files into a symlinked location because a symlink could redirect sensitive runtime files elsewhere or be swapped mid-operation.

Source

Thrown at backend/tauri/src/client/runtime.rs:270

            }
            Err(error) => Err(error.into()),
        }
    }
}

impl Drop for CandidateFile {
    fn drop(&mut self) {
        if !self.cleaned {
            let _ = std::fs::remove_file(&self.path);
        }
    }
}

async fn prepare_private_dir(path: &Utf8Path) -> anyhow::Result<()> {
    if let Ok(metadata) = tokio::fs::symlink_metadata(path).await
        && is_symlink_or_reparse(&metadata)
    {
        anyhow::bail!("runtime candidate directory is a symlink or reparse point: {path}");
    }
    tokio::fs::create_dir_all(path).await?;
    let metadata = tokio::fs::symlink_metadata(path).await?;
    if is_symlink_or_reparse(&metadata) || !metadata.is_dir() {
        anyhow::bail!("runtime candidate path is not a private directory: {path}");
    }
    #[cfg(unix)]
    {
        use std::os::unix::fs::PermissionsExt;
        tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)).await?;
    }
    Ok(())
}

#[cfg(unix)]
fn is_symlink_or_reparse(metadata: &std::fs::Metadata) -> bool {
    metadata.file_type().is_symlink()
}

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Remove the symlink at the candidate path and let the app create a real directory (passing the real target path explicitly if needed).
  2. Point the candidate directory configuration at the physical path instead of the symlink.
  3. On Windows, replace junctions/reparse points created by cloud-sync tools with real directories excluded from syncing.

Example fix

// before (path is a symlink)
let store = RuntimeStore::new(Utf8PathBuf::from("/app/data/candidates-linked"));
// after (real directory)
std::fs::remove_file("/app/data/candidates-linked")?;
std::fs::create_dir("/app/data/candidates")?;
let store = RuntimeStore::new(Utf8PathBuf::from("/app/data/candidates"));
Defensive patterns

Strategy: validation

Validate before calling

use std::os::unix::fs::MetadataExt;
let md = std::fs::symlink_metadata(&candidate_path)?;
if md.file_type().is_symlink() {
    return Err(anyhow::anyhow!("candidate dir must not be a symlink"));
}
if !md.is_dir() {
    return Err(anyhow::anyhow!("candidate path is not a directory"));
}

Type guard

fn is_plain_dir(path: &Utf8Path) -> bool {
    std::fs::symlink_metadata(path.as_std_path())
        .map(|md| md.is_dir() && !md.file_type().is_symlink())
        .unwrap_or(false)
}

Try / catch

match store.create_candidate(&bytes).await {
    Ok(c) => c,
    Err(e) if e.to_string().contains("symlink or reparse point") => {
        // refuse to proceed; surface a configuration/integrity error to the user
        return Err(IntegrityError::SymlinkedCandidateDir.into());
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: create_candidate_with_names, cleanup_stale_candidates, or candidate_collision_retries_with_exclusive_create calls prepare_private_dir while candidate_dir itself is a symlink or reparse point (checked before create_dir_all).

Common situations: User or packaging replaced the app data directory with a symlink to another disk; portable installs with linked config dirs; Windows junction/reparse points created by sync tools (Dropbox, OneDrive).

Related errors


AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08). Data as JSON: /api/errors/d45d0376a6fb9d74. Report an issue: GitHub.