diff --git a/App/App.csproj b/App/App.csproj
index d4f2bed..8b7e810 100644
--- a/App/App.csproj
+++ b/App/App.csproj
@@ -62,11 +62,14 @@
         </PackageReference>
         <PackageReference Include="H.NotifyIcon.WinUI" Version="2.2.0" />
         <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.1" />
+        <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
+        <PackageReference Include="Microsoft.Extensions.Options" Version="9.0.1" />
         <PackageReference Include="Microsoft.WindowsAppSDK" Version="1.6.250108002" />
     </ItemGroup>
 
     <ItemGroup>
         <ProjectReference Include="..\CoderSdk\CoderSdk.csproj" />
+        <ProjectReference Include="..\MutagenSdk\MutagenSdk.csproj" />
         <ProjectReference Include="..\Vpn.Proto\Vpn.Proto.csproj" />
         <ProjectReference Include="..\Vpn\Vpn.csproj" />
     </ItemGroup>
diff --git a/App/App.xaml.cs b/App/App.xaml.cs
index 9895fc8..e1c5cb4 100644
--- a/App/App.xaml.cs
+++ b/App/App.xaml.cs
@@ -6,8 +6,12 @@
 using Coder.Desktop.App.ViewModels;
 using Coder.Desktop.App.Views;
 using Coder.Desktop.App.Views.Pages;
+using Coder.Desktop.Vpn;
+using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
 using Microsoft.UI.Xaml;
+using Microsoft.Win32;
 
 namespace Coder.Desktop.App;
 
@@ -17,12 +21,28 @@ public partial class App : Application
 
     private bool _handleWindowClosed = true;
 
