diff --git a/build-relocatable-install.py b/build-relocatable-install.py
index 8bd52e776b120efcec360df4fefa9be7fe1a4a5f..9b645a2448f53372e401a9ce50242c92e42a4d82 100755
--- a/build-relocatable-install.py
+++ b/build-relocatable-install.py
@@ -108,6 +108,7 @@ SCRIPTS = [
 ]
 EXECUTABLES = [
     'pressure-vessel-try-setlocale',
+    'pressure-vessel-with-lock',
     'pressure-vessel-wrap',
 ]
 LIBCAPSULE_TOOLS = [
diff --git a/meson.build b/meson.build
index ba209631e207da76ddd8409786bc045d63653f70..217500ef0156641262042d29fb8bee29e7d599e6 100644
--- a/meson.build
+++ b/meson.build
@@ -164,11 +164,39 @@ add_project_arguments(
   language : 'c',
 )
 
+executable(
+  'pressure-vessel-with-lock',
+  sources : [
+    'src/bwrap-lock.c',
+    'src/bwrap-lock.h',
+    'src/glib-backports.c',
+    'src/glib-backports.h',
+    'src/flatpak-utils.c',
+    'src/flatpak-utils-private.h',
+    'src/utils.c',
+    'src/utils.h',
+    'src/with-lock.c',
+  ],
+  c_args : [
+    '-Wno-unused-local-typedefs',
+  ],
+  dependencies : [
+    dependency('gio-unix-2.0', required : true),
+    subproject('libglnx').get_variable('libglnx_dep'),
+  ],
+  install : true,
+  install_dir : get_option('bindir'),
+  build_rpath : '${ORIGIN}/../' + get_option('libdir'),
+  install_rpath : '${ORIGIN}/../' + get_option('libdir'),
+)
+
 executable(
   'pressure-vessel-wrap',
   sources : [
     'src/bwrap.c',
     'src/bwrap.h',
+    'src/bwrap-lock.c',
+    'src/bwrap-lock.h',
     'src/glib-backports.c',
     'src/glib-backports.h',
     'src/flatpak-bwrap.c',
diff --git a/src/bwrap-lock.c b/src/bwrap-lock.c
new file mode 100644
index 0000000000000000000000000000000000000000..bb82887c93df4666976958408cbc7877bf4ad9c6
--- /dev/null
+++ b/src/bwrap-lock.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright © 2019 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "subprojects/libglnx/config.h"
+
+#include <gio/gio.h>
+
+#include "bwrap-lock.h"
+
+/**
+ * PvBwrapLock:
+ *
+ * A read/write lock compatible with the locks taken out by
+ * `bwrap --lock-file FILENAME` and Flatpak.
+ */
+struct _PvBwrapLock
+{
+  int fd;
+};
+
+/**
+ * pv_bwrap_lock_new:
+ * @path: Runtime directory to lock; the actual lock file will be `$path/.ref`
+ * @flags: Flags affecting how we lock the directory
+ * @error: Used to raise an error on failure
+ *
+ * Take out a lock on a directory.
+ *
+ * If %PV_BWRAP_LOCK_FLAGS_WRITE is in @flags, the lock is a write-lock,
+ * which can be held by at most one process at a time. This is appropriate
+ * when about to modify or delete the runtime. Otherwise it is a read-lock,
+ * which excludes writers but does not exclude other readers. This is
+ * appropriate when running an app or game using the runtime.
+ *
+ * If %PV_BWRAP_LOCK_FLAGS_WAIT is not in @flags, raise %G_IO_ERROR_BUSY
+ * if the lock cannot be obtained immediately.
+ *
+ * Returns: (nullable): A lock (release and free with pv_bwrap_lock_free())
+ *  or %NULL.
+ */
+PvBwrapLock *
+pv_bwrap_lock_new (const gchar *path,
+                   PvBwrapLockFlags flags,
+                   GError **error)
+{
+  glnx_autofd int fd = -1;
+  struct flock l = {
+    .l_type = F_RDLCK,
+    .l_whence = SEEK_SET,
+    .l_start = 0,
+    .l_len = 0
+  };
+  const char *type_str = "reading";
+  int open_flags = O_CLOEXEC | O_NOCTTY;
+  int cmd;
+
+  g_return_val_if_fail (path != NULL, NULL);
+  g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+  if (flags & PV_BWRAP_LOCK_FLAGS_CREATE)
+    open_flags |= O_RDWR | O_CREAT;
+  else if (flags & PV_BWRAP_LOCK_FLAGS_WRITE)
+    open_flags |= O_RDWR;
+  else
+    open_flags |= O_RDONLY;
+
+  fd = TEMP_FAILURE_RETRY (openat (AT_FDCWD, path, open_flags, 0644));
+
+  if (fd < 0)
+    {
+      glnx_throw_errno_prefix (error, "openat(%s)", path);
+      return NULL;
+    }
+
+  if (flags & PV_BWRAP_LOCK_FLAGS_WAIT)
+    cmd = F_SETLKW;
+  else
+    cmd = F_SETLK;
+
+  if (flags & PV_BWRAP_LOCK_FLAGS_WRITE)
+    {
+      l.l_type = F_WRLCK;
+      type_str = "writing";
+    }
+
+  if (TEMP_FAILURE_RETRY (fcntl (fd, cmd, &l)) < 0)
+    {
+      int saved_errno = errno;
+
+      if (saved_errno == EACCES || saved_errno == EAGAIN)
+        {
+          g_set_error (error, G_IO_ERROR, G_IO_ERROR_BUSY,
+                       "Unable to lock %s for %s: file is busy",
+                       path, type_str);
+          return NULL;
+        }
+
+      glnx_throw_errno_prefix (error, "Unable to lock %s for %s",
+                               path, type_str);
+      return NULL;
+    }
+
+  return pv_bwrap_lock_new_take (glnx_steal_fd (&fd));
+}
+
+/**
+ * pv_bwrap_lock_new_take:
+ * @fd: A file descriptor, already locked
+ *
+ * Convert a simple file descriptor into a #PvBwrapLock.
+ *
+ * Returns: (not nullable): A lock (release and free
+ *  with pv_bwrap_lock_free())
+ */
+PvBwrapLock *
+pv_bwrap_lock_new_take (int fd)
+{
+  PvBwrapLock *self = NULL;
+
+  g_return_val_if_fail (fd >= 0, NULL);
+
+  self = g_slice_new0 (PvBwrapLock);
+  self->fd = glnx_steal_fd (&fd);
+  return self;
+}
+
+void
+pv_bwrap_lock_free (PvBwrapLock *self)
+{
+  glnx_autofd int fd = -1;
+
+  g_return_if_fail (self != NULL);
+
+  fd = glnx_steal_fd (&self->fd);
+  g_slice_free (PvBwrapLock, self);
+  /* fd is closed by glnx_autofd if necessary, and that releases the lock */
+}
+
+int
+pv_bwrap_lock_steal_fd (PvBwrapLock *self)
+{
+  g_return_val_if_fail (self != NULL, -1);
+  return glnx_steal_fd (&self->fd);
+}
diff --git a/src/bwrap-lock.h b/src/bwrap-lock.h
new file mode 100644
index 0000000000000000000000000000000000000000..3c85bf78f0d3ae4a26c90ab7031a660a9e02db4a
--- /dev/null
+++ b/src/bwrap-lock.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright © 2019 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "glib-backports.h"
+#include "libglnx.h"
+
+/**
+ * PvBwrapLockFlags:
+ * @PV_BWRAP_LOCK_FLAGS_CREATE: If the lock file doesn't exist, create it
+ * @PV_BWRAP_LOCK_FLAGS_WAIT: If another process holds an incompatible lock,
+ *  wait for it to be released; by default pv_bwrap_lock_new()
+ *  raises %G_IO_ERROR_BUSY immediately
+ * @PV_BWRAP_LOCK_FLAGS_WRITE: Take a write-lock instead of a read-lock;
+ *  by default pv_bwrap_lock_new() takes a read-lock
+ * @PV_BWRAP_LOCK_FLAGS_NONE: None of the above
+ *
+ * Flags affecting how we take a lock on a runtime directory.
+ */
+typedef enum
+{
+  PV_BWRAP_LOCK_FLAGS_CREATE = (1 << 0),
+  PV_BWRAP_LOCK_FLAGS_WAIT = (1 << 1),
+  PV_BWRAP_LOCK_FLAGS_WRITE = (1 << 2),
+  PV_BWRAP_LOCK_FLAGS_NONE = 0
+} PvBwrapLockFlags;
+
+typedef struct _PvBwrapLock PvBwrapLock;
+
+PvBwrapLock *pv_bwrap_lock_new (const gchar *path,
+                                PvBwrapLockFlags flags,
+                                GError **error);
+PvBwrapLock *pv_bwrap_lock_new_take (int fd);
+void pv_bwrap_lock_free (PvBwrapLock *self);
+int pv_bwrap_lock_steal_fd (PvBwrapLock *self);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (PvBwrapLock, pv_bwrap_lock_free)
diff --git a/src/glib-backports.h b/src/glib-backports.h
index 925b30c6e65caf4471739f899ab922c4763837fd..70aba601342eb13e2b49f5215ec4cb1eb9c51ab3 100644
--- a/src/glib-backports.h
+++ b/src/glib-backports.h
@@ -67,3 +67,7 @@ void my_g_ptr_array_insert (GPtrArray *arr,
                             gint index_,
                             gpointer data);
 #endif
+
+#if !GLIB_CHECK_VERSION (2, 42, 0)
+#define G_OPTION_FLAG_NONE 0
+#endif
diff --git a/src/with-lock.c b/src/with-lock.c
new file mode 100644
index 0000000000000000000000000000000000000000..4015086b5e376bab691d3d216a1406ba2b7ad1cb
--- /dev/null
+++ b/src/with-lock.c
@@ -0,0 +1,347 @@
+/* pressure-vessel-with-lock — run a command with a lock held.
+ * Basically flock(1), but using fcntl locks compatible with those used
+ * by bubblewrap and Flatpak.
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+#include "subprojects/libglnx/config.h"
+
+#include <fcntl.h>
+#include <locale.h>
+#include <sysexits.h>
+#include <sys/prctl.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <gio/gio.h>
+
+#include "glib-backports.h"
+#include "libglnx.h"
+
+#include "bwrap-lock.h"
+#include "utils.h"
+
+#ifndef PR_SET_CHILD_SUBREAPER
+#define PR_SET_CHILD_SUBREAPER 36
+#endif
+
+static GPtrArray *global_locks = NULL;
+static gboolean opt_create = FALSE;
+static gboolean opt_subreaper = FALSE;
+static gboolean opt_verbose = FALSE;
+static gboolean opt_version = FALSE;
+static gboolean opt_wait = FALSE;
+static gboolean opt_write = FALSE;
+
+static gboolean
+opt_fd_cb (const char *name,
+           const char *value,
+           gpointer data,
+           GError **error)
+{
+  char *endptr;
+  gint64 i64 = g_ascii_strtoll (value, &endptr, 10);
+  int fd;
+  int fd_flags;
+
+  g_return_val_if_fail (global_locks != NULL, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+  g_return_val_if_fail (value != NULL, FALSE);
+
+  if (i64 < 0 || i64 > G_MAXINT || endptr == value || *endptr != '\0')
+    {
+      g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE,
+                   "Integer out of range or invalid: %s", value);
+      return FALSE;
+    }
+
+  fd = (int) i64;
+
+  fd_flags = fcntl (fd, F_GETFD);
+
+  if (fd_flags < 0)
+    return glnx_throw_errno_prefix (error, "Unable to receive --fd %d", fd);
+
+  if ((fd_flags & FD_CLOEXEC) == 0
+      && fcntl (fd, F_SETFD, fd_flags | FD_CLOEXEC) != 0)
+    return glnx_throw_errno_prefix (error,
+                                    "Unable to configure --fd %d for "
+                                    "close-on-exec",
+                                    fd);
+
+  g_ptr_array_add (global_locks, pv_bwrap_lock_new_take (fd));
+  return TRUE;
+}
+
+static gboolean
+opt_lock_file_cb (const char *name,
+                  const char *value,
+                  gpointer data,
+                  GError **error)
+{
+  PvBwrapLock *lock;
+  PvBwrapLockFlags flags = PV_BWRAP_LOCK_FLAGS_NONE;
+
+  g_return_val_if_fail (global_locks != NULL, FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+  g_return_val_if_fail (value != NULL, FALSE);
+
+  if (opt_create)
+    flags |= PV_BWRAP_LOCK_FLAGS_CREATE;
+
+  if (opt_write)
+    flags |= PV_BWRAP_LOCK_FLAGS_WRITE;
+
+  if (opt_wait)
+    flags |= PV_BWRAP_LOCK_FLAGS_WAIT;
+
+  lock = pv_bwrap_lock_new (value, flags, error);
+
+  if (lock == NULL)
+    return FALSE;
+
+  g_ptr_array_add (global_locks, lock);
+  return TRUE;
+}
+
+static GOptionEntry options[] =
+{
+  { "fd", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, opt_fd_cb,
+    "Take a file descriptor, already locked if desired, and keep it "
+    "open. May be repeated.",
+    NULL },
+
+  { "create", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_create,
+    "Create each subsequent lock file if it doesn't exist.",
+    NULL },
+  { "no-create", '\0',
+    G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_create,
+    "Don't create subsequent nonexistent lock files [default].",
+    NULL },
+
+  { "write", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_write,
+    "Lock each subsequent lock file for write access.",
+    NULL },
+  { "no-write", '\0',
+    G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_write,
+    "Lock each subsequent lock file for read-only access [default].",
+    NULL },
+
+  { "wait", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_wait,
+    "Wait for each subsequent lock file.",
+    NULL },
+  { "no-wait", '\0',
+    G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_wait,
+    "Exit unsuccessfully if a lock-file is busy [default].",
+    NULL },
+
+  { "lock-file", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, opt_lock_file_cb,
+    "Open the given file and lock it, affected by options appearing "
+    "earlier on the command-line. May be repeated.",
+    NULL },
+
+  { "subreaper", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_subreaper,
+    "Do not exit until all child processes have exited.",
+    NULL },
+  { "no-subreaper", '\0',
+    G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_subreaper,
+    "Only wait for a direct child process [default].",
+    NULL },
+
+  { "verbose", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_verbose,
+    "Be more verbose.", NULL },
+  { "version", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_version,
+    "Print version number and exit.", NULL },
+  { NULL }
+};
+
+static void
+cli_log_func (const gchar *log_domain,
+              GLogLevelFlags log_level,
+              const gchar *message,
+              gpointer user_data)
+{
+  g_printerr ("%s: %s\n", (const char *) user_data, message);
+}
+
+int
+main (int argc,
+      char *argv[])
+{
+  g_autoptr(GPtrArray) locks = NULL;
+  g_autoptr(GOptionContext) context = NULL;
+  g_autoptr(GError) local_error = NULL;
+  GError **error = &local_error;
+  int ret = EX_USAGE;
+  char **command_and_args;
+  GPid child_pid;
+  int wait_status = -1;
+
+  setlocale (LC_ALL, "");
+  pv_avoid_gvfs ();
+
+  locks = g_ptr_array_new_with_free_func ((GDestroyNotify) pv_bwrap_lock_free);
+  global_locks = locks;
+
+  g_set_prgname ("pressure-vessel-with-lock");
+
+  g_log_set_handler (G_LOG_DOMAIN,
+                     G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE,
+                     cli_log_func, (void *) g_get_prgname ());
+
+  context = g_option_context_new (
+      "COMMAND [ARG...]\n"
+      "Run COMMAND [ARG...] with a lock held, a subreaper, or similar.\n");
+
+  g_option_context_add_main_entries (context, options, NULL);
+
+  if (!g_option_context_parse (context, &argc, &argv, error))
+    {
+      if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY))
+        ret = EX_TEMPFAIL;
+      else if (local_error->domain == G_OPTION_ERROR)
+        ret = EX_USAGE;
+      else
+        ret = EX_UNAVAILABLE;
+
+      goto out;
+    }
+
+  if (opt_version)
+    {
+      g_print ("%s:\n"
+               " Package: pressure-vessel\n"
+               " Version: %s\n",
+               argv[0], VERSION);
+      ret = 0;
+      goto out;
+    }
+
+  if (opt_verbose)
+    g_log_set_handler (G_LOG_DOMAIN,
+                       G_LOG_LEVEL_DEBUG | G_LOG_LEVEL_INFO,
+                       cli_log_func, (void *) g_get_prgname ());
+
+  if (argc >= 2 && strcmp (argv[1], "--") == 0)
+    {
+      argv++;
+      argc--;
+    }
+
+  if (argc < 2)
+    {
+      g_printerr ("%s: Usage: %s [OPTIONS] COMMAND [ARG...]\n",
+                  g_get_prgname (),
+                  g_get_prgname ());
+      goto out;
+    }
+
+  ret = EX_UNAVAILABLE;
+
+  command_and_args = argv + 1;
+
+  if (opt_subreaper
+      && prctl (PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) != 0)
+    {
+      glnx_throw_errno_prefix (error,
+                               "Unable to manage background processes");
+      goto out;
+    }
+
+  g_debug ("Launching child process...");
+
+  if (!g_spawn_async (NULL,   /* working directory */
+                      command_and_args,
+                      NULL,   /* environment */
+                      G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD,
+                      NULL, NULL,   /* child setup + user_data */
+                      &child_pid,
+                      &local_error))
+    {
+      ret = 127;
+      goto out;
+    }
+
+  while (1)
+    {
+      pid_t died = wait (&wait_status);
+
+      if (died < 0)
+        {
+          if (errno == EINTR)
+            {
+              continue;
+            }
+          else if (errno == ECHILD)
+            {
+              g_debug ("No more child processes, exiting");
+              break;
+            }
+          else
+            {
+              glnx_throw_errno_prefix (error, "wait");
+              goto out;
+            }
+        }
+      else if (died == child_pid)
+        {
+          if (WIFEXITED (wait_status))
+            {
+              ret = WEXITSTATUS (wait_status);
+              g_debug ("Command exited with status %d", ret);
+            }
+          else if (WIFSIGNALED (wait_status))
+            {
+              ret = 128 + WTERMSIG (wait_status);
+              g_debug ("Command killed by signal %d", ret - 128);
+            }
+          else
+            {
+              ret = EX_SOFTWARE;
+              g_debug ("Command terminated in an unknown way (wait status %d)",
+                       wait_status);
+            }
+        }
+      else
+        {
+          g_debug ("Indirect child %lld exited with wait status %d",
+                   (long long) died, wait_status);
+          g_warn_if_fail (opt_subreaper);
+        }
+    }
+
+out:
+  global_locks = NULL;
+
+  if (local_error != NULL)
+    g_warning ("%s", local_error->message);
+
+  return ret;
+}
diff --git a/src/wrap.c b/src/wrap.c
index 4bbf8470a1c62d7c8c3d4a02327cd0ece236526c..4cfcb48239fe878f4cd249f71807a374af6e0093 100644
--- a/src/wrap.c
+++ b/src/wrap.c
@@ -38,6 +38,7 @@
 #include <steam-runtime-tools/steam-runtime-tools.h>
 
 #include "bwrap.h"
