headroomlabs-ai/headroom · error · OSError
binary cache directory is not writable: {dest.parent}
Error message
binary cache directory is not writable: {dest.parent} What it means
After mkdir(parents=True, exist_ok=True) succeeds, _download re-checks with _is_writable_dir that the resulting directory actually accepts writes. This catches cases mkdir cannot: directories that exist but are root-owned with no write bit, filesystems mounted read-only after the fact, or permission changes racing the check. Raised as plain OSError naming dest.parent.
Source
Thrown at headroom/binaries.py:246
return url
# Only substitute the github.com host so that paths remain intact.
for prefix in ("https://github.com", "https://objects.githubusercontent.com"):
if url.startswith(prefix):
return mirror.rstrip("/") + url[len(prefix) :]
return url
# ---------- Download + verify --------------------------------------------- #
def _download(url: str, dest: Path, *, progress: bool = True) -> None:
if os.environ.get("HEADROOM_BINARIES_OFFLINE"):
raise OfflineError(f"offline mode (HEADROOM_BINARIES_OFFLINE=1) but fetch required: {url}")
if not _has_writable_existing_parent(dest.parent):
raise OSError(f"binary cache directory parent is not writable: {dest.parent}")
dest.parent.mkdir(parents=True, exist_ok=True)
if not _is_writable_dir(dest.parent):
raise OSError(f"binary cache directory is not writable: {dest.parent}")
final_url = _mirror_url(url)
req = urllib.request.Request(final_url, headers={"User-Agent": "headroom-binaries/1"})
attempts = 3
for attempt in range(1, attempts + 1):
try:
with urllib.request.urlopen(req, timeout=60) as resp: # noqa: S310 (https)
total = int(resp.headers.get("Content-Length") or 0)
_stream_to(resp, dest, total, label=dest.name, show_progress=progress)
return
except urllib.error.URLError as e:
dest.unlink(missing_ok=True)
if attempt == attempts:
raise BinaryFetchError(
f"failed to download {final_url} after {attempts} attempts: {e}"
) from e
# GitHub release assets occasionally return a transient 5xx or
# reset while redirecting to the object store. A short bounded
# retry keeps proxy startup reliable without hiding persistentView on GitHub (pinned to 322425c43b)
Solutions
- chmod/chown the cache directory for the runtime user: chown appuser <dir> && chmod u+w <dir>.
- Point the cache at a user-writable path (HOME/.cache/...) instead of a shared/system location.
- For read-only mounts, remount read-write or move the cache elsewhere.
Example fix
# before $ ls -ld /opt/headroom/bin # drwxr-xr-x root root -> OSError # after sudo chown -R appuser: /opt/headroom/bin
Defensive patterns
Strategy: validation
Validate before calling
import os, tempfile
def dir_accepts_writes(d: str) -> bool:
try:
with tempfile.TemporaryFile(dir=d):
return True
except OSError:
return False
if not dir_accepts_writes(cache_dir):
raise SystemExit(f"{cache_dir} exists but is not writable for this user") Try / catch
try:
ensure_binary(tool)
except OSError as e:
if "not writable" in str(e):
subprocess.run(["chown", "-R", getpass.getuser(), cache_dir]) # or alert ops
ensure_binary(tool) # one targeted retry after fixing perms
else:
raise Prevention
- Set explicit ownership/modes on cache dirs in Dockerfiles (chown appuser).
- Avoid root-owned hostPath/Docker volumes for caches used by non-root services.
- Watch for read-only mounts (NFS, ro volumes) when choosing a cache location.
When it happens
Trigger: Cache parent exists and is listable (so the parent check passed) but lacks write permission for the current user — mode 755 root-owned dir with the process running as non-root; or the directory sits on a read-only mount.
Common situations: Docker volumes mounted from the host with root ownership, Kubernetes hostPath volumes, or shared cache dirs shared across users with restrictive umask.
Related errors
- binary cache directory parent is not writable: {dest.parent}
- {tool}: distributed via PyPI only; `pip install headroom-ai`
- {tool}: no prebuilt binary for {plat.key()}; supported: {sup
- offline mode (HEADROOM_BINARIES_OFFLINE=1) but fetch require
- failed to download {final_url} after {attempts} attempts: {e
AI-assisted analysis of headroomlabs-ai/headroom@322425c43b (2026-08-15).
Data as JSON: /api/errors/e73234980b919034.
Report an issue: GitHub.