phacility/phabricator · error · Exception

Only patches in the '%s' namespace may contain '%s' keys.

Error message

Only patches in the '%s' namespace may contain '%s' keys.

What it means

The 'legacy' key marks patches that predate the modern patch system and is meaningful only for Phabricator's own first-party storage. Third-party applications and libraries (any namespace other than 'phabricator') cannot declare legacy patches, so the loader rejects the key outright for them.

Source

Thrown at src/infrastructure/storage/patch/PhabricatorSQLPatchList.php:140

        $full_key = "{$namespace}:{$key}";

        if (isset($specs[$full_key])) {
          throw new Exception(
            pht(
              "%s '%s' has a patch '%s' which duplicates an ".
              "existing patch key.",
              __CLASS__,
              get_class($patch_list),
              $key));
        }

        $patch['key']     = $key;
        $patch['fullKey'] = $full_key;
        $patch['dead']    = (bool)idx($patch, 'dead', false);

        if (isset($patch['legacy'])) {
          if ($namespace != 'phabricator') {
            throw new Exception(
              pht(
                "Only patches in the '%s' namespace may contain '%s' keys.",
                'phabricator',
                'legacy'));
          }
        } else {
          $patch['legacy'] = false;
        }

        if (!array_key_exists('phase', $patch)) {
          $patch['phase'] = $default_phase;
        }

        $patch_phase = $patch['phase'];

        if (!isset($phases[$patch_phase])) {
          throw new Exception(
            pht(

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Delete the 'legacy' key from the patch in your application's list — it is meaningless outside the 'phabricator' namespace.
  2. If you genuinely need first-party legacy semantics, the code must live in the phabricator namespace, which only the upstream project uses.
  3. Re-run 'bin/storage status' to confirm the list loads.

Example fix

// before (namespace: 'myapp')
'0000.initial.sql' => array(
  'type' => 'sql',
  'name' => $this->getPatchPath('0000.initial.sql'),
  'legacy' => true,
),

// after
'0000.initial.sql' => array(
  'type' => 'sql',
  'name' => $this->getPatchPath('0000.initial.sql'),
);
Defensive patterns

Strategy: validation

Validate before calling

// Only the phabricator namespace may mark patches legacy:
$list = new MyApplicationPatchList();
if ($list->getNamespace() !== 'phabricator') {
  foreach ($list->getPatches() as $patch) {
    if (array_key_exists('legacy', $patch)) {
      throw new Exception('Only the phabricator namespace may use the legacy key');
    }
  }
}

Prevention

When it happens

Trigger: A patch list whose getNamespace() returns something other than 'phabricator' returns a patch containing a 'legacy' key. This usually happens when copying an early patch from phabricator's own list into an application's list.

Common situations: Bootstrapping a new application by copying the head of Phabricator's core patch list (which starts with legacy patches); porting old pre-modern-patch-system definitions into a fork's extension.

Related errors


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/c3f2aac27c96c355. Report an issue: GitHub.