getgrav/grav · error · RuntimeException
400
400
Error message
Grav has already been installed here!
What it means
Install::prepare() compares the directory of the update package's installer with the directory of the live index.php: if they are the same, copying would overwrite the package into itself, so it aborts with RuntimeException 400 'Grav has already been installed here!'. It means the updater found its own files sitting in the target site root — typically because an update zip was extracted into the live Grav root.
Source
Thrown at system/src/Grav/Installer/Install.php:313
/**
* @return void
* @throws RuntimeException
*/
public function prepare(): void
{
// Locate the new Grav update and the target site from the filesystem.
$this->ensureLocation();
$location = realpath(__DIR__);
$target = realpath(GRAV_ROOT . '/index.php');
if (!$location) {
throw new RuntimeException('Internal Error', 500);
}
if ($target && dirname($location, 4) === dirname($target)) {
// We cannot copy files into themselves, abort!
throw new RuntimeException('Grav has already been installed here!', 400);
}
// Load the installer classes.
foreach ($this->classMap as $class_name => $path) {
// Make sure that none of the Grav\Installer classes have been loaded, otherwise installation may fail!
if (class_exists($class_name, false)) {
throw new RuntimeException(sprintf('Cannot update Grav, class %s has already been loaded!', $class_name), 500);
}
require $path;
}
$this->legacySupport();
$this->location = dirname($location, 4);
$versions = Versions::instance(USER_DIR . 'config/versions.yaml');
$this->updater = new VersionUpdater('core/grav', __DIR__ . '/updates', $this->getVersion(), $versions);View on GitHub (pinned to 6040efed04)
Solutions
- Keep the downloaded update zip outside the Grav root (e.g. tmp/) and install from it: `bin/gpm direct-install tmp/grav-update.zip`
- If the package was already extracted into the webroot, remove that extracted copy (system/, vendor/, etc. that came from the zip) or restore from backup, then run direct-install from the zip
Example fix
# before cd /var/www/grav && unzip ~/grav-update-v1.8.0.zip -d . && bin/gpm direct-install . # self-copy -> 400 # after bin/gpm direct-install /tmp/grav-update-v1.8.0.zip # install from the zip outside GRAV_ROOT
Defensive patterns
Strategy: validation
Validate before calling
// Ensure the update source and the live site are not the same directory
$updateDir = realpath($updateZipOrDir);
$targetDir = realpath(GRAV_ROOT . '/index.php') ? dirname(realpath(GRAV_ROOT . '/index.php')) : null;
if ($targetDir && $updateDir && str_starts_with($updateDir, $targetDir . DIRECTORY_SEPARATOR)) {
throw new \RuntimeException('Update package lives inside the site root — move it to tmp/ first');
} Prevention
- Always keep grav-update zips in tmp/ (or any dir outside GRAV_ROOT) and install from the zip
- Never extract an update package into the live webroot 'to inspect it'
- If you already extracted one, remove those files (or restore a backup) before re-running direct-install
When it happens
Trigger: Unzipping a grav-update package directly into GRAV_ROOT and then running direct-install on/inside it; pointing direct-install at a folder that is the live site itself; running the installer from a copy of Grav that shares the target root.
Common situations: Manually extracting the update to 'just look at it' in the webroot, then upgrading; confusing the download location with the install source in shell history.
Related errors
- Following requirements have failed:
- Could not migrate the Twig-sandbox allowlists to the additiv
- Backup location: {$backup_root} does not exist...
- Invalid backup location: {$backup_root}
- Internal Error
AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17).
Data as JSON: /api/errors/8d0ef18cb9b5f56e.
Report an issue: GitHub.