pypa/pip · error · InstallationError

Invalid script entry point name

Error message

Invalid script entry point name {entry.name!r}: the script would be installed outside the scripts directory ({scripts_dir}).

What it means

Raised as InstallationError by _raise_for_invalid_entrypoint when a console_scripts entry point name contains path separators or '..' components such that os.path.join(scripts_dir, entry.name) resolves outside the scripts directory (or exactly to it). This is a path-traversal guard preventing a malicious/buggy wheel from writing a script outside the scripts dir.

Solutions

  1. Remove path separators and '..' from the console_scripts entry name.
  2. Rebuild the wheel with a plain, single-component entry name.
  3. Do not install wheels from untrusted sources that exhibit this.

Example fix

# before
[console_scripts]
../evil = pkg.mod:main

# after
[console_scripts]
evil = pkg.mod:main
Defensive patterns

Strategy: validation

Validate before calling

import os, re

def safe_entry_name(name: str) -> bool:
    # must be a single path component, no separators, no traversal
    return bool(re.fullmatch(r"[^/\\]+", name)) and not name.startswith(".")

# assert safe_entry_name("mycli")
# assert not safe_entry_name("../evil")

Prevention

When it happens

Trigger: entry_points.txt defines a name like '../bin/x', '/abs/path', or 'sub/dir/x'; is_within_directory(scripts_dir, dest) is false, so the install is aborted before distlib writes anything.

Common situations: Malicious wheel attempting directory escape; malformed build config producing entry-point names with slashes; non-standard packaging tools.

Related errors


AI-assisted analysis of pypa/pip@f399c37189 (2026-08-08). Data as JSON: /api/errors/5032ec375c6a120e. Report an issue: GitHub.

Appendix: source

Thrown at src/pip/_internal/operations/install/wheel.py:417

            "information."
        )


def _raise_for_invalid_entrypoint(specification: str, scripts_dir: str) -> None:
    entry = get_export_entry(specification)
    if entry is None:
        return

    if entry.suffix is None:
        raise MissingCallableSuffix(str(entry))

    # distlib joins the entry point name onto the scripts directory, so a name
    # with path separators or ``..`` components can resolve elsewhere. The script
    # must resolve to a path strictly inside the scripts directory.
    dest = os.path.join(scripts_dir, entry.name)
    resolves_to_scripts_dir = os.path.abspath(dest) == os.path.abspath(scripts_dir)
    if resolves_to_scripts_dir or not is_within_directory(scripts_dir, dest):
        raise InstallationError(
            f"Invalid script entry point name {entry.name!r}: the script "
            f"would be installed outside the scripts directory ({scripts_dir})."
        )


class PipScriptMaker(ScriptMaker):
    # Override distlib's default script template with one that
    # doesn't import `re` module, allowing scripts to load faster.
    script_template = textwrap.dedent("""\
        import sys
        from %(module)s import %(import_name)s
        if __name__ == '__main__':
            sys.argv[0] = sys.argv[0].removesuffix('.exe')
            sys.exit(%(func)s())
""")

    def make(
        self, specification: str, options: dict[str, Any] | None = None

View on GitHub (pinned to f399c37189)