diff --git a/pressure-vessel/runtime.c b/pressure-vessel/runtime.c
index a932497d37dbb4fd619bdfd21008c11961d35443..c6b630347a7a5c7a82cfb775d1d09fea6a3dd3c5 100644
--- a/pressure-vessel/runtime.c
+++ b/pressure-vessel/runtime.c
@@ -604,6 +604,137 @@ pv_runtime_maybe_garbage_collect_subdir (const char *description,
     }
 }
 
+static gboolean
+is_old_runtime_deployment (const char *name)
+{
+  if (g_str_has_prefix (name, "scout_before_"))
+    return TRUE;
+
+  if (g_str_has_prefix (name, "soldier_before_"))
+    return TRUE;
+
+  if (g_str_has_prefix (name, "scout_0."))
+    return TRUE;
+
+  if (g_str_has_prefix (name, "soldier_0."))
+    return TRUE;
+
+  if (g_str_has_prefix (name, ".scout_")
+      && g_str_has_suffix (name, "_unpack-temp"))
+    return TRUE;
+
+  if (g_str_has_prefix (name, ".soldier_")
+      && g_str_has_suffix (name, "_unpack-temp"))
+    return TRUE;
+
+  return FALSE;
+}
+
+gboolean
+pv_runtime_garbage_collect_legacy (const char *variable_dir,
+                                   const char *runtime_base,
+                                   GError **error)
+{
+  g_autoptr(GError) local_error = NULL;
+  g_autoptr(PvBwrapLock) variable_lock = NULL;
+  g_autoptr(PvBwrapLock) base_lock = NULL;
+  g_auto(GLnxDirFdIterator) variable_dir_iter = { FALSE };
+  g_auto(GLnxDirFdIterator) runtime_base_iter = { FALSE };
+  glnx_autofd int variable_dir_fd = -1;
+  glnx_autofd int runtime_base_fd = -1;
+  struct
+  {
+    const char *path;
+    GLnxDirFdIterator *iter;
+  } iters[] = {
+    { variable_dir, &variable_dir_iter },
+    { runtime_base, &runtime_base_iter },
+  };
+  gsize i;
+
+  g_return_val_if_fail (variable_dir != NULL, FALSE);
+  g_return_val_if_fail (runtime_base != NULL, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  if (!glnx_opendirat (AT_FDCWD, variable_dir, TRUE,
+                       &variable_dir_fd, error))
+    return FALSE;
+
+  if (!glnx_opendirat (AT_FDCWD, runtime_base, TRUE,
+                       &runtime_base_fd, error))
+    return FALSE;
+
+  variable_lock = pv_bwrap_lock_new (variable_dir_fd, ".ref",
+                                     (PV_BWRAP_LOCK_FLAGS_CREATE
+                                      | PV_BWRAP_LOCK_FLAGS_WRITE),
+                                     &local_error);
+
+  /* If we can't take the lock immediately, just don't do GC */
+  if (variable_lock == NULL)
+    return TRUE;
+
+  /* We take out locks on both the variable directory and the base
+   * directory, because historically in the shell scripts we only
+   * locked the base directory, and we later moved to locking only the
+   * variable directory. Now that we're in C code it seems safest to
+   * lock both. */
+  base_lock = pv_bwrap_lock_new (runtime_base_fd, ".ref",
+                                 (PV_BWRAP_LOCK_FLAGS_CREATE
+                                  | PV_BWRAP_LOCK_FLAGS_WRITE),
+                                 &local_error);
+
+  /* Same here */
+  if (base_lock == NULL)
+    return TRUE;
+
+  for (i = 0; i < G_N_ELEMENTS (iters); i++)
+    {
+      if (!glnx_dirfd_iterator_init_at (AT_FDCWD, iters[i].path,
+                                        TRUE, iters[i].iter, error))
+        return FALSE;
+
+      while (TRUE)
+        {
+          struct dirent *dent;
+
+          if (!glnx_dirfd_iterator_next_dent_ensure_dtype (iters[i].iter,
+                                                           &dent, NULL, error))
+            return FALSE;
+
+          if (dent == NULL)
+            break;
+
+          switch (dent->d_type)
+            {
+              case DT_DIR:
+                break;
+
+              case DT_BLK:
+              case DT_CHR:
+              case DT_FIFO:
+              case DT_LNK:
+              case DT_REG:
+              case DT_SOCK:
+              case DT_UNKNOWN:
+              default:
+                g_debug ("Ignoring %s/%s: not a directory",
+                         iters[i].path, dent->d_name);
+                continue;
+            }
+
+          if (!is_old_runtime_deployment (dent->d_name))
+            continue;
+
+          pv_runtime_maybe_garbage_collect_subdir ("legacy runtime",
+                                                   iters[i].path,
+                                                   iters[i].iter->fd,
+                                                   dent->d_name);
+        }
+    }
+
+  return TRUE;
+}
+
 static gboolean
 pv_runtime_garbage_collect (PvRuntime *self,
                             PvBwrapLock *variable_dir_lock,
diff --git a/pressure-vessel/runtime.h b/pressure-vessel/runtime.h
index 45ab34a027f00f12c19591e25be95942ea32207b..3669a67cbd906041cb62e5c97882896bab97b2b6 100644
--- a/pressure-vessel/runtime.h
+++ b/pressure-vessel/runtime.h
@@ -97,4 +97,8 @@ gboolean pv_runtime_bind (PvRuntime *self,
                           GError **error);
 void pv_runtime_cleanup (PvRuntime *self);
 
+gboolean pv_runtime_garbage_collect_legacy (const char *variable_dir,
+                                            const char *runtime_base,
+                                            GError **error);
+
 G_DEFINE_AUTOPTR_CLEANUP_FUNC (PvRuntime, g_object_unref)
diff --git a/pressure-vessel/wrap.c b/pressure-vessel/wrap.c
index bb23db603d24b6c67f9e9200aae95a85f152a39d..fda9ca640634e416338b2916a514fe93bdf9c879 100644
--- a/pressure-vessel/wrap.c
+++ b/pressure-vessel/wrap.c
@@ -712,6 +712,7 @@ static char *opt_fake_home = NULL;
 static char **opt_filesystems = NULL;
 static char *opt_freedesktop_app_id = NULL;
 static char *opt_steam_app_id = NULL;
+static gboolean opt_gc_legacy_runtimes = FALSE;
 static gboolean opt_gc_runtimes = TRUE;
 static gboolean opt_generate_locales = TRUE;
 static char *opt_home = NULL;
@@ -1032,6 +1033,15 @@ static GOptionEntry options[] =
     "Make --unshare-home use ~/.var/app/com.steampowered.AppN "
     "as home directory. [Default: $STEAM_COMPAT_APP_ID or $SteamAppId]",
     "N" },
+  { "gc-legacy-runtimes", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_gc_legacy_runtimes,
+    "Garbage-collect old unpacked runtimes in $PRESSURE_VESSEL_RUNTIME_BASE.",
+    NULL },
+  { "no-gc-legacy-runtimes", '\0',
+    G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_gc_legacy_runtimes,
+    "Don't garbage-collect old unpacked runtimes in "
+    "$PRESSURE_VESSEL_RUNTIME_BASE [default].",
+    NULL },
   { "gc-runtimes", '\0',
     G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_gc_runtimes,
     "If using --variable-dir, garbage-collect old temporary "
@@ -1339,6 +1349,7 @@ main (int argc,
                                                      TRUE);
 
   opt_share_home = tristate_environment ("PRESSURE_VESSEL_SHARE_HOME");
+  opt_gc_legacy_runtimes = pv_boolean_environment ("PRESSURE_VESSEL_GC_LEGACY_RUNTIMES", FALSE);
   opt_gc_runtimes = pv_boolean_environment ("PRESSURE_VESSEL_GC_RUNTIMES", TRUE);
   opt_generate_locales = pv_boolean_environment ("PRESSURE_VESSEL_GENERATE_LOCALES", TRUE);
 
@@ -1747,6 +1758,21 @@ main (int argc,
                               NULL);
     }
 
+  if (opt_gc_legacy_runtimes
+      && opt_runtime_base != NULL
+      && opt_runtime_base[0] != '\0'
+      && opt_variable_dir != NULL)
+    {
+      if (!pv_runtime_garbage_collect_legacy (opt_variable_dir,
+                                              opt_runtime_base,
+                                              &local_error))
+        {
+          g_warning ("Unable to clean up old runtimes: %s",
+                     local_error->message);
+          g_clear_error (&local_error);
+        }
+    }
+
   if (opt_runtime != NULL || opt_runtime_archive != NULL)
     {
       PvRuntimeFlags flags = PV_RUNTIME_FLAGS_NONE;