{"record":{"id":"3a666eda6c7b5ffc","repo":"github/spec-kit","slug":"could-not-safely-create-download-file-exc","errorCode":null,"errorMessage":"Could not safely create download file: {exc}","messagePattern":"Could not safely create download file: (.+?)","errorType":"exception","errorClass":"ExtensionError","httpStatus":null,"severity":"error","filePath":"src/specify_cli/extensions/_commands.py","lineNumber":170,"sourceCode":"                response.geturl() if hasattr(response, \"geturl\") else download_url\n            )\n            content_type = (\n                response.getheader(\"Content-Type\")\n                if hasattr(response, \"getheader\")\n                else None\n            )\n    except urllib.error.URLError as exc:\n        raise ExtensionError(f\"Failed to download from {url}: {exc}\") from exc\n\n    download_fd = -1\n    download_file = None\n    try:\n        try:\n            download_fd = _safe_open_download_zip(\n                project_root, download_dir, archive_filename\n            )\n        except OSError as exc:\n            raise ExtensionError(\n                f\"Could not safely create download file: {exc}\"\n            ) from exc\n\n        try:\n            download_file = os.fdopen(download_fd, \"w+b\")\n            download_fd = -1\n            download_file.write(archive_data)\n            download_file.flush()\n            download_file.seek(0)\n        except OSError as exc:\n            raise ExtensionError(\n                f\"Could not safely write download file: {exc}\"\n            ) from exc\n\n        format_source = (\n            final_url\n            if archive_format_from_name(final_url) is not None\n            else url","sourceCodeStart":152,"sourceCodeEnd":188,"githubUrl":"https://github.com/github/spec-kit/blob/bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c/src/specify_cli/extensions/_commands.py#L152-L188","documentation":"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.","triggerScenarios":"`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.","commonSituations":"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.","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"],"exampleFix":"# before: .specify is read-only\nchmod -w .specify\nspecify extension install https://example.com/ext.zip  # fails\n\n# after\nchmod +w .specify\nspecify extension install https://example.com/ext.zip","handlingStrategy":"try-catch","validationCode":"import os\nfrom pathlib import Path\n\ndef cache_area_writable(project_root: Path) -> bool:\n    spec = project_root / '.specify'\n    if not spec.exists():\n        return os.access(project_root, os.W_OK)\n    return spec.is_dir() and not spec.is_symlink() and os.access(spec, os.W_OK)","typeGuard":null,"tryCatchPattern":"from specify_cli.extensions import ExtensionError\n\ntry:\n    install_from_url_cmd(project_root, url, speckit_version)\nexcept ExtensionError as e:\n    if 'Could not safely create download file' in str(e):\n        cause = e.__cause__  # OSError with errno: EACCES / ENOSPC / ENOTDIR ...\n        ...","preventionTips":["Pre-flight check that .specify exists as a real, writable directory","Keep the project on storage with headroom for the 50 MiB download cap"],"tags":["extensions","filesystem","permissions","download"],"backgroundTag":null,"analyzedSha":"bf88c9f9a82fa370c7a7257aa2b3cf10b457b65c","analyzedAt":"2026-08-14T19:43:37.150Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}