diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,11 @@
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
+## 0.3.6.5 - 2024-11-15
+
+- Nix 2.24 support
+  This includes the newly separate `nix-flake` library as a dependency of `hercules-ci-cnix-expr`.
+
 ## 0.3.6.4 - 2024-06-12
 
 Got rid of unnecessary data files, improving packaging.
diff --git a/hercules-ci-cnix-expr.cabal b/hercules-ci-cnix-expr.cabal
--- a/hercules-ci-cnix-expr.cabal
+++ b/hercules-ci-cnix-expr.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.4
 
 name:           hercules-ci-cnix-expr
-version:        0.3.6.4
+version:        0.3.6.5
 synopsis:       Bindings for the Nix evaluator
 category:       Nix, CI, Testing, DevOps
 homepage:       https://docs.hercules-ci.com
@@ -33,6 +33,10 @@
   description: Build for Nix >=2.5pre*
   default: False
 
+flag nix-2_24
+  description: Build for Nix >=2.24
+  default: False
+
 flag ide
   description: Whether to enable IDE workarounds. You shouldn't need this.
   default: False
@@ -125,6 +129,9 @@
     pkgconfig-depends:
       -- MixEvalArgs has moved here
       nix-cmd >= 2.8
+  if flag(nix-2_24)
+    pkgconfig-depends:
+      nix-flake
 
 
 test-suite hercules-ci-cnix-expr-unit-tests
diff --git a/src/Hercules/CNix/Expr.hs b/src/Hercules/CNix/Expr.hs
--- a/src/Hercules/CNix/Expr.hs
+++ b/src/Hercules/CNix/Expr.hs
@@ -131,6 +131,12 @@
 C.include "<nix/args/root.hh>"
 #endif
 
+#if NIX_IS_AT_LEAST(2,24,0)
+C.include "<nix/config-global.hh>"
+C.include "<nix/eval-gc.hh>"
+C.include "<nix/common-eval-args.hh>"
+#endif
+
 C.include "hercules-ci-cnix/expr.hxx"
 
 C.include "hercules-ci-cnix/string.hxx"
@@ -165,6 +171,9 @@
 #endif
       nix::settings.experimentalFeatures.assign(features);
 #endif
+#if NIX_IS_AT_LEAST(2,24,0)
+      nix::flake::initLib(flakeSettings);
+#endif
     } |]
 
 -- | Initialize the current (main) thread for stack overflow detection.
@@ -280,8 +289,13 @@
 -- | (private) Make an EvalState and leak it.
 newEvalState :: MonadIO m => Store -> m (Ptr EvalState)
 newEvalState (Store store) = liftIO
-#if NIX_IS_AT_LEAST(2,17,0)
+#if NIX_IS_AT_LEAST(2,24,0)
   [C.throwBlock| EvalState* {
+    nix::LookupPath emptyLookupPath;
+    return new EvalState(emptyLookupPath, *$(refStore* store), fetchSettings, evalSettings);
+  } |]
+#elif NIX_IS_AT_LEAST(2,17,0)
+  [C.throwBlock| EvalState* {
     nix::SearchPath searchPaths;
     return new EvalState(searchPaths, *$(refStore* store));
   } |]
@@ -325,7 +339,7 @@
   }|]
 
 addInternalAllowedPaths :: Ptr EvalState -> IO ()
-addInternalAllowedPaths evalState = do
+addInternalAllowedPaths _evalState = do
   pass
 
 evalFile :: Ptr EvalState -> FilePath -> IO RawValue
