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

  1. Remove out-of-project entries from the copy_files list; copy them explicitly to the target instead.
  2. Replace symlinks with real files inside the project, or copy the symlink target contents into the project first.
  3. Verify paths resolve within the project: run `realpath <file>` and compare against the project root before copying.
  4. 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 &copy_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

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


AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29). Data as JSON: /api/errors/01e23f92db38d5c4. Report an issue: GitHub.