astrid-runtime/astrid · error

workspace capsule portal contains a redirect: {}

Error message

workspace capsule portal contains a redirect: {}

What it means

While walking the workspace capsule portal, an entry inside the tree is itself a symlink. The barrier treats any redirect inside the portal as a potential path-traversal vector and aborts the inventory with InvalidData rather than following it. The message names the offending child path.

Source

Thrown at crates/astrid-kernel/src/legacy_migration_barrier/host_fs.rs:165

                "workspace capsule portal is not a regular directory: {}",
                root.display()
            ),
        ));
    }
    astrid_core::platform_fs::verify_no_redirects(root)?;
    let mut targets = Vec::new();
    let mut stack = vec![root.to_path_buf()];
    while let Some(dir) = stack.pop() {
        let mut entries = fs::read_dir(&dir)
            .map_err(io::Error::other)?
            .collect::<Result<Vec<_>, _>>()
            .map_err(io::Error::other)?;
        entries.sort_by_key(std::fs::DirEntry::file_name);
        for entry in entries {
            let path = entry.path();
            let metadata = fs::symlink_metadata(&path).map_err(io::Error::other)?;
            if metadata.file_type().is_symlink() {
                return Err(io::Error::new(
                    io::ErrorKind::InvalidData,
                    format!(
                        "workspace capsule portal contains a redirect: {}",
                        path.display()
                    ),
                ));
            }
            if !metadata.is_dir() {
                continue;
            }
            astrid_core::platform_fs::verify_no_redirects(&path)?;
            if path.join("Capsule.toml").is_file() {
                targets.push(path.clone());
                if targets.len() > MAX_WORKSPACE_TARGETS {
                    return Err(io::Error::other(
                        "workspace capsule portal exceeds target limit",
                    ));
                }

View on GitHub (pinned to affd8760f4)

Solutions

  1. Replace the reported symlink with a real copy of its target: rm the link, then `cp -a` or `mv` the target into place.
  2. Exclude symlinks from the portal directory, keeping only physical capsule directories containing Capsule.toml.
  3. If the link is required, relocate the linked content physically under the portal and update consumers to the new path.
  4. Re-run the migration after cleaning; the error names the exact path each time, so fix iteratively.

Example fix

// before
ln -s ~/shared-capsule ~/.astrid/workspaces/team-capsule
// after
rm ~/.astrid/workspaces/team-capsule
cp -a ~/shared-capsule ~/.astrid/workspaces/team-capsule
Defensive patterns

Strategy: validation

Validate before calling

fn tree_has_symlinks(root: &std::path::Path) -> std::io::Result<Vec<std::path::PathBuf>> {
    let mut hits = Vec::new();
    for entry in walkdir_like(root) {
        if std::fs::symlink_metadata(&entry)?.file_type().is_symlink() {
            hits.push(entry);
        }
    }
    Ok(hits) // must be empty before migration
}

Type guard

fn is_plain_entry(m: &std::fs::Metadata) -> bool {
    !m.file_type().is_symlink()
}

Try / catch

match collect_workspace_targets(root) {
    Ok(t) => t,
    Err(e) if e.to_string().contains("contains a redirect") => {
        let p = extract_path(&e.to_string());
        eprintln!("replace symlink with a real copy: {p}");
        Vec::new()
    }
    Err(e) => return Err(e.into()),
}

Prevention

When it happens

Trigger: collect_workspace_targets iterates sorted directory entries and calls symlink_metadata on each child; any child whose file type is a symlink raises this immediately, before recursion. Also triggered if the portal root's own verify_no_redirects pass misses a deeper entry and the walk reaches it.

Common situations: Editors or package managers creating convenience symlinks (e.g. node_modules links, shared asset links) inside a workspace; users linking a capsule dir into their dotfiles; migration of data between machines that preserved symlinks.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09). Data as JSON: /api/errors/503ef1888b8ff65e. Report an issue: GitHub.