+#if !DEBUG
+    private const string MutagenControllerConfigSection = "AppMutagenController";
+#else
+    private const string MutagenControllerConfigSection = "DebugAppMutagenController";
+#endif
+
     public App()
     {
-        var services = new ServiceCollection();
+        var builder = Host.CreateApplicationBuilder();
+
+        (builder.Configuration as IConfigurationBuilder).Add(
+            new RegistryConfigurationSource(Registry.LocalMachine, @"SOFTWARE\Coder Desktop"));
+
+        var services = builder.Services;
+
         services.AddSingleton<ICredentialManager, CredentialManager>();
         services.AddSingleton<IRpcController, RpcController>();
 
+        services.AddOptions<MutagenControllerConfig>()
+            .Bind(builder.Configuration.GetSection(MutagenControllerConfigSection));
+        services.AddSingleton<ISyncSessionController, MutagenController>();
+
         // SignInWindow views and view models
         services.AddTransient<SignInViewModel>();
         services.AddTransient<SignInWindow>();
diff --git a/App/Services/MutagenController.cs b/App/Services/MutagenController.cs
new file mode 100644
index 0000000..7f48426
--- /dev/null
+++ b/App/Services/MutagenController.cs
@@ -0,0 +1,438 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Coder.Desktop.MutagenSdk;
+using Coder.Desktop.MutagenSdk.Proto.Selection;
+using Coder.Desktop.MutagenSdk.Proto.Service.Daemon;
+using Coder.Desktop.MutagenSdk.Proto.Service.Synchronization;
+using Coder.Desktop.Vpn.Utilities;
+using Microsoft.Extensions.Options;
+using TerminateRequest = Coder.Desktop.MutagenSdk.Proto.Service.Daemon.TerminateRequest;
+
+namespace Coder.Desktop.App.Services;
+
+// <summary>
+// A file synchronization session to a Coder workspace agent.
+// </summary>
+// <remarks>
+// This implementation is a placeholder while implementing the daemon lifecycle. It's implementation
+// will be backed by the MutagenSDK eventually.
+// </remarks>
+public class SyncSession
+{
+    public string name { get; init; } = "";
+    public string localPath { get; init; } = "";
+    public string workspace { get; init; } = "";
+    public string agent { get; init; } = "";
+    public string remotePath { get; init; } = "";
+}
+
+public interface ISyncSessionController
+{
+    Task<List<SyncSession>> ListSyncSessions(CancellationToken ct);
+    Task<SyncSession> CreateSyncSession(SyncSession session, CancellationToken ct);
+
+    Task TerminateSyncSession(SyncSession session, CancellationToken ct);
+
+    // <summary>
+    // Initializes the controller; running the daemon if there are any saved sessions. Must be called and
+    // complete before other methods are allowed.
+    // </summary>
+    Task Initialize(CancellationToken ct);
+}
+
+// These values are the config option names used in the registry. Any option
+// here can be configured with `(Debug)?AppMutagenController:OptionName` in the registry.
+//
+// They should not be changed without backwards compatibility considerations.
+// If changed here, they should also be changed in the installer.
+public class MutagenControllerConfig
+{
+    [Required] public string MutagenExecutablePath { get; set; } = @"c:\mutagen.exe";
+}
+
+// <summary>
+// A file synchronization controller based on the Mutagen Daemon.
+// </summary>
+public sealed class MutagenController : ISyncSessionController, IAsyncDisposable
+{
+    // Lock to protect all non-readonly class members.
+    private readonly RaiiSemaphoreSlim _lock = new(1, 1);
+
+    // daemonProcess is non-null while the daemon is running, starting, or
+    // in the process of stopping.
+    private Process? _daemonProcess;
+
+    private LogWriter? _logWriter;
+
+    // holds an in-progress task starting or stopping the daemon. If task is null,
+    // then we are not starting or stopping, and the _daemonProcess will be null if
+    // the daemon is currently stopped. If the task is not null, the daemon is
+    // starting or stopping. If stopping, the result is null.
+    private Task<MutagenClient?>? _inProgressTransition;
+
+    // holds a client connected to the running mutagen daemon, if the daemon is running.
+    private MutagenClient? _mutagenClient;
+
+    // holds a local count of SyncSessions, primarily so we can tell when to shut down
+    // the daemon because it is unneeded.
+    private int _sessionCount = -1;
+
+    // set to true if we are disposing the controller. Prevents the daemon from being
+    // restarted.
+    private bool _disposing;
+
+    private readonly string _mutagenExecutablePath;
+
+
+    private readonly string _mutagenDataDirectory = Path.Combine(
+        Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+        "CoderDesktop",
+        "mutagen");
+
+    public MutagenController(IOptions<MutagenControllerConfig> config)
+    {
+        _mutagenExecutablePath = config.Value.MutagenExecutablePath;
+    }
+
+    public MutagenController(string executablePath, string dataDirectory)
+    {
+        _mutagenExecutablePath = executablePath;
+        _mutagenDataDirectory = dataDirectory;
+    }
+
+    public async ValueTask DisposeAsync()
+    {
+        Task<MutagenClient?>? transition = null;
+        using (_ = await _lock.LockAsync(CancellationToken.None))
+        {
+            _disposing = true;
+            if (_inProgressTransition == null && _daemonProcess == null && _mutagenClient == null) return;
+            transition = _inProgressTransition;
+        }
+
+        if (transition != null) await transition;
+        await StopDaemon(new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token);
+        GC.SuppressFinalize(this);
+    }
+
+
+    public async Task<SyncSession> CreateSyncSession(SyncSession session, CancellationToken ct)
+    {
+        // reads of _sessionCount are atomic, so don't bother locking for this quick check.
+        if (_sessionCount == -1) throw new InvalidOperationException("Controller must be Initialized first");
+        var client = await EnsureDaemon(ct);
+        // TODO: implement
+        using (_ = await _lock.LockAsync(ct))
+        {
+            _sessionCount += 1;
+        }
+
+        return session;
+    }
+
+
+    public async Task<List<SyncSession>> ListSyncSessions(CancellationToken ct)
+    {
+        // reads of _sessionCount are atomic, so don't bother locking for this quick check.
+        switch (_sessionCount)
+        {
+            case < 0:
+                throw new InvalidOperationException("Controller must be Initialized first");
+            case 0:
+                // If we already know there are no sessions, don't start up the daemon
+                // again.
+                return new List<SyncSession>();
+        }
+
+        var client = await EnsureDaemon(ct);
+        // TODO: implement
+        return new List<SyncSession>();
+    }
+
+    public async Task Initialize(CancellationToken ct)
+    {
+        using (_ = await _lock.LockAsync(ct))
+        {
+            if (_sessionCount != -1) throw new InvalidOperationException("Initialized more than once");
+            _sessionCount = -2; // in progress
+        }
+
+        var client = await EnsureDaemon(ct);
+        var sessions = await client.Synchronization.ListAsync(new ListRequest
+        {
+            Selection = new Selection
+            {
+                All = true,
+            },
+        }, cancellationToken: ct);
+
+        using (_ = await _lock.LockAsync(ct))
+        {
+            _sessionCount = sessions == null ? 0 : sessions.SessionStates.Count;
+            // check first that no other transition is happening
+            if (_sessionCount != 0 || _inProgressTransition != null)
+                return;
+
+            // don't pass the CancellationToken; we're not going to wait for
+            // this Task anyway.
+            var transition = StopDaemon(new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token);
+            _inProgressTransition = transition;
+            _ = transition.ContinueWith(RemoveTransition, CancellationToken.None);
+            // here we don't need to wait for the transition to complete
+            // before returning from Initialize(), since other operations
+            // will wait for the _inProgressTransition to complete before
+            // doing anything.
+        }
+    }
+
+    public async Task TerminateSyncSession(SyncSession session, CancellationToken ct)
+    {
+        if (_sessionCount < 0) throw new InvalidOperationException("Controller must be Initialized first");
+        var client = await EnsureDaemon(ct);
+        // TODO: implement
+
+        // here we don't use the Cancellation Token, since we want to decrement and possibly
+        // stop the daemon even if we were cancelled, since we already successfully terminated
+        // the session.
+        using (_ = await _lock.LockAsync(CancellationToken.None))
+        {
+            _sessionCount -= 1;
+            if (_sessionCount == 0)
+                // check first that no other transition is happening
+                if (_inProgressTransition == null)
+                {
+                    var transition = StopDaemon(CancellationToken.None);
+                    _inProgressTransition = transition;
+                    _ = transition.ContinueWith(RemoveTransition, CancellationToken.None);
+                    // here we don't need to wait for the transition to complete
+                    // before returning, since other operations
+                    // will wait for the _inProgressTransition to complete before
+                    // doing anything.
+                }
+        }
+    }
+
+
+    private async Task<MutagenClient> EnsureDaemon(CancellationToken ct)
+    {
+        while (true)
+        {
+            ct.ThrowIfCancellationRequested();
+            Task<MutagenClient?> transition;
+            using (_ = await _lock.LockAsync(ct))
+            {
+                if (_disposing) throw new ObjectDisposedException(ToString(), "async disposal underway");
+                if (_mutagenClient != null && _inProgressTransition == null) return _mutagenClient;
+                if (_inProgressTransition != null)
+                {
+                    transition = _inProgressTransition;
+                }
+                else
+                {
+                    // no transition in progress, this implies the _mutagenClient
+                    // must be null, and we are stopped.
+                    _inProgressTransition = StartDaemon(ct);
+                    transition = _inProgressTransition;
+                    _ = transition.ContinueWith(RemoveTransition, ct);
+                }
+            }
+
+            // wait for the transition without holding the lock.
+            var result = await transition;
+            if (result != null) return result;
+        }
+    }
+
+    // <summary>
+    // Remove the completed transition from _inProgressTransition
+    // </summary>
+    private void RemoveTransition(Task<MutagenClient?> transition)
+    {
+        using var _ = _lock.Lock();
+        if (_inProgressTransition == transition) _inProgressTransition = null;
+    }
+
+    private async Task<MutagenClient?> StartDaemon(CancellationToken ct)
+    {
+        // stop any orphaned daemon
+        try
+        {
+            var client = new MutagenClient(_mutagenDataDirectory);
+            await client.Daemon.TerminateAsync(new TerminateRequest(), cancellationToken: ct);
+        }
+        catch (FileNotFoundException)
+        {
+            // Mainline; no daemon running.
+        }
+
+        // If we get some failure while creating the log file or starting the process, we'll retry
+        // it up to 5 times x 100ms. Those issues should resolve themselves quickly if they are
+        // going to at all.
+        const int maxAttempts = 5;
+        ListResponse? sessions = null;
+        for (var attempts = 1; attempts <= maxAttempts; attempts++)
+        {
+            ct.ThrowIfCancellationRequested();
+            try
+            {
+                using (_ = await _lock.LockAsync(ct))
+                {
+                    StartDaemonProcessLocked();
+                }
+            }
+            catch (Exception e) when (e is not OperationCanceledException)
+            {
+                if (attempts == maxAttempts)
+                    throw;
+                // back off a little and try again.
+                await Task.Delay(100, ct);
+                continue;
+            }
+
+            break;
+        }
+
+        return await WaitForDaemon(ct);
+    }
+
+    private async Task<MutagenClient?> WaitForDaemon(CancellationToken ct)
+    {
+        while (true)
+        {
+            ct.ThrowIfCancellationRequested();
+            try
+            {
+                MutagenClient? client;
+                using (_ = await _lock.LockAsync(ct))
+                {
+                    client = _mutagenClient ?? new MutagenClient(_mutagenDataDirectory);
+                }
+
+                _ = await client.Daemon.VersionAsync(new VersionRequest(), cancellationToken: ct);
+
+                using (_ = await _lock.LockAsync(ct))
+                {
+                    if (_mutagenClient != null)
+                        // Some concurrent process already wrote a client; unexpected
+                        // since we should be ensuring only one transition is happening
+                        // at a time. Start over with the new client.
+                        continue;
+                    _mutagenClient = client;
+                    return _mutagenClient;
+                }
+            }
+            catch (Exception e) when
+                (e is not OperationCanceledException) // TODO: Are there other permanent errors we can detect?
+            {
+                // just wait a little longer for the daemon to come up
+                await Task.Delay(100, ct);
+            }
+        }
+    }
+
+    private void StartDaemonProcessLocked()
+    {
+        if (_daemonProcess != null)
+            throw new InvalidOperationException("startDaemonLock called when daemonProcess already present");
+
+        // create the log file first, so ensure we have permissions
+        var logPath = Path.Combine(_mutagenDataDirectory, "daemon.log");
+        var logStream = new StreamWriter(logPath, true);
+
+        _daemonProcess = new Process();
+        _daemonProcess.StartInfo.FileName = _mutagenExecutablePath;
+        _daemonProcess.StartInfo.Arguments = "daemon run";
+        _daemonProcess.StartInfo.Environment.Add("MUTAGEN_DATA_DIRECTORY", _mutagenDataDirectory);
+        // shell needs to be disabled since we set the environment
+        // https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.processstartinfo.environment?view=net-8.0
+        _daemonProcess.StartInfo.UseShellExecute = false;
+        _daemonProcess.StartInfo.RedirectStandardError = true;
+        _daemonProcess.Start();
+
+        var writer = new LogWriter(_daemonProcess.StandardError, logStream);
+        Task.Run(() => { _ = writer.Run(); });
+        _logWriter = writer;
+    }
+
+    private async Task<MutagenClient?> StopDaemon(CancellationToken ct)
+    {
+        Process? process;
+        MutagenClient? client;
+        LogWriter? writer;
+        using (_ = await _lock.LockAsync(ct))
+        {
+            process = _daemonProcess;
+            client = _mutagenClient;
+            writer = _logWriter;
+            _daemonProcess = null;
+            _mutagenClient = null;
+            _logWriter = null;
+        }
+
+        try
+        {
+            if (client == null)
+            {
+                if (process == null) return null;
+                process.Kill(true);
+            }
+            else
+            {
+                try
+                {
+                    await client.Daemon.TerminateAsync(new TerminateRequest(), cancellationToken: ct);
+                }
+                catch
+                {
+                    if (process == null) return null;
+                    process.Kill(true);
+                }
+            }
+
+            if (process == null) return null;
+            var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
+            cts.CancelAfter(TimeSpan.FromSeconds(5));
+            await process.WaitForExitAsync(cts.Token);
+        }
+        finally
+        {
+            client?.Dispose();
+            process?.Dispose();
+            writer?.Dispose();
+        }
+
+        return null;
+    }
+}
+
+public class LogWriter(StreamReader reader, StreamWriter writer) : IDisposable
+{
+    public void Dispose()
+    {
+        reader.Dispose();
+        writer.Dispose();
+        GC.SuppressFinalize(this);
+    }
+
+    public async Task Run()
+    {
+        try
+        {
+            string? line;
+            while ((line = await reader.ReadLineAsync()) != null) await writer.WriteLineAsync(line);
+        }
+        catch
+        {
+            // TODO: Log?
+        }
+        finally
+        {
+            Dispose();
+        }
+    }
+}
diff --git a/App/packages.lock.json b/App/packages.lock.json
index 264df38..8988638 100644
--- a/App/packages.lock.json
+++ b/App/packages.lock.json
@@ -35,6 +35,46 @@
           "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1"
         }
       },
