diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # libnix
 
+## 0.4.1.0 -- 2021-11-23
+
+* Add `executables` to the `RunOptions`. Backwards compatible if you use `defaultRunOptions`.
+
 ## 0.4.0.1 -- 2021-11-19
 
 * Fix `Prefetch.url`. The constructed command was missing flags for `urlUnpack = True`.
diff --git a/Foreign/Nix/Shellout.hs b/Foreign/Nix/Shellout.hs
--- a/Foreign/Nix/Shellout.hs
+++ b/Foreign/Nix/Shellout.hs
@@ -58,17 +58,20 @@
   deriving (Show, Eq)
 
 -- | Parse a nix expression and check for syntactic validity.
+
+-- Runs @nix-instantiate@.
 parseNixExpr :: MonadIO m => Text -> NixAction ParseError m NixExpr
-parseNixExpr e =
+parseNixExpr e = do
+  exec <- Helpers.getExecOr exeNixInstantiate "nix-instantiate"
   mapActionError parseParseError
     $ NixExpr
-    <$> evalNixOutput "nix-instantiate" [ "--parse", "-E", e ]
+    <$> evalNixOutput exec [ "--parse", "-E", e ]
 
 
 parseParseError :: Text -> ParseError
 parseParseError
   (stripPrefix "error: syntax error, "
-               -> Just mes) = SyntaxError $ mes
+               -> Just mes) = SyntaxError mes
 parseParseError _           = UnknownParseError
 
 ------------------------------------------------------------------------------
@@ -82,18 +85,25 @@
   deriving (Show, Eq)
 
 -- | Instantiate a parsed expression into a derivation.
+--
+-- Runs @nix-instantiate@.
 instantiate :: (MonadIO m) => NixExpr -> NixAction InstantiateError m (StorePath Derivation)
-instantiate (NixExpr e) =
+instantiate (NixExpr e) = do
+  exec <- Helpers.getExecOr exeNixInstantiate "nix-instantiate"
   mapActionError parseInstantiateError
-    $ evalNixOutput "nix-instantiate" [ "-E", e ]
+    $ evalNixOutput exec [ "-E", e ]
       >>= toNixFilePath StorePath
 
 -- | Just tests if the expression can be evaluated.
 -- That doesn’t mean it has to instantiate however.
+--
+-- Runs @nix-instantiate@.
 eval :: MonadIO m => NixExpr -> NixAction InstantiateError m ()
 eval (NixExpr e) = do
+  exec <- Helpers.getExecOr exeNixInstantiate "nix-instantiate"
+
   _instantiateOutput <- mapActionError parseInstantiateError
-       $ evalNixOutput "nix-instantiate" [ "--eval", "-E", e ]
+       $ evalNixOutput exec [ "--eval", "-E", e ]
   pure ()
 
 parseInstantiateError :: Text -> InstantiateError
@@ -110,18 +120,23 @@
 
 -- | Finally derivations are realized into full store outputs.
 -- This will typically take a while so it should be executed asynchronously.
+--
+-- Runs @nix-store@.
 realize :: MonadIO m => StorePath Derivation -> NixAction RealizeError m (StorePath Realized)
 realize (StorePath d) =
      storeOp [ "-r", Text.pack d ]
 
 -- | Copy the given file or folder to the nix store and return it’s path.
+--
+-- Runs @nix-store@.
 addToStore :: MonadIO m => FilePath -> NixAction RealizeError m (StorePath Realized)
 addToStore fp = storeOp [ "--add", Text.pack fp ]
 
 storeOp :: (MonadIO m) => [Text] -> NixAction RealizeError m (StorePath Realized)
-storeOp op =
+storeOp op = do
+  exec <- Helpers.getExecOr exeNixInstantiate "nix-store"
   mapActionError (const UnknownRealizeError)
-    $ evalNixOutput "nix-store" op
+    $ evalNixOutput exec op
       >>= toNixFilePath StorePath
 
 ------------------------------------------------------------------------------
@@ -145,7 +160,7 @@
 
 -- | Take args and return either error message or output path
 evalNixOutput :: (MonadIO m)
-              => Text
+              => Helpers.Executable
               -- ^ name of executable
               -> [Text]
               -- ^ arguments
