{"record":{"id":"6058021e4f4d26ab","repo":"astrid-runtime/astrid","slug":"directory-symlink-not-allowed-in-capsule-source","errorCode":null,"errorMessage":"directory symlink {} not allowed in capsule source tree (refusing to recurse — risk of cycles / cross-tree copies)","messagePattern":"directory symlink (.+?) not allowed in capsule source tree \\(refusing to recurse — risk of cycles / cross-tree copies\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/astrid-capsule-install/src/copy.rs","lineNumber":156,"sourceCode":"        bail!(\n            \"symlink {} resolves outside the capsule source root ({}); \\\n             refusing to copy (sandbox-escape vector)\",\n            src_path.display(),\n            resolved.display()\n        );\n    }\n\n    let resolved_meta = std::fs::metadata(&resolved)\n        .with_context(|| format!(\"stat resolved symlink target {}\", resolved.display()))?;\n\n    if resolved_meta.is_dir() {\n        // Directory symlinks open the door to (a) infinite recursion\n        // when the link points to an ancestor and (b) ballooning\n        // copies of legitimately-shared trees. `npm install` only\n        // produces FILE symlinks for `.bin/` entries; we don't need\n        // directory symlinks for any current capsule layout. Refuse\n        // them outright.\n        bail!(\n            \"directory symlink {} not allowed in capsule source tree \\\n             (refusing to recurse — risk of cycles / cross-tree copies)\",\n            src_path.display()\n        );\n    }\n\n    if is_wasm(src_path) {\n        // Same filter as the regular-file branch — `.wasm` lives in\n        // `bin/<hash>.wasm`, not in the per-capsule dir.\n        return Ok(());\n    }\n\n    // File symlink, target inside the root: copy the resolved\n    // bytes. `std::fs::copy` follows the link by default.\n    std::fs::copy(src_path, dst_path)\n        .with_context(|| format!(\"failed to copy {}\", src_path.display()))?;\n    Ok(())\n}","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-capsule-install/src/copy.rs#L138-L174","documentation":"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.","triggerScenarios":"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 -> ..","commonSituations":"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.","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."],"exampleFix":"// before\ndeps/shared -> /workspaces/shared\n// after\ncp -rL /workspaces/shared deps/shared","handlingStrategy":"validation","validationCode":"// pre-screen for directory symlinks before install:\nfn has_dir_symlinks(root: &Path) -> anyhow::Result<bool> {\n    for entry in walkdir::WalkDir::new(root).follow_links(false) {\n        let p = entry?.path().to_path_buf();\n        let md = fs::symlink_metadata(&p)?;\n        if md.file_type().is_symlink() && p.canonicalize()?.is_dir() { return Ok(true); }\n    }\n    Ok(false)\n}","typeGuard":null,"tryCatchPattern":"match copy_capsule_dir_inner(...) {\n    Err(e) if e.to_string().contains(\"directory symlink\") => {\n        eprintln!(\"replace directory symlinks with copies: cp -rL <target> <link-path>\");\n    }\n    other => other?,\n}","preventionTips":["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."],"tags":["rust","security","symlink","filesystem"],"backgroundTag":"path-traversal-blocked","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}