+      "Microsoft.Extensions.Hosting": {
+        "type": "Direct",
+        "requested": "[9.0.1, )",
+        "resolved": "9.0.1",
+        "contentHash": "3wZNcVvC8RW44HDqqmIq+BqF5pgmTQdbNdR9NyYw33JSMnJuclwoJ2PEkrJ/KvD1U/hmqHVL3l5If+Hn3D1fWA==",
+        "dependencies": {
+          "Microsoft.Extensions.Configuration": "9.0.1",
+          "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Configuration.Binder": "9.0.1",
+          "Microsoft.Extensions.Configuration.CommandLine": "9.0.1",
+          "Microsoft.Extensions.Configuration.EnvironmentVariables": "9.0.1",
+          "Microsoft.Extensions.Configuration.FileExtensions": "9.0.1",
+          "Microsoft.Extensions.Configuration.Json": "9.0.1",
+          "Microsoft.Extensions.Configuration.UserSecrets": "9.0.1",
+          "Microsoft.Extensions.DependencyInjection": "9.0.1",
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Diagnostics": "9.0.1",
+          "Microsoft.Extensions.FileProviders.Abstractions": "9.0.1",
+          "Microsoft.Extensions.FileProviders.Physical": "9.0.1",
+          "Microsoft.Extensions.Hosting.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Logging": "9.0.1",
+          "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Logging.Configuration": "9.0.1",
+          "Microsoft.Extensions.Logging.Console": "9.0.1",
+          "Microsoft.Extensions.Logging.Debug": "9.0.1",
+          "Microsoft.Extensions.Logging.EventLog": "9.0.1",
+          "Microsoft.Extensions.Logging.EventSource": "9.0.1",
+          "Microsoft.Extensions.Options": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Options": {
+        "type": "Direct",
+        "requested": "[9.0.1, )",
+        "resolved": "9.0.1",
+        "contentHash": "nggoNKnWcsBIAaOWHA+53XZWrslC7aGeok+aR+epDPRy7HI7GwMnGZE8yEsL2Onw7kMOHVHwKcsDls1INkNUJQ==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Primitives": "9.0.1"
+        }
+      },
       "Microsoft.WindowsAppSDK": {
         "type": "Direct",
         "requested": "[1.6.250108002, )",
@@ -50,6 +90,28 @@
         "resolved": "3.29.3",
         "contentHash": "t7nZFFUFwigCwZ+nIXHDLweXvwIpsOXi+P7J7smPT/QjI3EKxnCzTQOhBqyEh6XEzc/pNH+bCFOOSjatrPt6Tw=="
       },
+      "Grpc.Core.Api": {
+        "type": "Transitive",
+        "resolved": "2.67.0",
+        "contentHash": "cL1/2f8kc8lsAGNdfCU25deedXVehhLA6GXKLLN4hAWx16XN7BmjYn3gFU+FBpir5yJynvDTHEypr3Tl0j7x/Q=="
+      },
+      "Grpc.Net.Client": {
+        "type": "Transitive",
+        "resolved": "2.67.0",
+        "contentHash": "ofTjJQfegWkVlk5R4k/LlwpcucpsBzntygd4iAeuKd/eLMkmBWoXN+xcjYJ5IibAahRpIJU461jABZvT6E9dwA==",
+        "dependencies": {
+          "Grpc.Net.Common": "2.67.0",
+          "Microsoft.Extensions.Logging.Abstractions": "6.0.0"
+        }
+      },
+      "Grpc.Net.Common": {
+        "type": "Transitive",
+        "resolved": "2.67.0",
+        "contentHash": "gazn1cD2Eol0/W5ZJRV4PYbNrxJ9oMs8pGYux5S9E4MymClvl7aqYSmpqgmWAUWvziRqK9K+yt3cjCMfQ3x/5A==",
+        "dependencies": {
+          "Grpc.Core.Api": "2.67.0"
+        }
+      },
       "H.GeneratedIcons.System.Drawing": {
         "type": "Transitive",
         "resolved": "2.2.0",
@@ -66,15 +128,242 @@
           "H.GeneratedIcons.System.Drawing": "2.2.0"
         }
       },
