kovidgoyal/kitty · error · TransmissionError

EISDIR

EISDIR

Error message

Cannot write data to a directory entry

What it means

DestFile.write_data refuses to append payload bytes to an entry whose ftype is FileType.directory, because directories have no file content. It raises TransmissionError with code EISDIR, mirroring the OS errno.

Source

Thrown at kitty/file_transmission.py:515

            else:
                os.chmod(self.name, self.permissions)
        if self.mtime != FileTransmissionCommand.mtime:
            if is_symlink:
                with suppress(NotImplementedError):
                    os.utime(self.name, ns=(self.mtime, self.mtime), follow_symlinks=False)
            else:
                os.utime(self.name, ns=(self.mtime, self.mtime))

    def unlink_existing_if_needed(self, force: bool = False) -> None:
        if force or self.needs_unlink:
            with suppress(FileNotFoundError):
                os.unlink(self.name)
            self.existing_stat = None
            self.needs_unlink = False

    def write_data(self, all_files: dict[str, 'DestFile'], data: bytes | memoryview, is_last: bool) -> None:
        if self.ftype is FileType.directory:
            raise TransmissionError(code=ErrorCode.EISDIR, file_id=self.file_id, msg='Cannot write data to a directory entry')
        if self.closed:
            raise TransmissionError(file_id=self.file_id, msg='Cannot write to a closed file')
        if self.ftype in (FileType.symlink, FileType.link):
            self.link_target += data
            self.bytes_written += len(data)
            if is_last:
                lt = self.link_target.decode('utf-8', 'replace')
                base = self.make_parent_dirs()
                self.unlink_existing_if_needed(force=True)
                if lt.startswith('fid:'):
                    lt = all_files[lt[4:]].name
                    if self.ftype is FileType.symlink:
                        lt = os.path.relpath(lt, os.path.dirname(self.name))
                elif lt.startswith('fid_abs:'):
                    lt = all_files[lt[8:]].name
                elif lt.startswith('path:'):
                    lt = lt[5:]
                    if not os.path.isabs(lt) and self.ftype is FileType.link:

View on GitHub (pinned to 6d5d0c4406)

Solutions

  1. Fix the sender to skip data actions for directory entries (only send file/end_data for regular files).
  2. If hand-writing the protocol, ensure action=data is only used after a file-type start with content.

Example fix

# before
if spec.is_dir:
    pass  # still falls through to send data
# after
if spec.is_dir:
    continue  # no data frames for directories
Defensive patterns

Strategy: validation

Validate before calling

if entry.ftype is FileType.directory:
    return  # skip data phase for dirs

Type guard

def accepts_data(ftype) -> bool:
    return ftype is not FileType.directory

Try / catch

try:
    add_data(ftc)
except TransmissionError as e:
    if e.code is ErrorCode.EISDIR: log.warning('ignoring data for directory %s', e.file_id)
    else: raise

Prevention

When it happens

Trigger: A peer sends an 'action=data' (or end_data) command for a file_id previously started with a directory entry — e.g. buggy sender that always streams data even for dir entries.

Common situations: Directory-transfer senders that emit an empty data frame for every spec including directories; version mismatch between sender/receiver implementations; hand-crafted test sequences.

Related errors


AI-assisted analysis of kovidgoyal/kitty@6d5d0c4406 (2026-08-27). Data as JSON: /api/errors/51ff3ed0b3944ca2. Report an issue: GitHub.