NousResearch/hermes-agent · error · OSError

Cannot edit non-file path: {path}

Error message

Cannot edit non-file path: {path}

What it means

Raised by _read_text_if_exists in the ACP adapter's edit-approval layer (VS Code/Zed/JetBrains integration). When building an EditProposal for an incoming write_file/patch call, it reads the current file content to show the user a before/after diff. If the path exists but is not a regular file (directory, fifo/socket, broken symlink to a special file), it raises OSError because there is no text to diff against. This is a guard: edit approvals only make sense for real files.

Source

Thrown at acp_adapter/edit_approval.py:78

    _EDIT_APPROVAL_REQUESTER.reset(token)


def clear_edit_approval_requester() -> None:
    """Clear the current requester; primarily used by tests."""

    _EDIT_APPROVAL_REQUESTER.set(None)


def get_edit_approval_requester() -> EditApprovalRequester | None:
    return _EDIT_APPROVAL_REQUESTER.get()


def _read_text_if_exists(path: str) -> str | None:
    p = Path(path).expanduser()
    if not p.exists():
        return None
    if not p.is_file():
        raise OSError(f"Cannot edit non-file path: {path}")
    return p.read_text(encoding="utf-8", errors="replace")


def _proposal_for_write_file(arguments: dict[str, Any]) -> EditProposal:
    path = str(arguments.get("path") or "")
    if not path:
        raise ValueError("path required")
    content = arguments.get("content")
    if content is None:
        raise ValueError("content required")
    return EditProposal(
        tool_name="write_file",
        path=path,
        old_text=_read_text_if_exists(path),
        new_text=str(content),
        arguments=dict(arguments),
    )

View on GitHub (pinned to c896c09c42)

Solutions

  1. Correct the caller to target a regular file path (strip trailing slashes, verify with ls -l).
  2. If patching a directory rename/deletion, handle those patch hunks separately instead of routing them through _read_text_if_exists.
  3. Pre-resolve symlinks and check Path.is_file() before issuing the edit from the client.
  4. In the ACP handler, catch OSError and return a structured 'invalid path' response to the editor instead of a stack trace.

Example fix

# before
p = Path(path).expanduser()
if not p.is_file():
    raise OSError(f"Cannot edit non-file path: {path}")

# after — validate in the proposal builder before touching the FS
p = Path(path).expanduser()
if p.is_dir():
    raise OSError(f"Cannot edit non-file path: {path}")
Defensive patterns

Strategy: type-guard

Validate before calling

from pathlib import Path
p = Path(path).expanduser()
if p.exists() and not p.is_file():
    return error_response(f"Refusing to edit non-file: {path}")  # before building a proposal

Type guard

def is_editable_file(path: str) -> bool:
    p = Path(path).expanduser()
    return p.is_file()

Try / catch

try:
    proposal = _build_proposal(tool_name, arguments)
except OSError as err:
    # return structured ACP error, not a stack trace
    return {"error": {"code": -32602, "message": str(err)}}

Prevention

When it happens

Trigger: An editor/agent sends a write_file or patch whose path points at a directory, /dev/null-like device, or named pipe; a symlink chain resolving to a directory; patch headers with a trailing-slash path ('src/dir/') parsed as a file edit.

Common situations: Diff/patch payloads that include directory rename or file-deletion entries being routed through the edit-approval builder; agent mistakenly targeting a folder path with write_file; tools that treat /dev/stdin or fifos as editable.

Related errors


AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14). Data as JSON: /api/errors/a8abdc3e93001be4. Report an issue: GitHub.