diff --git a/hooks/approve-ssh-key b/hooks/approve-ssh-key
index 8e937264081f4b7ddc86a12a0edbf0453fffad0e..f4e4ef56a88a5057e72a33c579733cfcf7b05d8e 100755
--- a/hooks/approve-ssh-key
+++ b/hooks/approve-ssh-key
@@ -11,11 +11,7 @@ ENABLE_SSHD = '/usr/bin/steamos-polkit-helpers/steamos-enable-sshd'
 
 logger = logging.getLogger(os.path.realpath(__file__))
 
-try:
-    import steam_devkit.utils
-except:
-    sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
-    import steam_devkit.utils
+import devkit_utils
 
 if __name__ == '__main__':
     logger.setLevel(logging.INFO)
@@ -44,7 +40,7 @@ if __name__ == '__main__':
     key_identifier = request.split(' ')[2]
     request = f'Development host at IP {requestIP} key: {key_identifier!r}'
 
-    steam_devkit.utils.validate_steam_client()
+    devkit_utils.validate_steam_client()
 
     with tempfile.TemporaryDirectory(prefix='approve-ssh-key') as tempdir:
         logger.debug('Issue approve-ssh-key request to the running Steam client')
@@ -53,8 +49,8 @@ if __name__ == '__main__':
             urllib_quote_plus(response),
             urllib_quote_plus(request),
         )
-        steam_devkit.utils.execute_steam_client_command(cmd)
-        with steam_devkit.utils.wait_on_file_response(response, timeout=30) as f:
+        devkit_utils.execute_steam_client_command(cmd)
+        with devkit_utils.wait_on_file_response(response, timeout=30) as f:
             logger.debug('Got response from Steam client')
             # Make sure sshd is enabled to support development features
             if os.path.exists(ENABLE_SSHD):
