getgrav/grav · critical · RuntimeException
Versions file cannot be read
Error message
Versions file cannot be read
What it means
Versions (a final class used by Grav's installer/upgrader) lazily reads user/config/versions.yaml in its private constructor via file_get_contents(). The file existence is checked with is_file(), but if reading still returns false — permissions, ACLs, open_basedir restrictions, or a race deleting the file — it throws RuntimeException 'Versions file cannot be read'. Because Versions is memoized per filename, this aborts installer initialization.
Source
Thrown at system/src/Grav/Installer/Versions.php:322
$var = array_pop($path);
$current = &$this->items;
foreach ($path as $field) {
if (!is_array($current) || !isset($current[$field])) {
return;
}
$current = &$current[$field];
}
unset($current[$var]);
$this->updated = true;
}
private function __construct(protected string $filename)
{
$content = is_file($this->filename) ? file_get_contents($this->filename) : null;
if (false === $content) {
throw new \RuntimeException('Versions file cannot be read');
}
$this->items = $content ? Yaml::parse($content) : [];
}
}
View on GitHub (pinned to 6040efed04)
Solutions
- Fix permissions on user/config/versions.yaml so the PHP process can read AND write it (typically chown to the web user and chmod 644 or 664).
- If open_basedir is active, ensure the Grav user/config path is inside the allowed paths.
- When running bin/grav upgrade/clearcache from CLI, run it as the same user that owns the Grav files (sudo -u www-data ...).
- As a last resort, delete versions.yaml (Grav recreates it; you lose installed-version history, forcing plugins/themes to re-declare versions on next upgrade).
Example fix
# before (shell) $ ls -l user/config/versions.yaml -rw------- 1 root root 1240 ... # unreadable by web user -> exception # after $ chown www-data:www-data user/config/versions.yaml && chmod 664 user/config/versions.yaml
Defensive patterns
Strategy: validation
Validate before calling
$file = USER_DIR . 'config/versions.yaml';
if (\is_file($file) && !\is_readable($file)) {
throw new \RuntimeException('Fix permissions on ' . $file . ' before continuing.');
}
$versions = \Grav\Installer\Versions::instance($file); Type guard
function versionsFileIsReadable(string $filename): bool
{
return !\is_file($filename) || \is_readable($filename);
} Try / catch
try {
$versions = \Grav\Installer\Versions::instance();
} catch (\RuntimeException $e) {
if ('Versions file cannot be read' === $e->getMessage()) {
// report an actionable permission error to the operator
fwrite(STDERR, 'Make user/config/versions.yaml readable by this process (chown/chmod), then retry.');
exit(1);
}
throw $e;
} Prevention
- Keep all files under user/config owned and writable by the PHP process user; verify after rsync/root deployments.
- Run CLI upgrades (bin/grav upgrade) as the web user, not root.
- Add a deploy-time check: is_readable/is_writable on user/config/versions.yaml fails the deploy loudly.
When it happens
Trigger: Versions::instance('/path/versions.yaml') where the file exists but is unreadable by the PHP process (mode 000/600 owned by another user, open_basedir excluding the path); user/config/versions.yaml made unwritable/unreadable by a deploy or restore; running CLI install/upgrades (bin/grav, scheduler) as a different user than the web server.
Common situations: Shared hosting with restrictive file modes; files transferred by root/rsync leaving root-owned versions.yaml; security hardening that chmod 600'd config files; containers where the config volume is mounted read-only or with a different uid.
Related errors
- Failed to update {file}: {message}
- No backups defined...
- Backup location: {$backup_root} does not exist...
- Cache folder not defined.
- Setup: Configuration reload loop detected!
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/70fb2c78a5ea512f.
Report an issue: GitHub.