github/spec-kit · error · PresetError

Failed to save preset archive: {e}

Error message

Failed to save preset archive: {e}

What it means

The archive bytes downloaded but could not be persisted locally: an IOError/OSError (excluding URLError, caught earlier) occurred while writing the staging file or during the atomic os.replace into the cache. Raised as PresetError; the finally block unlinks the partial staging file so no corrupt archive remains.

Source

Thrown at src/specify_cli/presets/__init__.py:4959

            )
            archive_path = build_safe_download_path(
                target_dir,
                pack_id,
                version,
                error_type=PresetError,
                label="preset",
                suffix=archive_suffix(archive_format),
            )
            os.replace(staging_path, archive_path)
            staging_path = None
            return archive_path

        except urllib.error.URLError as e:
            raise PresetError(
                f"Failed to download preset from {download_url}: {e}"
            )
        except IOError as e:
            raise PresetError(f"Failed to save preset archive: {e}")
        finally:
            if staging_path is not None:
                staging_path.unlink(missing_ok=True)

    def clear_cache(self):
        """Clear all catalog cache files, including per-URL hashed caches."""
        if self.cache_dir.exists():
            for f in self.cache_dir.iterdir():
                if f.is_file() and f.name.startswith("catalog"):
                    f.unlink(missing_ok=True)


class PresetResolver:
    """Resolves template names to file paths using a priority stack.

    Resolution order:
    1. .specify/templates/overrides/          - Project-local overrides
    2. .specify/presets/<preset-id>/          - Installed presets

View on GitHub (pinned to bf88c9f9a8)

Solutions

  1. Check free disk space and expand/clean the cache partition
  2. Fix ownership/permissions of the preset cache directory (often ~/.cache/specify or the configured cache_dir)
  3. Pass a writable target_dir to download_preset_archive if the default cache path is restricted
  4. Remove stale root-owned archive files blocking os.replace

Example fix

# before
archive = manager.download_preset_archive(pack_id)  # default cache dir read-only

# after
archive = manager.download_preset_archive(pack_id, target_dir="/tmp/writable-downloads")
Defensive patterns

Strategy: try-catch

Validate before calling

import shutil
target = pathlib.Path(target_dir or manager.cache_dir / "downloads")
writable = target.exists() and os.access(target, os.W_OK)
free_ok = shutil.disk_usage(target).free > 100 * 1024 * 1024  # 100MB headroom

Try / catch

try:
    archive = manager.download_preset_archive(pack_id)
except PresetError as e:
    if "Failed to save preset archive" in str(e):
        archive = manager.download_preset_archive(pack_id, target_dir=tempfile.mkdtemp())
    else:
        raise

Prevention

When it happens

Trigger: Cache/download directory is read-only or out of disk space; permissions missing on the cache dir; target path on a filesystem that fails the rename (cross-device or immutable file).

Common situations: Running with a restricted HOME or XDG cache; CI container with a small tmpfs filling up; cache dir owned by root after a previous sudo run; antivirus/locking interfering with os.replace on some platforms.

Related errors


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