@@ -390,7 +404,11 @@
     =<< [C.throwBlock| Value* {
           Value result;
           $(EvalState *evalState)->autoCallFunction(
+#if NIX_IS_AT_LEAST(2,24,0)
+                  const_cast<Bindings &>(*$(Value *autoArgs)->attrs()),
+#else
                   *$(Value *autoArgs)->attrs,
+#endif
                   *$(Value *fun),
                   result);
           return new (NoGC) Value (result);
@@ -419,8 +437,14 @@
     <$> [C.throwBlock| int {
           Value *v = $(Value *v);
           EvalState &evalState = *$(EvalState *evalState);
-          Bindings::iterator iter = v->attrs->find(evalState.sRecurseForDerivations);
-          if (iter == v->attrs->end()) {
+#if NIX_IS_AT_LEAST(2,24,0)
+          auto attrs = v->attrs();
+#else
+          auto attrs = v->attrs;
+#endif
+          // Bindings::const_iterator iter; // 2.24
+          auto iter = attrs->find(evalState.sRecurseForDerivations);
+          if (iter == attrs->end()) {
             return 0;
           } else {
             // Previously this bool was unpacked manually and included a special
@@ -444,8 +468,14 @@
       Value &v = *$(Value *v);
       EvalState &evalState = *$(EvalState *evalState);
       Symbol k = evalState.symbols.create($bs-cstr:k);
-      Bindings::iterator iter = v.attrs->find(k);
-      if (iter == v.attrs->end()) {
+#if NIX_IS_AT_LEAST(2,24,0)
+      auto attrs = v.attrs();
+#else
+      auto attrs = v.attrs;
+#endif
+      // Bindings::const_iterator iter; // 2.24
+      auto iter = attrs->find(k);
+      if (iter == attrs->end()) {
         return nullptr;
       } else {
         return iter->value;
@@ -459,8 +489,13 @@
 
 getAttrs :: Ptr EvalState -> Value NixAttrs -> IO (Map ByteString RawValue)
 getAttrs evalState (Value (RawValue v)) = do
+#if NIX_IS_AT_LEAST(2,24,0)
+  begin <- [C.exp| const Attr *{ $(Value *v)->attrs()->begin() }|]
+  end <- [C.exp| const Attr *{ $(Value *v)->attrs()->end() }|]
+#else
   begin <- [C.exp| Attr *{ $(Value *v)->attrs->begin() }|]
   end <- [C.exp| Attr *{ $(Value *v)->attrs->end() }|]
+#endif
   let gather :: Map ByteString RawValue -> Ptr Attr' -> IO (Map ByteString RawValue)
       gather acc i | i == end = pure acc
       gather acc i = do
@@ -486,7 +521,11 @@
       EvalState &state = *$(EvalState *evalState);
       auto drvInfo = getDerivation(state, *$(Value *v), false);
       if (!drvInfo)
+#if NIX_IS_AT_LEAST(2,24,0)
+        throw EvalError(state, "Not a valid derivation");
+#else
         throw EvalError("Not a valid derivation");
+#endif
 
 #if NIX_IS_AT_LEAST(2,7,0)
       StorePath storePath = drvInfo->requireDrvPath();
@@ -607,9 +646,20 @@
     EvalState &evalState = *$(EvalState *evalState);
     Value *r = new (NoGC) Value();
     std::string flakeRefStr($bs-ptr:flakeRef, $bs-len:flakeRef);
-    auto flakeRef = nix::parseFlakeRef(flakeRefStr, {}, true);
+    auto flakeRef = nix::parseFlakeRef(
+#if NIX_IS_AT_LEAST(2,24,0)
+      fetchSettings,
+#endif
+      flakeRefStr,
+      {},
+      true);
     nix::flake::callFlake(evalState,
-      nix::flake::lockFlake(evalState, flakeRef,
+      nix::flake::lockFlake(
+#if NIX_IS_AT_LEAST(2,24,0)
+        flakeSettings,
+#endif
+        evalState,
+        flakeRef,
         nix::flake::LockFlags {
           .updateLockFile = false,
           .useRegistries = false,
@@ -632,9 +682,20 @@
     EvalState &evalState = *$(EvalState *evalState);
     Value *r = new (NoGC) Value();
     std::string path($bs-ptr:absPath, $bs-len:absPath);
-    auto flakeRef = nix::parseFlakeRef(path, {}, true);
+    auto flakeRef = nix::parseFlakeRef(
+#if NIX_IS_AT_LEAST(2,24,0)
+      fetchSettings,
+#endif
+      path,
+      {},
+      true);
     nix::flake::callFlake(evalState,
-      nix::flake::lockFlake(evalState, flakeRef,
+      nix::flake::lockFlake(
+#if NIX_IS_AT_LEAST(2,24,0)
+        flakeSettings,
+#endif
+        evalState,
+        flakeRef,
         nix::flake::LockFlags {
           .updateLockFile = false,
           .useRegistries = false,
@@ -667,9 +728,18 @@
     attrs.emplace("ref", ref);
     attrs.emplace("rev", rev);
 
-    auto flakeRef = nix::FlakeRef::fromAttrs(attrs);
+    auto flakeRef = nix::FlakeRef::fromAttrs(
+#if NIX_IS_AT_LEAST(2,24,0)
+      fetchSettings,
+#endif
+      attrs);
     nix::flake::callFlake(evalState,
-      nix::flake::lockFlake(evalState, flakeRef,
+      nix::flake::lockFlake(
+#if NIX_IS_AT_LEAST(2,24,0)
+        flakeSettings,
+#endif
+        evalState,
+        flakeRef,
         nix::flake::LockFlags {
           .updateLockFile = false,
           .useRegistries = false,
@@ -969,11 +1039,35 @@
 instance ToRawValue a => ToValue (Vector a) where
   type NixTypeFor (Vector a) = NixList
   toValue evalState vec =
+#if NIX_IS_AT_LEAST(2,24,0)
     coerce <$> do
       let l :: C.CInt
           l = fromIntegral (length vec)
+      b <-
+        [C.block| ListBuilder* {
+          EvalState &evalState = *$(EvalState *evalState);
+          return new (NoGC) ListBuilder(evalState, $(int l));
+        }|]
+      vec & V.imapM_ \i a -> do
+        RawValue aRaw <- toRawValue evalState a
+        let ix = fromIntegral i
+        [C.block| void {
+          ListBuilder &b = *$(ListBuilder *b);
+          b[$(int ix)] = $(Value *aRaw);
+        }|]
       v <-
         [C.block| Value* {
+          Value * v = new (NoGC) Value();
+          v->mkList(*$(ListBuilder *b));
+          return v;
+        }|]
+      Value <$> mkRawValue v
+#else
+    coerce <$> do
+      let l :: C.CInt
+          l = fromIntegral (length vec)
+      v <-
+        [C.block| Value* {
           EvalState &evalState = *$(EvalState *evalState);
           Value *v = new (NoGC) Value();
           evalState.mkList(*v, $(int l));
@@ -987,6 +1081,7 @@
           v.listElems()[$(int ix)] = $(Value *aRaw);
         }|]
       Value <$> mkRawValue v
+#endif
 
 instance ToRawValue a => ToRawValue [a]
 
diff --git a/src/Hercules/CNix/Expr/Context.hs b/src/Hercules/CNix/Expr/Context.hs
--- a/src/Hercules/CNix/Expr/Context.hs
+++ b/src/Hercules/CNix/Expr/Context.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE CPP #-}
 {-# LANGUAGE EmptyDataDecls #-}
 {-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE TemplateHaskellQuotes #-}
@@ -9,6 +10,9 @@
     Value',
     Attr',
     BindingsBuilder',
+#if NIX_IS_AT_LEAST(2,24,0)
+    ListBuilder',
+#endif
     module Hercules.CNix.Store.Context,
     (=:),
   )
@@ -29,6 +33,10 @@
 
 data BindingsBuilder'
 
+#if NIX_IS_AT_LEAST(2,24,0)
+data ListBuilder'
+#endif
+
 context :: C.Context
 context =
   C.cppCtx
@@ -51,4 +59,8 @@
           =: [t|Attr'|]
           <> C.TypeName "BindingsBuilder"
           =: [t|BindingsBuilder'|]
+#if NIX_IS_AT_LEAST(2,24,0)
+          <> C.TypeName "ListBuilder"
+          =: [t|ListBuilder'|]
+#endif
     }
diff --git a/src/Hercules/CNix/Expr/Typed.hs b/src/Hercules/CNix/Expr/Typed.hs
--- a/src/Hercules/CNix/Expr/Typed.hs
+++ b/src/Hercules/CNix/Expr/Typed.hs
@@ -1,6 +1,7 @@
 {-# LANGUAGE EmptyDataDecls #-}
 {-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE CPP #-}
 
 module Hercules.CNix.Expr.Typed
   ( Value (..),
@@ -151,12 +152,20 @@
 
 getBool :: Value Bool -> IO Bool
 getBool (Value (RawValue v)) =
-  (0 /=)
-    <$> [C.exp| int { $(Value *v)->boolean ? 1 : 0 }|]
+  (0 /=) <$>
+#if NIX_IS_AT_LEAST(2,24,0)
+    [C.exp| int { $(Value *v)->boolean() ? 1 : 0 }|]
+#else
+    [C.exp| int { $(Value *v)->boolean ? 1 : 0 }|]
+#endif
 
 getInt :: Value NixInt -> IO Int64
 getInt (Value (RawValue v)) =
+#if NIX_IS_AT_LEAST(2,24,0)
+  [C.exp| int64_t { $(Value *v)->integer() }|]
+#else
   [C.exp| int64_t { $(Value *v)->integer }|]
+#endif
 
 -- NOT coerceToString
 getStringIgnoreContext :: Value NixString -> IO ByteString
@@ -172,8 +181,12 @@
 
 hasContext :: Value NixString -> IO Bool
 hasContext (Value (RawValue v)) =
-  (0 /=)
-    <$> [C.exp| int { $(Value *v)->string.context ? 1 : 0 }|]
+  (0 /=) <$>
+#if NIX_IS_AT_LEAST(2,24,0)
+    [C.exp| int { $(Value *v)->context() ? 1 : 0 }|]
+#else
+    [C.exp| int { $(Value *v)->string.context ? 1 : 0 }|]
+#endif
 
 class CheckType a where
   checkType :: Ptr EvalState -> RawValue -> IO (Maybe (Value a))
