diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,77 @@
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
+## [0.8.0]
+
+### Added
+
+ - Hercules CI Effects, a new feature for running programs that interact with
+   the real world, with useful features for continous deployment.
+
+    - Effects only run after the build completes successfully
+
+    - Effects are defined like a derivation, not unlike a Nix shell
+
+    - Independent processes can run concurrently as distinct effects
+
+    - No two commits in the same repo run effects at the same time; no need to
+      worry about concurrency in deployment scripts
+
+    - Effects each run in their own sandbox with access to network, Nix store,
+      remote state file API and secrets
+
+    - Secrets are configured locally on your agents, so you don't have to trust
+      a third party with your cloud credentials
+
+ - Hercules CI Agent is now a flake. The highlights are
+    - `nixosModules` overriding the NixOS-distributed module to the in-repo version
+      - `agent-profile` for agent machines, or
+      - `agent-service` for just the service definition
+    - `packages`
+      - `hercules-ci-cli` the user command line interface
+      - `hercules-ci-agent` for custom installation methods, etc
+
+ - The `hci` command (flake: `defaultApp`)
+    - `hci login` to authenticate yourself
+    - `hci state` to work with Effects state files
+    - `hci effect` to run effects locally
+
+ - Commit metadata as a `ci.nix` argument. Make your `ci.nix` a function:
+
+   ```nix
+   { src ? { ref = null; rev = null; }}:
+   # rest of your ci.nix
+   ```
+
+   `src.ref` will have e.g. `refs/heads/master` and `rev` will have the
+   git commit SHA.
+
+ - Shell derivations will only be built for their dependencies. Add a
+   `mkShell`-based expression like you would add a derivation.
+
+   This behavior can be requested explicitly for shells and non-shell
+   derivations alike by appending ` // { buildDependenciesOnly = true; }` to
+   the attribute definition.
+
+ - Attributes can now be marked to require or ignore a build failure in the
+   derivation it references directly.
+   ([support#34](https://github.com/hercules-ci/support/issues/34))
+
+ - `concurrentTasks` now has a default, `"auto"` for ease of setup and to help
+   avoid underutilization.
+
+### Fixed
+
+ - The parent directory name will match the repo name [support#40](https://github.com/hercules-ci/support/issues/40)
+
+ - Previously, lines from Nix's configured netrc file were ignored. Now they are appended to Hercules CI's netrc lines.
+
+### Changed
+
+ - Cachix caches without `signingKeys` will be pushed to, as part of the recently
+   introduced write token feature (Cachix-managed signing keys)
+
+
 ## [0.7.5] - 2020-11-27
 
 ### Fixed
@@ -47,7 +118,7 @@
    normal NixOS expectations. Notably, it does not configure automatic garbage
    collection and it does not preconfigure NixOps keys deployment.
 
-   The configuration interface will remain unchanged for `0.7` but `0.8` will
+   The configuration interface in the hercules-ci-agent repo will remain unchanged for `0.7` but `0.8` will
    match the upstream interface.
 
 ## [0.7.3] - 2020-07-18
@@ -330,6 +401,8 @@
 
 - Initial release
 
+[0.8.0]: https://github.com/hercules-ci/hercules-ci-agent/compare/hercules-ci-agent-0.7.5...hercules-ci-agent-0.8.0
+[0.7.5]: https://github.com/hercules-ci/hercules-ci-agent/compare/hercules-ci-agent-0.7.4...hercules-ci-agent-0.7.5
 [0.7.4]: https://github.com/hercules-ci/hercules-ci-agent/compare/hercules-ci-agent-0.7.3...hercules-ci-agent-0.7.4
 [0.7.3]: https://github.com/hercules-ci/hercules-ci-agent/compare/hercules-ci-agent-0.7.2...hercules-ci-agent-0.7.3
 [0.7.2]: https://github.com/hercules-ci/hercules-ci-agent/compare/hercules-ci-agent-0.7.1...hercules-ci-agent-0.7.2
diff --git a/cbits/hercules-aliases.h b/cbits/hercules-aliases.h
--- a/cbits/hercules-aliases.h
+++ b/cbits/hercules-aliases.h
@@ -3,6 +3,8 @@
 #include "hercules-store.hh"
 #include "hercules-logger.hh"
 #include "derivations.hh"
+#include <hercules-ci-cnix/store.hxx>
+#include <hercules-ci-cnix/expr.hxx>
 
 // inline-c-cpp doesn't seem to handle namespace operator or template
 // syntax so we help it a bit for now. This definition can be inlined
diff --git a/hercules-ci-agent-worker/Hercules/Agent/Worker.hs b/hercules-ci-agent-worker/Hercules/Agent/Worker.hs
--- a/hercules-ci-agent-worker/Hercules/Agent/Worker.hs
+++ b/hercules-ci-agent-worker/Hercules/Agent/Worker.hs
@@ -1,5 +1,6 @@
 {-# LANGUAGE BlockArguments #-}
 {-# LANGUAGE DataKinds #-}
+{-# LANGUAGE LambdaCase #-}
 {-# LANGUAGE NumericUnderscores #-}
 
 module Hercules.Agent.Worker
@@ -7,11 +8,10 @@
   )
 where
 
-import CNix
-import qualified CNix.Internal.Raw
 import Conduit
 import Control.Concurrent.STM
 import qualified Control.Exception.Lifted as EL
+import Control.Monad.Except
 import Control.Monad.IO.Unlift
 import Control.Monad.Trans.Control
 import qualified Data.ByteString as BS
@@ -25,7 +25,6 @@
 import Data.IORef
 import qualified Data.Map as M
 import qualified Data.Set as S
-import Data.Typeable (typeOf)
 import Data.UUID (UUID)
 import Data.Vector (Vector)
 import qualified Data.Vector as V
@@ -34,26 +33,36 @@
 import qualified Hercules.API.Logs.LogEntry as LogEntry
 import Hercules.API.Logs.LogMessage (LogMessage)
 import qualified Hercules.API.Logs.LogMessage as LogMessage
+import Hercules.Agent.Sensitive
 import qualified Hercules.Agent.Socket as Socket
-import Hercules.Agent.Worker.Build
+import Hercules.Agent.Worker.Build (runBuild)
 import qualified Hercules.Agent.Worker.Build.Logger as Logger
-import qualified Hercules.Agent.WorkerProtocol.Command as Command
+import Hercules.Agent.Worker.Effect (runEffect)
+import Hercules.Agent.Worker.HerculesStore (nixStore, setBuilderCallback, withHerculesStore)
+import Hercules.Agent.Worker.HerculesStore.Context (HerculesStore)
 import Hercules.Agent.WorkerProtocol.Command
   ( Command,
   )
+import qualified Hercules.Agent.WorkerProtocol.Command as Command
 import qualified Hercules.Agent.WorkerProtocol.Command.Build as Build
 import qualified Hercules.Agent.WorkerProtocol.Command.BuildResult as BuildResult
-import qualified Hercules.Agent.WorkerProtocol.Command.Eval as Eval
+import qualified Hercules.Agent.WorkerProtocol.Command.Effect as Effect
 import Hercules.Agent.WorkerProtocol.Command.Eval
   ( Eval,
   )
-import qualified Hercules.Agent.WorkerProtocol.Event as Event
+import qualified Hercules.Agent.WorkerProtocol.Command.Eval as Eval
 import Hercules.Agent.WorkerProtocol.Event
   ( Event (Exception),
   )
+import qualified Hercules.Agent.WorkerProtocol.Event as Event
 import qualified Hercules.Agent.WorkerProtocol.Event.Attribute as Attribute
 import qualified Hercules.Agent.WorkerProtocol.Event.AttributeError as AttributeError
 import qualified Hercules.Agent.WorkerProtocol.LogSettings as LogSettings
+import Hercules.CNix as CNix
+import Hercules.CNix.Expr (Match (IsAttrs, IsString), NixAttrs, RawValue, autoCallFunction, evalArgs, evalFile, getAttrBool, getAttrList, getAttrs, getDrvFile, getRecurseForDerivations, getStringIgnoreContext, init, isDerivation, isFunctor, match, rawValueType, withEvalStateConduit)
+import Hercules.CNix.Expr.Context (EvalState)
+import qualified Hercules.CNix.Expr.Raw
+import Hercules.CNix.Expr.Typed (Value)
 import Hercules.Error
 import Katip
 import qualified Language.C.Inline.Cpp.Exceptions as C
@@ -62,26 +71,25 @@
 import qualified System.Environment as Environment
 import System.IO (BufferMode (LineBuffering), hSetBuffering)
 import System.Posix.IO (dup, fdToHandle, stdError)
+import System.Posix.Signals (Handler (Catch), installHandler, raiseSignal, sigINT, sigTERM)
 import System.Timeout (timeout)
 import UnliftIO.Async (wait, withAsync)
 import UnliftIO.Exception (bracket, catch)
 import Prelude ()
 import qualified Prelude
 
-data HerculesState
-  = HerculesState
-      { drvsCompleted :: TVar (Map Text (UUID, BuildResult.BuildStatus)),
-        drvsInProgress :: IORef (Set Text),
-        herculesStore :: Ptr (Ref HerculesStore),
-        wrappedStore :: Ptr (Ref NixStore),
-        shortcutChannel :: Chan (Maybe Event)
-      }
+data HerculesState = HerculesState
+  { drvsCompleted :: TVar (Map Text (UUID, BuildResult.BuildStatus)),
+    drvsInProgress :: IORef (Set Text),
+    herculesStore :: Ptr (Ref HerculesStore),
+    wrappedStore :: Store,
+    shortcutChannel :: Chan (Maybe Event)
+  }
 
-data BuildException
-  = BuildException
-      { buildExceptionDerivationPath :: Text,
-        buildExceptionDetail :: Maybe Text
-      }
+data BuildException = BuildException
+  { buildExceptionDerivationPath :: Text,
+    buildExceptionDetail :: Maybe Text
+  }
   deriving (Show, Typeable)
 
 instance Exception BuildException
@@ -89,7 +97,8 @@
 main :: IO ()
 main = do
   hSetBuffering stderr LineBuffering
-  CNix.init
+  Hercules.CNix.Expr.init
+  _ <- installHandler sigTERM (Catch $ raiseSignal sigINT) Nothing
   Logger.initLogger
   [options] <- Environment.getArgs
   let allOptions =
@@ -109,25 +118,27 @@
   withStore $ \wrappedStore_ -> withHerculesStore wrappedStore_ $ \herculesStore_ -> withKatip $ do
     liftIO $ setBuilderCallback herculesStore_ mempty
     ch <- liftIO newChan
-    let st = HerculesState
-          { drvsCompleted = drvsCompleted_,
-            drvsInProgress = drvsInProgress_,
-            herculesStore = herculesStore_,
-            wrappedStore = wrappedStore_,
-            shortcutChannel = ch
-          }
+    let st =
+          HerculesState
+            { drvsCompleted = drvsCompleted_,
+              drvsInProgress = drvsInProgress_,
+              herculesStore = herculesStore_,
+              wrappedStore = wrappedStore_,
+              shortcutChannel = ch
+            }
     let runner :: KatipContextT IO ()
         runner =
           ( ( do
-                command <- runConduitRes -- Res shouldn't be necessary
-                  ( transPipe liftIO (sourceHandle stdin)
-                      .| conduitDecode
-                      .| printCommands
-                      .| await
-                  )
-                  >>= \case
-                    Just c -> pure c
-                    Nothing -> panic "Not a valid starting command"
+                command <-
+                  runConduitRes -- Res shouldn't be necessary
+                    ( transPipe liftIO (sourceHandle stdin)
+                        .| conduitDecode
+                        .| printCommands
+                        .| await
+                    )
+                    >>= \case
+                      Just c -> pure c
+                      Nothing -> panic "Not a valid starting command"
                 runCommand st ch command
             )
               `safeLiftedCatch` ( \e -> liftIO $ do
@@ -146,7 +157,7 @@
                 .| concatMapC (\x -> [Chunk x, Flush])
                 .| transPipe liftIO (sinkHandleFlush stdout)
             )
-    void $ do
+    void $
       withAsync runner $ \runnerAsync -> do
         writer -- runner can stop writer only by passing Nothing in channel (finally)
         logLocM DebugS "Writer done"
@@ -156,7 +167,7 @@
 printCommands =
   mapMC
     ( \x -> do
-        katipAddContext (sl "command" (show x :: Text)) $ do
+        katipAddContext (sl "command" (show x :: Text)) $
           logLocM DebugS "Received command"
         pure x
     )
@@ -187,40 +198,51 @@
 -- runCommand' :: HerculesState -> Command -> ConduitM Command Event (ResourceT IO) ()
 runCommand herculesState ch command = do
   -- TODO don't do this
-  mainThread <- liftIO $ myThreadId
+  mainThread <- liftIO myThreadId
   UnliftIO unlift <- askUnliftIO
   case command of
-    Command.Eval eval -> Logger.withLoggerConduit (logger $ Eval.logSettings eval) $ Logger.withTappedStderr Logger.tapper $ connectCommand ch $ do
-      void $ liftIO
-        $ flip
-          forkFinally
-          ( \eeu -> case eeu of
-              Left e -> throwIO $ FatalError $ "Failed to fork: " <> show e
-              Right _ -> pure ()
-          )
-        $ unlift
-        $ runConduitRes
-          ( Data.Conduit.handleC
-              ( \e -> do
-                  yield $ Event.Error (renderException e)
-                  liftIO $ throwTo mainThread e
-              )
-              ( do
-                  runEval herculesState eval
-                  liftIO $ throwTo mainThread ExitSuccess
-              )
-              .| sinkChanTerminate (shortcutChannel herculesState)
-          )
-      awaitForever $ \case
-        Command.BuildResult (BuildResult.BuildResult path attempt result) -> do
-          katipAddContext (sl "path" path <> sl "result" (show result :: Text))
-            $ logLocM DebugS
-            $ "Received remote build result"
-          liftIO $ atomically $ modifyTVar (drvsCompleted herculesState) (M.insert path (attempt, result))
-        _ -> pass
+    Command.Eval eval -> Logger.withLoggerConduit (logger $ Eval.logSettings eval) $
+      Logger.withTappedStderr Logger.tapper $
+        connectCommand ch $ do
+          void $
+            liftIO $
+              flip
+                forkFinally
+                (escalateAs \e -> FatalError $ "Failed to fork: " <> show e)
+                $ unlift $
+                  runConduitRes
+                    ( Data.Conduit.handleC
+                        ( \e -> do
+                            yield $ Event.Error (renderException e)
+                            liftIO $ throwTo mainThread e
+                        )
+                        ( do
+                            runEval herculesState eval
+                            liftIO $ throwTo mainThread ExitSuccess
+                        )
+                        .| sinkChanTerminate (shortcutChannel herculesState)
+                    )
+          awaitForever $ \case
+            Command.BuildResult (BuildResult.BuildResult path attempt result) -> do
+              katipAddContext (sl "path" path <> sl "result" (show result :: Text)) $
+                logLocM
+                  DebugS
+                  "Received remote build result"
+              liftIO $ atomically $ modifyTVar (drvsCompleted herculesState) (M.insert path (attempt, result))
+            _ -> pass
     Command.Build build ->
-      Logger.withLoggerConduit (logger $ Build.logSettings build) $ Logger.withTappedStderr Logger.tapper $ do
-        connectCommand ch $ runBuild (wrappedStore herculesState) build
+      katipAddNamespace "Build" $
+        Logger.withLoggerConduit (logger $ Build.logSettings build) $
+          Logger.withTappedStderr Logger.tapper $
+            connectCommand ch $ runBuild (wrappedStore herculesState) build
+    Command.Effect effect ->
+      katipAddNamespace "Effect" $
+        Logger.withLoggerConduit (logger $ Effect.logSettings effect) $
+          Logger.withTappedStderr Logger.tapper $
+            connectCommand ch $ do
+              runEffect (wrappedStore herculesState) effect >>= \case
+                ExitSuccess -> yield $ Event.EffectResult 0
+                ExitFailure n -> yield $ Event.EffectResult n
     _ ->
       panic "Not a valid starting command"
 
@@ -236,7 +258,7 @@
               atomically $ Socket.write socket ping
           )
           (const m)
-  Socket.withReliableSocket socketConfig $ \socket -> withPings socket $ katipAddNamespace "Build" do
+  Socket.withReliableSocket socketConfig $ \socket -> withPings socket do
     let conduit =
           entriesSource
             .| Logger.unbatch
@@ -246,21 +268,21 @@
             .| socketSink socket
         batch = Logger.batch .| mapC (LogMessage.LogEntries . V.fromList)
         batchAndEnd =
-          ( (foldMapTap (Last . ims) `fuseUpstream` batch) >>= \case
-              Last (Just (i, ms)) -> yield $ LogMessage.End {i = i + 1, ms = ms}
-              Last Nothing -> yield $ LogMessage.End 0 0
-          )
+          (foldMapTap (Last . ims) `fuseUpstream` batch) >>= \case
+            Last (Just (i, ms)) -> yield $ LogMessage.End {i = i + 1, ms = ms}
+            Last Nothing -> yield $ LogMessage.End 0 0
           where
             ims (Chunk logEntry) = Just (LogEntry.i logEntry, LogEntry.ms logEntry)
             ims _ = Nothing
-        renumber i = await >>= traverse_ \case
-          Flush -> yield Flush >> renumber i
-          Chunk e -> do
-            yield $ Chunk e {LogEntry.i = i}
-            renumber (i + 1)
-    runConduit $ conduit
+        renumber i =
+          await >>= traverse_ \case
+            Flush -> yield Flush >> renumber i
+            Chunk e -> do
+              yield $ Chunk e {LogEntry.i = i}
+              renumber (i + 1)
+    runConduit conduit
     logLocM DebugS "Syncing"
-    liftIO (timeout 600_000_000 $ Socket.syncIO $ socket) >>= \case
+    liftIO (timeout 600_000_000 $ Socket.syncIO socket) >>= \case
       Just _ -> pass
       Nothing -> panic "Could not push logs within 10 minutes after completion"
     logLocM DebugS "Logger done"
@@ -274,11 +296,12 @@
 foldMapTap :: (Monoid b, Monad m) => (a -> b) -> ConduitT a a m b
 foldMapTap f = go mempty
   where
-    go b = await >>= \case
-      Nothing -> pure b
-      Just a -> do
-        yield a
-        go (b <> f a)
+    go b =
+      await >>= \case
+        Nothing -> pure b
+        Just a -> do
+          yield a
+          go (b <> f a)
 
 withKatip :: (MonadUnliftIO m) => KatipContextT m a -> m a
 withKatip m = do
@@ -291,7 +314,7 @@
   let makeLogEnv = registerScribe "stderr" handleScribe defaultScribeSettings =<< initLogEnv "Worker" "production"
       initialContext = ()
       extraNs = mempty -- "Worker" is already set in initLogEnv.
-        -- closeScribes will stop accepting new logs, flush existing ones and clean up resources
+      -- closeScribes will stop accepting new logs, flush existing ones and clean up resources
   bracket (liftIO makeLogEnv) (liftIO . closeScribes) $ \logEnv ->
     runKatipContextT logEnv initialContext extraNs m
 
@@ -300,13 +323,14 @@
   baseURL <- case Network.URI.parseURI $ toS $ LogSettings.baseURL l of
     Just x -> pure x
     Nothing -> panic "LogSettings: invalid base url"
-  pure Socket.SocketConfig
-    { makeHello = pure (LogMessage.LogEntries mempty),
-      checkVersion = Socket.checkVersion',
-      baseURL = baseURL,
-      path = LogSettings.path l,
-      token = encodeUtf8 $ LogSettings.reveal $ LogSettings.token l
-    }
+  pure
+    Socket.SocketConfig
+      { makeHello = pure (LogMessage.LogEntries mempty),
+        checkVersion = Socket.checkVersion',
+        baseURL = baseURL,
+        path = LogSettings.path l,
+        token = encodeUtf8 $ reveal $ LogSettings.token l
+      }
 
 -- TODO: test
 autoArgArgs :: Map Text Eval.Arg -> [ByteString]
@@ -321,13 +345,16 @@
   bracket acquire release . const
   where
     acquire =
-      liftIO $ join $ atomicModifyIORef ref $ \inprg ->
-        if drvPath `S.member` inprg
-          then (inprg, throwIO $ FatalError "Refusing to build derivation that should have been built remotely. Presumably, substitution has failed.")
-          else (S.insert drvPath inprg, pass)
+      liftIO $
+        join $
+          atomicModifyIORef ref $ \inprg ->
+            if drvPath `S.member` inprg
+              then (inprg, throwIO $ FatalError "Refusing to build derivation that should have been built remotely. Presumably, substitution has failed.")
+              else (S.insert drvPath inprg, pass)
     release _ =
-      liftIO $ atomicModifyIORef ref $ \inprg ->
-        (S.delete drvPath inprg, ())
+      liftIO $
+        atomicModifyIORef ref $ \inprg ->
+          (S.delete drvPath inprg, ())
 
 anyAlternative :: (Foldable l, Alternative f) => l a -> f a
 anyAlternative = getAlt . foldMap (Alt . pure)
@@ -335,22 +362,26 @@
 yieldAttributeError :: Monad m => [ByteString] -> SomeException -> ConduitT i Event m ()
 yieldAttributeError path e
   | (Just e') <- fromException e =
-    yield $ Event.AttributeError $ AttributeError.AttributeError
-      { AttributeError.path = path,
-        AttributeError.message =
-          "Could not build derivation " <> buildExceptionDerivationPath e'
-            <> ", which is required during evaluation."
-            <> foldMap (" " <>) (buildExceptionDetail e'),
-        AttributeError.errorDerivation = Just (buildExceptionDerivationPath e'),
-        AttributeError.errorType = Just "BuildException"
-      }
+    yield $
+      Event.AttributeError $
+        AttributeError.AttributeError
+          { AttributeError.path = path,
+            AttributeError.message =
+              "Could not build derivation " <> buildExceptionDerivationPath e'
+                <> ", which is required during evaluation."
+                <> foldMap (" " <>) (buildExceptionDetail e'),
+            AttributeError.errorDerivation = Just (buildExceptionDerivationPath e'),
+            AttributeError.errorType = Just "BuildException"
+          }
 yieldAttributeError path e =
-  yield $ Event.AttributeError $ AttributeError.AttributeError
-    { AttributeError.path = path,
-      AttributeError.message = renderException e,
-      AttributeError.errorDerivation = Nothing,
-      AttributeError.errorType = Just (show (typeOf e))
-    }
+  yield $
+    Event.AttributeError $
+      AttributeError.AttributeError
+        { AttributeError.path = path,
+          AttributeError.message = renderException e,
+          AttributeError.errorDerivation = Nothing,
+          AttributeError.errorType = Just (show (typeOf e))
+        }
 
 maybeThrowBuildException :: MonadIO m => BuildResult.BuildStatus -> Text -> m ()
 maybeThrowBuildException result plainDrvText =
@@ -372,51 +403,56 @@
   s <- storeUri store
   UnliftIO unlift <- lift askUnliftIO
   let decode = decodeUtf8With lenientDecode
-  liftIO $ setBuilderCallback hStore $ \path -> unlift $ katipAddContext (sl "fullpath" (decode path)) $ do
-    logLocM DebugS "Building"
-    let (plainDrv, bangOut) = BS.span (/= fromIntegral (ord '!')) path
-        outputName = BS.dropWhile (== fromIntegral (ord '!')) bangOut
-        plainDrvText = decode plainDrv
-    withDrvInProgress st plainDrvText $ do
-      liftIO $ writeChan shortcutChan $ Just $ Event.Build plainDrvText (decode outputName) Nothing
-      derivation <- liftIO $ getDerivation store plainDrv
-      outputPath <- liftIO $ getDerivationOutputPath derivation outputName
-      katipAddContext (sl "outputPath" (decode outputPath)) $ do
-        logLocM DebugS "Naively calling ensurePath"
-      liftIO (ensurePath (wrappedStore st) outputPath) `catch` \e0 -> do
-        katipAddContext (sl "message" (show (e0 :: SomeException) :: Text)) $
-          logLocM DebugS "Recovering from failed wrapped.ensurePath"
-        (attempt0, result) <-
-          liftIO $ atomically $ do
-            c <- readTVar drvsCompl
-            anyAlternative $ M.lookup plainDrvText c
-        liftIO $ maybeThrowBuildException result plainDrvText
-        liftIO $ clearSubstituterCaches
-        liftIO $ clearPathInfoCache store
-        liftIO (ensurePath (wrappedStore st) outputPath) `catch` \e1 -> do
-          katipAddContext (sl "message" (show (e1 :: SomeException) :: Text)) $
-            logLocM DebugS "Recovering from fresh ensurePath"
-          liftIO $ writeChan shortcutChan $ Just $ Event.Build plainDrvText (decode outputName) (Just attempt0)
-          -- TODO sync
-          result' <-
-            liftIO $ atomically $ do
-              c <- readTVar drvsCompl
-              (attempt1, r) <- anyAlternative $ M.lookup plainDrvText c
-              guard (attempt1 /= attempt0)
-              pure r
-          liftIO $ maybeThrowBuildException result' plainDrvText
-          liftIO $ clearSubstituterCaches
-          liftIO $ clearPathInfoCache store
-          liftIO (ensurePath (wrappedStore st) outputPath) `catch` \e2 -> do
-            liftIO $ throwIO $
-              BuildException
-                plainDrvText
-                ( Just $
-                    "It could not be retrieved on the evaluating agent, despite a successful rebuild. Exception: "
-                      <> show (e2 :: SomeException)
-                )
-    logLocM DebugS ("Built")
-  withEvalState store $ \evalState -> do
+  liftIO $
+    setBuilderCallback hStore $ \path -> unlift $
+      katipAddContext (sl "fullpath" (decode path)) $ do
+        logLocM DebugS "Building"
+        let (plainDrv, bangOut) = BS.span (/= fromIntegral (ord '!')) path
+            outputName = BS.dropWhile (== fromIntegral (ord '!')) bangOut
+            plainDrvText = decode plainDrv
+        withDrvInProgress st plainDrvText $ do
+          liftIO $ writeChan shortcutChan $ Just $ Event.Build plainDrvText (decode outputName) Nothing
+          derivation <- liftIO $ getDerivation store plainDrv
+          outputPath <- liftIO $ getDerivationOutputPath derivation outputName
+          katipAddContext (sl "outputPath" (decode outputPath)) $
+            logLocM DebugS "Naively calling ensurePath"
+          liftIO (ensurePath (wrappedStore st) outputPath) `catch` \e0 -> do
+            katipAddContext (sl "message" (show (e0 :: SomeException) :: Text)) $
+              logLocM DebugS "Recovering from failed wrapped.ensurePath"
+            (attempt0, result) <-
+              liftIO $
+                atomically $ do
+                  c <- readTVar drvsCompl
+                  anyAlternative $ M.lookup plainDrvText c
+            liftIO $ maybeThrowBuildException result plainDrvText
+            liftIO clearSubstituterCaches
+            liftIO $ clearPathInfoCache store
+            liftIO (ensurePath (wrappedStore st) outputPath) `catch` \e1 -> do
+              katipAddContext (sl "message" (show (e1 :: SomeException) :: Text)) $
+                logLocM DebugS "Recovering from fresh ensurePath"
+              liftIO $ writeChan shortcutChan $ Just $ Event.Build plainDrvText (decode outputName) (Just attempt0)
+              -- TODO sync
+              result' <-
+                liftIO $
+                  atomically $ do
+                    c <- readTVar drvsCompl
+                    (attempt1, r) <- anyAlternative $ M.lookup plainDrvText c
+                    guard (attempt1 /= attempt0)
+                    pure r
+              liftIO $ maybeThrowBuildException result' plainDrvText
+              liftIO clearSubstituterCaches
+              liftIO $ clearPathInfoCache store
+              liftIO (ensurePath (wrappedStore st) outputPath) `catch` \e2 ->
+                liftIO $
+                  throwIO $
+                    BuildException
+                      plainDrvText
+                      ( Just $
+                          "It could not be retrieved on the evaluating agent, despite a successful rebuild. Exception: "
+                            <> show (e2 :: SomeException)
+                      )
+        logLocM DebugS "Built"
+  withEvalStateConduit store $ \evalState -> do
     katipAddContext (sl "storeURI" (decode s)) $
       logLocM DebugS "EvalState loaded."
     args <-
@@ -432,7 +468,7 @@
 walk ::
   (MonadUnliftIO m, KatipContext m) =>
   Ptr EvalState ->
-  Bindings ->
+  Value NixAttrs ->
   RawValue ->
   ConduitT i Event m ()
 walk evalState = walk' True [] 10
@@ -447,7 +483,7 @@
       -- | Depth of tree remaining
       Integer ->
       -- | Auto arguments to pass to (attrset-)functions
-      Bindings ->
+      Value NixAttrs ->
       -- | Current node of the walk
       RawValue ->
       -- | Program that performs the walk and emits 'Event's
@@ -465,16 +501,50 @@
                 if isDeriv
                   then do
                     drvPath <- getDrvFile evalState v
-                    yield $
-                      Event.Attribute Attribute.Attribute
-                        { Attribute.path = path,
-                          Attribute.drv = drvPath
-                        }
+                    typE <- runExceptT do
+                      isEffect <- liftEitherAs Left =<< liftIO (getAttrBool evalState attrValue "isEffect")
+                      case isEffect of
+                        Just True -> throwE $ Right Attribute.Effect
+                        _ -> pass
+                      isDependenciesOnly <- liftEitherAs Left =<< liftIO (getAttrBool evalState attrValue "buildDependenciesOnly")
+                      case isDependenciesOnly of
+                        Just True -> throwE $ Right Attribute.DependenciesOnly
+                        _ -> pass
+                      phases <- liftEitherAs Left =<< liftIO (getAttrList evalState attrValue "phases")
+                      case phases of
+                        Just [aSingularPhase] ->
+                          liftIO (match evalState aSingularPhase) >>= liftEitherAs Left >>= \case
+                            IsString phaseNameValue -> do
+                              phaseName <- liftIO (getStringIgnoreContext phaseNameValue)
+                              when (phaseName == "nobuildPhase") do
+                                throwE $ Right Attribute.DependenciesOnly
+                            _ -> pass
+                        _ -> pass
+                      mayFail <- liftEitherAs Left =<< liftIO (getAttrBool evalState attrValue "ignoreFailure")
+                      case mayFail of
+                        Just True -> throwE $ Right Attribute.MayFail
+                        _ -> pass
+                      mustFail <- liftEitherAs Left =<< liftIO (getAttrBool evalState attrValue "requireFailure")
+                      case mustFail of
+                        Just True -> throwE $ Right Attribute.MustFail
+                        _ -> pass
+                    let yieldAttribute typ =
+                          yield $
+                            Event.Attribute
+                              Attribute.Attribute
+                                { Attribute.path = path,
+                                  Attribute.drv = drvPath,
+                                  Attribute.typ = typ
+                                }
+                    case typE of
+                      Left (Left e) -> yieldAttributeError path e
+                      Left (Right t) -> yieldAttribute t
+                      Right _ -> yieldAttribute Attribute.Regular
                   else do
                     walkAttrset <-
                       if forceWalkAttrset
                         then pure True
-                        else-- Hydra doesn't seem to obey this, because it walks the
+                        else -- Hydra doesn't seem to obey this, because it walks the
                         -- x64_64-linux etc attributes per package. Maybe those
                         -- are special cases?
                         -- For now, we will assume that people don't build a whole Nixpkgs
@@ -486,28 +556,34 @@
                         walk' True path (depthRemaining - 1) autoArgs x
                       else do
                         attrs <- liftIO $ getAttrs attrValue
-                        void
-                          $ flip M.traverseWithKey attrs
-                          $ \name value ->
-                            when (depthRemaining > 0 && walkAttrset) $
-                              walk' -- TODO: else warn
-                                False
-                                (path ++ [name])
-                                (depthRemaining - 1)
-                                autoArgs
-                                value
+                        void $
+                          flip M.traverseWithKey attrs $
+                            \name value ->
+                              when (depthRemaining > 0 && walkAttrset) $
+                                walk' -- TODO: else warn
+                                  False
+                                  (path ++ [name])
+                                  (depthRemaining - 1)
+                                  autoArgs
+                                  value
               _any -> do
                 vt <- liftIO $ rawValueType v
                 unless
                   ( lastMay path
                       == Just "recurseForDerivations"
                       && vt
-                      == CNix.Internal.Raw.Bool
+                      == Hercules.CNix.Expr.Raw.Bool
                   )
-                  $ logLocM DebugS
-                  $ logStr
-                  $ "Ignoring "
-                    <> show path
-                    <> " : "
-                    <> (show vt :: Text)
+                  $ logLocM DebugS $
+                    logStr $
+                      "Ignoring "
+                        <> show path
+                        <> " : "
+                        <> (show vt :: Text)
                 pass
+
+liftEitherAs :: MonadError e m => (e0 -> e) -> Either e0 a -> m a
+liftEitherAs f = liftEither . rmap
+  where
+    rmap (Left e) = Left (f e)
+    rmap (Right a) = Right a
diff --git a/hercules-ci-agent-worker/Hercules/Agent/Worker/Build.hs b/hercules-ci-agent-worker/Hercules/Agent/Worker/Build.hs
--- a/hercules-ci-agent-worker/Hercules/Agent/Worker/Build.hs
+++ b/hercules-ci-agent-worker/Hercules/Agent/Worker/Build.hs
@@ -3,9 +3,6 @@
 
 module Hercules.Agent.Worker.Build where
 
-import CNix
-import CNix.Internal.Context (Derivation)
-import Cachix.Client.Store (Store, queryPathInfo, validPathInfoNarHash, validPathInfoNarSize)
 import Conduit
 import Data.Conduit.Katip.Orphans ()
 import Foreign (ForeignPtr)
@@ -15,11 +12,17 @@
 import Hercules.Agent.WorkerProtocol.Event (Event)
 import qualified Hercules.Agent.WorkerProtocol.Event as Event
 import qualified Hercules.Agent.WorkerProtocol.Event.BuildResult as Event.BuildResult
+import Hercules.CNix
+  ( DerivationOutput (derivationOutputName, derivationOutputPath),
+    getDerivationOutputs,
+  )
+import qualified Hercules.CNix as CNix
+import Hercules.CNix.Store (Store, queryPathInfo, validPathInfoNarHash, validPathInfoNarSize)
+import Hercules.CNix.Store.Context (Derivation)
 import Katip
 import Protolude hiding (yield)
-import Unsafe.Coerce
 
-runBuild :: (MonadIO m, KatipContext m) => Ptr (Ref NixStore) -> Command.Build.Build -> ConduitT i Event m ()
+runBuild :: (MonadIO m, KatipContext m) => Store -> Command.Build.Build -> ConduitT i Event m ()
 runBuild store build = do
   let extraPaths = Command.Build.inputDerivationOutputPaths build
       drvPath = encodeUtf8 $ Command.Build.drvPath build
@@ -31,7 +34,7 @@
       pure $ Command.Build.materializeDerivation build
     Left (e :: SomeException) -> liftIO do
       CNix.logInfo $ "while retrieving dependencies: " <> toS (displayException e)
-      CNix.logInfo $ "unable to retrieve dependency; will build locally"
+      CNix.logInfo "unable to retrieve dependency; attempting fallback to local build"
       pure True
   derivationMaybe <- liftIO $ Build.getDerivation store drvPath
   derivation <- case derivationMaybe of
@@ -44,23 +47,21 @@
   yield $ Event.BuildResult buildResult
 
 -- TODO: case distinction on BuildStatus enumeration
-enrichResult :: Ptr (Ref NixStore) -> ForeignPtr Derivation -> Build.BuildResult -> IO Event.BuildResult.BuildResult
-enrichResult _ _ result@Build.BuildResult {isSuccess = False} = pure $
-  Event.BuildResult.BuildFailure {errorMessage = Build.errorMessage result}
+enrichResult :: Store -> ForeignPtr Derivation -> Build.BuildResult -> IO Event.BuildResult.BuildResult
+enrichResult _ _ result@Build.BuildResult {isSuccess = False} =
+  pure $
+    Event.BuildResult.BuildFailure {errorMessage = Build.errorMessage result}
 enrichResult store derivation _ = do
   drvOuts <- getDerivationOutputs derivation
   outputInfos <- for drvOuts $ \drvOut -> do
-    vpi <- queryPathInfo (coerceStore store) (derivationOutputPath drvOut)
+    vpi <- queryPathInfo store (derivationOutputPath drvOut)
     hash_ <- validPathInfoNarHash vpi
     let size = validPathInfoNarSize vpi
-    pure Event.BuildResult.OutputInfo
-      { name = derivationOutputName drvOut,
-        path = derivationOutputPath drvOut,
-        hash = hash_,
-        size = size
-      }
+    pure
+      Event.BuildResult.OutputInfo
+        { name = derivationOutputName drvOut,
+          path = derivationOutputPath drvOut,
+          hash = hash_,
+          size = size
+        }
   pure $ Event.BuildResult.BuildSuccess outputInfos
-
--- TODO factor out cnix library and avoid unsafeCoerce https://github.com/hercules-ci/hercules-ci-agent/issues/223
-coerceStore :: Ptr (Ref NixStore) -> Store
-coerceStore = unsafeCoerce
diff --git a/hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Logger.hs b/hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Logger.hs
--- a/hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Logger.hs
+++ b/hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Logger.hs
@@ -5,7 +5,6 @@
 
 module Hercules.Agent.Worker.Build.Logger where
 
-import CNix.Internal.Context
 import Conduit (MonadUnliftIO, filterC)
 import qualified Data.ByteString.Char8 as BSC
 import Data.ByteString.Unsafe (unsafePackMallocCString)
@@ -15,6 +14,8 @@
 import Foreign (alloca, nullPtr, peek)
 import Hercules.API.Logs.LogEntry (LogEntry)
 import qualified Hercules.API.Logs.LogEntry as LogEntry
+import Hercules.Agent.Worker.Build.Logger.Context (Fields, HerculesLoggerEntry, context)
+import Hercules.CNix.Store.Context (unsafeMallocBS)
 import Katip
 import qualified Language.C.Inline.Cpp as C
 import qualified Language.C.Inline.Cpp.Exceptions as C
@@ -123,7 +124,7 @@
 forNonNull p f = if p == nullPtr then pure Nothing else Just <$> f p
 
 -- popping multiple lines into an array would be nice
-convertEntry :: Ptr HerculesLoggerEntry -> IO (LogEntry)
+convertEntry :: Ptr HerculesLoggerEntry -> IO LogEntry
 convertEntry logEntryPtr = alloca \millisPtr -> alloca \textStrPtr -> alloca \levelPtr -> alloca \activityIdPtr -> alloca \typePtr -> alloca \parentPtr -> alloca \fieldsPtrPtr ->
   do
     r <-
@@ -162,12 +163,13 @@
         textStr <- peek textStrPtr
         text_ <- unsafePackMallocCString textStr
         level_ <- peek levelPtr
-        pure $ LogEntry.Msg
-          { i = i_,
-            ms = ms_,
-            level = fromIntegral level_,
-            msg = decode text_
-          }
+        pure $
+          LogEntry.Msg
+            { i = i_,
+              ms = ms_,
+              level = fromIntegral level_,
+              msg = decode text_
+            }
       2 -> do
         text_ <- unsafePackMallocCString =<< peek textStrPtr
         act_ <- peek activityIdPtr
@@ -175,41 +177,43 @@
         parent_ <- peek parentPtr
         typ_ <- peek typePtr
         fields_ <- convertAndDeleteFields =<< peek fieldsPtrPtr
-        pure $ LogEntry.Start
-          { i = i_,
-            ms = ms_,
-            act = LogEntry.ActivityId act_,
-            level = fromIntegral level_,
-            typ = LogEntry.ActivityType typ_,
-            text = decode text_,
-            parent = LogEntry.ActivityId parent_,
-            fields = fields_
-          }
+        pure $
+          LogEntry.Start
+            { i = i_,
+              ms = ms_,
+              act = LogEntry.ActivityId act_,
+              level = fromIntegral level_,
+              typ = LogEntry.ActivityType typ_,
+              text = decode text_,
+              parent = LogEntry.ActivityId parent_,
+              fields = fields_
+            }
       3 -> do
         act_ <- peek activityIdPtr
-        pure $ LogEntry.Stop
-          { i = i_,
-            ms = ms_,
-            act = LogEntry.ActivityId act_
-          }
+        pure $
+          LogEntry.Stop
+            { i = i_,
+              ms = ms_,
+              act = LogEntry.ActivityId act_
+            }
       4 -> do
         act_ <- peek activityIdPtr
         typ_ <- peek typePtr
         fields_ <- convertAndDeleteFields =<< peek fieldsPtrPtr
-        pure $ LogEntry.Result
-          { i = i_,
-            ms = ms_,
-            act = LogEntry.ActivityId act_,
-            rtype = LogEntry.ResultType typ_,
-            fields = fields_
-          }
+        pure $
+          LogEntry.Result
+            { i = i_,
+              ms = ms_,
+              act = LogEntry.ActivityId act_,
+              rtype = LogEntry.ResultType typ_,
+              fields = fields_
+            }
       _ -> panic "convertEntry invalid internal type"
 
 convertAndDeleteFields :: Ptr Fields -> IO (Vector LogEntry.Field)
 convertAndDeleteFields fieldsPtr = flip
   finally
-  ( [C.block| void { delete $(LoggerFields *fieldsPtr); }|]
-  )
+  [C.block| void { delete $(LoggerFields *fieldsPtr); }|]
   do
     size <- [C.exp| size_t { $(LoggerFields *fieldsPtr)->size() }|]
     V.generateM (fromIntegral size) $ \i' ->
@@ -251,22 +255,23 @@
 withLoggerConduit logger io = withAsync (logger popper) $ \popperAsync ->
   ((io `finally` liftIO close) <* wait popperAsync) `onException` liftIO (timeout 2_000_000 (wait popperAsync))
   where
-    popper = liftIO popMany >>= \case
-      lns | null lns -> pass
-      lns -> do
-        yield lns
-        popper
+    popper =
+      liftIO popMany >>= \case
+        lns | null lns -> pass
+        lns -> do
+          yield lns
+          popper
 
 -- | Remove spammy progress results. Use 'nubProgress' instead?
 filterProgress :: Monad m => ConduitT (Flush LogEntry) (Flush LogEntry) m ()
 filterProgress = filterC \case
-  Chunk (LogEntry.Result {rtype = LogEntry.ResultTypeProgress}) -> False
+  Chunk LogEntry.Result {rtype = LogEntry.ResultTypeProgress} -> False
   _ -> True
 
 nubProgress :: Monad m => ConduitT (Flush LogEntry) (Flush LogEntry) m ()
 nubProgress = nubSubset (toChunk >=> toProgressKey)
   where
-    toProgressKey k@(LogEntry.Result {rtype = LogEntry.ResultTypeProgress}) = Just k {LogEntry.i = 0}
+    toProgressKey k@LogEntry.Result {rtype = LogEntry.ResultTypeProgress} = Just k {LogEntry.i = 0}
     toProgressKey _ = Nothing
     toChunk (Chunk a) = Just a
     toChunk Flush = Nothing
@@ -279,37 +284,41 @@
 batch :: Monad m => ConduitT (Flush a) [a] m ()
 batch = go []
   where
-    go acc = await >>= \case
-      Nothing -> do
-        unless (null acc) (yield $ reverse acc)
-      Just Flush -> do
-        unless (null acc) (yield $ reverse acc)
-        go []
-      Just (Chunk c) -> do
-        go (c : acc)
+    go acc =
+      await >>= \case
+        Nothing -> do
+          unless (null acc) (yield $ reverse acc)
+        Just Flush -> do
+          unless (null acc) (yield $ reverse acc)
+          go []
+        Just (Chunk c) -> do
+          go (c : acc)
 
 nubSubset :: (Eq k, Monad m) => (a -> Maybe k) -> ConduitT a a m ()
-nubSubset toKey = await >>= \case
-  Nothing -> pass
-  Just firstA -> yield firstA
-    >> case toKey firstA of
-      Nothing -> nubSubset toKey
-      Just firstK -> nubSubset1 toKey firstK
+nubSubset toKey =
+  await >>= \case
+    Nothing -> pass
+    Just firstA ->
+      yield firstA
+        >> case toKey firstA of
+          Nothing -> nubSubset toKey
+          Just firstK -> nubSubset1 toKey firstK
 
 nubSubset1 :: (Eq k, Monad m) => (a -> Maybe k) -> k -> ConduitT a a m ()
-nubSubset1 toKey prevKey = await >>= \case
-  Nothing -> pass
-  Just a -> case toKey a of
-    Nothing -> do
-      yield a
-      nubSubset1 toKey prevKey
-    Just ak -> do
-      unless (ak == prevKey) do
+nubSubset1 toKey prevKey =
+  await >>= \case
+    Nothing -> pass
+    Just a -> case toKey a of
+      Nothing -> do
         yield a
-      nubSubset1 toKey ak
+        nubSubset1 toKey prevKey
+      Just ak -> do
+        unless (ak == prevKey) do
+          yield a
+        nubSubset1 toKey ak
 
 tryReadLine :: MonadUnliftIO m => Handle -> m (Either () ByteString)
-tryReadLine s = tryJust (\e -> guard $ isEOFError e) (liftIO (BSC.hGetLine s))
+tryReadLine s = tryJust (guard . isEOFError) (liftIO (BSC.hGetLine s))
 
 tapper :: (KatipContext m, MonadUnliftIO m) => TapState -> m ()
 tapper s = do
@@ -317,9 +326,6 @@
     Left _ -> pass
     Right "__%%hercules terminate log%%__" -> pass
     Right ln -> do
-      katipAddContext (sl "line" (decode ln))
-        $ logLocM DebugS
-        $ "Intercepted stderr"
       liftIO
         [C.throwBlock| void {
           std::string s = $bs-cstr:ln;
@@ -374,7 +380,8 @@
         hSetBuffering h LineBuffering
         pure h
   readableHandle <- mkHandle readable
-  pure TapState
-    { originalStderrCopy = oldStdError,
-      readableStderrEnd = readableHandle
-    }
+  pure
+    TapState
+      { originalStderrCopy = oldStdError,
+        readableStderrEnd = readableHandle
+      }
diff --git a/hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Logger/Context.hs b/hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Logger/Context.hs
new file mode 100644
--- /dev/null
+++ b/hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Logger/Context.hs
@@ -0,0 +1,35 @@
+{-# LANGUAGE EmptyDataDecls #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Hercules.Agent.Worker.Build.Logger.Context where
+
+import qualified Data.Map as M
+import qualified Language.C.Inline.Context as C
+import qualified Language.C.Inline.Cpp as C
+import qualified Language.C.Types as C
+import Protolude
+
+data HerculesLoggerEntry
+
+data Fields
+
+data LogEntryQueue
+
+context :: C.Context
+context =
+  C.cppCtx <> C.fptrCtx
+    <> C.bsCtx
+    <> loggerContext
+
+(=:) :: k -> a -> Map k a
+(=:) = M.singleton
+
+loggerContext :: C.Context
+loggerContext =
+  mempty
+    { C.ctxTypesTable =
+        C.TypeName "HerculesLoggerEntry" =: [t|HerculesLoggerEntry|]
+          <> C.TypeName "LogEntryQueue" =: [t|LogEntryQueue|]
+          <> C.TypeName "LoggerFields" =: [t|Fields|]
+    }
diff --git a/hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Prefetched.hs b/hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Prefetched.hs
--- a/hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Prefetched.hs
+++ b/hercules-ci-agent-worker/Hercules/Agent/Worker/Build/Prefetched.hs
@@ -9,11 +9,10 @@
 
 module Hercules.Agent.Worker.Build.Prefetched where
 
-import CNix
-import CNix.Internal.Context
 import qualified Data.ByteString.Char8 as C8
 import Foreign (FinalizerPtr, ForeignPtr, alloca, newForeignPtr, nullPtr, peek)
 import Foreign.C (peekCString)
+import Hercules.CNix.Store
 import qualified Language.C.Inline.Cpp as C
 import qualified Language.C.Inline.Cpp.Exceptions as C
 import Protolude
@@ -38,7 +37,7 @@
 
 C.include "<nix/fs-accessor.hh>"
 
-C.include "hercules-aliases.h"
+C.include "<hercules-ci-cnix/store.hxx>"
 
 C.using "namespace nix"
 
@@ -78,25 +77,23 @@
 toBuildStatus (-1) = Successful
 toBuildStatus _ = UnknownFailure
 
-data BuildResult
-  = BuildResult
-      { isSuccess :: Bool,
-        status :: BuildStatus,
-        startTime :: C.CTime,
-        stopTime :: C.CTime,
-        errorMessage :: Text
-      }
+data BuildResult = BuildResult
+  { isSuccess :: Bool,
+    status :: BuildStatus,
+    startTime :: C.CTime,
+    stopTime :: C.CTime,
+    errorMessage :: Text
+  }
   deriving (Show)
 
 nullableForeignPtr :: FinalizerPtr a -> Ptr a -> IO (Maybe (ForeignPtr a))
 nullableForeignPtr _ rawPtr | rawPtr == nullPtr = pure Nothing
 nullableForeignPtr finalize rawPtr = Just <$> newForeignPtr finalize rawPtr
 
-getDerivation :: Ptr (Ref NixStore) -> ByteString -> IO (Maybe (ForeignPtr Derivation))
-getDerivation store derivationPath =
+getDerivation :: Store -> ByteString -> IO (Maybe (ForeignPtr Derivation))
+getDerivation (Store store) derivationPath =
   nullableForeignPtr finalizeDerivation
     =<< [C.throwBlock| Derivation *{
-      Store &store = **$(refStore* store);
       std::string derivationPath($bs-ptr:derivationPath, $bs-len:derivationPath);
       std::list<nix::ref<nix::Store>> stores = getDefaultSubstituters();
       stores.push_front(*$(refStore* store));
@@ -129,8 +126,8 @@
     }|]
 
 -- | @buildDerivation derivationPath derivationText@
-buildDerivation :: Ptr (Ref NixStore) -> ByteString -> ForeignPtr Derivation -> Maybe [ByteString] -> IO BuildResult
-buildDerivation store derivationPath derivation extraInputs =
+buildDerivation :: Store -> ByteString -> ForeignPtr Derivation -> Maybe [ByteString] -> IO BuildResult
+buildDerivation (Store store) derivationPath derivation extraInputs =
   let extraInputsMerged = C8.intercalate "\n" (fromMaybe [] extraInputs)
       materializeDerivation = if isNothing extraInputs then 1 else 0
    in alloca $ \successPtr ->
@@ -235,10 +232,11 @@
                 stopTimeValue <- peek stopTimePtr
                 errorMessageValue0 <- peek errorMessagePtr
                 errorMessageValue <- peekCString errorMessageValue0
-                pure $ BuildResult
-                  { isSuccess = successValue /= 0,
-                    status = toBuildStatus statusValue,
-                    startTime = startTimeValue,
-                    stopTime = stopTimeValue,
-                    errorMessage = toS errorMessageValue
-                  }
+                pure $
+                  BuildResult
+                    { isSuccess = successValue /= 0,
+                      status = toBuildStatus statusValue,
+                      startTime = startTimeValue,
+                      stopTime = stopTimeValue,
+                      errorMessage = toS errorMessageValue
+                    }
diff --git a/hercules-ci-agent-worker/Hercules/Agent/Worker/Effect.hs b/hercules-ci-agent-worker/Hercules/Agent/Worker/Effect.hs
new file mode 100644
--- /dev/null
+++ b/hercules-ci-agent-worker/Hercules/Agent/Worker/Effect.hs
@@ -0,0 +1,49 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Hercules.Agent.Worker.Effect where
+
+import Control.Monad.Catch (MonadThrow)
+import GHC.ForeignPtr (ForeignPtr)
+import Hercules.Agent.Worker.Build.Prefetched (buildDerivation)
+import qualified Hercules.Agent.Worker.Build.Prefetched as Build
+import qualified Hercules.Agent.WorkerProtocol.Command.Effect as Command.Effect
+import Hercules.CNix (Store)
+import qualified Hercules.CNix as CNix
+import Hercules.CNix.Store.Context (Derivation)
+import qualified Hercules.Effect as Effect
+import Katip (KatipContext)
+import Protolude
+import UnliftIO.Directory (getCurrentDirectory)
+
+runEffect :: (MonadIO m, KatipContext m, MonadThrow m) => Store -> Command.Effect.Effect -> m ExitCode
+runEffect store command = do
+  derivation <- prepareDerivation store command
+  dir <- getCurrentDirectory
+  Effect.runEffect derivation (Command.Effect.token command) (Command.Effect.secretsPath command) (Command.Effect.apiBaseURL command) dir
+
+prepareDerivation :: MonadIO m => Store -> Command.Effect.Effect -> m (ForeignPtr Derivation)
+prepareDerivation store command = do
+  let extraPaths = Command.Effect.inputDerivationOutputPaths command
+      drvPath = encodeUtf8 $ Command.Effect.drvPath command
+      ensureDeps = for_ extraPaths $ \input ->
+        liftIO $ CNix.ensurePath store input
+  liftIO $ do
+    ensureDeps `catch` \e -> do
+      CNix.logInfo $ "while retrieving dependencies: " <> toS (displayException (e :: SomeException))
+      CNix.logInfo "unable to retrieve dependency; attempting fallback to local build"
+      CNix.ensurePath store drvPath
+      derivation <- CNix.getDerivation store drvPath
+      depDrvPaths <- CNix.getDerivationInputs derivation
+      for_ depDrvPaths \(depDrv, _outputs) -> do
+        depDerivation <- CNix.getDerivation store depDrv
+        _nixBuildResult <- liftIO $ buildDerivation store depDrv depDerivation mempty
+        pass
+  derivation <-
+    liftIO (Build.getDerivation store drvPath) >>= \case
+      Just drv -> pure drv
+      Nothing -> panic $ "Could not retrieve derivation " <> show drvPath <> " from local store or binary caches."
+  sources <- liftIO $ CNix.getDerivationSources derivation
+  for_ sources \src -> do
+    liftIO $ CNix.ensurePath store src
+  pure derivation
diff --git a/hercules-ci-agent-worker/Hercules/Agent/Worker/HerculesStore.hs b/hercules-ci-agent-worker/Hercules/Agent/Worker/HerculesStore.hs
new file mode 100644
--- /dev/null
+++ b/hercules-ci-agent-worker/Hercules/Agent/Worker/HerculesStore.hs
@@ -0,0 +1,102 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Hercules.Agent.Worker.HerculesStore
+  ( module Hercules.Agent.Worker.HerculesStore,
+    HerculesStore,
+  )
+where
+
+import Control.Exception
+  ( catch,
+  )
+import qualified Data.ByteString.Unsafe as BS
+import Data.Coerce (coerce)
+import qualified Foreign.C
+import Foreign.C.String (withCString)
+import Foreign.StablePtr (castStablePtrToPtr, newStablePtr)
+import Hercules.Agent.Worker.HerculesStore.Context
+  ( ExceptionPtr,
+    HerculesStore,
+    context,
+  )
+import Hercules.CNix (Store)
+import Hercules.CNix.Expr (Store (Store))
+import Hercules.CNix.Store.Context (Ref)
+import qualified Language.C.Inline.Cpp as C
+import qualified Language.C.Inline.Cpp.Exceptions as C
+import Protolude
+import Prelude ()
+
+C.context context
+
+C.include "<cstring>"
+
+C.include "<nix/config.h>"
+
+C.include "<nix/shared.hh>"
+
+C.include "<nix/store-api.hh>"
+
+C.include "<nix/get-drvs.hh>"
+
+C.include "<nix/derivations.hh>"
+
+C.include "<nix/affinity.hh>"
+
+C.include "<nix/globals.hh>"
+
+C.include "hercules-aliases.h"
+
+C.using "namespace nix"
+
+withHerculesStore ::
+  Store ->
+  (Ptr (Ref HerculesStore) -> IO a) ->
+  IO a
+withHerculesStore (Store wrappedStore) =
+  bracket
+    ( liftIO
+        [C.block| refHerculesStore* {
+          refStore &s = *$(refStore *wrappedStore);
+          refHerculesStore hs(new HerculesStore({}, s));
+          return new refHerculesStore(hs);
+        } |]
+    )
+    (\x -> liftIO [C.exp| void { delete $(refHerculesStore* x) } |])
+
+nixStore :: Ptr (Ref HerculesStore) -> Store
+nixStore = coerce
+
+printDiagnostics :: Ptr (Ref HerculesStore) -> IO ()
+printDiagnostics s =
+  [C.throwBlock| void{
+    (*$(refHerculesStore* s))->printDiagnostics();
+  }|]
+
+-- TODO catch pure exceptions from displayException
+setBuilderCallback :: Ptr (Ref HerculesStore) -> (ByteString -> IO ()) -> IO ()
+setBuilderCallback s callback = do
+  p <-
+    mkBuilderCallback $ \cstr exceptionToThrowPtr ->
+      Control.Exception.catch (BS.unsafePackMallocCString cstr >>= callback) $ \e ->
+        withCString (displayException (e :: SomeException)) $ \renderedException -> do
+          stablePtr <- castStablePtrToPtr <$> newStablePtr e
+          [C.block| void {
+            (*$(exception_ptr *exceptionToThrowPtr)) = std::make_exception_ptr(HaskellException(std::string($(const char* renderedException)), $(void* stablePtr)));
+          }|]
+  [C.throwBlock| void {
+    (*$(refHerculesStore* s))->setBuilderCallback($(void (*p)(const char *, exception_ptr *) ));
+  }|]
+
+type BuilderCallback = Foreign.C.CString -> Ptr ExceptionPtr -> IO ()
+
+-- Work around a problem in ghcide with foreign imports.
+#ifndef __GHCIDE__
+foreign import ccall "wrapper"
+  mkBuilderCallback :: BuilderCallback -> IO (FunPtr BuilderCallback)
+#else
+mkBuilderCallback :: BuilderCallback -> IO (FunPtr BuilderCallback)
+mkBuilderCallback = panic "This is a stub to work around a ghcide issue. Please compile without -D__GHCIDE__"
+#endif
diff --git a/hercules-ci-agent-worker/Hercules/Agent/Worker/HerculesStore/Context.hs b/hercules-ci-agent-worker/Hercules/Agent/Worker/HerculesStore/Context.hs
new file mode 100644
--- /dev/null
+++ b/hercules-ci-agent-worker/Hercules/Agent/Worker/HerculesStore/Context.hs
@@ -0,0 +1,35 @@
+{-# LANGUAGE EmptyDataDecls #-}
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Hercules.Agent.Worker.HerculesStore.Context where
+
+import qualified Data.Map as M
+import Hercules.CNix.Expr.Context (Ref)
+import qualified Hercules.CNix.Store.Context as Store
+import qualified Language.C.Inline.Context as C
+import qualified Language.C.Inline.Cpp as C
+import qualified Language.C.Types as C
+import Protolude
+
+data HerculesStore
+
+data ExceptionPtr
+
+context :: C.Context
+context =
+  C.cppCtx <> C.fptrCtx
+    <> C.bsCtx
+    <> Store.context
+    <> herculesStoreContext
+
+(=:) :: k -> a -> Map k a
+(=:) = M.singleton
+
+herculesStoreContext :: C.Context
+herculesStoreContext =
+  mempty
+    { C.ctxTypesTable =
+        C.TypeName "refHerculesStore" =: [t|Ref HerculesStore|]
+          <> C.TypeName "exception_ptr" =: [t|ExceptionPtr|]
+    }
diff --git a/hercules-ci-agent.cabal b/hercules-ci-agent.cabal
--- a/hercules-ci-agent.cabal
+++ b/hercules-ci-agent.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.4
 
 name:           hercules-ci-agent
-version:        0.7.5
+version:        0.8.0
 synopsis:       Runs Continuous Integration tasks on your machines
 category:       Nix, CI, Testing, DevOps
 homepage:       https://docs.hercules-ci.com
@@ -49,18 +49,23 @@
   exposed-modules:
       Data.Fixed.Extras
       Data.Time.Extras
+      Hercules.Agent.NixFile
       Hercules.Agent.Producer
+      Hercules.Agent.Sensitive
       Hercules.Agent.Socket
       Hercules.Agent.STM
       Hercules.Agent.WorkerProtocol.Command
       Hercules.Agent.WorkerProtocol.Command.Build
       Hercules.Agent.WorkerProtocol.Command.BuildResult
+      Hercules.Agent.WorkerProtocol.Command.Effect
       Hercules.Agent.WorkerProtocol.Command.Eval
       Hercules.Agent.WorkerProtocol.Event
       Hercules.Agent.WorkerProtocol.Event.Attribute
       Hercules.Agent.WorkerProtocol.Event.AttributeError
       Hercules.Agent.WorkerProtocol.Event.BuildResult
       Hercules.Agent.WorkerProtocol.LogSettings
+      Hercules.Effect
+      Hercules.Effect.Container
       Data.Conduit.Extras
       Data.Conduit.Katip.Orphans
 
@@ -81,24 +86,32 @@
     , bytestring
     , conduit
     , containers
+    , directory
     , dlist
     , exceptions
+    , filepath
     , hercules-ci-api-agent
+    , hercules-ci-api-core
+    , hercules-ci-cnix-store
     , katip
+    , lens
+    , lens-aeson
     , lifted-async
     , lifted-base
     , monad-control
     , mtl
     , network-uri
-    , optparse-applicative
     , protolude >= 0.3
     , process
+    , process-extras
     , safe-exceptions
     , stm
+    , temporary
     , text
     , time
     , transformers-base
     , unbounded-delays
+    , unix
     , unliftio-core
     , unliftio
     , uuid
@@ -106,57 +119,6 @@
     , wuss
   default-language: Haskell2010
 
--- ugly hack to avoid https://gitlab.haskell.org/ghc/ghc/issues/16477
-library internal-ffi
-  exposed-modules:
-      Hercules.Agent.StoreFFI
-  hs-source-dirs: src-internal-ffi
-  build-depends:       base, protolude
-  default-language:    Haskell2010
-
-library cnix
-  import: cxx-opts
-  exposed-modules:
-      CNix
-      CNix.Internal.Context
-      CNix.Internal.Raw
-      CNix.Internal.Store
-      CNix.Internal.Typed
-  hs-source-dirs: src-cnix
-  include-dirs:
-      cbits
-  cxx-sources:
-     cbits/hercules-store.cxx
-     cbits/hercules-logger.cxx
-  build-depends:
-      base
-    , inline-c
-    , inline-c-cpp
-    , internal-ffi
-    , bytestring
-    , cachix >= 0.5.1
-    , conduit
-    , containers
-    , protolude
-    , unliftio-core
-  pkgconfig-depends:
-      nix-store >= 2.0
-    , nix-expr >= 2.0
-    , nix-main >= 2.0
-    , bdw-gc
-  extra-libraries:
-      boost_context
-  default-language: Haskell2010
-  default-extensions: DeriveGeneric DeriveTraversable DisambiguateRecordFields FlexibleContexts InstanceSigs LambdaCase MultiParamTypeClasses NoImplicitPrelude OverloadedStrings RankNTypes TupleSections TypeApplications TypeOperators
-  ghc-options:
-    -Werror=incomplete-patterns -Werror=missing-fields
-    -Wall
-    -fwarn-tabs
-    -fwarn-unused-imports
-    -fwarn-missing-signatures
-    -fwarn-name-shadowing
-    -fwarn-incomplete-patterns
-
 executable hercules-ci-agent
   main-is: Main.hs
   other-modules:
@@ -176,10 +138,12 @@
       Hercules.Agent.Config
       Hercules.Agent.Config.BinaryCaches
       Hercules.Agent.Compat
+      Hercules.Agent.Effect
       Hercules.Agent.Env
       Hercules.Agent.EnvironmentInfo
       Hercules.Agent.Evaluate
       Hercules.Agent.Evaluate.TraversalQueue
+      Hercules.Agent.Files
       Hercules.Agent.Init
       Hercules.Agent.Log
       Hercules.Agent.Nix
@@ -203,14 +167,14 @@
       aeson
     , async
     , attoparsec
-    , base >=4.7 && <5
+    , base
     , base64-bytestring
     , binary
     , binary-conduit
     , bytestring
     , cachix
     , cachix-api
-    , cnix
+    , hercules-ci-cnix-store
     , conduit
     , conduit-extra
     , containers
@@ -219,8 +183,8 @@
     , exceptions
     , filepath
     , hercules-ci-agent
-    , hercules-ci-api-core == 0.1.1.0
-    , hercules-ci-api-agent == 0.2.2.0
+    , hercules-ci-api-core == 0.1.2.0
+    , hercules-ci-api-agent == 0.3.0.0
     , hostname
     , http-client
     , http-client-tls
@@ -236,6 +200,7 @@
     , network
     , optparse-applicative
     , process
+    , process-extras
     , protolude
     , safe-exceptions
     , servant >=0.14.1
@@ -266,11 +231,18 @@
       Hercules.Agent.Worker.Build
       Hercules.Agent.Worker.Build.Prefetched
       Hercules.Agent.Worker.Build.Logger
+      Hercules.Agent.Worker.Build.Logger.Context
+      Hercules.Agent.Worker.Effect
       Paths_hercules_ci_agent
+      Hercules.Agent.Worker.HerculesStore
+      Hercules.Agent.Worker.HerculesStore.Context
   autogen-modules:
       Paths_hercules_ci_agent
   hs-source-dirs:
       hercules-ci-agent-worker
+  cxx-sources:
+      cbits/hercules-store.cxx
+      cbits/hercules-logger.cxx
   default-extensions: DeriveGeneric DeriveTraversable DisambiguateRecordFields FlexibleContexts InstanceSigs LambdaCase MultiParamTypeClasses NoImplicitPrelude OverloadedStrings RankNTypes TupleSections TypeApplications TypeOperators
   ghc-options:
     -Werror=incomplete-patterns -Werror=missing-fields
@@ -290,31 +262,39 @@
   build-depends:
       aeson
     , async
-    , base >=4.7 && <5
+    , base
     , binary
     , binary-conduit
     , bytestring
     , cachix
-    , cnix
     , conduit
     , containers
+    , directory
     , exceptions
+    , filepath
     , hercules-ci-agent
     , hercules-ci-api-agent
     , hercules-ci-api-core
-    , internal-ffi
+    , hercules-ci-cnix-store
+    , hercules-ci-cnix-expr
     , inline-c
     , inline-c-cpp
     , katip
+    , lens
+    , lens-aeson
     , lifted-async
     , lifted-base
     , monad-control
+    , mtl
     , network-uri
-    , optparse-applicative
     , protolude
+    , process
+    , process-extras
     , safe-exceptions
     , stm
     , text
+    , temporary
+    , transformers
     , transformers-base
     , unix
     , unliftio
@@ -350,11 +330,11 @@
       aeson
     , async
     , attoparsec
-    , base >=4.7 && <5
+    , base
     , binary
     , binary-conduit
     , bytestring
-    , cnix
+    , hercules-ci-cnix-store
     , conduit
     , containers
     , exceptions
@@ -367,7 +347,6 @@
     , lifted-async
     , lifted-base
     , monad-control
-    , optparse-applicative
     , process
     , protolude
     , safe-exceptions
diff --git a/hercules-ci-agent/Data/Functor/Partitioner.hs b/hercules-ci-agent/Data/Functor/Partitioner.hs
--- a/hercules-ci-agent/Data/Functor/Partitioner.hs
+++ b/hercules-ci-agent/Data/Functor/Partitioner.hs
@@ -20,13 +20,12 @@
 import Protolude
 
 -- TODO: Profunctor
-data Partitioner a b
-  = forall m.
-    (Monoid m) =>
-    Partitioner
-      { ingest :: a -> Maybe m,
-        digest :: m -> b
-      }
+data Partitioner a b = forall m.
+  (Monoid m) =>
+  Partitioner
+  { ingest :: a -> Maybe m,
+    digest :: m -> b
+  }
 
 partitionList :: Partitioner a b -> [a] -> b
 partitionList (Partitioner ing dig) = dig . fold . mapMaybe ing
@@ -42,16 +41,16 @@
   fmap f (Partitioner ing dig) = (Partitioner ing (f . dig))
 
 instance Applicative (Partitioner a) where
-
   pure a =
     Partitioner {ingest = const (Nothing :: Maybe ()), digest = pure a}
 
-  (Partitioner ingp digp) <*> (Partitioner ingq digq) = Partitioner
-    { ingest = \a -> case ingp a of
-        Just mp -> Just (mp, mempty)
-        Nothing -> (mempty,) <$> ingq a,
-      digest = \(mp, mq) -> digp mp (digq mq)
-    }
+  (Partitioner ingp digp) <*> (Partitioner ingq digq) =
+    Partitioner
+      { ingest = \a -> case ingp a of
+          Just mp -> Just (mp, mempty)
+          Nothing -> (mempty,) <$> ingq a,
+        digest = \(mp, mq) -> digp mp (digq mq)
+      }
 
 -- instance Profunctor Partitioner
 -- ...
@@ -71,22 +70,24 @@
   Ord k =>
   (k -> v -> Maybe a) ->
   Partitioner (WithKey k v) (Map k a)
-partWithKey f = Partitioner
-  { ingest = \(WithKey (k, v)) -> M.singleton k <$> f k v,
-    digest = identity
-  }
+partWithKey f =
+  Partitioner
+    { ingest = \(WithKey (k, v)) -> M.singleton k <$> f k v,
+      digest = identity
+    }
 
 traversePartWithKey ::
   (Ord k, Applicative f) =>
   (k -> v -> Maybe (f a)) ->
   Partitioner (WithKey k v) (f (Map k a))
-traversePartWithKey f = Partitioner
-  { ingest = \(WithKey (k, v)) -> do
-      -- Maybe
-      x <- f k v
-      pure $ Ap $ (M.singleton k <$> x),
-    digest = getAp
-  }
+traversePartWithKey f =
+  Partitioner
+    { ingest = \(WithKey (k, v)) -> do
+        -- Maybe
+        x <- f k v
+        pure $ Ap $ (M.singleton k <$> x),
+      digest = getAp
+    }
 
 -- | Use with 'partWithKey' to match on the key.
 --
diff --git a/hercules-ci-agent/Hercules/Agent.hs b/hercules-ci-agent/Hercules/Agent.hs
--- a/hercules-ci-agent/Hercules/Agent.hs
+++ b/hercules-ci-agent/Hercules/Agent.hs
@@ -10,9 +10,7 @@
     withAsync,
   )
 import Control.Concurrent.Lifted (forkFinally, killThread)
-import Control.Concurrent.STM (TVar, readTVar)
 import Control.Concurrent.STM.TChan
-import Control.Exception (displayException)
 import Control.Exception.Lifted (bracket)
 import Control.Monad.IO.Unlift (MonadUnliftIO)
 import qualified Data.Aeson as A
@@ -20,10 +18,11 @@
 import Data.Time (getCurrentTime)
 import qualified Data.UUID.V4 as UUID
 import qualified Hercules.API.Agent.Build.BuildTask as BuildTask
+import qualified Hercules.API.Agent.Effect.EffectTask as EffectTask
 import qualified Hercules.API.Agent.Evaluate.EvaluateTask as EvaluateTask
 import qualified Hercules.API.Agent.LifeCycle as LifeCycle
-import qualified Hercules.API.Agent.LifeCycle.StartInfo as StartInfo
 import Hercules.API.Agent.LifeCycle.StartInfo (tasksInProgress)
+import qualified Hercules.API.Agent.LifeCycle.StartInfo as StartInfo
 import qualified Hercules.API.Agent.Socket.AgentPayload as AgentPayload
 import qualified Hercules.API.Agent.Socket.ServicePayload as ServicePayload
 import Hercules.API.Agent.Tasks
@@ -43,11 +42,12 @@
     tasksClient,
   )
 import qualified Hercules.Agent.Config as Config
-import qualified Hercules.Agent.Env as Env
+import qualified Hercules.Agent.Effect as Effect
 import Hercules.Agent.Env
   ( App,
     runHerculesClient,
   )
+import qualified Hercules.Agent.Env as Env
 import Hercules.Agent.EnvironmentInfo (extractAgentInfo)
 import qualified Hercules.Agent.Evaluate as Evaluate
 import qualified Hercules.Agent.Init as Init
@@ -55,7 +55,6 @@
 import qualified Hercules.Agent.Options as Options
 import Hercules.Agent.STM
 import Hercules.Agent.Socket as Socket
-import Hercules.Agent.Socket (serviceChan)
 import Hercules.Agent.Token (withAgentToken)
 import Hercules.Error
   ( cap,
@@ -103,28 +102,32 @@
 
 run :: Env.Env -> Config.FinalConfig -> IO ()
 run env _cfg = do
-  Env.runApp env
-    $ katipAddContext (sl "agent-version" (A.String herculesAgentVersion))
-    $ (configureLimits >>)
-    $ (configChecks >>)
-    $ withAgentToken
-    $ withLifeCycle \hello -> withTaskState \tasks ->
-      withAgentSocket hello tasks \socket ->
-        withApplicationLevelPinger socket $ do
-          logLocM InfoS "Agent online."
-          forever $ do
-            (liftIO $ atomically $ readTChan $ serviceChan socket) >>= \case
-              ServicePayload.ServiceInfo _ -> pass
-              ServicePayload.StartEvaluation evalTask ->
-                launchTask tasks socket (Task.upcastId $ EvaluateTask.id evalTask) do
-                  Cache.withCaches do
-                    Evaluate.performEvaluation evalTask
-                    pure $ TaskStatus.Successful ()
-              ServicePayload.StartBuild buildTask ->
-                launchTask tasks socket (Task.upcastId $ BuildTask.id buildTask) do
-                  Cache.withCaches $
-                    Build.performBuild buildTask
-              ServicePayload.Cancel cancellation -> cancelTask tasks socket cancellation
+  Env.runApp env $
+    katipAddContext (sl "agent-version" (A.String herculesAgentVersion)) $
+      (configureLimits >>) $
+        (configChecks >>) $
+          withAgentToken $
+            withLifeCycle \hello -> withTaskState \tasks ->
+              withAgentSocket hello tasks \socket ->
+                withApplicationLevelPinger socket $ do
+                  logLocM InfoS "Agent online."
+                  forever $ do
+                    liftIO (atomically $ readTChan $ serviceChan socket) >>= \case
+                      ServicePayload.ServiceInfo _ -> pass
+                      ServicePayload.StartEvaluation evalTask ->
+                        launchTask tasks socket (Task.upcastId $ EvaluateTask.id evalTask) do
+                          Cache.withCaches do
+                            Evaluate.performEvaluation evalTask
+                            pure $ TaskStatus.Successful ()
+                      ServicePayload.StartBuild buildTask ->
+                        launchTask tasks socket (Task.upcastId $ BuildTask.id buildTask) do
+                          Cache.withCaches $
+                            Build.performBuild buildTask
+                      ServicePayload.StartEffect effectTask ->
+                        launchTask tasks socket (Task.upcastId $ EffectTask.id effectTask) do
+                          Cache.withCaches $
+                            Effect.performEffect effectTask
+                      ServicePayload.Cancel cancellation -> cancelTask tasks socket cancellation
 
 withTaskState :: (TVar (Map (Id (Task Task.Any)) ThreadId) -> App a) -> App a
 withTaskState f = do
@@ -176,10 +179,10 @@
           report $ TaskStatus.Exceptional $ toS $ displayException e
       -- TODO use socket
       report status =
-        retry (cap 60 exponential)
-          $ noContent
-          $ runHerculesClient
-          $ tasksSetStatus tasksClient taskId status
+        retry (cap 60 exponential) $
+          noContent $
+            runHerculesClient $
+              tasksSetStatus tasksClient taskId status
   void @_ @ThreadId $
     flip forkFinally done do
       insertSelf
@@ -206,16 +209,16 @@
   now <- liftIO getCurrentTime
   freshId <- Id <$> liftIO UUID.nextRandom
   let startInfo = StartInfo.StartInfo {id = freshId, startTime = now}
-      hello = StartInfo.Hello
-        { agentInfo = agentInfo,
-          startInfo = startInfo,
-          tasksInProgress = []
-        }
+      hello =
+        StartInfo.Hello
+          { agentInfo = agentInfo,
+            startInfo = startInfo,
+            tasksInProgress = []
+          }
       req r =
-        retry (cap 60 exponential)
-          $ noContent
-          $ runHerculesClient
-          $ r
+        retry (cap 60 exponential) $
+          noContent $
+            runHerculesClient r
       sayGoodbye = req $ LifeCycle.goodbye lifeCycleClient startInfo
   bracket pass (\() -> sayGoodbye) (\() -> app hello)
 
@@ -234,10 +237,11 @@
 tryIncreaseResourceLimitTo :: (MonadUnliftIO m, KatipContext m) => Resource -> Text -> ResourceLimit -> m ()
 tryIncreaseResourceLimitTo resource resourceName target = katipAddContext (sl "resource" resourceName) do
   lims <- liftIO $ getResourceLimit resource
-  let lims' = ResourceLimits
-        { softLimit = target & atLeast (softLimit lims) & atMost (hardLimit lims),
-          hardLimit = hardLimit lims
-        }
+  let lims' =
+        ResourceLimits
+          { softLimit = target & atLeast (softLimit lims) & atMost (hardLimit lims),
+            hardLimit = hardLimit lims
+          }
       atLeast ResourceLimitInfinity _ = ResourceLimitInfinity
       atLeast _ ResourceLimitInfinity = ResourceLimitInfinity
       atLeast ResourceLimitUnknown r = r
@@ -250,8 +254,8 @@
       atMost l ResourceLimitInfinity = l
       atMost ResourceLimitUnknown r = r
       -- atMost l ResourceLimitUnknown = l -- already covered
-      showLimit (ResourceLimitInfinity) = "infinity"
-      showLimit (ResourceLimitUnknown) = A.Null
+      showLimit ResourceLimitInfinity = "infinity"
+      showLimit ResourceLimitUnknown = A.Null
       showLimit (ResourceLimit n) = A.Number $ fromIntegral n
   liftIO (setResourceLimit resource lims') `catch` \e ->
     katipAddContext
diff --git a/hercules-ci-agent/Hercules/Agent/AgentSocket.hs b/hercules-ci-agent/Hercules/Agent/AgentSocket.hs
--- a/hercules-ci-agent/Hercules/Agent/AgentSocket.hs
+++ b/hercules-ci-agent/Hercules/Agent/AgentSocket.hs
@@ -2,10 +2,10 @@
 
 import qualified Data.Map as M
 import Hercules.API.Agent.LifeCycle.StartInfo (Hello, tasksInProgress)
-import qualified Hercules.API.Agent.Socket.AgentPayload as AgentPayload
 import Hercules.API.Agent.Socket.AgentPayload (AgentPayload)
-import qualified Hercules.API.Agent.Socket.ServicePayload as ServicePayload
+import qualified Hercules.API.Agent.Socket.AgentPayload as AgentPayload
 import Hercules.API.Agent.Socket.ServicePayload (ServicePayload)
+import qualified Hercules.API.Agent.Socket.ServicePayload as ServicePayload
 import Hercules.API.Id (Id)
 import Hercules.API.Task (Task)
 import qualified Hercules.API.Task as Task
diff --git a/hercules-ci-agent/Hercules/Agent/Build.hs b/hercules-ci-agent/Hercules/Agent/Build.hs
--- a/hercules-ci-agent/Hercules/Agent/Build.hs
+++ b/hercules-ci-agent/Hercules/Agent/Build.hs
@@ -4,15 +4,15 @@
 import qualified Data.Map as M
 import qualified Hercules.API.Agent.Build as API.Build
 import qualified Hercules.API.Agent.Build.BuildEvent as BuildEvent
-import qualified Hercules.API.Agent.Build.BuildEvent.OutputInfo as OutputInfo
 import Hercules.API.Agent.Build.BuildEvent.OutputInfo
   ( OutputInfo,
   )
+import qualified Hercules.API.Agent.Build.BuildEvent.OutputInfo as OutputInfo
 import qualified Hercules.API.Agent.Build.BuildEvent.Pushed as Pushed
-import qualified Hercules.API.Agent.Build.BuildTask as BuildTask
 import Hercules.API.Agent.Build.BuildTask
   ( BuildTask,
   )
+import qualified Hercules.API.Agent.Build.BuildTask as BuildTask
 import Hercules.API.Servant (noContent)
 import Hercules.API.TaskStatus (TaskStatus)
 import qualified Hercules.API.TaskStatus as TaskStatus
@@ -23,6 +23,7 @@
 import qualified Hercules.Agent.Env as Env
 import Hercules.Agent.Log
 import qualified Hercules.Agent.Nix as Nix
+import Hercules.Agent.Sensitive (Sensitive (Sensitive))
 import qualified Hercules.Agent.ServiceInfo as ServiceInfo
 import Hercules.Agent.WorkerProcess
 import qualified Hercules.Agent.WorkerProcess as WorkerProcess
@@ -43,7 +44,7 @@
   statusRef <- newIORef Nothing
   extraNixOptions <- Nix.askExtraOptions
   workerEnv <- liftIO $ WorkerProcess.prepareEnv (WorkerProcess.WorkerEnvSettings {nixPath = mempty})
-  let opts = [show $ extraNixOptions]
+  let opts = [show extraNixOptions]
       procSpec =
         (System.Process.proc workerExe opts)
           { env = Just workerEnv,
@@ -59,16 +60,21 @@
         _ -> pass
   baseURL <- asks (ServiceInfo.bulkSocketBaseURL . Env.serviceInfo)
   materialize <- asks (not . Config.nixUserIsTrusted . Env.config)
-  liftIO $ writeChan commandChan $ Just $ Command.Build $ Command.Build.Build
-    { drvPath = BuildTask.derivationPath buildTask,
-      inputDerivationOutputPaths = encodeUtf8 <$> BuildTask.inputDerivationOutputPaths buildTask,
-      logSettings = LogSettings.LogSettings
-        { token = LogSettings.Sensitive $ BuildTask.logToken buildTask,
-          path = "/api/v1/logs/build/socket",
-          baseURL = toS $ Network.URI.uriToString identity baseURL ""
-        },
-      materializeDerivation = materialize
-    }
+  liftIO $
+    writeChan commandChan $
+      Just $
+        Command.Build $
+          Command.Build.Build
+            { drvPath = BuildTask.derivationPath buildTask,
+              inputDerivationOutputPaths = encodeUtf8 <$> BuildTask.inputDerivationOutputPaths buildTask,
+              logSettings =
+                LogSettings.LogSettings
+                  { token = Sensitive $ BuildTask.logToken buildTask,
+                    path = "/api/v1/logs/build/socket",
+                    baseURL = toS $ Network.URI.uriToString identity baseURL ""
+                  },
+              materializeDerivation = materialize
+            }
   exitCode <- runWorker procSpec (stderrLineHandler "Builder") commandChan writeEvent
   logLocM DebugS $ "Worker exit: " <> logStr (show exitCode :: Text)
   case exitCode of
@@ -76,13 +82,13 @@
     _ -> panic $ "Worker failed: " <> show exitCode
   status <- readIORef statusRef
   case status of
-    Just (BuildResult.BuildSuccess {outputs = outs'}) -> do
+    Just BuildResult.BuildSuccess {outputs = outs'} -> do
       let outs = convertOutputs (BuildTask.derivationPath buildTask) outs'
       reportOutputInfos buildTask outs
       push buildTask outs
       reportSuccess buildTask
       pure $ TaskStatus.Successful ()
-    Just (BuildResult.BuildFailure {}) -> pure $ TaskStatus.Terminated ()
+    Just BuildResult.BuildFailure {} -> pure $ TaskStatus.Terminated ()
     Nothing -> pure $ TaskStatus.Exceptional "Build did not complete"
 
 convertOutputs :: Text -> [BuildResult.OutputInfo] -> Map Text OutputInfo
diff --git a/hercules-ci-agent/Hercules/Agent/Cache.hs b/hercules-ci-agent/Hercules/Agent/Cache.hs
--- a/hercules-ci-agent/Hercules/Agent/Cache.hs
+++ b/hercules-ci-agent/Hercules/Agent/Cache.hs
@@ -2,8 +2,6 @@
 
 module Hercules.Agent.Cache where
 
-import qualified CNix
-import qualified Cachix.Client.Store as Store
 import qualified Data.Map as M
 import qualified Data.Text as T
 import qualified Data.Text.IO as T
@@ -14,15 +12,16 @@
 import qualified Hercules.Agent.Env as Env
 import qualified Hercules.Agent.Nix as Nix
 import qualified Hercules.Agent.SecureDirectory as SecureDirectory
+import qualified Hercules.CNix as CNix
+import qualified Hercules.CNix.Store as Store
 import qualified Hercules.Formats.NixCache as NixCache
 import Katip
 import Protolude
 import System.IO (hClose)
-import qualified Unsafe.Coerce
 
 withCaches :: App a -> App a
 withCaches m = do
-  netrcLns <- Cachix.getNetrcLines
+  netrcLns <- (<>) <$> Cachix.getNetrcLines <*> Nix.getNetrcLines
   csubsts <- Cachix.getSubstituters
   cpubkeys <- Cachix.getTrustedPublicKeys
   nixCaches <- asks (Config.nixCaches . Env.binaryCaches)
@@ -81,12 +80,9 @@
     Store.addToPathSet path pathSet
   pure pathSet
 
-castStore :: Ptr (CNix.Ref CNix.NixStore) -> Store.Store
-castStore = Unsafe.Coerce.unsafeCoerce
-
-signClosure :: Ptr (CNix.Ref CNix.NixStore) -> ForeignPtr CNix.SecretKey -> Store.PathSet -> IO (Sum Int, Sum Int)
+signClosure :: CNix.Store -> ForeignPtr CNix.SecretKey -> Store.PathSet -> IO (Sum Int, Sum Int)
 signClosure store key' pathSet = withForeignPtr key' \key -> do
-  closure <- Store.computeFSClosure (castStore store) Store.defaultClosureParams pathSet
+  closure <- Store.computeFSClosure store Store.defaultClosureParams pathSet
   closure
     & Store.traversePathSet
       ( \path -> do
diff --git a/hercules-ci-agent/Hercules/Agent/Cachix.hs b/hercules-ci-agent/Hercules/Agent/Cachix.hs
--- a/hercules-ci-agent/Hercules/Agent/Cachix.hs
+++ b/hercules-ci-agent/Hercules/Agent/Cachix.hs
@@ -23,36 +23,38 @@
       nixStore = nixStore,
       clientEnv = clientEnv
     } <-
-    asks $ Agent.Cachix.getEnv
+    asks Agent.Cachix.getEnv
   pushCache <-
-    escalate
-      $ maybeToEither (FatalError $ "Cache not found " <> cache)
-      $ M.lookup cache pushCaches
+    escalate $
+      maybeToEither (FatalError $ "Cache not found " <> cache) $
+        M.lookup cache pushCaches
   ul <- askUnliftIO
+  let pushParams =
+        Cachix.Push.PushParams
+          { pushParamsName = Agent.Cachix.pushCacheName pushCache,
+            pushParamsSecret = Agent.Cachix.pushCacheSecret pushCache,
+            pushParamsStore = nixStore,
+            pushParamsClientEnv = clientEnv,
+            pushParamsStrategy = \storePath ->
+              let ctx = withNamedContext "path" storePath
+               in Cachix.Push.PushStrategy
+                    { onAlreadyPresent = pass,
+                      onAttempt = \retryStatus size ->
+                        ctx $
+                          withNamedContext "size" size $
+                            withNamedContext "retry" (show retryStatus :: Text) $
+                              logLocM DebugS "pushing",
+                      on401 = throwIO $ FatalError "Cachix push is unauthorized",
+                      onError = \err -> throwIO $ FatalError $ "Error pushing to cachix: " <> show err,
+                      onDone = ctx $ logLocM DebugS "push done",
+                      withXzipCompressor = Cachix.Push.defaultWithXzipCompressor,
+                      omitDeriver = False
+                    }
+          }
   void $
     Cachix.Push.pushClosure
-      ( \f l ->
-          liftIO $ Cachix.Push.mapConcurrentlyBounded workers (fmap (unliftIO ul) f) l
-      )
-      clientEnv
-      nixStore
-      pushCache
-      ( \storePath ->
-          let ctx = withNamedContext "path" storePath
-           in Cachix.Push.PushStrategy
-                { onAlreadyPresent = pass,
-                  onAttempt = \retryStatus size ->
-                    ctx
-                      $ withNamedContext "size" size
-                      $ withNamedContext "retry" (show retryStatus :: Text)
-                      $ logLocM DebugS "pushing",
-                  on401 = throwIO $ FatalError $ "Cachix push is unauthorized",
-                  onError = \err -> throwIO $ FatalError $ "Error pushing to cachix: " <> show err,
-                  onDone = ctx $ logLocM DebugS "push done",
-                  withXzipCompressor = Cachix.Push.defaultWithXzipCompressor,
-                  omitDeriver = False
-                }
-      )
+      (\f l -> liftIO $ Cachix.Push.mapConcurrentlyBounded workers (fmap (unliftIO ul) f) l)
+      pushParams
       paths
 
 getNetrcLines :: App [Text]
diff --git a/hercules-ci-agent/Hercules/Agent/Cachix/Env.hs b/hercules-ci-agent/Hercules/Agent/Cachix/Env.hs
--- a/hercules-ci-agent/Hercules/Agent/Cachix/Env.hs
+++ b/hercules-ci-agent/Hercules/Agent/Cachix/Env.hs
@@ -1,19 +1,23 @@
 module Hercules.Agent.Cachix.Env where
 
 import qualified Cachix.Client.Push as Cachix
-import Cachix.Client.Store (Store)
+import Hercules.CNix.Store (Store)
 import Hercules.Formats.CachixCache (CachixCache)
 import Protolude
 import Servant.Client (ClientEnv)
 
-data Env
-  = Env
-      { pushCaches :: Map Text Cachix.PushCache,
-        cacheKeys :: Map Text CachixCache,
-        netrcLines :: [Text],
-        nixStore :: Store,
-        clientEnv :: ClientEnv
-      }
+data PushCache = PushCache
+  { pushCacheName :: Text,
+    pushCacheSecret :: Cachix.PushSecret
+  }
+
+data Env = Env
+  { pushCaches :: Map Text PushCache,
+    cacheKeys :: Map Text CachixCache,
+    netrcLines :: [Text],
+    nixStore :: Store,
+    clientEnv :: ClientEnv
+  }
 
 class HasEnv env where
   getEnv :: env -> Env
diff --git a/hercules-ci-agent/Hercules/Agent/Cachix/Init.hs b/hercules-ci-agent/Hercules/Agent/Cachix/Init.hs
--- a/hercules-ci-agent/Hercules/Agent/Cachix/Init.hs
+++ b/hercules-ci-agent/Hercules/Agent/Cachix/Init.hs
@@ -5,11 +5,11 @@
 import Cachix.Client.Env (cachixVersion)
 import qualified Cachix.Client.Push as Cachix.Push
 import qualified Cachix.Client.Secrets as Cachix.Secrets
-import qualified Cachix.Client.Store as Cachix.Store
 import Cachix.Client.URI (defaultCachixBaseUrl)
 import qualified Data.Map as M
 import Hercules.Agent.Cachix.Env as Env
 import qualified Hercules.Agent.Config as Config
+import Hercules.CNix.Store (openStore)
 import Hercules.Error
 import qualified Hercules.Formats.CachixCache as CachixCache
 import qualified Katip as K
@@ -35,15 +35,16 @@
   K.katipAddContext (K.sl "caches" (M.keys cks)) $
     K.logLocM K.DebugS ("Cachix init " <> K.logStr (show (M.keys cks) :: Text))
   pcs <- liftIO $ toPushCaches cks
-  store <- liftIO Cachix.Store.openStore
+  store <- liftIO openStore
   httpManager <- newTlsManagerWith customManagerSettings
-  pure Env.Env
-    { pushCaches = pcs,
-      netrcLines = toNetrcLines cks,
-      cacheKeys = cks,
-      nixStore = store,
-      clientEnv = mkClientEnv httpManager defaultCachixBaseUrl
-    }
+  pure
+    Env.Env
+      { pushCaches = pcs,
+        netrcLines = toNetrcLines cks,
+        cacheKeys = cks,
+        nixStore = store,
+        clientEnv = mkClientEnv httpManager defaultCachixBaseUrl
+      }
 
 toNetrcLines :: Map Text CachixCache.CachixCache -> [Text]
 toNetrcLines = concatMap toNetrcLine . M.toList
@@ -52,19 +53,30 @@
       pt <- toList $ CachixCache.authToken keys
       pure $ "machine " <> name <> ".cachix.org" <> " password " <> pt
 
-toPushCaches :: Map Text CachixCache.CachixCache -> IO (Map Text Cachix.Push.PushCache)
+toPushCaches :: Map Text CachixCache.CachixCache -> IO (Map Text PushCache)
 toPushCaches = sequenceA . M.mapMaybeWithKey toPushCaches'
   where
     toPushCaches' name keys =
       let t = fromMaybe "" (CachixCache.authToken keys)
        in do
             sk <- head $ CachixCache.signingKeys keys
-            Just $ escalateAs FatalError $ do
-              k' <- Cachix.Secrets.parseSigningKeyLenient sk
-              pure Cachix.Push.PushCache
-                { pushCacheName = name,
-                  pushCacheSecret =
-                    Cachix.Push.PushSigningKey
-                      (Servant.Auth.Client.Token $ encodeUtf8 t)
-                      k'
-                }
+            Just $
+              escalateAs FatalError $ do
+                k' <- Cachix.Secrets.parseSigningKeyLenient sk
+                pure
+                  PushCache
+                    { pushCacheName = name,
+                      pushCacheSecret =
+                        Cachix.Push.PushSigningKey
+                          (Servant.Auth.Client.Token $ encodeUtf8 t)
+                          k'
+                    }
+            <|> do
+              token <- head $ CachixCache.authToken keys
+              Just $
+                pure
+                  PushCache
+                    { pushCacheName = name,
+                      pushCacheSecret =
+                        Cachix.Push.PushToken (Servant.Auth.Client.Token $ encodeUtf8 token)
+                    }
diff --git a/hercules-ci-agent/Hercules/Agent/Client.hs b/hercules-ci-agent/Hercules/Agent/Client.hs
--- a/hercules-ci-agent/Hercules/Agent/Client.hs
+++ b/hercules-ci-agent/Hercules/Agent/Client.hs
@@ -1,5 +1,9 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleInstances #-}
 {-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE UndecidableInstances #-}
 {-# OPTIONS_GHC -O0 #-}
+{-# OPTIONS_GHC -Wno-orphans #-}
 
 -- TODO https://github.com/haskell-servant/servant/issues/986
 module Hercules.Agent.Client
@@ -16,32 +20,47 @@
 import Hercules.API.Agent.Build (BuildAPI)
 import Hercules.API.Agent.Evaluate (EvalAPI)
 import Hercules.API.Agent.LifeCycle (LifeCycleAPI)
+import Hercules.API.Agent.State (ContentLength)
 import Hercules.API.Agent.Tasks (TasksAPI)
 import Hercules.API.Logs (LogsAPI)
 import Hercules.API.Servant (useApi)
 import Protolude
+import Servant.API
 import Servant.API.Generic
 import Servant.Auth.Client ()
-import qualified Servant.Client
-import Servant.Client (ClientM)
 import Servant.Client.Generic (AsClientT)
+import Servant.Client.Streaming (ClientM)
+import qualified Servant.Client.Streaming
 
+-- | Bad instance to make it the client for State api compile. GHC seems to pick
+-- the wrong overlappable instance.
+instance
+  FromSourceIO
+    ByteString
+    ( Headers
+        '[Hercules.API.Agent.State.ContentLength]
+        (SourceIO ByteString)
+    )
+  where
+  fromSourceIO = addHeader (-1) . fromSourceIO
+
 client :: AgentAPI ClientAuth (AsClientT ClientM)
-client = fromServant $ Servant.Client.client (servantApi @ClientAuth)
+client = fromServant $ Servant.Client.Streaming.client (servantApi @ClientAuth)
 
 tasksClient :: TasksAPI ClientAuth (AsClientT ClientM)
-tasksClient = useApi tasks $ Hercules.Agent.Client.client
+tasksClient = useApi tasks Hercules.Agent.Client.client
 
 evalClient :: EvalAPI ClientAuth (AsClientT ClientM)
-evalClient = useApi eval $ Hercules.Agent.Client.client
+evalClient = useApi eval Hercules.Agent.Client.client
 
 buildClient :: BuildAPI ClientAuth (AsClientT ClientM)
-buildClient = useApi build $ Hercules.Agent.Client.client
+buildClient = useApi build Hercules.Agent.Client.client
 
 lifeCycleClient :: LifeCycleAPI ClientAuth (AsClientT ClientM)
-lifeCycleClient = useApi lifeCycle $ Hercules.Agent.Client.client
+lifeCycleClient = useApi lifeCycle Hercules.Agent.Client.client
 
 logsClient :: LogsAPI () (AsClientT ClientM)
 logsClient =
-  fromServant $ Servant.Client.client $
-    (Proxy @(AddAPIVersion (ToServantApi (LogsAPI ()))))
+  fromServant $
+    Servant.Client.Streaming.client
+      (Proxy @(AddAPIVersion (ToServantApi (LogsAPI ()))))
diff --git a/hercules-ci-agent/Hercules/Agent/Config.hs b/hercules-ci-agent/Hercules/Agent/Config.hs
--- a/hercules-ci-agent/Hercules/Agent/Config.hs
+++ b/hercules-ci-agent/Hercules/Agent/Config.hs
@@ -14,6 +14,7 @@
   )
 where
 
+import GHC.Conc (getNumProcessors)
 import Katip (Severity (..))
 import Protolude hiding (to)
 import qualified System.Environment
@@ -28,28 +29,29 @@
 data Purpose = Input | Final
 
 -- | Whether the 'Final' value is optional.
-data Sort = Required | Optional
+data Sort = Required | Optional | From Sort Type
 
 type family Item purpose sort a where
+  Item 'Input ('From sort b) a = Item 'Input sort b
+  Item 'Final ('From sort b) a = Item 'Final sort a
   Item 'Input _sort a = Maybe a
   Item 'Final 'Required a = a
   Item 'Final 'Optional a = Maybe a
 
 type FinalConfig = Config 'Final
 
-data Config purpose
-  = Config
-      { herculesApiBaseURL :: Item purpose 'Required Text,
-        nixUserIsTrusted :: Item purpose 'Required Bool,
-        concurrentTasks :: Item purpose 'Required Integer,
-        baseDirectory :: Item purpose 'Required FilePath,
-        -- | Read-only
-        staticSecretsDirectory :: Item purpose 'Required FilePath,
-        workDirectory :: Item purpose 'Required FilePath,
-        clusterJoinTokenPath :: Item purpose 'Required FilePath,
-        binaryCachesPath :: Item purpose 'Required FilePath,
-        logLevel :: Item purpose 'Required Severity
-      }
+data Config purpose = Config
+  { herculesApiBaseURL :: Item purpose 'Required Text,
+    nixUserIsTrusted :: Item purpose 'Required Bool,
+    concurrentTasks :: Item purpose ('From 'Required (Either () Int)) Int,
+    baseDirectory :: Item purpose 'Required FilePath,
+    -- | Read-only
+    staticSecretsDirectory :: Item purpose 'Required FilePath,
+    workDirectory :: Item purpose 'Required FilePath,
+    clusterJoinTokenPath :: Item purpose 'Required FilePath,
+    binaryCachesPath :: Item purpose 'Required FilePath,
+    logLevel :: Item purpose 'Required Severity
+  }
   deriving (Generic)
 
 deriving instance Show (Config 'Final)
@@ -61,7 +63,10 @@
     .= herculesApiBaseURL
     <*> dioptional (Toml.bool "nixUserIsTrusted")
     .= nixUserIsTrusted
-    <*> dioptional (Toml.integer "concurrentTasks")
+    <*> dioptional
+      ( Toml.dimatch matchRight Right (Toml.int "concurrentTasks")
+          <|> Toml.dimatch matchLeft Left (Toml.textBy (\() -> "auto") isAuto "concurrentTasks")
+      )
     .= concurrentTasks
     <*> dioptional (Toml.string keyBaseDirectory)
     .= baseDirectory
@@ -76,6 +81,18 @@
     <*> dioptional (Toml.enumBounded "logLevel")
     .= logLevel
 
+matchLeft :: Either a b -> Maybe a
+matchLeft (Left a) = Just a
+matchLeft _ = Nothing
+
+matchRight :: Either a1 a2 -> Maybe a2
+matchRight (Right a) = Just a
+matchRight _ = Nothing
+
+isAuto :: Text -> Either Text ()
+isAuto "auto" = Right ()
+isAuto _ = Left "The only permissible string value is \"auto\""
+
 keyClusterJoinTokenPath :: Key
 keyClusterJoinTokenPath = "clusterJoinTokenPath"
 
@@ -91,9 +108,6 @@
 defaultApiBaseUrl :: Text
 defaultApiBaseUrl = "https://hercules-ci.com"
 
-defaultConcurrentTasks :: Integer
-defaultConcurrentTasks = 4
-
 readConfig :: ConfigPath -> IO (Config 'Input)
 readConfig loc = case loc of
   TomlPath fp -> Toml.decodeFile tomlCodec (toS fp)
@@ -116,20 +130,27 @@
           (binaryCachesPath input)
       workDir = fromMaybe (baseDir </> "work") (workDirectory input)
   dabu <- determineDefaultApiBaseUrl
-  let rawConcurrentTasks = fromMaybe defaultConcurrentTasks $ concurrentTasks input
+  numProc <- getNumProcessors
+  let -- make sure the default is at least two, for ifd
+      autoConcurrentTasks = max 2 numProc
+      configuredConcurrentTasks = case concurrentTasks input of
+        Nothing -> autoConcurrentTasks
+        Just (Left _auto) -> autoConcurrentTasks
+        Just (Right n) -> fromIntegral n
   validConcurrentTasks <-
-    case rawConcurrentTasks of
-      x | not (x >= 1) -> throwIO $ FatalError "concurrentTasks must be at least 1"
-      x -> pure x
+    case configuredConcurrentTasks of
+      x | x >= 1 -> pure x
+      _ -> throwIO $ FatalError "concurrentTasks must be at least 1"
   let apiBaseUrl = fromMaybe dabu $ herculesApiBaseURL input
-  pure Config
-    { herculesApiBaseURL = apiBaseUrl,
-      nixUserIsTrusted = fromMaybe False $ nixUserIsTrusted input,
-      binaryCachesPath = binaryCachesP,
-      clusterJoinTokenPath = clusterJoinTokenP,
-      concurrentTasks = validConcurrentTasks,
-      baseDirectory = baseDir,
-      staticSecretsDirectory = staticSecretsDir,
-      workDirectory = workDir,
-      logLevel = logLevel input & fromMaybe InfoS
-    }
+  pure
+    Config
+      { herculesApiBaseURL = apiBaseUrl,
+        nixUserIsTrusted = fromMaybe False $ nixUserIsTrusted input,
+        binaryCachesPath = binaryCachesP,
+        clusterJoinTokenPath = clusterJoinTokenP,
+        concurrentTasks = validConcurrentTasks,
+        baseDirectory = baseDir,
+        staticSecretsDirectory = staticSecretsDir,
+        workDirectory = workDir,
+        logLevel = logLevel input & fromMaybe InfoS
+      }
diff --git a/hercules-ci-agent/Hercules/Agent/Config/BinaryCaches.hs b/hercules-ci-agent/Hercules/Agent/Config/BinaryCaches.hs
--- a/hercules-ci-agent/Hercules/Agent/Config/BinaryCaches.hs
+++ b/hercules-ci-agent/Hercules/Agent/Config/BinaryCaches.hs
@@ -20,12 +20,11 @@
 import Protolude hiding (catchJust)
 import System.IO.Error (isDoesNotExistError)
 
-data BinaryCaches
-  = BinaryCaches
-      { cachixCaches :: Map Text CachixCache,
-        nixCaches :: Map Text NixCache,
-        unknownKinds :: Map Text UnknownKind
-      }
+data BinaryCaches = BinaryCaches
+  { cachixCaches :: Map Text CachixCache,
+    nixCaches :: Map Text NixCache,
+    unknownKinds :: Map Text UnknownKind
+  }
 
 data UnknownKind = UnknownKind {kind :: Text}
   deriving (Generic, FromJSON)
@@ -35,8 +34,8 @@
   parseJSON =
     parseBag
       ( BinaryCaches
-          <$> part (\_name -> whenKind "CachixCache" $ \v -> Just $ (parseJSON v))
-          <*> part (\_name -> whenKind "NixCache" $ \v -> Just $ (parseJSON v))
+          <$> part (\_name -> whenKind "CachixCache" $ \v -> Just $ parseJSON v)
+          <*> part (\_name -> whenKind "NixCache" $ \v -> Just $ parseJSON v)
           <*> part (\_name v -> Just $ parseJSON v)
       )
 
@@ -48,15 +47,17 @@
       (mfilter isDoesNotExistError . Just)
       (liftIO (BL.readFile (toS fname)))
       ( \_e ->
-          liftIO $ throwIO $ FatalError $
-            "The binary-caches.json file does not exist.\
-            \\nAccording to the configuration, it should be in\
-            \\n  "
-              <> toS fname
-              <> "\
-                 \\n\
-                 \\nFor more information about binary-caches.json, see\n\
-                 \\nhttps://docs.hercules-ci.com/hercules-ci/reference/binary-caches-json/"
+          liftIO $
+            throwIO $
+              FatalError $
+                "The binary-caches.json file does not exist.\
+                \\nAccording to the configuration, it should be in\
+                \\n  "
+                  <> toS fname
+                  <> "\
+                     \\n\
+                     \\nFor more information about binary-caches.json, see\n\
+                     \\nhttps://docs.hercules-ci.com/hercules-ci/reference/binary-caches-json/"
       )
   bcs <- escalateAs (FatalError . toS) $ eitherDecode bytes
   validate (toS fname) bcs
diff --git a/hercules-ci-agent/Hercules/Agent/Effect.hs b/hercules-ci-agent/Hercules/Agent/Effect.hs
new file mode 100644
--- /dev/null
+++ b/hercules-ci-agent/Hercules/Agent/Effect.hs
@@ -0,0 +1,91 @@
+module Hercules.Agent.Effect where
+
+import Data.IORef
+import qualified Hercules.API.Agent.Effect.EffectTask as EffectTask
+import Hercules.API.TaskStatus (TaskStatus)
+import qualified Hercules.API.TaskStatus as TaskStatus
+import qualified Hercules.Agent.Config as Config
+import Hercules.Agent.Env hiding (config)
+import qualified Hercules.Agent.Env as Env
+import Hercules.Agent.Files
+import Hercules.Agent.Log
+import qualified Hercules.Agent.Nix as Nix
+import Hercules.Agent.Sensitive (Sensitive (Sensitive))
+import qualified Hercules.Agent.ServiceInfo as ServiceInfo
+import Hercules.Agent.WorkerProcess
+import qualified Hercules.Agent.WorkerProcess as WorkerProcess
+import qualified Hercules.Agent.WorkerProtocol.Command as Command
+import qualified Hercules.Agent.WorkerProtocol.Command.Effect as Command.Effect
+import qualified Hercules.Agent.WorkerProtocol.Event as Event
+import qualified Hercules.Agent.WorkerProtocol.LogSettings as LogSettings
+import qualified Network.URI
+import Protolude
+import System.FilePath ((</>))
+import qualified System.Posix.Signals as PS
+import System.Process
+
+performEffect :: EffectTask.EffectTask -> App TaskStatus
+performEffect effectTask = withWorkDir "effect" $ \workDir -> do
+  workerExe <- getWorkerExe
+  commandChan <- liftIO newChan
+  extraNixOptions <- Nix.askExtraOptions
+  workerEnv <- liftIO $ WorkerProcess.prepareEnv (WorkerProcess.WorkerEnvSettings {nixPath = mempty})
+  effectResult <- liftIO $ newIORef Nothing
+  let opts = [show extraNixOptions]
+      procSpec =
+        (System.Process.proc workerExe opts)
+          { env = Just workerEnv,
+            close_fds = True,
+            cwd = Just workDir
+          }
+      writeEvent :: Event.Event -> App ()
+      writeEvent event = case event of
+        Event.EffectResult e -> do
+          liftIO $ writeIORef effectResult (Just e)
+        Event.Exception e -> do
+          panic e
+        _ -> pass
+  config <- asks Env.config
+  let materialize = not (Config.nixUserIsTrusted config)
+  baseURL <- asks (ServiceInfo.bulkSocketBaseURL . Env.serviceInfo)
+  liftIO $
+    writeChan commandChan $
+      Just $
+        Command.Effect $
+          Command.Effect.Effect
+            { drvPath = EffectTask.derivationPath effectTask,
+              inputDerivationOutputPaths = encodeUtf8 <$> EffectTask.inputDerivationOutputPaths effectTask,
+              logSettings =
+                LogSettings.LogSettings
+                  { token = Sensitive $ EffectTask.logToken effectTask,
+                    path = "/api/v1/logs/build/socket",
+                    baseURL = toS $ Network.URI.uriToString identity baseURL ""
+                  },
+              materializeDerivation = materialize,
+              secretsPath = toS $ Config.staticSecretsDirectory config </> "secrets.json",
+              token = Sensitive (EffectTask.token effectTask),
+              apiBaseURL = Config.herculesApiBaseURL config
+            }
+  exitCode <- runWorker procSpec (stderrLineHandler "Effect worker") commandChan writeEvent
+  logLocM DebugS $ "Worker exit: " <> logStr (show exitCode :: Text)
+  let showSig n | n == PS.sigABRT = " (Aborted)"
+      showSig n | n == PS.sigBUS = " (Bus)"
+      showSig n | n == PS.sigCHLD = " (Child)"
+      showSig n | n == PS.sigFPE = " (Floating point exception)"
+      showSig n | n == PS.sigHUP = " (Hangup)"
+      showSig n | n == PS.sigILL = " (Illegal instruction)"
+      showSig n | n == PS.sigINT = " (Interrupted)"
+      showSig n | n == PS.sigKILL = " (Killed)"
+      showSig n | n == PS.sigPIPE = " (Broken pipe)"
+      showSig n | n == PS.sigQUIT = " (Quit)"
+      showSig n | n == PS.sigSEGV = " (Segmentation fault)"
+      showSig n | n == PS.sigTERM = " (Terminated)"
+      showSig _ = ""
+  case exitCode of
+    ExitSuccess -> pass
+    ExitFailure n -> panic $ "Effect worker failed with exit code " <> show n <> showSig (negate $ fromIntegral n)
+  liftIO (readIORef effectResult) >>= \case
+    Nothing -> pure $ TaskStatus.Exceptional "Effect worker terminated without reporting status"
+    Just 0 -> pure $ TaskStatus.Successful ()
+    Just n | n > 0 -> pure $ TaskStatus.Terminated ()
+    Just n -> pure $ TaskStatus.Exceptional $ "Effect process exited with status code " <> show n <> showSig (negate $ fromIntegral n)
diff --git a/hercules-ci-agent/Hercules/Agent/Env.hs b/hercules-ci-agent/Hercules/Agent/Env.hs
--- a/hercules-ci-agent/Hercules/Agent/Env.hs
+++ b/hercules-ci-agent/Hercules/Agent/Env.hs
@@ -26,33 +26,32 @@
 import qualified Network.HTTP.Client
 import Protolude
 import qualified Servant.Auth.Client
-import qualified Servant.Client
+import qualified Servant.Client.Streaming
 
-data Env
-  = Env
-      { manager :: Network.HTTP.Client.Manager,
-        config :: FinalConfig,
-        herculesBaseUrl :: Servant.Client.BaseUrl,
-        herculesClientEnv :: Servant.Client.ClientEnv,
-        serviceInfo :: ServiceInfo.Env,
-        -- TODO: The implicit limitation here is that we can
-        --       only have one token at a time. I wouldn't be surprised if this becomes
-        --       problematic at some point. Perhaps we should switch to a polymorphic
-        --       reader monad like RIO when we hit that limitation.
-        currentToken :: Servant.Auth.Client.Token,
-        binaryCaches :: Config.BinaryCaches.BinaryCaches,
-        cachixEnv :: Cachix.Env,
-        nixEnv :: Nix.Env,
-        socket :: AgentSocket,
-        -- katip
-        kNamespace :: K.Namespace,
-        kContext :: K.LogContexts,
-        kLogEnv :: K.LogEnv
-      }
+data Env = Env
+  { manager :: Network.HTTP.Client.Manager,
+    config :: FinalConfig,
+    herculesBaseUrl :: Servant.Client.Streaming.BaseUrl,
+    herculesClientEnv :: Servant.Client.Streaming.ClientEnv,
+    serviceInfo :: ServiceInfo.Env,
+    -- TODO: The implicit limitation here is that we can
+    --       only have one token at a time. I wouldn't be surprised if this becomes
+    --       problematic at some point. Perhaps we should switch to a polymorphic
+    --       reader monad like RIO when we hit that limitation.
+    currentToken :: Servant.Auth.Client.Token,
+    binaryCaches :: Config.BinaryCaches.BinaryCaches,
+    cachixEnv :: Cachix.Env,
+    nixEnv :: Nix.Env,
+    socket :: AgentSocket,
+    -- katip
+    kNamespace :: K.Namespace,
+    kContext :: K.LogContexts,
+    kLogEnv :: K.LogEnv
+  }
 
 activePushCaches :: App [Text]
 activePushCaches = do
-  bc <- asks (binaryCaches)
+  bc <- asks binaryCaches
   pure $
     M.keys
       ( void (Config.BinaryCaches.cachixCaches bc)
@@ -71,25 +70,24 @@
 runApp env (App m) = runReaderT m env
 
 runHerculesClient ::
-  (Servant.Auth.Client.Token -> Servant.Client.ClientM a) ->
+  NFData a =>
+  (Servant.Auth.Client.Token -> Servant.Client.Streaming.ClientM a) ->
   App a
 runHerculesClient f = do
   tok <- asks currentToken
   runHerculesClient' (f tok)
 
-runHerculesClient' :: Servant.Client.ClientM a -> App a
+runHerculesClient' :: NFData a => Servant.Client.Streaming.ClientM a -> App a
 runHerculesClient' m = do
   clientEnv <- asks herculesClientEnv
-  escalate =<< liftIO (Servant.Client.runClientM m clientEnv)
+  escalate =<< liftIO (Servant.Client.Streaming.runClientM m clientEnv)
 
 instance K.Katip App where
-
   getLogEnv = asks kLogEnv
 
   localLogEnv f (App m) = App (local (\s -> s {kLogEnv = f (kLogEnv s)}) m)
 
 instance K.KatipContext App where
-
   getKatipContext = asks kContext
 
   localKatipContext f (App m) = App (local (\s -> s {kContext = f (kContext s)}) m)
diff --git a/hercules-ci-agent/Hercules/Agent/EnvironmentInfo.hs b/hercules-ci-agent/Hercules/Agent/EnvironmentInfo.hs
--- a/hercules-ci-agent/Hercules/Agent/EnvironmentInfo.hs
+++ b/hercules-ci-agent/Hercules/Agent/EnvironmentInfo.hs
@@ -1,19 +1,18 @@
 module Hercules.Agent.EnvironmentInfo where
 
 import Control.Lens
-  ( (^..),
+  ( to,
+    (^..),
     (^?),
-    to,
   )
 import qualified Data.Aeson as Aeson
 import Data.Aeson.Lens
-  ( _Array,
+  ( key,
+    _Array,
     _Number,
     _String,
-    key,
   )
 import qualified Data.ByteString.Lazy as LBS
-import Data.Char (isSpace)
 import qualified Data.Text as T
 import qualified Hercules.API.Agent.LifeCycle.AgentInfo as AgentInfo
 import Hercules.Agent.CabalInfo as CabalInfo
@@ -32,29 +31,30 @@
   cachixPushCaches <- Cachix.Info.activePushCaches
   pushCaches <- Env.activePushCaches
   concurrentTasks <- asks (Config.concurrentTasks . Env.config)
-  let s = AgentInfo.AgentInfo
-        { hostname = toS hostname,
-          agentVersion = CabalInfo.herculesAgentVersion, -- TODO: Add git revision
-          nixVersion = nixExeVersion nix,
-          platforms = nixPlatforms nix,
-          cachixPushCaches = cachixPushCaches,
-          pushCaches = pushCaches,
-          systemFeatures = nixSystemFeatures nix,
-          substituters = nixSubstituters nix, -- TODO: Add cachix substituters
-          concurrentTasks = fromIntegral concurrentTasks
-        }
+  let s =
+        AgentInfo.AgentInfo
+          { hostname = toS hostname,
+            agentVersion = CabalInfo.herculesAgentVersion, -- TODO: Add git revision
+            nixVersion = nixExeVersion nix,
+            platforms = nixPlatforms nix,
+            cachixPushCaches = cachixPushCaches,
+            pushCaches = pushCaches,
+            systemFeatures = nixSystemFeatures nix,
+            substituters = nixSubstituters nix, -- TODO: Add cachix substituters
+            concurrentTasks = fromIntegral concurrentTasks
+          }
   logLocM DebugS $ "Determined environment info: " <> logStr (show s :: Text)
   pure s
 
-data NixInfo
-  = NixInfo
-      { nixExeVersion :: Text,
-        nixPlatforms :: [Text],
-        nixSystemFeatures :: [Text],
-        nixSubstituters :: [Text],
-        nixTrustedPublicKeys :: [Text],
-        nixNarinfoCacheNegativeTTL :: Maybe Integer
-      }
+data NixInfo = NixInfo
+  { nixExeVersion :: Text,
+    nixPlatforms :: [Text],
+    nixSystemFeatures :: [Text],
+    nixSubstituters :: [Text],
+    nixTrustedPublicKeys :: [Text],
+    nixNarinfoCacheNegativeTTL :: Maybe Integer,
+    nixNetrcFile :: Maybe Text
+  }
 
 getNixInfo :: IO NixInfo
 getNixInfo = do
@@ -62,50 +62,56 @@
   version <- Process.readProcess "nix" ["--version"] stdinEmpty
   rawJson <- Process.readProcess "nix" ["show-config", "--json"] stdinEmpty
   cfg <-
-    case Aeson.eitherDecode (LBS.fromStrict $ encodeUtf8 $ toS $ rawJson) of
+    case Aeson.eitherDecode (LBS.fromStrict $ encodeUtf8 $ toS rawJson) of
       Left e -> panic $ "Could not parse nix show-config --json: " <> show e
       Right r -> pure r
-  pure NixInfo
-    { nixExeVersion = T.dropAround isSpace (toS version),
-      nixPlatforms =
-        ((cfg :: Aeson.Value) ^.. key "system" . key "value" . _String)
-          <> ( cfg
-                 ^.. key "extra-platforms"
-                 . key "value"
-                 . _Array
-                 . traverse
-                 . _String
-             ),
-      nixSystemFeatures =
-        cfg
-          ^.. key "system-features"
-          . key "value"
-          . _Array
-          . traverse
-          . _String,
-      nixSubstituters =
-        cfg
-          ^.. key "substituters"
-          . key "value"
-          . _Array
-          . traverse
-          . _String
-          . to cleanUrl,
-      nixTrustedPublicKeys =
-        cfg
-          ^.. key "trusted-public-keys"
-          . key "value"
-          . _Array
-          . traverse
-          . _String
-          . to cleanUrl,
-      nixNarinfoCacheNegativeTTL =
-        cfg
-          ^? key "narinfo-cache-negative-ttl"
-          . key "value"
-          . _Number
-          . to floor
-    }
+  pure
+    NixInfo
+      { nixExeVersion = T.dropAround isSpace (toS version),
+        nixPlatforms =
+          ((cfg :: Aeson.Value) ^.. key "system" . key "value" . _String)
+            <> ( cfg
+                   ^.. key "extra-platforms"
+                     . key "value"
+                     . _Array
+                     . traverse
+                     . _String
+               ),
+        nixSystemFeatures =
+          cfg
+            ^.. key "system-features"
+              . key "value"
+              . _Array
+              . traverse
+              . _String,
+        nixSubstituters =
+          cfg
+            ^.. key "substituters"
+              . key "value"
+              . _Array
+              . traverse
+              . _String
+              . to cleanUrl,
+        nixTrustedPublicKeys =
+          cfg
+            ^.. key "trusted-public-keys"
+              . key "value"
+              . _Array
+              . traverse
+              . _String
+              . to cleanUrl,
+        nixNarinfoCacheNegativeTTL =
+          cfg
+            ^? key "narinfo-cache-negative-ttl"
+              . key "value"
+              . _Number
+              . to floor,
+        nixNetrcFile =
+          cfg
+            ^? key "netrc-file"
+              . key "value"
+              . _String
+      }
 
 cleanUrl :: Text -> Text
 cleanUrl t | "@" `T.isInfixOf` t = "<URI censored; might contain secret>"
diff --git a/hercules-ci-agent/Hercules/Agent/Evaluate.hs b/hercules-ci-agent/Hercules/Agent/Evaluate.hs
--- a/hercules-ci-agent/Hercules/Agent/Evaluate.hs
+++ b/hercules-ci-agent/Hercules/Agent/Evaluate.hs
@@ -4,14 +4,16 @@
 
 module Hercules.Agent.Evaluate
   ( performEvaluation,
+    findNixFile,
   )
 where
 
-import CNix (withStore)
 import Conduit
 import qualified Control.Concurrent.Async.Lifted as Async.Lifted
 import Control.Concurrent.Chan.Lifted
-import Control.Monad.IO.Unlift
+import qualified Data.Aeson as A
+import qualified Data.ByteString.Lazy as BL
+import Data.Char (isAsciiLower, isAsciiUpper)
 import Data.Conduit.Process (sourceProcessWithStreams)
 import Data.IORef
   ( atomicModifyIORef,
@@ -39,19 +41,21 @@
 import Hercules.API.Servant (noContent)
 import qualified Hercules.Agent.Cache as Agent.Cache
 import qualified Hercules.Agent.Client
-import qualified Hercules.Agent.Config as Config
 import Hercules.Agent.Env
 import qualified Hercules.Agent.Env as Env
 import qualified Hercules.Agent.Evaluate.TraversalQueue as TraversalQueue
+import Hercules.Agent.Files
 import Hercules.Agent.Log
 import qualified Hercules.Agent.Nix as Nix
 import Hercules.Agent.Nix.RetrieveDerivationInfo
   ( retrieveDerivationInfo,
   )
+import Hercules.Agent.NixFile (findNixFile)
 import Hercules.Agent.NixPath
   ( renderSubPath,
   )
 import Hercules.Agent.Producer
+import Hercules.Agent.Sensitive (Sensitive (Sensitive))
 import qualified Hercules.Agent.ServiceInfo as ServiceInfo
 import Hercules.Agent.WorkerProcess ()
 import qualified Hercules.Agent.WorkerProcess as WorkerProcess
@@ -62,6 +66,7 @@
 import qualified Hercules.Agent.WorkerProtocol.Event.Attribute as WorkerAttribute
 import qualified Hercules.Agent.WorkerProtocol.Event.AttributeError as WorkerAttributeError
 import qualified Hercules.Agent.WorkerProtocol.LogSettings as LogSettings
+import Hercules.CNix (withStore)
 import Hercules.Error (defaultRetry, quickRetry)
 import qualified Network.HTTP.Client.Conduit as HTTP.Conduit
 import qualified Network.HTTP.Simple as HTTP.Simple
@@ -70,7 +75,6 @@
 import qualified Servant.Client
 import qualified System.Directory as Dir
 import System.FilePath
-import System.IO.Temp (withTempDirectory)
 import System.Process
 
 eventLimit :: Int
@@ -90,19 +94,34 @@
   EvaluateTask.EvaluateTask ->
   (Syncing EvaluateEvent.EvaluateEvent -> App ()) ->
   App ()
-produceEvaluationTaskEvents task writeToBatch = withWorkDir $ \tmpdir -> do
+produceEvaluationTaskEvents task writeToBatch = withWorkDir "eval" $ \tmpdir -> do
   logLocM DebugS "Retrieving evaluation task"
   let emitSingle = writeToBatch . Syncable
       sync = syncer writeToBatch
-  projectDir <-
-    fetchSource
-      (tmpdir </> "primary")
-      (EvaluateTask.primaryInput task)
-  withNamedContext "projectDir" projectDir $
-    logLocM DebugS "Determined projectDir"
   inputLocations <-
-    flip M.traverseWithKey (EvaluateTask.otherInputs task) $
-      \k src -> fetchSource (tmpdir </> ("arg-" <> toS k)) src
+    EvaluateTask.otherInputs task
+      & M.traverseWithKey \k src -> do
+        let fetchName = tmpdir </> ("fetch-" <> toS k)
+            argName = tmpdir </> ("arg-" <> toS k)
+            metaName = do
+              meta <- EvaluateTask.inputMetadata task & M.lookup k
+              nameValue <- meta & M.lookup "name"
+              name <- case A.fromJSON nameValue of
+                A.Success a -> pure a
+                _ -> Nothing
+              guard (isValidName name)
+              pure name
+            destName
+              | Just sourceName <- metaName = argName </> sourceName
+              | otherwise = argName
+        fetched <- fetchSource fetchName src
+        liftIO do
+          Dir.createDirectoryIfMissing True (takeDirectory destName)
+          renamePathTryHarder fetched destName
+        pure destName
+  projectDir <- case M.lookup "src" inputLocations of
+    Nothing -> panic "No primary source provided"
+    Just x -> pure x
   nixPath <-
     EvaluateTask.nixPath task
       & ( traverse
@@ -111,10 +130,10 @@
             $ \identifier -> case M.lookup identifier inputLocations of
               Just x -> pure x
               Nothing ->
-                throwIO
-                  $ FatalError
-                  $ "Nix path references undefined input "
-                    <> identifier
+                throwIO $
+                  FatalError $
+                    "Nix path references undefined input "
+                      <> identifier
         )
   autoArguments' <-
     EvaluateTask.autoArguments task
@@ -122,20 +141,30 @@
         ( \identifier -> case M.lookup identifier inputLocations of
             Just x | "/" `isPrefixOf` x -> pure x
             Just x ->
-              throwIO
-                $ FatalError
-                $ "input "
-                  <> identifier
-                  <> " was not resolved to an absolute path: "
-                  <> toS x
+              throwIO $
+                FatalError $
+                  "input "
+                    <> identifier
+                    <> " was not resolved to an absolute path: "
+                    <> toS x
             Nothing ->
-              throwIO
-                $ FatalError
-                $ "auto argument references undefined input "
-                  <> identifier
+              throwIO $
+                FatalError $
+                  "auto argument references undefined input "
+                    <> identifier
         )
-  let autoArguments = autoArguments'
-        <&> \sp -> Eval.ExprArg $ encodeUtf8 $ renderSubPath $ toS <$> sp
+  let autoArguments =
+        autoArguments'
+          & M.mapWithKey \k sp ->
+            let argPath = encodeUtf8 $ renderSubPath $ toS <$> sp
+             in case do
+                  inputId <- EvaluateTask.autoArguments task & M.lookup k
+                  EvaluateTask.inputMetadata task & M.lookup (EvaluateTask.path inputId) of
+                  Nothing -> Eval.ExprArg argPath
+                  Just attrs ->
+                    Eval.ExprArg $
+                      -- TODO pass directly to avoid having to escape (or just escape properly)
+                      "builtins.fromJSON ''" <> BL.toStrict (A.encode attrs) <> "'' // { outPath = " <> argPath <> "; }"
   msgCounter <- liftIO $ newIORef 0
   let fixIndex ::
         MonadIO m =>
@@ -154,14 +183,15 @@
           then do
             truncMsg <-
               fixIndex $
-                EvaluateEvent.Message Message.Message
-                  { index = -1,
-                    typ = Message.Error,
-                    message =
-                      "Evaluation limit reached. Does your nix expression produce infinite attributes? Please make sure that your project is finite. If it really does require more than "
-                        <> show eventLimit
-                        <> " attributes or messages, please contact info@hercules-ci.com."
-                  }
+                EvaluateEvent.Message
+                  Message.Message
+                    { index = -1,
+                      typ = Message.Error,
+                      message =
+                        "Evaluation limit reached. Does your nix expression produce infinite attributes? Please make sure that your project is finite. If it really does require more than "
+                          <> show eventLimit
+                          <> " attributes or messages, please contact info@hercules-ci.com."
+                    }
             emitSingle truncMsg
             panic "Evaluation limit reached."
           else emitSingle =<< fixIndex update
@@ -170,11 +200,12 @@
   liftIO (findNixFile projectDir) >>= \case
     Left e ->
       emit $
-        EvaluateEvent.Message Message.Message
-          { Message.index = -1, -- will be set by emit
-            Message.typ = Message.Error,
-            Message.message = e
-          }
+        EvaluateEvent.Message
+          Message.Message
+            { Message.index = -1, -- will be set by emit
+              Message.typ = Message.Error,
+              Message.message = e
+            }
     Right file -> TraversalQueue.with $ \derivationQueue ->
       let doIt = do
             Async.Lifted.concurrently_ evaluation emitDrvs
@@ -223,6 +254,16 @@
               emit $ EvaluateEvent.DerivationInfo drvInfo
        in doIt
 
+isValidName :: FilePath -> Bool
+isValidName "" = False
+isValidName cs@(c0 : _) = all isValidNameChar cs && c0 /= '.'
+  where
+    isValidNameChar c =
+      isAsciiUpper c
+        || isAsciiLower c
+        || isDigit c
+        || c `elem` ("+-._?=" :: [Char])
+
 runEvalProcess ::
   FilePath ->
   FilePath ->
@@ -239,17 +280,19 @@
 runEvalProcess projectDir file autoArguments nixPath emit uploadDerivationInfos flush logToken = do
   extraOpts <- Nix.askExtraOptions
   baseURL <- asks (ServiceInfo.bulkSocketBaseURL . Env.serviceInfo)
-  let eval = Eval.Eval
-        { Eval.cwd = projectDir,
-          Eval.file = toS file,
-          Eval.autoArguments = autoArguments,
-          Eval.extraNixOptions = extraOpts,
-          Eval.logSettings = LogSettings.LogSettings
-            { token = LogSettings.Sensitive logToken,
-              path = "/api/v1/logs/build/socket",
-              baseURL = toS $ Network.URI.uriToString identity baseURL ""
-            }
-        }
+  let eval =
+        Eval.Eval
+          { Eval.cwd = projectDir,
+            Eval.file = toS file,
+            Eval.autoArguments = autoArguments,
+            Eval.extraNixOptions = extraOpts,
+            Eval.logSettings =
+              LogSettings.LogSettings
+                { token = Sensitive logToken,
+                  path = "/api/v1/logs/build/socket",
+                  baseURL = toS $ Network.URI.uriToString identity baseURL ""
+                }
+          }
   buildRequiredIndex <- liftIO $ newIORef (0 :: Int)
   commandChan <- newChan
   writeChan commandChan $ Just $ Command.Eval eval
@@ -261,39 +304,51 @@
           workerEventsP
           ( \case
               Event.Attribute a -> do
-                emit $ EvaluateEvent.Attribute $ AttributeEvent.AttributeEvent
-                  { AttributeEvent.expressionPath = decode <$> WorkerAttribute.path a,
-                    AttributeEvent.derivationPath = decode $ WorkerAttribute.drv a
-                  }
+                emit $
+                  EvaluateEvent.Attribute $
+                    AttributeEvent.AttributeEvent
+                      { AttributeEvent.expressionPath = decode <$> WorkerAttribute.path a,
+                        AttributeEvent.derivationPath = decode $ WorkerAttribute.drv a,
+                        AttributeEvent.typ = case WorkerAttribute.typ a of
+                          WorkerAttribute.Regular -> AttributeEvent.Regular
+                          WorkerAttribute.MustFail -> AttributeEvent.MustFail
+                          WorkerAttribute.MayFail -> AttributeEvent.MayFail
+                          WorkerAttribute.Effect -> AttributeEvent.Effect
+                          WorkerAttribute.DependenciesOnly -> AttributeEvent.DependenciesOnly
+                      }
                 continue
               Event.AttributeError e -> do
-                emit $ EvaluateEvent.AttributeError $ AttributeErrorEvent.AttributeErrorEvent
-                  { AttributeErrorEvent.expressionPath = decode <$> WorkerAttributeError.path e,
-                    AttributeErrorEvent.errorMessage = WorkerAttributeError.message e,
-                    AttributeErrorEvent.errorType = WorkerAttributeError.errorType e,
-                    AttributeErrorEvent.errorDerivation = WorkerAttributeError.errorDerivation e
-                  }
+                emit $
+                  EvaluateEvent.AttributeError $
+                    AttributeErrorEvent.AttributeErrorEvent
+                      { AttributeErrorEvent.expressionPath = decode <$> WorkerAttributeError.path e,
+                        AttributeErrorEvent.errorMessage = WorkerAttributeError.message e,
+                        AttributeErrorEvent.errorType = WorkerAttributeError.errorType e,
+                        AttributeErrorEvent.errorDerivation = WorkerAttributeError.errorDerivation e
+                      }
                 continue
               Event.EvaluationDone ->
                 writeChan commandChan Nothing
               Event.Error e -> do
                 emit $
-                  EvaluateEvent.Message Message.Message
-                    { Message.index = -1, -- will be set by emit
-                      Message.typ = Message.Error,
-                      Message.message = e
-                    }
+                  EvaluateEvent.Message
+                    Message.Message
+                      { Message.index = -1, -- will be set by emit
+                        Message.typ = Message.Error,
+                        Message.message = e
+                      }
                 continue
               Event.Build drv outputName notAttempt -> do
                 status <-
                   withNamedContext "derivation" drv $ do
                     currentIndex <- liftIO $ atomicModifyIORef buildRequiredIndex (\i -> (i + 1, i))
                     emit $
-                      EvaluateEvent.BuildRequired BuildRequired.BuildRequired
-                        { BuildRequired.derivationPath = drv,
-                          BuildRequired.index = currentIndex,
-                          BuildRequired.outputName = outputName
-                        }
+                      EvaluateEvent.BuildRequired
+                        BuildRequired.BuildRequired
+                          { BuildRequired.derivationPath = drv,
+                            BuildRequired.index = currentIndex,
+                            BuildRequired.outputName = outputName
+                          }
                     let pushDerivations = do
                           caches <- activePushCaches
                           forM_ caches $ \cache -> do
@@ -302,19 +357,22 @@
                     Async.Lifted.concurrently_
                       (uploadDerivationInfos drv)
                       pushDerivations
-                    emit $ EvaluateEvent.BuildRequest BuildRequest.BuildRequest
-                      { derivationPath = drv,
-                        forceRebuild = isJust notAttempt
-                      }
+                    emit $
+                      EvaluateEvent.BuildRequest
+                        BuildRequest.BuildRequest
+                          { derivationPath = drv,
+                            forceRebuild = isJust notAttempt
+                          }
                     flush
                     status <- drvPoller notAttempt drv
                     logLocM DebugS $ "Got derivation status " <> logStr (show status :: Text)
                     return status
                 writeChan commandChan $ Just $ Command.BuildResult $ uncurry (BuildResult.BuildResult drv) status
                 continue
+              Event.Exception e -> panic e
               -- Unused during eval
               Event.BuildResult {} -> pass
-              Event.Exception e -> panic e
+              Event.EffectResult {} -> pass
           )
           ( \case
               ExitSuccess -> logLocM DebugS "Clean worker exit"
@@ -335,22 +393,22 @@
   -- NiceToHave: replace renderNixPath by something structured like -I
   -- to support = and : in paths
   workerEnv <- liftIO $ WorkerProcess.prepareEnv (WorkerProcess.WorkerEnvSettings {nixPath = nixPath})
-  wps <-
-    pure
-      (System.Process.proc workerExe opts)
-        { env = Just workerEnv,
-          close_fds = True, -- Disable on Windows?
-          cwd = Just (Eval.cwd eval)
-        }
+  let wps =
+        (System.Process.proc workerExe opts)
+          { env = Just workerEnv,
+            close_fds = True, -- Disable on Windows?
+            cwd = Just (Eval.cwd eval)
+          }
   WorkerProcess.runWorker wps (stderrLineHandler "evaluator") commandChan writeEvent
 
 drvPoller :: Maybe UUID -> Text -> App (UUID, BuildResult.BuildStatus)
 drvPoller notAttempt drvPath = do
   resp <-
-    defaultRetry $ runHerculesClient $
-      getDerivationStatus2
-        Hercules.Agent.Client.evalClient
-        drvPath
+    defaultRetry $
+      runHerculesClient $
+        getDerivationStatus2
+          Hercules.Agent.Client.evalClient
+          drvPath
   let oneSecond = 1000 * 1000
       again = do
         liftIO $ threadDelay oneSecond
@@ -377,18 +435,18 @@
   -- Fewer retries in order to speed up the tests.
   quickRetry $ do
     (x, _, _) <-
-      liftIO
-        $ (`runReaderT` Servant.Client.manager clientEnv)
-        $ HTTP.Conduit.withResponse request
-        $ \response -> do
-          let tarball = HTTP.Conduit.responseBody response
-              procSpec =
-                (System.Process.proc "tar" ["-xz"]) {cwd = Just targetDir}
-          sourceProcessWithStreams
-            procSpec
-            tarball
-            Conduit.stderrC
-            Conduit.stderrC
+      liftIO $
+        (`runReaderT` Servant.Client.manager clientEnv) $
+          HTTP.Conduit.withResponse request $
+            \response -> do
+              let tarball = HTTP.Conduit.responseBody response
+                  procSpec =
+                    (System.Process.proc "tar" ["-xz"]) {cwd = Just targetDir}
+              sourceProcessWithStreams
+                procSpec
+                tarball
+                Conduit.stderrC
+                Conduit.stderrC
     case x of
       ExitSuccess -> pass
       ExitFailure {} -> throwIO $ SubprocessFailure "Extracting tarball"
@@ -402,64 +460,19 @@
 findTarballDir fp = do
   nodes <- Dir.listDirectory fp
   case nodes of
-    [x] -> Dir.doesDirectoryExist (fp </> x) >>= \case
-      True -> pure $ fp </> x
-      False -> pure fp
+    [x] ->
+      Dir.doesDirectoryExist (fp </> x) >>= \case
+        True -> pure $ fp </> x
+        False -> pure fp
     _ -> pure fp
 
-type Ambiguity = [FilePath]
-
-searchPath :: [Ambiguity]
-searchPath = [["nix/ci.nix", "ci.nix"], ["default.nix"]]
-
-findNixFile :: FilePath -> IO (Either Text FilePath)
-findNixFile projectDir = do
-  searchResult <-
-    for searchPath $ traverse $ \relPath ->
-      let path = projectDir </> relPath
-       in Dir.doesFileExist path >>= \case
-            True -> pure $ Just (relPath, path)
-            False -> pure Nothing
-  case filter (not . null) $ map catMaybes searchResult of
-    [(_relPath, unambiguous)] : _ -> pure (pure unambiguous)
-    ambiguous : _ ->
-      pure
-        $ Left
-        $ "Don't know what to do, expecting only one of "
-          <> englishConjunction "or" (map fst ambiguous)
-    [] ->
-      pure
-        $ Left
-        $ "Please provide a Nix expression to build. Could not find any of "
-          <> englishConjunction "or" (concat searchPath)
-          <> " in your source"
-
-englishConjunction :: Show a => Text -> [a] -> Text
-englishConjunction _ [] = "none"
-englishConjunction _ [a] = show a
-englishConjunction connective [a1, a2] =
-  show a1 <> " " <> connective <> " " <> show a2
-englishConjunction connective (a : as) =
-  show a <> ", " <> englishConjunction connective as
-
 postBatch :: EvaluateTask.EvaluateTask -> [EvaluateEvent.EvaluateEvent] -> App ()
 postBatch task events =
-  noContent $ defaultRetry $
-    runHerculesClient
-      ( tasksUpdateEvaluation
-          Hercules.Agent.Client.evalClient
-          (EvaluateTask.id task)
-          events
-      )
-
--- TODO: configurable temp directory
-withWorkDir :: (FilePath -> App b) -> App b
-withWorkDir f = do
-  UnliftIO {unliftIO = unlift} <- askUnliftIO
-  workDir <- asks (Config.workDirectory . config)
-  liftIO $ withTempDirectory workDir "eval" $ unlift . f
-
-readFileMaybe :: MonadIO m => FilePath -> m (Maybe Text)
-readFileMaybe fp = liftIO do
-  exists <- Dir.doesFileExist fp
-  guard exists & traverse \_ -> readFile fp
+  noContent $
+    defaultRetry $
+      runHerculesClient
+        ( tasksUpdateEvaluation
+            Hercules.Agent.Client.evalClient
+            (EvaluateTask.id task)
+            events
+        )
diff --git a/hercules-ci-agent/Hercules/Agent/Evaluate/TraversalQueue.hs b/hercules-ci-agent/Hercules/Agent/Evaluate/TraversalQueue.hs
--- a/hercules-ci-agent/Hercules/Agent/Evaluate/TraversalQueue.hs
+++ b/hercules-ci-agent/Hercules/Agent/Evaluate/TraversalQueue.hs
@@ -29,13 +29,12 @@
     writeChan,
   )
 
-data Queue a
-  = Queue
-      { chan :: Chan (Maybe a),
-        visitedSet :: IORef (Set a),
-        -- | Increased on enqueue, decreased when processed
-        size :: TVar Int
-      }
+data Queue a = Queue
+  { chan :: Chan (Maybe a),
+    visitedSet :: IORef (Set a),
+    -- | Increased on enqueue, decreased when processed
+    size :: TVar Int
+  }
 
 with :: MonadBaseControl IO m => (Queue a -> m ()) -> m ()
 with = Control.Exception.Lifted.bracket new close
@@ -54,9 +53,10 @@
   writeChan (chan env) (Just msg)
 
 waitUntilDone :: MonadBase IO m => Queue a -> m ()
-waitUntilDone env = liftBase $ atomically $ do
-  n <- readTVar (size env)
-  check (n == 0)
+waitUntilDone env = liftBase $
+  atomically $ do
+    n <- readTVar (size env)
+    check (n == 0)
 
 readJust_ ::
   (MonadBase IO m, MonadIO m) =>
diff --git a/hercules-ci-agent/Hercules/Agent/Files.hs b/hercules-ci-agent/Hercules/Agent/Files.hs
new file mode 100644
--- /dev/null
+++ b/hercules-ci-agent/Hercules/Agent/Files.hs
@@ -0,0 +1,41 @@
+{-# LANGUAGE BlockArguments #-}
+
+module Hercules.Agent.Files where
+
+import Control.Monad.IO.Unlift
+import qualified Hercules.Agent.Config as Config
+import Hercules.Agent.Env
+import Protolude
+import System.Directory
+import System.FilePath
+import System.IO.Error
+import System.IO.Temp
+
+withWorkDir :: Text -> (FilePath -> App b) -> App b
+withWorkDir hint f = do
+  UnliftIO {unliftIO = unlift} <- askUnliftIO
+  workDir <- asks (Config.workDirectory . config)
+  liftIO $ withTempDirectory workDir (toS hint) $ unlift . f
+
+readFileMaybe :: MonadIO m => FilePath -> m (Maybe Text)
+readFileMaybe fp = liftIO do
+  exists <- doesFileExist fp
+  guard exists & traverse \_ -> readFile fp
+
+renamePathTryHarder :: FilePath -> FilePath -> IO ()
+renamePathTryHarder source dest = do
+  catchJust
+    ( \case
+        e | isPermissionError e -> Just e
+        _ -> Nothing
+    )
+    doIt
+    (const tryHarder)
+  where
+    doIt = renamePath source dest
+    modifyPermissions f p = getPermissions p >>= setPermissions p . f
+    tryHarder = do
+      modifyPermissions (setOwnerWritable True) (takeDirectory source)
+      modifyPermissions (setOwnerWritable True) source
+      modifyPermissions (setOwnerWritable True) (takeDirectory dest)
+      doIt
diff --git a/hercules-ci-agent/Hercules/Agent/Init.hs b/hercules-ci-agent/Hercules/Agent/Init.hs
--- a/hercules-ci-agent/Hercules/Agent/Init.hs
+++ b/hercules-ci-agent/Hercules/Agent/Init.hs
@@ -1,16 +1,16 @@
 module Hercules.Agent.Init where
 
-import qualified CNix
 import qualified Hercules.Agent.Cachix.Init
 import qualified Hercules.Agent.Compat as Compat
 import qualified Hercules.Agent.Config as Config
 import qualified Hercules.Agent.Config.BinaryCaches as BC
-import qualified Hercules.Agent.Env as Env
 import Hercules.Agent.Env (Env (Env))
+import qualified Hercules.Agent.Env as Env
 import qualified Hercules.Agent.Nix.Init
 import qualified Hercules.Agent.SecureDirectory as SecureDirectory
 import qualified Hercules.Agent.ServiceInfo as ServiceInfo
 import qualified Hercules.Agent.Token as Token
+import qualified Hercules.CNix
 import qualified Katip as K
 import qualified Network.HTTP.Client.TLS
 import Protolude
@@ -34,21 +34,22 @@
   cachix <- withLogging $ Hercules.Agent.Cachix.Init.newEnv config (BC.cachixCaches bcs)
   nix <- Hercules.Agent.Nix.Init.newEnv
   serviceInfo <- ServiceInfo.newEnv clientEnv
-  pure Env
-    { manager = manager,
-      config = config,
-      herculesBaseUrl = baseUrl,
-      herculesClientEnv = clientEnv,
-      serviceInfo = serviceInfo,
-      currentToken = Servant.Auth.Client.Token $ encodeUtf8 token,
-      binaryCaches = bcs,
-      cachixEnv = cachix,
-      socket = panic "Socket not defined yet.", -- Hmm, needs different monad?
-      kNamespace = emptyNamespace,
-      kContext = mempty,
-      kLogEnv = logEnv,
-      nixEnv = nix
-    }
+  pure
+    Env
+      { manager = manager,
+        config = config,
+        herculesBaseUrl = baseUrl,
+        herculesClientEnv = clientEnv,
+        serviceInfo = serviceInfo,
+        currentToken = Servant.Auth.Client.Token $ encodeUtf8 token,
+        binaryCaches = bcs,
+        cachixEnv = cachix,
+        socket = panic "Socket not defined yet.", -- Hmm, needs different monad?
+        kNamespace = emptyNamespace,
+        kContext = mempty,
+        kLogEnv = logEnv,
+        nixEnv = nix
+      }
 
 setupLogging :: Config.FinalConfig -> (K.LogEnv -> IO ()) -> IO ()
 setupLogging cfg f = do
@@ -63,5 +64,5 @@
 
 initCNix :: IO ()
 initCNix = do
-  CNix.init
-  CNix.setTalkative
+  Hercules.CNix.init
+  Hercules.CNix.setTalkative
diff --git a/hercules-ci-agent/Hercules/Agent/Log.hs b/hercules-ci-agent/Hercules/Agent/Log.hs
--- a/hercules-ci-agent/Hercules/Agent/Log.hs
+++ b/hercules-ci-agent/Hercules/Agent/Log.hs
@@ -38,7 +38,7 @@
     -- "This is the lowest level function [...] useful when implementing centralised logging services."
     Katip.Core.logKatipItem (Katip.Core.SimpleLogPayload . M.toList . fmap (Katip.Core.AnyLogPayload :: A.Value -> Katip.Core.AnyLogPayload) <$> item)
 stderrLineHandler processRole pid ln =
-  withNamedContext "worker" (pid :: Int)
-    $ logLocM InfoS
-    $ logStr
-    $ processRole <> ": " <> decodeUtf8With lenientDecode ln
+  withNamedContext "worker" (pid :: Int) $
+    logLocM InfoS $
+      logStr $
+        processRole <> ": " <> decodeUtf8With lenientDecode ln
diff --git a/hercules-ci-agent/Hercules/Agent/Nix.hs b/hercules-ci-agent/Hercules/Agent/Nix.hs
--- a/hercules-ci-agent/Hercules/Agent/Nix.hs
+++ b/hercules-ci-agent/Hercules/Agent/Nix.hs
@@ -1,8 +1,13 @@
 module Hercules.Agent.Nix where
 
+import Control.Monad.Trans.Maybe
+import qualified Data.ByteString as BS
+import qualified Data.Text as T
 import Hercules.Agent.Env as Agent.Env
+import Hercules.Agent.EnvironmentInfo (getNixInfo, nixNetrcFile)
 import Hercules.Agent.Nix.Env as Nix.Env
 import Protolude
+import System.Directory (doesFileExist)
 import System.Process (readProcess)
 import qualified System.Process
 
@@ -42,3 +47,17 @@
 nixProc exe opts args = do
   extraOpts <- getExtraOptionArguments
   pure $ System.Process.proc (toS exe) (map toS (opts <> extraOpts <> ["--"] <> args))
+
+getNetrcLines :: App [Text]
+getNetrcLines = liftIO $ do
+  info <- getNixInfo
+  let fm = toS <$> nixNetrcFile info
+  fmap fold <$> runMaybeT $ do
+    f <- MaybeT $ pure fm
+    exs <- lift $ doesFileExist f
+    guard exs
+    bs <- liftIO $ BS.readFile f
+    pure $
+      bs
+        & decodeUtf8
+        & T.lines
diff --git a/hercules-ci-agent/Hercules/Agent/Nix/Env.hs b/hercules-ci-agent/Hercules/Agent/Nix/Env.hs
--- a/hercules-ci-agent/Hercules/Agent/Nix/Env.hs
+++ b/hercules-ci-agent/Hercules/Agent/Nix/Env.hs
@@ -2,7 +2,6 @@
 
 import Protolude
 
-data Env
-  = Env
-      { extraOptions :: [(Text, Text)]
-      }
+data Env = Env
+  { extraOptions :: [(Text, Text)]
+  }
diff --git a/hercules-ci-agent/Hercules/Agent/Nix/Init.hs b/hercules-ci-agent/Hercules/Agent/Nix/Init.hs
--- a/hercules-ci-agent/Hercules/Agent/Nix/Init.hs
+++ b/hercules-ci-agent/Hercules/Agent/Nix/Init.hs
@@ -28,6 +28,7 @@
     throwIO $ FatalError "Please configure your system's Nix with:  narinfo-cache-negative-ttl = 0  "
   -- Might want to take stuff from Config and put it in
   -- extraOptions here.
-  pure Env
-    { extraOptions = []
-    }
+  pure
+    Env
+      { extraOptions = []
+      }
diff --git a/hercules-ci-agent/Hercules/Agent/Nix/RetrieveDerivationInfo.hs b/hercules-ci-agent/Hercules/Agent/Nix/RetrieveDerivationInfo.hs
--- a/hercules-ci-agent/Hercules/Agent/Nix/RetrieveDerivationInfo.hs
+++ b/hercules-ci-agent/Hercules/Agent/Nix/RetrieveDerivationInfo.hs
@@ -1,7 +1,5 @@
 module Hercules.Agent.Nix.RetrieveDerivationInfo where
 
-import CNix
-import CNix.Internal.Context (Derivation)
 import qualified Data.Map as M
 import qualified Data.Text as T
 import Foreign.ForeignPtr (ForeignPtr)
@@ -9,11 +7,12 @@
   ( DerivationInfo (DerivationInfo),
   )
 import qualified Hercules.API.Agent.Evaluate.EvaluateEvent.DerivationInfo as DerivationInfo
+import Hercules.CNix
 import Protolude
 
 retrieveDerivationInfo ::
   MonadIO m =>
-  Ptr (Ref NixStore) ->
+  Store ->
   DerivationInfo.DerivationPathText ->
   m DerivationInfo
 retrieveDerivationInfo store drvPath = liftIO $ do
@@ -30,22 +29,23 @@
   let requiredSystemFeatures = maybe [] splitFeatures $ M.lookup "requiredSystemFeatures" env
       splitFeatures = filter (not . T.null) . T.split (== ' ') . decode
       decode = decodeUtf8With lenientDecode
-  pure $ DerivationInfo
-    { derivationPath = drvPath,
-      platform = decodeUtf8With lenientDecode platform,
-      requiredSystemFeatures = requiredSystemFeatures,
-      inputDerivations = inputDrvPaths & map (\(i, os) -> (decode i, map decode os)) & M.fromList,
-      inputSources = map decode sourcePaths,
-      outputs =
-        outputs
-          & map
-            ( \output ->
-                ( decode $ derivationOutputName output,
-                  DerivationInfo.OutputInfo
-                    { DerivationInfo.path = decode $ derivationOutputPath output,
-                      DerivationInfo.isFixed = derivationOutputHashAlgo output /= ""
-                    }
-                )
-            )
-          & M.fromList
-    }
+  pure $
+    DerivationInfo
+      { derivationPath = drvPath,
+        platform = decode platform,
+        requiredSystemFeatures = requiredSystemFeatures,
+        inputDerivations = inputDrvPaths & map (bimap decode (map decode)) & M.fromList,
+        inputSources = map decode sourcePaths,
+        outputs =
+          outputs
+            & map
+              ( \output ->
+                  ( decode $ derivationOutputName output,
+                    DerivationInfo.OutputInfo
+                      { DerivationInfo.path = decode $ derivationOutputPath output,
+                        DerivationInfo.isFixed = derivationOutputHashAlgo output /= ""
+                      }
+                  )
+              )
+            & M.fromList
+      }
diff --git a/hercules-ci-agent/Hercules/Agent/Options.hs b/hercules-ci-agent/Hercules/Agent/Options.hs
--- a/hercules-ci-agent/Hercules/Agent/Options.hs
+++ b/hercules-ci-agent/Hercules/Agent/Options.hs
@@ -10,11 +10,10 @@
 import Options.Applicative
 import Protolude hiding (option)
 
-data Options
-  = Options
-      { configFile :: ConfigPath,
-        mode :: Mode
-      }
+data Options = Options
+  { configFile :: ConfigPath,
+    mode :: Mode
+  }
 
 parseOptions :: Parser Options
 parseOptions =
diff --git a/hercules-ci-agent/Hercules/Agent/ServiceInfo.hs b/hercules-ci-agent/Hercules/Agent/ServiceInfo.hs
--- a/hercules-ci-agent/Hercules/Agent/ServiceInfo.hs
+++ b/hercules-ci-agent/Hercules/Agent/ServiceInfo.hs
@@ -6,27 +6,28 @@
 import Hercules.Error (escalate)
 import Network.URI
 import Protolude
-import qualified Servant.Client
+import qualified Servant.Client.Streaming
 
-data Env
-  = Env
-      { agentSocketBaseURL :: URI,
-        bulkSocketBaseURL :: URI
-      }
+data Env = Env
+  { agentSocketBaseURL :: URI,
+    bulkSocketBaseURL :: URI
+  }
 
-newEnv :: Servant.Client.ClientEnv -> IO Env
+newEnv :: Servant.Client.Streaming.ClientEnv -> IO Env
 newEnv clientEnv = do
-  serviceInfo <- escalate =<< Servant.Client.runClientM (API.LifeCycle.getServiceInfo Client.lifeCycleClient) clientEnv
+  serviceInfo <- escalate =<< Servant.Client.Streaming.runClientM (API.LifeCycle.getServiceInfo Client.lifeCycleClient) clientEnv
   let parse varName text = case parseURI (toS text) of
         Nothing -> panic $ varName <> " invalid: " <> ServiceInfo.agentSocketBaseURL serviceInfo
-        Just uri -> uri <$ do
-          case uriScheme uri of
-            "http:" -> pass
-            "https:" -> pass
-            x -> panic $ varName <> " has invalid uri scheme" <> toS x
+        Just uri ->
+          uri <$ do
+            case uriScheme uri of
+              "http:" -> pass
+              "https:" -> pass
+              x -> panic $ varName <> " has invalid uri scheme" <> toS x
   agentSocketBaseURL_ <- parse "agentSocketBaseURL" $ ServiceInfo.agentSocketBaseURL serviceInfo
   bulkSocketBaseURL_ <- parse "bulkSocketBaseURL" $ ServiceInfo.bulkSocketBaseURL serviceInfo
-  pure Env
-    { agentSocketBaseURL = agentSocketBaseURL_,
-      bulkSocketBaseURL = bulkSocketBaseURL_
-    }
+  pure
+    Env
+      { agentSocketBaseURL = agentSocketBaseURL_,
+        bulkSocketBaseURL = bulkSocketBaseURL_
+      }
diff --git a/hercules-ci-agent/Hercules/Agent/Token.hs b/hercules-ci-agent/Hercules/Agent/Token.hs
--- a/hercules-ci-agent/Hercules/Agent/Token.hs
+++ b/hercules-ci-agent/Hercules/Agent/Token.hs
@@ -2,7 +2,7 @@
 
 import Control.Lens ((^?))
 import qualified Data.Aeson as Aeson
-import Data.Aeson.Lens (_String, key)
+import Data.Aeson.Lens (key, _String)
 import qualified Data.ByteString.Base64.Lazy as B64L
 import qualified Data.ByteString.Lazy as BL
 import qualified Data.Text as T
@@ -50,26 +50,27 @@
     False -> pure Nothing
 
 ensureAgentSession :: App Text
-ensureAgentSession = readAgentSessionKey >>= \case
-  Just sessKey -> do
-    logLocM DebugS "Found agent session key"
-    let handler e = do
-          logLocM WarningS $ "Failed to check whether session token matches cluster join token. Using old session. Remove session.key if you need to force a session update. Exception: " <> logStr (displayException e)
-          pure sessKey
-    safeLiftedHandle handler $ do
-      cjt <- getClusterJoinTokenId
-      logLocM DebugS $ "Found clusterJoinTokenId " <> logStr cjt
-      scjt <- getSessionClusterJoinTokenId sessKey
-      logLocM DebugS $ "Found sessionClusterJoinTokenId " <> logStr scjt
-      if cjt == scjt
-        then pure sessKey
-        else do
-          logLocM DebugS "Getting new session key to match cluster join token..."
-          updateAgentSession
-  Nothing -> do
-    writeAgentSessionKey "x" -- Sanity check
-    logLocM DebugS "Creating agent session"
-    updateAgentSession
+ensureAgentSession =
+  readAgentSessionKey >>= \case
+    Just sessKey -> do
+      logLocM DebugS "Found agent session key"
+      let handler e = do
+            logLocM WarningS $ "Failed to check whether session token matches cluster join token. Using old session. Remove session.key if you need to force a session update. Exception: " <> logStr (displayException e)
+            pure sessKey
+      safeLiftedHandle handler $ do
+        cjt <- getClusterJoinTokenId
+        logLocM DebugS $ "Found clusterJoinTokenId " <> logStr cjt
+        scjt <- getSessionClusterJoinTokenId sessKey
+        logLocM DebugS $ "Found sessionClusterJoinTokenId " <> logStr scjt
+        if cjt == scjt
+          then pure sessKey
+          else do
+            logLocM DebugS "Getting new session key to match cluster join token..."
+            updateAgentSession
+    Nothing -> do
+      writeAgentSessionKey "x" -- Sanity check
+      logLocM DebugS "Creating agent session"
+      updateAgentSession
 
 updateAgentSession :: App Text
 updateAgentSession = do
diff --git a/hercules-ci-agent/Hercules/Agent/WorkerProcess.hs b/hercules-ci-agent/Hercules/Agent/WorkerProcess.hs
--- a/hercules-ci-agent/Hercules/Agent/WorkerProcess.hs
+++ b/hercules-ci-agent/Hercules/Agent/WorkerProcess.hs
@@ -30,11 +30,10 @@
 import System.Timeout (timeout)
 import Prelude ()
 
-data WorkerException
-  = WorkerException
-      { originalException :: SomeException,
-        exitStatus :: Maybe ExitCode
-      }
+data WorkerException = WorkerException
+  { originalException :: SomeException,
+    exitStatus :: Maybe ExitCode
+  }
   deriving (Show, Typeable)
 
 instance Exception WorkerException where
@@ -44,10 +43,9 @@
         Nothing -> ""
         Just s -> " (worker: " <> show s <> ")"
 
-data WorkerEnvSettings
-  = WorkerEnvSettings
-      { nixPath :: [EvaluateTask.NixPathElement (EvaluateTask.SubPathOf FilePath)]
-      }
+data WorkerEnvSettings = WorkerEnvSettings
+  { nixPath :: [EvaluateTask.NixPathElement (EvaluateTask.SubPathOf FilePath)]
+  }
 
 -- | Filter out impure env vars by wildcard, set NIX_PATH
 modifyEnv :: WorkerEnvSettings -> Map [Char] [Char] -> Map [Char] [Char]
@@ -84,40 +82,41 @@
             std_out = CreatePipe,
             std_err = CreatePipe
           }
-  liftIO $ withCreateProcess createProcessSpec $ \mIn mOut mErr processHandle -> do
-    (inHandle, outHandle, errHandle) <-
-      case (,,) <$> mIn <*> mOut <*> mErr of
-        Just x -> pure x
-        Nothing ->
-          throwIO $
-            mkIOError
-              illegalOperationErrorType
-              "Process did not return all handles"
-              Nothing -- no handle
-              Nothing -- no path
-    pidMaybe <- liftIO $ getPid processHandle
-    let pid = case pidMaybe of Just x -> fromIntegral x; Nothing -> 0
-    let stderrPiper =
-          liftIO $
-            runConduit
-              (sourceHandle errHandle .| linesUnboundedAsciiC .| awaitForever (liftIO . unlift . stderrLineHandler pid))
-    let eventConduit = sourceHandle outHandle .| conduitDecode
-        commandConduit =
-          sourceChan commandChan
-            .| conduitEncode
-            .| concatMapC (\x -> [Chunk x, Flush])
-            .| handleC handleEPIPE (sinkHandleFlush inHandle)
-        handleEPIPE e | ioeGetErrorType e == ResourceVanished = pure ()
-        handleEPIPE e = throwIO e
-    let cmdThread = runConduit commandConduit `finally` hClose outHandle
-        eventThread = unlift $ conduitToCallbacks eventConduit eventHandler
-    -- plain forkIO so it can process all of stderr in case of an exception
-    void $ forkIO stderrPiper
-    withAsync (waitForProcess processHandle) $ \exitAsync -> do
-      withAsync cmdThread $ \_ -> do
-        eventThread
-          `Safe.catch` \e -> do
-            let oneSecond = 1000 * 1000
-            maybeStatus <- timeout (5 * oneSecond) (wait exitAsync)
-            throwIO $ WorkerException e maybeStatus
-        wait exitAsync
+  liftIO $
+    withCreateProcess createProcessSpec $ \mIn mOut mErr processHandle -> do
+      (inHandle, outHandle, errHandle) <-
+        case (,,) <$> mIn <*> mOut <*> mErr of
+          Just x -> pure x
+          Nothing ->
+            throwIO $
+              mkIOError
+                illegalOperationErrorType
+                "Process did not return all handles"
+                Nothing -- no handle
+                Nothing -- no path
+      pidMaybe <- liftIO $ getPid processHandle
+      let pid = maybe 0 fromIntegral pidMaybe
+      let stderrPiper =
+            liftIO $
+              runConduit
+                (sourceHandle errHandle .| linesUnboundedAsciiC .| awaitForever (liftIO . unlift . stderrLineHandler pid))
+      let eventConduit = sourceHandle outHandle .| conduitDecode
+          commandConduit =
+            sourceChan commandChan
+              .| conduitEncode
+              .| concatMapC (\x -> [Chunk x, Flush])
+              .| handleC handleEPIPE (sinkHandleFlush inHandle)
+          handleEPIPE e | ioeGetErrorType e == ResourceVanished = pure ()
+          handleEPIPE e = throwIO e
+      let cmdThread = runConduit commandConduit `finally` hClose outHandle
+          eventThread = unlift $ conduitToCallbacks eventConduit eventHandler
+      -- plain forkIO so it can process all of stderr in case of an exception
+      void $ forkIO stderrPiper
+      withAsync (waitForProcess processHandle) $ \exitAsync -> do
+        withAsync cmdThread $ \_ -> do
+          eventThread
+            `Safe.catch` \e -> do
+              let oneSecond = 1000 * 1000
+              maybeStatus <- timeout (5 * oneSecond) (wait exitAsync)
+              throwIO $ WorkerException e maybeStatus
+          wait exitAsync
diff --git a/src-cnix/CNix.hs b/src-cnix/CNix.hs
deleted file mode 100644
--- a/src-cnix/CNix.hs
+++ /dev/null
@@ -1,263 +0,0 @@
-{-# LANGUAGE QuasiQuotes #-}
-{-# LANGUAGE TemplateHaskell #-}
-
-module CNix
-  ( module CNix,
-    RawValue,
-    rawValueType,
-    module CNix.Internal.Store,
-    module CNix.Internal.Typed,
-    type NixStore,
-    type EvalState,
-    type Ref,
-    type SecretKey,
-  )
-where
-
--- TODO: No more Ptr EvalState
--- TODO: No more NixStore when EvalState is already there
--- TODO: Map Nix-specific C++ exceptions to a CNix exception type
-import CNix.Internal.Context
-import CNix.Internal.Raw
-import CNix.Internal.Store
-import CNix.Internal.Typed
-import Conduit
-import qualified Data.Map as M
-import qualified Foreign.C.String
-import qualified Language.C.Inline.Cpp as C
-import qualified Language.C.Inline.Cpp.Exceptions as C
-import Protolude hiding (evalState, throwIO)
-
-newtype Bindings = Bindings (Ptr Bindings')
-
-C.context context
-
-C.include "<stdio.h>"
-
-C.include "<cstring>"
-
-C.include "<math.h>"
-
-C.include "<nix/config.h>"
-
-C.include "<nix/shared.hh>"
-
-C.include "<nix/eval.hh>"
-
-C.include "<nix/eval-inline.hh>"
-
-C.include "<nix/store-api.hh>"
-
-C.include "<nix/common-eval-args.hh>"
-
-C.include "<nix/get-drvs.hh>"
-
-C.include "<nix/derivations.hh>"
-
-C.include "<nix/affinity.hh>"
-
-C.include "<nix/globals.hh>"
-
-C.include "hercules-aliases.h"
-
-C.include "<gc/gc.h>"
-
-C.include "<gc/gc_cpp.h>"
-
-C.include "<gc/gc_allocator.h>"
-
-C.using "namespace nix"
-
-C.verbatim "\nGC_API void GC_CALL GC_throw_bad_alloc() { throw std::bad_alloc(); }\n"
-
-init :: IO ()
-init =
-  void
-    [C.throwBlock| void {
-      nix::initNix();
-      nix::initGC();
-    } |]
-
-setTalkative :: IO ()
-setTalkative =
-  [C.throwBlock| void {
-    nix::verbosity = nix::lvlTalkative;
-  } |]
-
-setDebug :: IO ()
-setDebug =
-  [C.throwBlock| void {
-    nix::verbosity = nix::lvlVomit;
-  } |]
-
-setGlobalOption :: Text -> Text -> IO ()
-setGlobalOption opt value = do
-  let optionStr = encodeUtf8 opt
-      valueStr = encodeUtf8 value
-  [C.throwBlock| void {
-    globalConfig.set($bs-cstr:optionStr, $bs-cstr:valueStr);
-  }|]
-
-setOption :: Text -> Text -> IO ()
-setOption opt value = do
-  let optionStr = encodeUtf8 opt
-      valueStr = encodeUtf8 value
-  [C.throwBlock| void {
-    settings.set($bs-cstr:optionStr, $bs-cstr:valueStr);
-  }|]
-
-logInfo :: Text -> IO ()
-logInfo t = do
-  let bstr = encodeUtf8 t
-  [C.throwBlock| void {
-    printInfo($bs-cstr:bstr);
-  }|]
-
-mkBindings :: Ptr Bindings' -> IO Bindings
-mkBindings p = pure $ Bindings p
-
-withEvalState ::
-  MonadResource m =>
-  Ptr (Ref NixStore) ->
-  (Ptr EvalState -> ConduitT i o m r) ->
-  ConduitT i o m r
-withEvalState store =
-  bracketP
-    ( liftIO $
-        [C.throwBlock| EvalState* {
-          Strings searchPaths;
-          return new EvalState(searchPaths, *$(refStore* store));
-        } |]
-    )
-    (\x -> liftIO $ [C.throwBlock| void { delete $(EvalState* x); } |])
-
-evalFile :: Ptr EvalState -> FilePath -> IO RawValue
-evalFile evalState filename = do
-  filename' <- Foreign.C.String.newCString filename
-  mkRawValue
-    =<< [C.throwBlock| Value* {
-      Value value;
-      $(EvalState *evalState)->evalFile($(const char *filename'), value);
-      return new (NoGC) Value(value);
-    }|]
-
--- leaks
-newStrings :: IO (Ptr Strings)
-newStrings = [C.exp| Strings* { new (NoGC) Strings() }|]
-
-appendString :: Ptr Strings -> ByteString -> IO ()
-appendString ss s =
-  [C.block| void {
-    $(Strings *ss)->push_back($bs-ptr:s);
-  }|]
-
-evalArgs :: Ptr EvalState -> [ByteString] -> IO Bindings
-evalArgs evalState args = do
-  argsStrings <- newStrings
-  forM_ args $ appendString argsStrings
-  mkBindings
-    =<< [C.throwBlock| Bindings * {
-      Strings *args = $(Strings *argsStrings);
-      struct MixEvalArgs evalArgs;
-      Bindings *autoArgs;
-      EvalState &state = *$(EvalState *evalState);
-
-      evalArgs.parseCmdline(*args);
-      autoArgs = evalArgs.getAutoArgs(state);
-      if (autoArgs == NULL) {
-        //cerr << "Could not evaluate automatic arguments" << endl;
-        abort(); // FIXME
-      }
-      return autoArgs;
-
-      // catch (EvalError & e) {
-      // catch (Error & e) {
-      // catch (std::exception & e) {
-
-    }|]
-
-autoCallFunction :: Ptr EvalState -> RawValue -> Bindings -> IO RawValue
-autoCallFunction evalState (RawValue fun) (Bindings autoArgs) =
-  mkRawValue
-    =<< [C.throwBlock| Value* {
-          Value result;
-          $(EvalState *evalState)->autoCallFunction(
-                  *$(Bindings *autoArgs),
-                  *$(Value *fun),
-                  result);
-          return new (NoGC) Value (result);
-        }|]
-
-isDerivation :: Ptr EvalState -> RawValue -> IO Bool
-isDerivation evalState (RawValue v) =
-  (0 /=)
-    <$> [C.throwBlock| int {
-          if ($(Value *v) == NULL) { throw std::invalid_argument("forceValue value must be non-null"); }
-          return $(EvalState *evalState)->isDerivation(*$(Value *v));
-        }|]
-
-isFunctor :: Ptr EvalState -> RawValue -> IO Bool
-isFunctor evalState (RawValue v) =
-  (0 /=)
-    <$> [C.throwBlock| int {
-          if ($(Value *v) == NULL) { throw std::invalid_argument("forceValue value must be non-null"); }
-          return $(EvalState *evalState)->isFunctor(*$(Value *v));
-        }|]
-
-getRecurseForDerivations :: Ptr EvalState -> Value NixAttrs -> IO Bool
-getRecurseForDerivations evalState (Value (RawValue v)) =
-  (0 /=)
-    <$> [C.throwBlock| int {
-          Value *v = $(Value *v);
-          EvalState *evalState = $(EvalState *evalState);
-          Symbol rfd = evalState->symbols.create("recurseForDerivations");
-          Bindings::iterator iter = v->attrs->find(rfd);
-          if (iter == v->attrs->end()) {
-            return 0;
-          } else {
-            evalState->forceValue(*iter->value);
-            if (iter->value->type == ValueType::tBool) {
-              return iter->value->boolean ? 1 : 0;
-            } else {
-              // They can be empty attrsets???
-              // Observed in nixpkgs master 67e2de195a4aa0a50ffb1e1ba0b4fb531dca67dc
-              return 1;
-            }
-          }
-        } |]
-
-getAttrBindings :: Value NixAttrs -> IO Bindings
-getAttrBindings (Value (RawValue v)) = mkBindings =<< [C.exp| Bindings *{ $(Value *v)->attrs } |]
-
-getAttrs :: Value NixAttrs -> IO (Map ByteString RawValue)
-getAttrs (Value (RawValue v)) = do
-  begin <- [C.exp| Attr *{ $(Value *v)->attrs->begin() }|]
-  end <- [C.exp| Attr *{ $(Value *v)->attrs->end() }|]
-  let gather :: Map ByteString RawValue -> Ptr Attr' -> IO (Map ByteString RawValue)
-      gather acc i | i == end = pure acc
-      gather acc i
-        | otherwise =
-          do
-            name <- unsafeMallocBS [C.exp| const char *{ strdup(static_cast<std::string>($(Attr *i)->name).c_str()) } |]
-            value <- mkRawValue =<< [C.exp| Value *{ new (NoGC) Value(*$(Attr *i)->value) } |]
-            let acc' = M.insert name value acc
-            seq acc' pass
-            gather acc' =<< [C.exp| Attr *{ &$(Attr *i)[1] }|]
-  gather mempty begin
-
-getDrvFile :: MonadIO m => Ptr EvalState -> RawValue -> m ByteString
-getDrvFile evalState (RawValue v) =
-  unsafeMallocBS
-    [C.throwBlock| const char *{
-      EvalState &state = *$(EvalState *evalState);
-      auto drvInfo = getDerivation(state, *$(Value *v), false);
-      if (!drvInfo)
-        throw EvalError("Not a valid derivation");
-
-      std::string drvPath = drvInfo->queryDrvPath();
-
-      // write it (?)
-      auto drv = state.store->derivationFromPath(drvPath);
-
-      return strdup(drvPath.c_str());
-    }|]
diff --git a/src-cnix/CNix/Internal/Context.hs b/src-cnix/CNix/Internal/Context.hs
deleted file mode 100644
--- a/src-cnix/CNix/Internal/Context.hs
+++ /dev/null
@@ -1,81 +0,0 @@
-{-# LANGUAGE EmptyDataDecls #-}
-{-# LANGUAGE QuasiQuotes #-}
-{-# LANGUAGE TemplateHaskell #-}
-
-module CNix.Internal.Context
-  ( module CNix.Internal.Context,
-    NixStore,
-  )
-where
-
-import Cachix.Client.Store.Context (NixStore)
-import Data.ByteString.Unsafe (unsafePackMallocCString)
-import qualified Data.Map as M
-import qualified Foreign.C.String
-import Hercules.Agent.StoreFFI (ExceptionPtr)
-import qualified Language.C.Inline.Context as C
-import qualified Language.C.Inline.Cpp as C
-import qualified Language.C.Types as C
-import Protolude
-
-data EvalState
-
-data Strings
-
-data Ref a
-
-data Bindings'
-
-data Value'
-
-data Attr'
-
-data HerculesStore
-
-data Derivation
-
-data Fields
-
-data HerculesLoggerEntry
-
-data LogEntryQueue
-
-data StringsIterator
-
-data DerivationOutputsIterator
-
-data DerivationInputsIterator
-
-data StringPairsIterator
-
-data StringPairs
-
-data SecretKey
-
-context :: C.Context
-context =
-  C.cppCtx <> C.fptrCtx
-    <> C.bsCtx
-      { C.ctxTypesTable =
-          M.singleton (C.TypeName "refStore") [t|Ref NixStore|]
-            <> M.singleton (C.TypeName "EvalState") [t|EvalState|]
-            <> M.singleton (C.TypeName "Bindings") [t|Bindings'|]
-            <> M.singleton (C.TypeName "Value") [t|Value'|]
-            <> M.singleton (C.TypeName "Attr") [t|Attr'|]
-            <> M.singleton (C.TypeName "Strings") [t|Strings|]
-            <> M.singleton (C.TypeName "StringsIterator") [t|StringsIterator|]
-            <> M.singleton (C.TypeName "StringPairs") [t|StringPairs|]
-            <> M.singleton (C.TypeName "StringPairsIterator") [t|StringPairsIterator|]
-            <> M.singleton (C.TypeName "refHerculesStore") [t|Ref HerculesStore|]
-            <> M.singleton (C.TypeName "Derivation") [t|Derivation|]
-            <> M.singleton (C.TypeName "LoggerFields") [t|Fields|]
-            <> M.singleton (C.TypeName "HerculesLoggerEntry") [t|HerculesLoggerEntry|]
-            <> M.singleton (C.TypeName "LogEntryQueue") [t|LogEntryQueue|]
-            <> M.singleton (C.TypeName "DerivationOutputsIterator") [t|DerivationOutputsIterator|]
-            <> M.singleton (C.TypeName "DerivationInputsIterator") [t|DerivationInputsIterator|]
-            <> M.singleton (C.TypeName "exception_ptr") [t|ExceptionPtr|]
-            <> M.singleton (C.TypeName "SecretKey") [t|SecretKey|]
-      }
-
-unsafeMallocBS :: MonadIO m => IO Foreign.C.String.CString -> m ByteString
-unsafeMallocBS m = liftIO (unsafePackMallocCString =<< m)
diff --git a/src-cnix/CNix/Internal/Raw.hs b/src-cnix/CNix/Internal/Raw.hs
deleted file mode 100644
--- a/src-cnix/CNix/Internal/Raw.hs
+++ /dev/null
@@ -1,109 +0,0 @@
-{-# LANGUAGE QuasiQuotes #-}
-{-# LANGUAGE TemplateHaskell #-}
-
-module CNix.Internal.Raw where
-
-import CNix.Internal.Context
-import qualified Language.C.Inline.Cpp as C
-import qualified Language.C.Inline.Cpp.Exceptions as C
-import Protolude hiding (evalState)
-import Prelude ()
-
-C.context context
-
-C.include "<nix/config.h>"
-
-C.include "<nix/eval.hh>"
-
-C.include "<nix/eval-inline.hh>"
-
-C.include "hercules-aliases.h"
-
-C.include "<gc/gc.h>"
-
-C.include "<gc/gc_cpp.h>"
-
-C.include "<gc/gc_allocator.h>"
-
-C.using "namespace nix"
-
-newtype RawValue = RawValue (Ptr Value')
-
--- | Takes ownership of the value.
-mkRawValue :: Ptr Value' -> IO RawValue
-mkRawValue p = pure $ RawValue p
-
--- | Similar to Nix's Value->type but conflates the List variations
-data RawValueType
-  = Int
-  | Bool
-  | String
-  | Path
-  | Null
-  | Attrs
-  | List
-  | Thunk
-  | App
-  | Lambda
-  | Blackhole
-  | PrimOp
-  | PrimOpApp
-  | External
-  | Float
-  | Other
-  deriving (Generic, Show, Eq, Ord)
-
--- | You may need to 'forceValue' first.
-rawValueType :: RawValue -> IO RawValueType
-rawValueType (RawValue v) =
-  f
-    <$> [C.block| int {
-      switch ($(Value* v)->type) {
-        case tInt:         return 1;
-        case tBool:        return 2;
-        case tString:      return 3;
-        case tPath:        return 4;
-        case tNull:        return 5;
-        case tAttrs:       return 6;
-        case tList1:       return 7;
-        case tList2:       return 8;
-        case tListN:       return 9;
-        case tThunk:       return 10;
-        case tApp:         return 11;
-        case tLambda:      return 12;
-        case tBlackhole:   return 13;
-        case tPrimOp:      return 14;
-        case tPrimOpApp:   return 15;
-        case tExternal:    return 16;
-        case tFloat:       return 17;
-        default: return 0;
-      }
-    }|]
-  where
-    f 1 = Int
-    f 2 = Bool
-    f 3 = String
-    f 4 = Path
-    f 5 = Null
-    f 6 = Attrs
-    f 7 = List
-    f 8 = List
-    f 9 = List
-    f 10 = Thunk
-    f 11 = App
-    f 12 = Lambda
-    f 13 = Blackhole
-    f 14 = PrimOp
-    f 15 = PrimOpApp
-    f 16 = External
-    f 17 = Float
-    f _ = Other
-
-forceValue :: Exception a => Ptr EvalState -> RawValue -> IO (Either a ())
-forceValue evalState (RawValue v) =
-  try
-    [C.catchBlock|  {
-      Value *v = $(Value *v);
-      if (v == NULL) throw std::invalid_argument("forceValue value must be non-null");
-      $(EvalState *evalState)->forceValue(*v);
-    }|]
diff --git a/src-cnix/CNix/Internal/Store.hs b/src-cnix/CNix/Internal/Store.hs
deleted file mode 100644
--- a/src-cnix/CNix/Internal/Store.hs
+++ /dev/null
@@ -1,415 +0,0 @@
-{-# LANGUAGE QuasiQuotes #-}
-{-# LANGUAGE TemplateHaskell #-}
-
-module CNix.Internal.Store
-  ( module CNix.Internal.Store,
-    HerculesStore,
-  )
-where
-
-import CNix.Internal.Context
-import Control.Exception
-import Control.Monad.IO.Unlift
-import qualified Data.ByteString.Unsafe as BS
-import Data.Coerce
-import qualified Data.Map as M
-import Foreign.C.String (withCString)
-import Foreign.ForeignPtr
-import Foreign.StablePtr
-import Hercules.Agent.StoreFFI
-import qualified Language.C.Inline.Cpp as C
-import qualified Language.C.Inline.Cpp.Exceptions as C
-import Protolude
-import System.IO.Unsafe (unsafePerformIO)
-import Prelude ()
-
-C.context context
-
-C.include "<cstring>"
-
-C.include "<nix/config.h>"
-
-C.include "<nix/shared.hh>"
-
-C.include "<nix/store-api.hh>"
-
-C.include "<nix/get-drvs.hh>"
-
-C.include "<nix/derivations.hh>"
-
-C.include "<nix/affinity.hh>"
-
-C.include "<nix/globals.hh>"
-
-C.include "hercules-aliases.h"
-
-C.using "namespace nix"
-
-withStore :: MonadUnliftIO m => (Ptr (Ref NixStore) -> m a) -> m a
-withStore m = do
-  UnliftIO ul <- askUnliftIO
-  liftIO $ withStore' $ \a -> ul (m a)
-
-withStore' ::
-  (Ptr (Ref NixStore) -> IO r) ->
-  IO r
-withStore' =
-  bracket
-    ( liftIO $
-        [C.throwBlock| refStore* {
-            refStore s = openStore();
-            return new refStore(s);
-          } |]
-    )
-    (\x -> liftIO $ [C.exp| void { delete $(refStore* x) } |])
-
-withStoreFromURI ::
-  MonadUnliftIO m =>
-  Text ->
-  (Ptr (Ref NixStore) -> m r) ->
-  m r
-withStoreFromURI storeURIText f = do
-  let storeURI = encodeUtf8 storeURIText
-  (UnliftIO unlift) <- askUnliftIO
-  liftIO $
-    bracket
-      [C.throwBlock| refStore* {
-        refStore s = openStore($bs-cstr:storeURI);
-        return new refStore(s);
-      }|]
-      (\x -> [C.exp| void { delete $(refStore* x) } |])
-      (unlift . f)
-
-storeUri :: MonadIO m => Ptr (Ref NixStore) -> m ByteString
-storeUri store =
-  unsafeMallocBS
-    [C.block| const char* {
-       std::string uri = (*$(refStore* store))->getUri();
-       return strdup(uri.c_str());
-     } |]
-
-ensurePath :: Ptr (Ref NixStore) -> ByteString -> IO ()
-ensurePath store path =
-  [C.throwBlock| void {
-    (*$(refStore* store))->ensurePath(std::string($bs-ptr:path, $bs-len:path));
-  } |]
-
-clearPathInfoCache :: Ptr (Ref NixStore) -> IO ()
-clearPathInfoCache store =
-  [C.throwBlock| void {
-    (*$(refStore* store))->clearPathInfoCache();
-  } |]
-
-clearSubstituterCaches :: IO ()
-clearSubstituterCaches =
-  [C.throwBlock| void {
-    auto subs = nix::getDefaultSubstituters();
-    for (auto sub : subs) {
-      sub->clearPathInfoCache();
-    }
-  } |]
-
-buildPath :: Ptr (Ref NixStore) -> ByteString -> IO ()
-buildPath store path =
-  [C.throwBlock| void {
-    PathSet ps({std::string($bs-ptr:path, $bs-len:path)});
-    (*$(refStore* store))->buildPaths(ps);
-   } |]
-
-getDerivation :: Ptr (Ref NixStore) -> ByteString -> IO (ForeignPtr Derivation)
-getDerivation store path = do
-  ptr <-
-    [C.throwBlock| Derivation *{
-      return new Derivation(
-          (*$(refStore* store))->derivationFromPath(std::string($bs-ptr:path, $bs-len:path))
-        );
-    } |]
-  newForeignPtr finalizeDerivation ptr
-
--- Useful for testingg
-getDerivationFromFile :: ByteString -> IO (ForeignPtr Derivation)
-getDerivationFromFile path = do
-  ptr <-
-    [C.throwBlock| Derivation *{
-      return new Derivation(
-          readDerivation(std::string($bs-ptr:path, $bs-len:path))
-        );
-    } |]
-  newForeignPtr finalizeDerivation ptr
-
-finalizeDerivation :: FinalizerPtr Derivation
-{-# NOINLINE finalizeDerivation #-}
-finalizeDerivation =
-  unsafePerformIO
-    [C.exp|
-    void (*)(Derivation *) {
-      [](Derivation *v) {
-        delete v;
-      }
-    } |]
-
--- | Throws when missing
-getDerivationOutputPath :: ForeignPtr Derivation -> ByteString -> IO ByteString
-getDerivationOutputPath fd outputName = withForeignPtr fd $ \d ->
-  [C.throwBlock|
-    const char *{
-      std::string outputName($bs-ptr:outputName, $bs-len:outputName);
-      Derivation *d = $(Derivation *d);
-      return strdup(d->outputs.at(outputName).path.c_str());
-    }
-  |]
-    >>= BS.unsafePackMallocCString
-
-data DerivationOutput
-  = DerivationOutput
-      { derivationOutputName :: !ByteString,
-        derivationOutputPath :: !ByteString,
-        derivationOutputHashAlgo :: !ByteString,
-        derivationOutputHash :: !ByteString
-      }
-
-getDerivationOutputs :: ForeignPtr Derivation -> IO [DerivationOutput]
-getDerivationOutputs derivation =
-  bracket
-    [C.exp| DerivationOutputsIterator* {
-      new DerivationOutputsIterator($fptr-ptr:(Derivation *derivation)->outputs.begin())
-    }|]
-    deleteDerivationOutputsIterator
-    $ \i -> fix $ \continue -> do
-      isEnd <- (0 /=) <$> [C.exp| bool { *$(DerivationOutputsIterator *i) == $fptr-ptr:(Derivation *derivation)->outputs.end() }|]
-      if isEnd
-        then pure []
-        else do
-          name <- [C.exp| const char*{ strdup((*$(DerivationOutputsIterator *i))->first.c_str()) }|] >>= BS.unsafePackMallocCString
-          path <- [C.exp| const char*{ strdup((*$(DerivationOutputsIterator *i))->second.path.c_str()) }|] >>= BS.unsafePackMallocCString
-          hash_ <- [C.exp| const char*{ strdup((*$(DerivationOutputsIterator *i))->second.hash.c_str()) }|] >>= BS.unsafePackMallocCString
-          hashAlgo <- [C.exp| const char*{ strdup((*$(DerivationOutputsIterator *i))->second.hashAlgo.c_str()) }|] >>= BS.unsafePackMallocCString
-          [C.block| void { (*$(DerivationOutputsIterator *i))++; }|]
-          (DerivationOutput name path hashAlgo hash_ :) <$> continue
-
-deleteDerivationOutputsIterator :: Ptr DerivationOutputsIterator -> IO ()
-deleteDerivationOutputsIterator a = [C.block| void { delete $(DerivationOutputsIterator *a); }|]
-
-getDerivationPlatform :: ForeignPtr Derivation -> IO ByteString
-getDerivationPlatform derivation =
-  unsafeMallocBS
-    [C.exp| const char* {
-       strdup($fptr-ptr:(Derivation *derivation)->platform.c_str())
-     } |]
-
-getDerivationSources :: ForeignPtr Derivation -> IO [ByteString]
-getDerivationSources derivation =
-  bracket
-    [C.throwBlock| Strings* {
-      Strings *r = new Strings();
-      for (auto i : $fptr-ptr:(Derivation *derivation)->inputSrcs) {
-        r->push_back(i);
-      }
-      return r;
-    }|]
-    deleteStrings
-    toByteStrings
-
-getDerivationInputs :: ForeignPtr Derivation -> IO [(ByteString, [ByteString])]
-getDerivationInputs derivation =
-  bracket
-    [C.exp| DerivationInputsIterator* {
-      new DerivationInputsIterator($fptr-ptr:(Derivation *derivation)->inputDrvs.begin())
-    }|]
-    deleteDerivationInputsIterator
-    $ \i -> fix $ \continue -> do
-      isEnd <- (0 /=) <$> [C.exp| bool { *$(DerivationInputsIterator *i) == $fptr-ptr:(Derivation *derivation)->inputDrvs.end() }|]
-      if isEnd
-        then pure []
-        else do
-          name <- [C.exp| const char*{ strdup((*$(DerivationInputsIterator *i))->first.c_str()) }|] >>= BS.unsafePackMallocCString
-          outs <-
-            bracket
-              [C.block| Strings*{ 
-                Strings *r = new Strings();
-                for (auto i : (*$(DerivationInputsIterator *i))->second) {
-                  r->push_back(i);
-                }
-                return r;
-              }|]
-              deleteStrings
-              toByteStrings
-          [C.block| void { (*$(DerivationInputsIterator *i))++; }|]
-          ((name, outs) :) <$> continue
-
-deleteDerivationInputsIterator :: Ptr DerivationInputsIterator -> IO ()
-deleteDerivationInputsIterator a = [C.block| void { delete $(DerivationInputsIterator *a); }|]
-
-getDerivationEnv :: ForeignPtr Derivation -> IO (Map ByteString ByteString)
-getDerivationEnv derivation =
-  [C.exp| StringPairs* { &($fptr-ptr:(Derivation *derivation)->env) }|]
-    >>= toByteStringMap
-
-getDerivationOutputNames :: ForeignPtr Derivation -> IO [ByteString]
-getDerivationOutputNames derivation =
-  bracket
-    [C.throwBlock| Strings* {
-      Strings *r = new Strings();
-      for (auto i : $fptr-ptr:(Derivation *derivation)->outputs) {
-        r->push_back(i.first);
-      }
-      return r;
-    }|]
-    deleteStrings
-    toByteStrings
-
-deleteStringPairs :: Ptr StringPairs -> IO ()
-deleteStringPairs s = [C.block| void { delete $(StringPairs *s); }|]
-
-deleteStrings :: Ptr Strings -> IO ()
-deleteStrings s = [C.block| void { delete $(Strings *s); }|]
-
-finalizeStrings :: FinalizerPtr Strings
-{-# NOINLINE finalizeStrings #-}
-finalizeStrings =
-  unsafePerformIO
-    [C.exp|
-    void (*)(Strings *) {
-      [](Strings *v) {
-        delete v;
-      }
-    } |]
-
-getStringsLength :: Ptr Strings -> IO C.CSize
-getStringsLength strings = [C.exp| size_t { $(Strings *strings)->size() }|]
-
-toByteStrings :: Ptr Strings -> IO [ByteString]
-toByteStrings strings = do
-  i <- [C.exp| StringsIterator *{ new StringsIterator($(Strings *strings)->begin()) } |]
-  fix $ \go -> do
-    isEnd <- (0 /=) <$> [C.exp| bool { *$(StringsIterator *i) == $(Strings *strings)->end() }|]
-    if isEnd
-      then pure []
-      else do
-        s <- [C.exp| const char*{ strdup((*$(StringsIterator *i))->c_str()) }|]
-        bs <- BS.unsafePackMallocCString s
-        [C.block| void { (*$(StringsIterator *i))++; }|]
-        (bs :) <$> go
-
-toByteStringMap :: Ptr StringPairs -> IO (Map ByteString ByteString)
-toByteStringMap strings = M.fromList <$> do
-  i <- [C.exp| StringPairsIterator *{ new StringPairsIterator($(StringPairs *strings)->begin()) } |]
-  fix $ \go -> do
-    isEnd <- (0 /=) <$> [C.exp| bool { *$(StringPairsIterator *i) == $(StringPairs *strings)->end() }|]
-    if isEnd
-      then pure []
-      else do
-        k <- [C.exp| const char*{ strdup((*$(StringPairsIterator *i))->first.c_str()) }|]
-        v <- [C.exp| const char*{ strdup((*$(StringPairsIterator *i))->second.c_str()) }|]
-        bk <- BS.unsafePackMallocCString k
-        bv <- BS.unsafePackMallocCString v
-        [C.block| void { (*$(StringPairsIterator *i))++; }|]
-        ((bk, bv) :) <$> go
-
-withStrings :: (Ptr Strings -> IO a) -> IO a
-withStrings =
-  bracket
-    [C.exp| Strings *{ new Strings() }|]
-    (\sp -> [C.block| void { delete $(Strings *sp); }|])
-
-pushString :: Ptr Strings -> ByteString -> IO ()
-pushString strings s =
-  [C.block| void { $(Strings *strings)->push_back($bs-cstr:s); }|]
-
-copyClosure :: Ptr (Ref NixStore) -> Ptr (Ref NixStore) -> [ByteString] -> IO ()
-copyClosure src dest paths = do
-  withStrings $ \pathStrings -> do
-    paths & traverse_ (pushString pathStrings)
-    [C.throwBlock| void {
-      StringSet pathSet;
-      for (auto path : *$(Strings *pathStrings)) {
-        pathSet.insert(path);
-      }
-      nix::copyClosure(*$(refStore* src), *$(refStore* dest), pathSet);
-    }|]
-
-parseSecretKey :: ByteString -> IO (ForeignPtr SecretKey)
-parseSecretKey bs =
-  [C.throwBlock| SecretKey* {
-    return new SecretKey($bs-cstr:bs);
-  }|]
-    >>= newForeignPtr finalizeSecretKey
-
-finalizeSecretKey :: FinalizerPtr SecretKey
-{-# NOINLINE finalizeSecretKey #-}
-finalizeSecretKey =
-  unsafePerformIO
-    [C.exp|
-    void (*)(SecretKey *) {
-      [](SecretKey *v) {
-        delete v;
-      }
-    } |]
-
-signPath ::
-  Ptr (Ref NixStore) ->
-  -- | Secret signing key
-  Ptr SecretKey ->
-  -- | Store path
-  ByteString ->
-  -- | False if the signature was already present, True if the signature was added
-  IO Bool
-signPath store secretKey path = (== 1) <$> do
-  [C.throwBlock| int {
-    nix::ref<nix::Store> store = *$(refStore *store);
-    std::string storePath($bs-cstr:path);
-    auto currentInfo = store->queryPathInfo(storePath);
-
-    auto info2(*currentInfo);
-    info2.sigs.clear();
-    info2.sign(*$(SecretKey *secretKey));
-    assert(!info2.sigs.empty());
-    auto sig = *info2.sigs.begin();
-
-    if (currentInfo->sigs.count(sig)) {
-      return 0;
-    } else {
-      store->addSignatures(storePath, info2.sigs);
-      return 1;
-    }
-  }|]
-
------ Hercules -----
-withHerculesStore ::
-  Ptr (Ref NixStore) ->
-  (Ptr (Ref HerculesStore) -> IO a) ->
-  IO a
-withHerculesStore wrappedStore =
-  bracket
-    ( liftIO $
-        [C.block| refHerculesStore* {
-          refStore &s = *$(refStore *wrappedStore);
-          refHerculesStore hs(new HerculesStore({}, s));
-          return new refHerculesStore(hs);
-        } |]
-    )
-    (\x -> liftIO $ [C.exp| void { delete $(refHerculesStore* x) } |])
-
-nixStore :: Ptr (Ref HerculesStore) -> Ptr (Ref NixStore)
-nixStore = coerce
-
-printDiagnostics :: Ptr (Ref HerculesStore) -> IO ()
-printDiagnostics s =
-  [C.throwBlock| void{
-    (*$(refHerculesStore* s))->printDiagnostics();
-  }|]
-
--- TODO catch pure exceptions from displayException
-setBuilderCallback :: Ptr (Ref HerculesStore) -> (ByteString -> IO ()) -> IO ()
-setBuilderCallback s callback = do
-  p <-
-    mkBuilderCallback $ \cstr exceptionToThrowPtr ->
-      Control.Exception.catch (BS.unsafePackMallocCString cstr >>= callback) $ \e ->
-        withCString (displayException (e :: SomeException)) $ \renderedException -> do
-          stablePtr <- castStablePtrToPtr <$> newStablePtr e
-          [C.block| void {
-            (*$(exception_ptr *exceptionToThrowPtr)) = std::make_exception_ptr(HaskellException(std::string($(const char* renderedException)), $(void* stablePtr)));
-          }|]
-  [C.throwBlock| void {
-    (*$(refHerculesStore* s))->setBuilderCallback($(void (*p)(const char *, exception_ptr *) ));
-  }|]
diff --git a/src-cnix/CNix/Internal/Typed.hs b/src-cnix/CNix/Internal/Typed.hs
deleted file mode 100644
--- a/src-cnix/CNix/Internal/Typed.hs
+++ /dev/null
@@ -1,77 +0,0 @@
-{-# LANGUAGE EmptyDataDecls #-}
-
-module CNix.Internal.Typed where
-
-import CNix.Internal.Context
-import CNix.Internal.Raw
-import Protolude hiding
-  ( evalState,
-    throwIO,
-  )
-import Prelude (userError)
-
--- | Runtime-Typed Value. This implies that it has been forced,
--- because otherwise the type would not be known.
-newtype Value a = Value {rtValue :: RawValue}
-
-data NixInt
-
-data NixFloat
-
-data NixString
-
-data NixPath
-
-data NixAttrs
-
-data NixFunction
-
-data NixList
-
-data NixPrimOp
-
-data NixPrimOpApp
-
-data NixExternal
-
--- TODO: actually encapsulate the constructor
-unsafeAssertType :: RawValue -> Value a
-unsafeAssertType = Value
-
--- This is useful because you regain exhaustiveness checking.
--- Otherwise a bunch of downcast functions might do.
-data Match
-  = IsInt (Value NixInt)
-  | IsBool (Value Bool)
-  | IsString (Value NixString)
-  | IsPath (Value NixPath)
-  | IsNull (Value ())
-  | IsAttrs (Value NixAttrs)
-  | IsList (Value NixList)
-  | IsFunction (Value NixFunction)
-  | IsExternal (Value NixExternal)
-  | IsFloat (Value NixFloat)
-
--- FIXME: errors don't provide any clue here
-match :: Ptr EvalState -> RawValue -> IO (Either SomeException Match)
-match es v = forceValue es v >>= \case
-  Left e -> pure (Left e)
-  Right _ -> rawValueType v <&> \case
-    Int -> pure $ IsInt $ unsafeAssertType v
-    Bool -> pure $ IsBool $ unsafeAssertType v
-    String -> pure $ IsString $ unsafeAssertType v
-    Path -> pure $ IsPath $ unsafeAssertType v
-    Null -> pure $ IsNull $ unsafeAssertType v
-    Attrs -> pure $ IsAttrs $ unsafeAssertType v
-    List -> pure $ IsList $ unsafeAssertType v
-    Thunk -> Left $ SomeException $ userError "Could not force Nix thunk" -- FIXME: custom exception?
-    App -> Left $ SomeException $ userError "Could not force Nix thunk (App)"
-    Blackhole ->
-      Left $ SomeException $ userError "Could not force Nix thunk (Blackhole)"
-    Lambda -> pure $ IsFunction $ unsafeAssertType v
-    PrimOp -> pure $ IsFunction $ unsafeAssertType v
-    PrimOpApp -> pure $ IsFunction $ unsafeAssertType v
-    External -> pure $ IsExternal $ unsafeAssertType v
-    Float -> pure $ IsFloat $ unsafeAssertType v
-    Other ->
-      Left $ SomeException $ userError "Unknown runtime type in Nix value"
diff --git a/src-internal-ffi/Hercules/Agent/StoreFFI.hs b/src-internal-ffi/Hercules/Agent/StoreFFI.hs
deleted file mode 100644
--- a/src-internal-ffi/Hercules/Agent/StoreFFI.hs
+++ /dev/null
@@ -1,20 +0,0 @@
-{-# OPTIONS_GHC -fobject-code #-}
-{-# LANGUAGE CPP #-}
-
-module Hercules.Agent.StoreFFI where
-
-import qualified Foreign.C
-import Protolude
-
-type BuilderCallback = Foreign.C.CString -> Ptr ExceptionPtr -> IO ()
-
--- Work around a problem in ghcide with foreign imports.
-#ifndef GHCIDE
-foreign import ccall "wrapper"
-  mkBuilderCallback :: BuilderCallback -> IO (FunPtr BuilderCallback)
-#else
-mkBuilderCallback :: BuilderCallback -> IO (FunPtr BuilderCallback)
-mkBuilderCallback = panic "This is a stub to work around a ghcide issue. Please compile without -DGHCIDE"
-#endif
-
-data ExceptionPtr
diff --git a/src/Data/Conduit/Katip/Orphans.hs b/src/Data/Conduit/Katip/Orphans.hs
--- a/src/Data/Conduit/Katip/Orphans.hs
+++ b/src/Data/Conduit/Katip/Orphans.hs
@@ -7,13 +7,11 @@
 import Katip
 
 instance Katip m => Katip (ConduitT i o m) where
-
   getLogEnv = lift getLogEnv
 
   localLogEnv f = transPipe (localLogEnv f)
 
 instance KatipContext m => KatipContext (ConduitT i o m) where
-
   getKatipContext = lift getKatipContext
 
   getKatipNamespace = lift getKatipNamespace
diff --git a/src/Hercules/Agent/NixFile.hs b/src/Hercules/Agent/NixFile.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/Agent/NixFile.hs
@@ -0,0 +1,44 @@
+module Hercules.Agent.NixFile
+  ( findNixFile,
+  )
+where
+
+import Protolude
+import qualified System.Directory as Dir
+import System.FilePath ((</>))
+
+type Ambiguity = [FilePath]
+
+searchPath :: [Ambiguity]
+searchPath = [["nix/ci.nix", "ci.nix"], ["default.nix"]]
+
+findNixFile :: FilePath -> IO (Either Text FilePath)
+findNixFile projectDir = do
+  searchResult <-
+    for searchPath $
+      traverse $ \relPath ->
+        let path = projectDir </> relPath
+         in Dir.doesFileExist path >>= \case
+              True -> pure $ Just (relPath, path)
+              False -> pure Nothing
+  case filter (not . null) $ map catMaybes searchResult of
+    [(_relPath, unambiguous)] : _ -> pure (pure unambiguous)
+    ambiguous : _ ->
+      pure $
+        Left $
+          "Don't know what to do, expecting only one of "
+            <> englishConjunction "or" (map fst ambiguous)
+    [] ->
+      pure $
+        Left $
+          "Please provide a Nix expression to build. Could not find any of "
+            <> englishConjunction "or" (concat searchPath)
+            <> " in your source"
+
+englishConjunction :: Show a => Text -> [a] -> Text
+englishConjunction _ [] = "none"
+englishConjunction _ [a] = show a
+englishConjunction connective [a1, a2] =
+  show a1 <> " " <> connective <> " " <> show a2
+englishConjunction connective (a : as) =
+  show a <> ", " <> englishConjunction connective as
diff --git a/src/Hercules/Agent/Producer.hs b/src/Hercules/Agent/Producer.hs
--- a/src/Hercules/Agent/Producer.hs
+++ b/src/Hercules/Agent/Producer.hs
@@ -20,11 +20,10 @@
 
 -- | A thread producing zero or more payloads and a final value.
 -- Handles exception propagation.
-data Producer p r
-  = Producer
-      { producerQueueRead :: STM (Msg p r),
-        producerThread :: ThreadId
-      }
+data Producer p r = Producer
+  { producerQueueRead :: STM (Msg p r),
+    producerThread :: ThreadId
+  }
   deriving (Functor)
 
 data ProducerCancelled = ProducerCancelled
@@ -164,13 +163,13 @@
                   doPerformBatch buf
                   beginReading
          in beginReading
-  liftIO
-    $ withAsync
+  liftIO $
+    withAsync
       ( forever $ do
           threadDelay maxDelay
           atomically $ writeTQueue flushes ()
       )
-    $ \_flusher -> unlift $ withProducer producer f
+      $ \_flusher -> unlift $ withProducer producer f
 
 syncer :: MonadIO m => (Syncing a -> m ()) -> m ()
 syncer writer = do
diff --git a/src/Hercules/Agent/Sensitive.hs b/src/Hercules/Agent/Sensitive.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/Agent/Sensitive.hs
@@ -0,0 +1,22 @@
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
+module Hercules.Agent.Sensitive where
+
+import Data.Binary
+import Protolude
+import Text.Show
+
+-- | newtype wrapper to avoid leaking sensitive data through Show
+newtype Sensitive a = Sensitive {reveal :: a}
+  deriving (Generic)
+  deriving newtype (Binary, Eq, Ord)
+  deriving (Functor, Applicative, Monad) via Identity
+
+-- | @const "<sensitive>"@
+instance Show (Sensitive a) where
+  show _ = "<sensitive>"
+
+revealContainer :: Functor f => Sensitive (f a) -> f (Sensitive a)
+revealContainer (Sensitive fa) = Sensitive <$> fa
diff --git a/src/Hercules/Agent/Socket.hs b/src/Hercules/Agent/Socket.hs
--- a/src/Hercules/Agent/Socket.hs
+++ b/src/Hercules/Agent/Socket.hs
@@ -24,8 +24,8 @@
 import Data.Time.Extras
 import Hercules.API.Agent.LifeCycle.ServiceInfo (ServiceInfo)
 import qualified Hercules.API.Agent.LifeCycle.ServiceInfo as ServiceInfo
-import qualified Hercules.API.Agent.Socket.Frame as Frame
 import Hercules.API.Agent.Socket.Frame (Frame)
+import qualified Hercules.API.Agent.Socket.Frame as Frame
 import Hercules.Agent.STM (atomically, newTChanIO, newTVarIO)
 import Katip (KatipContext, Severity (..), katipAddContext, katipAddNamespace, logLocM, sl)
 import Network.URI (URI, uriAuthority, uriPath, uriPort, uriQuery, uriRegName, uriScheme)
@@ -37,25 +37,23 @@
 import UnliftIO.Timeout (timeout)
 import Wuss (runSecureClientWith)
 
-data Socket r w
-  = Socket
-      { write :: w -> STM (),
-        serviceChan :: TChan r,
-        sync :: STM (STM ())
-      }
+data Socket r w = Socket
+  { write :: w -> STM (),
+    serviceChan :: TChan r,
+    sync :: STM (STM ())
+  }
 
 syncIO :: Socket r w -> IO ()
 syncIO = join . fmap atomically . atomically . sync
 
 -- | Parameters to start 'withReliableSocket'.
-data SocketConfig ap sp m
-  = SocketConfig
-      { makeHello :: m ap,
-        checkVersion :: (sp -> m (Either Text ())),
-        baseURL :: URI,
-        path :: Text,
-        token :: ByteString
-      }
+data SocketConfig ap sp m = SocketConfig
+  { makeHello :: m ap,
+    checkVersion :: (sp -> m (Either Text ())),
+    baseURL :: URI,
+    path :: Text,
+    token :: ByteString
+  }
 
 requiredServiceVersion :: (Int, Int)
 requiredServiceVersion = (2, 0)
@@ -74,15 +72,16 @@
         writeTVar agentMessageNextN (c + 1)
         pure $ Frame.Msg {n = c, p = p}
       socketThread = runReliableSocket socketConfig writeQueue serviceMessageChan highestAcked
-      socket = Socket
-        { write = tagPayload >=> writeTBQueue writeQueue,
-          serviceChan = serviceMessageChan,
-          sync = do
-            counterAtSyncStart <- (\n -> n - 1) <$> readTVar agentMessageNextN
-            pure do
-              acked <- readTVar highestAcked
-              guard $ acked >= counterAtSyncStart
-        }
+      socket =
+        Socket
+          { write = tagPayload >=> writeTBQueue writeQueue,
+            serviceChan = serviceMessageChan,
+            sync = do
+              counterAtSyncStart <- (\n -> n - 1) <$> readTVar agentMessageNextN
+              pure do
+                acked <- readTVar highestAcked
+                guard $ acked >= counterAtSyncStart
+          }
   race socketThread (f socket) <&> either identity identity
 
 checkVersion' :: Applicative m => ServiceInfo -> m (Either Text ())
@@ -136,11 +135,12 @@
       handshake conn = katipAddNamespace "Handshake" do
         siMsg <- recv conn
         case siMsg of
-          Frame.Oob {o = o'} -> checkVersion socketConfig o' >>= \case
-            Left e -> do
-              send conn [Frame.Exception e]
-              throwIO $ FatalError "It looks like you're running a development version of hercules-ci-agent that is not yet supported on hercules-ci.com. Please use the stable branch or a tag."
-            Right _ -> pass
+          Frame.Oob {o = o'} ->
+            checkVersion socketConfig o' >>= \case
+              Left e -> do
+                send conn [Frame.Exception e]
+                throwIO $ FatalError "It looks like you're running a development version of hercules-ci-agent that is not yet supported on hercules-ci.com. Please use the stable branch or a tag."
+              Right _ -> pass
           _ -> throwIO $ FatalError "Unexpected message. This is either a bug or you might need to update your agent."
         hello <- makeHello socketConfig
         send conn [Frame.Oob hello]
@@ -221,13 +221,13 @@
             -- terminate other threads via race_
             pass
           else noAckCleanupThread' expectedN
-  forever
-    $ handle logWarningPause
-    $ withConnection' socketConfig
-    $ \conn -> do
-      katipAddNamespace "Handshake" do
-        handshake conn
-      readThread conn `race_` writeThread conn `race_` noAckCleanupThread
+  forever $
+    handle logWarningPause $
+      withConnection' socketConfig $
+        \conn -> do
+          katipAddNamespace "Handshake" do
+            handshake conn
+          readThread conn `race_` writeThread conn `race_` noAckCleanupThread
 
 msgN :: Frame o a -> Maybe Integer
 msgN (Frame.Msg {n = n}) = Just n
@@ -262,6 +262,7 @@
 
 withTimeout :: (Exception e, MonadIO m, MonadUnliftIO m) => NominalDiffTime -> e -> m a -> m a
 withTimeout t e _ | t <= 0 = throwIO e
-withTimeout t e m = timeout (ceiling $ t * 1_000_000) m >>= \case
-  Nothing -> throwIO e
-  Just a -> pure a
+withTimeout t e m =
+  timeout (ceiling $ t * 1_000_000) m >>= \case
+    Nothing -> throwIO e
+    Just a -> pure a
diff --git a/src/Hercules/Agent/WorkerProtocol/Command.hs b/src/Hercules/Agent/WorkerProtocol/Command.hs
--- a/src/Hercules/Agent/WorkerProtocol/Command.hs
+++ b/src/Hercules/Agent/WorkerProtocol/Command.hs
@@ -5,6 +5,7 @@
 import Data.Binary
 import qualified Hercules.Agent.WorkerProtocol.Command.Build as Build
 import qualified Hercules.Agent.WorkerProtocol.Command.BuildResult as BuildResult
+import qualified Hercules.Agent.WorkerProtocol.Command.Effect as Effect
 import qualified Hercules.Agent.WorkerProtocol.Command.Eval as Eval
 import Protolude
 
@@ -12,4 +13,5 @@
   = Eval Eval.Eval
   | BuildResult BuildResult.BuildResult
   | Build Build.Build
+  | Effect Effect.Effect
   deriving (Generic, Binary, Show, Eq)
diff --git a/src/Hercules/Agent/WorkerProtocol/Command/Build.hs b/src/Hercules/Agent/WorkerProtocol/Command/Build.hs
--- a/src/Hercules/Agent/WorkerProtocol/Command/Build.hs
+++ b/src/Hercules/Agent/WorkerProtocol/Command/Build.hs
@@ -6,11 +6,10 @@
 import Hercules.Agent.WorkerProtocol.LogSettings
 import Protolude
 
-data Build
-  = Build
-      { drvPath :: Text,
-        inputDerivationOutputPaths :: [ByteString],
-        logSettings :: LogSettings,
-        materializeDerivation :: Bool
-      }
+data Build = Build
+  { drvPath :: Text,
+    inputDerivationOutputPaths :: [ByteString],
+    logSettings :: LogSettings,
+    materializeDerivation :: Bool
+  }
   deriving (Generic, Binary, Show, Eq)
diff --git a/src/Hercules/Agent/WorkerProtocol/Command/BuildResult.hs b/src/Hercules/Agent/WorkerProtocol/Command/BuildResult.hs
--- a/src/Hercules/Agent/WorkerProtocol/Command/BuildResult.hs
+++ b/src/Hercules/Agent/WorkerProtocol/Command/BuildResult.hs
@@ -7,11 +7,11 @@
 import Protolude
 
 data BuildResult = BuildResult Text UUID BuildStatus
-  deriving (Generic, Binary, Show, Eq)
+  deriving (Generic, Binary, Show, Eq, NFData)
 
 -- | Subset of @DerivationStatus@, with a @Binary@ instance.
 data BuildStatus
   = Success
   | Failure
   | DependencyFailure
-  deriving (Generic, Binary, Show, Eq)
+  deriving (Generic, Binary, Show, Eq, NFData)
diff --git a/src/Hercules/Agent/WorkerProtocol/Command/Effect.hs b/src/Hercules/Agent/WorkerProtocol/Command/Effect.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/Agent/WorkerProtocol/Command/Effect.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE DeriveAnyClass #-}
+
+module Hercules.Agent.WorkerProtocol.Command.Effect where
+
+import Data.Binary
+import Hercules.Agent.Sensitive
+import Hercules.Agent.WorkerProtocol.LogSettings
+import Protolude
+
+data Effect = Effect
+  { drvPath :: Text,
+    apiBaseURL :: Text,
+    logSettings :: LogSettings,
+    inputDerivationOutputPaths :: [ByteString],
+    materializeDerivation :: Bool,
+    secretsPath :: FilePath,
+    token :: Sensitive Text
+  }
+  deriving (Generic, Binary, Show, Eq)
diff --git a/src/Hercules/Agent/WorkerProtocol/Command/Eval.hs b/src/Hercules/Agent/WorkerProtocol/Command/Eval.hs
--- a/src/Hercules/Agent/WorkerProtocol/Command/Eval.hs
+++ b/src/Hercules/Agent/WorkerProtocol/Command/Eval.hs
@@ -6,17 +6,16 @@
 import Hercules.Agent.WorkerProtocol.LogSettings
 import Protolude
 
-data Eval
-  = Eval
-      { cwd :: FilePath,
-        file :: Text,
-        autoArguments :: Map Text Arg,
-        -- | NB currently the options will leak from one evaluation to
-        --   the next if you're running them in the same worker!
-        --   (as of now, we use one worker process per evaluation)
-        extraNixOptions :: [(Text, Text)],
-        logSettings :: LogSettings
-      }
+data Eval = Eval
+  { cwd :: FilePath,
+    file :: Text,
+    autoArguments :: Map Text Arg,
+    -- | NB currently the options will leak from one evaluation to
+    --   the next if you're running them in the same worker!
+    --   (as of now, we use one worker process per evaluation)
+    extraNixOptions :: [(Text, Text)],
+    logSettings :: LogSettings
+  }
   deriving (Generic, Binary, Show, Eq)
 
 data Arg
diff --git a/src/Hercules/Agent/WorkerProtocol/Event.hs b/src/Hercules/Agent/WorkerProtocol/Event.hs
--- a/src/Hercules/Agent/WorkerProtocol/Event.hs
+++ b/src/Hercules/Agent/WorkerProtocol/Event.hs
@@ -17,5 +17,6 @@
   | Error Text
   | Build Text Text (Maybe UUID)
   | BuildResult BuildResult
+  | EffectResult Int
   | Exception Text
   deriving (Generic, Binary, Show, Eq)
diff --git a/src/Hercules/Agent/WorkerProtocol/Event/Attribute.hs b/src/Hercules/Agent/WorkerProtocol/Event/Attribute.hs
--- a/src/Hercules/Agent/WorkerProtocol/Event/Attribute.hs
+++ b/src/Hercules/Agent/WorkerProtocol/Event/Attribute.hs
@@ -6,10 +6,17 @@
 import Protolude
 import Prelude ()
 
-data Attribute
-  = Attribute
-      { path :: [ByteString],
-        drv :: ByteString
-        -- TODO: metadata
-      }
+data AttributeType
+  = Regular
+  | MustFail
+  | MayFail
+  | DependenciesOnly
+  | Effect
+  deriving (Generic, Binary, Show, Eq)
+
+data Attribute = Attribute
+  { path :: [ByteString],
+    drv :: ByteString,
+    typ :: AttributeType
+  }
   deriving (Generic, Binary, Show, Eq)
diff --git a/src/Hercules/Agent/WorkerProtocol/Event/AttributeError.hs b/src/Hercules/Agent/WorkerProtocol/Event/AttributeError.hs
--- a/src/Hercules/Agent/WorkerProtocol/Event/AttributeError.hs
+++ b/src/Hercules/Agent/WorkerProtocol/Event/AttributeError.hs
@@ -6,11 +6,10 @@
 import Protolude
 import Prelude ()
 
-data AttributeError
-  = AttributeError
-      { path :: [ByteString],
-        message :: Text,
-        errorType :: Maybe Text,
-        errorDerivation :: Maybe Text
-      }
+data AttributeError = AttributeError
+  { path :: [ByteString],
+    message :: Text,
+    errorType :: Maybe Text,
+    errorDerivation :: Maybe Text
+  }
   deriving (Generic, Binary, Show, Eq)
diff --git a/src/Hercules/Agent/WorkerProtocol/Event/BuildResult.hs b/src/Hercules/Agent/WorkerProtocol/Event/BuildResult.hs
--- a/src/Hercules/Agent/WorkerProtocol/Event/BuildResult.hs
+++ b/src/Hercules/Agent/WorkerProtocol/Event/BuildResult.hs
@@ -10,15 +10,14 @@
   | BuildSuccess {outputs :: [OutputInfo]}
   deriving (Generic, Binary, Show, Eq)
 
-data OutputInfo
-  = OutputInfo
-      { -- | e.g. out, dev
-        name :: ByteString,
-        -- | store path
-        path :: ByteString,
-        -- | typically sha256:...
-        hash :: ByteString,
-        -- | nar size in bytes
-        size :: Int64
-      }
+data OutputInfo = OutputInfo
+  { -- | e.g. out, dev
+    name :: ByteString,
+    -- | store path
+    path :: ByteString,
+    -- | typically sha256:...
+    hash :: ByteString,
+    -- | nar size in bytes
+    size :: Int64
+  }
   deriving (Generic, Binary, Show, Eq)
diff --git a/src/Hercules/Agent/WorkerProtocol/LogSettings.hs b/src/Hercules/Agent/WorkerProtocol/LogSettings.hs
--- a/src/Hercules/Agent/WorkerProtocol/LogSettings.hs
+++ b/src/Hercules/Agent/WorkerProtocol/LogSettings.hs
@@ -1,23 +1,17 @@
 {-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
 
 module Hercules.Agent.WorkerProtocol.LogSettings where
 
 import Data.Binary
+import Hercules.Agent.Sensitive
 import Protolude
-import Text.Show
 
-data LogSettings
-  = LogSettings
-      { path :: Text,
-        baseURL :: Text,
-        token :: Sensitive Text
-      }
+data LogSettings = LogSettings
+  { path :: Text,
+    baseURL :: Text,
+    token :: Sensitive Text
+  }
   deriving (Generic, Binary, Show, Eq)
-
--- | newtype wrapper to avoid leaking sensitive data through Show
-newtype Sensitive a = Sensitive {reveal :: a}
-  deriving (Generic, Binary, Eq, Ord)
-
--- | @const "<sensitive>"@
-instance Show (Sensitive a) where
-  show _ = "<sensitive>"
diff --git a/src/Hercules/Effect.hs b/src/Hercules/Effect.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/Effect.hs
@@ -0,0 +1,140 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Hercules.Effect where
+
+import Control.Monad.Catch (MonadThrow)
+import qualified Data.Aeson as A
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as BL
+import qualified Data.Map as M
+import Foreign (ForeignPtr)
+import Hercules.Agent.Sensitive (Sensitive (Sensitive, reveal), revealContainer)
+import Hercules.CNix.Store (getDerivationArguments, getDerivationBuilder, getDerivationEnv)
+import Hercules.CNix.Store.Context (Derivation)
+import Hercules.Effect.Container (BindMount (BindMount))
+import qualified Hercules.Effect.Container as Container
+import Hercules.Error (escalateAs)
+import qualified Hercules.Formats.Secret as Formats.Secret
+import Katip (KatipContext, Severity (..), logLocM, logStr)
+import Protolude
+import System.FilePath
+import UnliftIO.Directory (createDirectory, createDirectoryIfMissing)
+
+parseDrvSecretsMap :: Map ByteString ByteString -> Either Text (Map Text Text)
+parseDrvSecretsMap drvEnv =
+  case drvEnv & M.lookup "secretsMap" of
+    Nothing -> pure mempty
+    Just secretsMapText -> case A.eitherDecode (BL.fromStrict secretsMapText) of
+      Left _ -> Left "Could not parse secretsMap variable in derivation. It must be a JSON dictionary of strings referencing agent secret names."
+      Right r -> Right r
+
+-- | Write secrets to file based on secretsMap value
+writeSecrets :: (MonadIO m, KatipContext m) => FilePath -> Map Text Text -> Map Text (Sensitive Formats.Secret.Secret) -> FilePath -> m ()
+writeSecrets sourceFile secretsMap extraSecrets destinationDirectory = write . fmap reveal . addExtra =<< gather
+  where
+    addExtra = flip M.union extraSecrets
+    write = liftIO . BS.writeFile (destinationDirectory </> "secrets.json") . BL.toStrict . A.encode
+    gather =
+      if null secretsMap
+        then pure mempty
+        else do
+          secretsBytes <- liftIO $ BS.readFile sourceFile
+          r <- case A.eitherDecode $ BL.fromStrict secretsBytes of
+            Left e -> do
+              logLocM ErrorS $ "Could not parse secrets file " <> logStr sourceFile <> ": " <> logStr e
+              throwIO $ FatalError "Could not parse secrets file as configured on agent."
+            Right r -> pure (Sensitive r)
+          createDirectoryIfMissing True destinationDirectory
+          secretsMap & M.traverseWithKey \destinationName (secretName :: Text) -> do
+            case revealContainer (r <&> M.lookup secretName) of
+              Nothing ->
+                liftIO $
+                  throwIO $
+                    FatalError $
+                      "Secret " <> secretName <> " does not exist, so we can't find a secret for " <> destinationName <> ". Please make sure that the secret name matches a secret on your agents."
+              Just ssecret ->
+                pure do
+                  secret <- ssecret
+                  -- Currently this is `id` but we might want to fork the
+                  -- format here or omit some fields.
+                  pure $
+                    Formats.Secret.Secret
+                      { data_ = Formats.Secret.data_ secret
+                      }
+
+runEffect :: (MonadThrow m, KatipContext m) => ForeignPtr Derivation -> Sensitive Text -> FilePath -> Text -> FilePath -> m ExitCode
+runEffect derivation token secretsPath apiBaseURL dir = do
+  drvBuilder <- liftIO $ getDerivationBuilder derivation
+  drvArgs <- liftIO $ getDerivationArguments derivation
+  drvEnv <- liftIO $ getDerivationEnv derivation
+  drvSecretsMap <- escalateAs FatalError $ parseDrvSecretsMap drvEnv
+  let mkDir d = let newDir = dir </> d in toS newDir <$ createDirectory newDir
+  buildDir <- mkDir "build"
+  etcDir <- mkDir "etc"
+  secretsDir <- mkDir "secrets"
+  runcDir <- mkDir "runc-state"
+  let extraSecrets =
+        M.singleton
+          "hercules-ci"
+          ( do
+              tok <- token
+              pure $
+                Formats.Secret.Secret
+                  { data_ = M.singleton "token" $ A.String tok
+                  }
+          )
+  writeSecrets secretsPath drvSecretsMap extraSecrets (toS secretsDir)
+  liftIO $ do
+    -- Nix sandbox sets tmp to buildTopDir
+    -- Nix sandbox reference: https://github.com/NixOS/nix/blob/24e07c428f21f28df2a41a7a9851d5867f34753a/src/libstore/build.cc#L2545
+    --
+    -- TODO: what if we have structuredAttrs?
+    -- TODO: implement passAsFile?
+    let overridableEnv, onlyImpureOverridableEnv, fixedEnv :: Map Text Text
+        overridableEnv =
+          M.fromList
+            [ ("PATH", "/path-not-set"),
+              ("HOME", "/homeless-shelter"),
+              ("NIX_STORE", "/nix/store"), -- TODO store.storeDir
+              ("NIX_BUILD_CORES", "1"), -- not great
+              ("NIX_REMOTE", "daemon"),
+              ("IN_HERCULES_CI_EFFECT", "true"),
+              ("HERCULES_CI_API_BASE_URL", apiBaseURL),
+              ("HERCULES_CI_SECRETS_JSON", "/secrets/secrets.json")
+            ]
+        -- NB: this is lossy. Consider using ByteString-based process functions
+        drvEnv' = drvEnv & M.mapKeys (decodeUtf8With lenientDecode) & fmap (decodeUtf8With lenientDecode)
+        impureEnvVars = mempty -- TODO
+        fixedEnv =
+          M.fromList
+            [ ("NIX_LOG_FD", "2"),
+              ("TERM", "xterm-256color")
+            ]
+        onlyImpureOverridableEnv =
+          M.fromList
+            [ ("NIX_BUILD_TOP", "/build"),
+              ("TMPDIR", "/build"),
+              ("TEMPDIR", "/build"),
+              ("TMP", "/build"),
+              ("TEMP", "/build")
+            ]
+        (//) :: Ord k => Map k a -> Map k a -> Map k a
+        (//) = flip M.union
+    Container.run
+      runcDir
+      Container.Config
+        { extraBindMounts =
+            [ BindMount {pathInContainer = "/build", pathInHost = buildDir, readOnly = False},
+              BindMount {pathInContainer = "/etc", pathInHost = etcDir, readOnly = False},
+              BindMount {pathInContainer = "/secrets", pathInHost = secretsDir, readOnly = True},
+              BindMount {pathInContainer = "/etc/resolv.conf", pathInHost = "/etc/resolv.conf", readOnly = True},
+              BindMount {pathInContainer = "/nix/var/nix/daemon-socket/socket", pathInHost = "/nix/var/nix/daemon-socket/socket", readOnly = True}
+            ],
+          executable = decodeUtf8With lenientDecode drvBuilder,
+          arguments = map (decodeUtf8With lenientDecode) drvArgs,
+          environment = overridableEnv // drvEnv' // onlyImpureOverridableEnv // impureEnvVars // fixedEnv,
+          workingDirectory = "/build",
+          hostname = "hercules-ci",
+          rootReadOnly = False
+        }
diff --git a/src/Hercules/Effect/Container.hs b/src/Hercules/Effect/Container.hs
new file mode 100644
--- /dev/null
+++ b/src/Hercules/Effect/Container.hs
@@ -0,0 +1,138 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE NumericUnderscores #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+module Hercules.Effect.Container where
+
+import Control.Lens
+import Data.Aeson (Value (String), eitherDecode, encode, object, toJSON)
+import Data.Aeson.Lens
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as BL
+import qualified Data.Map as M
+import qualified Data.UUID.V4 as UUID
+import GHC.IO.Exception (IOErrorType (HardwareFault))
+import Protolude
+import System.Directory (createDirectory)
+import System.FilePath ((</>))
+import System.IO.Error (ioeGetErrorType)
+import System.Posix.IO (closeFd, fdToHandle)
+import System.Posix.Terminal (openPseudoTerminal)
+import System.Process (CreateProcess (..), StdStream (UseHandle), proc, waitForProcess, withCreateProcess)
+import System.Process.ByteString (readCreateProcessWithExitCode)
+
+data BindMount = BindMount
+  { pathInContainer :: Text,
+    pathInHost :: Text,
+    readOnly :: Bool
+  }
+
+defaultBindMount :: Text -> BindMount
+defaultBindMount path = BindMount {pathInContainer = path, pathInHost = path, readOnly = True}
+
+data Config = Config
+  { extraBindMounts :: [BindMount],
+    executable :: Text,
+    arguments :: [Text],
+    environment :: Map Text Text,
+    workingDirectory :: Text,
+    hostname :: Text,
+    rootReadOnly :: Bool
+  }
+
+effectToRuncSpec :: Config -> Value -> Value
+effectToRuncSpec config spec =
+  let defaultMounts = [defaultBindMount "/nix/store"]
+      mounts =
+        foldMap
+          ( \bindMount ->
+              pure $
+                object
+                  [ ("destination", String $ pathInContainer bindMount),
+                    ("source", String $ pathInHost bindMount),
+                    ("type", "bind"),
+                    ( "options",
+                      toJSON $
+                        ["bind" :: Text]
+                          <> ["ro" | readOnly bindMount]
+                    )
+                  ]
+          )
+          (defaultMounts <> extraBindMounts config)
+   in spec
+        & key "process" . key "args" .~ toJSON ([executable config] <> arguments config)
+        & key "mounts" . _Array %~ (<> mounts)
+        & key "process" . key "terminal" .~ toJSON True
+        & key "process" . key "env" .~ toJSON (config & environment & M.toList <&> \(k, v) -> k <> "=" <> v)
+        & key "process" . key "cwd" .~ toJSON (config & workingDirectory)
+        & key "hostname" .~ toJSON (config & hostname)
+        & key "root" . key "readonly" .~ toJSON (config & rootReadOnly)
+
+run :: FilePath -> Config -> IO ExitCode
+run dir config = do
+  let runcExe = "runc"
+      createConfigJsonSpec =
+        (System.Process.proc runcExe ["spec", "--rootless"])
+          { cwd = Just dir
+          }
+      configJsonPath = dir </> "config.json"
+  (exit, _out, err) <- readCreateProcessWithExitCode createConfigJsonSpec ""
+  case exit of
+    ExitSuccess -> pass
+    ExitFailure e -> do
+      putErrText (decodeUtf8With lenientDecode err)
+      panic $ "Could not create container configuration template. runc terminated with exit code " <> show e
+  templateBytes <- BS.readFile configJsonPath
+  template <- case eitherDecode (BL.fromStrict templateBytes) of
+    Right a -> pure a
+    Left e -> throwIO (FatalError $ "decoding runc config.json template: " <> show e)
+  let configJson = effectToRuncSpec config template
+  BS.writeFile (dir </> "config.json") (BL.toStrict $ encode configJson)
+  createDirectory (dir </> "rootfs")
+  name <- do
+    uuid <- UUID.nextRandom
+    pure $ "hercules-ci-" <> show uuid
+  (exitCode, _) <- bracket
+    openPseudoTerminal
+    ( \(fd1, fd2) -> handle (\(_e :: SomeException) -> pass) do
+        closeFd fd1
+        when (fd2 /= fd1) (closeFd fd2)
+    )
+    $ \(master, terminal) -> do
+      concurrently
+        ( do
+            terminalHandle <- fdToHandle terminal
+            let createProcSpec =
+                  (System.Process.proc runcExe ["run", name])
+                    { std_in = UseHandle terminalHandle, -- can't pass /dev/null :(
+                      std_out = UseHandle terminalHandle,
+                      std_err = UseHandle terminalHandle,
+                      cwd = Just dir
+                    }
+            withCreateProcess createProcSpec \_subStdin _noOut _noErr processHandle -> do
+              waitForProcess processHandle
+                `onException` ( do
+                                  putErrText "Terminating effect process..."
+                                  _ <- System.Process.withCreateProcess (System.Process.proc runcExe ["kill", name]) \_ _ _ kh ->
+                                    waitForProcess kh
+                                  threadDelay 3_000_000
+                                  _ <- System.Process.withCreateProcess (System.Process.proc runcExe ["kill", name, "KILL"]) \_ _ _ kh ->
+                                    waitForProcess kh
+                                  putErrText "Killed effect process."
+                              )
+        )
+        ( do
+            masterHandle <- fdToHandle master
+            let shovel =
+                  handleEOF (BS.hGetLine masterHandle) >>= \case
+                    "" -> pass
+                    someBytes | "@nix" `BS.isPrefixOf` someBytes -> do
+                      -- TODO use it (example @nix { "action": "setPhase", "phase": "effectPhase" })
+                      shovel
+                    someBytes -> do
+                      BS.hPut stderr (someBytes <> "\n")
+                      shovel
+                handleEOF = handle \e -> if ioeGetErrorType e == HardwareFault then pure "" else throwIO e
+            shovel
+        )
+  pure exitCode
diff --git a/test/Hercules/Agent/Nix/RetrieveDerivationInfoSpec.hs b/test/Hercules/Agent/Nix/RetrieveDerivationInfoSpec.hs
--- a/test/Hercules/Agent/Nix/RetrieveDerivationInfoSpec.hs
+++ b/test/Hercules/Agent/Nix/RetrieveDerivationInfoSpec.hs
@@ -2,10 +2,10 @@
 
 module Hercules.Agent.Nix.RetrieveDerivationInfoSpec where
 
-import CNix
 import qualified Data.Map as M
 import Hercules.API.Agent.Evaluate.EvaluateEvent.DerivationInfo
 import Hercules.Agent.Nix.RetrieveDerivationInfo
+import Hercules.CNix
 import Protolude
 import Test.Hspec
 
diff --git a/test/TestMain.hs b/test/TestMain.hs
--- a/test/TestMain.hs
+++ b/test/TestMain.hs
@@ -1,13 +1,13 @@
 module Main where
 
-import CNix
+import Hercules.CNix (init)
 import Protolude
 import qualified Spec
 import Test.Hspec.Runner
 
 main :: IO ()
 main = do
-  CNix.init
+  init
   hspecWith config Spec.spec
   where
     config = defaultConfig {configColorMode = ColorAlways}
