diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,12 @@
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## 0.4.1.0 - 2026-01-06
+
+### Added
+
+ - Nix 2.29 - 2.33 support
+
 ## 0.4.0.0 - 2025-07-18
 
 ### Changed
diff --git a/cbits/string.cxx b/cbits/string.cxx
--- a/cbits/string.cxx
+++ b/cbits/string.cxx
@@ -1,10 +1,11 @@
 #include "hercules-ci-cnix/string.hxx"
+#include <cstring>
 
 namespace hercules_ci_cnix {
 
 char * stringdup(const std::string & s) {
     char * p = (char *)malloc(s.size() + 1);
-    std::copy(s.begin(), s.end(), p);
+    std::memcpy(p, s.data(), s.size());
     p[s.size()] = '\0';
     return p;
 }
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.4.0.0
+version:        0.4.1.0
 synopsis:       Haskell bindings for Nix's libstore
 category:       Nix
 homepage:       https://docs.hercules-ci.com
@@ -24,10 +24,19 @@
   description: Whether to enable IDE workarounds. You shouldn't need this.
   default: False
 
+flag nix-2_31
+  description: nix-store >= 2.31
+  default: False
+  manual: False
+
 -- match the C++ language standard Nix is using
 common cxx-opts
-  cxx-options:
-    -std=c++2a
+  if flag(nix-2_31)
+    cxx-options:
+      -std=c++23
+  else
+    cxx-options:
+      -std=c++2a
 
   cxx-options:
     -Wall
@@ -42,12 +51,22 @@
   if impl(ghc >= 8.10)
     ghc-options:
       -optcxx-Wall
-      -optcxx-std=c++2a
+    if flag(nix-2_31)
+      ghc-options:
+        -optcxx-std=c++23
+    else
+      ghc-options:
+        -optcxx-std=c++2a
   else
     -- Remove soon
     ghc-options:
-      -optc-std=c++2a
       -optc-Wall
+    if flag(nix-2_31)
+      ghc-options:
+        -optc-std=c++23
+    else
+      ghc-options:
+        -optc-std=c++2a
     if os(darwin)
       ghc-options: -pgmc=clang++
 
@@ -79,7 +98,7 @@
       include
       cbits
   pkgconfig-depends:
-      nix-store >= 2.28 && < 2.29
+      nix-store >= 2.28 && < 2.34
   install-includes:
       hercules-ci-cnix/store.hxx
       hercules-ci-cnix/string.hxx
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
@@ -2,6 +2,9 @@
 #pragma once
 
 #include <nix/store/path-info.hh>
+#if NIX_IS_AT_LEAST(2, 29, 0)
+#include <nix/store/derived-path-map.hh>
+#endif
 
 typedef nix::ref<nix::Store> refStore;
 
@@ -11,4 +14,9 @@
 typedef nix::PathSet::iterator PathSetIterator;
 typedef nix::ref<const nix::ValidPathInfo> refValidPathInfo;
 
+// https://github.com/NixOS/nix/pull/13129
+#if NIX_IS_AT_LEAST(2, 29, 0)
+typedef nix::DerivedPathMap<nix::StringSet>::Map::iterator DerivationInputsIterator;
+#else
 typedef nix::DerivedPathMap<std::set<nix::OutputName>>::Map::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
@@ -38,7 +38,6 @@
 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>"
 
 C.include "<gc/gc.h>"
 C.include "<gc/gc_cpp.h>"
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
@@ -62,11 +62,20 @@
     >>= traverse Std.String.copyToByteString
 
 getExtraPlatforms :: IO (Set ByteString)
+#if NIX_IS_AT_LEAST(2, 29, 0)
 getExtraPlatforms =
   byteStringSet
     [C.block| std::set<std::string>*{
-      return new nix::StringSet(nix::settings.extraPlatforms.get());
+      auto extraPlatforms = nix::settings.extraPlatforms.get();
+      return new std::set<std::string>(extraPlatforms.begin(), extraPlatforms.end());
     }|]
+#else
+getExtraPlatforms =
+  byteStringSet
+    [C.block| std::set<std::string>*{
+      return new std::set<std::string>(nix::settings.extraPlatforms.get());
+    }|]
+#endif
 
 getSystem :: IO ByteString
 getSystem =
@@ -76,11 +85,20 @@
     }|]
 
 getSystemFeatures :: IO (Set ByteString)
