diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
index a110432..30ea7e7 100644
--- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
+++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift
@@ -51,9 +51,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
         #elseif arch(x86_64)
             let mutagenBinary = "mutagen-darwin-amd64"
         #endif
-        fileSyncDaemon = MutagenDaemon(
+        let fileSyncDaemon = MutagenDaemon(
             mutagenPath: Bundle.main.url(forResource: mutagenBinary, withExtension: nil)
         )
+        Task {
+            await fileSyncDaemon.tryStart()
+        }
+        self.fileSyncDaemon = fileSyncDaemon
     }
 
     func applicationDidFinishLaunching(_: Notification) {
diff --git a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift
index 4559716..1253e42 100644
--- a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift	
+++ b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift	
@@ -20,7 +20,7 @@ final class PreviewFileSync: FileSyncDaemon {
         state = .stopped
     }
 
-    func createSession(localPath _: String, agentHost _: String, remotePath _: String) async throws(DaemonError) {}
+    func createSession(arg _: CreateSyncSessionRequest) async throws(DaemonError) {}
 
     func deleteSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
 
diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
index dc946c8..7400635 100644
--- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift
@@ -166,10 +166,6 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
         defer { loading = false }
         do throws(DaemonError) {
             try await fileSync.deleteSessions(ids: [selection!])
-            if fileSync.sessionState.isEmpty {
-                // Last session was deleted, stop the daemon
-                await fileSync.stop()
-            }
         } catch {
             actionError = error
         }
diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
index 7b902f2..66b20ba 100644
--- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
@@ -100,9 +100,10 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
                 try await fileSync.deleteSessions(ids: [existingSession.id])
             }
             try await fileSync.createSession(
-                localPath: localPath,
-                agentHost: remoteHostname,
-                remotePath: remotePath
+                arg: .init(
+                    alpha: .init(path: localPath, protocolKind: .local),
+                    beta: .init(path: remotePath, protocolKind: .ssh(host: remoteHostname))
+                )
             )
         } catch {
             createError = error
diff --git a/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift
index 61bf219..d361581 100644
--- a/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift
+++ b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift
@@ -103,8 +103,8 @@ struct FilePickerTests {
                 try disclosureGroup.expand()
 
                 // Disclosure group should expand out to 3 more directories
-                try #expect(await eventually { @MainActor in
-                    return try view.findAll(ViewType.DisclosureGroup.self).count == 6
+                #expect(await eventually { @MainActor in
+                    return view.findAll(ViewType.DisclosureGroup.self).count == 6
                 })
             }
         }
diff --git a/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift b/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift
new file mode 100644
index 0000000..916faf6
--- /dev/null
+++ b/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift
@@ -0,0 +1,167 @@
+@testable import Coder_Desktop
+import Foundation
+import GRPC
+import NIO
+import Subprocess
+import Testing
+import VPNLib
+import XCTest
+
+@MainActor
+@Suite(.timeLimit(.minutes(1)))
+class FileSyncDaemonTests {
+    let tempDir: URL
+    let mutagenBinary: URL
+    let mutagenDataDirectory: URL
+    let mutagenAlphaDirectory: URL
+    let mutagenBetaDirectory: URL
+
+    // Before each test
+    init() throws {
+        tempDir = FileManager.default.makeTempDir()!
+        #if arch(arm64)
+            let binaryName = "mutagen-darwin-arm64"
+        #elseif arch(x86_64)
+            let binaryName = "mutagen-darwin-amd64"
+        #endif
+        mutagenBinary = Bundle.main.url(forResource: binaryName, withExtension: nil)!
+        mutagenDataDirectory = tempDir.appending(path: "mutagen")
+        mutagenAlphaDirectory = tempDir.appending(path: "alpha")
+        try FileManager.default.createDirectory(at: mutagenAlphaDirectory, withIntermediateDirectories: true)
+        mutagenBetaDirectory = tempDir.appending(path: "beta")
+        try FileManager.default.createDirectory(at: mutagenBetaDirectory, withIntermediateDirectories: true)
+    }
+
+    // After each test
+    deinit {
+        try? FileManager.default.removeItem(at: tempDir)
+    }
+
+    private func statesEqual(_ first: DaemonState, _ second: DaemonState) -> Bool {
+        switch (first, second) {
+        case (.stopped, .stopped):
+            true
+        case (.running, .running):
+            true
+        case (.unavailable, .unavailable):
+            true
+        default:
+            false
+        }
+    }
+
+    @Test
+    func fullSync() async throws {
+        let daemon = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory)
+        #expect(statesEqual(daemon.state, .stopped))
+        #expect(daemon.sessionState.count == 0)
+
+        // The daemon won't start until we create a session
+        await daemon.tryStart()
+        #expect(statesEqual(daemon.state, .stopped))
+        #expect(daemon.sessionState.count == 0)
+
+        try await daemon.createSession(
+            arg: .init(
+                alpha: .init(
+                    path: mutagenAlphaDirectory.path(),
+                    protocolKind: .local
+                ),
+                beta: .init(
+                    path: mutagenBetaDirectory.path(),
+                    protocolKind: .local
+                )
+            )
+        )
+
+        // Daemon should have started itself
+        #expect(statesEqual(daemon.state, .running))
+        #expect(daemon.sessionState.count == 1)
+
+        // Write a file to Alpha
+        let alphaFile = mutagenAlphaDirectory.appendingPathComponent("test.txt")
+        try "Hello, World!".write(to: alphaFile, atomically: true, encoding: .utf8)
+        #expect(
+            await eventually(timeout: .seconds(5), interval: .milliseconds(100)) { @MainActor in
+                return FileManager.default.fileExists(
+                    atPath: self.mutagenBetaDirectory.appending(path: "test.txt").path()
+                )
+            })
+
+        try await daemon.deleteSessions(ids: daemon.sessionState.map(\.id))
+        #expect(daemon.sessionState.count == 0)
+        // Daemon should have stopped itself once all sessions are deleted
+        #expect(statesEqual(daemon.state, .stopped))
+    }
+
+    @Test
+    func autoStopStart() async throws {
+        let daemon = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory)
+        #expect(statesEqual(daemon.state, .stopped))
+        #expect(daemon.sessionState.count == 0)
+
+        try await daemon.createSession(
+            arg: .init(
+                alpha: .init(
+                    path: mutagenAlphaDirectory.path(),
+                    protocolKind: .local
+                ),
+                beta: .init(
+                    path: mutagenBetaDirectory.path(),
+                    protocolKind: .local
+                )
+            )
+        )
+
+        try await daemon.createSession(
+            arg: .init(
+                alpha: .init(
+                    path: mutagenAlphaDirectory.path(),
+                    protocolKind: .local
+                ),
+                beta: .init(
+                    path: mutagenBetaDirectory.path(),
+                    protocolKind: .local
+                )
+            )
+        )
+
+        #expect(statesEqual(daemon.state, .running))
+        #expect(daemon.sessionState.count == 2)
+
+        try await daemon.deleteSessions(ids: [daemon.sessionState[0].id])
+        #expect(daemon.sessionState.count == 1)
+        #expect(statesEqual(daemon.state, .running))
+
+        try await daemon.deleteSessions(ids: [daemon.sessionState[0].id])
+        #expect(daemon.sessionState.count == 0)
+        #expect(statesEqual(daemon.state, .stopped))
+    }
+
+    @Test
+    func orphaned() async throws {
+        let daemon1 = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory)
+        await daemon1.refreshSessions()
+        try await daemon1.createSession(arg:
+            .init(
+                alpha: .init(
+                    path: mutagenAlphaDirectory.path(),
+                    protocolKind: .local
+                ),
+                beta: .init(
+                    path: mutagenBetaDirectory.path(),
+                    protocolKind: .local
+                )
+            )
+        )
+        #expect(statesEqual(daemon1.state, .running))
+        #expect(daemon1.sessionState.count == 1)
+
+        let daemon2 = MutagenDaemon(mutagenPath: mutagenBinary, mutagenDataDirectory: mutagenDataDirectory)
+        await daemon2.tryStart()
+        #expect(statesEqual(daemon2.state, .running))
+
+        // Daemon 2 should have killed daemon 1, causing it to fail
+        #expect(daemon1.state.isFailed)
+    }
+}
diff --git a/Coder-Desktop/Coder-DesktopTests/Util.swift b/Coder-Desktop/Coder-DesktopTests/Util.swift
index 249aa10..c5239a9 100644
--- a/Coder-Desktop/Coder-DesktopTests/Util.swift
+++ b/Coder-Desktop/Coder-DesktopTests/Util.swift
@@ -47,7 +47,7 @@ class MockFileSyncDaemon: FileSyncDaemon {
         []
     }
 
