Skip to content

Uncaught ValueError in path_in_repo()/is_dirty() when an edit block references an absolute path (session crash) #5620

Description

@aniruddhaadak80

Issue

Summary

GitRepo.path_in_repo() and GitRepo.is_dirty(path) call normalize_path() without catching ValueError, so any filename that resolves outside the repo root lexically crashes aider with an unhandled traceback. The same normalize_path() call is already guarded with try/except ValueError in ignored_file_raw() (added for issue #1524), but these two sibling call sites were missed.

This matters because the path reaching these functions comes from LLM-generated edit blocks: when the model emits an edit header with an absolute filename (C:\project\module.py, /etc/hosts, ...), BaseCoder.allowed_to_edit() passes it straight into repo.path_in_repo(), and check_for_dirty_commit() passes it into repo.is_dirty(). One hallucinated absolute path in a SEARCH/REPLACE block then kills the whole session mid-edit-application instead of just skipping that hunk.

Affected code

aider/repo.py:

def normalize_path(self, path):
    orig_path = path
    res = self.normalized_path.get(orig_path)
    if res:
        return res

    path = str(Path(PurePosixPath((Path(self.root) / path).relative_to(self.root))))
    self.normalized_path[orig_path] = path
    return path

(Path(self.root) / absolute_or_foreign_path) discards the root, and .relative_to(self.root) then raises:

ValueError: 'C:\\Windows\\win.ini' is not in the subpath of 'C:\\project'

Unguarded callers:

# repo.py:567-575
def path_in_repo(self, path):
    ...
    normalized = self.normalize_path(path)          # <-- unguarded
    return normalized in tracked_files

# repo.py:598-601
def is_dirty(self, path=None):
    if path and not self.path_in_repo(path):        # <-- propagates
        return False

Guarded counterpart (proof this class of input is expected):

# repo.py:542-563
def ignored_file_raw(self, fname):
    if self.subtree_only:
        try:
            fname_path = Path(self.normalize_path(fname))
            ...
        except ValueError:
            # Issue #1524 ...
            return True
    ...
    try:
        fname = self.normalize_path(fname)
    except ValueError:
        return True

LLM-controlled entry points:

  • aider/coders/base_coder.py:2191-2199allowed_to_edit(path)self.repo.path_in_repo(path)
  • aider/coders/base_coder.py:2175-2181check_for_dirty_commit(path)self.repo.is_dirty(path)

Minimal reproduction

Against current main (Python 3.12, Windows):

import subprocess, tempfile
from pathlib import Path
import git
from aider.io import InputOutput
from aider.repo import GitRepo

base = Path(tempfile.mkdtemp()).resolve()
subprocess.run(["git", "init"], cwd=base, capture_output=True)
repo = GitRepo(InputOutput(pretty=False, yes=True), None, str(base))

print(repo.path_in_repo(r"C:\Windows\win.ini"))

Result:

CRASH ValueError: 'C:\\Windows\\win.ini' is not in the subpath of
'C:\\Users\\...\\Temp\\aider_norm3_...'

End-to-end trigger: ask a model to make an edit where the response block header uses an absolute path (models do this occasionally, especially when discussing files outside the chat). With --yes-always there is no interactive step that would otherwise stop the flow before path_in_repo() runs.

Note that relative parent-paths (src/../file.py) do not trigger it (they remain lexically under root after the join), which is why this only shows up for absolute or aliased paths — easy to miss in review.

Expected behavior

A filename that isn't inside the repo should be treated like the existing #1524 handling: skip the edit with a warning ("skipping edits to outside the repo"), not crash the session.

Suggested fix

Wrap the normalize_path() calls in path_in_repo() (returning False) and mirror the guard used by ignored_file_raw(), or catch ValueError once in allowed_to_edit() around both checks. Happy to send a PR either way.

Version and model info

  • Aider v0.86.3.dev53+g5dc9490bb (main @ 5dc9490bb)
  • Python 3.12.10, Windows 11 (repro is platform-independent)
  • Bug present regardless of model; any edit format that uses filenames in blocks (diff / whole / udiff)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions