diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5831353..2c32c64 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [1.0.8] - 2026-05-04
+
+### Fixed
+- Single-instance protection: launching PassKey when it is already running in the system tray
+ now brings the existing window to the foreground instead of silently starting (and immediately
+ hiding) a second invisible process. This was the primary cause of the "app installs but
+ doesn't start" symptom reported on v1.0.7.
+- Startup crash log: if the application fails to initialise before the main window is created,
+ a diagnostic file is written to `%LOCALAPPDATA%\PassKey\startup-crash.log`. This makes
+ silent startup failures diagnosable without attaching a debugger.
+
## [1.0.7] - 2026-05-02
### Fixed
@@ -117,7 +128,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Giuseppe Imperato** — concept, design, product decisions
- **[Claude](https://www.anthropic.com/claude) by Anthropic** — architecture, implementation, documentation
-[Unreleased]: https://github.com/pexatar/PassKey/compare/v1.0.7...HEAD
+[Unreleased]: https://github.com/pexatar/PassKey/compare/v1.0.8...HEAD
+[1.0.8]: https://github.com/pexatar/PassKey/compare/v1.0.7...v1.0.8
[1.0.7]: https://github.com/pexatar/PassKey/compare/v1.0.6...v1.0.7
[1.0.6]: https://github.com/pexatar/PassKey/compare/v1.0.5...v1.0.6
[1.0.5]: https://github.com/pexatar/PassKey/compare/v1.0.0...v1.0.5
diff --git a/Installer/PassKey.iss b/Installer/PassKey.iss
index 4436142..6663869 100644
--- a/Installer/PassKey.iss
+++ b/Installer/PassKey.iss
@@ -4,8 +4,8 @@
[Setup]
AppId={{A7F3C2D1-8E4B-4F9A-B6D5-3C1E7A2F0D84}
AppName=PassKey
-AppVersion=1.0.7
-AppVerName=PassKey 1.0.7
+AppVersion=1.0.8
+AppVerName=PassKey 1.0.8
AppPublisher=Giuseppe Imperato
AppPublisherURL=https://github.com/pexatar/PassKey
AppSupportURL=https://github.com/pexatar/PassKey/issues
diff --git a/README.md b/README.md
index 2967e84..784dcc4 100644
--- a/README.md
+++ b/README.md
@@ -57,8 +57,8 @@ PassKey stores your passwords, credit cards, identities, and secure notes in a l
| Version | Platform | Type |
|---------|----------|------|
-| [v1.0.7](https://github.com/pexatar/PassKey/releases/tag/v1.0.7) | Windows x64 | Installer EXE |
-| [v1.0.7](https://github.com/pexatar/PassKey/releases/tag/v1.0.7) | Windows x64 | Portable ZIP |
+| [v1.0.8](https://github.com/pexatar/PassKey/releases/tag/v1.0.8) | Windows x64 | Installer EXE |
+| [v1.0.8](https://github.com/pexatar/PassKey/releases/tag/v1.0.8) | Windows x64 | Portable ZIP |
> **Requirements:** Windows 10 version 1809 (build 17763) or later, x64 processor.
> No .NET runtime required — PassKey is fully self-contained.
diff --git a/src/PassKey.Desktop/PassKey.Desktop.csproj b/src/PassKey.Desktop/PassKey.Desktop.csproj
index eab9afb..0ed73e7 100644
--- a/src/PassKey.Desktop/PassKey.Desktop.csproj
+++ b/src/PassKey.Desktop/PassKey.Desktop.csproj
@@ -15,9 +15,9 @@
true
false
PassKey.Desktop
- 1.0.7
- 1.0.7.0
- 1.0.7.0
+ 1.0.8
+ 1.0.8.0
+ 1.0.8.0
app.manifest
Assets\PassKey.ico
DISABLE_XAML_GENERATED_MAIN
diff --git a/src/PassKey.Desktop/Program.cs b/src/PassKey.Desktop/Program.cs
index 54c826b..8882d9f 100644
--- a/src/PassKey.Desktop/Program.cs
+++ b/src/PassKey.Desktop/Program.cs
@@ -1,3 +1,4 @@
+using System.Reflection;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
@@ -5,18 +6,116 @@ namespace PassKey.Desktop;
public static class Program
{
+ // Named event used for single-instance detection and window-restore signalling.
+ // AutoReset: each Set() wakes exactly one WaitOne() call.
+ private const string ShowEventName = "PassKey.Desktop.ShowWindow";
+
+ // Path to the startup crash log — written only when Application.Start() throws.
+ private static readonly string CrashLogPath = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "PassKey", "startup-crash.log");
+
[STAThread]
public static void Main(string[] args)
{
- WinRT.ComWrappersSupport.InitializeComWrappers();
+ // ── Single-instance ──────────────────────────────────────────────────
+ // If another instance is already running, signal it to restore its window
+ // and exit immediately. This prevents the "app doesn't appear to start"
+ // symptom when PassKey is already running minimised in the system tray.
+
+ EventWaitHandle? showEvent = null;
+ bool isFirstInstance = true;
+
+ try
+ {
+ showEvent = new EventWaitHandle(
+ initialState: false,
+ mode: EventResetMode.AutoReset,
+ name: ShowEventName,
+ createdNew: out isFirstInstance);
+ }
+ catch
+ {
+ // Named-event creation failed (unusual system configuration) —
+ // treat as first instance so the app always starts.
+ }
+ if (!isFirstInstance)
+ {
+ try { showEvent?.Set(); } catch { }
+ showEvent?.Dispose();
+ return;
+ }
+
+ // Start a daemon thread that listens for subsequent launch attempts and
+ // restores the main window via the UI dispatcher whenever signalled.
+ if (showEvent != null)
+ {
+ new Thread(() => MonitorShowEvent(showEvent))
+ {
+ IsBackground = true,
+ Name = "PassKey.SingleInstance.Monitor"
+ }.Start();
+ }
+
+ // ── App startup ──────────────────────────────────────────────────────
// Note: ApplySavedLanguage() is called inside App() constructor before
// InitializeComponent(), following the official Microsoft docs pattern.
- Application.Start(p =>
+ try
+ {
+ WinRT.ComWrappersSupport.InitializeComWrappers();
+
+ Application.Start(p =>
+ {
+ var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
+ SynchronizationContext.SetSynchronizationContext(context);
+ _ = new App();
+ });
+ }
+ catch (Exception ex)
+ {
+ // Write a crash log so users can share it for support.
+ // The log is only created on an actual startup failure.
+ WriteCrashLog(ex);
+ throw;
+ }
+ }
+
+ ///
+ /// Background loop: waits for the named event to be signalled (a subsequent
+ /// launch attempt), then marshals a call
+ /// onto the UI thread so the window becomes visible again.
+ ///
+ private static void MonitorShowEvent(EventWaitHandle showEvent)
+ {
+ while (true)
+ {
+ try
+ {
+ showEvent.WaitOne();
+ App.MainWindow?.DispatcherQueue.TryEnqueue(() => App.MainWindow?.RestoreWindow());
+ }
+ catch
+ {
+ // Silently ignore — this is a non-critical background thread.
+ }
+ }
+ }
+
+ private static void WriteCrashLog(Exception ex)
+ {
+ try
+ {
+ var logDir = Path.GetDirectoryName(CrashLogPath)!;
+ Directory.CreateDirectory(logDir);
+ var version = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "unknown";
+ File.AppendAllText(
+ CrashLogPath,
+ $"[{DateTime.Now:s}] PassKey v{version} startup crash:{Environment.NewLine}{ex}{Environment.NewLine}{Environment.NewLine}");
+ }
+ catch
{
- var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
- SynchronizationContext.SetSynchronizationContext(context);
- _ = new App();
- });
+ // If we can't write the log, there is nothing more we can do.
+ }
}
}