+#if NIX_IS_AT_LEAST(2, 29, 0)
 getSystemFeatures =
   byteStringSet
     [C.block| std::set<std::string>*{
-      return new nix::StringSet(nix::settings.systemFeatures.get());
+      auto systemFeatures = nix::settings.systemFeatures.get();
+      return new std::set<std::string>(systemFeatures.begin(), systemFeatures.end());
     }|]
+#else
+getSystemFeatures =
+  byteStringSet
+    [C.block| std::set<std::string>*{
+      return new std::set<std::string>(nix::settings.systemFeatures.get());
+    }|]
+#endif
 
 getMaxBuildJobs :: IO Word
 getMaxBuildJobs = do
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
@@ -45,9 +45,8 @@
     validPathInfoDeriver',
     validPathInfoReferences,
     validPathInfoReferences',
-
     computeFSClosure,
-    ClosureParams(..),
+    ClosureParams (..),
     defaultClosureParams,
 
     -- * Realisation
@@ -81,7 +80,6 @@
     getDerivationSources',
     getDerivationInputs,
     getDerivationInputs',
-
     getDerivationOutputNames,
     DerivationOutput (..),
     DerivationOutputDetail (..),
@@ -102,6 +100,7 @@
     HashType (..),
 
     -- * Utilities
+
     --
     -- To be moved and deprecated
     Strings,
@@ -154,6 +153,8 @@
 import Foreign.ForeignPtr.Unsafe (unsafeForeignPtrToPtr)
 import Foreign.Storable (peek)
 import Hercules.CNix.Encapsulation (HasEncapsulation (..), nullableMoveToForeignPtrWrapper)
+import Hercules.CNix.Memory (Delete (delete), Finalizer (finalizer), toForeignPtr, withDelete)
+import qualified Hercules.CNix.Memory
 import Hercules.CNix.Std.Set (StdSet, stdSetCtx)
 import qualified Hercules.CNix.Std.Set as Std.Set
 import Hercules.CNix.Std.String (stdStringCtx)
@@ -174,8 +175,6 @@
     unsafeMallocBS,
   )
 import qualified Hercules.CNix.Store.Context as C hiding (context)
-import Hercules.CNix.Memory (Delete(delete), Finalizer (finalizer), withDelete, toForeignPtr)
-import qualified Hercules.CNix.Memory
 import Hercules.CNix.Store.Instances ()
 import qualified Language.C.Inline.Cpp as C
 import qualified Language.C.Inline.Cpp.Exception as C
@@ -197,6 +196,10 @@
 C.include "<nix/store/path-with-outputs.hh>"
 C.include "<nix/util/signals.hh>"
 
+#if NIX_IS_AT_LEAST(2, 29, 0)
+C.include "<nix/store/store-open.hh>"
+#endif
+
 C.include "hercules-ci-cnix/store.hxx"
 
 C.include "hercules-ci-cnix/string.hxx"
@@ -222,14 +225,14 @@
 openStore =
   coerce
     [C.throwBlock| refStore * {
-      refStore s = openStore();
+      refStore s = nix::openStore();
       return new refStore(s);
     } |]
 
 releaseStore :: Store -> IO ()
 releaseStore (Store store) = delete store
 
-withStore :: MonadUnliftIO m => (Store -> m a) -> m a
+withStore :: (MonadUnliftIO m) => (Store -> m a) -> m a
 withStore m = do
   UnliftIO ul <- askUnliftIO
   liftIO $ withStore' $ \a -> ul (m a)
@@ -241,7 +244,7 @@
   bracket openStore releaseStore
 
 withStoreFromURI ::
-  MonadUnliftIO m =>
+  (MonadUnliftIO m) =>
   Text ->
   (Store -> m r) ->
   m r
@@ -251,42 +254,52 @@
   liftIO $
     withDelete
       [C.throwBlock| refStore* {
-        refStore s = openStore($bs-cstr:storeURI);
+        refStore s = nix::openStore($bs-cstr:storeURI);
         return new refStore(s);
       }|]
       (unlift . f . Store)
 