+      "Microsoft.Extensions.Configuration": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "VuthqFS+ju6vT8W4wevdhEFiRi1trvQtkzWLonApfF5USVzzDcTBoY3F24WvN/tffLSrycArVfX1bThm/9xY2A==",
+        "dependencies": {
+          "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Primitives": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Configuration.Abstractions": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "+4hfFIY1UjBCXFTTOd+ojlDPq6mep3h5Vq5SYE3Pjucr7dNXmq4S/6P/LoVnZFz2e/5gWp/om4svUFgznfULcA==",
+        "dependencies": {
+          "Microsoft.Extensions.Primitives": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Configuration.Binder": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "w7kAyu1Mm7eParRV6WvGNNwA8flPTub16fwH49h7b/yqJZFTgYxnOVCuiah3G2bgseJMEq4DLjjsyQRvsdzRgA==",
+        "dependencies": {
+          "Microsoft.Extensions.Configuration.Abstractions": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Configuration.CommandLine": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "5WC1OsXfljC1KHEyL0yefpAyt1UZjrZ0/xyOqFowc5VntbE79JpCYOTSYFlxEuXm3Oq5xsgU2YXeZLTgAAX+DA==",
+        "dependencies": {
+          "Microsoft.Extensions.Configuration": "9.0.1",
+          "Microsoft.Extensions.Configuration.Abstractions": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Configuration.EnvironmentVariables": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "5HShUdF8KFAUSzoEu0DOFbX09FlcFtHxEalowyjM7Kji0EjdF0DLjHajb2IBvoqsExAYox+Z2GfbfGF7dH7lKQ==",
+        "dependencies": {
+          "Microsoft.Extensions.Configuration": "9.0.1",
+          "Microsoft.Extensions.Configuration.Abstractions": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Configuration.FileExtensions": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "QBOI8YVAyKqeshYOyxSe6co22oag431vxMu5xQe1EjXMkYE4xK4J71xLCW3/bWKmr9Aoy1VqGUARSLFnotk4Bg==",
+        "dependencies": {
+          "Microsoft.Extensions.Configuration": "9.0.1",
+          "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+          "Microsoft.Extensions.FileProviders.Abstractions": "9.0.1",
+          "Microsoft.Extensions.FileProviders.Physical": "9.0.1",
+          "Microsoft.Extensions.Primitives": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Configuration.Json": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "z+g+lgPET1JRDjsOkFe51rkkNcnJgvOK5UIpeTfF1iAi0GkBJz5/yUuTa8a9V8HUh4gj4xFT5WGoMoXoSDKfGg==",
+        "dependencies": {
+          "Microsoft.Extensions.Configuration": "9.0.1",
+          "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Configuration.FileExtensions": "9.0.1",
+          "Microsoft.Extensions.FileProviders.Abstractions": "9.0.1",
+          "System.Text.Json": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Configuration.UserSecrets": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "esGPOgLZ1tZddEomexhrU+LJ5YIsuJdkh0tU7r4WVpNZ15dLuMPqPW4Xe4txf3T2PDUX2ILe3nYQEDjZjfSEJg==",
+        "dependencies": {
+          "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Configuration.Json": "9.0.1",
+          "Microsoft.Extensions.FileProviders.Abstractions": "9.0.1",
+          "Microsoft.Extensions.FileProviders.Physical": "9.0.1"
+        }
+      },
       "Microsoft.Extensions.DependencyInjection.Abstractions": {
         "type": "Transitive",
         "resolved": "9.0.1",
         "contentHash": "Tr74eP0oQ3AyC24ch17N8PuEkrPbD0JqIfENCYqmgKYNOmL8wQKzLJu3ObxTUDrjnn4rHoR1qKa37/eQyHmCDA=="
       },
+      "Microsoft.Extensions.Diagnostics": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "4ZmP6turxMFsNwK/MCko2fuIITaYYN/eXyyIRq1FjLDKnptdbn6xMb7u0zfSMzCGpzkx4RxH/g1jKN2IchG7uA==",
+        "dependencies": {
+          "Microsoft.Extensions.Configuration": "9.0.1",
+          "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Diagnostics.Abstractions": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "pfAPuVtHvG6dvZtAa0OQbXdDqq6epnr8z0/IIUjdmV0tMeI8Aj9KxDXvdDvqr+qNHTkmA7pZpChNxwNZt4GXVg==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Options": "9.0.1",
+          "System.Diagnostics.DiagnosticSource": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.FileProviders.Abstractions": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "DguZYt1DWL05+8QKWL3b6bW7A2pC5kYFMY5iXM6W2M23jhvcNa8v6AU8PvVJBcysxHwr9/jax0agnwoBumsSwg==",
+        "dependencies": {
+          "Microsoft.Extensions.Primitives": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.FileProviders.Physical": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "TKDMNRS66UTMEVT38/tU9hA63UTMvzI3DyNm5mx8+JCf3BaOtxgrvWLCI1y3J52PzT5yNl/T2KN5Z0KbApLZcg==",
+        "dependencies": {
+          "Microsoft.Extensions.FileProviders.Abstractions": "9.0.1",
+          "Microsoft.Extensions.FileSystemGlobbing": "9.0.1",
+          "Microsoft.Extensions.Primitives": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.FileSystemGlobbing": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "Mxcp9NXuQMvAnudRZcgIb5SqlWrlullQzntBLTwuv0MPIJ5LqiGwbRqiyxgdk+vtCoUkplb0oXy5kAw1t469Ug=="
+      },
+      "Microsoft.Extensions.Hosting.Abstractions": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "CwSMhLNe8HLkfbFzdz0CHWJhtWH3TtfZSicLBd/itFD+NqQtfGHmvqXHQbaFFl3mQB5PBb2gxwzWQyW2pIj7PA==",
+        "dependencies": {
+          "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.1",
+          "Microsoft.Extensions.FileProviders.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Logging.Abstractions": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Logging": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "E/k5r7S44DOW+08xQPnNbO8DKAQHhkspDboTThNJ6Z3/QBb4LC6gStNWzVmy3IvW7sUD+iJKf4fj0xEkqE7vnQ==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection": "9.0.1",
+          "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Options": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Logging.Abstractions": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "w2gUqXN/jNIuvqYwX3lbXagsizVNXYyt6LlF57+tMve4JYCEgCMMAjRce6uKcDASJgpMbErRT1PfHy2OhbkqEA==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+          "System.Diagnostics.DiagnosticSource": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Logging.Configuration": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "MeZePlyu3/74Wk4FHYSzXijADJUhWa7gxtaphLxhS8zEPWdJuBCrPo0sezdCSZaKCL+cZLSLobrb7xt2zHOxZQ==",
+        "dependencies": {
+          "Microsoft.Extensions.Configuration": "9.0.1",
+          "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Configuration.Binder": "9.0.1",
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Logging": "9.0.1",
+          "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Options": "9.0.1",
+          "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Logging.Console": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "YUzguHYlWfp4upfYlpVe3dnY59P25wc+/YLJ9/NQcblT3EvAB1CObQulClll7NtnFbbx4Js0a0UfyS8SbRsWXQ==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Logging": "9.0.1",
+          "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Logging.Configuration": "9.0.1",
+          "Microsoft.Extensions.Options": "9.0.1",
+          "System.Text.Json": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Logging.Debug": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "pzdyibIV8k4sym0Sszcp2MJCuXrpOGs9qfOvY+hCRu8k4HbdVoeKOLnacxHK6vEPITX5o5FjjsZW2zScLXTjYA==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Logging": "9.0.1",
+          "Microsoft.Extensions.Logging.Abstractions": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Logging.EventLog": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "+a4RlbwFWjsMujNNhf1Jy9Nm5CpMT+nxXxfgrkRSloPo0OAWhPSPsrFo6VWpvgIPPS41qmfAVWr3DqAmOoVZgQ==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Logging": "9.0.1",
+          "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Options": "9.0.1",
+          "System.Diagnostics.EventLog": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Logging.EventSource": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "d47ZRZUOg1dGOX+yisWScQ7w4+92OlR9beS2UXaiadUCA3RFoZzobzVgrzBX7Oo/qefx9LxdRcaeFpWKb3BNBw==",
+        "dependencies": {
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Logging": "9.0.1",
+          "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Options": "9.0.1",
+          "Microsoft.Extensions.Primitives": "9.0.1",
+          "System.Text.Json": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Options.ConfigurationExtensions": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "8RRKWtuU4fR+8MQLR/8CqZwZ9yc2xCpllw/WPRY7kskIqEq0hMcEI4AfUJO72yGiK2QJkrsDcUvgB5Yc+3+lyg==",
+        "dependencies": {
+          "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Configuration.Binder": "9.0.1",
+          "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Options": "9.0.1",
+          "Microsoft.Extensions.Primitives": "9.0.1"
+        }
+      },
       "Microsoft.Extensions.Primitives": {
         "type": "Transitive",
-        "resolved": "5.0.1",
-        "contentHash": "5WPSmL4YeP7eW+Vc8XZ4DwjYWBAiSwDV9Hm63JJWcz1Ie3Xjv4KuJXzgCstj48LkLfVCYa7mLcx7y+q6yqVvtw=="
+        "resolved": "9.0.1",
+        "contentHash": "bHtTesA4lrSGD1ZUaMIx6frU3wyy0vYtTa/hM6gGQu5QNrydObv8T5COiGUWsisflAfmsaFOe9Xvw5NSO99z0g=="
       },
       "Microsoft.Web.WebView2": {
         "type": "Transitive",
@@ -104,6 +393,16 @@
         "resolved": "9.0.0",
         "contentHash": "QhkXUl2gNrQtvPmtBTQHb0YsUrDiDQ2QS09YbtTTiSjGcf7NBqtYbrG/BE06zcBPCKEwQGzIv13IVdXNOSub2w=="
       },
+      "System.Diagnostics.DiagnosticSource": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "yOcDWx4P/s1I83+7gQlgQLmhny2eNcU0cfo1NBWi+en4EAI38Jau+/neT85gUW6w1s7+FUJc2qNOmmwGLIREqA=="
+      },
+      "System.Diagnostics.EventLog": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "iVnDpgYJsRaRFnk77kcLA3+913WfWDtnAKrQl9tQ5ahqKANTaJKmQdsuPWWiAPWE9pk1Kj4Pg9JGXWfFYYyakQ=="
+      },
       "System.Drawing.Common": {
         "type": "Transitive",
         "resolved": "9.0.0",
@@ -125,13 +424,35 @@
           "System.Collections.Immutable": "9.0.0"
         }
       },
+      "System.Text.Encodings.Web": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "XkspqduP2t1e1x2vBUAD/xZ5ZDvmywuUwsmB93MvyQLospJfqtX0GsR/kU0vUL2h4kmvf777z3txV2W4NrQ9Qg=="
+      },
+      "System.Text.Json": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "eqWHDZqYPv1PvuvoIIx5pF74plL3iEOZOl/0kQP+Y0TEbtgNnM2W6k8h8EPYs+LTJZsXuWa92n5W5sHTWvE3VA==",
+        "dependencies": {
+          "System.IO.Pipelines": "9.0.1",
+          "System.Text.Encodings.Web": "9.0.1"
+        }
+      },
       "Coder.Desktop.CoderSdk": {
         "type": "Project"
       },
+      "Coder.Desktop.MutagenSdk": {
+        "type": "Project",
+        "dependencies": {
+          "Google.Protobuf": "[3.29.3, )",
+          "Grpc.Net.Client": "[2.67.0, )"
+        }
+      },
       "Coder.Desktop.Vpn": {
         "type": "Project",
         "dependencies": {
           "Coder.Desktop.Vpn.Proto": "[1.0.0, )",
+          "Microsoft.Extensions.Configuration": "[9.0.1, )",
           "Semver": "[3.0.0, )",
           "System.IO.Pipelines": "[9.0.1, )"
         }
@@ -163,6 +484,16 @@
         "type": "Transitive",
         "resolved": "9.0.0",
         "contentHash": "z8FfGIaoeALdD+KF44A2uP8PZIQQtDGiXsOLuN8nohbKhkyKt7zGaZb+fKiCxTuBqG22Q7myIAioSWaIcOOrOw=="
+      },
+      "System.Diagnostics.EventLog": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "iVnDpgYJsRaRFnk77kcLA3+913WfWDtnAKrQl9tQ5ahqKANTaJKmQdsuPWWiAPWE9pk1Kj4Pg9JGXWfFYYyakQ=="
+      },
+      "System.Text.Encodings.Web": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "XkspqduP2t1e1x2vBUAD/xZ5ZDvmywuUwsmB93MvyQLospJfqtX0GsR/kU0vUL2h4kmvf777z3txV2W4NrQ9Qg=="
       }
     },
     "net8.0-windows10.0.19041/win-x64": {
@@ -185,6 +516,16 @@
         "type": "Transitive",
         "resolved": "9.0.0",
         "contentHash": "z8FfGIaoeALdD+KF44A2uP8PZIQQtDGiXsOLuN8nohbKhkyKt7zGaZb+fKiCxTuBqG22Q7myIAioSWaIcOOrOw=="
+      },
+      "System.Diagnostics.EventLog": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "iVnDpgYJsRaRFnk77kcLA3+913WfWDtnAKrQl9tQ5ahqKANTaJKmQdsuPWWiAPWE9pk1Kj4Pg9JGXWfFYYyakQ=="
+      },
+      "System.Text.Encodings.Web": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "XkspqduP2t1e1x2vBUAD/xZ5ZDvmywuUwsmB93MvyQLospJfqtX0GsR/kU0vUL2h4kmvf777z3txV2W4NrQ9Qg=="
       }
     },
     "net8.0-windows10.0.19041/win-x86": {
@@ -207,6 +548,16 @@
         "type": "Transitive",
         "resolved": "9.0.0",
         "contentHash": "z8FfGIaoeALdD+KF44A2uP8PZIQQtDGiXsOLuN8nohbKhkyKt7zGaZb+fKiCxTuBqG22Q7myIAioSWaIcOOrOw=="
+      },
+      "System.Diagnostics.EventLog": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "iVnDpgYJsRaRFnk77kcLA3+913WfWDtnAKrQl9tQ5ahqKANTaJKmQdsuPWWiAPWE9pk1Kj4Pg9JGXWfFYYyakQ=="
+      },
+      "System.Text.Encodings.Web": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "XkspqduP2t1e1x2vBUAD/xZ5ZDvmywuUwsmB93MvyQLospJfqtX0GsR/kU0vUL2h4kmvf777z3txV2W4NrQ9Qg=="
       }
     }
   }
