kovidgoyal/kitty · error · TransmissionError

EINVAL

EINVAL

Error message

Cannot send a directory

What it means

SourceFile.__init__ stats the outgoing path; if it is a directory, transmission is refused with TransmissionError(EINVAL, 'Cannot send a directory'). The file-transfer send protocol handles individual files/symlinks, not recursive directory sends, at this level.

Source

Thrown at kitty/file_transmission.py:659

    def commit(self, send_os_error: Callable[[OSError, str, 'ActiveReceive', str], None]) -> None:
        directories = sorted((df for df in self.files.values() if df.ftype is FileType.directory), key=lambda x: len(x.name), reverse=True)
        for df in directories:
            with suppress(OSError):
                # we ignore failures to apply directory metadata as we have already sent an OK for the dir
                df.apply_metadata()


class SourceFile:
    def __init__(self, ftc: FileTransmissionCommand):
        self.file_id = ftc.file_id
        self.path = ftc.name
        self.ttype = ftc.ttype
        self.waiting_for_signature = True if self.ttype is TransmissionType.rsync else False
        self.transmitted = False
        self.stat = os.stat(self.path, follow_symlinks=False)
        if stat.S_ISDIR(self.stat.st_mode):
            raise TransmissionError(ErrorCode.EINVAL, msg='Cannot send a directory', file_id=self.file_id)
        self.compressor: ZlibCompressor | IdentityCompressor = IdentityCompressor()
        self.target = b''
        self.open_file: io.BufferedReader | None = None
        if stat.S_ISLNK(self.stat.st_mode):
            self.target = os.readlink(self.path).encode('utf-8')
        else:
            self.open_file = open(self.path, 'rb')
            if ftc.compression is Compression.zlib:
                self.compressor = ZlibCompressor()
        from kittens.transfer import rsync

        self.differ = rsync.Differ() if self.waiting_for_signature else None
        self.buf = bytearray()
        self.write_pos = 0

    def write(self, b: ReadableBuffer) -> None:
        self.buf[self.write_pos : self.write_pos + len(b)] = b
        self.write_pos += len(b)

View on GitHub (pinned to 6d5d0c4406)

Solutions

  1. Check os.path.isdir before enqueueing and expand to individual files (os.walk/scandir).
  2. Verify glob expansion in the calling script produced file paths.

Example fix

# before
send(path)
# after
if os.path.isdir(path):
    for root, _, files in os.walk(path):
        for f in files:
            send(os.path.join(root, f))
else:
    send(path)
Defensive patterns

Strategy: type-guard

Validate before calling

import os
assert not os.path.isdir(path), f'cannot send directory: {path}'

Type guard

def is_sendable_file(path: str) -> bool:
    import os
    return not os.path.isdir(path)

Try / catch

try:
    SourceFile(ftc)
except TransmissionError as e:
    if e.code is ErrorCode.EINVAL and 'directory' in e.msg: expand_and_retry(path)
    else: raise

Prevention

When it happens

Trigger: Constructing a SourceFile (via the send command handler) whose ftc.name resolves to a directory — e.g. passing a dir path where a file path is expected.

Common situations: Shell glob failing to expand so the literal dir path is passed; scripts that forget isdir checks; users trying to 'kitten transfer' a folder.

Related errors


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