diff --git a/Coder-Desktop/Coder-Desktop/Info.plist b/Coder-Desktop/Coder-Desktop/Info.plist
index 8609906b..5e59b253 100644
--- a/Coder-Desktop/Coder-Desktop/Info.plist
+++ b/Coder-Desktop/Coder-Desktop/Info.plist
@@ -2,6 +2,15 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>NSAppTransportSecurity</key>
+	<dict>
+		<!--
+		Required to make HTTP (not HTTPS) requests to workspace agents
+		(i.e. workspace.coder:4). These are already encrypted over wireguard.
+		-->
+		<key>NSAllowsArbitraryLoads</key>
+		<true/>
+	</dict>
 	<key>NetworkExtension</key>
 	<dict>
 		<key>NEMachServiceName</key>
diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift
new file mode 100644
index 00000000..4ee31a62
--- /dev/null
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift
@@ -0,0 +1,232 @@
+import CoderSDK
+import Foundation
+import SwiftUI
+
+struct FilePicker: View {
+    @Environment(\.dismiss) var dismiss
+    @StateObject private var model: FilePickerModel
+    @State private var selection: FilePickerEntryModel?
+
+    @Binding var outputAbsPath: String
+
+    let inspection = Inspection<Self>()
+
+    init(
+        host: String,
+        outputAbsPath: Binding<String>
+    ) {
+        _model = StateObject(wrappedValue: FilePickerModel(host: host))
+        _outputAbsPath = outputAbsPath
+    }
+
+    var body: some View {
+        VStack(spacing: 0) {
+            if model.rootIsLoading {
+                Spacer()
+                ProgressView()
+                    .controlSize(.large)
+                Spacer()
+            } else if let loadError = model.error {
+                Text("\(loadError.description)")
+                    .font(.headline)
+                    .foregroundColor(.red)
+                    .multilineTextAlignment(.center)
+                    .frame(maxWidth: .infinity, maxHeight: .infinity)
+                    .padding()
+            } else {
+                List(selection: $selection) {
+                    ForEach(model.rootEntries) { entry in
+                        FilePickerEntry(entry: entry).tag(entry)
+                    }
+                }.contextMenu(
+                    forSelectionType: FilePickerEntryModel.self,
+                    menu: { _ in },
+                    primaryAction: { selections in
+                        // Per the type of `selection`, this will only ever be a set of
+                        // one entry.
+                        selections.forEach { entry in withAnimation { entry.isExpanded.toggle() } }
+                    }
+                ).listStyle(.sidebar)
+            }
+            Divider()
+            HStack {
+                Spacer()
+                Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction)
+                Button("Select", action: submit).keyboardShortcut(.defaultAction).disabled(selection == nil)
+            }.padding(20)
+        }
+        .onAppear {
+            model.loadRoot()
+        }
+        .onReceive(inspection.notice) { inspection.visit(self, $0) } // ViewInspector
+    }
+
+    private func submit() {
+        guard let selection else { return }
+        outputAbsPath = selection.absolute_path
+        dismiss()
+    }
+}
+
+@MainActor
+class FilePickerModel: ObservableObject {
+    @Published var rootEntries: [FilePickerEntryModel] = []
+    @Published var rootIsLoading: Bool = false
+    @Published var error: ClientError?
+
+    // It's important that `AgentClient` is a reference type (class)
+    // as we were having performance issues with a struct (unless it was a binding).
+    let client: AgentClient
+
+    init(host: String) {
+        client = AgentClient(agentHost: host)
+    }
+
+    func loadRoot() {
+        error = nil
+        rootIsLoading = true
+        Task {
+            defer { rootIsLoading = false }
+            do throws(ClientError) {
+                rootEntries = try await client
+                    .listAgentDirectory(.init(path: [], relativity: .root))
+                    .toModels(client: client)
+            } catch {
+                self.error = error
+            }
+        }
+    }
+}
+
+struct FilePickerEntry: View {
+    @ObservedObject var entry: FilePickerEntryModel
+
+    var body: some View {
+        Group {
+            if entry.dir {
+                directory
+            } else {
+                Label(entry.name, systemImage: "doc")
+                    .help(entry.absolute_path)
+                    .selectionDisabled()
+                    .foregroundColor(.secondary)
+            }
+        }
+    }
+
+    private var directory: some View {
+        DisclosureGroup(isExpanded: $entry.isExpanded) {
+            if let entries = entry.entries {
+                ForEach(entries) { entry in
+                    FilePickerEntry(entry: entry).tag(entry)
+                }
+            }
+        } label: {
+            Label {
+                Text(entry.name)
+                ZStack {
+                    ProgressView().controlSize(.small).opacity(entry.isLoading && entry.error == nil ? 1 : 0)
+                    Image(systemName: "exclamationmark.triangle.fill")
+                        .opacity(entry.error != nil ? 1 : 0)
+                }
+            } icon: {
+                Image(systemName: "folder")
+            }.help(entry.error != nil ? entry.error!.description : entry.absolute_path)
+        }
+    }
+}
+
+@MainActor
+class FilePickerEntryModel: Identifiable, Hashable, ObservableObject {
+    nonisolated let id: [String]
+    let name: String
+    // Components of the path as an array
+    let path: [String]
+    let absolute_path: String
+    let dir: Bool
+
+    let client: AgentClient
+
+    @Published var entries: [FilePickerEntryModel]?
+    @Published var isLoading = false
+    @Published var error: ClientError?
+    @Published private var innerIsExpanded = false
+    var isExpanded: Bool {
+        get { innerIsExpanded }
+        set {
+            if !newValue {
+                withAnimation { self.innerIsExpanded = false }
+            } else {
+                Task {
+                    self.loadEntries()
+                }
+            }
+        }
+    }
+
+    init(
+        name: String,
+        client: AgentClient,
+        absolute_path: String,
+        path: [String],
+        dir: Bool = false,
+        entries: [FilePickerEntryModel]? = nil
+    ) {
+        self.name = name
+        self.client = client
+        self.path = path
+        self.dir = dir
+        self.absolute_path = absolute_path
+        self.entries = entries
+
+        // Swift Arrays are copy on write
+        id = path
+    }
+
+    func loadEntries() {
+        self.error = nil
+        withAnimation { isLoading = true }
+        Task {
+            defer {
+                withAnimation {
+                    isLoading = false
+                    innerIsExpanded = true
+                }
+            }
+            do throws(ClientError) {
+                entries = try await client
+                    .listAgentDirectory(.init(path: path, relativity: .root))
+                    .toModels(client: client)
+            } catch {
+                self.error = error
+            }
+        }
+    }
+
+    nonisolated static func == (lhs: FilePickerEntryModel, rhs: FilePickerEntryModel) -> Bool {
+        lhs.id == rhs.id
+    }
+
+    nonisolated func hash(into hasher: inout Hasher) {
+        hasher.combine(id)
+    }
+}
+
+extension LSResponse {
+    @MainActor
+    func toModels(client: AgentClient) -> [FilePickerEntryModel] {
+        contents.compactMap { entry in
+            // Filter dotfiles from the picker
+            guard !entry.name.hasPrefix(".") else { return nil }
+
+            return FilePickerEntryModel(
+                name: entry.name,
+                client: client,
+                absolute_path: entry.absolute_path_string,
+                path: self.absolute_path + [entry.name],
+                dir: entry.is_dir,
+                entries: nil
+            )
+        }
+    }
+}
diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
index 0e42ea0c..7b902f21 100644
--- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
+++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift
@@ -13,6 +13,7 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
 
     @State private var loading: Bool = false
     @State private var createError: DaemonError?
+    @State private var pickingRemote: Bool = false
 
     var body: some View {
         let agents = vpn.menuState.onlineAgents
@@ -46,7 +47,16 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
                     }
                 }
                 Section {
-                    TextField("Remote Path", text: $remotePath)
+                    HStack(spacing: 5) {
+                        TextField("Remote Path", text: $remotePath)
+                        Spacer()
+                        Button {
+                            pickingRemote = true
+                        } label: {
+                            Image(systemName: "folder")
+                        }.disabled(remoteHostname == nil)
+                            .help(remoteHostname == nil ? "Select a workspace first" : "Open File Picker")
+                    }
                 }
             }.formStyle(.grouped).scrollDisabled(true).padding(.horizontal)
             Divider()
@@ -72,6 +82,9 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
                 set: { if !$0 { createError = nil } }
             )) {} message: {
                 Text(createError?.description ?? "An unknown error occurred.")
+            }.sheet(isPresented: $pickingRemote) {
+                FilePicker(host: remoteHostname!, outputAbsPath: $remotePath)
+                    .frame(width: 300, height: 400)
             }
     }
 
diff --git a/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift
new file mode 100644
index 00000000..61bf2196
--- /dev/null
+++ b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift
@@ -0,0 +1,115 @@
+@testable import Coder_Desktop
+@testable import CoderSDK
+import Mocker
+import SwiftUI
+import Testing
+import ViewInspector
+
+@MainActor
+@Suite(.timeLimit(.minutes(1)))
+struct FilePickerTests {
+    let mockResponse: LSResponse
+
+    init() {
+        mockResponse = LSResponse(
+            absolute_path: ["/"],
+            absolute_path_string: "/",
+            contents: [
+                LSFile(name: "home", absolute_path_string: "/home", is_dir: true),
+                LSFile(name: "tmp", absolute_path_string: "/tmp", is_dir: true),
+                LSFile(name: "etc", absolute_path_string: "/etc", is_dir: true),
+                LSFile(name: "README.md", absolute_path_string: "/README.md", is_dir: false),
+            ]
+        )
+    }
+
+    @Test
+    func testLoadError() async throws {
+        let host = "test-error.coder"
+        let sut = FilePicker(host: host, outputAbsPath: .constant(""))
+        let view = sut
+
+        let url = URL(string: "http://\(host):4")!
+
+        let errorMessage = "Connection failed"
+        Mock(
+            url: url.appendingPathComponent("/api/v0/list-directory"),
+            contentType: .json,
+            statusCode: 500,
+            data: [.post: errorMessage.data(using: .utf8)!]
+        ).register()
+
+        try await ViewHosting.host(view) {
+            try await sut.inspection.inspect { view in
+                try #expect(await eventually { @MainActor in
+                    let text = try view.find(ViewType.Text.self)
+                    return try text.string().contains("Connection failed")
+                })
+            }
+        }
+    }
+
+    @Test
+    func testSuccessfulFileLoad() async throws {
+        let host = "test-success.coder"
+        let sut = FilePicker(host: host, outputAbsPath: .constant(""))
+        let view = sut
+
+        let url = URL(string: "http://\(host):4")!
+
+        try Mock(
+            url: url.appendingPathComponent("/api/v0/list-directory"),
+            statusCode: 200,
+            data: [.post: Client.encoder.encode(mockResponse)]
+        ).register()
+
+        try await ViewHosting.host(view) {
+            try await sut.inspection.inspect { view in
+                try #expect(await eventually { @MainActor in
+                    _ = try view.find(ViewType.List.self)
+                    return true
+                })
+                _ = try view.find(text: "README.md")
+                _ = try view.find(text: "home")
+                let selectButton = try view.find(button: "Select")
+                #expect(selectButton.isDisabled())
+            }
+        }
+    }
+
+    @Test
+    func testDirectoryExpansion() async throws {
+        let host = "test-expansion.coder"
+        let sut = FilePicker(host: host, outputAbsPath: .constant(""))
+        let view = sut
+
+        let url = URL(string: "http://\(host):4")!
+
+        try Mock(
+            url: url.appendingPathComponent("/api/v0/list-directory"),
+            statusCode: 200,
+            data: [.post: Client.encoder.encode(mockResponse)]
+        ).register()
+
+        try await ViewHosting.host(view) {
+            try await sut.inspection.inspect { view in
+                try #expect(await eventually { @MainActor in
+                    _ = try view.find(ViewType.List.self)
+                    return true
+                })
+
+                let disclosureGroup = try view.find(ViewType.DisclosureGroup.self)
+                #expect(view.findAll(ViewType.DisclosureGroup.self).count == 3)
+                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
+                })
+            }
+        }
+    }
+
+    // TODO: The writing of more extensive tests is blocked by ViewInspector,
+    // as it can't select an item in a list...
+}
diff --git a/Coder-Desktop/Coder-DesktopTests/Util.swift b/Coder-Desktop/Coder-DesktopTests/Util.swift
index 4301cbc4..249aa10b 100644
--- a/Coder-Desktop/Coder-DesktopTests/Util.swift
+++ b/Coder-Desktop/Coder-DesktopTests/Util.swift
@@ -57,3 +57,28 @@ class MockFileSyncDaemon: FileSyncDaemon {
 }
 
 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 {
+    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 false
+}
diff --git a/Coder-Desktop/CoderSDK/AgentClient.swift b/Coder-Desktop/CoderSDK/AgentClient.swift
new file mode 100644
index 00000000..ecdd3d43
--- /dev/null
+++ b/Coder-Desktop/CoderSDK/AgentClient.swift
@@ -0,0 +1,7 @@
+public final class AgentClient: Sendable {
+    let client: Client
+
+    public init(agentHost: String) {
+        client = Client(url: URL(string: "http://\(agentHost):4")!)
+    }
+}
diff --git a/Coder-Desktop/CoderSDK/AgentLS.swift b/Coder-Desktop/CoderSDK/AgentLS.swift
new file mode 100644
index 00000000..7110f405
--- /dev/null
+++ b/Coder-Desktop/CoderSDK/AgentLS.swift
@@ -0,0 +1,43 @@
+public extension AgentClient {
+    func listAgentDirectory(_ req: LSRequest) async throws(ClientError) -> LSResponse {
+        let res = try await client.request("/api/v0/list-directory", method: .post, body: req)
+        guard res.resp.statusCode == 200 else {
+            throw client.responseAsError(res)
+        }
+        return try client.decode(LSResponse.self, from: res.data)
+    }
+}
+
+public struct LSRequest: Sendable, Codable {
+    // e.g. [], ["repos", "coder"]
+    public let path: [String]
+    // Whether the supplied path is relative to the user's home directory,
+    // or the root directory.
+    public let relativity: LSRelativity
+
+    public init(path: [String], relativity: LSRelativity) {
+        self.path = path
+        self.relativity = relativity
+    }
+
+    public enum LSRelativity: String, Sendable, Codable {
+        case root
+        case home
+    }
+}
+
+public struct LSResponse: Sendable, Codable {
+    public let absolute_path: [String]
+    // e.g. Windows: "C:\\Users\\coder"
+    //      Linux: "/home/coder"
+    public let absolute_path_string: String
+    public let contents: [LSFile]
+}
+
+public struct LSFile: Sendable, Codable {
+    public let name: String
+    // e.g. "C:\\Users\\coder\\hello.txt"
+    //      "/home/coder/hello.txt"
+    public let absolute_path_string: String
+    public let is_dir: Bool
+}
diff --git a/Coder-Desktop/CoderSDK/Client.swift b/Coder-Desktop/CoderSDK/Client.swift
index 239db14a..98e1c8a9 100644
--- a/Coder-Desktop/CoderSDK/Client.swift
+++ b/Coder-Desktop/CoderSDK/Client.swift
@@ -1,6 +1,6 @@
 import Foundation
 
-public struct Client {
+public struct Client: Sendable {
     public let url: URL
     public var token: String?
     public var headers: [HTTPHeader]