diff --git a/src/ClaudeDo.Installer/Core/InstallModeDetector.cs b/src/ClaudeDo.Installer/Core/InstallModeDetector.cs
index 69616ab..50b93c6 100644
--- a/src/ClaudeDo.Installer/Core/InstallModeDetector.cs
+++ b/src/ClaudeDo.Installer/Core/InstallModeDetector.cs
@@ -32,6 +32,13 @@ public sealed class InstallModeDetector
return new DetectedState(InstallerMode.Config, manifest, release, latestVersion);
}
+ ///
+ /// Returns true only when both versions parse as System.Version (major.minor[.build[.revision]])
+ /// AND latest > current. Semver pre-release tags like "0.2.0-beta" fail to parse and are
+ /// treated as "not newer" — the user drops into Config mode with no update offered.
+ /// This is deliberate: offering an update we can't compare is worse than silently skipping it.
+ /// If the project starts shipping pre-release tags, revisit this.
+ ///
private static bool IsNewer(string latest, string current)
{
if (!Version.TryParse(latest, out var lv)) return false;
diff --git a/tests/ClaudeDo.Installer.Tests/InstallModeDetectorTests.cs b/tests/ClaudeDo.Installer.Tests/InstallModeDetectorTests.cs
index 521be81..223a0a7 100644
--- a/tests/ClaudeDo.Installer.Tests/InstallModeDetectorTests.cs
+++ b/tests/ClaudeDo.Installer.Tests/InstallModeDetectorTests.cs
@@ -102,4 +102,23 @@ public sealed class InstallModeDetectorTests : IDisposable
Assert.Equal(InstallerMode.Config, state.Mode);
}
+
+ [Fact]
+ public async Task Detect_Config_WhenInstalledVersion_IsUnparseable()
+ {
+ // install.json has been tampered with or written by an older installer with a
+ // version string we can't compare. Must not crash; must land on Config (no update).
+ InstallManifestStore.Write(_tempDir,
+ new InstallManifest("garbage", _tempDir, _tempDir, DateTimeOffset.UtcNow));
+
+ var fake = new FakeReleaseClient
+ {
+ Release = new GiteaRelease("v0.2.0", "v0.2.0", Array.Empty())
+ };
+ var detector = new InstallModeDetector(fake);
+
+ var state = await detector.DetectAsync(_tempDir, CancellationToken.None);
+
+ Assert.Equal(InstallerMode.Config, state.Mode);
+ }
}