diff --git a/Installer/Program.cs b/Installer/Program.cs
index 0bec102..7945f5b 100644
--- a/Installer/Program.cs
+++ b/Installer/Program.cs
@@ -250,17 +250,22 @@ private static int BuildMsiPackage(MsiOptions opts)
         programFiles64Folder.AddDir(installDir);
         project.AddDir(programFiles64Folder);
 
-        // Add registry values that are consumed by the manager. Note that these
-        // should not be changed. See Vpn.Service/Program.cs and
-        // Vpn.Service/ManagerConfig.cs for more details.
+
         project.AddRegValues(
+            // Add registry values that are consumed by the manager. Note that these
+            // should not be changed. See Vpn.Service/Program.cs and
+            // Vpn.Service/ManagerConfig.cs for more details.
             new RegValue(RegistryHive, RegistryKey, "Manager:ServiceRpcPipeName", "Coder.Desktop.Vpn"),
             new RegValue(RegistryHive, RegistryKey, "Manager:TunnelBinaryPath",
                 $"[INSTALLFOLDER]{opts.VpnDir}\\coder-vpn.exe"),
             new RegValue(RegistryHive, RegistryKey, "Manager:LogFileLocation",
                 @"[INSTALLFOLDER]coder-desktop-service.log"),
             new RegValue(RegistryHive, RegistryKey, "Manager:TunnelBinarySignatureSigner", "Coder Technologies Inc."),
-            new RegValue(RegistryHive, RegistryKey, "Manager:TunnelBinaryAllowVersionMismatch", "false"));
+            new RegValue(RegistryHive, RegistryKey, "Manager:TunnelBinaryAllowVersionMismatch", "false"),
+            // Add registry values that are consumed by the App MutagenController. See App/Services/MutagenController.cs
+            new RegValue(RegistryHive, RegistryKey, "AppMutagenController:MutagenExecutablePath",
+                @"[INSTALLFOLDER]mutagen.exe")
+        );
 
         // Note: most of this control panel info will not be visible as this
         // package is usually hidden in favor of the bootstrapper showing
