diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,7 +1,11 @@
 # Changelog for status-notifier-item
 
-## 0.3.2.8 - 2026-02-13
-- Relax dependency bounds for GHC 9.12 (`template-haskell`, `optparse-applicative`).
+## 0.3.2.9 - 2026-02-17
+- Host: eliminate duplicate `ItemAdded` deliveries to newly-registered update
+  handlers by making handler registration + initial replay atomic with respect
+  to item map updates.
+- Add a deterministic host integration test that reproduces and guards against
+  duplicate `ItemAdded` events from replay/live-update races.
 
 ## 0.3.2.7 - 2026-02-13
 - Watcher: default `--log-level` is now INFO.
diff --git a/src/StatusNotifier/Host/Service.hs b/src/StatusNotifier/Host/Service.hs
--- a/src/StatusNotifier/Host/Service.hs
+++ b/src/StatusNotifier/Host/Service.hs
@@ -158,9 +158,13 @@
 
       addHandler handler = do
         unique <- newUnique
-        modifyMVar_ updateHandlersVar (return . ((unique, handler):))
-        let doUpdateForInfo info = doUpdateForHandler ItemAdded info (unique, handler)
-        readMVar itemInfoMapVar >>= mapM_ doUpdateForInfo
+        modifyMVar_ itemInfoMapVar $ \itemInfoMap -> do
+          -- Register and replay under the item map lock so a concurrent add
+          -- cannot be delivered both live and via replay.
+          modifyMVar_ updateHandlersVar (return . ((unique, handler):))
+          let doUpdateForInfo info = doUpdateForHandler ItemAdded info (unique, handler)
+          mapM_ doUpdateForInfo itemInfoMap
+          return itemInfoMap
         return unique
 
       removeHandler unique =
diff --git a/status-notifier-item.cabal b/status-notifier-item.cabal
--- a/status-notifier-item.cabal
+++ b/status-notifier-item.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           status-notifier-item
-version:        0.3.2.8
+version:        0.3.2.9
 synopsis:       A wrapper over the StatusNotifierItem/libappindicator dbus specification
 description:    Please see the README on Github at <https://github.com/IvanMalison/status-notifier-item#readme>
 category:       Desktop
@@ -53,7 +53,7 @@
     , filepath <1.6
     , hslogger <1.4
     , lens <5.4
-    , template-haskell <2.24
+    , template-haskell <2.23
     , text <2.2
     , transformers <0.7
     , vector <0.14
@@ -68,7 +68,7 @@
   build-depends:
       base >=4.7 && <5
     , dbus >=1.2.1 && <2
-    , optparse-applicative <0.20
+    , optparse-applicative <0.19
     , status-notifier-item
   default-language: Haskell2010
 
@@ -80,7 +80,7 @@
       ./item
   build-depends:
       base >=4.7 && <5
-    , optparse-applicative <0.20
+    , optparse-applicative <0.19
     , status-notifier-item
   default-language: Haskell2010
 
@@ -95,7 +95,7 @@
     , dbus >=1.2.1 && <2
     , dbus-hslogger >=0.1.0.1 && <0.2
     , hslogger <1.4
-    , optparse-applicative <0.20
+    , optparse-applicative <0.19
     , status-notifier-item
   default-language: Haskell2010
 
diff --git a/test/HostSpec.hs b/test/HostSpec.hs
--- a/test/HostSpec.hs
+++ b/test/HostSpec.hs
@@ -2,9 +2,19 @@
 
 module HostSpec (spec) where
 
-import Control.Concurrent (newChan, readChan, writeChan)
+import Control.Concurrent
+  ( forkIO
+  , newChan
+  , newEmptyMVar
+  , putMVar
+  , readChan
+  , takeMVar
+  , threadDelay
+  , tryPutMVar
+  , writeChan
+  )
 import Control.Exception (finally)
-import Control.Monad (replicateM)
+import Control.Monad (replicateM, void)
 import Data.List (sort)
 import qualified Data.Map.Strict as Map
 import DBus (busName_, objectPath_)
@@ -218,6 +228,51 @@
           current <- itemInfoMap host
           pure $ not $ Map.member (busName_ "org.test.HostWatcherOwnerChange") current
       removed `shouldBe` True
+
+    it "does not emit duplicate ItemAdded when addUpdateHandler races with replay" $ \() -> do
+      _watcher <- startWatcher
+      hostClient <- connectSession
+      Just host <- build defaultParams {dbusClient = Just hostClient, uniqueIdentifier = "host-j"}
+
+      itemClient <- connectSession
+      buildStarted <- newEmptyMVar
+      continueBuild <- newEmptyMVar
+      let itemName = "org.test.HostHandlerRace"
+          iface =
+            Interface
+              { interfaceName = "org.kde.StatusNotifierItem"
+              , interfaceMethods = []
+              , interfaceProperties =
+                  [ readOnlyProperty "IconName" $ do
+                      void $ tryPutMVar buildStarted ()
+                      takeMVar continueBuild
+                      pure ("folder" :: String)
+                  , readOnlyProperty "OverlayIconName" (pure ("" :: String))
+                  , readOnlyProperty "ItemIsMenu" (pure False)
+                  ]
+              , interfaceSignals = []
+              }
+      export itemClient (objectPath_ defaultPath) iface
+      _ <- requestName itemClient (busName_ itemName) []
+      WatcherClient.registerStatusNotifierItem itemClient itemName
+        `shouldReturn` Right ()
+
+      -- Host is now blocked building the item while holding itemInfoMapVar.
+      takeMVar buildStarted
+
+      events <- newChan
+      _ <- forkIO $ do
+        threadDelay 50000
+        putMVar continueBuild ()
+      _ <- addUpdateHandler host (\ut info -> writeChan events (ut, itemServiceName info))
+
+      first <- timeout 1500000 (readChan events)
+      first `shouldBe` Just (ItemAdded, busName_ itemName)
+      second <- timeout 300000 (readChan events)
+      second `shouldBe` Nothing
+
+      _ <- releaseName itemClient (busName_ itemName)
+      pure ()
 
   describe "propertyUpdateFailureLogLevel" $ do
     it "returns DEBUG when there are successful updates" $ do
