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

  1. 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).
  2. If open_basedir is active, ensure the Grav user/config path is inside the allowed paths.
  3. When running bin/grav upgrade/clearcache from CLI, run it as the same user that owns the Grav files (sudo -u www-data ...).
  4. 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

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


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/70fb2c78a5ea512f. Report an issue: GitHub.