diff --git a/Tests.App/Services/MutagenControllerTest.cs b/Tests.App/Services/MutagenControllerTest.cs
new file mode 100644
index 0000000..40d6a48
--- /dev/null
+++ b/Tests.App/Services/MutagenControllerTest.cs
@@ -0,0 +1,138 @@
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Coder.Desktop.App.Services;
+
+namespace Coder.Desktop.Tests.App.Services;
+
+[TestFixture]
+public class MutagenControllerTest
+{
+    [OneTimeSetUp]
+    public async Task DownloadMutagen()
+    {
+        var ct = new CancellationTokenSource(TimeSpan.FromSeconds(60)).Token;
+        var scriptDirectory = Path.GetFullPath(Path.Combine(TestContext.CurrentContext.TestDirectory,
+            "..", "..", "..", "..", "scripts"));
+        var process = new Process();
+        process.StartInfo.FileName = "powershell.exe";
+        process.StartInfo.UseShellExecute = false;
+        process.StartInfo.Arguments = $"-ExecutionPolicy Bypass -File Get-Mutagen.ps1 -arch {_arch}";
+        process.StartInfo.RedirectStandardError = true;
+        process.StartInfo.RedirectStandardOutput = true;
+        process.StartInfo.WorkingDirectory = scriptDirectory;
+        process.Start();
+        var output = await process.StandardOutput.ReadToEndAsync(ct);
+        TestContext.Out.Write(output);
+        var error = await process.StandardError.ReadToEndAsync(ct);
+        TestContext.Error.Write(error);
+        Assert.That(process.ExitCode, Is.EqualTo(0));
+        _mutagenBinaryPath = Path.Combine(scriptDirectory, "files", $"mutagen-windows-{_arch}.exe");
+        Assert.That(File.Exists(_mutagenBinaryPath));
+    }
+
+    [SetUp]
+    public void CreateTempDir()
+    {
+        _tempDirectory = Directory.CreateTempSubdirectory(GetType().Name);
+        TestContext.Out.WriteLine($"temp directory: {_tempDirectory}");
+    }
+
+    private readonly string _arch = RuntimeInformation.ProcessArchitecture switch
+    {
+        Architecture.X64 => "x64",
+        Architecture.Arm64 => "arm64",
+        // We only support amd64 and arm64 on Windows currently.
+        _ => throw new PlatformNotSupportedException(
+            $"Unsupported architecture '{RuntimeInformation.ProcessArchitecture}'. Coder only supports x64 and arm64."),
+    };
+
+    private string _mutagenBinaryPath;
+    private DirectoryInfo _tempDirectory;
+
+    [Test(Description = "Shut down daemon when no sessions")]
+    [CancelAfter(30_000)]
+    public async Task ShutdownNoSessions(CancellationToken ct)
+    {
+        // NUnit runs each test in a temporary directory
+        var dataDirectory = _tempDirectory.FullName;
+        await using var controller = new MutagenController(_mutagenBinaryPath, dataDirectory);
+        await controller.Initialize(ct);
+
+        // log file tells us the daemon was started.
+        var logPath = Path.Combine(dataDirectory, "daemon.log");
+        Assert.That(File.Exists(logPath));
+
+        var lockPath = Path.Combine(dataDirectory, "daemon", "daemon.lock");
+        // If we can lock the daemon.lock file, it means the daemon has stopped.
+        while (true)
+        {
+            ct.ThrowIfCancellationRequested();
+            try
+            {
+                await using var lockFile = new FileStream(lockPath, FileMode.Open, FileAccess.Write, FileShare.None);
+            }
+            catch (IOException e)
+            {
+                TestContext.Out.WriteLine($"Didn't get lock (will retry): {e.Message}");
+                await Task.Delay(100, ct);
+            }
+
+            break;
+        }
+    }
+
+    [Test(Description = "Daemon is restarted when we create a session")]
+    [CancelAfter(30_000)]
+    public async Task CreateRestartsDaemon(CancellationToken ct)
+    {
+        // NUnit runs each test in a temporary directory
+        var dataDirectory = _tempDirectory.FullName;
+        await using (var controller = new MutagenController(_mutagenBinaryPath, dataDirectory))
+        {
+            await controller.Initialize(ct);
+            await controller.CreateSyncSession(new SyncSession(), ct);
+        }
+
+        var logPath = Path.Combine(dataDirectory, "daemon.log");
+        Assert.That(File.Exists(logPath));
+        var logLines = File.ReadAllLines(logPath);
+
+        // Here we're going to use the log to verify the daemon was started 2 times.
+        // slightly brittle, but unlikely this log line will change.
+        Assert.That(logLines.Count(s => s.Contains("[sync] Session manager initialized")), Is.EqualTo(2));
+    }
+
+    [Test(Description = "Controller kills orphaned daemon")]
+    [CancelAfter(30_000)]
+    public async Task Orphaned(CancellationToken ct)
+    {
+        // NUnit runs each test in a temporary directory
+        var dataDirectory = _tempDirectory.FullName;
+        MutagenController? controller1 = null;
+        MutagenController? controller2 = null;
+        try
+        {
+            controller1 = new MutagenController(_mutagenBinaryPath, dataDirectory);
+            await controller1.Initialize(ct);
+            await controller1.CreateSyncSession(new SyncSession(), ct);
+
+            controller2 = new MutagenController(_mutagenBinaryPath, dataDirectory);
+            await controller2.Initialize(ct);
+        }
+        finally
+        {
+            if (controller1 != null) await controller1.DisposeAsync();
+            if (controller2 != null) await controller2.DisposeAsync();
+        }
+
+        var logPath = Path.Combine(dataDirectory, "daemon.log");
+        Assert.That(File.Exists(logPath));
+        var logLines = File.ReadAllLines(logPath);
+
+        // Here we're going to use the log to verify the daemon was started 3 times.
+        // slightly brittle, but unlikely this log line will change.
+        Assert.That(logLines.Count(s => s.Contains("[sync] Session manager initialized")), Is.EqualTo(3));
+    }
+
+    // TODO: Add more tests once we actually implement creating sessions on the daemon
+}
diff --git a/Tests.Vpn.Service/packages.lock.json b/Tests.Vpn.Service/packages.lock.json
index 45e0457..7ba4c03 100644
--- a/Tests.Vpn.Service/packages.lock.json
+++ b/Tests.Vpn.Service/packages.lock.json
@@ -474,6 +474,7 @@
         "type": "Project",
         "dependencies": {
           "Coder.Desktop.Vpn.Proto": "[1.0.0, )",
+          "Microsoft.Extensions.Configuration": "[9.0.1, )",
           "Semver": "[3.0.0, )",
           "System.IO.Pipelines": "[9.0.1, )"
         }
diff --git a/Tests.Vpn/Tests.Vpn.csproj b/Tests.Vpn/Tests.Vpn.csproj
index b1ff6c6..2b9e30f 100644
--- a/Tests.Vpn/Tests.Vpn.csproj
+++ b/Tests.Vpn/Tests.Vpn.csproj
@@ -3,7 +3,7 @@
     <PropertyGroup>
         <AssemblyName>Coder.Desktop.Tests.Vpn</AssemblyName>
         <RootNamespace>Coder.Desktop.Tests.Vpn</RootNamespace>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net8.0-windows</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
         <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
