diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## 0.4.0.0 - 2025-07-18
+
+### Changed
+
+ - `init` now installs synchronous signal handlers for you. You may remove your call to `installDefaultSigINTHandler`.
+ - The `nix-main` library dependency has been dropped. (if you don't also use `hercules-ci-cnix-expr`)
+
 ## 0.3.7.0 - 2025-05-05
 
 ### Added
diff --git a/cbits/signals.cxx b/cbits/signals.cxx
new file mode 100644
--- /dev/null
+++ b/cbits/signals.cxx
@@ -0,0 +1,65 @@
+#include <csignal>
+#include <unistd.h>
+
+#include <nix/util/signals.hh>
+
+#include "signals.hxx"
+
+extern "C" {
+
+// Storage for previous signal handlers
+static struct sigaction old_sigint_action;
+static struct sigaction old_sigterm_action;
+static struct sigaction old_sighup_action;
+
+// Get pointer to the old action structure for a given signal
+static struct sigaction* get_old_action(int sig) {
+    switch (sig) {
+        case SIGINT: return &old_sigint_action;
+        case SIGTERM: return &old_sigterm_action;
+        case SIGHUP: return &old_sighup_action;
+        default: return nullptr;
+    }
+}
+
+// C signal handler that runs immediately in signal context
+// This calls nix::triggerInterrupt() synchronously then defers to the previous handler
+static void hercules_nix_signal_handler(int sig) {
+    // Set the Nix interrupt flag immediately
+    // This ensures checkInterrupt() calls after EINTR will work correctly
+    nix::unix::triggerInterrupt();
+    
+    // Call the previous handler to preserve existing behavior
+    struct sigaction* old_action = get_old_action(sig);
+    if (old_action && old_action->sa_handler != SIG_DFL && old_action->sa_handler != SIG_IGN) {
+        if (old_action->sa_flags & SA_SIGINFO) {
+            // Previous handler expects siginfo_t
+            // We can't call it properly without the siginfo, so just call the basic handler
+            if (old_action->sa_handler) {
+                old_action->sa_handler(sig);
+            }
+        } else {
+            // Previous handler is a simple function pointer
+            old_action->sa_handler(sig);
+        }
+    }
+}
+
+// Install our C signal handler for the given signal, chaining to the previous handler
+// Returns 0 on success, -1 on error
+int hercules_install_signal_handler(int sig) {
+    struct sigaction new_action;
+    new_action.sa_handler = hercules_nix_signal_handler;
+    sigemptyset(&new_action.sa_mask);
+    new_action.sa_flags = 0;  // Ensure SA_RESTART is not set
+    
+    struct sigaction* old_action = get_old_action(sig);
+    if (!old_action) {
+        return -1;  // Unknown signal
+    }
+    
+    // Install new handler and save the old one
+    return sigaction(sig, &new_action, old_action);
+}
+
+} // extern "C"
diff --git a/cbits/signals.hxx b/cbits/signals.hxx
new file mode 100644
--- /dev/null
+++ b/cbits/signals.hxx
@@ -0,0 +1,17 @@
+#ifndef HERCULES_SIGNALS_H
+#define HERCULES_SIGNALS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+// Install our C signal handler for the given signal
+// This handler calls nix::triggerInterrupt() synchronously
+// Returns 0 on success, -1 on error
+int hercules_install_signal_handler(int sig);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif // HERCULES_SIGNALS_H
diff --git a/hercules-ci-cnix-store.cabal b/hercules-ci-cnix-store.cabal
--- a/hercules-ci-cnix-store.cabal
+++ b/hercules-ci-cnix-store.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.4
 
 name:           hercules-ci-cnix-store
-version:        0.3.7.0
+version:        0.4.0.0
 synopsis:       Haskell bindings for Nix's libstore
 category:       Nix
 homepage:       https://docs.hercules-ci.com
@@ -19,27 +19,15 @@
   type: git
   location: https://github.com/hercules-ci/hercules-ci-agent
 
--- Deprecated
-flag nix-2_4
-  description: Build for Nix >=2.4pre*
-  default: True
 
-flag nix-2_15
-  description: Build for Nix >=2.15.0
-  default: False
-
 flag ide
   description: Whether to enable IDE workarounds. You shouldn't need this.
   default: False
 
 -- match the C++ language standard Nix is using
 common cxx-opts
-  if flag(nix-2_15)
-    cxx-options:
-      -std=c++2a
-  else
-    cxx-options:
-      -std=c++17
+  cxx-options:
+    -std=c++2a
 
   cxx-options:
     -Wall
@@ -54,16 +42,11 @@
   if impl(ghc >= 8.10)
     ghc-options:
       -optcxx-Wall
-    if flag(nix-2_15)
-      ghc-options:
-        -optcxx-std=c++2a
-    else
-      ghc-options:
-        -optcxx-std=c++17
+      -optcxx-std=c++2a
   else
     -- Remove soon
     ghc-options:
-      -optc-std=c++17
+      -optc-std=c++2a
       -optc-Wall
     if os(darwin)
       ghc-options: -pgmc=clang++
@@ -94,15 +77,17 @@
       Hercules.CNix.Verbosity
   include-dirs:
       include
+      cbits
   pkgconfig-depends:
-      nix-store (>= 2.4 && < 2.19) || (>= 2.19.3 && < 2.21) || (>= 2.24.10 && < 2.25) || (>= 2.28 && < 2.29)
-    , nix-main >= 2.4 && < 2.19 || (>= 2.19.3 && < 2.21) || (>= 2.24.10 && < 2.25) || (>= 2.28 && < 2.29)
+      nix-store >= 2.28 && < 2.29
   install-includes:
       hercules-ci-cnix/store.hxx
       hercules-ci-cnix/string.hxx
   hs-source-dirs: src
   cxx-sources:
       cbits/string.cxx
+      cbits/signals.cxx
+      cbits/signals.hxx
   build-depends:
       base >= 4.7 && <5
     , inline-c
diff --git a/include/hercules-ci-cnix/store.hxx b/include/hercules-ci-cnix/store.hxx
--- a/include/hercules-ci-cnix/store.hxx
+++ b/include/hercules-ci-cnix/store.hxx
@@ -1,11 +1,7 @@
 
 #pragma once
 
-#if NIX_IS_AT_LEAST(2,28,0)
 #include <nix/store/path-info.hh>
-#else
-#include <nix/path-info.hh>
-#endif
 
 typedef nix::ref<nix::Store> refStore;
 
@@ -15,8 +11,4 @@
 typedef nix::PathSet::iterator PathSetIterator;
 typedef nix::ref<const nix::ValidPathInfo> refValidPathInfo;
 
-#if NIX_IS_AT_LEAST(2,18,0)
 typedef nix::DerivedPathMap<std::set<nix::OutputName>>::Map::iterator DerivationInputsIterator;
-#else
-typedef nix::DerivationInputs::iterator DerivationInputsIterator;
-#endif
diff --git a/src/Hercules/CNix.hs b/src/Hercules/CNix.hs
--- a/src/Hercules/CNix.hs
+++ b/src/Hercules/CNix.hs
@@ -23,6 +23,7 @@
 
 import Data.ByteString.Unsafe (unsafePackMallocCString)
 import Hercules.CNix.Store
+import qualified Hercules.CNix.Util
 import Hercules.CNix.Verbosity
   ( Verbosity (Debug, Talkative),
     setVerbosity,
@@ -34,28 +35,11 @@
 
 C.context context
 
-#if NIX_IS_AT_LEAST(2, 28, 0)
-
 C.include "<nix/util/config-global.hh>"
 C.include "<nix/store/derivations.hh>"
 C.include "<nix/store/globals.hh>"
 C.include "<nix/main/shared.hh>"
 
-#else
-C.include "<stdio.h>"
-C.include "<cstring>"
-C.include "<math.h>"
-C.include "<nix/config.h>"
-C.include "<nix/shared.hh>"
-C.include "<nix/store-api.hh>"
-C.include "<nix/get-drvs.hh>"
-C.include "<nix/derivations.hh>"
-C.include "<nix/globals.hh>"
-#  if NIX_IS_AT_LEAST(2, 24, 0)
-C.include "<config-global.hh>"
-#  endif
-#endif
-
 C.include "<gc/gc.h>"
 C.include "<gc/gc_cpp.h>"
 C.include "<gc/gc_allocator.h>"
@@ -66,12 +50,16 @@
 C.using "namespace nix"
 C.using "namespace hercules_ci_cnix"
 
+-- | Initialize the Nix store library. If you also use the Nix evaluator, you should
+-- call 'Hercules.CNix.Expr.init' instead.
 init :: IO ()
-init =
+init = do
   void
     [C.throwBlock| void {
-      nix::initNix();
+      nix::initLibStore();
     } |]
+  -- Install signal handlers using Haskell primitives (replaces nix::unix::startSignalHandlerThread)
+  Hercules.CNix.Util.installDefaultSigINTHandler
 
 setTalkative :: IO ()
 setTalkative = setVerbosity Talkative
diff --git a/src/Hercules/CNix/Exception.hs b/src/Hercules/CNix/Exception.hs
--- a/src/Hercules/CNix/Exception.hs
+++ b/src/Hercules/CNix/Exception.hs
@@ -17,15 +17,13 @@
 
 C.context context
 
-#if NIX_IS_AT_LEAST(2, 28, 0)
-C.include "<nix/main/shared.hh>"
 C.include "<nix/store/globals.hh>"
-#else
-C.include "<nix/config.h>"
-C.include "<nix/shared.hh>"
-C.include "<nix/globals.hh>"
-#endif
+C.include "<nix/util/logging.hh>"
+C.include "<nix/util/signals.hh>"
+C.include "<nix/util/error.hh>"
 
+C.using "namespace nix"
+
 -- | Log C++ exceptions and call 'exitWith' the way Nix would exit when an
 -- exception occurs.
 handleExceptions :: IO a -> IO a
@@ -57,7 +55,29 @@
   [C.throwBlock| int {
     auto & eptr = *$fptr-ptr:(std::exception_ptr *eptr);
     std::string programName($bs-ptr:programName, $bs-len:programName);
-    return nix::handleExceptions(programName, [&]() {
-      std::rethrow_exception(eptr);
-    });
+    // Based on nix::handleExceptions, but without the libmain-specific stuff
+    std::string error = ANSI_RED "error:" ANSI_NORMAL " ";
+    try {
+      try {
+        try {
+          std::rethrow_exception(eptr);
+        } catch (...) {
+          // Avoid throwing another interrupt error in the print routines that actually catch this.
+          setInterruptThrown();
+          throw;
+        }
+      } catch (BaseError & e) {
+          logError(e.info());
+          return e.info().status;
+      } catch (std::bad_alloc & e) {
+          printError(error + "out of memory");
+          return 1;
+      } catch (std::exception & e) {
+          printError(error + e.what());
+          return 1;
+      }
+    } catch (...) {
+      // Nix would exit with 1, but this is a truly exceptional error, so we return -1
+      return -1;
+    }
   }|]
diff --git a/src/Hercules/CNix/Settings.hs b/src/Hercules/CNix/Settings.hs
--- a/src/Hercules/CNix/Settings.hs
+++ b/src/Hercules/CNix/Settings.hs
@@ -40,12 +40,7 @@
 C.include "<set>"
 C.include "<string>"
 
-#if NIX_IS_AT_LEAST(2, 28, 0)
 C.include "<nix/store/globals.hh>"
-#else
-C.include "<nix/config.h>"
-C.include "<nix/globals.hh>"
-#endif
 
 C.include "hercules-ci-cnix/string.hxx"
 
diff --git a/src/Hercules/CNix/Store.hs b/src/Hercules/CNix/Store.hs
--- a/src/Hercules/CNix/Store.hs
+++ b/src/Hercules/CNix/Store.hs
@@ -189,9 +189,6 @@
 
 C.include "<variant>"
 
-#if NIX_IS_AT_LEAST(2, 28, 0)
-
-C.include "<nix/main/shared.hh>"
 C.include "<nix/store/store-api.hh>"
 C.include "<nix/store/derivations.hh>"
 C.include "<nix/store/globals.hh>"
@@ -200,43 +197,6 @@
 C.include "<nix/store/path-with-outputs.hh>"
 C.include "<nix/util/signals.hh>"
 
-#else
-
-C.include "<nix/config.h>"
-
-C.include "<nix/shared.hh>"
-
-C.include "<nix/store-api.hh>"
-
-C.include "<nix/get-drvs.hh>"
-
-C.include "<nix/derivations.hh>"
-
-C.include "<nix/globals.hh>"
-
-C.include "<nix/path.hh>"
-
-C.include "<nix/worker-protocol.hh>"
-
-C.include "<nix/path-with-outputs.hh>"
-
-C.include "<nix/hash.hh>"
-
-#  if NIX_IS_AT_LEAST(2,19,0)
-
-C.include "<nix/signals.hh>"
-
-C.include "<nix/hash.hh>"
-
-#  endif
-
-#  if ! NIX_IS_AT_LEAST(2,20,0)
-
-C.include "<nix/nar-info-disk-cache.hh>"
-
-#  endif
-#endif
-
 C.include "hercules-ci-cnix/store.hxx"
 
 C.include "hercules-ci-cnix/string.hxx"
@@ -604,7 +564,6 @@
   deriving (Eq, Show)
 
 getDerivationOutputs :: Store -> ByteString -> Derivation -> IO [DerivationOutput]
-#if NIX_IS_AT_LEAST(2, 24, 0)
 getDerivationOutputs (Store store) drvName (Derivation derivationFPtr) =
   withForeignPtr derivationFPtr \derivation ->
   withDelete
@@ -775,357 +734,7 @@
                     )
           )
             <*> continue