+#include "bwrap-lock.h"
 #include "flatpak-bwrap-private.h"
 #include "flatpak-run-private.h"
 #include "flatpak-utils-private.h"
@@ -1809,61 +1810,76 @@ opt_share_home_cb (const gchar *option_name,
 
 static GOptionEntry options[] =
 {
-  { "env-if-host", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_env_if_host,
+  { "env-if-host", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING_ARRAY, &opt_env_if_host,
     "Set VAR=VAL if COMMAND is run with /usr from the host system, "
     "but not if it is run with /usr from RUNTIME.", "VAR=VAL" },
-  { "freedesktop-app-id", 0, 0, G_OPTION_ARG_STRING, &opt_freedesktop_app_id,
+  { "freedesktop-app-id", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &opt_freedesktop_app_id,
     "Make --unshare-home use ~/.var/app/ID as home directory, where ID "
     "is com.example.MyApp or similar. This interoperates with Flatpak. "
     "[Default: $PRESSURE_VESSEL_FDO_APP_ID if set]",
     "ID" },
-  { "steam-app-id", 0, 0, G_OPTION_ARG_STRING, &opt_steam_app_id,
+  { "steam-app-id", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &opt_steam_app_id,
     "Make --unshare-home use ~/.var/app/com.steampowered.AppN "
     "as home directory. [Default: $SteamAppId]", "N" },
-  { "home", 0, 0, G_OPTION_ARG_FILENAME, &opt_home,
+  { "home", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_home,
     "Use HOME as home directory. Implies --unshare-home. "
     "[Default: $PRESSURE_VESSEL_HOME if set]", "HOME" },
-  { "host-fallback", 0, 0, G_OPTION_ARG_NONE, &opt_host_fallback,
+  { "host-fallback", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_host_fallback,
     "Run COMMAND on the host system if we cannot run it in a container.", NULL },
-  { "host-ld-preload", 0, 0, G_OPTION_ARG_CALLBACK, &opt_host_ld_preload_cb,
+  { "host-ld-preload", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, &opt_host_ld_preload_cb,
     "Add MODULE from the host system to LD_PRELOAD when executing COMMAND.",
     "MODULE" },
-  { "runtime", 0, 0, G_OPTION_ARG_FILENAME, &opt_runtime,
+  { "runtime", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_runtime,
     "Mount the given sysroot or merged /usr in the container, and augment "
     "it with the host system's graphics stack. The empty string "
     "means don't use a runtime. [Default: $PRESSURE_VESSEL_RUNTIME or '']",
     "RUNTIME" },
-  { "runtime-base", 0, 0, G_OPTION_ARG_FILENAME, &opt_runtime_base,
+  { "runtime-base", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_runtime_base,
     "If a --runtime is a relative path, look for it relative to BASE. "
     "[Default: $PRESSURE_VESSEL_RUNTIME_BASE or '.']",
     "BASE" },
-  { "share-home", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_share_home_cb,
+  { "share-home", '\0',
+    G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_share_home_cb,
     "Use the real home directory. "
     "[Default unless $PRESSURE_VESSEL_HOME is set or "
     "$PRESSURE_VESSEL_SHARE_HOME is 0]",
     NULL },
-  { "unshare-home", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_share_home_cb,
+  { "unshare-home", '\0',
+    G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_share_home_cb,
     "Use an app-specific home directory chosen according to --home, "
     "--freedesktop-app-id, --steam-app-id or $SteamAppId. "
     "[Default if $PRESSURE_VESSEL_HOME is set or "
     "$PRESSURE_VESSEL_SHARE_HOME is 0]",
     NULL },
-  { "shell", 0, 0, G_OPTION_ARG_CALLBACK, opt_shell_cb,
+  { "shell", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, opt_shell_cb,
     "--shell=after is equivalent to --shell-after, and so on. "
     "[Default: $PRESSURE_VESSEL_SHELL or 'none']",
     "{none|after|fail|instead}" },
-  { "shell-after", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_shell_cb,
+  { "shell-after", '\0',
+    G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_shell_cb,
     "Run an interactive shell after COMMAND. Executing \"$@\" in that "
     "shell will re-run COMMAND [ARGS].",
     NULL },
-  { "shell-fail", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_shell_cb,
+  { "shell-fail", '\0',
+    G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_shell_cb,
     "Run an interactive shell after COMMAND, but only if it fails.",
     NULL },
-  { "shell-instead", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_shell_cb,
+  { "shell-instead", '\0',
+    G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_shell_cb,
     "Run an interactive shell instead of COMMAND. Executing \"$@\" in that "
     "shell will run COMMAND [ARGS].",
     NULL },
-  { "terminal", 0, 0, G_OPTION_ARG_CALLBACK, opt_terminal_cb,
+  { "terminal", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, opt_terminal_cb,
     "none: disable features that would use a terminal; "
     "auto: equivalent to xterm if a --shell option is used, or none; "
     "xterm: put game output (and --shell if used) in an xterm; "
@@ -1871,13 +1887,17 @@ static GOptionEntry options[] =
     "controlling tty "
     "[Default: $PRESSURE_VESSEL_TERMINAL or 'auto']",
     "{none|auto|xterm|tty}" },
-  { "tty", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_terminal_cb,
+  { "tty", '\0',
+    G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_terminal_cb,
     "Equivalent to --terminal=tty", NULL },
-  { "xterm", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_terminal_cb,
+  { "xterm", '\0',
+    G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_terminal_cb,
     "Equivalent to --terminal=xterm", NULL },
-  { "verbose", 0, 0, G_OPTION_ARG_NONE, &opt_verbose,
+  { "verbose", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_verbose,
     "Be more verbose.", NULL },
-  { "version", 0, 0, G_OPTION_ARG_NONE, &opt_version,
+  { "version", '\0',
+    G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_version,
     "Print version number and exit.", NULL },
   { NULL }
 };
@@ -1949,6 +1969,7 @@ main (int argc,
   g_autoptr(GString) adjusted_ld_preload = g_string_new ("");
   g_autofree gchar *cwd_p = NULL;
   g_autofree gchar *cwd_l = NULL;
+  g_autoptr(PvBwrapLock) runtime_lock = NULL;
   const gchar *home;
   g_autofree gchar *bwrap_help = NULL;
   g_autofree gchar *tools_dir = NULL;
@@ -2027,7 +2048,10 @@ main (int argc,
 
   if (opt_version)
     {
-      g_print ("pressure-vessel version %s\n", VERSION);
+      g_print ("%s:\n"
+               " Package: pressure-vessel\n"
+               " Version: %s\n",
+               argv[0], VERSION);
       ret = 0;
       goto out;
     }
@@ -2307,8 +2331,37 @@ main (int argc,
 
   if (opt_runtime != NULL && opt_runtime[0] != '\0')
     {
+      g_autofree gchar *usr = NULL;
+      g_autofree gchar *files_ref = NULL;
+
       g_debug ("Configuring runtime...");
 
+      /* Take a lock on the runtime until we're finished with setup,
+       * to make sure it doesn't get deleted. */
+      files_ref = g_build_filename (opt_runtime, ".ref", NULL);
+      runtime_lock = pv_bwrap_lock_new (files_ref,
+                                        PV_BWRAP_LOCK_FLAGS_CREATE,
+                                        error);
+
+      /* If the runtime is being deleted, ... don't use it, I suppose? */
+      if (runtime_lock == NULL)
+        goto out;
+
+      usr = g_build_filename (opt_runtime, "usr", NULL);
+
+      /* Tell bwrap to hold a reference to files/.ref until it exits.
+       * If files is a merged-/usr, it's mounted as /usr (as if for
+       * Flatpak); otherwise it's mounted at /, which we need to cope
+       * with too. */
+      if (g_file_test (usr, G_FILE_TEST_IS_DIR))
+        flatpak_bwrap_add_args (bwrap,
+                                "--lock-file", "/.ref",
+                                NULL);
+      else
+        flatpak_bwrap_add_args (bwrap,
+                                "--lock-file", "/usr/.ref",
+                                NULL);
+
       search_path_append (bin_path, "/overrides/bin");
       search_path_append (bin_path, g_getenv ("PATH"));
       flatpak_bwrap_add_args (bwrap,
@@ -2513,6 +2566,32 @@ main (int argc,
   if (!flatpak_bwrap_bundle_args (bwrap, 1, -1, FALSE, error))
     goto out;
 
+  /* If we are using a runtime, pass the lock fd to the executed process,
+   * and make it act as a subreaper for the game itself.
+   *
+   * If we were using --unshare-pid then we could use bwrap --sync-fd
+   * and rely on bubblewrap's init process for this, but we currently
+   * can't do that without breaking gameoverlayrender.so's assumptions. */
+  if (runtime_lock != NULL)
+    {
+      g_autofree gchar *with_lock = NULL;
+      g_autofree gchar *fd_str = NULL;
+      int fd = pv_bwrap_lock_steal_fd (runtime_lock);
+
+      with_lock = g_build_filename (tools_dir,
+                                    "pressure-vessel-with-lock",
+                                    NULL);
+      fd_str = g_strdup_printf ("%d", fd);
+
+      flatpak_bwrap_add_fd (bwrap, fd);
+      flatpak_bwrap_add_args (bwrap,
+                              with_lock,
+                              "--subreaper",
+                              "--fd", fd_str,
+                              "--",
+                              NULL);
+    }
+
   g_debug ("Adding wrapped command...");
   flatpak_bwrap_append_args (bwrap, wrapped_command->argv);
 
diff --git a/tests/relocatable-install.py b/tests/relocatable-install.py
index ef1d224c8aeeeaecc03d7129d833ce4f3259eb88..0110424bae3e54b8ea43d4c18f073c3c30a439b3 100755
--- a/tests/relocatable-install.py
+++ b/tests/relocatable-install.py
@@ -78,6 +78,7 @@ class TapTest:
 
 
 EXES = [
+    'pressure-vessel-with-lock',
     'pressure-vessel-wrap',
 ]
 WRAPPED = [