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 urlView on GitHub (pinned to bf88c9f9a8)
Solutions
- Check the underlying errno in the chained exception (`__cause__`) — it distinguishes permission vs ENOTDIR validation vs ENOENT
- Ensure the project tree is writable: `ls -ld .specify .specify/extension-catalogs*` and fix ownership/permissions
- Free disk space if errno is ENOSPC/EDQUOT
- 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
- Pre-flight check that .specify exists as a real, writable directory
- Keep the project on storage with headroom for the 50 MiB download cap
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
- Could not read manifest {path}: {e}
- Could not safely write download file: {exc}
- {exc}
- Directory not found: {source_path}
- No extension.yml found in {source_path}
AI-assisted analysis of github/spec-kit@bf88c9f9a8 (2026-08-14).
Data as JSON: /api/errors/3a666eda6c7b5ffc.
Report an issue: GitHub.