ComposioHQ/composio · warning · UnsafePathComponentError

Refusing to write {label} ending in a space or dot: {name!r}

Error message

Refusing to write {label} ending in a space or dot: {name!r}

What it means

safe_basename rejects filenames ending in a space or a dot ('file ', 'file.'). Windows silently strips trailing spaces/dots, which breaks round-tripping and can collide with other names, so such basenames are refused up front.

Source

Thrown at python/composio/utils/safe_path.py:186

        )

    raw_basename = PureWindowsPath(name).name
    if not raw_basename or not raw_basename.strip() or set(raw_basename) == {"."}:
        raise UnsafePathComponentError(
            f"Path traversal detected: {label} {name!r} leaves no usable "
            "basename to write to."
        )
    if "\x00" in raw_basename:
        raise UnsafePathComponentError(
            f"Refusing to write {label} containing a NUL byte: {name!r}"
        )
    if any(ord(char) < 32 or char in '<>:"|?*' for char in raw_basename):
        raise UnsafePathComponentError(
            f"Refusing to write {label} containing characters reserved by "
            f"Windows: {name!r}"
        )
    if raw_basename.endswith((" ", ".")):
        raise UnsafePathComponentError(
            f"Refusing to write {label} ending in a space or dot: {name!r}"
        )

    basename = raw_basename.strip()
    try:
        encoded_length = len(os.fsencode(basename))
    except UnicodeEncodeError as e:
        raise UnsafePathComponentError(
            f"Refusing to write {label} containing invalid Unicode: {name!r}"
        ) from e
    if encoded_length > MAX_COMPONENT_LENGTH:
        raise UnsafePathComponentError(
            f"Refusing to write {label} longer than {MAX_COMPONENT_LENGTH} bytes: "
            f"{basename[:32]!r}... ({encoded_length} bytes)"
        )
    # Compare everything before the first dot: on Windows `NUL.tar.gz` opens
    # the null device just as `NUL` does, so any number of extensions provides
    # no protection.

View on GitHub (pinned to 64b1b85502)

Solutions

  1. Strip trailing spaces and dots from the filename before passing it in (note the SDK itself only strips for internal checks, the write uses raw basename)
  2. Generate a normalized filename

Example fix

# before
secure_basename_join(base, 'data.')
# after
secure_basename_join(base, 'data.'.rstrip(' .'))
Defensive patterns

Strategy: validation

Validate before calling

def no_trailing_space_dot(v):
    return isinstance(v, str) and not v.rstrip().rstrip('.').endswith((' ', '.')) and not PureWindowsPath(v).name.endswith((' ', '.'))

Type guard

from pathlib import PureWindowsPath
def is_trimmed_name(v: str) -> bool:
    return not PureWindowsPath(v).name.endswith((' ', '.'))

Try / catch

from composio.exceptions import UnsafePathComponentError
try:
    p = secure_basename_join(base, name)
except UnsafePathComponentError:
    p = secure_basename_join(base, name.rstrip(' .'))

Prevention

When it happens

Trigger: secure_basename_join(base, name) where the extracted basename ends with ' ' or '.' — e.g. 'my report ' or 'data.'.

Common situations: Copy-pasted filenames with invisible trailing whitespace; API-supplied names with trailing dots from truncated extensions.

Related errors


AI-assisted analysis of ComposioHQ/composio@64b1b85502 (2026-08-28). Data as JSON: /api/errors/b267492be1583090. Report an issue: GitHub.