diff --git a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
index c296826..9fddf53 100644
--- a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
+++ b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
@@ -10,6 +10,7 @@
+
diff --git a/src/ClaudeDo.Ui/Services/InstallerLocator.cs b/src/ClaudeDo.Ui/Services/InstallerLocator.cs
new file mode 100644
index 0000000..de15542
--- /dev/null
+++ b/src/ClaudeDo.Ui/Services/InstallerLocator.cs
@@ -0,0 +1,47 @@
+namespace ClaudeDo.Ui.Services;
+
+public sealed class InstallerLocator
+{
+ private const string InstallJson = "install.json";
+ private const string InstallerExe = "ClaudeDo.Installer.exe";
+ private const string UninstallerSubdir = "uninstaller";
+
+ public string? Find()
+ => FindByWalkingUp(AppContext.BaseDirectory) ?? FindByRegistry();
+
+ public string? FindByWalkingUp(string startDir)
+ {
+ var dir = new DirectoryInfo(startDir);
+ while (dir is not null)
+ {
+ var manifest = Path.Combine(dir.FullName, InstallJson);
+ if (File.Exists(manifest))
+ {
+ var candidate = Path.Combine(dir.FullName, UninstallerSubdir, InstallerExe);
+ return File.Exists(candidate) ? candidate : null;
+ }
+ dir = dir.Parent;
+ }
+ return null;
+ }
+
+ [System.Runtime.Versioning.SupportedOSPlatform("windows")]
+ public string? FindByRegistry()
+ {
+ if (!OperatingSystem.IsWindows()) return null;
+
+ try
+ {
+ using var key = Microsoft.Win32.Registry.LocalMachine
+ .OpenSubKey(@"Software\Microsoft\Windows\CurrentVersion\Uninstall\ClaudeDo");
+ var location = key?.GetValue("InstallLocation") as string;
+ if (string.IsNullOrEmpty(location)) return null;
+ var candidate = Path.Combine(location, UninstallerSubdir, InstallerExe);
+ return File.Exists(candidate) ? candidate : null;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
diff --git a/tests/ClaudeDo.Ui.Tests/Services/InstallerLocatorTests.cs b/tests/ClaudeDo.Ui.Tests/Services/InstallerLocatorTests.cs
new file mode 100644
index 0000000..4ddc948
--- /dev/null
+++ b/tests/ClaudeDo.Ui.Tests/Services/InstallerLocatorTests.cs
@@ -0,0 +1,58 @@
+using ClaudeDo.Ui.Services;
+
+namespace ClaudeDo.Ui.Tests.Services;
+
+public class InstallerLocatorTests : IDisposable
+{
+ private readonly string _root;
+
+ public InstallerLocatorTests()
+ {
+ _root = Path.Combine(Path.GetTempPath(), "ClaudeDo.Ui.Tests-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(_root);
+ }
+
+ public void Dispose() { try { Directory.Delete(_root, true); } catch { } }
+
+ [Fact]
+ public void Find_WalkUpFromAppDir_ToInstallJsonSibling()
+ {
+ var installDir = Path.Combine(_root, "ClaudeDo");
+ var appDir = Path.Combine(installDir, "app");
+ var uninstallerDir = Path.Combine(installDir, "uninstaller");
+ Directory.CreateDirectory(appDir);
+ Directory.CreateDirectory(uninstallerDir);
+
+ File.WriteAllText(Path.Combine(installDir, "install.json"), "{}");
+ var installerPath = Path.Combine(uninstallerDir, "ClaudeDo.Installer.exe");
+ File.WriteAllText(installerPath, "x");
+
+ var locator = new InstallerLocator();
+ var found = locator.FindByWalkingUp(appDir);
+
+ Assert.Equal(installerPath, found);
+ }
+
+ [Fact]
+ public void Find_ReturnsNullWhenNoInstallJson()
+ {
+ var appDir = Path.Combine(_root, "somewhere", "app");
+ Directory.CreateDirectory(appDir);
+
+ var locator = new InstallerLocator();
+ Assert.Null(locator.FindByWalkingUp(appDir));
+ }
+
+ [Fact]
+ public void Find_ReturnsNullWhenInstallerMissingFromUninstallerDir()
+ {
+ var installDir = Path.Combine(_root, "ClaudeDo");
+ var appDir = Path.Combine(installDir, "app");
+ Directory.CreateDirectory(appDir);
+ Directory.CreateDirectory(Path.Combine(installDir, "uninstaller"));
+ File.WriteAllText(Path.Combine(installDir, "install.json"), "{}");
+
+ var locator = new InstallerLocator();
+ Assert.Null(locator.FindByWalkingUp(appDir));
+ }
+}