-storeUri :: MonadIO m => Store -> m ByteString
+-- | Returns the human-readable URI. A more complete URI (with params)
+-- could be obtained via getReference().render() since Nix 2.31,
+-- but is currently not implemented.
+-- See https://github.com/NixOS/nix/commit/1b7ffa53af168aab255c9ee4c1ca5a192c269738
+{- ORMOLU_DISABLE -}
+storeUri :: (MonadIO m) => Store -> m ByteString
 storeUri (Store store) = liftIO do
-  BS.unsafePackMallocCString =<<
-    [C.block| const char* {
-       std::string uri = (*$(refStore* store))->getUri();
-       return stringdup(uri);
-     } |]
+  BS.unsafePackMallocCString
+    =<< [C.block| const char* {
+#if NIX_IS_AT_LEAST(2, 31, 0)
+      std::string uri = (*$(refStore* store))->config.getHumanReadableURI();
+#else
+      std::string uri = (*$(refStore* store))->getUri();
+#endif
+      return stringdup(uri);
+    }|]
+{- ORMOLU_ENABLE -}
 
 -- | Usually @"/nix/store"@
-storeDir :: MonadIO m => Store -> m ByteString
+storeDir :: (MonadIO m) => Store -> m ByteString
 storeDir (Store store) = liftIO do
-  BS.unsafePackMallocCString =<<
-    [C.block| const char* {
-       std::string uri = (*$(refStore* store))->storeDir;
-       return stringdup(uri);
-     } |]
+  BS.unsafePackMallocCString
+    =<< [C.block| const char* {
+      std::string uri = (*$(refStore* store))->storeDir;
+      return stringdup(uri);
+    }|]
 
 getStoreProtocolVersion :: Store -> IO Int
 getStoreProtocolVersion (Store store) =
   fromIntegral
     <$> [C.throwBlock| int {
-       Store &store = **$(refStore* store);
-       return store.getProtocol();
-     } |]
+      Store &store = **$(refStore* store);
+      return store.getProtocol();
+    }|]
 
 getClientProtocolVersion :: IO Int
 getClientProtocolVersion =
   fromIntegral
     <$> [C.throwBlock| int {
-       return PROTOCOL_VERSION;
-     } |]
+      return PROTOCOL_VERSION;
+    }|]
 
 -- | Store-agnostic store path representation: hash and name. Does not have a storedir or subpath inside the store path.
 newtype StorePath = StorePath (ForeignPtr NixStorePath)
@@ -445,20 +458,23 @@
   set <- Std.Set.new
   for_ outputs (\o -> Std.String.withString o (Std.Set.insertP set))
   moveToForeignPtrWrapper
-    =<< [C.exp| nix::StorePathWithOutputs * {
-    new StorePathWithOutputs {*$fptr-ptr:(nix::StorePath *storePath), *$fptr-ptr:(std::set<std::string>* set)}
-  }|]
+    =<< [C.throwBlock| nix::StorePathWithOutputs * {
+      auto nixSet = nix::StringSet{$fptr-ptr:(std::set<std::string>* set)->begin(), $fptr-ptr:(std::set<std::string>* set)->end()};
+      return new StorePathWithOutputs {*$fptr-ptr:(nix::StorePath *storePath), nixSet};
+    }|]
 
 getStorePath :: StorePathWithOutputs -> IO StorePath
 getStorePath swo = mask_ do
   moveToForeignPtrWrapper
     =<< [C.exp| nix::StorePath * {
-    new StorePath($fptr-ptr:(nix::StorePathWithOutputs *swo)->path)
-  }|]
+      new StorePath($fptr-ptr:(nix::StorePathWithOutputs *swo)->path)
+    }|]
 
 getOutputs :: StorePathWithOutputs -> IO [ByteString]
 getOutputs swo = mask_ do
