astrid-runtime/astrid · error

directory symlink {} not allowed in capsule source tree (ref

Error message

directory symlink {} not allowed in capsule source tree (refusing to recurse — risk of cycles / cross-tree copies)

What it means

handle_symlink refuses directory symlinks outright. Directory links risk infinite recursion (a link pointing at an ancestor) and ballooning copies of shared trees; the current capsule layout (mirroring npm's file-only .bin entries) never needs them, so the copy routine hard-fails instead of recursing.

Source

Thrown at crates/astrid-capsule-install/src/copy.rs:156

        bail!(
            "symlink {} resolves outside the capsule source root ({}); \
             refusing to copy (sandbox-escape vector)",
            src_path.display(),
            resolved.display()
        );
    }

    let resolved_meta = std::fs::metadata(&resolved)
        .with_context(|| format!("stat resolved symlink target {}", resolved.display()))?;

    if resolved_meta.is_dir() {
        // Directory symlinks open the door to (a) infinite recursion
        // when the link points to an ancestor and (b) ballooning
        // copies of legitimately-shared trees. `npm install` only
        // produces FILE symlinks for `.bin/` entries; we don't need
        // directory symlinks for any current capsule layout. Refuse
        // them outright.
        bail!(
            "directory symlink {} not allowed in capsule source tree \
             (refusing to recurse — risk of cycles / cross-tree copies)",
            src_path.display()
        );
    }

    if is_wasm(src_path) {
        // Same filter as the regular-file branch — `.wasm` lives in
        // `bin/<hash>.wasm`, not in the per-capsule dir.
        return Ok(());
    }

    // File symlink, target inside the root: copy the resolved
    // bytes. `std::fs::copy` follows the link by default.
    std::fs::copy(src_path, dst_path)
        .with_context(|| format!("failed to copy {}", src_path.display()))?;
    Ok(())
}

View on GitHub (pinned to affd8760f4)

Solutions

  1. Remove the directory symlink from the capsule source tree.
  2. Replace it by physically copying the directory's contents into the tree (e.g. `cp -rL`).
  3. Rebuild the capsule source with a packager that dereferences/copies directories rather than linking them.

Example fix

// before
deps/shared -> /workspaces/shared
// after
cp -rL /workspaces/shared deps/shared
Defensive patterns

Strategy: validation

Validate before calling

// pre-screen for directory symlinks before install:
fn has_dir_symlinks(root: &Path) -> anyhow::Result<bool> {
    for entry in walkdir::WalkDir::new(root).follow_links(false) {
        let p = entry?.path().to_path_buf();
        let md = fs::symlink_metadata(&p)?;
        if md.file_type().is_symlink() && p.canonicalize()?.is_dir() { return Ok(true); }
    }
    Ok(false)
}

Try / catch

match copy_capsule_dir_inner(...) {
    Err(e) if e.to_string().contains("directory symlink") => {
        eprintln!("replace directory symlinks with copies: cp -rL <target> <link-path>");
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling copy_capsule_dir_inner on a capsule source tree containing a symlink whose target is a directory — e.g. node_modules/foo -> ../../shared/foo, or a self/ancestor link like parent -> ..

Common situations: pnpm/yarn workspaces with directory symlinks in node_modules; a developer creating a dir link for convenience; vendoring a tree that was assembled with symlinks instead of copies.

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/6058021e4f4d26ab. Report an issue: GitHub.