astrid-runtime/astrid · error

capsule materialization target is redirected

Error message

capsule materialization target is redirected: {error}

What it means

This error is thrown by `repair_published_materialization` when, before wiping a stale capsule materialization directory, `astrid_core::platform_fs::verify_no_redirects(target)` finds symlinks or other redirect entries inside the target tree. The kernel refuses to `remove_dir_all` a directory whose contents are redirected (e.g. contain symlinks), because destroying it could follow links and damage files outside the cache directory. It is a defensive integrity check on the durable cache path before deletion.

Solutions

  1. Remove the target directory manually (after inspecting it for symlinks pointing outside the cache) so the next run starts from a clean, redirect-free directory.
  2. Find the symlink entries reported in the underlying `verify_no_redirects` error and replace them with real files/directories, then re-run the operation.
  3. Stop any tooling (rsync link-dest, pnpm-style symlinking, overlay mounts) that is materializing the cache directory with symlinks instead of real content.
  4. If the target is intentionally linked, point the materialization target at a dedicated directory path that nothing else manages.

Example fix

// before (developer shared cache entries via symlinks)
$ ln -s /shared/build/wasm pkg/out.wasm
// after (use real files, or a fresh target)
$ rm -rf ~/.cache/astrid/capsules/<name>-<hash>
$ astrid capsule install <name>  // re-materializes real files
Defensive patterns

Strategy: validation

Validate before calling

use std::path::Path;

fn target_is_clean_real_dir(target: &Path) -> Result<(), String> {
    let md = std::fs::symlink_metadata(target)
        .map_err(|e| format!("cannot stat target {}: {e}", target.display()))?;
    if md.file_type().is_symlink() || !md.is_dir() {
        return Err("target is a symlink or not a directory".into());
    }
    // Reject any symlinked entry inside the tree before invoking the library.
    fn scan(dir: &Path) -> Result<(), String> {
        for entry in std::fs::read_dir(dir).map_err(|e| e.to_string())? {
            let entry = entry.map_err(|e| e.to_string())?;
            if entry.file_type().map_err(|e| e.to_string())?.is_symlink() {
                return Err(format!("symlink inside target: {}", entry.path().display()));
            }
            if entry.file_type().map_err(|e| e.to_string())?.is_dir() {
                scan(&entry.path())?;
            }
        }
        Ok(())
    }
    scan(target)
}

Type guard

fn is_real_directory(target: &Path) -> bool {
    std::fs::symlink_metadata(target)
        .map(|md| md.is_dir() && !md.file_type().is_symlink())
        .unwrap_or(false)
}

Prevention

When it happens

Trigger: Calling `ensure_published_materialization` or `capture_bound_materialization` when the existing target directory fails `verify_published_materialization` (stale manifest/content) AND `verify_no_redirects` then detects a symlink or redirect entry inside the target directory tree.

Common situations: A developer or tool manually symlinked files inside the capsule cache directory to elsewhere (e.g. to shared build artifacts); a cache directory was partially replaced with links by a custom sync tool; a cache-shared volume or overlayfs setup introduces symlinked entries; a previous buggy version of the materializer wrote symlinks.

Related errors


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

Appendix: source

Thrown at crates/astrid-kernel/src/capsule_materialization.rs:197

        let target_metadata = match std::fs::symlink_metadata(target) {
            Ok(metadata) => Some(metadata),
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
            Err(error) => return Err(anyhow::anyhow!("inspect capsule materialization: {error}")),
        };
        if let Some(metadata) = target_metadata {
            if metadata.file_type().is_symlink() || !metadata.is_dir() {
                anyhow::bail!("capsule materialization target is redirected or not a directory");
            }
            if let Ok(bound_manifest) =
                astrid_capsule::discovery::load_manifest(&target.join("Capsule.toml"))
                && self
                    .verify_published_materialization(target, principal, &bound_manifest, snapshot)
                    .is_ok()
            {
                return Ok(bound_manifest);
            }
            astrid_core::platform_fs::verify_no_redirects(target).map_err(|error| {
                anyhow::anyhow!("capsule materialization target is redirected: {error}")
            })?;
            std::fs::remove_dir_all(target).map_err(|error| {
                anyhow::anyhow!("remove stale capsule materialization: {error}")
            })?;
        }
        astrid_capsule_install::materialize_capsule_package(snapshot.package(), target)
            .map_err(|error| anyhow::anyhow!("materialize durable capsule package: {error:#}"))?;
        let bound_manifest = astrid_capsule::discovery::load_manifest(&target.join("Capsule.toml"))
            .map_err(|error| anyhow::anyhow!(error))?;
        self.verify_published_materialization(target, principal, &bound_manifest, snapshot)?;
        Ok(bound_manifest)
    }

    /// Recheck the immutable publication after taking activation locks.
    #[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
    pub(crate) fn confirm_published_materialization(
        &self,
        dir: &Path,

View on GitHub (pinned to affd8760f4)