-  traverse Std.String.moveToByteString =<< toListP =<< moveToForeignPtrWrapper
+  traverse Std.String.moveToByteString
+    =<< toListP
+    =<< moveToForeignPtrWrapper
     =<< [C.throwBlock| std::vector<std::string>* {
       auto r = new std::vector<std::string>();
       for (auto s : $fptr-ptr:(nix::StorePathWithOutputs *swo)->outputs)
@@ -491,11 +507,12 @@
 finalizeDerivation =
   unsafePerformIO
     [C.exp|
-    void (*)(Derivation *) {
-      [](Derivation *v) {
-        delete v;
+      void (*)(Derivation *) {
+        [](Derivation *v) {
+          delete v;
+        }
       }
-    } |]
+    |]
 {-# DEPRECATED finalizeDerivation "Use 'finalizer' instead" #-}
 
 getDerivation :: Store -> StorePath -> IO Derivation
@@ -507,7 +524,7 @@
       return new Derivation(
           store.derivationFromPath(*$fptr-ptr:(nix::StorePath *spwo))
         );
-    } |]
+    }|]
 
 -- Useful for testing
 getDerivationFromString ::
@@ -566,174 +583,175 @@
 getDerivationOutputs :: Store -> ByteString -> Derivation -> IO [DerivationOutput]
 getDerivationOutputs (Store store) drvName (Derivation derivationFPtr) =
   withForeignPtr derivationFPtr \derivation ->
-  withDelete
-    [C.exp| DerivationOutputsIterator* {
+    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);
+    }|]
+      \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 {
-                      [&](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));
-                        switch (dof.ca.method.raw) {
-                          case ContentAddressMethod::Raw::Text:
-                            // FIXME (RFC 92)
-                            fim = -1;
-                            break;
-                          case ContentAddressMethod::Raw::Flat:
-                            fim = 0;
-                            break;
-                          case ContentAddressMethod::Raw::NixArchive:
-                            fim = 1;
-                            break;
-                          case ContentAddressMethod::Raw::Git:
-                            // FIXME (git-hashing)
-                            fim = -1;
-                            break;
-                          default:
-                            fim = -1;
-                            break;
-                        }
+                        std::string nameString = i->first;
+                        name = stringdup(nameString);
+                        path = nullptr;
+                        std::visit(overloaded {
+                          [&](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));
+                            switch (dof.ca.method.raw) {
+                              case ContentAddressMethod::Raw::Text:
+                                // FIXME (RFC 92)
+                                fim = -1;
+                                break;
+                              case ContentAddressMethod::Raw::Flat:
+                                fim = 0;
+                                break;
+                              case ContentAddressMethod::Raw::NixArchive:
+                                fim = 1;
+                                break;
+                              case ContentAddressMethod::Raw::Git:
+                                // FIXME (git-hashing)
+                                fim = -1;
+                                break;
+                              default:
+                                fim = -1;
+                                break;
+                            }
 
-                        const Hash & hash = dof.ca.hash;
+                            const Hash & hash = dof.ca.hash;
 
-                        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;
-                          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;
-                        switch(dof.method.raw) {
-                          case ContentAddressMethod::Raw::Text:
-                            // FIXME (RFC 92)
-                            fim = -1;
-                            break;
-                          case ContentAddressMethod::Raw::Flat:
-                            fim = 0;
-                            break;
-                          case ContentAddressMethod::Raw::NixArchive:
-                            fim = 1;
-                            break;
-                          case ContentAddressMethod::Raw::Git:
-                            // FIXME (git-hashing)
-                            fim = -1;
-                            break;
-                          default:
-                            fim = -1;
-                            break;
-                        }
-                        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;
-                          default:
-                            hashType = -1;
-                            break;
-                        }
-                      },
-                      [&](DerivationOutput::Deferred) -> void {
-                        typ = 3;
-                      },
-                      [&](DerivationOutput::Impure) -> void {
-                        typ = 4;
-                      },
-                    },
-                    i->second.raw
-                    );
-                    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
+                            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;
+                              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;
+                            switch(dof.method.raw) {
+                              case ContentAddressMethod::Raw::Text:
+                                // FIXME (RFC 92)
+                                fim = -1;
+                                break;
+                              case ContentAddressMethod::Raw::Flat:
+                                fim = 0;
+                                break;
+                              case ContentAddressMethod::Raw::NixArchive:
+                                fim = 1;
+                                break;
+                              case ContentAddressMethod::Raw::Git:
+                                // FIXME (git-hashing)
+                                fim = -1;
+                                break;
+                              default:
+                                fim = -1;
+                                break;
+                            }
+                            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;
+                              default:
+                                hashType = -1;
+                                break;
+                            }
+                          },
+                          [&](DerivationOutput::Deferred) -> void {
+                            typ = 3;
+                          },
+                          [&](DerivationOutput::Impure) -> void {
+                            typ = 4;
+                          },
+                        },
+                        i->second.raw
+                        );
+                        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
 
 instance Delete DerivationOutputsIterator where
   delete a = [C.block| void { delete $(DerivationOutputsIterator *a); }|]
