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
- Remove the directory symlink from the capsule source tree.
- Replace it by physically copying the directory's contents into the tree (e.g. `cp -rL`).
- 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
- Assemble capsule sources with `cp -rL` so links are dereferenced.
- Avoid pnpm/workspace-style directory links in vendored trees.
- Audit source trees for symlinks (`find . -type l`) before packaging.
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
- legacy state source is redirected: {}
- legacy state source contains a redirect: {}
- leftover capsule authority receipt is not a regular file: {}
- legacy capsule authority root is not a regular directory: {}
- legacy capsule authority root contains a non-regular entry:
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/6058021e4f4d26ab.
Report an issue: GitHub.