-    func createSession(localPath _: String, agentHost _: String, remotePath _: String) async throws(DaemonError) {}
+    func createSession(arg _: CreateSyncSessionRequest) async throws(DaemonError) {}
 
     func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
 
@@ -61,24 +61,32 @@ extension Inspection: @unchecked Sendable, @retroactive InspectionEmissary {}
 public func eventually(
     timeout: Duration = .milliseconds(500),
     interval: Duration = .milliseconds(10),
-    condition: @escaping () async throws -> Bool
-) async throws -> Bool {
+    condition: @Sendable () async throws -> Bool
+) async rethrows -> Bool {
     let endTime = ContinuousClock.now.advanced(by: timeout)
 
-    var lastError: Error?
-
     while ContinuousClock.now < endTime {
         do {
             if try await condition() { return true }
-            lastError = nil
         } catch {
-            lastError = error
             try await Task.sleep(for: interval)
         }
     }
 
-    if let lastError {
-        throw lastError
+    return try await condition()
+}
+
+extension FileManager {
+    func makeTempDir() -> URL? {
+        let tempDirectory = FileManager.default.temporaryDirectory
+        let directoryName = String(Int.random(in: 0 ..< 1_000_000))
+        let directoryURL = tempDirectory.appendingPathComponent(directoryName)
+
+        do {
+            try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
+            return directoryURL
+        } catch {
+            return nil
+        }
     }
-    return false
 }
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
index 9e10f2a..7f300fb 100644
--- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
@@ -14,7 +14,7 @@ public protocol FileSyncDaemon: ObservableObject {
     func tryStart() async
     func stop() async
     func refreshSessions() async
-    func createSession(localPath: String, agentHost: String, remotePath: String) async throws(DaemonError)
+    func createSession(arg: CreateSyncSessionRequest) async throws(DaemonError)
     func deleteSessions(ids: [String]) async throws(DaemonError)
     func pauseSessions(ids: [String]) async throws(DaemonError)
     func resumeSessions(ids: [String]) async throws(DaemonError)
@@ -76,21 +76,6 @@ public class MutagenDaemon: FileSyncDaemon {
             state = .unavailable
             return
         }
-
-        // If there are sync sessions, the daemon should be running
-        Task {
-            do throws(DaemonError) {
-                try await start()
-            } catch {
-                state = .failed(error)
-                return
-            }
-            await refreshSessions()
-            if sessionState.isEmpty {
-                logger.info("No sync sessions found on startup, stopping daemon")
-                await stop()
-            }
-        }
     }
 
     public func tryStart() async {
@@ -99,6 +84,12 @@ public class MutagenDaemon: FileSyncDaemon {
             try await start()
         } catch {
             state = .failed(error)
+            return
+        }
+        await refreshSessions()
+        if sessionState.isEmpty {
+            logger.info("No sync sessions found on startup, stopping daemon")
+            await stop()
         }
     }
 
diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift
index d1d3f6c..aaf86b1 100644
--- a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift
+++ b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift
@@ -17,11 +17,7 @@ public extension MutagenDaemon {
         sessionState = sessions.sessionStates.map { FileSyncSession(state: $0) }
     }
 
-    func createSession(
-        localPath: String,
-        agentHost: String,
-        remotePath: String
-    ) async throws(DaemonError) {
+    func createSession(arg: CreateSyncSessionRequest) async throws(DaemonError) {
         if case .stopped = state {
             do throws(DaemonError) {
                 try await start()
@@ -35,17 +31,14 @@ public extension MutagenDaemon {
         let req = Synchronization_CreateRequest.with { req in
             req.prompter = promptID
             req.specification = .with { spec in
-                spec.alpha = .with { alpha in
-                    alpha.protocol = .local
-                    alpha.path = localPath
+                spec.alpha = arg.alpha.mutagenURL
+                spec.beta = arg.beta.mutagenURL
+                // TODO: Ingest configs from somewhere
+                spec.configuration = .with {
+                    // ALWAYS ignore VCS directories for now
+                    // https://mutagen.io/documentation/synchronization/version-control-systems/
+                    $0.ignoreVcsmode = .ignore
                 }
-                spec.beta = .with { beta in
-                    beta.protocol = .ssh
-                    beta.host = agentHost
-                    beta.path = remotePath
-                }
-                // TODO: Ingest a config from somewhere
-                spec.configuration = Synchronization_Configuration()
                 spec.configurationAlpha = Synchronization_Configuration()
                 spec.configurationBeta = Synchronization_Configuration()
             }
@@ -64,20 +57,26 @@ public extension MutagenDaemon {
     func deleteSessions(ids: [String]) async throws(DaemonError) {
         // Terminating sessions does not require prompting, according to the
         // Mutagen CLI
-        let (stream, promptID) = try await host(allowPrompts: false)
-        defer { stream.cancel() }
-        guard case .running = state else { return }
         do {
-            _ = try await client!.sync.terminate(Synchronization_TerminateRequest.with { req in
-                req.prompter = promptID
-                req.selection = .with { selection in
-                    selection.specifications = ids
-                }
-            }, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout)))
-        } catch {
-            throw .grpcFailure(error)
+            let (stream, promptID) = try await host(allowPrompts: false)
+            defer { stream.cancel() }
+            guard case .running = state else { return }
+            do {
+                _ = try await client!.sync.terminate(Synchronization_TerminateRequest.with { req in
+                    req.prompter = promptID
+                    req.selection = .with { selection in
+                        selection.specifications = ids
+                    }
+                }, callOptions: .init(timeLimit: .timeout(sessionMgmtReqTimeout)))
+            } catch {
+                throw .grpcFailure(error)
+            }
         }
         await refreshSessions()
+        if sessionState.isEmpty {
+            // Last session was deleted, stop the daemon
+            await stop()
+        }
     }
 
     func pauseSessions(ids: [String]) async throws(DaemonError) {
@@ -135,3 +134,44 @@ public extension MutagenDaemon {
         await refreshSessions()
     }
 }
+
+public struct CreateSyncSessionRequest {
+    public let alpha: Endpoint
+    public let beta: Endpoint
+
+    public init(alpha: Endpoint, beta: Endpoint) {
+        self.alpha = alpha
+        self.beta = beta
+    }
+}
+
+public struct Endpoint {
+    public let path: String
+    public let protocolKind: ProtocolKind
+
+    public init(path: String, protocolKind: ProtocolKind) {
+        self.path = path
+        self.protocolKind = protocolKind
+    }
+
+    public enum ProtocolKind {
+        case local
+        case ssh(host: String)
+    }
+
+    var mutagenURL: Url_URL {
+        switch protocolKind {
+        case .local:
+            .with { url in
+                url.path = path
+                url.protocol = .local
+            }
+        case let .ssh(host):
+            .with { url in
+                url.path = path
+                url.protocol = .ssh
+                url.host = host
+            }
+        }
+    }
+}
diff --git a/Coder-Desktop/project.yml b/Coder-Desktop/project.yml
index fb38d35..d256767 100644
--- a/Coder-Desktop/project.yml
+++ b/Coder-Desktop/project.yml
@@ -164,7 +164,7 @@ targets:
         SKIP_INSTALL: NO
         LD_RUNPATH_SEARCH_PATHS:
           # Load frameworks from the SE bundle.
-          - "@executable_path/../../Contents/Library/SystemExtensions/com.coder.Coder-Desktop.VPN.systemextension/Contents/Frameworks" 
+          - "@executable_path/../../Contents/Library/SystemExtensions/com.coder.Coder-Desktop.VPN.systemextension/Contents/Frameworks"
           - "@executable_path/../Frameworks"
           - "@loader_path/Frameworks"
     dependencies:
@@ -192,6 +192,8 @@ targets:
     platform: macOS
     sources:
       - path: Coder-DesktopTests
+      - path: Resources
+        buildPhase: resources
     settings:
       base:
         BUNDLE_LOADER: "$(TEST_HOST)"
diff --git a/Makefile b/Makefile
index ebb8e38..115f6e8 100644
--- a/Makefile
+++ b/Makefile
@@ -116,7 +116,7 @@ fmt: ## Run Swift file formatter
 		$(FMTFLAGS) .
 
 .PHONY: test
-test: $(XCPROJECT) ## Run all tests
+test: $(addprefix $(PROJECT)/Resources/,$(MUTAGEN_RESOURCES)) $(XCPROJECT) ## Run all tests
 	set -o pipefail && xcodebuild test \
 		-project $(XCPROJECT) \
 		-scheme $(SCHEME) \