gitbutlerapp/gitbutler · error

Path to read from at '{}' isn't in the worktree directory '{

Error message

Path to read from at '{}' isn't in the worktree directory '{}'

What it means

Thrown by read_file_from_workspace when the requested path, after both it and the worktree root are canonicalized with gix::path::realpath, no longer lies inside the worktree. The double-check exists because the input may be absolute or travel through symlinks; realpath resolves symlinks, so a link inside the worktree pointing outside is caught. This is a sandbox guard preventing reads of arbitrary files through the workspace API.

Source

Thrown at crates/gitbutler-repo/src/commands.rs:308

            Some(entry) => {
                let blob = repo.find_blob(entry.id())?;
                FileInfo::from_content(relative_path, &blob.data)
            }
            None => FileInfo::deleted(),
        })
    }

    /// Note that `path` can be relative or absolute, and we must validate that it's in the worktree.
    fn read_file_from_workspace(&self, path: &Path) -> Result<FileInfo> {
        let workdir = self.workdir_or_fail()?;
        let canonical_workdir = gix::path::realpath(&workdir)?;
        let path = gix::path::realpath(canonical_workdir.join(path))?;
        // Double-check that the path is still in the worktree - this might not be the case
        // if it was aboslute to begin with, or leads through symlinks.
        let relative_path = match path.strip_prefix(&canonical_workdir) {
            Ok(relative_path) => relative_path.to_owned(),
            Err(_) => {
                bail!(
                    "Path to read from at '{}' isn't in the worktree directory '{}'",
                    path.display(),
                    canonical_workdir.display()
                );
            }
        };

        let out = match path.symlink_metadata() {
            Ok(md) => {
                if md.is_file() {
                    let content = std::fs::read(&path)?;
                    FileInfo::from_content(&relative_path, &content)
                } else if md.is_symlink() {
                    let content = std::fs::read_link(&path)?;
                    FileInfo::utf8_text_or_binary(&relative_path, &gix::path::into_bstr(content))
                } else if md.is_dir() {
                    // Directories on disk (notably git submodules, which appear
                    // as real directories in the worktree but are represented

View on GitHub (pinned to caf1f223d3)

Solutions

  1. Pass paths relative to the worktree root and let the API join them onto the workdir
  2. Remove or relocate symlinks that resolve outside the worktree
  3. If you legitimately need a file outside the worktree, read it directly with std::fs instead of this API

Example fix

// before: read_file_from_workspace(Path::new("/etc/hosts"))
// after: read only files inside the worktree, by relative path
read_file_from_workspace(Path::new("src/main.rs"))?;
Defensive patterns

Strategy: validation

Validate before calling

let workdir = gix::path::realpath(repo.workdir())?;
let candidate = gix::path::realpath(workdir.join(path))?;
if candidate.strip_prefix(&workdir).is_err() {
    // would escape the worktree (symlink or absolute path) - reject before calling
}

Prevention

When it happens

Trigger: Calling read_file_from_workspace(path) where path is absolute and outside the worktree, contains '..' components that escape after canonicalization, or traverses a symlink inside the worktree that resolves to a location outside it - strip_prefix(canonical_workdir) then fails.

Common situations: Monorepos where build tooling symlinks node_modules or cache directories outside the tree; user- or agent-supplied absolute paths passed unfiltered; worktrees checked out under symlinked directories (macOS /tmp -> /private/tmp style) making prefix comparison fail.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20). Data as JSON: /api/errors/6d8443b1a3ea0b7b. Report an issue: GitHub.