astrid-runtime/astrid · error
legacy capsule contains symlink
Error message
legacy capsule contains symlink {} What it means
Raised by copy_legacy_tree while walking a legacy capsule directory during canonical_legacy_archive: an entry is a symlink. The library refuses to copy symlinks into the canonical legacy archive to keep archives deterministic and free of links that could point outside the tree.
Solutions
- Find and remove the symlink (the message names its path): replace it with a real directory or a copy of the target content.
- Re-copy the capsule tree with symlink dereferencing (e.g. `cp -rL`) to materialize targets as regular files, then retry the migration.
- If the link was intentional for out-of-tree content, move the content physically into the capsule directory and link nothing.
Example fix
# before: node_modules points elsewhere node_modules -> /shared/deps # after: materialize the content as real files cp -rL /shared/deps node_modules
Defensive patterns
Strategy: validation
Validate before calling
fn has_symlinks(dir: &Path) -> std::io::Result<bool> {
for e in walkdir(dir) {
if e.file_type().is_symlink() { return Ok(true); }
}
Ok(false)
} Type guard
fn is_regular_entry(md: &std::fs::Metadata) -> bool {
md.is_file() || md.is_dir()
} Try / catch
if let Err(e) = migrate(...) {
if e.to_string().contains("contains symlink") {
materialize_symlinks(&capsule_dir)?; // cp -rL equivalent
migrate(...)?;
}
} Prevention
- Don't replace dependency folders with symlinks
- Use `cp -rL` when copying capsule trees
- Check trees for links with `find . -type l` before migrating
When it happens
Trigger: Migrating a legacy capsule whose directory tree contains a symlink anywhere under it; copy_legacy_tree recurses via canonical_legacy_archive and also calls itself, so a symlink at any depth triggers it.
Common situations: Users replaced large dependency folders with symlinks to save space; node_modules-style links present in legacy layouts; Capsule trees checked out on filesystems or via tools that create links; restored backups that turned directories into 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
- capsule source symlink
- capsule materialization destination is not a directory
- capsule projection contains a symbolic link
- capsule source contains unsupported special file
- capsule source symlink
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/d6e084e0bbf1d060.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-capsule-install/src/storage.rs:700
let destination = staging.path().join("wit").join(relative);
if let Some(parent) = destination.parent() {
fs::create_dir_all(parent)?;
}
fs::copy(&source, &destination)
.with_context(|| format!("restore content-addressed WIT blob {hash}"))?;
}
canonical_capsule_archive(staging.path())
}
fn copy_legacy_tree(source: &Path, destination: &Path) -> anyhow::Result<()> {
fs::create_dir_all(destination)?;
for (path, metadata) in read_dir_sorted(source)? {
let relative = path
.file_name()
.ok_or_else(|| anyhow::anyhow!("legacy capsule entry has no name"))?;
let destination = destination.join(relative);
if metadata.file_type().is_symlink() {
bail!("legacy capsule contains symlink {}", path.display());
}
if metadata.is_dir() {
copy_legacy_tree(&path, &destination)?;
} else if metadata.is_file() {
let name = path.file_name().and_then(|name| name.to_str());
if matches!(name, Some("meta.json" | "authority.json" | ".env.json")) {
continue;
}
fs::copy(path, destination)?;
} else {
bail!("legacy capsule contains special file {}", path.display());
}
}
Ok(())
}
fn manifest_identity(source_dir: &Path) -> anyhow::Result<(String, String)> {
let manifest = fs::read_to_string(source_dir.join("Capsule.toml"))View on GitHub (pinned to affd8760f4)