diff --git a/hooks/devkit_utils/__init__.py b/hooks/devkit_utils/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4d5feba02ba3307e1348ba72963b935229b1a0c0
--- /dev/null
+++ b/hooks/devkit_utils/__init__.py
@@ -0,0 +1,265 @@
+#!/usr/bin/env python
+# encoding: utf-8
+"""Utility functions for the Steam client hook scripts"""
+
+import sys
+import os
+import traceback
+import tempfile
+import json
+import logging
+import fcntl
+import errno
+import contextlib
+import time
+import fcntl
+
+
+import logging as logging_module
+logger = logging_module.getLogger(__name__)
+
+
+@contextlib.contextmanager
+def wrap_outputs(stderr_prefix):
+    # capture stderr to file to support debugging
+    stderr_fd = sys.stderr.fileno()
+    tf = tempfile.NamedTemporaryFile(
+        mode='w+',
+        prefix=stderr_prefix,
+        delete=True)
+    if sys.version_info >= (3, 4):
+        # this API in the os module is only available for python3
+        # but it does not seem to work with subprocess anyway
+        os.set_inheritable(tf.file.fileno(), True)
+        assert os.get_inheritable(tf.file.fileno())
+    sys.stderr = tf.file
+
+    # we can only write out a json response to stdout,
+    # so redirect stdout to stderr,
+    # and keep a handle on the original stdout for the response
+    stdout_fd = os.dup(sys.stdout.fileno())
+    os.dup2(sys.stderr.fileno(), sys.stdout.fileno())
+
+    ctx = {}
+    try:
+        yield ctx
+    except:
+        logger.error(traceback.format_exc())
+    finally:
+        tf.flush()
+        tf.seek(0)
+        os.write(stderr_fd, tf.read().encode('utf-8'))
+        if 'ret' in ctx:
+            os.write(stdout_fd, json.dumps(ctx['ret']).encode('utf-8'))
+
+
+class SteamClientNotRunningException(Exception):
+    def __init__(self, error_message):
+        self.error_message = error_message
+
+    def __str__(self):
+        return self.error_message
+
+
+def validate_steam_client():
+    """Verify that the steam client is running, and permissions are adequate"""
+    pid_path = os.path.normpath(
+        os.path.realpath(
+            os.path.expanduser('~/.steam/steam.pid')))
+    if not os.path.exists(pid_path):
+        raise SteamClientNotRunningException('{0} does not exist'.format(pid_path))
+    try:
+        pid = int(open(pid_path, 'rt').read())
+    except Exception:
+        raise SteamClientNotRunningException('{0} is invalid'.format(pid_path))
+    try:
+        os.kill(pid, 0)
+    except OSError:
+        raise SteamClientNotRunningException('{0} does not refer to a valid process'.format(pid_path))
+    logger.info('Found steam client pid %s', pid)
+
+
+def execute_steam_client_command(cmd):
+    """Send a command to the steam client over the IPC pipe"""
+    pipe_path = os.path.normpath(
+        os.path.realpath(
+            os.path.expanduser('~/.steam/steam.pipe')))
+    try:
+        pipe = open(pipe_path, 'wb+', 0)
+    except IOError:
+        raise Exception('cannot open steam client pipe')
+    session_token = open(os.path.expanduser('~/.steam/steam.token')).read()
+    pipe_cmd = 'devkit-1 steam://devkit-1/{0}/{1}'.format(
+        session_token,
+        cmd
+    )
+    logger.debug('Sending command line:')
+    logger.debug(pipe_cmd)
+    pipe.write('{0}\n'.format(pipe_cmd).encode('utf-8'))
+    pipe.close()
+
+
+def save_argv(gameid, argv):
+    """Save command line and arguments if provided"""
+
+    if argv is None:
+        return
+
+    argvfile = os.path.join(os.getenv("HOME"), "devkit-game",
+                            gameid + "-argv.json")
+    try:
+        with open(argvfile, "w") as argvf:
+            fcntl.flock(argvf, fcntl.LOCK_EX)
+            json.dump(argv, argvf)
+            fcntl.flock(argvf, fcntl.LOCK_UN)
+    except IOError:
+        raise Exception(
+            "Unable to open argv file for writing: {0}".format(argvfile))
+
+
+def obtain_argv(gameid, argv):
+    """Obtain command line with arguments"""
+
+    # If present and not None or [], just return the local arguments
+    if argv:
+        return argv
+
+    # From here, expect arguments to have been saved previously
+    argvfile = os.path.join(os.getenv("HOME"), "devkit-game",
+                            gameid + "-argv.json")
+    try:
+        with open(argvfile, "r") as argvf:
+            fcntl.flock(argvf, fcntl.LOCK_EX)
+            argv = json.load(argvf)
+            fcntl.flock(argvf, fcntl.LOCK_UN)
+    except IOError:
+        raise Exception(
+            "Unable to open argv file for reading: {0}".format(argvfile))
+    return argv
+
+
+def save_settings(gameid, data):
+    """Save settings"""
+    settingsfile = os.path.join(os.getenv("HOME"), "devkit-game",
+                                gameid + "-settings.json")
+    settings = dict()
+
+    if data.get('clear_settings', False):
+        settings = {}
+    else:
+        try:
+            with open(settingsfile, "r") as f:
+                fcntl.flock(f, fcntl.LOCK_EX)
+                settings = json.load(f)
+                fcntl.flock(f, fcntl.LOCK_UN)
+        except IOError as e:
+            if (e.errno != errno.ENOENT):
+                raise
+
+    # Merge settings from new json
+    if 'settings' in data:
+        settings.update(data['settings'])
+
+    try:
+        with open(settingsfile, "w") as f:
+            fcntl.flock(f, fcntl.LOCK_EX)
+            json.dump(settings, f)
+            fcntl.flock(f, fcntl.LOCK_UN)
+    except (IOError):
+        raise Exception(
+            "Unable to open settings file for writing: {0}".format(
+                settingsfile
+            ))
+
+    return settings
+
+
+def load_settings(gameid):
+    settingsfile = os.path.join(os.getenv("HOME"), "devkit-game", gameid + '-settings.json')
+
+    if not os.path.isfile(settingsfile):
+        return None
+
+    with open(settingsfile, "r") as f:
+        fcntl.flock(f, fcntl.LOCK_EX)
+        settings = json.load(f)
+        fcntl.flock(f, fcntl.LOCK_UN)
+
+    return settings
+
+
+class SteamResponse_Timeout(Exception):
+    pass
+
+
+class SteamResponse_Error(Exception):
+    def __init__(self, error_response):
+        self.error_response = error_response
+
+    def __str__(self):
+        return self.error_response
+
+
+@contextlib.contextmanager
+def wait_on_file_response(path, timeout=5):
+    """
+The pipe to the Steam Client is one way.
+Responses from the Steam Client are written to filesystem.
+Protocol is as follows:
+- Steam Client creates a 'path.lock' file
+- Steam Client writes either 'path' or 'path.error' to indicate a problem
+- Steam Client deletes 'path.lock'
+- Caller (us) can then read the response
+
+NOTE 1: this function is used as a context manager and will block until a response comes in or timeout.
+
+NOTE 2: the files are created by Steam when responding to a command. If the files already exist the response protocol will break.
+    """
+    lock_path = '{0}.lock'.format(path)
+    error_path = '{0}.error'.format(path)
+    max_count = timeout
+    while True:
+        time.sleep(1)
+        if os.path.exists(error_path) or os.path.exists(path) and not os.path.exists(lock_path):
+            if os.path.exists(error_path):
+                with open(error_path, 'r') as f:
+                    fcntl.flock(f, fcntl.LOCK_EX)
+                    error_response = f.read()
+                    fcntl.flock(f, fcntl.LOCK_UN)
+                    raise SteamResponse_Error(error_response)
+            with open(path, 'r') as f:
+                fcntl.flock(f, fcntl.LOCK_EX)
+                success_response = f.read()
+                yield success_response
+                fcntl.flock(f, fcntl.LOCK_UN)
+                return
+        max_count -= 1
+        if max_count > 0:
+            continue
+        raise SteamResponse_Timeout()
+
+
+# Setting up as a context manager so we never miss the deletion
+# Creating a temporary .lock file to guard the create operation
+@contextlib.contextmanager
+def create_pid(pid_path):
+    os.makedirs(os.path.dirname(pid_path), exist_ok=True)
+    lock_path = '{0}.lock'.format(pid_path)
+    try:
+        lock_file = os.open(lock_path, os.O_CREAT | os.O_EXCL)
+    except IOError as e:
+        logger.error('cannot create lock file %s for pid file %s', lock_path, pid_path)
+        logger.error('remove the lock file manually and run again if you are confident no other instance is active')
+        raise
+
+    pid_file = open(pid_path,'w')
+    pid_file.write(str(os.getpid()))
+    pid_file.flush()
+    os.close(lock_file)
+    os.unlink(lock_path)
+    try:
+        yield pid_file
+    finally:
+        pid_file.close()
+        # Assume that's atomic and all is well, no need for another .lock
+        os.unlink(pid_path)