-#else
--- < 2.24
-getDerivationOutputs (Store store) drvName (Derivation derivationFPtr) =
-  withForeignPtr derivationFPtr \derivation ->
-  withDelete
-    [C.exp| DerivationOutputsIterator* {
-      new DerivationOutputsIterator($(Derivation *derivation)->outputs.begin())
-    }|] \i ->
-    fix $ \continue -> do
-      isEnd <- (0 /=) <$> [C.exp| bool { *$(DerivationOutputsIterator *i) == $(Derivation *derivation)->outputs.end() }|]
-      if isEnd
-        then pure []
-        else
-          ( mask_ do
-              alloca \nameP -> alloca \pathP -> alloca \typP -> alloca \fimP ->
-                alloca \hashTypeP -> alloca \hashValueP -> alloca \hashSizeP -> do
-                  [C.throwBlock| void {
-                    Store &store = **$(refStore *store);
-                    std::string drvName = std::string($bs-ptr:drvName, $bs-len:drvName);
-                    nix::DerivationOutputs::iterator &i = *$(DerivationOutputsIterator *i);
-                    const char *&name = *$(const char **nameP);
-                    int &typ = *$(int *typP);
-                    StorePath *& path = *$(nix::StorePath **pathP);
-                    int &fim = *$(int *fimP);
-                    int &hashType = *$(int *hashTypeP);
-                    char *&hashValue = *$(char **hashValueP);
-                    int &hashSize = *$(int *hashSizeP);
 
-                    std::string nameString = i->first;
-                    name = stringdup(nameString);
-                    path = nullptr;
-                    std::visit(overloaded {
-#if NIX_IS_AT_LEAST(2, 18, 0)
-                      [&](DerivationOutput::InputAddressed doi) -> void {
-                        typ = 0;
-                        path = new StorePath(doi.path);
-                      },
-                      [&](DerivationOutput::CAFixed dof) -> void {
-                        typ = 1;
-                        path = new StorePath(dof.path(store, $(Derivation *derivation)->name, nameString));
-                        std::visit(overloaded {
-                          [&](nix::FileIngestionMethod fim_) -> void {
-                            switch (fim_) {
-                              case nix::FileIngestionMethod::Flat:
-                                fim = 0;
-                                break;
-                              case nix::FileIngestionMethod::Recursive:
-                                fim = 1;
-                                break;
-                              default:
-                                fim = -1;
-                                break;
-                            }
-                          },
-                          [&](nix::TextIngestionMethod) -> void {
-                            // FIXME (RFC 92)
-                            fim = -1;
-                          }
-                        }, dof.ca.method.raw);
-
-                        const Hash & hash = dof.ca.hash;
-
-#if NIX_IS_AT_LEAST(2, 20, 0)
-                        switch (hash.algo) {
-                          case HashAlgorithm::MD5:
-                            hashType = 0;
-                            break;
-                          case HashAlgorithm::SHA1:
-                            hashType = 1;
-                            break;
-                          case HashAlgorithm::SHA256:
-                            hashType = 2;
-                            break;
-                          case HashAlgorithm::SHA512:
-                            hashType = 3;
-                            break;
-#else
-                        switch (hash.type) {
-                          case htMD5:
-                            hashType = 0;
-                            break;
-                          case htSHA1:
-                            hashType = 1;
-                            break;
-                          case htSHA256:
-                            hashType = 2;
-                            break;
-                          case htSHA512:
-                            hashType = 3;
-                            break;
-#endif
-                          default:
-                            hashType = -1;
-                            break;
-                        }
-                        hashSize = hash.hashSize;
-                        hashValue = (char*)malloc(hashSize);
-                        std::memcpy((void*)(hashValue),
-                                    (void*)(hash.hash),
-                                    hashSize);
-                      },
-                      [&](DerivationOutput::CAFloating dof) -> void {
-                        typ = 2;
-                        std::visit(overloaded {
-                          [&](nix::FileIngestionMethod fim_) -> void {
-                            switch (fim_) {
-                              case nix::FileIngestionMethod::Flat:
-                                fim = 0;
-                                break;
-                              case nix::FileIngestionMethod::Recursive:
-                                fim = 1;
-                                break;
-                              default:
-                                fim = -1;
-                                break;
-                            }
-                          },
-                          [&](nix::TextIngestionMethod) -> void {
-                            // FIXME (RFC 92)
-                            fim = -1;
-                          }
-                        }, dof.method.raw);
-#if NIX_IS_AT_LEAST(2, 20, 0)
-                        switch (dof.hashAlgo) {
-                          case HashAlgorithm::MD5:
-                            hashType = 0;
-                            break;
-                          case HashAlgorithm::SHA1:
-                            hashType = 1;
-                            break;
-                          case HashAlgorithm::SHA256:
-                            hashType = 2;
-                            break;
-                          case HashAlgorithm::SHA512:
-                            hashType = 3;
-                            break;
-#else
-                        switch (dof.hashType) {
-                          case htMD5:
-                            hashType = 0;
-                            break;
-                          case htSHA1:
-                            hashType = 1;
-                            break;
-                          case htSHA256:
-                            hashType = 2;
-                            break;
-                          case htSHA512:
-                            hashType = 3;
-                            break;
-#endif
-                          default:
-                            hashType = -1;
-                            break;
-                        }
-                      },
-                      [&](DerivationOutput::Deferred) -> void {
-                        typ = 3;
-                      },
-                      [&](DerivationOutput::Impure) -> void {
-                        typ = 4;
-                      },
-                    },
-                    i->second.raw
-#else
-                      [&](DerivationOutputInputAddressed doi) -> void {
-                        typ = 0;
-                        path = new StorePath(doi.path);
-                      },
-                      [&](DerivationOutputCAFixed dof) -> void {
-                        typ = 1;
-                        path = new StorePath(dof.path(store, $(Derivation *derivation)->name, nameString));
-#if NIX_IS_AT_LEAST(2, 16, 0)
-                        std::visit(overloaded {
-                          [&](nix::FileIngestionMethod fim_) -> void {
-                            switch (fim_) {
-                              case nix::FileIngestionMethod::Flat:
-                                fim = 0;
-                                break;
-                              case nix::FileIngestionMethod::Recursive:
-                                fim = 1;
-                                break;
-                              default:
-                                fim = -1;
-                                break;
-                            }
-                          },
-                          [&](nix::TextIngestionMethod) -> void {
-                            // FIXME (RFC 92)
-                            fim = -1;
-                          }
-#  if NIX_IS_AT_LEAST(2, 17, 0)
-                        }, dof.ca.method.raw);
-#  else
-                        }, dof.ca.getMethod().raw);
-#  endif
-#else
-                        switch (dof.hash.method) {
-                          case nix::FileIngestionMethod::Flat:
-                            fim = 0;
-                            break;
-                          case nix::FileIngestionMethod::Recursive:
-                            fim = 1;
-                            break;
-                          default:
-                            fim = -1;
-                            break;
-                        }
-#endif
-
-#if NIX_IS_AT_LEAST(2, 17, 0)
-                        const Hash & hash = dof.ca.hash;
-#elif NIX_IS_AT_LEAST(2, 16, 0)
-                        const Hash & hash = dof.ca.getHash();
-#else
-                        const Hash & hash = dof.hash.hash;
-#endif
-                        switch (hash.type) {
-                          case htMD5: 
-                            hashType = 0;
-                            break;
-                          case htSHA1: 
-                            hashType = 1;
-                            break;
-                          case htSHA256: 
-                            hashType = 2;
-                            break;
-                          case htSHA512: 
-                            hashType = 3;
-                            break;
-                          default:
-                            hashType = -1;
-                            break;
-                        }
-                        hashSize = hash.hashSize;
-                        hashValue = (char*)malloc(hashSize);
-                        std::memcpy((void*)(hashValue),
-                                    (void*)(hash.hash),
-                                    hashSize);
-                      },
-                      [&](DerivationOutputCAFloating dof) -> void {
-                        typ = 2;
-#if NIX_IS_AT_LEAST(2, 16, 0)
-                        std::visit(overloaded {
-                          [&](nix::FileIngestionMethod fim_) -> void {
-                            switch (fim_) {
-                              case nix::FileIngestionMethod::Flat:
-                                fim = 0;
-                                break;
-                              case nix::FileIngestionMethod::Recursive:
-                                fim = 1;
-                                break;
-                              default:
-                                fim = -1;
-                                break;
-                            }
-                          },
-                          [&](nix::TextIngestionMethod) -> void {
-                            // FIXME (RFC 92)
-                            fim = -1;
-                          }
-                        }, dof.method.raw);
-#else
-                        switch (dof.method) {
-                          case nix::FileIngestionMethod::Flat:
-                            fim = 0;
-                            break;
-                          case nix::FileIngestionMethod::Recursive:
-                            fim = 1;
-                            break;
-                          default:
-                            fim = -1;
-                            break;
-                        }
-#endif
-                        switch (dof.hashType) {
-                          case htMD5: 
-                            hashType = 0;
-                            break;
-                          case htSHA1: 
-                            hashType = 1;
-                            break;
-                          case htSHA256: 
-                            hashType = 2;
-                            break;
-                          case htSHA512: 
-                            hashType = 3;
-                            break;
-                          default:
-                            hashType = -1;
-                            break;
-                        }
-                      },
-                      [&](DerivationOutputDeferred) -> void {
-                        typ = 3;
-                      },
-#if NIX_IS_AT_LEAST(2,8,0)
-                      [&](DerivationOutputImpure) -> void {
-                        typ = 4;
-                      },
-#endif
-                    },
-#if NIX_IS_AT_LEAST(2,8,0)
-                      i->second.raw()
-#else
-                      i->second.output
-#endif
-#endif
-                    );
-                    i++;
-                  }|]
-                  name <- unsafePackMallocCString =<< peek nameP
-                  path <- nullableMoveToForeignPtrWrapper =<< peek pathP
-                  typ <- peek typP
-                  let getFileIngestionMethod = peek fimP <&> \case 0 -> Flat; 1 -> Recursive; _ -> panic "getDerivationOutputs: unknown fim"
-                      getHashType =
-                        peek hashTypeP <&> \case
-                          0 -> MD5
-                          1 -> SHA1
-                          2 -> SHA256
-                          3 -> SHA512
-                          _ -> panic "getDerivationOutputs: unknown hashType"
-                  detail <- case typ of
-                    0 -> pure $ DerivationOutputInputAddressed (fromMaybe (panic "getDerivationOutputs: impossible DOIA path missing") path)
-                    1 -> do
-                      hashValue <- peek hashValueP
-                      hashSize <- peek hashSizeP
-                      hashString <- SBS.packCStringLen (hashValue, fromIntegral hashSize)
-                      free hashValue
-                      hashType <- getHashType
-                      fim <- getFileIngestionMethod
-                      pure $ DerivationOutputCAFixed (FixedOutputHash fim (Hash hashType hashString)) (fromMaybe (panic "getDerivationOutputs: impossible DOCF path missing") path)
-                    2 -> do
-                      hashType <- getHashType
-                      fim <- getFileIngestionMethod
-                      pure $ DerivationOutputCAFloating fim hashType
-                    3 -> pure DerivationOutputDeferred
-                    4 -> panic "getDerivationOutputs: impure derivations not supported yet"
-                    _ -> panic "getDerivationOutputs: impossible getDerivationOutputs typ"
-                  pure
-                    ( DerivationOutput
-                        { derivationOutputName = name,
-                          derivationOutputPath = path,
-                          derivationOutputDetail = detail
-                        }
-                        :
-                    )
-          )
-            <*> continue
-#endif
-
 instance Delete DerivationOutputsIterator where
   delete a = [C.block| void { delete $(DerivationOutputsIterator *a); }|]
 
