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.3.6.1 - 2024-02-12
+
+### Added
+
+- Nix support up to 2.20
+
 ## 0.3.6.0 - 2023-06-28
 
 ### Added
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.0
+version:        0.3.6.1
 synopsis:       Bindings for the Nix evaluator
 category:       Nix, CI, Testing, DevOps
 homepage:       https://docs.hercules-ci.com
diff --git a/include/hercules-ci-cnix/expr.hxx b/include/hercules-ci-cnix/expr.hxx
--- a/include/hercules-ci-cnix/expr.hxx
+++ b/include/hercules-ci-cnix/expr.hxx
@@ -5,5 +5,4 @@
 
 typedef nix::Strings::iterator StringsIterator;
 typedef nix::DerivationOutputs::iterator DerivationOutputsIterator;
-typedef nix::DerivationInputs::iterator DerivationInputsIterator;
 typedef nix::StringPairs::iterator StringPairsIterator;
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
@@ -127,6 +127,10 @@
 
 C.include "<nix/flake/flakeref.hh>"
 
+#if NIX_IS_AT_LEAST(2,19,0)
+C.include "<nix/args/root.hh>"
+#endif
+
 C.include "hercules-ci-cnix/expr.hxx"
 
 C.include "<gc/gc.h>"
@@ -269,34 +273,37 @@
     printInfo($bs-cstr:bstr);
   }|]
 
+-- | (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)
+  [C.throwBlock| EvalState* {
+    nix::SearchPath searchPaths;
+    return new EvalState(searchPaths, *$(refStore* store));
+  } |]
+#else
+  [C.throwBlock| EvalState* {
+    Strings searchPaths;
+    return new EvalState(searchPaths, *$(refStore* store));
+  } |]
+#endif
+
+-- | (private) Don't leak it.
+deleteEvalState :: MonadIO m => (Ptr EvalState) -> m ()
+deleteEvalState st = liftIO [C.throwBlock| void { delete $(EvalState* st); } |]
+
 withEvalState ::
   Store ->
   (Ptr EvalState -> IO a) ->
   IO a
-withEvalState (Store store) =
-  bracket
-    ( liftIO
-        [C.throwBlock| EvalState* {
-          Strings searchPaths;
-          return new EvalState(searchPaths, *$(refStore* store));
-        } |]
-    )
-    (\x -> liftIO [C.throwBlock| void { delete $(EvalState* x); } |])
+withEvalState store = bracket (newEvalState store) deleteEvalState
 
 withEvalStateConduit ::
   MonadResource m =>
   Store ->
   (Ptr EvalState -> ConduitT i o m r) ->
   ConduitT i o m r
-withEvalStateConduit (Store store) =
-  bracketP
-    ( liftIO
-        [C.throwBlock| EvalState* {
-          Strings searchPaths;
-          return new EvalState(searchPaths, *$(refStore* store));
-        } |]
-    )
-    (\x -> liftIO [C.throwBlock| void { delete $(EvalState* x); } |])
+withEvalStateConduit store = bracketP (newEvalState store) deleteEvalState
 
 -- | Insert an allowed path. Only has an effect when in restricted or pure mode.
 addAllowedPath :: Ptr EvalState -> ByteString -> IO ()
@@ -304,9 +311,13 @@
   [C.throwBlock| void {
     std::string path = std::string($bs-ptr:path, $bs-len:path);
     EvalState &evalState = *$(EvalState *evalState);
+#if NIX_IS_AT_LEAST(2,20,0)
+    evalState.allowPath(path);
+#else
     if (evalState.allowedPaths) {
       evalState.allowedPaths->insert(path);
     }
+#endif
   }|]
 
 addInternalAllowedPaths :: Ptr EvalState -> IO ()
@@ -318,14 +329,17 @@
   filename' <- Foreign.C.String.newCString filename
   mkRawValue
     =<< [C.throwBlock| Value* {
+      EvalState & state = *$(EvalState *evalState);
       Value value;
       auto cstr = $(const char *filename');
-#if NIX_IS_AT_LEAST(2,16,0)
+#if NIX_IS_AT_LEAST(2,19,0)
+      SourcePath path {state.rootPath(CanonPath(cstr))};
+#elif NIX_IS_AT_LEAST(2,16,0)
       SourcePath path = CanonPath(cstr);
 #else
       std::string path = cstr;
 #endif
-      $(EvalState *evalState)->evalFile(path, value);
+      state.evalFile(path, value);
       return new (NoGC) Value(value);
     }|]
 
@@ -339,6 +353,12 @@
     $(Strings *ss)->push_back(std::string($bs-ptr:s, $bs-len:s));
   }|]
 