@@ -744,17 +762,17 @@
 
 getDerivationPlatform :: Derivation -> IO ByteString
 getDerivationPlatform derivation =
-  BS.unsafePackMallocCString =<<
-    [C.exp| const char* {
-       stringdup($fptr-ptr:(Derivation *derivation)->platform)
-     } |]
+  BS.unsafePackMallocCString
+    =<< [C.exp| const char* {
+      stringdup($fptr-ptr:(Derivation *derivation)->platform)
+    }|]
 
 getDerivationBuilder :: Derivation -> IO ByteString
 getDerivationBuilder derivation =
-  BS.unsafePackMallocCString =<<
-    [C.exp| const char* {
-       stringdup($fptr-ptr:(Derivation *derivation)->builder)
-     } |]
+  BS.unsafePackMallocCString
+    =<< [C.exp| const char* {
+      stringdup($fptr-ptr:(Derivation *derivation)->builder)
+    }|]
 
 getDerivationArguments :: Derivation -> IO [ByteString]
 getDerivationArguments derivation =
@@ -790,38 +808,48 @@
 getDerivationInputs' :: Derivation -> IO [(StorePath, [ByteString])]
 getDerivationInputs' (Derivation derivationFPtr) =
   withForeignPtr derivationFPtr \derivation ->
-  withDelete
-    [C.exp| DerivationInputsIterator* {
-      new DerivationInputsIterator($(Derivation *derivation)->inputDrvs.map.begin())
-    }|]
-    $ \i -> fix $ \continue -> do
-      isEnd <- (0 /=) <$> [C.exp| bool { *$(DerivationInputsIterator *i) == $(Derivation *derivation)->inputDrvs.map.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();
+    withDelete
+      [C.exp| DerivationInputsIterator* {
+        new DerivationInputsIterator($(Derivation *derivation)->inputDrvs.map.begin())
+      }|]
+      $ \i -> fix $ \continue -> do
+        isEnd <- checkIteratorEnd i derivation
+        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 (const auto & i : (*$(DerivationInputsIterator *i))->second.value) {
-                  r->push_back(i);
-                }
+                  for (const auto & i : (*$(DerivationInputsIterator *i))->second.value) {
+                    r->push_back(i);
+                  }
 
-                // for (const auto &i : iter->second.childMap) {
-                // TODO (RFC 92)
-                //}
+                  // for (const auto &i : iter->second.childMap) {
+                  // TODO (RFC 92)
+                  //}
 
-                return r;
-              }|]
-              toByteStrings
-          [C.block| void { (*$(DerivationInputsIterator *i))++; }|]
-          ((name, outs) :) <$> continue
+                  return r;
+                }|]
+                toByteStrings
+            [C.block| void { (*$(DerivationInputsIterator *i))++; }|]
+            ((name, outs) :) <$> continue
+  where
+#if NIX_IS_AT_LEAST(2, 29, 0)
+    checkIteratorEnd i derivation = (0 /=) <$> [C.exp| bool {
+      (*$(DerivationInputsIterator *i)) == $(Derivation *derivation)->inputDrvs.map.end()
+    }|]
+#else
+    checkIteratorEnd i derivation = (0 /=) <$> [C.exp| bool {
+      *$(DerivationInputsIterator *i) == $(Derivation *derivation)->inputDrvs.map.end()
+    }|]
+#endif
 
 instance Delete DerivationInputsIterator where
   delete a = [C.block| void { delete $(DerivationInputsIterator *a); }|]
@@ -838,15 +866,16 @@
 
 getDerivationOutputNames :: ForeignPtr C.Derivation -> IO [ByteString]
 getDerivationOutputNames fptr =