@@ -1179,7 +788,6 @@
 
 -- | Get the inputs of a derivation, ignoring dependencies on outputs of outputs (RFC 92 inputs).
 getDerivationInputs' :: Derivation -> IO [(StorePath, [ByteString])]
-#if NIX_IS_AT_LEAST(2, 18, 0)
 getDerivationInputs' (Derivation derivationFPtr) =
   withForeignPtr derivationFPtr \derivation ->
   withDelete
@@ -1214,36 +822,6 @@
               toByteStrings
           [C.block| void { (*$(DerivationInputsIterator *i))++; }|]
           ((name, outs) :) <$> continue
-#else
-getDerivationInputs' (Derivation derivationFPtr) =
-  withForeignPtr derivationFPtr \derivation ->
-  withDelete
-    [C.exp| DerivationInputsIterator* {
-      new DerivationInputsIterator($(Derivation *derivation)->inputDrvs.begin())
-    }|]
-    $ \i -> fix $ \continue -> do
-      isEnd <- (0 /=) <$> [C.exp| bool { *$(DerivationInputsIterator *i) == $(Derivation *derivation)->inputDrvs.end() }|]
-      if isEnd
-        then pure []
-        else do
-          name <-
-            [C.throwBlock| nix::StorePath *{
-              return new StorePath((*$(DerivationInputsIterator *i))->first);
-            }|]
-              >>= moveToForeignPtrWrapper
-          outs <-
-            withDelete
-              [C.block| Strings*{ 
-                Strings *r = new Strings();
-                for (auto i : (*$(DerivationInputsIterator *i))->second) {
-                  r->push_back(i);
-                }
-                return r;
-              }|]
-              toByteStrings
-          [C.block| void { (*$(DerivationInputsIterator *i))++; }|]
-          ((name, outs) :) <$> continue
-#endif
 
 instance Delete DerivationInputsIterator where
   delete a = [C.block| void { delete $(DerivationInputsIterator *a); }|]
