github/spec-kit · error · ExtensionError

Could not safely create download file: {exc}

Error message

Could not safely create download file: {exc}

What it means

Before writing downloaded bytes, the installer opens the transient cache file via _safe_open_download_zip (O_CREAT|O_EXCL, symlink-refusing, under the validated cache dir). An OSError from that open — cache-dir validation failure, permission denied, disk full, EDQUOT, or the path already existing — is wrapped as 'Could not safely create download file'. The ENOTDIR/ELOOP security sub-cases (277-279) also surface here.

Source

Thrown at src/specify_cli/extensions/_commands.py:170

                response.geturl() if hasattr(response, "geturl") else download_url
            )
            content_type = (
                response.getheader("Content-Type")
                if hasattr(response, "getheader")
                else None
            )
    except urllib.error.URLError as exc:
        raise ExtensionError(f"Failed to download from {url}: {exc}") from exc

    download_fd = -1
    download_file = None
    try:
        try:
            download_fd = _safe_open_download_zip(
                project_root, download_dir, archive_filename
            )
        except OSError as exc:
            raise ExtensionError(
                f"Could not safely create download file: {exc}"
            ) from exc

        try:
            download_file = os.fdopen(download_fd, "w+b")
            download_fd = -1
            download_file.write(archive_data)
            download_file.flush()
            download_file.seek(0)
        except OSError as exc:
            raise ExtensionError(
                f"Could not safely write download file: {exc}"
            ) from exc

        format_source = (
            final_url
            if archive_format_from_name(final_url) is not None
            else url

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Check the underlying errno in the chained exception (`__cause__`) — it distinguishes permission vs ENOTDIR validation vs ENOENT
  2. Ensure the project tree is writable: `ls -ld .specify .specify/extension-catalogs*` and fix ownership/permissions
  3. Free disk space if errno is ENOSPC/EDQUOT
  4. If a symlinked cache component is the cause (ENOTDIR/ELOOP in the cause chain), remove the symlink and let the tool recreate a real directory

Example fix

# before: .specify is read-only
chmod -w .specify
specify extension install https://example.com/ext.zip  # fails

# after
chmod +w .specify
specify extension install https://example.com/ext.zip
Defensive patterns

Strategy: try-catch

Validate before calling

import os
from pathlib import Path

def cache_area_writable(project_root: Path) -> bool:
    spec = project_root / '.specify'
    if not spec.exists():
        return os.access(project_root, os.W_OK)
    return spec.is_dir() and not spec.is_symlink() and os.access(spec, os.W_OK)

Try / catch

from specify_cli.extensions import ExtensionError

try:
    install_from_url_cmd(project_root, url, speckit_version)
except ExtensionError as e:
    if 'Could not safely create download file' in str(e):
        cause = e.__cause__  # OSError with errno: EACCES / ENOSPC / ENOTDIR ...
        ...

Prevention

When it happens

Trigger: `specify extension install <url>` when .specify/extension-catalogs cache/download dir can't be created or opened: read-only project root, unwritable .specify, existing stray file colliding with the O_EXCL create, or a symlinked cache path tripping validation.

Common situations: Project root owned by another user (running as non-owner); .specify made read-only by tooling; disk full in CI; leftover artifacts from a killed previous download; container with a read-only bind mount.

Related errors


AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14). Data as JSON: /api/errors/3a666eda6c7b5ffc. Report an issue: GitHub.