langchain-ai/deepagents · error · OSError

debug log directory is not a real directory: {path}

Error message

debug log directory is not a real directory: {path}

What it means

_prepare_debug_directory creates the per-thread debug log directory with mode 0o700 and then verifies it is a genuine directory and not a symlink (Windows check via lstat). If lstat reveals the path is a symlink or not a directory, the library refuses to write debug logs there to prevent symlink attacks on debug output. This is a security hardening error, not a normal usage failure.

Source

Thrown at libs/code/deepagents_code/_debug.py:302

    valid = ", ".join(LOG_LEVELS)
    message = f"ignoring invalid {LOG_LEVEL}={raw!r}; expected one of {valid}"
    _warn(message)
    return fallback


def _prepare_debug_directory(path: Path) -> None:
    """Create or tighten the debug directory to owner-only access.

    Raises:
        OSError: If the directory cannot be created, opened, or tightened.
    """
    with contextlib.suppress(FileExistsError):
        path.mkdir(mode=0o700)
    if os.name == "nt":
        metadata = path.lstat()
        if stat.S_ISLNK(metadata.st_mode) or not stat.S_ISDIR(metadata.st_mode):
            msg = f"debug log directory is not a real directory: {path}"
            raise OSError(msg)
        _set_windows_owner_only_dacl(path)
        return
    flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0)
    fd = os.open(path, flags)
    try:
        metadata = os.fstat(fd)
        if metadata.st_uid != os.geteuid():
            msg = f"debug log directory is not owned by the current user: {path}"
            raise OSError(msg)
        os.fchmod(fd, 0o700)
    finally:
        os.close(fd)


def _thread_log_name(thread_id: str) -> str:
    """Return a traversal-safe log filename for a thread identifier."""
    if (
        len(thread_id) <= _MAX_THREAD_FILENAME_LENGTH

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Delete the path at the reported location (it is a symlink or a file, not a real directory) and let the library recreate it.
  2. Ensure only trusted processes can write to the parent directory where the debug log dir is created.
  3. Retry after removing the bad entry; the next bind_debug_logging_to_thread call will recreate it with mode 0o700.

Example fix

// before (path is a symlink)
debug/logs -> /somewhere/else
// after (remove and recreate)
rm debug/logs
mkdir debug/logs
Defensive patterns

Strategy: validation

Validate before calling

import os, stat
p = Path(debug_dir)
if p.is_symlink() or (p.exists() and not p.is_dir()):
    p.unlink()  # remove bad entry before binding debug logging

Type guard

def is_real_directory(p: Path) -> bool:
    try:
        return not p.is_symlink() and stat.S_ISDIR(p.lstat().st_mode)
    except OSError:
        return False

Try / catch

try:
    bind_debug_logging_to_thread(thread_id)
except OSError as exc:
    if 'not a real directory' in str(exc):
        shutil.rmtree(path, ignore_errors=True) or os.remove(path)
        bind_debug_logging_to_thread(thread_id)  # retry once

Prevention

When it happens

Trigger: On Windows only: an attacker (or leftover artifact) replaced the debug log path with a symlink or a non-directory file between mkdir and the lstat check, and bind_debug_logging_to_thread was called.

Common situations: A stale symlink left at the debug directory location from a previous setup; a temp-cleanup tool replaced the directory with a junction; another process races to substitute a symlink at the path.

Related errors


AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29). Data as JSON: /api/errors/b2e7f32206fb5e43. Report an issue: GitHub.