diff --git a/Tests.Vpn/packages.lock.json b/Tests.Vpn/packages.lock.json
index 10f6f62..725c743 100644
--- a/Tests.Vpn/packages.lock.json
+++ b/Tests.Vpn/packages.lock.json
@@ -1,7 +1,7 @@
 {
   "version": 1,
   "dependencies": {
-    "net8.0": {
+    "net8.0-windows7.0": {
       "coverlet.collector": {
         "type": "Direct",
         "requested": "[6.0.4, )",
@@ -46,10 +46,27 @@
         "resolved": "17.12.0",
         "contentHash": "4svMznBd5JM21JIG2xZKGNanAHNXplxf/kQDFfLHXQ3OnpJkayRK/TjacFjA+EYmoyuNXHo/sOETEfcYtAzIrA=="
       },
+      "Microsoft.Extensions.Configuration": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "VuthqFS+ju6vT8W4wevdhEFiRi1trvQtkzWLonApfF5USVzzDcTBoY3F24WvN/tffLSrycArVfX1bThm/9xY2A==",
+        "dependencies": {
+          "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Primitives": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Configuration.Abstractions": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "+4hfFIY1UjBCXFTTOd+ojlDPq6mep3h5Vq5SYE3Pjucr7dNXmq4S/6P/LoVnZFz2e/5gWp/om4svUFgznfULcA==",
+        "dependencies": {
+          "Microsoft.Extensions.Primitives": "9.0.1"
+        }
+      },
       "Microsoft.Extensions.Primitives": {
         "type": "Transitive",
-        "resolved": "5.0.1",
-        "contentHash": "5WPSmL4YeP7eW+Vc8XZ4DwjYWBAiSwDV9Hm63JJWcz1Ie3Xjv4KuJXzgCstj48LkLfVCYa7mLcx7y+q6yqVvtw=="
+        "resolved": "9.0.1",
+        "contentHash": "bHtTesA4lrSGD1ZUaMIx6frU3wyy0vYtTa/hM6gGQu5QNrydObv8T5COiGUWsisflAfmsaFOe9Xvw5NSO99z0g=="
       },
       "Microsoft.TestPlatform.ObjectModel": {
         "type": "Transitive",
@@ -95,6 +112,7 @@
         "type": "Project",
         "dependencies": {
           "Coder.Desktop.Vpn.Proto": "[1.0.0, )",
+          "Microsoft.Extensions.Configuration": "[9.0.1, )",
           "Semver": "[3.0.0, )",
           "System.IO.Pipelines": "[9.0.1, )"
         }
diff --git a/Vpn.DebugClient/Vpn.DebugClient.csproj b/Vpn.DebugClient/Vpn.DebugClient.csproj
index bc81b6b..0eda43d 100644
--- a/Vpn.DebugClient/Vpn.DebugClient.csproj
+++ b/Vpn.DebugClient/Vpn.DebugClient.csproj
@@ -4,7 +4,7 @@
         <AssemblyName>Coder.Desktop.Vpn.DebugClient</AssemblyName>
         <RootNamespace>Coder.Desktop.Vpn.DebugClient</RootNamespace>
         <OutputType>Exe</OutputType>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net8.0-windows</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
         <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
diff --git a/Vpn.DebugClient/packages.lock.json b/Vpn.DebugClient/packages.lock.json
index 403a41b..473422b 100644
--- a/Vpn.DebugClient/packages.lock.json
+++ b/Vpn.DebugClient/packages.lock.json
@@ -1,16 +1,33 @@
 {
   "version": 1,
   "dependencies": {
-    "net8.0": {
+    "net8.0-windows7.0": {
       "Google.Protobuf": {
         "type": "Transitive",
         "resolved": "3.29.3",
         "contentHash": "t7nZFFUFwigCwZ+nIXHDLweXvwIpsOXi+P7J7smPT/QjI3EKxnCzTQOhBqyEh6XEzc/pNH+bCFOOSjatrPt6Tw=="
       },
+      "Microsoft.Extensions.Configuration": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "VuthqFS+ju6vT8W4wevdhEFiRi1trvQtkzWLonApfF5USVzzDcTBoY3F24WvN/tffLSrycArVfX1bThm/9xY2A==",
+        "dependencies": {
+          "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Primitives": "9.0.1"
+        }
+      },
+      "Microsoft.Extensions.Configuration.Abstractions": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "+4hfFIY1UjBCXFTTOd+ojlDPq6mep3h5Vq5SYE3Pjucr7dNXmq4S/6P/LoVnZFz2e/5gWp/om4svUFgznfULcA==",
+        "dependencies": {
+          "Microsoft.Extensions.Primitives": "9.0.1"
+        }
+      },
       "Microsoft.Extensions.Primitives": {
         "type": "Transitive",
-        "resolved": "5.0.1",
-        "contentHash": "5WPSmL4YeP7eW+Vc8XZ4DwjYWBAiSwDV9Hm63JJWcz1Ie3Xjv4KuJXzgCstj48LkLfVCYa7mLcx7y+q6yqVvtw=="
+        "resolved": "9.0.1",
+        "contentHash": "bHtTesA4lrSGD1ZUaMIx6frU3wyy0vYtTa/hM6gGQu5QNrydObv8T5COiGUWsisflAfmsaFOe9Xvw5NSO99z0g=="
       },
       "Semver": {
         "type": "Transitive",
@@ -29,6 +46,7 @@
         "type": "Project",
         "dependencies": {
           "Coder.Desktop.Vpn.Proto": "[1.0.0, )",
+          "Microsoft.Extensions.Configuration": "[9.0.1, )",
           "Semver": "[3.0.0, )",
           "System.IO.Pipelines": "[9.0.1, )"
         }
diff --git a/Vpn.Service/packages.lock.json b/Vpn.Service/packages.lock.json
index ace2cdb..fb4185a 100644
--- a/Vpn.Service/packages.lock.json
+++ b/Vpn.Service/packages.lock.json
@@ -416,6 +416,7 @@
         "type": "Project",
         "dependencies": {
           "Coder.Desktop.Vpn.Proto": "[1.0.0, )",
+          "Microsoft.Extensions.Configuration": "[9.0.1, )",
           "Semver": "[3.0.0, )",
           "System.IO.Pipelines": "[9.0.1, )"
         }
diff --git a/Vpn.Service/RegistryConfigurationSource.cs b/Vpn/RegistryConfigurationSource.cs
similarity index 96%
rename from Vpn.Service/RegistryConfigurationSource.cs
rename to Vpn/RegistryConfigurationSource.cs
index 8e2dd0d..2e67b87 100644
--- a/Vpn.Service/RegistryConfigurationSource.cs
+++ b/Vpn/RegistryConfigurationSource.cs
@@ -1,7 +1,7 @@
 using Microsoft.Extensions.Configuration;
 using Microsoft.Win32;
 
-namespace Coder.Desktop.Vpn.Service;
+namespace Coder.Desktop.Vpn;
 
 public class RegistryConfigurationSource : IConfigurationSource
 {
diff --git a/Vpn/Vpn.csproj b/Vpn/Vpn.csproj
index c08b669..76a72eb 100644
--- a/Vpn/Vpn.csproj
+++ b/Vpn/Vpn.csproj
@@ -3,7 +3,7 @@
     <PropertyGroup>
         <AssemblyName>Coder.Desktop.Vpn</AssemblyName>
         <RootNamespace>Coder.Desktop.Vpn</RootNamespace>
-        <TargetFramework>net8.0</TargetFramework>
+        <TargetFramework>net8.0-windows</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
         <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
@@ -14,6 +14,7 @@
     </ItemGroup>
 
     <ItemGroup>
+        <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.1" />
         <PackageReference Include="Semver" Version="3.0.0" />
         <PackageReference Include="System.IO.Pipelines" Version="9.0.1" />
     </ItemGroup>
diff --git a/Vpn/packages.lock.json b/Vpn/packages.lock.json
index 5eca812..8876fe4 100644
--- a/Vpn/packages.lock.json
+++ b/Vpn/packages.lock.json
@@ -1,7 +1,17 @@
 {
   "version": 1,
   "dependencies": {
-    "net8.0": {
+    "net8.0-windows7.0": {
+      "Microsoft.Extensions.Configuration": {
+        "type": "Direct",
+        "requested": "[9.0.1, )",
+        "resolved": "9.0.1",
+        "contentHash": "VuthqFS+ju6vT8W4wevdhEFiRi1trvQtkzWLonApfF5USVzzDcTBoY3F24WvN/tffLSrycArVfX1bThm/9xY2A==",
+        "dependencies": {
+          "Microsoft.Extensions.Configuration.Abstractions": "9.0.1",
+          "Microsoft.Extensions.Primitives": "9.0.1"
+        }
+      },
       "Semver": {
         "type": "Direct",
         "requested": "[3.0.0, )",
@@ -22,10 +32,18 @@
         "resolved": "3.29.3",
         "contentHash": "t7nZFFUFwigCwZ+nIXHDLweXvwIpsOXi+P7J7smPT/QjI3EKxnCzTQOhBqyEh6XEzc/pNH+bCFOOSjatrPt6Tw=="
       },
+      "Microsoft.Extensions.Configuration.Abstractions": {
+        "type": "Transitive",
+        "resolved": "9.0.1",
+        "contentHash": "+4hfFIY1UjBCXFTTOd+ojlDPq6mep3h5Vq5SYE3Pjucr7dNXmq4S/6P/LoVnZFz2e/5gWp/om4svUFgznfULcA==",
+        "dependencies": {
+          "Microsoft.Extensions.Primitives": "9.0.1"
+        }
+      },
       "Microsoft.Extensions.Primitives": {
         "type": "Transitive",
-        "resolved": "5.0.1",
-        "contentHash": "5WPSmL4YeP7eW+Vc8XZ4DwjYWBAiSwDV9Hm63JJWcz1Ie3Xjv4KuJXzgCstj48LkLfVCYa7mLcx7y+q6yqVvtw=="
+        "resolved": "9.0.1",
+        "contentHash": "bHtTesA4lrSGD1ZUaMIx6frU3wyy0vYtTa/hM6gGQu5QNrydObv8T5COiGUWsisflAfmsaFOe9Xvw5NSO99z0g=="
       },
       "Coder.Desktop.Vpn.Proto": {
         "type": "Project",
diff --git a/scripts/Get-Mutagen.ps1 b/scripts/Get-Mutagen.ps1
new file mode 100644
index 0000000..c540809
--- /dev/null
+++ b/scripts/Get-Mutagen.ps1
@@ -0,0 +1,44 @@
+# Usage: Get-Mutagen.ps1 -arch <x64|arm64>
+param (
+    [ValidateSet("x64", "arm64")]
+    [Parameter(Mandatory = $true)]
+    [string] $arch
+)
+
+function Download-File([string] $url, [string] $outputPath, [string] $etagFile) {
+    Write-Host "Downloading '$url' to '$outputPath'"
+    # We use `curl.exe` here because `Invoke-WebRequest` is notoriously slow.
+    & curl.exe `
+        --progress-bar `
+        --show-error `
+        --fail `
+        --location `
+        --etag-compare $etagFile `
+        --etag-save $etagFile `
+        --output $outputPath `
+        $url
+    if ($LASTEXITCODE -ne 0) { throw "Failed to download $url" }
+    if (!(Test-Path $outputPath) -or (Get-Item $outputPath).Length -eq 0) {
+        throw "Failed to download '$url', output file '$outputPath' is missing or empty"
+    }
+}
+
+$goArch = switch ($arch) {
+    "x64" { "amd64" }
+    "arm64" { "arm64" }
+    default { throw "Unsupported architecture: $arch" }
+}
+
+# Download the mutagen binary from our bucket for this platform if we don't have
+# it yet (or it's different).
+$mutagenVersion = "v0.18.1"
+$mutagenPath = Join-Path $PSScriptRoot "files\mutagen-windows-$($arch).exe"
+$mutagenUrl = "https://storage.googleapis.com/coder-desktop/mutagen/$($mutagenVersion)/mutagen-windows-$($goArch).exe"
+$mutagenEtagFile = $mutagenPath + ".etag"
+Download-File $mutagenUrl $mutagenPath $mutagenEtagFile
+
+# Download mutagen agents tarball.
+$mutagenAgentsPath = Join-Path $PSScriptRoot "files\mutagen-agents.tar.gz"
+$mutagenAgentsUrl = "https://storage.googleapis.com/coder-desktop/mutagen/$($mutagenVersion)/mutagen-agents.tar.gz"
+$mutagenAgentsEtagFile = $mutagenAgentsPath + ".etag"
+Download-File $mutagenAgentsUrl $mutagenAgentsPath $mutagenAgentsEtagFile
diff --git a/scripts/Publish.ps1 b/scripts/Publish.ps1
index fa3a571..5f7a25e 100644
--- a/scripts/Publish.ps1
+++ b/scripts/Publish.ps1
@@ -83,30 +83,6 @@ function Add-CoderSignature([string] $path) {
     }
 }
 
-function Download-File([string] $url, [string] $outputPath, [string] $etagFile) {
-    Write-Host "Downloading '$url' to '$outputPath'"
-    # We use `curl.exe` here because `Invoke-WebRequest` is notoriously slow.
-    & curl.exe `
-        --progress-bar `
-        --show-error `
-        --fail `
-        --location `
-        --etag-compare $etagFile `
-        --etag-save $etagFile `
-        --output $outputPath `
-        $url
-    if ($LASTEXITCODE -ne 0) { throw "Failed to download $url" }
-    if (!(Test-Path $outputPath) -or (Get-Item $outputPath).Length -eq 0) {
-        throw "Failed to download '$url', output file '$outputPath' is missing or empty"
-    }
-}
-
-$goArch = switch ($arch) {
-    "x64" { "amd64" }
-    "arm64" { "arm64" }
-    default { throw "Unsupported architecture: $arch" }
-}
-
 # CD to the root of the repo
 $repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
 Push-Location $repoRoot
@@ -169,21 +145,15 @@ if ($null -eq $wintunDllSrc) {
 $wintunDllDest = Join-Path $vpnFilesPath "wintun.dll"
 Copy-Item $wintunDllSrc $wintunDllDest
 
-# Download the mutagen binary from our bucket for this platform if we don't have
-# it yet (or it's different).
-$mutagenVersion = "v0.18.1"
-$mutagenSrcPath = Join-Path $repoRoot "scripts\files\mutagen-windows-$($goArch).exe"
-$mutagenSrcUrl = "https://storage.googleapis.com/coder-desktop/mutagen/$($mutagenVersion)/mutagen-windows-$($goArch).exe"
-$mutagenEtagFile = $mutagenSrcPath + ".etag"
-Download-File $mutagenSrcUrl $mutagenSrcPath $mutagenEtagFile
+$scriptRoot = Join-Path $repoRoot "scripts"
+$getMutagen = Join-Path $scriptRoot "Get-Mutagen.ps1"
+& $getMutagen -arch $arch
+
+$mutagenSrcPath = Join-Path $scriptRoot "files\mutagen-windows-$($arch).exe"
 $mutagenDestPath = Join-Path $vpnFilesPath "mutagen.exe"
 Copy-Item $mutagenSrcPath $mutagenDestPath
 
-# Download mutagen agents tarball.
-$mutagenAgentsSrcPath = Join-Path $repoRoot "scripts\files\mutagen-agents.tar.gz"
-$mutagenAgentsSrcUrl = "https://storage.googleapis.com/coder-desktop/mutagen/$($mutagenVersion)/mutagen-agents.tar.gz"
-$mutagenAgentsEtagFile = $mutagenAgentsSrcPath + ".etag"
-Download-File $mutagenAgentsSrcUrl $mutagenAgentsSrcPath $mutagenAgentsEtagFile
+$mutagenAgentsSrcPath = Join-Path $scriptRoot "files\mutagen-agents.tar.gz"
 $mutagenAgentsDestPath = Join-Path $vpnFilesPath "mutagen-agents.tar.gz"
 Copy-Item $mutagenAgentsSrcPath $mutagenAgentsDestPath