-  withForeignPtr fptr \ptr -> withDelete
-    [C.throwBlock| Strings* {
+  withForeignPtr fptr \ptr ->
+    withDelete
+      [C.throwBlock| Strings* {
       Strings *r = new Strings();
       for (auto i : $(Derivation *ptr)->outputs) {
         r->push_back(i.first);
       }
       return r;
     }|]
-    toByteStrings
+      toByteStrings
 
 instance Delete StringPairs where
   delete s = [C.block| void { delete $(StringPairs *s); }|]
@@ -965,12 +994,11 @@
 {-# NOINLINE finalizeSecretKey #-}
 finalizeSecretKey =
   unsafePerformIO
-    [C.exp|
-    void (*)(SecretKey *) {
+    [C.exp| void (*)(SecretKey *) {
       [](SecretKey *v) {
         delete v;
       }
-    } |]
+    }|]
 {-# DEPRECATED finalizeSecretKey "Use 'finalizer' instead" #-}
 
 signPath ::
@@ -984,28 +1012,28 @@
 signPath (Store store) secretKey (StorePath path) =
   (== 1) <$> do
     [C.throwBlock| int {
-    ReceiveInterrupts _;
-    nix::ref<nix::Store> store = *$(refStore *store);
-    const StorePath &storePath = *$fptr-ptr:(nix::StorePath *path);
-    const SecretKey &secretKey = *$(SecretKey *secretKey);
-    auto currentInfo = store->queryPathInfo(storePath);
+      ReceiveInterrupts _;
+      nix::ref<nix::Store> store = *$(refStore *store);
+      const StorePath &storePath = *$fptr-ptr:(nix::StorePath *path);
+      const SecretKey &secretKey = *$(SecretKey *secretKey);
+      auto currentInfo = store->queryPathInfo(storePath);
 
-    auto info2(*currentInfo);
-    info2.sigs.clear();
-    {
-      auto signer = std::make_unique<LocalSigner>(SecretKey { secretKey });
-      info2.sign(*store, *signer);
-    }
-    assert(!info2.sigs.empty());
-    auto sig = *info2.sigs.begin();
+      auto info2(*currentInfo);
+      info2.sigs.clear();
+      {
+        auto signer = std::make_unique<LocalSigner>(SecretKey { secretKey });
+        info2.sign(*store, *signer);
+      }
+      assert(!info2.sigs.empty());
+      auto sig = *info2.sigs.begin();
 
-    if (currentInfo->sigs.count(sig)) {
-      return 0;
-    } else {
-      store->addSignatures(storePath, info2.sigs);
-      return 1;
-    }
-  }|]
+      if (currentInfo->sigs.count(sig)) {
+        return 0;
+      } else {
+        store->addSignatures(storePath, info2.sigs);
+        return 1;
+      }
+    }|]
 
 -- | Follow symlinks to the store and chop off the parts after the top-level store name
 followLinksToStorePath :: Store -> ByteString -> IO StorePath
@@ -1035,16 +1063,16 @@
   StorePath ->
   -- | ValidPathInfo or exception
   IO (ForeignPtr (Ref ValidPathInfo))
-queryPathInfo (Store store) (StorePath pathFPtr) = 
+queryPathInfo (Store store) (StorePath pathFPtr) =
   withForeignPtr pathFPtr \path -> do
-  vpi <-
-    [C.throwBlock| refValidPathInfo* {
-      ReceiveInterrupts _;
-      Store &store = **$(refStore* store);
-      StorePath &path = *$(nix::StorePath *path);
-      return new refValidPathInfo(store.queryPathInfo(path));
-    }|]
-  toForeignPtr vpi
+    vpi <-
+      [C.throwBlock| refValidPathInfo* {
+        ReceiveInterrupts _;
+        Store &store = **$(refStore* store);
+        StorePath &path = *$(nix::StorePath *path);
+        return new refValidPathInfo(store.queryPathInfo(path));
+      }|]
+    toForeignPtr vpi
 
 -- | Query only the local client cache ("narinfo cache") - does not query the actual store or daemon.
 --
@@ -1057,30 +1085,31 @@
   IO (Maybe (Maybe (ForeignPtr (Ref ValidPathInfo))))
 queryPathInfoFromClientCache (Store store) (StorePath pathFPtr) =
   withForeignPtr pathFPtr \path ->
-  alloca \isKnownP -> do
-    mvpi <- [C.throwBlock| refValidPathInfo* {
-        ReceiveInterrupts _;
-        Store &store = **$(refStore* store);
-        StorePath &path = *$(nix::StorePath *path);
-        bool &isKnown = *$(bool* isKnownP);
-        std::optional<std::shared_ptr<const ValidPathInfo>> maybeVPI =
-            store.queryPathInfoFromClientCache(path);
-        if (!maybeVPI) {
-          isKnown = false;
-          return nullptr;
-        }
-        else {
-          isKnown = true;
-          std::shared_ptr<const ValidPathInfo> &vpi = *maybeVPI;
-          if (vpi)
-            return new refValidPathInfo(vpi);
-          else
+    alloca \isKnownP -> do
+      mvpi <-
+        [C.throwBlock| refValidPathInfo* {
+          ReceiveInterrupts _;
+          Store &store = **$(refStore* store);
+          StorePath &path = *$(nix::StorePath *path);
+          bool &isKnown = *$(bool* isKnownP);
+          std::optional<std::shared_ptr<const ValidPathInfo>> maybeVPI =
+              store.queryPathInfoFromClientCache(path);
+          if (!maybeVPI) {
+            isKnown = false;
             return nullptr;
-        }
-      }|]
-    isKnown <- peek isKnownP <&> (/= 0)
-    for (guard isKnown) \_ -> do
-      mvpi & traverseNonNull toForeignPtr
+          }
+          else {
+            isKnown = true;
+            std::shared_ptr<const ValidPathInfo> &vpi = *maybeVPI;
+            if (vpi)
+              return new refValidPathInfo(vpi);
+            else
+              return nullptr;
+          }
+        }|]
+      isKnown <- peek isKnownP <&> (/= 0)
+      for (guard isKnown) \_ -> do
+        mvpi & traverseNonNull toForeignPtr
 
 instance Finalizer (Ref ValidPathInfo) where
   finalizer = finalizeRefValidPathInfo -- must be CAF
@@ -1089,10 +1118,9 @@
 {-# NOINLINE finalizeRefValidPathInfo #-}
 finalizeRefValidPathInfo =
   unsafePerformIO
-    [C.exp|
-      void (*)(refValidPathInfo *) {
-        [](refValidPathInfo *v){ delete v; }
-      }|]
+    [C.exp| void (*)(refValidPathInfo *) {
+      [](refValidPathInfo *v){ delete v; }
+    }|]
 {-# DEPRECATED finalizeRefValidPathInfo "Use 'finalizer' instead" #-}
 
 -- | The narSize field of a ValidPathInfo struct. Source: path-info.hh / store-api.hh
@@ -1100,9 +1128,9 @@
 validPathInfoNarSize vpi =
   fromIntegral $
     toInteger
-      [C.pure| long
-        { (*$fptr-ptr:(refValidPathInfo* vpi))->narSize }
-      |]
+      [C.pure| long {
+        (*$fptr-ptr:(refValidPathInfo* vpi))->narSize
+      }|]
 
 -- | Copy the narHash field of a ValidPathInfo struct. Source: path-info.hh / store-api.hh
 validPathInfoNarHash32 :: ForeignPtr (Ref ValidPathInfo) -> IO ByteString
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
@@ -35,13 +35,13 @@
 setInterruptThrown =
   [C.throwBlock| void {
     nix::setInterruptThrown();
-  } |]
+  }|]
 
 triggerInterrupt :: IO ()
 triggerInterrupt =
   [C.throwBlock| void {
     nix::unix::triggerInterrupt();
-  } |]
+  }|]
 
 -- | Install a signal handler that will put Nix into the interrupted state and
 -- throws 'UserInterrupt' in the main thread (as is usual), assuming this
@@ -67,7 +67,8 @@
   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
+      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
@@ -89,7 +90,7 @@
   -- leaks onInterruptPtr
   [C.throwBlock| void {
     nix::createInterruptCallback($(void (*onInterruptPtr)()));
-  } |]
+  }|]
 
 #ifndef __GHCIDE__
 foreign import ccall "wrapper"