diff --git a/Foreign/Nix/Shellout/Helpers.hs b/Foreign/Nix/Shellout/Helpers.hs
--- a/Foreign/Nix/Shellout/Helpers.hs
+++ b/Foreign/Nix/Shellout/Helpers.hs
@@ -3,7 +3,7 @@
 {-# LANGUAGE FlexibleContexts #-}
 module Foreign.Nix.Shellout.Helpers where
 
-import Foreign.Nix.Shellout.Types ( NixActionError(..), RunOptions (logFn), LogFn (LogFn), NixAction )
+import Foreign.Nix.Shellout.Types ( NixActionError(..), RunOptions (logFn, executables), LogFn (LogFn), NixAction, Executables )
 import qualified System.Process as P
 import qualified Data.Text.IO as TIO
 import qualified Data.Text as T
@@ -26,26 +26,47 @@
 import Control.Monad.Except (MonadError (throwError))
 import Control.Monad.Reader (asks)
 import Control.Monad.Trans (lift)
+import Data.Function ((&))
 
+-- | Something we can run
+data Executable =
+  ExeFromPathEnv Text
+  -- ^ name of the executable, to be looked up in PATH
+  | ExeFromFilePath FilePath
+  -- ^ a file path to the executable (can be relative or absolute)
+
+-- | Get an executable from the 'Executables' option (by its getter)
+-- or if not set use the given 'Text' as the name of the excutable
+-- to be looked up in @PATH@.
+getExecOr :: Monad m => (Executables -> Maybe FilePath) -> Text ->  NixAction e m Executable
+getExecOr getter exeName =
+  let f = \case
+        Nothing -> ExeFromPathEnv exeName
+        Just fp -> ExeFromFilePath fp
+  in asks (f . getter . executables)
+
 -- | Read the output of a process into a NixAction.
 -- | Keeps stderr if process returns a failure exit code.
 -- | The text is decoded as @UTF-8@.
 readProcess :: (MonadIO m)
             => ((Text, Text) -> ExitCode -> ExceptT e m a)
             -- ^ handle (stdout, stderr) depending on the return value
-            -> Text
-            -- ^ name of executable
+            -> Executable
+            -- ^ executable to run
             -> [Text]
             -- ^ arguments
             -> NixAction e m a
 readProcess with exec args = do
+  let exec' = case exec of
+        ExeFromPathEnv name -> name
+        ExeFromFilePath fp -> fp & Text.pack
   -- log every call based on the LogFn the user passed
   (LogFn l) <- asks logFn
-  lift $ l exec args
+  lift $ l exec' args
 
   (exc, out, err) <- liftIO
     $ readCreateProcessWithExitCodeAndEncoding
-        (P.proc (Text.unpack exec) (map Text.unpack args)) SIO.utf8 ""
+        (P.proc (Text.unpack exec') (map Text.unpack args)) SIO.utf8 ""
   lift (runExceptT (with (out, err) exc)) >>= \case
     Left e ->
       throwError $ NixActionError
diff --git a/Foreign/Nix/Shellout/Prefetch.hs b/Foreign/Nix/Shellout/Prefetch.hs
--- a/Foreign/Nix/Shellout/Prefetch.hs
+++ b/Foreign/Nix/Shellout/Prefetch.hs
@@ -40,6 +40,7 @@
 import qualified Data.Text.Lazy.Encoding as Text.Lazy.Encoding
 import qualified Data.List as List
 import Control.Monad.IO.Class (MonadIO)
+import Foreign.Nix.Shellout.Helpers (getExecOr)
 
 data PrefetchError
   = PrefetchOutputMalformed Text
@@ -76,9 +77,10 @@
 
 -- | Runs @nix-prefetch-url@.
 url :: (MonadIO m) => UrlOptions -> NixAction PrefetchError m (Sha256, StorePath Realized)
-url UrlOptions{..} = Helpers.readProcess handler exec args
+url UrlOptions{..} = do
+  exec <- Helpers.getExecOr exeNixPrefetchUrl "nix-prefetch-url"
+  Helpers.readProcess handler exec args
   where
-    exec = "nix-prefetch-url"
     args =  bool [] ["--unpack"] urlUnpack
          <> maybe [] (\n -> ["--name", n]) urlName
          <> [ "--type", "sha256"
@@ -89,8 +91,8 @@
     handler (out, err) = \case
       ExitSuccess -> withExceptT PrefetchOutputMalformed $ do
         let ls = T.lines $ T.stripEnd out
-        path <- tryLast (exec <> " didn’t output a store path") ls
-        sha  <- let errS = (exec <> " didn’t output a hash")
+        path <- tryLast "nix-prefetch-url didn’t output a store path" ls
+        sha  <- let errS = "nix-prefetch-url didn’t output a hash"
                 in tryInit errS ls >>= tryLast errS
         pure (Sha256 sha, StorePath $ Text.unpack path)
       ExitFailure _ -> throwE $
@@ -137,9 +139,10 @@
 
 -- | Runs @nix-prefetch-git@.
 git :: (MonadIO m) => GitOptions -> NixAction PrefetchError m GitOutput
-git GitOptions{..} = Helpers.readProcess handler exec args
+git GitOptions{..} = do
+  exec <- getExecOr exeNixPrefetchGit "nix-prefetch-git"
+  Helpers.readProcess handler exec args
   where
-    exec = "nix-prefetch-git"
     args =  bool ["--no-deepClone"] ["--deepClone"] gitDeepClone
          <> bool [] ["--leave-dotGit"] gitLeaveDotGit
          <> bool [] ["--fetch-submodules"] gitFetchSubmodules
@@ -153,7 +156,7 @@
 
     handler (out, err) = \case
       ExitSuccess -> withExceptT PrefetchOutputMalformed $ do
-        let error' msg = exec <> " " <> msg
+        let error' msg = "nix-prefetch-git " <> msg
             jsonError :: [Char] -> Text
             jsonError = \msg -> error' (T.intercalate "\n"
                       [ "parsing json output failed:"
@@ -181,6 +184,6 @@
         pure GitOutput{..}
 
       ExitFailure _ -> throwE $
-        if ("hash mismatch for URL" `T.isInfixOf` err)
+        if "hash mismatch for URL" `T.isInfixOf` err
         then ExpectedHashError
         else UnknownPrefetchError
diff --git a/Foreign/Nix/Shellout/Types.hs b/Foreign/Nix/Shellout/Types.hs
--- a/Foreign/Nix/Shellout/Types.hs
+++ b/Foreign/Nix/Shellout/Types.hs
@@ -16,6 +16,8 @@
   RunOptions(..),
   defaultRunOptions,
   LogFn(..),
+  Executables(..),
+  defaultExecutables,
   NixActionError(..),
   mapActionError,
 
@@ -32,9 +34,11 @@
 --
 -- Might get more fields in the future, use 'defaultRunOptions'
 -- to be backwards-compatbile.
-newtype RunOptions m = RunOptions {
-  logFn :: LogFn m
+data RunOptions m = RunOptions {
+  logFn :: LogFn m,
   -- ^ The command line logging function.
+  executables :: Executables
+  -- ^ A record of all executables this library could need.
 }
 
 -- | Logging function to call before running a command.
@@ -44,13 +48,43 @@
 -- the second argument is the list of arguments.
 newtype LogFn m = LogFn (Text -> [Text] -> m ())
 
+-- | All executables this library might need.
+--
+-- If you set an executable to @Just filepath@, the internal code will use the given path instead of looking up the executable name in @PATH@.
+-- This is useful if you want to ensure that the executables always exist before calling into this library.
+--
+-- 'NixAction' functions document the executables they use in their docstrings.
+--
+-- If an executable can’t be found, an 'IOException' is thrown (by the process spawn function).
+data Executables = Executables {
+  exeNixInstantiate :: Maybe FilePath,
+  -- ^ @nix-instantiate@; usually you can expect nix to exist on the system (but we don’t check before running the commands)
+  exeNixStore :: Maybe FilePath,
+  -- ^ @nix-store@
+  exeNixPrefetchUrl :: Maybe FilePath,
+  -- ^ @nix-prefetch-url@; as of nix @2.3@, this executable comes with the nix distribution, so if @nix-store@ is available, this should also be available
+  exeNixPrefetchGit :: Maybe FilePath
+  -- ^ @nix-prefetch-git@; This is usually provided by the @nix-prefetch-scripts@ package and /not/ installed with nix
+ }
+
+-- |  all executables are taken from @PATH@
+defaultExecutables :: Executables
+defaultExecutables = Executables {
+    exeNixInstantiate = Nothing,
+    exeNixStore = Nothing,
+    exeNixPrefetchUrl = Nothing,
+    exeNixPrefetchGit = Nothing
+  }
+
 -- |
 -- @
 -- logFn = nothing is done/logged
+-- executable = all executables are taken from @PATH@
 -- @
 defaultRunOptions :: Monad m => RunOptions m
 defaultRunOptions = RunOptions {
-  logFn = LogFn (\_prog _args -> pure ())
+  logFn = LogFn (\_prog _args -> pure ()),
+  executables = defaultExecutables
 }
 
 -- | Calls a command that returns an error and the whole stderr on failure.
diff --git a/libnix.cabal b/libnix.cabal
--- a/libnix.cabal
+++ b/libnix.cabal
@@ -1,5 +1,5 @@
 name:                libnix
-version:             0.4.0.1
+version:             0.4.1.0
 synopsis:            Bindings to the nix package manager
 description:         Use the nix package manager from Haskell. All modules are designed to be imported qualified.
 category:            Foreign, Nix
diff --git a/tests/TestShellout.hs b/tests/TestShellout.hs
--- a/tests/TestShellout.hs
+++ b/tests/TestShellout.hs
@@ -1,5 +1,8 @@
 {-# LANGUAGE RecordWildCards #-}
 {-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE LambdaCase #-}
 module TestShellout where
 
 import qualified Data.Text as T
@@ -13,6 +16,9 @@
 import Data.Text (Text)
 import Control.Monad.IO.Class (liftIO)
 import Control.Monad ((>=>))
+import Control.Exception (SomeException, try, Exception (displayException))
+import Data.Function ((&))
+import qualified Data.List as List
 
 
 shelloutTests :: TestTree
@@ -28,31 +34,37 @@
     , multilineErrors
     , helloWorld
     , copyTempfileToStore ]
+  , testGroup "executable paths"
+    [ badInstantiateFilePath ]
   ]
 
-syntaxError, infiniteRecursion, notADerivation, someDerivation :: TestTree
-nixpkgsExists, multilineErrors, helloWorld, copyTempfileToStore :: TestTree
 
+syntaxError :: TestTree
 syntaxError = testCase "syntax error"
   $ parseNixExpr ";" `isENoFail`
       Left ("", SyntaxError "unexpected ';', at (string):1:1")
 
+infiniteRecursion :: TestTree
 infiniteRecursion = testCase "infinite recursion"
   $ parseInst "let a = a; in a"
   `isE` Left ( "error: infinite recursion encountered, at (string):1:10"
               , UnknownInstantiateError)
 
+notADerivation :: TestTree
 notADerivation = testCase "not a derivation"
   $ parseInst "42"
   `isE` Left ("", NotADerivation)
 
+someDerivation :: TestTree
 someDerivation = testCase "a basic derivation"
   $ assertNoFailure $ parseInst
       "derivation { name = \"foo\"; builder = \" \"; system = \" \"; }"
 
+nixpkgsExists :: TestTree
 nixpkgsExists = testCase "nixpkgs is accessible"
   $ assertNoFailure $ parseEval "import <nixpkgs> {}"
 
+multilineErrors :: TestTree
 multilineErrors = testCase "nixpkgs multiline stderr it parsed"
   $ parseEval "builtins.abort ''wow\nsuch error''"
   `isE` Left
@@ -60,9 +72,11 @@
              \message: 'wow\nsuch error'"
            , UnknownInstantiateError)
 
+helloWorld :: TestTree
 helloWorld = testCase "build the GNU hello package"
   $ assertNoFailure $ parseInstRealize "with import <nixpkgs> {}; hello"
 
+copyTempfileToStore :: TestTree
 copyTempfileToStore = testCase "copy a temporary file to store"
   $ assertNoFailure $ do
     (fp, h) <- liftIO $
@@ -70,6 +84,15 @@
     liftIO $ hClose h
     addToStore fp
 
+badInstantiateFilePath :: TestTree
+badInstantiateFilePath = testCase "not a derivation"
+  $ isExcBad "No such file or directory" (parseInst "42")
+  where
+    isExcBad = isException (defaultRunOptions {
+      executables = defaultExecutables {
+        exeNixInstantiate = Just "/path/does/not/exist"
+      }
+    })
 
 --------------------------------------------------------------------
 -- Helpers
@@ -92,10 +115,26 @@
     -> Either (Text, e) a
        -- ^ Left (subset of stdout, error)
     -> Assertion
-isE na match = runNixAction defaultRunOptions na >>= \res ->
+isE = isE' defaultRunOptions
+
+isE' :: (Eq a, Eq e, Show a, Show e)
+    => RunOptions IO
+    -> NixAction e IO a
+    -> Either (Text, e) a
+       -- ^ Left (subset of stdout, error)
+    -> Assertion
+isE' runOptions na match = runNixAction runOptions na >>= \res ->
   case (match, res) of
     (Right a, Right b) -> a @=? b
     (match', res') -> check match' res'
+
+isException :: RunOptions IO -> [Char] -> NixAction e IO a -> IO ()
+isException runOptions matchText na = try @SomeException (runNixAction runOptions na) >>= \case
+  Left exc -> do
+    let text = exc & displayException
+    assertBool ("threw exception, but did not match the given substring\nshould have contained: " <> matchText <> "\nbut was: " <> text)
+      $ matchText `List.isInfixOf` text
+  Right _a -> assertFailure "runNixAction should have thrown an exception"
 
 isENoFail :: (Eq e, Show a, Show e)
           => NixAction e IO a