+#if NIX_IS_AT_LEAST(2,19,0)
+C.verbatim "struct EvalArgs : nix::RootArgs, nix::MixEvalArgs { };"
+#else
+C.verbatim "struct EvalArgs : nix::MixEvalArgs { };"
+#endif
+
 evalArgs :: Ptr EvalState -> [ByteString] -> IO (Value NixAttrs)
 evalArgs evalState args = do
   argsStrings <- newStrings
@@ -346,7 +366,7 @@
   fmap unsafeAssertType . mkRawValue
     =<< [C.throwBlock| Value * {
       Strings *args = $(Strings *argsStrings);
-      struct MixEvalArgs evalArgs;
+      struct EvalArgs evalArgs;
       Bindings *autoArgs;
       EvalState &state = *$(EvalState *evalState);
 
@@ -523,7 +543,9 @@
     =<< [C.throwBlock| Value *{
       EvalState &evalState = *$(EvalState *evalState);
       std::string basePathStr = std::string($bs-ptr:basePath, $bs-len:basePath);
-#if NIX_IS_AT_LEAST(2,16,0)
+#if NIX_IS_AT_LEAST(2,19,0)
+      SourcePath basePath {evalState.rootPath(CanonPath(basePathStr))};
+#elif NIX_IS_AT_LEAST(2,16,0)
       SourcePath basePath = CanonPath(basePathStr);
 #else
       auto & basePath = basePathStr;
@@ -554,14 +576,19 @@
       return r;
     }|]
 
-mkPath :: ByteString -> IO (Value NixPath)
-mkPath path =
+mkPath :: Ptr EvalState -> ByteString -> IO (Value NixPath)
+mkPath evalState path =
   Value
     <$> ( mkRawValue
             =<< [C.throwBlock| Value *{
+      EvalState & state = *$(EvalState *evalState);
       Value *r = new (NoGC) Value();
       std::string s($bs-ptr:path, $bs-len:path);
+#if NIX_IS_AT_LEAST(2,19,0)
+      r->mkPath(state.rootPath(CanonPath(s)));
+#else
       r->mkPath(s.c_str());
+#endif
       return r;
   }|]
         )
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
@@ -43,8 +43,12 @@
 evalContext =
   mempty
     { C.ctxTypesTable =
-        C.TypeName "EvalState" =: [t|EvalState|]
-          <> C.TypeName "Value" =: [t|Value'|]
-          <> C.TypeName "Attr" =: [t|Attr'|]
-          <> C.TypeName "BindingsBuilder" =: [t|BindingsBuilder'|]
+        C.TypeName "EvalState"
+          =: [t|EvalState|]
+          <> C.TypeName "Value"
+          =: [t|Value'|]
+          <> C.TypeName "Attr"
+          =: [t|Attr'|]
+          <> C.TypeName "BindingsBuilder"
+          =: [t|BindingsBuilder'|]
     }
diff --git a/src/Hercules/CNix/Expr/Raw.hs b/src/Hercules/CNix/Expr/Raw.hs
--- a/src/Hercules/CNix/Expr/Raw.hs
+++ b/src/Hercules/CNix/Expr/Raw.hs
@@ -101,7 +101,7 @@
     f 11 = Thunk
     f _ = Other
 
-forceValue :: Exception a => Ptr EvalState -> RawValue -> IO (Either a ())
+forceValue :: (Exception a) => Ptr EvalState -> RawValue -> IO (Either a ())
 forceValue evalState (RawValue v) =
   try
     [C.catchBlock|  {
diff --git a/src/Hercules/CNix/Expr/Schema.hs b/src/Hercules/CNix/Expr/Schema.hs
--- a/src/Hercules/CNix/Expr/Schema.hs
+++ b/src/Hercules/CNix/Expr/Schema.hs
@@ -108,10 +108,10 @@
   = MissingAttribute Provenance Text
   | TypeError
       Provenance
+      -- | actual
       RawValueType
-      -- ^ actual
+      -- | expected
       [RawValueType]
-      -- ^ expected
   | InvalidText Provenance UnicodeException
   | StringContextNotAllowed Provenance
   | InvalidValue Provenance Text
@@ -312,14 +312,14 @@
     Nothing -> throwIO $ MissingAttribute (provenance as) (T.pack (symbolVal p))
     Just x -> pure x
 
-lookupDictBS :: MonadEval m => ByteString -> PSObject (Attrs' as w) -> m (Maybe (PSObject w))
+lookupDictBS :: (MonadEval m) => ByteString -> PSObject (Attrs' as w) -> m (Maybe (PSObject w))
 lookupDictBS name as = do
   evalState <- ask
   v <- check as
   liftIO (getAttr evalState v name)
     <&> fmap (\b -> PSObject {value = b, provenance = Attribute (provenance as) (decodeUtf8With lenientDecode name)})
 
-lookupDict :: MonadEval m => Text -> PSObject (Attrs' as w) -> m (Maybe (PSObject w))
+lookupDict :: (MonadEval m) => Text -> PSObject (Attrs' as w) -> m (Maybe (PSObject w))
 lookupDict name as = do
   evalState <- ask
   v <- check as
@@ -327,20 +327,20 @@
     <&> fmap (\b -> PSObject {value = b, provenance = Attribute (provenance as) name})
 
 -- | Like '#?!'. Throws an acceptable but not great error message.
-requireDictBS :: MonadEval m => ByteString -> PSObject (Attrs' as w) -> m (PSObject w)
+requireDictBS :: (MonadEval m) => ByteString -> PSObject (Attrs' as w) -> m (PSObject w)
 requireDictBS name as = do
   lookupDictBS name as >>= \case
     Nothing -> throwIO $ MissingAttribute (provenance as) (decodeUtf8With lenientDecode name)
     Just r -> pure r
 
 -- | Like '#?!'. Throws an acceptable but not great error message.
-requireDict :: MonadEval m => Text -> PSObject (Attrs' as w) -> m (PSObject w)
+requireDict :: (MonadEval m) => Text -> PSObject (Attrs' as w) -> m (PSObject w)
 requireDict name as = do
   lookupDict name as >>= \case
     Nothing -> throwIO $ MissingAttribute (provenance as) name
     Just r -> pure r
 
-dictionaryToMap :: MonadEval m => PSObject (Dictionary w) -> m (Map ByteString (PSObject w))
+dictionaryToMap :: (MonadEval m) => PSObject (Dictionary w) -> m (Map ByteString (PSObject w))
 dictionaryToMap dict = do
   es <- ask
   (liftIO . Expr.getAttrs es =<< check dict)
@@ -362,7 +362,7 @@
 
 class PossibleTypesForSchema s where
   typesForSchema :: Proxy s -> [RawValueType]
-  default typesForSchema :: HasRawValueType (NixTypeForSchema s) => Proxy s -> [RawValueType]
+  default typesForSchema :: (HasRawValueType (NixTypeForSchema s)) => Proxy s -> [RawValueType]
   typesForSchema _ = [getRawValueType (Proxy @(NixTypeForSchema s))]
 
 instance PossibleTypesForSchema (Attrs' as w)
@@ -483,12 +483,12 @@
   m Text
 getText_ = validateE getByteString_ decodeUtf8' InvalidText
 
-validate :: Monad m => (PSObject s -> m a) -> (Provenance -> a -> m b) -> PSObject s -> m b
+validate :: (Monad m) => (PSObject s -> m a) -> (Provenance -> a -> m b) -> PSObject s -> m b
 validate basicParse validator o = do
   a <- basicParse o
   validator (provenance o) a
 
-validateE :: MonadIO m => (PSObject s -> m a) -> (a -> Either e b) -> (Provenance -> e -> NixException) -> PSObject s -> m b
+validateE :: (MonadIO m) => (PSObject s -> m a) -> (a -> Either e b) -> (Provenance -> e -> NixException) -> PSObject s -> m b
 validateE basicParse validator thrower =
   validate basicParse \prov a ->
     case validator a of
@@ -530,7 +530,7 @@
 class FromPSObject schema a where
   -- | Parse an object assumed to be in schema @schema@ into a value of type @a@
   -- or throw a 'NixException'.
-  fromPSObject :: MonadEval m => PSObject schema -> m a
+  fromPSObject :: (MonadEval m) => PSObject schema -> m a
 
 instance FromPSObject StringWithoutContext ByteString where
   fromPSObject o = do
@@ -560,7 +560,7 @@
     v <- check o
     liftIO (Expr.fromValue v)
 
-instance forall a b. FromPSObject a b => FromPSObject [a] [b] where
+instance forall a b. (FromPSObject a b) => FromPSObject [a] [b] where
   fromPSObject o = do
     traverseArray fromPSObject o
 
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
@@ -162,7 +162,11 @@
 getStringIgnoreContext (Value (RawValue v)) =
   unsafeMallocBS
     [C.exp| const char *{
+#if NIX_IS_AT_LEAST(2,19,0)
+    strdup($(Value *v)->c_str())
+#else
     strdup($(Value *v)->string.s)
+#endif
   }|]
 
 hasContext :: Value NixString -> IO Bool
diff --git a/test/Hercules/CNix/Expr/SchemaSpec.hs b/test/Hercules/CNix/Expr/SchemaSpec.hs
--- a/test/Hercules/CNix/Expr/SchemaSpec.hs
+++ b/test/Hercules/CNix/Expr/SchemaSpec.hs
@@ -15,7 +15,7 @@
 runES :: ReaderT (Ptr EvalState) m a -> m a
 runES m = runReaderT m evalState
 
-displaying :: Exception e => [Char] -> e -> Bool
+displaying :: (Exception e) => [Char] -> e -> Bool
 displaying text e = displayException e == text
 
 spec :: Spec