@@ -1414,14 +992,10 @@
 
     auto info2(*currentInfo);
     info2.sigs.clear();
-#if NIX_IS_AT_LEAST(2, 20, 0)
     {
       auto signer = std::make_unique<LocalSigner>(SecretKey { secretKey });
       info2.sign(*store, *signer);
     }
-#else
-    info2.sign(*store, secretKey);
-#endif
     assert(!info2.sigs.empty());
     auto sig = *info2.sigs.begin();
 
@@ -1483,7 +1057,6 @@
   IO (Maybe (Maybe (ForeignPtr (Ref ValidPathInfo))))
 queryPathInfoFromClientCache (Store store) (StorePath pathFPtr) =
   withForeignPtr pathFPtr \path ->
-#if NIX_IS_AT_LEAST(2, 20, 0)
   alloca \isKnownP -> do
     mvpi <- [C.throwBlock| refValidPathInfo* {
         ReceiveInterrupts _;
@@ -1508,41 +1081,7 @@
     isKnown <- peek isKnownP <&> (/= 0)
     for (guard isKnown) \_ -> do
       mvpi & traverseNonNull toForeignPtr
-#else
-  alloca \isKnownP -> do
-    mvpi <- [C.throwBlock| refValidPathInfo* {
-        ReceiveInterrupts _;
-        Store &store = **$(refStore* store);
-        StorePath &path = *$(nix::StorePath *path);
-        bool &isKnown = *$(bool* isKnownP);
 
-        std::string uri = store.getUri();
-        ref<NarInfoDiskCache> cache = nix::getNarInfoDiskCache();
-
-        // std::pair<nix::NarInfoDiskCache::Outcome, std::shared_ptr<NarInfo>>
-        auto [outcome, maybeNarInfo] =
-          cache->lookupNarInfo(uri, std::string(path.hashPart()));
-
-        if (outcome == nix::NarInfoDiskCache::oValid) {
-          assert(maybeNarInfo);
-          isKnown = true;
-          return new refValidPathInfo(maybeNarInfo);
-        }
-        else if (outcome == nix::NarInfoDiskCache::oInvalid) {
-          isKnown = true;
-          return nullptr;
-        }
-        else {
-          // nix::NarInfoDiskCache::oUnknown or unexpected value
-          isKnown = false;
-          return nullptr;
-        }
-      }|]
-    isKnown <- peek isKnownP <&> (/= 0)
-    for (guard isKnown) \_ -> do
-      mvpi & traverseNonNull toForeignPtr
-#endif
-
 instance Finalizer (Ref ValidPathInfo) where
   finalizer = finalizeRefValidPathInfo -- must be CAF
 
@@ -1570,13 +1109,7 @@
 validPathInfoNarHash32 vpi =
   unsafePackMallocCString
     =<< [C.block| const char *{
-#if NIX_IS_AT_LEAST(2,20,0)
       std::string s((*$fptr-ptr:(refValidPathInfo* vpi))->narHash.to_string(nix::HashFormat::Nix32, true));
-#elif NIX_IS_AT_LEAST(2,19,0)
-      std::string s((*$fptr-ptr:(refValidPathInfo* vpi))->narHash.to_string(nix::HashFormat::Base32, true));
-#else
-      std::string s((*$fptr-ptr:(refValidPathInfo* vpi))->narHash.to_string(nix::Base32, true));
-#endif
       return stringdup(s);
     }|]
 
diff --git a/src/Hercules/CNix/Store/Instances.hs b/src/Hercules/CNix/Store/Instances.hs
--- a/src/Hercules/CNix/Store/Instances.hs
+++ b/src/Hercules/CNix/Store/Instances.hs
@@ -17,15 +17,9 @@
 
 C.context $ context <> stdVectorCtx <> stdSetCtx
 
-#if NIX_IS_AT_LEAST(2, 28, 0)
 C.include "<nix/store/path.hh>"
 C.include "<nix/store/derivations.hh>"
 C.include "<nix/store/path-with-outputs.hh>"
-#else
-C.include "<nix/path.hh>"
-C.include "<nix/derivations.hh>"
-C.include "<nix/path-with-outputs.hh>"
-#endif
 
 _ = Proxy :: Proxy NixStorePath
 
diff --git a/src/Hercules/CNix/Util.hs b/src/Hercules/CNix/Util.hs
--- a/src/Hercules/CNix/Util.hs
+++ b/src/Hercules/CNix/Util.hs
@@ -25,17 +25,9 @@
 
 C.context context
 
-#if NIX_IS_AT_LEAST(2, 28, 0)
-
 C.include "<nix/util/signals.hh>"
 
-#else
-C.include "<nix/config.h>"
-C.include "<nix/util.hh>"
-#  if NIX_IS_AT_LEAST(2,19,0)
-C.include "<nix/signals.hh>"
-#  endif
-#endif
+C.include "signals.hxx"
 
 C.using "namespace nix"
 
@@ -47,16 +39,21 @@
 
 triggerInterrupt :: IO ()
 triggerInterrupt =
-#if NIX_IS_AT_LEAST(2,24,0)
   [C.throwBlock| void {
     nix::unix::triggerInterrupt();
   } |]
-#else
-  [C.throwBlock| void {
-    nix::triggerInterrupt();
-  } |]
-#endif
 
+-- | Install a signal handler that will put Nix into the interrupted state and
+-- throws 'UserInterrupt' in the main thread (as is usual), assuming this
+-- function is called from the main thread.
+--
+-- This installs a synchronous C signal handler that calls nix::triggerInterrupt()
+-- immediately when signals are delivered, eliminating race conditions between
+-- signal delivery and interrupt flag setting. The handler then chains to any
+-- previously installed handler to preserve existing behavior.
+--
+-- This may be removed from the library interface in the future, as it is no
+-- longer needed to be called explicitly.
 installDefaultSigINTHandler :: IO ()
 installDefaultSigINTHandler = do
   mainThread <- myThreadId
@@ -66,16 +63,11 @@
         for_ mt \t -> do
           throwTo t (toException UserInterrupt)
 
-  -- Install Nix interrupter in Haskell
-  _oldHandler <-
-    for [sigINT, sigTERM, sigHUP] \sig ->
-      installHandler
-        sig
-        ( Catch do
-            triggerInterrupt
-            defaultHaskellHandler
-        )
-        Nothing
+  -- Install synchronous C signal handlers first (these call triggerInterrupt immediately)
+  for_ [sigINT, sigTERM, sigHUP] \sig -> do
+    result <- [C.exp| int { hercules_install_signal_handler($(int sig)) } |]
+    when (result /= 0) $
+      panic $ "Failed to install synchronous signal handler for signal " <> show sig
 
   -- Install dummy SIGUSR1 handler for Nix interrupt signal propagation
   -- (installHandler uses process-wide sigprocmask, so this should apply to all
diff --git a/src/Hercules/CNix/Verbosity.hs b/src/Hercules/CNix/Verbosity.hs
--- a/src/Hercules/CNix/Verbosity.hs
+++ b/src/Hercules/CNix/Verbosity.hs
@@ -20,16 +20,7 @@
 
 C.context context
 
-#if NIX_IS_AT_LEAST(2, 28, 0)
-
 C.include "<nix/util/logging.hh>"
-
-#else
-C.include "<nix/config.h>"
-C.include "<nix/error.hh>"
-C.include "<nix/globals.hh>"
-C.include "<nix/logging.hh>"
-#endif
 
 data Verbosity
   = Error
