BloopAI/vibe-kanban · error · ContainerError
File {source_file:?} is outside project directory
Error message
File {source_file:?} is outside project directory What it means
copy_single_file canonicalizes both the source file and the source root and rejects any file whose canonical path is not inside the source root. This path-traversal guard prevents copying files outside the project directory via symlinks or '../' style paths. It protects the target from unintended files.
Source
Thrown at crates/local-deployment/src/copy.rs:86
tracing::warn!("Failed to copy file {:?}: {e}", entry.path());
}
}
}
Ok(())
}
fn copy_single_file(
source_file: &Path,
source_root: &Path,
target_root: &Path,
seen: &mut HashSet<PathBuf>,
) -> Result<bool, ContainerError> {
let canonical_source = source_root.canonicalize()?;
let canonical_file = source_file.canonicalize()?;
// Validate path is within source_dir
if !canonical_file.starts_with(canonical_source) {
return Err(ContainerError::Other(anyhow!(
"File {source_file:?} is outside project directory"
)));
}
if !seen.insert(canonical_file.clone()) {
return Ok(false);
}
let relative_path = source_file.strip_prefix(source_root).map_err(|e| {
ContainerError::Other(anyhow!(
"Failed to get relative path for {source_file:?}: {e}"
))
})?;
let target_file = target_root.join(relative_path);
if target_file.exists() {
return Ok(false);View on GitHub (pinned to 4deb7eca8f)
Solutions
- Remove out-of-project entries from the copy_files list; copy them explicitly to the target instead.
- Replace symlinks with real files inside the project, or copy the symlink target contents into the project first.
- Verify paths resolve within the project: run `realpath <file>` and compare against the project root before copying.
- If the file is legitimately needed, mount/copy it into the project directory beforehand.
Example fix
// before copy_files: vec!["/usr/local/bin/node".into()] // after copy_files: vec!["./vendor/bin/node".into()] // real file inside project
Defensive patterns
Strategy: validation
Validate before calling
let canonical_root = source_root.canonicalize()?;
for f in ©_files {
let p = source_root.join(f).canonicalize()?;
if !p.starts_with(&canonical_root) {
return Err(anyhow!("copy_files entry {:?} escapes project root", f));
}
} Type guard
fn is_inside(root: &Path, file: &Path) -> bool {
file.canonicalize().ok()
.zip(root.canonicalize().ok())
.map(|(f, r)| f.starts_with(r))
.unwrap_or(false)
} Try / catch
if let Err(ContainerError::Other(e)) = copy_project_files(...).await {
let msg = e.to_string();
if msg.contains("outside project directory") {
tracing::error!(%msg, "remove or inline the offending copy_files entry");
}
return Err(e);
} Prevention
- Never list symlinks pointing outside the repo in copy_files.
- Run `realpath` checks on copy list entries in CI.
- Resolve shared assets into the project tree before copying.
- Avoid '..' or absolute paths in copy_files entries.
When it happens
Trigger: copy_files contains an entry that resolves (after canonicalize) outside source_root: absolute paths elsewhere on disk, symlinked files pointing out of the project, or '../' relative entries.
Common situations: copy_files lists a symlink (e.g. node binaries, env files) pointing to /usr/... or $HOME; misconfigured copy list with typos like '../shared/config.json'; projects using symlinked asset directories outside the repo.
Related errors
- Failed to list projects (${res.status})
- result.error
- Server proof verification failed.
- Fallback response for "${table}" is not an object
- Fallback response missing "${table}" array
AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29).
Data as JSON: /api/errors/01e23f92db38d5c4.
Report an issue: GitHub.