diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,17 @@
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## 0.3.0.0 - 2022-03-15
+
+### Added
+
+ - `setVerbosity`
+ - `handleException` so your program's exception handling can match that of the Nix CLI
+
+### Removed
+
+ - Nix 2.3 support
+
 ## 0.2.1.2 - 2022-03-07
 
 ### Fixed
diff --git a/compat-2.3/derivation-output.cxx b/compat-2.3/derivation-output.cxx
deleted file mode 100644
--- a/compat-2.3/derivation-output.cxx
+++ /dev/null
@@ -1,23 +0,0 @@
-
-#include <nix/config.h>
-#include <nix/store-api.hh>
-#include <nix/derivations.hh>
-#include <nix/path-compat.hh>
-#include "nix/derivation-output.hh"
-
-using namespace nix;
-
-static std::string outputPathName(std::string_view drvName, std::string_view outputName) {
-    std::string res { drvName };
-    if (outputName != "out") {
-        res += "-";
-        res += outputName;
-    }
-    return res;
-}
-
-StorePath DerivationOutputCAFixed::path(const Store & store, std::string_view drvName, std::string_view outputName) const {
-    return parseStorePath(store, store.makeFixedOutputPath(
-        hash.method == FileIngestionMethod::Recursive, hash.hash,
-        outputPathName(drvName, outputName)));
-}
diff --git a/compat-2.3/include/nix-compat.hh b/compat-2.3/include/nix-compat.hh
deleted file mode 100644
--- a/compat-2.3/include/nix-compat.hh
+++ /dev/null
@@ -1,92 +0,0 @@
-#pragma once
-#include <nix/store-api.hh>
-#include <nix/derivation-output.hh>
-#include <nix/derivations.hh>
-#include <nix/path-compat.hh>
-
-// derivations.cc
-
-inline void validatePath(std::string_view s) {
-    if (s.size() == 0 || s[0] != '/')
-        throw nix::FormatError("bad path '%1%' in derivation", s);
-}
-
-inline compat::nix::DerivationOutput parseDerivationOutput(const nix::Store & store, std::string drvName,
-    std::string pathS, std::string hashAlgo, std::string hash)
-{
-    if (hashAlgo != "") {
-        auto method = nix::FileIngestionMethod::Flat;
-        if (std::string(hashAlgo, 0, 2) == "r:") {
-            method = nix::FileIngestionMethod::Recursive;
-            hashAlgo = hashAlgo.substr(2);
-        }
-        const auto hashType = nix::parseHashType(hashAlgo);
-        if (hash != "") {
-            validatePath(pathS);
-            return compat::nix::DerivationOutput {
-                .output = nix::DerivationOutputCAFixed {
-                    .drvName = drvName,
-                    .hash = nix::FixedOutputHash {
-                        .method = std::move(method),
-                        .hash = nix::Hash(hash, hashType),
-                    },
-                },
-            };
-        } else {
-            throw nix::Error("ca-derivations not supported in Nix 2.3");
-        }
-    } else {
-        if (pathS == "") {
-            return compat::nix::DerivationOutput {
-                .output = nix::DerivationOutputDeferred { }
-            };
-        }
-        validatePath(pathS);
-        return compat::nix::DerivationOutput {
-            .output = nix::DerivationOutputInputAddressed {
-                .path = parseStorePath(store, pathS),
-            }
-        };
-    }
-}
-
-inline compat::nix::DerivationOutput compatDerivationOutput(nix::Store &store, std::string drvName, nix::DerivationOutput &o) {
-  return parseDerivationOutput(store, drvName, o.path, o.hashAlgo, o.hash);
-}
-
-inline void compatComputeFSClosure(nix::Store &store, nix::StorePathSet &pathSet, nix::StorePathSet &closurePaths, bool flipDirection = false, bool includeOutputs = false, bool includeDerivers = false) {
-  nix::PathSet simpleClosurePaths;
-  store.computeFSClosure(compatPathSet(store, pathSet), simpleClosurePaths, flipDirection, includeOutputs, includeDerivers);
-  for (auto sp : simpleClosurePaths) {
-    closurePaths.insert(parseStorePath(store, sp));
-  }
-}
-
-inline nix::StorePathSet parseStorePathSet(const nix::Store &store, const nix::PathSet &pathSet) {
-  nix::StorePathSet r;
-  for (auto sp : pathSet) {
-    // TODO merge?
-    r.insert(parseStorePath(store, sp));
-  }
-  return r;
-}
-#define parseStorePathSet23(store, pathSet) parseStorePathSet(store, pathSet)
-
-inline std::optional<nix::StorePath> parseOptionalStorePath(const nix::Store &store, const nix::Path path) {
-  if (path != "unknown-deriver" && path != "")
-    return parseStorePath(store, path);
-  return {};
-}
-#define parseOptionalStorePath23(store, path) parseOptionalStorePath(store, path)
-
-#define toDerivedPaths24(x) (x)
-
-namespace nix {
-  StorePathWithOutputs parsePathWithOutputs(const Store &store, const std::string & s);
-}
-
-// util.hh
-
-// C++17 std::visit boilerplate
-template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
-template<class... Ts> overloaded(Ts...) -> overloaded<Ts...>;
diff --git a/compat-2.3/include/nix/content-address.hh b/compat-2.3/include/nix/content-address.hh
deleted file mode 100644
--- a/compat-2.3/include/nix/content-address.hh
+++ /dev/null
@@ -1,77 +0,0 @@
-#pragma once
-
-#include <variant>
-#include "hash.hh"
-
-namespace nix {
-
-enum struct FileIngestionMethod : uint8_t {
-    Flat = false,
-    Recursive = true
-};
-
-struct TextHash {
-    Hash hash;
-};
-
-/// Pair of a hash, and how the file system was ingested
-struct FixedOutputHash {
-    FileIngestionMethod method;
-    Hash hash;
-    std::string printMethodAlgo() const;
-};
-
-/*
-  We've accumulated several types of content-addressed paths over the years;
-  fixed-output derivations support multiple hash algorithms and serialisation
-  methods (flat file vs NAR). Thus, ‘ca’ has one of the following forms:
-
-  * ‘text:sha256:<sha256 hash of file contents>’: For paths
-    computed by makeTextPath() / addTextToStore().
-
-  * ‘fixed:<r?>:<ht>:<h>’: For paths computed by
-    makeFixedOutputPath() / addToStore().
-*/
-typedef std::variant<
-    TextHash, // for paths computed by makeTextPath() / addTextToStore
-    FixedOutputHash // for path computed by makeFixedOutputPath
-> ContentAddress;
-
-/* Compute the prefix to the hash algorithm which indicates how the files were
-   ingested. */
-std::string makeFileIngestionPrefix(const FileIngestionMethod m);
-
-/* Compute the content-addressability assertion (ValidPathInfo::ca)
-   for paths created by makeFixedOutputPath() / addToStore(). */
-std::string makeFixedOutputCA(FileIngestionMethod method, const Hash & hash);
-
-std::string renderContentAddress(ContentAddress ca);
-
-std::string renderContentAddress(std::optional<ContentAddress> ca);
-
-ContentAddress parseContentAddress(std::string_view rawCa);
-
-std::optional<ContentAddress> parseContentAddressOpt(std::string_view rawCaOpt);
-
-Hash getContentAddressHash(const ContentAddress & ca);
-
-/*
-  We only have one way to hash text with references, so this is single-value
-  type is only useful in std::variant.
-*/
-struct TextHashMethod { };
-struct FixedOutputHashMethod {
-  FileIngestionMethod fileIngestionMethod;
-  HashType hashType;
-};
-
-typedef std::variant<
-    TextHashMethod,
-    FixedOutputHashMethod
-  > ContentAddressMethod;
-
-ContentAddressMethod parseContentAddressMethod(std::string_view rawCaMethod);
-
-std::string renderContentAddressMethod(ContentAddressMethod caMethod);
-
-}
diff --git a/compat-2.3/include/nix/derivation-output.hh b/compat-2.3/include/nix/derivation-output.hh
deleted file mode 100644
--- a/compat-2.3/include/nix/derivation-output.hh
+++ /dev/null
@@ -1,68 +0,0 @@
-#pragma once
-
-#include "path.hh"
-#include "types.hh"
-#include "hash.hh"
-#include "content-address.hh"
-#include "sync.hh"
-
-#include <map>
-#include <variant>
-
-namespace nix {
-/* Abstract syntax of derivations. */
-
-/* The traditional non-fixed-output derivation type. */
-struct DerivationOutputInputAddressed
-{
-    StorePath path;
-};
-
-/* Fixed-output derivations, whose output paths are content addressed
-   according to that fixed output. */
-struct DerivationOutputCAFixed
-{
-    std::string drvName; // !!! not in Nix 2.4, which stores name in Derivation.
-    FixedOutputHash hash; /* hash used for expected hash computation */
-    StorePath path(const Store & store, std::string_view drvName, std::string_view outputName) const;
-};
-
-/* Floating-output derivations, whose output paths are content addressed, but
-   not fixed, and so are dynamically calculated from whatever the output ends
-   up being. */
-struct DerivationOutputCAFloating
-{
-    /* information used for expected hash computation */
-    FileIngestionMethod method;
-    HashType hashType;
-};
-
-/* Input-addressed output which depends on a (CA) derivation whose hash isn't
- * known atm
- */
-struct DerivationOutputDeferred {};
-
-}
-
-namespace compat::nix {
-
-using namespace ::nix;
-
-struct DerivationOutput
-{
-    std::variant<
-        DerivationOutputInputAddressed,
-        DerivationOutputCAFixed,
-        DerivationOutputCAFloating,
-        DerivationOutputDeferred
-    > output;
-
-    /* Note, when you use this function you should make sure that you're passing
-       the right derivation name. When in doubt, you should use the safer
-       interface provided by BasicDerivation::outputsAndOptPaths */
-    std::optional<StorePath> path(const Store & store, std::string_view drvName, std::string_view outputName) const;
-};
-
-typedef std::map<string, DerivationOutput> DerivationOutputs;
-
-}
diff --git a/compat-2.3/include/nix/path-compat.hh b/compat-2.3/include/nix/path-compat.hh
deleted file mode 100644
--- a/compat-2.3/include/nix/path-compat.hh
+++ /dev/null
@@ -1,42 +0,0 @@
-#pragma once
-#include "path.hh"
-#include <vector>
-
-inline nix::Path printPath23(const nix::Store &store, const nix::StorePath &sp) {
-  return (store.storeDir + "/").append(sp.to_string());
-}
-
-inline nix::StorePath parseStorePath(const nix::Store &store, const nix::Path path) {
-  nix::Path p = nix::canonPath(path);
-  if (nix::dirOf(p) != store.storeDir)
-    throw nix::Error("path '%s' is not in the Nix store", p);
-  return nix::StorePath(nix::baseNameOf(p));
-}
-
-inline nix::PathSet printPathSet23(const nix::Store &store, std::vector<nix::StorePathWithOutputs> &sps) {
-  nix::PathSet r;
-  for (auto spwo : sps) {
-    for (auto output : spwo.outputs) {
-      r.insert(printPath23(store, spwo.path) + "!" + output);
-    }
-  }
-  return r;
-}
-
-inline nix::PathSet compatPathSet(const nix::Store &store, nix::StorePathSet &sps) {
-  nix::PathSet r;
-  for (auto sp : sps) {
-    r.insert(printPath23(store, sp));
-  }
-  return r;
-}
-
-inline nix::StorePathSet compatStorePathSet(const nix::Store &store, nix::PathSet &sps) {
-  nix::StorePathSet r;
-  for (auto sp : sps) {
-    r.insert(parseStorePath(store, sp));
-  }
-  return r;
-}
-
-#define parseStorePath23(store, path) parseStorePath(store, path)
diff --git a/compat-2.3/include/nix/path-info.hh b/compat-2.3/include/nix/path-info.hh
deleted file mode 100644
--- a/compat-2.3/include/nix/path-info.hh
+++ /dev/null
@@ -1,3 +0,0 @@
-#pragma once
-
-#include <nix/store-api.hh>
diff --git a/compat-2.3/include/nix/path.hh b/compat-2.3/include/nix/path.hh
deleted file mode 100644
--- a/compat-2.3/include/nix/path.hh
+++ /dev/null
@@ -1,91 +0,0 @@
-#pragma once
-
-// #include "content-address.hh"
-#include <nix/config.h>
-#include "types.hh"
-
-namespace nix {
-
-class Store;
-struct Hash;
-
-class StorePath
-{
-    std::string baseName;
-
-public:
-
-    /* Size of the hash part of store paths, in base-32 characters. */
-    constexpr static size_t HashLen = 32; // i.e. 160 bits
-
-    StorePath() = delete;
-
-    StorePath(std::string_view baseName);
-
-    StorePath(const Hash & hash, std::string_view name);
-
-    std::string_view to_string() const
-    {
-        return baseName;
-    }
-
-    bool operator < (const StorePath & other) const
-    {
-        return baseName < other.baseName;
-    }
-
-    bool operator == (const StorePath & other) const
-    {
-        return baseName == other.baseName;
-    }
-
-    bool operator != (const StorePath & other) const
-    {
-        return baseName != other.baseName;
-    }
-
-    /* Check whether a file name ends with the extension for
-       derivations. */
-    bool isDerivation() const;
-
-    std::string_view name() const
-    {
-        return std::string_view(baseName).substr(HashLen + 1);
-    }
-
-    std::string_view hashPart() const
-    {
-        return std::string_view(baseName).substr(0, HashLen);
-    }
-
-    static StorePath dummy;
-};
-
-typedef std::set<StorePath> StorePathSet;
-typedef std::vector<StorePath> StorePaths;
-typedef std::map<string, StorePath> OutputPathMap;
-
-// typedef std::map<StorePath, std::optional<ContentAddress>> StorePathCAMap;
-
-struct StorePathWithOutputs
-{
-    StorePath path;
-    std::set<std::string> outputs;
-
-    std::string to_string(const Store & store) const;
-};
-
-std::pair<std::string_view, StringSet> parsePathWithOutputs(std::string_view s);
-
-}
-
-namespace std {
-
-template<> struct hash<nix::StorePath> {
-    std::size_t operator()(const nix::StorePath & path) const noexcept
-    {
-        return * (std::size_t *) path.to_string().data();
-    }
-};
-
-}
diff --git a/compat-2.3/path.cxx b/compat-2.3/path.cxx
deleted file mode 100644
--- a/compat-2.3/path.cxx
+++ /dev/null
@@ -1,54 +0,0 @@
-
-#include <nix/config.h>
-#include <nix/store-api.hh>
-#include <nix/path-compat.hh>
-#include <nix/path.hh>
-#include <nix-compat.hh>
-
-using namespace nix;
-
-static void checkName(std::string_view path, std::string_view name)
-{
-    if (name.empty())
-        throw Error("store path '%s' has an empty name", path);
-    if (name.size() > 211)
-        throw Error("store path '%s' has a name longer than 211 characters", path);
-    for (auto c : name)
-        if (!((c >= '0' && c <= '9')
-                || (c >= 'a' && c <= 'z')
-                || (c >= 'A' && c <= 'Z')
-                || c == '+' || c == '-' || c == '.' || c == '_' || c == '?' || c == '='))
-            throw Error("store path '%s' contains illegal character '%s'", path, c);
-}
-
-StorePath::StorePath(std::string_view _baseName)
-    : baseName(_baseName)
-{
-    if (baseName.size() < HashLen + 1)
-        throw Error("'%s' is too short to be a valid store path", baseName);
-    for (auto c : hashPart())
-        if (c == 'e' || c == 'o' || c == 'u' || c == 't'
-            || !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')))
-            throw Error("store path '%s' contains illegal base-32 character '%s'", baseName, c);
-    checkName(baseName, name());
-}
-
-StorePath::StorePath(const Hash & hash, std::string_view _name)
-    : baseName((hash.to_string(Base32, false) + "-").append(std::string(_name)))
-{
-    checkName(baseName, name());
-}
-
-std::pair<std::string_view, StringSet> nix::parsePathWithOutputs(std::string_view s)
-{
-    size_t n = s.find("!");
-    return n == s.npos
-        ? std::make_pair(s, std::set<string>())
-        : std::make_pair(((std::string_view) s).substr(0, n),
-            tokenizeString<std::set<string>>((std::string(s)).substr(n + 1), ","));
-}
-
-StorePathWithOutputs nix::parsePathWithOutputs(const Store &store, const std::string & s) {
-    auto [path, outputs] = nix::parsePathWithOutputs(s);
-    return {parseStorePath(store, std::string(path)), std::move(outputs)};
-}
diff --git a/compat-2.4/include/nix-compat.hh b/compat-2.4/include/nix-compat.hh
deleted file mode 100644
--- a/compat-2.4/include/nix-compat.hh
+++ /dev/null
@@ -1,20 +0,0 @@
-#pragma once
-#include <nix/store-api.hh>
-#include <nix/derivations.hh>
-#include <nix/path-with-outputs.hh>
-
-#define printPath23(store, path) (path)
-#define compatPathSet(store, paths) (paths)
-#define printPathSet23(store, pathSet) (pathSet)
-#define parseStorePath23(store, path) (path)
-#define parseStorePathSet23(store, pathSet) (pathSet)
-#define parseOptionalStorePath23(store, path) (path)
-#define toDerivedPaths24(x) toDerivedPaths(x)
-
-inline nix::StorePath parseStorePath(const nix::Store &store, const nix::Path path) {
-  return store.parseStorePath(path);
-}
-
-inline void compatComputeFSClosure(nix::Store &store, nix::StorePathSet &pathSet, nix::StorePathSet &closurePaths, bool flipDirection = false, bool includeOutputs = false, bool includeDerivers = false) {
-  store.computeFSClosure(pathSet, closurePaths, flipDirection, includeOutputs, includeDerivers);
-}
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,31 +1,33 @@
 cabal-version: 2.4
 
 name:           hercules-ci-cnix-store
-version:        0.2.1.2
+version:        0.3.0.0
 synopsis:       Haskell bindings for Nix's libstore
 category:       Nix
 homepage:       https://docs.hercules-ci.com
 bug-reports:    https://github.com/hercules-ci/hercules-ci-agent/issues
 author:         Hercules CI contributors
 maintainer:     info@hercules-ci.com
-copyright:      2018-2020 Hercules CI
+copyright:      2018-2021 Hercules CI
 license:        Apache-2.0
 build-type:     Custom
 extra-source-files:
     CHANGELOG.md
     test/data/*.drv
-    compat-2.3/include/*.hh
-    compat-2.3/include/nix/*.hh
-    compat-2.4/include/*.hh
 
 source-repository head
   type: git
   location: https://github.com/hercules-ci/hercules-ci-agent
 
+-- Deprecated
 flag nix-2_4
   description: Build for Nix >=2.4pre*
-  default: False
+  default: True
 
+flag nix-2_5
+  description: Build for Nix >=2.5pre*
+  default: True
+
 -- match the C++ language standard Nix is using
 common cxx-opts
   cxx-options:
@@ -59,6 +61,7 @@
   exposed-modules:
       Hercules.CNix
       Hercules.CNix.Encapsulation
+      Hercules.CNix.Exception
       Hercules.CNix.Settings
       Hercules.CNix.Std.Set
       Hercules.CNix.Std.String
@@ -69,38 +72,19 @@
       Hercules.CNix.Store.Context
       Hercules.CNix.Store.Instances
       Hercules.CNix.Util
+      Hercules.CNix.Verbosity
   include-dirs:
       include
   pkgconfig-depends:
-      nix-store >= 2.3 && < 2.8
-    , nix-main >= 2.3 && < 2.8
-  if flag(nix-2_4)
-    include-dirs:
-        compat-2.4/include
-    cpp-options: -DNIX_2_4
-    cxx-options: -DNIX_2_4
-  else
-    include-dirs:
-        compat-2.3/include
-    cxx-sources:
-        compat-2.3/derivation-output.cxx
-        compat-2.3/path.cxx
-    install-includes:
-        nix/path-info.hh
-        nix/path.hh
-        nix/path-compat.hh
-        nix/derivation-output.hh
-        nix/content-address.hh
-
+      nix-store >= 2.4 && < 2.8
+    , nix-main >= 2.4 && < 2.8
   install-includes:
       hercules-ci-cnix/store.hxx
-      nix-compat.hh
-
   hs-source-dirs: src
   build-depends:
       base >= 4.7 && <5
     , inline-c
-    , inline-c-cpp
+    , inline-c-cpp >= 0.5.0.0
     , bytestring
     , conduit
     , containers
@@ -122,7 +106,7 @@
     -fwarn-name-shadowing
     -fwarn-incomplete-patterns
 
-test-suite test
+test-suite hercules-ci-cnix-store-tests
   type: exitcode-stdio-1.0
   main-is: TestMain.hs
   other-modules:
@@ -130,6 +114,7 @@
       Hercules.CNix.Std.SetSpec
       Hercules.CNix.Store.DerivationSpec
       Hercules.CNix.Store.TestUtil
+      Hercules.CNix.VerbositySpec
   hs-source-dirs:
       test
   default-extensions: DeriveGeneric DeriveTraversable DisambiguateRecordFields FlexibleContexts InstanceSigs LambdaCase MultiParamTypeClasses NoImplicitPrelude OverloadedStrings RankNTypes TupleSections TypeApplications TypeOperators
diff --git a/src/Hercules/CNix.hs b/src/Hercules/CNix.hs
--- a/src/Hercules/CNix.hs
+++ b/src/Hercules/CNix.hs
@@ -14,8 +14,12 @@
 
 import Data.ByteString.Unsafe (unsafePackMallocCString)
 import Hercules.CNix.Store
+import Hercules.CNix.Verbosity
+  ( Verbosity (Debug, Talkative),
+    setVerbosity,
+  )
 import qualified Language.C.Inline.Cpp as C
-import qualified Language.C.Inline.Cpp.Exceptions as C
+import qualified Language.C.Inline.Cpp.Exception as C
 import Protolude hiding (evalState, throwIO)
 import System.IO.Unsafe (unsafePerformIO)
 
@@ -57,16 +61,10 @@
     } |]
 
 setTalkative :: IO ()
-setTalkative =
-  [C.throwBlock| void {
-    nix::verbosity = nix::lvlTalkative;
-  } |]
+setTalkative = setVerbosity Talkative
 
 setDebug :: IO ()
-setDebug =
-  [C.throwBlock| void {
-    nix::verbosity = nix::lvlVomit;
-  } |]
+setDebug = setVerbosity Debug
 
 setGlobalOption :: Text -> Text -> IO ()
 setGlobalOption opt value = do
diff --git a/src/Hercules/CNix/Exception.hs b/src/Hercules/CNix/Exception.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/CNix/Exception.hs
@@ -0,0 +1,42 @@
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Hercules.CNix.Exception where
+
+import Hercules.CNix.Store.Context (context)
+import qualified Language.C.Inline.Cpp as C
+import qualified Language.C.Inline.Cpp.Exception as C
+import Protolude
+import qualified System.Environment
+
+C.context context
+
+C.include "<nix/config.h>"
+C.include "<nix/shared.hh>"
+C.include "<nix/globals.hh>"
+
+handleExceptions :: IO a -> IO a
+handleExceptions io = do
+  progName <- System.Environment.getProgName
+  handleExceptions' exitWith (toS progName) io
+
+handleExceptions' :: (ExitCode -> IO a) -> Text -> IO a -> IO a
+handleExceptions' handleExit programName io =
+  let select (C.CppStdException eptr _msg _t) = Just eptr
+      select _ = Nothing
+
+      convertExit 0 = ExitSuccess
+      convertExit e = ExitFailure (fromIntegral e)
+
+      doHandle = handleExit . convertExit <=< handleExceptionPtr (encodeUtf8 programName)
+   in handleJust select doHandle io
+
+handleExceptionPtr :: ByteString -> C.CppExceptionPtr -> IO C.CInt
+handleExceptionPtr programName eptr =
+  [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);
+    });
+  }|]
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
@@ -1,5 +1,4 @@
 {-# LANGUAGE BlockArguments #-}
-{-# LANGUAGE CPP #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE ScopedTypeVariables #-}
@@ -46,7 +45,7 @@
 import qualified Hercules.CNix.Store.Context as C hiding (context)
 import Hercules.CNix.Store.Instances ()
 import qualified Language.C.Inline.Cpp as C
-import qualified Language.C.Inline.Cpp.Exceptions as C
+import qualified Language.C.Inline.Cpp.Exception as C
 import Protolude
 import System.IO.Unsafe (unsafePerformIO)
 import qualified Prelude
@@ -73,20 +72,12 @@
 
 C.include "<nix/worker-protocol.hh>"
 
-#ifdef NIX_2_4
 C.include "<nix/path-with-outputs.hh>"
-#endif
 
 C.include "hercules-ci-cnix/store.hxx"
 
-C.include "nix-compat.hh"
-
 C.using "namespace nix"
 
-#ifndef NIX_2_4
-C.using "namespace compat::nix"
-#endif
-
 forNonNull :: Applicative m => Ptr a -> (Ptr a -> m b) -> m (Maybe b)
 forNonNull = flip traverseNonNull
 
@@ -228,22 +219,11 @@
 --
 -- Throws C++ `BadStorePath` exception when invalid.
 parseStorePath :: Store -> ByteString -> IO StorePath
-#ifdef NIX_2_4
 parseStorePath (Store store) bs =
   moveStorePath
     =<< [C.throwBlock| nix::StorePath *{
       return new StorePath(std::move((*$(refStore* store))->parseStorePath(std::string($bs-ptr:bs, $bs-len:bs))));
     }|]
-#else
-parseStorePath (Store store) bs =
-  moveStorePath
-    =<< [C.throwBlock| nix::StorePath *{
-      auto p = canonPath(std::string($bs-ptr:bs, $bs-len:bs));
-      if (dirOf(p) != (*$(refStore* store))->storeDir)
-        throw Error("path '%s' is not in the Nix store", p);
-      return new StorePath(baseNameOf(p));
-    }|]
-#endif
 
 getStorePathBaseName :: StorePath -> IO ByteString
 getStorePathBaseName (StorePath sp) = do
@@ -262,7 +242,6 @@
     }|]
 
 storePathToPath :: Store -> StorePath -> IO ByteString
-#ifdef NIX_2_4
 storePathToPath (Store store) (StorePath sp) =
   BS.unsafePackMallocCString
     =<< [C.block| const char *{
@@ -271,16 +250,6 @@
       std::string s(store.printStorePath(sp));
       return strdup(s.c_str());
     }|]
-#else
-storePathToPath (Store store) (StorePath sp) =
-  BS.unsafePackMallocCString
-    =<< [C.block| const char *{
-      Store & store = **$(refStore* store);
-      StorePath &sp = *$fptr-ptr:(nix::StorePath *sp);
-      std::string s(printPath23(store, sp));
-      return strdup(s.c_str());
-    }|]
-#endif
 
 ensurePath :: Store -> StorePath -> IO ()
 ensurePath (Store store) (StorePath storePath) =
@@ -288,7 +257,7 @@
     ReceiveInterrupts _;
     Store &store = **$(refStore* store);
     StorePath &storePath = *$fptr-ptr:(nix::StorePath *storePath);
-    store.ensurePath(printPath23(store, storePath));
+    store.ensurePath(storePath);
   } |]
 
 clearPathInfoCache :: Store -> IO ()
@@ -355,7 +324,7 @@
     ReceiveInterrupts _;
     Store &store = **$(refStore* store);
     std::vector<StorePathWithOutputs> &paths = *$fptr-ptr:(std::vector<nix::StorePathWithOutputs>* paths);
-    store.buildPaths(toDerivedPaths24(printPathSet23(store, paths)));
+    store.buildPaths(toDerivedPaths(paths));
   }|]
 
 buildPath :: Store -> StorePathWithOutputs -> IO ()
@@ -385,7 +354,7 @@
       ReceiveInterrupts _;
       Store &store = **$(refStore* store);
       return new Derivation(
-          store.derivationFromPath(printPath23(store, *$fptr-ptr:(nix::StorePath *spwo)))
+          store.derivationFromPath(*$fptr-ptr:(nix::StorePath *spwo))
         );
     } |]
 
@@ -397,7 +366,6 @@
   -- | Contents
   ByteString ->
   IO Derivation
-#ifdef NIX_2_4
 getDerivationFromString (Store store) name contents = do
   moveToForeignPtrWrapper
     =<< [C.throwBlock| Derivation *{
@@ -405,37 +373,15 @@
       std::string name($bs-ptr:name, $bs-len:name);
       return new Derivation(parseDerivation(store, std::string($bs-ptr:contents, $bs-len:contents), name));
     }|]
-#else
-getDerivationFromString _store name contents = do
-  moveToForeignPtrWrapper =<< [C.throwBlock| Derivation *{
-      std::string name($bs-ptr:name, $bs-len:name);
-      std::string contents($bs-ptr:contents, $bs-len:contents);
-      AutoDelete tmpDir(createTempDir(), true);
-      Path tmpFile = (Path) tmpDir + "/" + name;
-      writeFile(tmpFile, contents, 0600);
-      return new Derivation(readDerivation(tmpFile));
-    }|]
-#endif
 
 getDerivationNameFromPath :: StorePath -> IO ByteString
 getDerivationNameFromPath storePath =
   BS.unsafePackMallocCString
-
-#ifdef NIX_2_4
     =<< [C.throwBlock| const char *{
       StorePath &sp = *$fptr-ptr:(nix::StorePath *storePath);
       std::string s(Derivation::nameFromPath(sp));
       return strdup(s.c_str());
     }|]
-#else
-    =<< [C.throwBlock| const char *{
-      StorePath &sp = *$fptr-ptr:(nix::StorePath *storePath);
-      std::string pathName(sp.name());
-      assert(isDerivation(pathName));
-      std::string drvName = std::string(pathName, 0, pathName.size() - drvExtension.size());
-      return strdup(drvName.c_str());
-    }|]
-#endif
 
 data DerivationOutput = DerivationOutput
   { derivationOutputName :: !ByteString,
@@ -503,11 +449,7 @@
                       },
                       [&](DerivationOutputCAFixed dof) -> void {
                         typ = 1;
-#ifdef NIX_2_4
                         path = new StorePath(dof.path(store, $fptr-ptr:(Derivation *derivation)->name, nameString));
-#else
-                        path = new StorePath(dof.path(store, dof.drvName, nameString));
-#endif
                         switch (dof.hash.method) {
                           case nix::FileIngestionMethod::Flat:
                             fim = 0;
@@ -576,11 +518,7 @@
                       [&](DerivationOutputDeferred) -> void {
                         typ = 3;
                       },
-#ifdef NIX_2_4
                     }, i->second.output);
-#else
-                    }, compatDerivationOutput(store, drvName, i->second).output);
-#endif
                     i++;
                   }|]
                   name <- unsafePackMallocCString =<< peek nameP
@@ -652,20 +590,25 @@
     toByteStrings
 
 getDerivationSources :: Store -> Derivation -> IO [StorePath]
-getDerivationSources (Store store) derivation = mask_ do
+getDerivationSources _ = getDerivationSources'
+
+getDerivationSources' :: Derivation -> IO [StorePath]
+getDerivationSources' derivation = mask_ do
   vec <-
     moveToForeignPtrWrapper
       =<< [C.throwBlock| std::vector<nix::StorePath*>* {
-        Store &store = **$(refStore* store);
         auto r = new std::vector<StorePath *>();
         for (auto s : $fptr-ptr:(Derivation *derivation)->inputSrcs)
-          r->push_back(new StorePath(parseStorePath23(store, s)));
+          r->push_back(new StorePath(s));
         return r;
       }|]
   traverse moveStorePath =<< Std.Vector.toList vec
 
 getDerivationInputs :: Store -> Derivation -> IO [(StorePath, [ByteString])]
-getDerivationInputs (Store store) derivation =
+getDerivationInputs _ = getDerivationInputs'
+
+getDerivationInputs' :: Derivation -> IO [(StorePath, [ByteString])]
+getDerivationInputs' derivation =
   bracket
     [C.exp| DerivationInputsIterator* {
       new DerivationInputsIterator($fptr-ptr:(Derivation *derivation)->inputDrvs.begin())
@@ -678,8 +621,7 @@
         else do
           name <-
             [C.throwBlock| nix::StorePath *{
-              Store &store = **$(refStore* store);
-              return new StorePath(parseStorePath23(store, (*$(DerivationInputsIterator *i))->first));
+              return new StorePath((*$(DerivationInputsIterator *i))->first);
             }|]
               >>= moveStorePath
           outs <-
@@ -797,13 +739,9 @@
         pathSet.insert(*spp);
 
       StorePathSet closurePaths;
-      compatComputeFSClosure(*src, pathSet, closurePaths);
+      src->computeFSClosure(pathSet, closurePaths);
 
-#ifdef NIX_2_4
       nix::copyPaths(*src, *dest, closurePaths);
-#else
-      nix::copyPaths(src, dest, compatPathSet(*src, closurePaths));
-#endif
     }|]
   for_ pathList (\(StorePath c) -> touchForeignPtr c)
 
@@ -840,22 +778,18 @@
     nix::ref<nix::Store> store = *$(refStore *store);
     const StorePath &storePath = *$fptr-ptr:(nix::StorePath *path);
     const SecretKey &secretKey = *$(SecretKey *secretKey);
-    auto currentInfo = store->queryPathInfo(printPath23(*store, storePath));
+    auto currentInfo = store->queryPathInfo(storePath);
 
     auto info2(*currentInfo);
     info2.sigs.clear();
-#ifdef NIX_2_4
     info2.sign(*store, secretKey);
-#else
-    info2.sign(secretKey);
-#endif
     assert(!info2.sigs.empty());
     auto sig = *info2.sigs.begin();
 
     if (currentInfo->sigs.count(sig)) {
       return 0;
     } else {
-      store->addSignatures(printPath23(*store, storePath), info2.sigs);
+      store->addSignatures(storePath, info2.sigs);
       return 1;
     }
   }|]
@@ -868,7 +802,7 @@
       ReceiveInterrupts _;
       Store &store = **$(refStore* store);
       std::string s = std::string($bs-ptr:bs, $bs-len:bs);
-      return new StorePath(parseStorePath23(store, store.followLinksToStorePath(s)));
+      return new StorePath(store.followLinksToStorePath(s));
     }|]
 
 queryPathInfo ::
@@ -883,7 +817,7 @@
       ReceiveInterrupts _;
       Store &store = **$(refStore* store);
       StorePath &path = *$fptr-ptr:(nix::StorePath *path);
-      return new refValidPathInfo(store.queryPathInfo(printPath23(store, path)));
+      return new refValidPathInfo(store.queryPathInfo(path));
     }|]
   newForeignPtr finalizeRefValidPathInfo vpi
 
@@ -915,26 +849,28 @@
     |]
 
 -- | Deriver field of a ValidPathInfo struct. Source: store-api.hh
---
--- Returns 'unknownDeriver' when missing.
 validPathInfoDeriver :: Store -> ForeignPtr (Ref ValidPathInfo) -> IO (Maybe StorePath)
-validPathInfoDeriver (Store store) vpi =
+validPathInfoDeriver _ = validPathInfoDeriver'
+
+validPathInfoDeriver' :: ForeignPtr (Ref ValidPathInfo) -> IO (Maybe StorePath)
+validPathInfoDeriver' vpi =
   moveStorePathMaybe
     =<< [C.throwBlock| nix::StorePath * {
-      Store &store = **$(refStore* store);
-      std::optional<StorePath> deriver = parseOptionalStorePath23(store, (*$fptr-ptr:(refValidPathInfo* vpi))->deriver);
+      std::optional<StorePath> deriver = (*$fptr-ptr:(refValidPathInfo* vpi))->deriver;
       return deriver ? new StorePath(*deriver) : nullptr;
     }|]
 
 -- | References field of a ValidPathInfo struct. Source: store-api.hh
 validPathInfoReferences :: Store -> ForeignPtr (Ref ValidPathInfo) -> IO [StorePath]
-validPathInfoReferences (Store store) vpi = do
+validPathInfoReferences _ = validPathInfoReferences'
+
+validPathInfoReferences' :: ForeignPtr (Ref ValidPathInfo) -> IO [StorePath]
+validPathInfoReferences' vpi = do
   sps <-
     moveToForeignPtrWrapper
       =<< [C.throwBlock| std::vector<nix::StorePath *>* {
-        Store &store = **$(refStore* store);
         auto sps = new std::vector<nix::StorePath *>();
-        for (auto sp : parseStorePathSet23(store, (*$fptr-ptr:(refValidPathInfo* vpi))->references))
+        for (auto sp : (*$fptr-ptr:(refValidPathInfo* vpi))->references)
           sps->push_back(new StorePath(sp));
         return sps;
       }|]
@@ -969,7 +905,7 @@
     ReceiveInterrupts _;
     Store &store = **$(refStore* store);
     StorePathSet &ret = *$fptr-ptr:(std::set<nix::StorePath>* retSet);
-    compatComputeFSClosure(store, *$fptr-ptr:(std::set<nix::StorePath>* startingSet), ret,
+    store.computeFSClosure(*$fptr-ptr:(std::set<nix::StorePath>* startingSet), ret,
       $(int flipDir), $(int inclOut), $(int inclDrv));
   }|]
   pure ret
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
@@ -1,4 +1,3 @@
-{-# LANGUAGE CPP #-}
 {-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE TemplateHaskell #-}
 {-# OPTIONS_GHC -Wno-orphans #-}
@@ -19,10 +18,7 @@
 
 C.include "<nix/path.hh>"
 C.include "<nix/derivations.hh>"
-
-#ifdef NIX_2_4
 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
@@ -17,7 +17,7 @@
   ( context,
   )
 import qualified Language.C.Inline.Cpp as C
-import qualified Language.C.Inline.Cpp.Exceptions as C
+import qualified Language.C.Inline.Cpp.Exception as C
 import Protolude
 import System.Mem.Weak (deRefWeak)
 import System.Posix (Handler (Catch), installHandler, sigHUP, sigINT, sigTERM, sigUSR1)
diff --git a/src/Hercules/CNix/Verbosity.hs b/src/Hercules/CNix/Verbosity.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/CNix/Verbosity.hs
@@ -0,0 +1,72 @@
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Hercules.CNix.Verbosity where
+
+import Foreign (fromBool, toBool)
+import Hercules.CNix.Store.Context (context)
+import qualified Language.C.Inline.Cpp as C
+import qualified Language.C.Inline.Cpp.Exception as C
+import Protolude
+
+C.context context
+
+C.include "<nix/config.h>"
+C.include "<nix/error.hh>"
+C.include "<nix/globals.hh>"
+C.include "<nix/logging.hh>"
+
+data Verbosity
+  = Error
+  | Warn
+  | Notice
+  | Info
+  | Talkative
+  | Chatty
+  | Debug
+  | Vomit
+  deriving (Eq, Ord, Enum, Bounded, Show, Read)
+
+setVerbosity :: Verbosity -> IO ()
+setVerbosity Error = [C.throwBlock| void { nix::verbosity = nix::lvlError; } |]
+setVerbosity Warn = [C.throwBlock| void { nix::verbosity = nix::lvlWarn; } |]
+setVerbosity Notice = [C.throwBlock| void { nix::verbosity = nix::lvlNotice; } |]
+setVerbosity Info = [C.throwBlock| void { nix::verbosity = nix::lvlInfo; } |]
+setVerbosity Talkative = [C.throwBlock| void { nix::verbosity = nix::lvlTalkative; } |]
+setVerbosity Chatty = [C.throwBlock| void { nix::verbosity = nix::lvlChatty; } |]
+setVerbosity Debug = [C.throwBlock| void { nix::verbosity = nix::lvlDebug; } |]
+setVerbosity Vomit = [C.throwBlock| void { nix::verbosity = nix::lvlVomit; } |]
+
+getVerbosity :: IO Verbosity
+getVerbosity =
+  [C.throwBlock| int { switch(nix::verbosity) {
+    case nix::lvlError: return 1;
+    case nix::lvlWarn: return 2;
+    case nix::lvlNotice: return 3;
+    case nix::lvlInfo: return 4;
+    case nix::lvlTalkative: return 5;
+    case nix::lvlChatty: return 6;
+    case nix::lvlDebug: return 7;
+    case nix::lvlVomit: return 8;
+    default: return 0;
+  } }|]
+    >>= \case
+      1 -> pure Error
+      2 -> pure Warn
+      3 -> pure Notice
+      4 -> pure Info
+      5 -> pure Talkative
+      6 -> pure Chatty
+      7 -> pure Debug
+      8 -> pure Vomit
+      _ -> throwIO (FatalError "Unknown nix::verbosity value")
+
+getShowTrace :: IO Bool
+getShowTrace =
+  [C.throwBlock| bool { return nix::loggerSettings.showTrace.get(); }|]
+    <&> toBool
+
+setShowTrace :: Bool -> IO ()
+setShowTrace b =
+  let b' = fromBool b
+   in [C.throwBlock| void { nix::loggerSettings.showTrace.assign($(bool b')); }|]
diff --git a/test/Hercules/CNix/VerbositySpec.hs b/test/Hercules/CNix/VerbositySpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Hercules/CNix/VerbositySpec.hs
@@ -0,0 +1,34 @@
+{-# LANGUAGE BlockArguments #-}
+
+module Hercules.CNix.VerbositySpec (spec) where
+
+import Hercules.CNix.Verbosity
+import Protolude
+import Test.Hspec
+
+spec :: Spec
+spec = do
+  describe "setVerbosity" do
+    it "affects getVerbosity" do
+      withResettingVerbosity do
+        for_ [minBound .. maxBound] \v -> do
+          setVerbosity v
+          v' <- getVerbosity
+          v' `shouldBe` v
+  describe "setShowTrace" do
+    it "affects getShowTrace" do
+      withResettingShowTrace do
+        for_ [True, False] \v -> do
+          setShowTrace v
+          v' <- getShowTrace
+          v' `shouldBe` v
+
+-- bracket on global state is still a leaky abstraction, so we don't add this
+-- to the library.
+withResettingVerbosity :: IO a -> IO a
+withResettingVerbosity =
+  bracket getVerbosity setVerbosity . const
+
+withResettingShowTrace :: IO a -> IO a
+withResettingShowTrace =
+  bracket getShowTrace setShowTrace . const
