diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,21 @@
 
 ## Unreleased
 
+## [0.3.0] - 2019-09-03
+
+### Changed
+
+- #222 allow NixOS installation only as root
+- #215 LTS-14.0 and replace stack2nix with haskell.nix
+- #216 add bulk store path querying as a performance optimization
+- #222 drop support for Nix 2.0.1 or lower
+
+### Fixed
+
+- #222 Provide guidance if /etc/nixos is not writable
+- #222 Human friendly exception reporting
+- #212 Use C++ to determine closure, avoids shell argument limit size
+
 ## [0.2.1] - 2019-07-05
 
 ### Added
@@ -94,7 +109,9 @@
 
 - Initial release @domenkozar
 
-[Unreleased]: https://github.com/cachix/cachix/compare/v0.1.4...HEAD
+[Unreleased]: https://github.com/cachix/cachix/compare/v0.3.0...HEAD
+[0.3.0]: https://github.com/cachix/cachix/compare/v0.2.1...v0.3.0
+[0.2.1]: https://github.com/cachix/cachix/compare/v0.2.0...v0.2.1
 [0.2.0]: https://github.com/cachix/cachix/compare/v0.1.3...v0.2.0
 [0.1.3]: https://github.com/cachix/cachix/compare/v0.1.2...v0.1.3
 [0.1.2]: https://github.com/cachix/cachix/compare/v0.1.1...v0.1.2
diff --git a/Setup.hs b/Setup.hs
--- a/Setup.hs
+++ b/Setup.hs
@@ -1,2 +1,3 @@
 import Distribution.Simple
+
 main = defaultMain
diff --git a/cachix.cabal b/cachix.cabal
--- a/cachix.cabal
+++ b/cachix.cabal
@@ -1,62 +1,76 @@
-cabal-version:  2.2
-name:           cachix
-version:        0.2.1
-synopsis:       Command line client for Nix binary cache hosting https://cachix.org
-homepage:       https://github.com/cachix/cachix#readme
-bug-reports:    https://github.com/cachix/cachix/issues
-author:         Domen Kožar
-maintainer:     domen@enlambda.com
-copyright:      2018 Domen Kožar
-license:        Apache-2.0
-license-file:   LICENSE
-build-type:     Simple
-data-files: *.input, *.output
-data-dir: test/data
+cabal-version:      2.2
+name:               cachix
+version:            0.3.0
+synopsis:
+  Command line client for Nix binary cache hosting https://cachix.org
+
+homepage:           https://github.com/cachix/cachix#readme
+bug-reports:        https://github.com/cachix/cachix/issues
+author:             Domen Kožar
+maintainer:         domen@enlambda.com
+copyright:          2018 Domen Kožar
+license:            Apache-2.0
+license-file:       LICENSE
+build-type:         Simple
 extra-source-files:
-    CHANGELOG.md
-    README.md
+  CHANGELOG.md
+  README.md
+  cbits/aliases.h
+  test/data/*.input
+  test/data/*.output
 
 common defaults
-  default-extensions: OverloadedStrings NoImplicitPrelude RecordWildCards DeriveGeneric
-  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -fwarn-tabs -fwarn-unused-imports -fwarn-missing-signatures -fwarn-name-shadowing -fwarn-incomplete-patterns
-  default-language: Haskell2010
+  default-extensions:
+    NoImplicitPrelude
+    DeriveGeneric
+    OverloadedStrings
+    RecordWildCards
 
+  ghc-options:
+    -Wall -Wcompat -Wincomplete-record-updates
+    -Wincomplete-uni-patterns -Wredundant-constraints -fwarn-tabs
+    -fwarn-unused-imports -fwarn-missing-signatures
+    -fwarn-name-shadowing -fwarn-incomplete-patterns
+
+  default-language:   Haskell2010
+
 source-repository head
-  type: git
+  type:     git
   location: https://github.com/cachix/cachix
 
 library
-  import: defaults
+  import:            defaults
   exposed-modules:
-      Cachix.Client
-      Cachix.Client.Commands
-      Cachix.Client.Config
-      Cachix.Client.Env
-      Cachix.Client.Exception
-      Cachix.Client.InstallationMode
-      Cachix.Client.NetRc
-      Cachix.Client.NixConf
-      Cachix.Client.NixVersion
-      Cachix.Client.OptionsParser
-      Cachix.Client.Push
-      Cachix.Client.Secrets
-      Cachix.Client.Servant
-      Cachix.Client.URI
-  other-modules:
-      Paths_cachix
-  autogen-modules:
-      Paths_cachix
-  hs-source-dirs:
-      src
+    Cachix.Client
+    Cachix.Client.Commands
+    Cachix.Client.Config
+    Cachix.Client.Config.Orphans
+    Cachix.Client.Env
+    Cachix.Client.Exception
+    Cachix.Client.InstallationMode
+    Cachix.Client.NetRc
+    Cachix.Client.NixConf
+    Cachix.Client.NixVersion
+    Cachix.Client.OptionsParser
+    Cachix.Client.Push
+    Cachix.Client.Secrets
+    Cachix.Client.Servant
+    Cachix.Client.Store
+    Cachix.Client.Store.Context
+    Cachix.Client.URI
+
+  other-modules:     Paths_cachix
+  autogen-modules:   Paths_cachix
+  hs-source-dirs:    src
   build-depends:
-      async
-    , base >=4.7 && <5
-    , base16-bytestring
+    , async
+    , base                  >=4.7     && <5
     , base64-bytestring
     , bytestring
     , cachix-api
-    , conduit >=1.3.0
+    , conduit               >=1.3.0
     , conduit-extra
+    , containers
     , cookie
     , cryptonite
     , dhall
@@ -65,12 +79,15 @@
     , filepath
     , fsnotify
     , here
+    , hnix-store-core
     , http-client
     , http-client-tls
     , http-conduit
     , http-types
+    , inline-c
+    , inline-c-cpp
     , lzma-conduit
-    , megaparsec >= 7.0.0
+    , megaparsec            >=7.0.0
     , memory
     , mmorph
     , netrc
@@ -80,53 +97,62 @@
     , resourcet
     , retry
     , safe-exceptions
-    , servant >=0.15
+    , servant               >=0.15
     , servant-auth
-    , servant-auth-client >=0.3.3.0
-    , servant-client >=0.15
-    , servant-client-core >=0.15
+    , servant-auth-client   >=0.3.3.0
+    , servant-client        >=0.15
+    , servant-client-core   >=0.15
     , servant-conduit
     , text
     , unix
     , uri-bytestring
     , versions
 
+  ghc-options:       -optc=-std=c++14
+
+  -- match what Nix is using
+  if os(osx)
+    ghc-options: -pgmc=clang++
+
+  cc-options:        -Wall
+  include-dirs:      cbits
+  extra-libraries:   stdc++
+  pkgconfig-depends: nix-store ==2.0 || >2.0, nix-main ==2.0 || >2.0
+
 executable cachix
-  import: defaults
-  main-is: Main.hs
-  other-modules:
-      Paths_cachix
-  autogen-modules:
-      Paths_cachix
-  hs-source-dirs:
-      cachix
-  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+  import:             defaults
+  main-is:            Main.hs
+  other-modules:      Paths_cachix
+  autogen-modules:    Paths_cachix
+  hs-source-dirs:     cachix
+  ghc-options:        -threaded -rtsopts -with-rtsopts=-N
   build-depends:
-      base >=4.7 && <5
+    , base        >=4.7 && <5
     , cachix
     , cachix-api
-  build-tool-depends: hspec-discover:hspec-discover
 
+  build-tool-depends: hspec-discover:hspec-discover -any
+
 test-suite cachix-test
-  import: defaults
-  type: exitcode-stdio-1.0
-  main-is: Main.hs
+  import:         defaults
+  type:           exitcode-stdio-1.0
+  main-is:        Main.hs
   other-modules:
-      InstallationModeSpec
-      NetRcSpec
-      NixConfSpec
-      NixVersionSpec
-      Spec
-      Paths_cachix
-  hs-source-dirs:
-      test
-  ghc-options: -threaded -rtsopts -with-rtsopts=-N
+    InstallationModeSpec
+    NetRcSpec
+    NixConfSpec
+    NixVersionSpec
+    Paths_cachix
+    Spec
+
+  hs-source-dirs: test
+  ghc-options:    -threaded -rtsopts -with-rtsopts=-N
   build-depends:
-      base >=4.7 && <5
+    , base        >=4.7 && <5
     , cachix
     , cachix-api
     , directory
+    , here
     , hspec
     , protolude
-    , here
     , temporary
diff --git a/cachix/Main.hs b/cachix/Main.hs
--- a/cachix/Main.hs
+++ b/cachix/Main.hs
@@ -1,13 +1,11 @@
 module Main (main) where
 
-import Prelude
-import Control.Exception (handle, SomeException, fromException)
-import System.IO
-import System.Exit (exitFailure, exitWith)
-import GHC.IO.Encoding
-
 import qualified Cachix.Client as CC
-
+import Cachix.Client.Exception (CachixException)
+import Control.Exception (displayException, handle)
+import GHC.IO.Encoding
+import System.Exit (exitFailure)
+import System.IO
 
 main :: IO ()
 main = do
@@ -19,10 +17,8 @@
 handleExceptions :: IO a -> IO a
 handleExceptions = handle handler
   where
-    handler :: SomeException -> IO a
-    handler e | (Just ee) <- fromException e = exitWith ee
+    handler :: CachixException -> IO a
     handler e = do
       hPutStrLn stderr ""
-      -- TODO: pretty print the record once fixed https://github.com/haskell-servant/servant/issues/807
-      hPrint stderr e
+      hPutStr stderr (displayException e)
       exitFailure
diff --git a/cbits/aliases.h b/cbits/aliases.h
new file mode 100644
--- /dev/null
+++ b/cbits/aliases.h
@@ -0,0 +1,8 @@
+#pragma once
+
+// 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
+// when it is supported by inline-c-cpp.
+typedef nix::ref<nix::Store> refStore;
+typedef nix::ref<const nix::ValidPathInfo> refValidPathInfo;
+typedef nix::PathSet::iterator PathSetIterator;
diff --git a/src/Cachix/Client.hs b/src/Cachix/Client.hs
--- a/src/Cachix/Client.hs
+++ b/src/Cachix/Client.hs
@@ -1,13 +1,12 @@
 module Cachix.Client
   ( main
-  ) where
+    )
+where
 
+import Cachix.Client.Commands as Commands
+import Cachix.Client.Env (cachixVersion, mkEnv)
+import Cachix.Client.OptionsParser (CachixCommand (..), getOpts)
 import Protolude
-
-import Cachix.Client.OptionsParser ( CachixCommand(..), getOpts )
-import Cachix.Client.Commands      as Commands
-import Cachix.Client.Env           ( mkEnv, cachixVersion )
-
 
 main :: IO ()
 main = do
diff --git a/src/Cachix/Client/Commands.hs b/src/Cachix/Client/Commands.hs
--- a/src/Cachix/Client/Commands.hs
+++ b/src/Cachix/Client/Commands.hs
@@ -1,95 +1,100 @@
-{-# LANGUAGE QuasiQuotes #-}
-{-# LANGUAGE CPP #-}
 {-# LANGUAGE DataKinds #-}
-{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE ScopedTypeVariables #-}
-module Cachix.Client.Commands
-  ( authtoken
-  , create
-  , generateKeypair
-  , push
-  , use
-  ) where
+{-# LANGUAGE TypeOperators #-}
 
-import           Crypto.Sign.Ed25519
-import           Control.Concurrent           (threadDelay)
-import           Control.Exception.Safe       (throwM)
-import           Control.Retry                (RetryStatus(rsIterNumber))
-import qualified Data.ByteString.Base64        as B64
-import           Data.List                      ( isSuffixOf )
-import           Data.Maybe                     ( fromJust )
-import           Data.String.Here
-import qualified Data.Text                      as T
-import           Network.HTTP.Types             (status404, status401)
-import           Protolude
-import           Servant.Auth.Client
-import           Servant.API                    ( NoContent )
-import           Servant.Client.Streaming
-import           Servant.Conduit                ()
-import           System.Directory               ( doesFileExist )
-import           System.FSNotify
-import           System.IO                      ( stdin, hIsTerminalDevice )
-import           System.Environment             ( lookupEnv )
+module Cachix.Client.Commands
+  ( authtoken,
+    create,
+    generateKeypair,
+    push,
+    use
+    )
+where
 
-import qualified Cachix.Api                    as Api
-import           Cachix.Api.Error
+import qualified Cachix.Api as Api
+import Cachix.Api.Error
+import Cachix.Client.Config
+  ( BinaryCacheConfig (..),
+    Config (..),
+    mkConfig,
+    writeConfig
+    )
+import qualified Cachix.Client.Config as Config
+import Cachix.Client.Env (Env (..))
+import Cachix.Client.Exception (CachixException (..))
+import Cachix.Client.InstallationMode
+import qualified Cachix.Client.NixConf as NixConf
+import Cachix.Client.NixVersion (assertNixVersion)
+import Cachix.Client.OptionsParser
+  ( CachixOptions (..),
+    PushArguments (..),
+    PushOptions (..)
+    )
+import Cachix.Client.Push
+import Cachix.Client.Secrets
+  ( SigningKey (SigningKey),
+    exportSigningKey,
+    parseSigningKeyLenient
+    )
+import Cachix.Client.Servant
 import qualified Cachix.Types.SigningKeyCreate as SigningKeyCreate
-import           Cachix.Client.Config           ( Config(..)
-                                                , BinaryCacheConfig(..)
-                                                , writeConfig
-                                                , mkConfig
-                                                )
-import qualified Cachix.Client.Config          as Config
-import           Cachix.Client.Env              ( Env(..) )
-import           Cachix.Client.Exception        ( CachixException(..) )
-import           Cachix.Client.OptionsParser    ( CachixOptions(..), UseOptions(..)
-                                                , PushArguments(..), PushOptions(..)
-                                                )
-import           Cachix.Client.InstallationMode
-import           Cachix.Client.NixVersion       ( getNixVersion )
-import qualified Cachix.Client.NixConf         as NixConf
-import           Cachix.Client.Push
-import           Cachix.Client.Secrets          ( SigningKey(SigningKey)
-                                                , parseSigningKeyLenient
-                                                , exportSigningKey
-                                                )
-import           Cachix.Client.Servant
-
+import Control.Concurrent (threadDelay)
+import Control.Exception.Safe (throwM)
+import Control.Retry (RetryStatus (rsIterNumber))
+import Crypto.Sign.Ed25519
+import qualified Data.ByteString.Base64 as B64
+import Data.List (isSuffixOf)
+import Data.Maybe (fromJust)
+import Data.String.Here
+import qualified Data.Text as T
+import Network.HTTP.Types (status401, status404)
+import Protolude
+import Servant.API (NoContent)
+import Servant.Auth.Client
+import Servant.Client.Streaming
+import Servant.Conduit ()
+import System.Directory (doesFileExist)
+import System.Environment (lookupEnv)
+import System.FSNotify
+import System.IO (hIsTerminalDevice, stdin)
 
 authtoken :: Env -> Text -> IO ()
 authtoken env token = do
   -- TODO: check that token actually authenticates!
   writeConfig (configPath (cachixoptions env)) $ case config env of
-    Just config -> config { authToken = Token (toS token) }
+    Just config -> config {authToken = Token (toS token)}
     Nothing -> mkConfig token
-  putStrLn ([hereLit|
+  putStrLn
+    ( [hereLit|
 Continue by creating a binary cache at https://cachix.org
-  |] :: Text)
+  |]
+        :: Text
+      )
 
 create :: Env -> Text -> IO ()
 create _ _ =
   throwIO $ DeprecatedCommand "Create command has been deprecated. Please visit https://cachix.org to create a binary cache."
 
 generateKeypair :: Env -> Text -> IO ()
-generateKeypair Env { config = Nothing } _ = throwIO $ NoConfig "Start with visiting https://cachix.org and copying the token to $ cachix authtoken <token>"
-generateKeypair env@Env { config = Just config } name = do
+generateKeypair Env {config = Nothing} _ = throwIO $ NoConfig "Start with visiting https://cachix.org and copying the token to $ cachix authtoken <token>"
+generateKeypair env@Env {config = Just config} name = do
   (PublicKey pk, sk) <- createKeypair
-
   let signingKey = exportSigningKey $ SigningKey sk
       signingKeyCreate = SigningKeyCreate.SigningKeyCreate (toS $ B64.encode pk)
       bcc = BinaryCacheConfig name signingKey
-
   -- we first validate if key can be added to the binary cache
-  (_ :: NoContent) <- escalate
-    =<< ((`runClientM` clientenv env)
-    $ Api.createKey (cachixBCClient name) (authToken config) signingKeyCreate)
-
+  (_ :: NoContent) <-
+    escalate
+      =<< ( (`runClientM` clientenv env)
+              $ Api.createKey (cachixBCClient name) (authToken config) signingKeyCreate
+            )
   -- if key was successfully added, write it to the config
   -- TODO: this breaks if more than one key is added, see #27
-  writeConfig (configPath (cachixoptions env)) $
-    config { binaryCaches = binaryCaches config <> [bcc] }
-
-  putStrLn ([iTrim|
+  writeConfig (configPath (cachixoptions env))
+    $ config {binaryCaches = binaryCaches config <> [bcc]}
+  putStrLn
+    ( [iTrim|
 Secret signing key has been saved in the file above. To populate
 your binary cache:
 
@@ -105,7 +110,9 @@
     $ cachix use ${name}
 
 IMPORTANT: Make sure to make a backup for the signing key above, as you have the only copy.
-  |] :: Text)
+  |]
+        :: Text
+      )
 
 envToToken :: Env -> Token
 envToToken env =
@@ -124,27 +131,24 @@
   -- 1. get cache public key
   res <- (`runClientM` clientenv env) $ Api.get (cachixBCClient name) (envToToken env)
   case res of
-    Left err | isErr err status401 && isJust (config env) -> throwM $ accessDeniedBinaryCache name
-             | isErr err status401 -> throwM notAuthenticatedBinaryCache
-             | isErr err status404 -> throwM $ BinaryCacheNotFound $ "Binary cache" <> name <> " does not exist."
-             | otherwise -> throwM err
+    Left err
+      | isErr err status401 && isJust (config env) -> throwM $ accessDeniedBinaryCache name
+      | isErr err status401 -> throwM notAuthenticatedBinaryCache
+      | isErr err status404 -> throwM $ BinaryCacheNotFound $ "Binary cache" <> name <> " does not exist."
+      | otherwise -> throwM err
     Right binaryCache -> do
-      nixVersion <- escalateAs UnsupportedNixVersion =<< getNixVersion
+      () <- escalateAs UnsupportedNixVersion =<< assertNixVersion
       user <- getUser
       nc <- NixConf.read NixConf.Global
       isTrusted <- isTrustedUser $ NixConf.readLines (catMaybes [nc]) NixConf.isTrustedUsers
       isNixOS <- doesFileExist "/etc/NIXOS"
       let nixEnv = NixEnv
-            { nixVersion = nixVersion
-            , isRoot = user == "root"
-            , isTrusted = isTrusted
-            , isNixOS = isNixOS
-            }
-      addBinaryCache (config env) binaryCache useOptions $
-        if useNixOS useOptions
-        then EchoNixOS nixVersion
-        else getInstallationMode nixEnv
-
+            { isRoot = user == "root",
+              isTrusted = isTrusted,
+              isNixOS = isNixOS
+              }
+      addBinaryCache (config env) binaryCache useOptions
+        $ fromMaybe (getInstallationMode nixEnv) (useMode useOptions)
 
 -- TODO: lots of room for perfomance improvements
 push :: Env -> PushArguments -> IO ()
@@ -153,97 +157,88 @@
   when (not hasNoStdin && not (null rawPaths)) $ throwIO $ AmbiguousInput "You provided both stdin and store path arguments, pick only one to proceed."
   inputStorePaths <-
     if hasNoStdin
-    then return rawPaths
-    else T.words <$> getContents
-
+      then return rawPaths
+      else T.words <$> getContents
   -- TODO: if empty, take whole nix store and warn: nix store-path --all
   when (null inputStorePaths) $ throwIO $ NoInput "You need to specify store paths either as stdin or as a cli argument"
   sk <- findSigningKey env name
-
-  void $ pushClosure
-    (mapConcurrentlyBounded 4)
-    (clientenv env)
-    PushCache
-      { pushCacheToken = envToToken env
-      , pushCacheName = name
-      , pushCacheSigningKey = sk
-      }
-    (pushStrategy env opts name)
-    inputStorePaths
+  store <- wait (storeAsync env)
+  void
+    $ pushClosure
+        (mapConcurrentlyBounded 4)
+        (clientenv env)
+        store PushCache
+        { pushCacheToken = envToToken env,
+          pushCacheName = name,
+          pushCacheSigningKey = sk
+          }
+        (pushStrategy env opts name)
+        inputStorePaths
   putText "All done."
-
 push env (PushWatchStore opts name) = withManager $ \mgr -> do
   _ <- watchDir mgr "/nix/store" filterF action
+  _ <- wait (storeAsync env)
   putText "Watching /nix/store for new builds ..."
   forever $ threadDelay 1000000
   where
     action :: Action
-#if MIN_VERSION_fsnotify(0,3,0)
     action (Removed fp _ _) =
-#else
-    action (Removed fp _) =
-#endif
       pushStorePath env opts name $ toS $ dropEnd 5 fp
     action _ = return ()
-
     filterF :: ActionPredicate
-#if MIN_VERSION_fsnotify(0,3,0)
     filterF (Removed fp _ _)
-#else
-    filterF (Removed fp _)
-#endif
       | ".lock" `isSuffixOf` fp = True
     filterF _ = False
-
     dropEnd :: Int -> [a] -> [a]
     dropEnd index xs = take (length xs - index) xs
 
 -- | Find a secret key in the 'Config' or environment variable
 findSigningKey
   :: Env
-  -> Text           -- ^ Cache name
-  -> IO SigningKey  -- ^ Secret key or exception
+  -> Text -- ^ Cache name
+  -> IO SigningKey -- ^ Secret key or exception
 findSigningKey env name = do
   maybeEnvSK <- lookupEnv "CACHIX_SIGNING_KEY"
-  let matches Config{..} = filter (\bc -> Config.name bc == name) binaryCaches
+  let matches Config {..} = filter (\bc -> Config.name bc == name) binaryCaches
       maybeBCSK = case config env of
         Nothing -> Nothing
         Just c -> Config.secretKey <$> head (matches c)
-      -- TODO: better error msg
+  -- TODO: better error msg
   escalateAs FatalError $ parseSigningKeyLenient $ fromJust $ maybeBCSK <|> toS <$> maybeEnvSK <|> panic "You need to: export CACHIX_SIGNING_KEY=XXX"
 
 retryText :: RetryStatus -> Text
 retryText retrystatus =
   if rsIterNumber retrystatus == 0
-  then ""
-  else "(retry #" <> show (rsIterNumber retrystatus) <> ") "
+    then ""
+    else "(retry #" <> show (rsIterNumber retrystatus) <> ") "
 
 pushStrategy :: Env -> PushOptions -> Text -> Text -> PushStrategy IO ()
 pushStrategy env opts name storePath = PushStrategy
-  { onAlreadyPresent = pass
-  , on401 = if isJust (config env)
-            then throwM $ accessDeniedBinaryCache name
-            else throwM notAuthenticatedBinaryCache
-  , onError = throwM
-  , onAttempt = \retrystatus -> 
+  { onAlreadyPresent = pass,
+    on401 =
+      if isJust (config env)
+        then throwM $ accessDeniedBinaryCache name
+        else throwM notAuthenticatedBinaryCache,
+    onError = throwM,
+    onAttempt = \retrystatus ->
       -- we append newline instead of putStrLn due to https://github.com/haskell/text/issues/242
-      putStr $ "pushing " <> retryText retrystatus <> storePath <> "\n"
-  , onDone = pass
-  , withXzipCompressor = defaultWithXzipCompressorWithLevel (compressionLevel opts)
-  }
+      putStr $ "pushing " <> retryText retrystatus <> storePath <> "\n",
+    onDone = pass,
+    withXzipCompressor = defaultWithXzipCompressorWithLevel (compressionLevel opts)
+    }
 
 pushStorePath :: Env -> PushOptions -> Text -> Text -> IO ()
 pushStorePath env opts name storePath = do
   sk <- findSigningKey env name
   -- use secret key from config or env
   -- TODO: this shouldn't happen for each store path
-
+  store <- wait (storeAsync env)
   pushSingleStorePath
     (clientenv env)
-    PushCache
-      { pushCacheToken = envToToken env
-      , pushCacheName = name
-      , pushCacheSigningKey = sk
+    store PushCache
+    { pushCacheToken = envToToken env,
+      pushCacheName = name,
+      pushCacheSigningKey = sk
       }
     (pushStrategy env opts name storePath)
     storePath
diff --git a/src/Cachix/Client/Config.hs b/src/Cachix/Client/Config.hs
--- a/src/Cachix/Client/Config.hs
+++ b/src/Cachix/Client/Config.hs
@@ -1,55 +1,55 @@
 {-# LANGUAGE DeriveAnyClass #-}
+
 module Cachix.Client.Config
-  ( Config(..)
-  , BinaryCacheConfig(..)
-  , readConfig
-  , writeConfig
-  , getDefaultFilename
-  , ConfigPath
-  , mkConfig
-  ) where
+  ( Config (..),
+    BinaryCacheConfig (..),
+    readConfig,
+    writeConfig,
+    getDefaultFilename,
+    ConfigPath,
+    mkConfig
+    )
+where
 
-import Dhall     hiding ( Text )
-import Dhall.Pretty     ( prettyExpr )
-import Dhall.Core (Expr(..), Chunks(..))
-import GHC.Generics     ( Generic )
-import Servant.Auth.Client
-import System.Directory ( doesFileExist, createDirectoryIfMissing
-                        , getXdgDirectory, XdgDirectory(..)
-                        )
-import System.Posix.Files ( setFileMode, unionFileModes, ownerReadMode
-                          , ownerWriteMode )
-import System.FilePath.Posix ( takeDirectory )
+import Cachix.Client.Config.Orphans ()
+import Dhall hiding (Text)
+import Dhall.Pretty (prettyExpr)
+import GHC.Generics (Generic)
 import Protolude
-
-
-data BinaryCacheConfig = BinaryCacheConfig
- { name :: Text
- , secretKey :: Text
- } deriving (Show, Generic, Interpret, Inject)
-
-data Config = Config
- { authToken :: Token
- , binaryCaches :: [BinaryCacheConfig]
- } deriving (Show, Generic, Interpret, Inject)
+import Servant.Auth.Client
+import System.Directory
+  ( XdgDirectory (..),
+    createDirectoryIfMissing,
+    doesFileExist,
+    getXdgDirectory
+    )
+import System.FilePath.Posix (takeDirectory)
+import System.Posix.Files
+  ( ownerReadMode,
+    ownerWriteMode,
+    setFileMode,
+    unionFileModes
+    )
 
-instance Interpret Token where
-  autoWith _ = Type
-    { extract = \(TextLit (Chunks [] t)) -> pure (Token (toS t))
-    , expected = Text
-    }
+data BinaryCacheConfig
+  = BinaryCacheConfig
+      { name :: Text,
+        secretKey :: Text
+        }
+  deriving (Show, Generic, Interpret, Inject)
 
-instance Inject Token where
-  injectWith _ = InputType
-    { embed = TextLit . Chunks [] . toS . getToken
-    , declared = Text
-    }
+data Config
+  = Config
+      { authToken :: Token,
+        binaryCaches :: [BinaryCacheConfig]
+        }
+  deriving (Show, Generic, Interpret, Inject)
 
 mkConfig :: Text -> Config
 mkConfig token = Config
-  { authToken = Token (toS token)
-  , binaryCaches = []
-  }
+  { authToken = Token (toS token),
+    binaryCaches = []
+    }
 
 type ConfigPath = FilePath
 
@@ -57,8 +57,8 @@
 readConfig filename = do
   doesExist <- doesFileExist filename
   if doesExist
-  then Just <$> input auto (toS filename)
-  else return Nothing
+    then Just <$> input auto (toS filename)
+    else return Nothing
 
 getDefaultFilename :: IO FilePath
 getDefaultFilename = do
diff --git a/src/Cachix/Client/Config/Orphans.hs b/src/Cachix/Client/Config/Orphans.hs
new file mode 100644
--- /dev/null
+++ b/src/Cachix/Client/Config/Orphans.hs
@@ -0,0 +1,28 @@
+{-# OPTIONS_GHC -Wno-orphans #-}
+
+module Cachix.Client.Config.Orphans
+  (
+  )
+where
+
+import Dhall hiding (Text)
+import Dhall.Core (Chunks (..), Expr (..))
+import Protolude
+import Servant.Auth.Client
+
+instance Interpret Token where
+
+  autoWith _ = Type
+    { extract = ex,
+      expected = Text
+      }
+    where
+      ex (TextLit (Chunks [] t)) = pure (Token (toS t))
+      ex _ = panic "Unexpected Dhall value. Did it typecheck?"
+
+instance Inject Token where
+
+  injectWith _ = InputType
+    { embed = TextLit . Chunks [] . toS . getToken,
+      declared = Text
+      }
diff --git a/src/Cachix/Client/Env.hs b/src/Cachix/Client/Env.hs
--- a/src/Cachix/Client/Env.hs
+++ b/src/Cachix/Client/Env.hs
@@ -1,53 +1,60 @@
 module Cachix.Client.Env
-  ( Env(..)
-  , mkEnv
-  , cachixVersion
-  ) where
+  ( Env (..),
+    mkEnv,
+    cachixVersion
+    )
+where
 
+import Cachix.Client.Config (Config, readConfig)
+import Cachix.Client.OptionsParser (CachixOptions (..))
+import Cachix.Client.Store (Store, openStore)
+import Cachix.Client.URI (getBaseUrl)
+import Control.Concurrent.Async (Async, async)
+import Data.Version (showVersion)
+import Network.HTTP.Client
+  ( ManagerSettings,
+    managerModifyRequest,
+    managerResponseTimeout,
+    responseTimeoutNone
+    )
+import Network.HTTP.Client.TLS (newTlsManagerWith, tlsManagerSettings)
+import Network.HTTP.Simple (setRequestHeader)
+import Paths_cachix (version)
 import Protolude
-
-import Data.Version                (showVersion)
-import Paths_cachix                (version)
-
-import Network.HTTP.Simple         ( setRequestHeader )
-import Network.HTTP.Client.TLS     ( newTlsManagerWith, tlsManagerSettings )
-import Network.HTTP.Client         ( managerResponseTimeout, managerModifyRequest
-                                   , responseTimeoutNone, ManagerSettings)
-import Servant.Client              ( mkClientEnv, ClientEnv )
-import System.Directory            ( canonicalizePath )
-
-import Cachix.Client.Config        ( readConfig, Config )
-import Cachix.Client.OptionsParser ( CachixOptions(..) )
-import Cachix.Client.URI           ( getBaseUrl )
-
-
-data Env = Env
-  { config :: Maybe Config
-  , clientenv :: ClientEnv
-  , cachixoptions :: CachixOptions
-  }
+import Servant.Client (ClientEnv, mkClientEnv)
+import System.Directory (canonicalizePath)
 
+data Env
+  = Env
+      { config :: Maybe Config,
+        clientenv :: ClientEnv,
+        cachixoptions :: CachixOptions,
+        storeAsync :: Async Store
+        }
 
 mkEnv :: CachixOptions -> IO Env
 mkEnv rawcachixoptions = do
+  store <- async openStore
   -- make sure path to the config is passed as absolute to dhall logic
   canonicalConfigPath <- canonicalizePath (configPath rawcachixoptions)
-  let cachixoptions = rawcachixoptions { configPath = canonicalConfigPath }
+  let cachixoptions = rawcachixoptions {configPath = canonicalConfigPath}
   config <- readConfig $ configPath cachixoptions
   manager <- newTlsManagerWith customManagerSettings
   let clientenv = mkClientEnv manager $ getBaseUrl (host cachixoptions)
   return Env
-    { config = config
-    , clientenv = clientenv
-    , cachixoptions = cachixoptions
-    }
+    { config = config,
+      clientenv = clientenv,
+      cachixoptions = cachixoptions,
+      storeAsync = store
+      }
 
 customManagerSettings :: ManagerSettings
-customManagerSettings = tlsManagerSettings
-  { managerResponseTimeout = responseTimeoutNone
-  -- managerModifyRequest :: Request -> IO Request
-  , managerModifyRequest = return . setRequestHeader "User-Agent" [toS cachixVersion]
-  }
+customManagerSettings =
+  tlsManagerSettings
+    { managerResponseTimeout = responseTimeoutNone,
+      -- managerModifyRequest :: Request -> IO Request
+      managerModifyRequest = return . setRequestHeader "User-Agent" [toS cachixVersion]
+      }
 
 cachixVersion :: Text
 cachixVersion = "cachix " <> toS (showVersion version)
diff --git a/src/Cachix/Client/Exception.hs b/src/Cachix/Client/Exception.hs
--- a/src/Cachix/Client/Exception.hs
+++ b/src/Cachix/Client/Exception.hs
@@ -1,4 +1,4 @@
-module Cachix.Client.Exception (CachixException(..)) where
+module Cachix.Client.Exception (CachixException (..)) where
 
 import Protolude
 
@@ -17,4 +17,7 @@
   | BinaryCacheNotFound Text
   deriving (Show, Typeable)
 
-instance Exception CachixException
+instance Exception CachixException where
+
+  displayException (NixOSInstructions txt) = toS txt
+  displayException e = show e
diff --git a/src/Cachix/Client/InstallationMode.hs b/src/Cachix/Client/InstallationMode.hs
--- a/src/Cachix/Client/InstallationMode.hs
+++ b/src/Cachix/Client/InstallationMode.hs
@@ -1,93 +1,116 @@
 {-# LANGUAGE QuasiQuotes #-}
+
 module Cachix.Client.InstallationMode
-  ( InstallationMode(..)
-  , NixEnv(..)
-  , getInstallationMode
-  , addBinaryCache
-  , isTrustedUser
-  , getUser
-  ) where
+  ( InstallationMode (..),
+    NixEnv (..),
+    getInstallationMode,
+    addBinaryCache,
+    isTrustedUser,
+    getUser,
+    fromString,
+    toString,
+    UseOptions (..)
+    )
+where
 
-import           Protolude
-import           Data.String.Here
-import qualified Data.Text as T
-import           Cachix.Client.Config (Config)
-import           Cachix.Client.Exception (CachixException(..))
+import Cachix.Api as Api
+import Cachix.Client.Config (Config)
+import Cachix.Client.Exception (CachixException (..))
 import qualified Cachix.Client.NetRc as NetRc
 import qualified Cachix.Client.NixConf as NixConf
-import           Cachix.Client.NixVersion ( NixVersion(..) )
-import           Cachix.Client.OptionsParser    ( UseOptions(..) )
-import           Cachix.Api as Api
-import           System.Directory               ( getPermissions, writable, createDirectoryIfMissing )
-import           System.Environment             ( lookupEnv )
-import           System.FilePath                ( replaceFileName, (</>) )
+import Data.String.Here
+import qualified Data.Text as T
+import Protolude
+import System.Directory (Permissions, createDirectoryIfMissing, getPermissions, writable)
+import System.Environment (lookupEnv)
+import System.FilePath ((</>), replaceFileName)
+import Prelude (String)
 
-data NixEnv = NixEnv
-  { nixVersion :: NixVersion
-  , isTrusted :: Bool
-  , isRoot :: Bool
-  , isNixOS :: Bool
-  }
+data NixEnv
+  = NixEnv
+      { isTrusted :: Bool,
+        isRoot :: Bool,
+        isNixOS :: Bool
+        }
 
+-- NOTE: update the list of options for --mode argument in OptionsParser.hs
 data InstallationMode
-  = Install NixVersion NixConf.NixConfLoc
-  | EchoNixOS NixVersion
-  | EchoNixOSWithTrustedUser NixVersion
+  = Install NixConf.NixConfLoc
+  | WriteNixOS
   | UntrustedRequiresSudo
-  | Nix20RequiresSudo
   deriving (Show, Eq)
 
+data UseOptions
+  = UseOptions
+      { useMode :: Maybe InstallationMode,
+        useNixOSFolder :: FilePath
+        }
+  deriving (Show)
+
+fromString :: String -> Maybe InstallationMode
+fromString "root-nixconf" = Just $ Install NixConf.Global
+fromString "user-nixconf" = Just $ Install NixConf.Local
+fromString "nixos" = Just WriteNixOS
+fromString "untrusted-requires-sudo" = Just UntrustedRequiresSudo
+fromString _ = Nothing
+
+toString :: InstallationMode -> String
+toString (Install NixConf.Global) = "root-nixconf"
+toString (Install NixConf.Local) = "user-nixconf"
+toString WriteNixOS = "nixos"
+toString UntrustedRequiresSudo = "untrusted-requires-sudo"
+
 getInstallationMode :: NixEnv -> InstallationMode
-getInstallationMode NixEnv{..}
-  | isNixOS && (isRoot || nixVersion /= Nix201) = EchoNixOS nixVersion
-  | isNixOS && not isTrusted = EchoNixOSWithTrustedUser nixVersion
-  | not isNixOS && isRoot = Install nixVersion NixConf.Global
-  | nixVersion /= Nix201 = Nix20RequiresSudo
-  | isTrusted = Install nixVersion NixConf.Local
+getInstallationMode NixEnv {..}
+  | isNixOS && isRoot = WriteNixOS
+  | not isNixOS && isRoot = Install NixConf.Global
+  | isTrusted = Install NixConf.Local
   | otherwise = UntrustedRequiresSudo
 
-
 -- | Add a Binary cache to nix.conf, print nixos config or fail
 addBinaryCache :: Maybe Config -> Api.BinaryCache -> UseOptions -> InstallationMode -> IO ()
-addBinaryCache _ _ _ UntrustedRequiresSudo = throwIO $
-  MustBeRoot "Run command as root OR execute: $ echo \"trusted-users = root $USER\" | sudo tee -a /etc/nix/nix.conf && sudo pkill nix-daemon"
-addBinaryCache _ _ _ Nix20RequiresSudo = throwIO $
-  MustBeRoot "Run command as root OR upgrade to latest Nix - to be able to use it without root (recommended)"
-addBinaryCache maybeConfig bc useOptions (EchoNixOS _) =
-  nixosBinaryCache maybeConfig Nothing bc useOptions
-addBinaryCache maybeConfig bc useOptions (EchoNixOSWithTrustedUser _) = do
-  user <- getUser
-  nixosBinaryCache maybeConfig (Just user) bc useOptions
-addBinaryCache maybeConfig bc@Api.BinaryCache{..} _ (Install nixversion ncl) = do
+addBinaryCache _ _ _ UntrustedRequiresSudo =
+  throwIO
+    $ MustBeRoot "Run command as root OR execute: $ echo \"trusted-users = root $USER\" | sudo tee -a /etc/nix/nix.conf && sudo pkill nix-daemon"
+addBinaryCache maybeConfig bc useOptions WriteNixOS =
+  nixosBinaryCache maybeConfig bc useOptions
+addBinaryCache maybeConfig bc@Api.BinaryCache {..} _ (Install ncl) = do
   -- TODO: might need locking one day
   gnc <- NixConf.read NixConf.Global
-  (input, output) <- case ncl of
-    NixConf.Global -> return ([gnc], gnc)
-    NixConf.Local -> do
-      lnc <- NixConf.read NixConf.Local
-      return ([gnc, lnc], lnc)
+  (input, output) <-
+    case ncl of
+      NixConf.Global -> return ([gnc], gnc)
+      NixConf.Local -> do
+        lnc <- NixConf.read NixConf.Local
+        return ([gnc, lnc], lnc)
   let nixconf = fromMaybe (NixConf.NixConf []) output
-  netrcLocMaybe <- forM (guard $ not  isPublic) $ const $ addPrivateBinaryCacheNetRC maybeConfig bc ncl
-  let
-    addNetRCLine :: NixConf.NixConf -> NixConf.NixConf
-    addNetRCLine = fromMaybe identity $ do
+  netrcLocMaybe <- forM (guard $ not isPublic) $ const $ addPrivateBinaryCacheNetRC maybeConfig bc ncl
+  let addNetRCLine :: NixConf.NixConf -> NixConf.NixConf
+      addNetRCLine = fromMaybe identity $ do
         netrcLoc <- netrcLocMaybe :: Maybe FilePath
         -- We only add the netrc line for local user configs for now.
         -- On NixOS we assume it will be picked up from the default location.
         guard (ncl == NixConf.Local)
         pure (NixConf.setNetRC $ toS netrcLoc)
-  NixConf.write nixversion ncl $ addNetRCLine $ NixConf.add bc (catMaybes input) nixconf
+  NixConf.write ncl $ addNetRCLine $ NixConf.add bc (catMaybes input) nixconf
   filename <- NixConf.getFilename ncl
   putStrLn $ "Configured " <> uri <> " binary cache in " <> toS filename
 
-nixosBinaryCache :: Maybe Config -> Maybe Text -> Api.BinaryCache -> UseOptions -> IO ()
-nixosBinaryCache maybeConfig maybeUser bc@Api.BinaryCache{..} UseOptions { useNixOSFolder=baseDirectory } = do
-  createDirectoryIfMissing True $ toS toplevel
-  writeFile (toS glueModuleFile) glueModule
-  writeFile (toS cacheModuleFile) cacheModule
-  unless isPublic $ void $ addPrivateBinaryCacheNetRC maybeConfig bc NixConf.Global
-  putText instructions
+nixosBinaryCache :: Maybe Config -> Api.BinaryCache -> UseOptions -> IO ()
+nixosBinaryCache maybeConfig bc@Api.BinaryCache {..} UseOptions {useNixOSFolder = baseDirectory} = do
+  eitherPermissions <- try $ getPermissions (toS toplevel) :: IO (Either SomeException Permissions)
+  case eitherPermissions of
+    Left _ -> throwIO $ NixOSInstructions noEtcPermissionInstructions
+    Right permissions
+      | writable permissions -> installFiles
+      | otherwise -> throwIO $ NixOSInstructions noEtcPermissionInstructions
   where
+    installFiles = do
+      createDirectoryIfMissing True $ toS toplevel
+      writeFile (toS glueModuleFile) glueModule
+      writeFile (toS cacheModuleFile) cacheModule
+      unless isPublic $ void $ addPrivateBinaryCacheNetRC maybeConfig bc NixConf.Global
+      putText instructions
     configurationNix :: Text
     configurationNix = toS $ toS baseDirectory </> "configuration.nix"
     namespace :: Text
@@ -98,12 +121,16 @@
     glueModuleFile = toplevel <> ".nix"
     cacheModuleFile :: Text
     cacheModuleFile = toplevel <> "/" <> toS name <> ".nix"
-
-    extra :: Text
-    extra = maybe "" (\user -> [i|trustedUsers = [ "root" "${user}" ];|]) maybeUser
+    noEtcPermissionInstructions :: Text
+    noEtcPermissionInstructions =
+      [iTrim|
+Could not install NixOS configuration to /etc/nixos/ due to lack of write permissions.
 
+Pass `--nixos-folder /etc/mynixos/` as an alternative location with write permissions.
+      |]
     instructions :: Text
-    instructions = [iTrim|
+    instructions =
+      [iTrim|
 Cachix configuration written to ${glueModuleFile}.
 Binary cache ${name} configuration written to ${cacheModuleFile}.
 
@@ -115,9 +142,9 @@
 
     $ nixos-rebuild switch
     |]
-
     glueModule :: Text
-    glueModule = [i|
+    glueModule =
+      [i|
 # WARN: this file will get overwritten by $ cachix use <name>
 { pkgs, lib, ... }:
 
@@ -131,9 +158,9 @@
   nix.binaryCaches = ["https://cache.nixos.org/"];
 }
     |]
-
     cacheModule :: Text
-    cacheModule = [i|
+    cacheModule =
+      [i|
 {
   nix = {
     binaryCaches = [
@@ -142,7 +169,6 @@
     binaryCachePublicKeys = [
       ${T.intercalate " " (map (\s -> "\"" <> s <> "\"") publicSigningKeys)}
     ];
-    ${extra}
   };
 }
     |]
diff --git a/src/Cachix/Client/NetRc.hs b/src/Cachix/Client/NetRc.hs
--- a/src/Cachix/Client/NetRc.hs
+++ b/src/Cachix/Client/NetRc.hs
@@ -1,60 +1,57 @@
 -- Deals with adding private caches to netrc
 module Cachix.Client.NetRc
   ( add
-  ) where
-
-import qualified Data.ByteString    as BS
-import           Data.List            (nubBy)
-import qualified Data.Text          as T
-import           Network.NetRc
-import           Protolude
-import           System.FilePath       ( takeDirectory )
-import           System.Directory     ( doesFileExist, createDirectoryIfMissing )
-import           Servant.Auth.Client  (getToken)
+    )
+where
 
 import qualified Cachix.Api as Api
-import           Cachix.Api.Error     (escalateAs)
-import           Cachix.Client.Config (Config, authToken)
-import           Cachix.Client.Exception (CachixException(NetRcParseError))
-
+import Cachix.Api.Error (escalateAs)
+import Cachix.Client.Config (Config, authToken)
+import Cachix.Client.Exception (CachixException (NetRcParseError))
+import qualified Data.ByteString as BS
+import Data.List (nubBy)
+import qualified Data.Text as T
+import Network.NetRc
+import Protolude
+import Servant.Auth.Client (getToken)
+import System.Directory (createDirectoryIfMissing, doesFileExist)
+import System.FilePath (takeDirectory)
 
 -- | Add a list of binary caches to netrc under `filename`.
 --   Makes sure there are no duplicate entries (using domain as a key).
 --   If file under filename doesn't exist it's created.
-add :: Config
-    -> [Api.BinaryCache]
-    -> FilePath
-    -> IO ()
+add
+  :: Config
+  -> [Api.BinaryCache]
+  -> FilePath
+  -> IO ()
 add config binarycaches filename = do
   doesExist <- doesFileExist filename
-  netrc <- if doesExist
-           then BS.readFile filename >>= parse
-           else return $ NetRc [] []
+  netrc <-
+    if doesExist
+      then BS.readFile filename >>= parse
+      else return $ NetRc [] []
   createDirectoryIfMissing True (takeDirectory filename)
   BS.writeFile filename $ netRcToByteString $ uniqueAppend netrc
   where
     parse :: ByteString -> IO NetRc
     parse contents = escalateAs (NetRcParseError . show) $ parseNetRc filename contents
-
     -- O(n^2) but who cares?
     uniqueAppend :: NetRc -> NetRc
     uniqueAppend (NetRc hosts macdefs) =
-      let
-        f :: NetRcHost -> NetRcHost -> Bool
-        f x y = nrhName x == nrhName y
-      in NetRc (nubBy f (new ++ hosts)) macdefs
-
+      let f :: NetRcHost -> NetRcHost -> Bool
+          f x y = nrhName x == nrhName y
+       in NetRc (nubBy f (new ++ hosts)) macdefs
     new :: [NetRcHost]
     new = map mkHost $ filter (not . Api.isPublic) binarycaches
-
     mkHost :: Api.BinaryCache -> NetRcHost
     mkHost bc = NetRcHost
-      { nrhName = toS $ stripPrefix "http://" $ stripPrefix "https://" (Api.uri bc)
-      , nrhLogin = ""
-      , nrhPassword = getToken (authToken config)
-      , nrhAccount = ""
-      , nrhMacros = []
-      }
+      { nrhName = toS $ stripPrefix "http://" $ stripPrefix "https://" (Api.uri bc),
+        nrhLogin = "",
+        nrhPassword = getToken (authToken config),
+        nrhAccount = "",
+        nrhMacros = []
+        }
       where
         -- stripPrefix that either strips or returns the same string
         stripPrefix :: Text -> Text -> Text
diff --git a/src/Cachix/Client/NixConf.hs b/src/Cachix/Client/NixConf.hs
--- a/src/Cachix/Client/NixConf.hs
+++ b/src/Cachix/Client/NixConf.hs
@@ -1,3 +1,6 @@
+{-# LANGUAGE DeriveFunctor #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+
 {- (Very limited) parser, rendered and modifier of nix.conf
 
 Supports subset of nix.conf given Nix 2.0 or Nix 1.0
@@ -7,45 +10,43 @@
 version considers as recommended.
 
 -}
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE DeriveFunctor #-}
 module Cachix.Client.NixConf
-  ( NixConf
-  , NixConfG(..)
-  , NixConfLine(..)
-  , NixConfLoc(..)
-  , render
-  , add
-  , read
-  , update
-  , write
-  , getFilename
-  , parser
-  , parse
-  , readLines
-  , writeLines
-  , isTrustedUsers
-  , defaultPublicURI
-  , defaultSigningKey
-  , substitutersKey
-  , trustedPublicKeysKey
-  , setNetRC
-  ) where
+  ( NixConf,
+    NixConfG (..),
+    NixConfLine (..),
+    NixConfLoc (..),
+    render,
+    add,
+    read,
+    update,
+    write,
+    getFilename,
+    parser,
+    parse,
+    readLines,
+    writeLines,
+    isTrustedUsers,
+    defaultPublicURI,
+    defaultSigningKey,
+    setNetRC
+    )
+where
 
+import Cachix.Api (BinaryCache (..))
 import Data.Char (isSpace)
-import qualified Data.Text as T
 import Data.List (nub)
+import qualified Data.Text as T
 import Protolude
+import System.Directory
+  ( XdgDirectory (..),
+    createDirectoryIfMissing,
+    doesFileExist,
+    getXdgDirectory
+    )
+import System.FilePath.Posix (takeDirectory)
 import qualified Text.Megaparsec as Mega
 import Text.Megaparsec.Char
-import System.Directory ( doesFileExist, createDirectoryIfMissing
-                        , getXdgDirectory, XdgDirectory(..)
-                        )
-import System.FilePath.Posix ( takeDirectory )
-import Cachix.Api (BinaryCache(..))
-import Cachix.Client.NixVersion (NixVersion(..))
 
-
 data NixConfLine
   = Substituters [Text]
   | TrustedUsers [Text]
@@ -63,7 +64,6 @@
 readLines nixconfs predicate = concatMap f nixconfs
   where
     f (NixConf xs) = foldl foldIt [] xs
-
     foldIt :: [Text] -> NixConfLine -> [Text]
     foldIt prev new = prev <> fromMaybe [] (predicate new)
 
@@ -86,9 +86,9 @@
 
 -- | Pure version of addIO
 add :: BinaryCache -> [NixConf] -> NixConf -> NixConf
-add BinaryCache{..} toRead toWrite =
-  writeLines isPublicKey (TrustedPublicKeys $ nub publicKeys) $
-    writeLines isSubstituter (Substituters $ nub substituters) toWrite
+add BinaryCache {..} toRead toWrite =
+  writeLines isPublicKey (TrustedPublicKeys $ nub publicKeys)
+    $ writeLines isSubstituter (Substituters $ nub substituters) toWrite
   where
     -- Note: some defaults are always appended since overriding some setttings in nix.conf overrides defaults otherwise
     substituters = (defaultPublicURI : readLines toRead isSubstituter) <> [uri]
@@ -96,99 +96,92 @@
 
 defaultPublicURI :: Text
 defaultPublicURI = "https://cache.nixos.org"
+
 defaultSigningKey :: Text
 defaultSigningKey = "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
 
-render :: NixVersion -> NixConf -> Text
-render nixversion (NixConf nixconflines) = T.unlines $ fmap go nixconflines
+render :: NixConf -> Text
+render (NixConf nixconflines) = T.unlines $ fmap go nixconflines
   where
     go :: NixConfLine -> Text
-    go (Substituters xs) = substitutersKey nixversion <> " = " <> T.unwords xs
+    go (Substituters xs) = "substituters" <> " = " <> T.unwords xs
     go (TrustedUsers xs) = "trusted-users = " <> T.unwords xs
-    go (TrustedPublicKeys xs) = trustedPublicKeysKey nixversion <> " = " <> T.unwords xs
+    go (TrustedPublicKeys xs) = "trusted-public-keys" <> " = " <> T.unwords xs
     go (NetRcFile filename) = "netrc-file = " <> filename
     go (Other line) = line
 
-substitutersKey :: NixVersion -> Text
-substitutersKey Nix1XX = "binary-caches"
-substitutersKey Nix20 = "substituters"
-substitutersKey Nix201 = "substituters"
-
-trustedPublicKeysKey :: NixVersion -> Text
-trustedPublicKeysKey Nix1XX = "binary-cache-public-keys"
-trustedPublicKeysKey Nix20 = "trusted-public-keys"
-trustedPublicKeysKey Nix201 = "trusted-public-keys"
-
-write :: NixVersion -> NixConfLoc -> NixConf -> IO ()
-write nixversion ncl nc = do
+write :: NixConfLoc -> NixConf -> IO ()
+write ncl nc = do
   filename <- getFilename ncl
   createDirectoryIfMissing True (takeDirectory filename)
-  writeFile filename $ render nixversion nc
+  writeFile filename $ render nc
 
 read :: NixConfLoc -> IO (Maybe NixConf)
 read ncl = do
   filename <- getFilename ncl
   doesExist <- doesFileExist filename
   if not doesExist
-  then return Nothing
-  else do
-    result <- parse <$> readFile filename
-    case result of
-      Left err -> do
-        putStrLn (Mega.errorBundlePretty err)
-        panic $ toS filename <> " failed to parse, please copy the above error and contents of nix.conf and open an issue at https://github.com/cachix/cachix"
-      Right conf -> return $ Just conf
+    then return Nothing
+    else
+      do
+        result <- parse <$> readFile filename
+        case result of
+          Left err -> do
+            putStrLn (Mega.errorBundlePretty err)
+            panic $ toS filename <> " failed to parse, please copy the above error and contents of nix.conf and open an issue at https://github.com/cachix/cachix"
+          Right conf -> return $ Just conf
 
-update :: NixVersion -> NixConfLoc -> (Maybe NixConf -> NixConf) -> IO ()
-update nixversion ncl f = do
+update :: NixConfLoc -> (Maybe NixConf -> NixConf) -> IO ()
+update ncl f = do
   nc <- f <$> read ncl
-  write nixversion ncl nc
+  write ncl nc
 
 setNetRC :: Text -> NixConf -> NixConf
 setNetRC netrc (NixConf nc) = NixConf $ filter noNetRc nc ++ [NetRcFile netrc]
-  where noNetRc (NetRcFile _) = False
-        noNetRc _ = True
+  where
+    noNetRc (NetRcFile _) = False
+    noNetRc _ = True
 
 data NixConfLoc = Global | Local
   deriving (Show, Eq)
 
 getFilename :: NixConfLoc -> IO FilePath
 getFilename ncl = do
-  dir <- case ncl of
-    Global -> return "/etc/nix"
-    Local -> getXdgDirectory XdgConfig "nix"
+  dir <-
+    case ncl of
+      Global -> return "/etc/nix"
+      Local -> getXdgDirectory XdgConfig "nix"
   return $ dir <> "/nix.conf"
 
 -- nix.conf Parser
-
 type Parser = Mega.Parsec Void Text
 
 -- TODO: handle comments
 parseLine :: ([Text] -> NixConfLine) -> Text -> Parser NixConfLine
 parseLine constr name = Mega.try $ do
-    _ <- optional (some (char ' '))
-    _ <- string name
-    _ <- many (char ' ')
-    _ <- char '='
-    _ <- many (char ' ')
-    values <- Mega.sepBy1 (many (Mega.satisfy (not . isSpace))) (some (char ' '))
-    _ <- many spaceChar
-    return $ constr (fmap toS values)
+  _ <- optional (some (char ' '))
+  _ <- string name
+  _ <- many (char ' ')
+  _ <- char '='
+  _ <- many (char ' ')
+  values <- Mega.sepBy1 (many (Mega.satisfy (not . isSpace))) (some (char ' '))
+  _ <- many spaceChar
+  return $ constr (fmap toS values)
 
 parseOther :: Parser NixConfLine
 parseOther = Mega.try $ Other . toS <$> Mega.someTill Mega.anySingle (void eol <|> Mega.eof)
 
 parseAltLine :: Parser NixConfLine
 parseAltLine =
-      (Other "" <$ eol)
-  <|> parseLine Substituters "substituters"
-  <|> parseLine TrustedPublicKeys "trusted-public-keys"
-  <|> parseLine TrustedUsers "trusted-users"
-  <|> parseLine TrustedPublicKeys "binary-cache-public-keys"
-  <|> parseLine Substituters "binary-caches"
-  -- NB: assume that space in this option means space in filename
-  <|> parseLine (NetRcFile . T.concat) "netrc-file"
-  <|> parseOther
+  (Other "" <$ eol)
+    <|> parseLine Substituters "substituters"
+    <|> parseLine TrustedPublicKeys "trusted-public-keys"
+    <|> parseLine TrustedUsers "trusted-users"
+    <|> parseLine TrustedPublicKeys "binary-cache-public-keys"
+    <|> parseLine Substituters "binary-caches"
+    -- NB: assume that space in this option means space in filename
+    <|> parseLine (NetRcFile . T.concat) "netrc-file"
+    <|> parseOther
 
 parser :: Parser NixConf
 parser = NixConf <$> many parseAltLine
diff --git a/src/Cachix/Client/NixVersion.hs b/src/Cachix/Client/NixVersion.hs
--- a/src/Cachix/Client/NixVersion.hs
+++ b/src/Cachix/Client/NixVersion.hs
@@ -1,37 +1,29 @@
 module Cachix.Client.NixVersion
-  ( getNixVersion
-  , parseNixVersion
-  , NixVersion(..)
-  ) where
+  ( assertNixVersion,
+    parseNixVersion
+    )
+where
 
+import Data.Text as T
+import Data.Versions
 import Protolude
 import System.Process (readProcessWithExitCode)
-import Data.Versions
-import Data.Text as T
 
-
-data NixVersion
-  = Nix20
-  | Nix201
-  | Nix1XX
-  deriving (Show, Eq)
-
-getNixVersion :: IO (Either Text NixVersion)
-getNixVersion = do
+assertNixVersion :: IO (Either Text ())
+assertNixVersion = do
   (exitcode, out, err) <- readProcessWithExitCode "nix-env" ["--version"] mempty
   unless (err == "") $ putStrLn $ "nix-env stderr: " <> err
   return $ case exitcode of
     ExitFailure i -> Left $ "'nix-env --version' exited with " <> show i
     ExitSuccess -> parseNixVersion $ toS out
 
-parseNixVersion :: Text -> Either Text NixVersion
+parseNixVersion :: Text -> Either Text ()
 parseNixVersion output =
-  let
-    verStr = T.drop 14 $ T.strip output
-    err = "Couldn't parse 'nix-env --version' output: " <> output
-  in case versioning verStr of
-    Left _ -> Left err
-    Right ver | verStr == "" -> Left err
-              | ver < Ideal (SemVer 1 99 0 [] []) -> Right Nix1XX
-              | ver < Ideal (SemVer 2 0 1 [] []) -> Right Nix20
-              | otherwise -> Right Nix201
+  let verStr = T.drop 14 $ T.strip output
+      err = "Couldn't parse 'nix-env --version' output: " <> output
+   in case versioning verStr of
+        Left _ -> Left err
+        Right ver
+          | verStr == "" -> Left err
+          | ver < Ideal (SemVer 2 0 1 [] []) -> Left "Nix 2.0.2 or lower is not supported. Please upgrade: https://nixos.org/nix/"
+          | otherwise -> Right ()
diff --git a/src/Cachix/Client/OptionsParser.hs b/src/Cachix/Client/OptionsParser.hs
--- a/src/Cachix/Client/OptionsParser.hs
+++ b/src/Cachix/Client/OptionsParser.hs
@@ -1,47 +1,58 @@
 module Cachix.Client.OptionsParser
-  ( CachixCommand(..)
-  , CachixOptions(..)
-  , UseOptions(..)
-  , PushArguments(..)
-  , PushOptions(..)
-  , BinaryCacheName
-  , getOpts
-  ) where
-
-import Data.Bifunctor            (first)
-import Protolude hiding          (option)
-import URI.ByteString            (URIRef, Absolute, parseURI, strictURIParserOptions
-                                 , serializeURIRef')
-import Options.Applicative
+  ( CachixCommand (..),
+    CachixOptions (..),
+    PushArguments (..),
+    PushOptions (..),
+    BinaryCacheName,
+    getOpts
+    )
+where
 
 import qualified Cachix.Client.Config as Config
-import Cachix.Client.URI         (defaultCachixURI)
+import qualified Cachix.Client.InstallationMode as InstallationMode
+import Cachix.Client.URI (defaultCachixURI)
+import Data.Bifunctor (first)
+import Options.Applicative
+import Protolude hiding (option)
+import URI.ByteString
+  ( Absolute,
+    URIRef,
+    parseURI,
+    serializeURIRef',
+    strictURIParserOptions
+    )
 
-data CachixOptions = CachixOptions
-  { host :: URIRef Absolute
-  , configPath :: Config.ConfigPath
-  , verbose :: Bool
-  } deriving Show
+data CachixOptions
+  = CachixOptions
+      { host :: URIRef Absolute,
+        configPath :: Config.ConfigPath,
+        verbose :: Bool
+        }
+  deriving (Show)
 
 parserCachixOptions :: Config.ConfigPath -> Parser CachixOptions
-parserCachixOptions defaultConfigPath = CachixOptions
-  <$> option uriOption ( long "host"
-                       <> value defaultCachixURI
-                       <> metavar "URI"
-                       <> showDefaultWith (toS . serializeURIRef')
-                       <> help "Host to connect to"
-                       )
- <*> strOption ( long "config"
+parserCachixOptions defaultConfigPath =
+  CachixOptions
+    <$> option uriOption
+          ( long "host"
+              <> value defaultCachixURI
+              <> metavar "URI"
+              <> showDefaultWith (toS . serializeURIRef')
+              <> help "Host to connect to"
+            )
+    <*> strOption
+          ( long "config"
               <> short 'c'
               <> value defaultConfigPath
               <> metavar "CONFIGPATH"
               <> showDefault
               <> help "Cachix configuration file"
-              )
-  <*> switch ( long "verbose"
-             <> short 'v'
-             <> help "Verbose mode"
-             )
+            )
+    <*> switch
+          ( long "verbose"
+              <> short 'v'
+              <> help "Verbose mode"
+            )
 
 uriOption :: ReadM (URIRef Absolute)
 uriOption = eitherReader $ \s ->
@@ -54,90 +65,101 @@
   | Create BinaryCacheName
   | GenerateKeypair BinaryCacheName
   | Push PushArguments
-  | Use BinaryCacheName UseOptions
+  | Use BinaryCacheName InstallationMode.UseOptions
   | Version
-  deriving Show
-
-data UseOptions = UseOptions
-  { useNixOS :: Bool
-  , useNixOSFolder :: FilePath
-  } deriving Show
+  deriving (Show)
 
 data PushArguments
   = PushPaths PushOptions Text [Text]
   | PushWatchStore PushOptions Text
-  deriving Show
+  deriving (Show)
 
-data PushOptions = PushOptions
-  { compressionLevel :: Int
-  } deriving Show
+data PushOptions
+  = PushOptions
+      { compressionLevel :: Int
+        }
+  deriving (Show)
 
 parserCachixCommand :: Parser CachixCommand
-parserCachixCommand = subparser $
-  command "authtoken" (infoH authtoken (progDesc "Configure token for authentication to cachix.org")) <>
-  command "create" (infoH create (progDesc "DEPRECATED: Go to https://cachix.org instead")) <>
-  command "generate-keypair" (infoH generateKeypair (progDesc "Generate keypair for a binary cache")) <>
-  command "push" (infoH push (progDesc "Upload Nix store paths to the binary cache")) <>
-  command "use" (infoH use (progDesc "Configure nix.conf to enable binary cache during builds"))
+parserCachixCommand =
+  subparser
+    $ command "authtoken" (infoH authtoken (progDesc "Configure token for authentication to cachix.org"))
+    <> command "create" (infoH create (progDesc "DEPRECATED: Go to https://cachix.org instead"))
+    <> command "generate-keypair" (infoH generateKeypair (progDesc "Generate keypair for a binary cache"))
+    <> command "push" (infoH push (progDesc "Upload Nix store paths to the binary cache"))
+    <> command "use" (infoH use (progDesc "Configure nix.conf to enable binary cache during builds"))
   where
     nameArg = strArgument (metavar "NAME")
     authtoken = AuthToken <$> strArgument (metavar "TOKEN")
     create = Create <$> nameArg
     generateKeypair = GenerateKeypair <$> nameArg
-
     validatedLevel l =
       l <$ unless (l `elem` [0 .. 9]) (readerError $ "value " <> show l <> " not in expected range: [0..9]")
-
     pushOptions :: Parser PushOptions
-    pushOptions = PushOptions
-       <$> option (auto >>= validatedLevel)
-            ( long "compression-level"
-            <> metavar "[0..9]"
-            <> help "The compression level for XZ compression.\
+    pushOptions =
+      PushOptions
+        <$> option (auto >>= validatedLevel)
+              ( long "compression-level"
+                  <> metavar "[0..9]"
+                  <> help
+                       "The compression level for XZ compression.\
                    \ Take compressor *and* decompressor memory usage into account before using [7..9]!"
-            <> showDefault
-            <> value 2
-            )
-
+                  <> showDefault
+                  <> value 2
+                )
     push = (\opts cache f -> Push $ f opts cache) <$> pushOptions <*> nameArg <*> (pushPaths <|> pushWatchStore)
-    pushPaths = (\paths opts cache -> PushPaths opts cache paths) <$>
-                   many (strArgument (metavar "PATHS..."))
-    pushWatchStore = (\() opts cache -> PushWatchStore opts cache) <$>
-                  flag' () ( long "watch-store"
-                          <> short 'w'
-                          <> help "Run in daemon mode and push store paths as they are added to /nix/store"
-                           )
-
-    use = Use <$> nameArg
-              <*> (UseOptions <$> switch ( long "nixos"
-                                        <> short 'n'
-                                        <> help "Write NixOS modules")
-                              <*> strOption ( long "nixos-folder"
-                                           <> short 'd'
-                                           <> help "Base directory for NixOS configuration"
-                                           <> value "/etc/nixos/"
-                                           <> showDefault
-                                           ))
+    pushPaths =
+      (\paths opts cache -> PushPaths opts cache paths)
+        <$> many (strArgument (metavar "PATHS..."))
+    pushWatchStore =
+      (\() opts cache -> PushWatchStore opts cache)
+        <$> flag' ()
+              ( long "watch-store"
+                  <> short 'w'
+                  <> help "Run in daemon mode and push store paths as they are added to /nix/store"
+                )
+    use =
+      Use <$> nameArg
+        <*> ( InstallationMode.UseOptions
+                <$> optional
+                      ( option (maybeReader InstallationMode.fromString)
+                          ( long "mode"
+                              <> short 'm'
+                              <> help "Mode in which to configure binary caches for Nix. Supported values: nixos, root-nixconf, user-nixconf"
+                            )
+                        )
+                <*> strOption
+                      ( long "nixos-folder"
+                          <> short 'd'
+                          <> help "Base directory for NixOS configuration"
+                          <> value "/etc/nixos/"
+                          <> showDefault
+                        )
+              )
 
 getOpts :: IO (CachixOptions, CachixCommand)
 getOpts = do
   configpath <- Config.getDefaultFilename
-  customExecParser (prefs showHelpOnEmpty) (opts configpath)
+  customExecParser (prefs showHelpOnEmpty) (optsInfo configpath)
 
-opts :: Config.ConfigPath -> ParserInfo (CachixOptions, CachixCommand)
-opts configpath = infoH parser desc
-  where parser = (,) <$> parserCachixOptions configpath <*> (parserCachixCommand <|> versionParser)
-        versionParser :: Parser CachixCommand
-        versionParser = flag' Version ( long "version"
-                     <> short 'V'
-                     <> help "Show cachix version"
-                     )
+optsInfo :: Config.ConfigPath -> ParserInfo (CachixOptions, CachixCommand)
+optsInfo configpath = infoH parser desc
+  where
+    parser = (,) <$> parserCachixOptions configpath <*> (parserCachixCommand <|> versionParser)
+    versionParser :: Parser CachixCommand
+    versionParser =
+      flag' Version
+        ( long "version"
+            <> short 'V'
+            <> help "Show cachix version"
+          )
 
 desc :: InfoMod a
-desc = fullDesc
+desc =
+  fullDesc
     <> progDesc "Sign into https://cachix.org to get started."
     <> header "cachix.org command interface"
-    -- TODO: usage footer
 
+-- TODO: usage footer
 infoH :: Parser a -> InfoMod a -> ParserInfo a
 infoH a = info (helper <*> a)
diff --git a/src/Cachix/Client/Push.hs b/src/Cachix/Client/Push.hs
--- a/src/Cachix/Client/Push.hs
+++ b/src/Cachix/Client/Push.hs
@@ -1,64 +1,70 @@
 {-# LANGUAGE DataKinds #-}
-{-# LANGUAGE TypeOperators #-}
 {-# LANGUAGE Rank2Types #-}
 {-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeOperators #-}
+
 module Cachix.Client.Push
   ( -- * Pushing a single path
-    pushSingleStorePath
-  , PushCache(..)
-  , PushStrategy(..)
-  , defaultWithXzipCompressor
-  , defaultWithXzipCompressorWithLevel
-
+    pushSingleStorePath,
+    PushCache (..),
+    PushStrategy (..),
+    defaultWithXzipCompressor,
+    defaultWithXzipCompressorWithLevel,
     -- * Pushing a closure of store paths
-  , pushClosure
-  , mapConcurrentlyBounded
-  ) where
-
-import           Crypto.Sign.Ed25519
-import qualified Control.Concurrent.QSem       as QSem
-import           Control.Concurrent.Async     (mapConcurrently)
-import           Control.Monad                ((>=>))
-import           Control.Monad.Trans.Resource (ResourceT)
-import           Control.Exception.Safe       (MonadMask, throwM)
-import           Control.Retry                (recoverAll, RetryStatus, RetryPolicy, constantDelay, limitRetries)
-import qualified Data.ByteString.Base64        as B64
-import           Data.Conduit
-import           Data.Conduit.Process
-import           Data.Conduit.Lzma              ( compress )
-import           Data.IORef
-import qualified Data.Text                      as T
-import           Network.HTTP.Types             (status404, status401)
-import           Protolude
-import           Servant.API
-import           Servant.Auth                   ()
-import           Servant.Auth.Client
-import           Servant.Client.Streaming hiding (ClientError)
-import           Servant.Conduit                ()
-import           System.Process                 ( readProcess )
+    pushClosure,
+    mapConcurrentlyBounded
+    )
+where
 
-import qualified Cachix.Api                    as Api
-import           Cachix.Api.Error
-import           Cachix.Api.Signing             (fingerprint, passthroughSizeSink, passthroughHashSink)
-import qualified Cachix.Types.NarInfoCreate    as Api
-import           Cachix.Client.Exception        ( CachixException(..) )
-import           Cachix.Client.Servant
-import           Cachix.Client.Secrets
+import qualified Cachix.Api as Api
+import Cachix.Api.Error
+import Cachix.Api.Signing (fingerprint, passthroughHashSink, passthroughHashSinkB16, passthroughSizeSink)
+import Cachix.Client.Exception (CachixException (..))
+import Cachix.Client.Secrets
+import Cachix.Client.Servant
+import Cachix.Client.Store (Store)
+import qualified Cachix.Client.Store as Store
+import qualified Cachix.Types.NarInfoCreate as Api
+import Control.Concurrent.Async (mapConcurrently)
+import qualified Control.Concurrent.QSem as QSem
+import Control.Exception.Safe (MonadMask, throwM)
+import Control.Monad ((>=>))
+import Control.Monad.Trans.Resource (ResourceT)
+import Control.Retry (RetryPolicy, RetryStatus, constantDelay, limitRetries, recoverAll)
+import Crypto.Sign.Ed25519
+import qualified Data.ByteString.Base64 as B64
+import Data.Conduit
+import Data.Conduit.Lzma (compress)
+import Data.Conduit.Process
+import Data.IORef
+import qualified Data.Set as Set
+import qualified Data.Text as T
+import Network.HTTP.Types (status401, status404)
+import Protolude
+import Servant.API
+import Servant.Auth ()
+import Servant.Auth.Client
+import Servant.Client.Streaming hiding (ClientError)
+import Servant.Conduit ()
+import qualified System.Nix.Base32
+import System.Process (readProcess)
 
+data PushCache
+  = PushCache
+      { pushCacheName :: Text,
+        pushCacheSigningKey :: SigningKey,
+        pushCacheToken :: Token
+        }
 
-data PushCache = PushCache
-  { pushCacheName :: Text
-  , pushCacheSigningKey :: SigningKey
-  , pushCacheToken :: Token
-  }
-data PushStrategy m r = PushStrategy
-  { onAlreadyPresent :: m r -- ^ Called when a path is already in the cache.
-  , onAttempt :: RetryStatus -> m ()
-  , on401 :: m r
-  , onError :: ClientError -> m r
-  , onDone :: m r
-  , withXzipCompressor :: forall a. (ConduitM ByteString ByteString (ResourceT IO) () -> m a) -> m a
-  }
+data PushStrategy m r
+  = PushStrategy
+      { onAlreadyPresent :: m r, -- ^ Called when a path is already in the cache.
+        onAttempt :: RetryStatus -> m (),
+        on401 :: m r,
+        onError :: ClientError -> m r,
+        onDone :: m r,
+        withXzipCompressor :: forall a. (ConduitM ByteString ByteString (ResourceT IO) () -> m a) -> m a
+        }
 
 defaultWithXzipCompressor :: forall m a. (ConduitM ByteString ByteString (ResourceT IO) () -> m a) -> m a
 defaultWithXzipCompressor = ($ compress (Just 2))
@@ -68,107 +74,112 @@
 
 pushSingleStorePath
   :: (MonadMask m, MonadIO m)
-  => ClientEnv  -- ^ cachix base url, connection manager, see 'Cachix.Client.URI.defaultCachixBaseUrl', 'Servant.Client.mkClientEnv'
+  => ClientEnv -- ^ cachix base url, connection manager, see 'Cachix.Client.URI.defaultCachixBaseUrl', 'Servant.Client.mkClientEnv'
+  -> Store
   -> PushCache -- ^ details for pushing to cache
   -> PushStrategy m r -- ^ how to report results, (some) errors, and do some things
   -> Text -- ^ store path
   -> m r -- ^ r is determined by the 'PushStrategy'
-pushSingleStorePath ce cache cb storePath = retryPath $ \retrystatus -> do
-
-  let (storeHash, storeSuffix) = splitStorePath $ toS storePath
+pushSingleStorePath clientEnv _store cache cb storePath = retryAll $ \retrystatus -> do
+  let storeHash = fst $ splitStorePath $ toS storePath
       name = pushCacheName cache
-
   -- Check if narinfo already exists
   -- TODO: query also cache.nixos.org? server-side?
-  res <- liftIO $ (`runClientM` ce) $ Api.narinfoHead
-    (cachixBCClient name)
-    (pushCacheToken cache)
-    (Api.NarInfoC storeHash)
+  res <-
+    liftIO $ (`runClientM` clientEnv)
+      $ Api.narinfoHead
+          (cachixBCClient name)
+          (pushCacheToken cache)
+          (Api.NarInfoC storeHash)
   case res of
     Right NoContent -> onAlreadyPresent cb -- we're done as store path is already in the cache
-    Left err | isErr err status404 -> doUpload
-             | isErr err status401 -> on401 cb
-             | otherwise -> onError cb err
-     where
-      doUpload = do
-        onAttempt cb retrystatus
-
-        narSizeRef <- liftIO $ newIORef 0
-        fileSizeRef <- liftIO $ newIORef 0
-        narHashRef <- liftIO $ newIORef ("" :: Text)
-        fileHashRef <- liftIO $ newIORef ("" :: Text)
-
-        -- stream store path as xz compressed nar file
-        let cmd = proc "nix-store" ["--dump", toS storePath]
-        -- TODO: print process stderr?
-        (ClosedStream, (stdoutStream, closeStdout), ClosedStream, cph) <- liftIO $ streamingProcess cmd
-        withXzipCompressor cb $ \xzCompressor -> do
-          let stream'
-                = stdoutStream
-                    .| passthroughSizeSink narSizeRef
-                    .| passthroughHashSink narHashRef
-                    .| xzCompressor
-                    .| passthroughSizeSink fileSizeRef
-                    .| passthroughHashSink fileHashRef
-
-          -- for now we need to use letsencrypt domain instead of cloudflare due to its upload limits
-          let newClientEnv = ce {
-                  baseUrl = (baseUrl ce) { baseUrlHost = toS name <> "." <> baseUrlHost (baseUrl ce)}
-                }
-          (_ :: NoContent) <- liftIO $ (`withClientM` newClientEnv)
-              (Api.createNar (cachixBCStreamingClient name) stream')
-              $ escalate >=> \NoContent -> do
-                  closeStdout
-                  exitcode <- waitForStreamingProcess cph
-                  when (exitcode /= ExitSuccess) $ throwM $ NarStreamingError exitcode $ show cmd
-
-                  -- TODO: we're done, so we can leave withClientM. Doing so
-                  --       will allow more concurrent requests when the number
-                  --       of XZ compressors is limited
-                  narSize <- readIORef narSizeRef
-                  narHashB16 <- readIORef narHashRef
-                  fileHash <- readIORef fileHashRef
-                  fileSize <- readIORef fileSizeRef
-
-                  -- TODO: #3: implement using pure haskell
-                  narHash <- ("sha256:" <>) . T.strip . toS <$> readProcess "nix-hash" ["--type", "sha256", "--to-base32", toS narHashB16] mempty
-
-                  deriverRaw <- T.strip . toS <$> readProcess "nix-store" ["-q", "--deriver", toS storePath] mempty
-                  let deriver = if deriverRaw == "unknown-deriver"
-                                then deriverRaw
-                                else T.drop 11 deriverRaw
-
-                  references <- sort . T.lines . T.strip . toS <$> readProcess "nix-store" ["-q", "--references", toS storePath] mempty
-
-                  let
-                      fp = fingerprint storePath narHash narSize references
-                      sig = dsign (signingSecretKey $ pushCacheSigningKey cache) fp
-                      nic = Api.NarInfoCreate
-                        { cStoreHash = storeHash
-                        , cStoreSuffix = storeSuffix
-                        , cNarHash = narHash
-                        , cNarSize = narSize
-                        , cFileSize = fileSize
-                        , cFileHash = fileHash
-                        , cReferences = fmap (T.drop 11) references
-                        , cDeriver = deriver
-                        , cSig = toS $ B64.encode $ unSignature sig
-                        }
+    Left err
+      | isErr err status404 -> uploadStorePath clientEnv _store cache cb storePath retrystatus
+      | isErr err status401 -> on401 cb
+      | otherwise -> onError cb err
 
-                  escalate $ Api.isNarInfoCreateValid nic
+uploadStorePath
+  :: (MonadMask m, MonadIO m)
+  => ClientEnv -- ^ cachix base url, connection manager, see 'Cachix.Client.URI.defaultCachixBaseUrl', 'Servant.Client.mkClientEnv'
+  -> Store
+  -> PushCache -- ^ details for pushing to cache
+  -> PushStrategy m r -- ^ how to report results, (some) errors, and do some things
+  -> Text -- ^ store path
+  -> RetryStatus
+  -> m r -- ^ r is determined by the 'PushStrategy'
+uploadStorePath clientEnv _store cache cb storePath retrystatus = do
+  let (storeHash, storeSuffix) = splitStorePath $ toS storePath
+      name = pushCacheName cache
+  onAttempt cb retrystatus
+  narSizeRef <- liftIO $ newIORef 0
+  fileSizeRef <- liftIO $ newIORef 0
+  narHashRef <- liftIO $ newIORef ("" :: ByteString)
+  fileHashRef <- liftIO $ newIORef ("" :: ByteString)
+  -- stream store path as xz compressed nar file
+  let cmd = proc "nix-store" ["--dump", toS storePath]
+  -- TODO: print process stderr?
+  (ClosedStream, (stdoutStream, closeStdout), ClosedStream, cph) <- liftIO $ streamingProcess cmd
+  withXzipCompressor cb $ \xzCompressor -> do
+    let stream' =
+          stdoutStream
+            .| passthroughSizeSink narSizeRef
+            .| passthroughHashSink narHashRef
+            .| xzCompressor
+            .| passthroughSizeSink fileSizeRef
+            .| passthroughHashSinkB16 fileHashRef
+    -- for now we need to use letsencrypt domain instead of cloudflare due to its upload limits
+    let newClientEnv =
+          clientEnv
+            { baseUrl = (baseUrl clientEnv) {baseUrlHost = toS name <> "." <> baseUrlHost (baseUrl clientEnv)}
+              }
+    (_ :: NoContent) <-
+      liftIO
+        $ (`withClientM` newClientEnv)
+            (Api.createNar (cachixBCStreamingClient name) stream')
+        $ escalate
+        >=> \NoContent -> do
+          closeStdout
+          exitcode <- waitForStreamingProcess cph
+          when (exitcode /= ExitSuccess) $ throwM $ NarStreamingError exitcode $ show cmd
+          -- TODO: we're done, so we can leave withClientM. Doing so
+          --       will allow more concurrent requests when the number
+          --       of XZ compressors is limited
+          narSize <- readIORef narSizeRef
+          narHash <- ("sha256:" <>) . System.Nix.Base32.encode <$> readIORef narHashRef
+          fileHash <- readIORef fileHashRef
+          fileSize <- readIORef fileSizeRef
+          deriverRaw <- T.strip . toS <$> readProcess "nix-store" ["-q", "--deriver", toS storePath] mempty
+          let deriver =
+                if deriverRaw == "unknown-deriver"
+                  then deriverRaw
+                  else T.drop 11 deriverRaw
+          references <- sort . T.lines . T.strip . toS <$> readProcess "nix-store" ["-q", "--references", toS storePath] mempty
+          let fp = fingerprint storePath narHash narSize references
+              sig = dsign (signingSecretKey $ pushCacheSigningKey cache) fp
+              nic = Api.NarInfoCreate
+                { cStoreHash = storeHash,
+                  cStoreSuffix = storeSuffix,
+                  cNarHash = narHash,
+                  cNarSize = narSize,
+                  cFileSize = fileSize,
+                  cFileHash = toS fileHash,
+                  cReferences = fmap (T.drop 11) references,
+                  cDeriver = deriver,
+                  cSig = toS $ B64.encode $ unSignature sig
+                  }
+          escalate $ Api.isNarInfoCreateValid nic
+          -- Upload narinfo with signature
+          escalate <=< (`runClientM` clientEnv)
+            $ Api.createNarinfo
+                (cachixBCClient name)
+                (Api.NarInfoC storeHash)
+                nic
+    onDone cb
 
-                  -- Upload narinfo with signature
-                  escalate <=< (`runClientM` ce) $ Api.createNarinfo
-                    (cachixBCClient name)
-                    (Api.NarInfoC storeHash)
-                    nic
-          onDone cb
+-- Catches all exceptions except skipAsyncExceptions
+retryAll :: (MonadIO m, MonadMask m) => (RetryStatus -> m a) -> m a
+retryAll = recoverAll defaultRetryPolicy
   where
-    -- Retry up to 5 times for each store path.
-    -- Catches all exceptions except skipAsyncExceptions
-    retryPath :: (MonadIO m, MonadMask m) => (RetryStatus -> m a) -> m a
-    retryPath = recoverAll defaultRetryPolicy
-
     defaultRetryPolicy :: RetryPolicy
     defaultRetryPolicy =
       constantDelay 50000 <> limitRetries 3
@@ -179,22 +190,41 @@
 pushClosure
   :: (MonadIO m, MonadMask m)
   => (forall a b. (a -> m b) -> [a] -> m [b])
-     -- ^ Traverse paths, responsible for bounding parallel processing of paths
-     --
-     -- For example: @'mapConcurrentlyBounded' 4@
+  -- ^ Traverse paths, responsible for bounding parallel processing of paths
+  --
+  -- For example: @'mapConcurrentlyBounded' 4@
   -> ClientEnv -- ^ See 'pushSingleStorePath'
+  -> Store
   -> PushCache
   -> (Text -> PushStrategy m r)
   -> [Text] -- ^ Initial store paths
   -> m [r] -- ^ Every @r@ per store path of the entire closure of store paths
-pushClosure traversal clientEnv pushCache pushStrategy inputStorePaths = do
-  
+pushClosure traversal clientEnv store pushCache pushStrategy inputStorePaths = do
   -- Get the transitive closure of dependencies
-  -- TODO: split args if too big
-  paths <- liftIO $ T.lines . toS <$> readProcess "nix-store" (fmap toS (["-qR"] <> inputStorePaths)) mempty
-
+  paths <-
+    liftIO $ do
+      inputs <- Store.newEmptyPathSet
+      for_ inputStorePaths $ \path -> do
+        normalized <- Store.followLinksToStorePath store (encodeUtf8 path)
+        Store.addToPathSet normalized inputs
+      closure <- Store.computeFSClosure store Store.defaultClosureParams inputs
+      Store.traversePathSet (pure . toSL) closure
+  -- Check what store paths are missing
+  -- TODO: query also cache.nixos.org? server-side?
+  missingHashesList <-
+    retryAll $ \_ ->
+      escalate
+        =<< liftIO
+              ( (`runClientM` clientEnv)
+                  $ Api.narinfoBulk
+                      (cachixBCClient (pushCacheName pushCache))
+                      (pushCacheToken pushCache)
+                      (fst . splitStorePath <$> paths)
+                )
+  let missingHashes = Set.fromList missingHashesList
+      missingPaths = filter (\path -> Set.member (fst (splitStorePath path)) missingHashes) paths
   -- TODO: make pool size configurable, on beefier machines this could be doubled
-  traversal (\path -> pushSingleStorePath clientEnv pushCache (pushStrategy path) path) paths
+  traversal (\path -> retryAll $ \retrystatus -> uploadStorePath clientEnv store pushCache (pushStrategy path) path retrystatus) missingPaths
 
 mapConcurrentlyBounded :: Traversable t => Int -> (a -> IO b) -> t a -> IO (t b)
 mapConcurrentlyBounded bound action items = do
@@ -204,7 +234,6 @@
 
 -------------------
 -- Private terms --
-
 splitStorePath :: Text -> (Text, Text)
 splitStorePath storePath =
   (T.take 32 (T.drop 11 storePath), T.drop 44 storePath)
diff --git a/src/Cachix/Client/Secrets.hs b/src/Cachix/Client/Secrets.hs
--- a/src/Cachix/Client/Secrets.hs
+++ b/src/Cachix/Client/Secrets.hs
@@ -1,24 +1,22 @@
 -- A facade for working with secrets and their representations, without delving
 -- into cryptography libraries.
-module Cachix.Client.Secrets (
-
-  -- * NAR signing
-  SigningKey(..)
-  , parseSigningKeyLenient
-  , exportSigningKey
-
-  -- TODO: * Auth token
-
-) where
+module Cachix.Client.Secrets
+  ( -- * NAR signing
+    SigningKey (..),
+    parseSigningKeyLenient,
+    exportSigningKey
+    )
+where
 
-import           Crypto.Sign.Ed25519
-import qualified Data.ByteString.Base64        as B64
-import qualified Data.ByteString.Char8         as BC
-import           Data.Char                      ( isSpace )
-import           Protolude
+-- TODO: * Auth token
+import Crypto.Sign.Ed25519
+import qualified Data.ByteString.Base64 as B64
+import qualified Data.ByteString.Char8 as BC
+import Data.Char (isSpace)
+import Protolude
 
 -- | A secret key for signing nars.
-newtype SigningKey = SigningKey { signingSecretKey :: SecretKey }
+newtype SigningKey = SigningKey {signingSecretKey :: SecretKey}
 
 parseSigningKeyLenientBS
   :: ByteString -- ^ ASCII (Base64)
@@ -28,7 +26,7 @@
       bcDropAround f = bcDropWhileEnd f . BC.dropWhile f
       stripped = bcDropAround isSpace raw
       nonNull = if BC.null stripped then Left "A signing key must not be empty" else pure stripped
-  in SigningKey . SecretKey . B64.decodeLenient <$> nonNull
+   in SigningKey . SecretKey . B64.decodeLenient <$> nonNull
 
 parseSigningKeyLenient
   :: Text -- ^ Base64
diff --git a/src/Cachix/Client/Servant.hs b/src/Cachix/Client/Servant.hs
--- a/src/Cachix/Client/Servant.hs
+++ b/src/Cachix/Client/Servant.hs
@@ -27,7 +27,7 @@
 import           Servant.Auth.Client      (Token)
 import qualified Servant.Client
 import           Servant.Client.Generic   (AsClientT)
-import           Servant.Client.Streaming
+import           Servant.Client.Streaming hiding (ClientError)
 import           Servant.Conduit          ()
 
 type ClientError =
diff --git a/src/Cachix/Client/Store.hs b/src/Cachix/Client/Store.hs
new file mode 100644
--- /dev/null
+++ b/src/Cachix/Client/Store.hs
@@ -0,0 +1,193 @@
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Cachix.Client.Store
+  ( Store,
+    -- * Getting a Store
+    openStore,
+    releaseStore,
+    -- * Query a path
+    followLinksToStorePath,
+    queryPathInfo,
+    -- * Get closures
+    computeFSClosure,
+    ClosureParams (..),
+    defaultClosureParams,
+    PathSet,
+    newEmptyPathSet,
+    addToPathSet,
+    traversePathSet,
+    -- * Miscellaneous
+    storeUri
+    )
+where
+
+import Cachix.Client.Store.Context (NixStore, Ref, ValidPathInfo, context)
+import qualified Cachix.Client.Store.Context as C hiding (context)
+import Data.ByteString.Unsafe (unsafePackMallocCString)
+import Data.Coerce
+import Foreign.ForeignPtr
+import qualified Language.C.Inline.Cpp as C
+import qualified Language.C.Inline.Cpp.Exceptions as C
+import Protolude
+import System.IO.Unsafe (unsafePerformIO)
+
+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 "aliases.h"
+
+C.using "namespace nix"
+
+-- | TODO: foreignptr
+newtype Store = Store (Ptr (Ref NixStore))
+
+openStore :: IO Store
+openStore =
+  coerce
+    [C.throwBlock| refStore* {
+      refStore s = openStore();
+      return new refStore(s);
+    } |]
+
+releaseStore :: Store -> IO ()
+releaseStore (Store store) = [C.exp| void { delete $(refStore* store) } |]
+
+-- | Follow symlinks to the store and chop off the parts after the top-level store name
+followLinksToStorePath :: Store -> ByteString -> IO ByteString
+followLinksToStorePath (Store store) bs =
+  unsafePackMallocCString
+    =<< [C.throwBlock| const char *{
+    return strdup((*$(refStore* store))->followLinksToStorePath(std::string($bs-ptr:bs, $bs-len:bs)).c_str());
+  }|]
+
+storeUri :: Store -> IO ByteString
+storeUri (Store store) =
+  unsafePackMallocCString
+    =<< [C.throwBlock| const char* {
+             std::string uri = (*$(refStore* store))->getUri();
+             return strdup(uri.c_str());
+           } |]
+
+-- TODO: test queryPathInfo
+queryPathInfo :: Store -> ByteString -> IO (ForeignPtr (Ref ValidPathInfo))
+queryPathInfo (Store store) path = do
+  vpi <-
+    [C.exp| refValidPathInfo* {
+             new refValidPathInfo((*$(refStore* store))->queryPathInfo($bs-cstr:path))
+           } |]
+  newForeignPtr finalizeRefValidPathInfo vpi
+
+finalizeRefValidPathInfo :: FinalizerPtr (Ref ValidPathInfo)
+
+{-# NOINLINE finalizeRefValidPathInfo #-}
+finalizeRefValidPathInfo =
+  unsafePerformIO
+    [C.exp|
+  void (*)(refValidPathInfo *) {
+    [](refValidPathInfo *v){ delete v; }
+  } |]
+
+----- PathSet -----
+newtype PathSet = PathSet (ForeignPtr (C.Set C.CxxString))
+
+finalizePathSet :: FinalizerPtr C.PathSet
+
+{-# NOINLINE finalizePathSet #-}
+finalizePathSet =
+  unsafePerformIO
+    [C.exp|
+  void (*)(PathSet *) {
+    [](PathSet *v){
+      delete v;
+    }
+  } |]
+
+newEmptyPathSet :: IO PathSet
+newEmptyPathSet = do
+  ptr <- [C.exp| PathSet *{ new PathSet() }|]
+  fptr <- newForeignPtr finalizePathSet ptr
+  pure $ PathSet fptr
+
+addToPathSet :: ByteString -> PathSet -> IO ()
+addToPathSet bs pathSet_ = withPathSet pathSet_ $ \pathSet ->
+  [C.throwBlock| void { 
+    $(PathSet *pathSet)->insert(std::string($bs-ptr:bs, $bs-len:bs));
+  }|]
+
+withPathSet :: PathSet -> (Ptr C.PathSet -> IO b) -> IO b
+withPathSet (PathSet pathSetFptr) = withForeignPtr pathSetFptr
+
+traversePathSet :: forall a. (ByteString -> IO a) -> PathSet -> IO [a]
+traversePathSet f pathSet_ = withPathSet pathSet_ $ \pathSet -> do
+  i <- [C.exp| PathSetIterator *{ new PathSetIterator($(PathSet *pathSet)->begin()) }|]
+  end <- [C.exp| PathSetIterator *{ new PathSetIterator ($(PathSet *pathSet)->end()) }|]
+  let cleanup =
+        [C.throwBlock| void {
+          delete $(PathSetIterator *i);
+          delete $(PathSetIterator *end);
+        }|]
+  flip finally cleanup
+    $ let go :: ([a] -> [a]) -> IO [a]
+          go acc = do
+            isDone <-
+              [C.exp| int {
+            *$(PathSetIterator *i) == *$(PathSetIterator *end)
+          }|]
+            if isDone /= 0
+              then pure $ acc []
+              else
+                do
+                  somePath <- unsafePackMallocCString =<< [C.exp| const char *{ strdup((*$(PathSetIterator *i))->c_str()) } |]
+                  a <- f somePath
+                  [C.throwBlock| void { (*$(PathSetIterator *i))++; } |]
+                  go (acc . (a :))
+     in go identity
+
+----- computeFSClosure -----
+data ClosureParams
+  = ClosureParams
+      { flipDirection :: Bool,
+        includeOutputs :: Bool,
+        includeDerivers :: Bool
+        }
+
+defaultClosureParams :: ClosureParams
+defaultClosureParams = ClosureParams
+  { flipDirection = False,
+    includeOutputs = False,
+    includeDerivers = False
+    }
+
+computeFSClosure :: Store -> ClosureParams -> PathSet -> IO PathSet
+computeFSClosure (Store store) params startingSet_ = withPathSet startingSet_ $ \startingSet -> do
+  let countTrue :: Bool -> C.CInt
+      countTrue True = 1
+      countTrue False = 0
+      flipDir = countTrue $ flipDirection params
+      inclOut = countTrue $ includeOutputs params
+      inclDrv = countTrue $ includeDerivers params
+  ps <-
+    [C.throwBlock| PathSet* {
+             PathSet *r = new PathSet();
+             (*$(refStore* store))->computeFSClosure(*$(PathSet *startingSet), *r, $(int flipDir), $(int inclOut), $(int inclDrv));
+             return r;
+           } |]
+  fp <- newForeignPtr finalizePathSet ps
+  pure $ PathSet fp
diff --git a/src/Cachix/Client/Store/Context.hs b/src/Cachix/Client/Store/Context.hs
new file mode 100644
--- /dev/null
+++ b/src/Cachix/Client/Store/Context.hs
@@ -0,0 +1,41 @@
+{-# LANGUAGE QuasiQuotes #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Cachix.Client.Store.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 hiding (Set)
+
+-- | A Nix @ref@
+data Ref a
+
+-- | A Nix @PathSet@ aka @std::set<Path>@ aka @std::set<std::string>>@
+type PathSet = Set CxxString
+
+data NixStore
+
+data ValidPathInfo
+
+-- | An STL @set@
+data Set a
+
+-- | An STL @::iterator@
+data Iterator a
+
+-- | An @std::string@
+data CxxString
+
+context :: C.Context
+context =
+  C.cppCtx <> C.fptrCtx <> C.bsCtx
+    <> mempty
+      { C.ctxTypesTable =
+          M.singleton (C.TypeName "refStore") [t|Ref NixStore|]
+            <> M.singleton (C.TypeName "refValidPathInfo")
+                 [t|Ref ValidPathInfo|]
+            <> M.singleton (C.TypeName "PathSet") [t|PathSet|]
+            <> M.singleton (C.TypeName "PathSetIterator") [t|Iterator PathSet|]
+        }
diff --git a/src/Cachix/Client/URI.hs b/src/Cachix/Client/URI.hs
--- a/src/Cachix/Client/URI.hs
+++ b/src/Cachix/Client/URI.hs
@@ -1,24 +1,25 @@
 {-# LANGUAGE GADTs #-}
 {-# LANGUAGE QuasiQuotes #-}
+
 module Cachix.Client.URI
-  ( getBaseUrl
-  , defaultCachixURI
-  , defaultCachixBaseUrl
-  ) where
+  ( getBaseUrl,
+    defaultCachixURI,
+    defaultCachixBaseUrl
+    )
+where
 
 import Protolude
+import Servant.Client
 import qualified URI.ByteString as UBS
 import URI.ByteString hiding (Scheme)
-import Servant.Client
 import URI.ByteString.QQ
 
-
 -- TODO: make getBaseUrl internal
 
 -- | Partial function from URI to BaseUrl
 --
 getBaseUrl :: URIRef Absolute -> BaseUrl
-getBaseUrl URI{..} =
+getBaseUrl URI {..} =
   case uriAuthority of
     Nothing -> panic "missing host in url"
     Just authority ->
@@ -33,10 +34,8 @@
           UBS.Scheme "http" -> Http
           UBS.Scheme "https" -> Https
           _ -> panic "uri can only be http/https"
-
         getPort :: Int
         getPort = maybe defaultPort portNumber $ authorityPort authority
-
         defaultPort :: Int
         defaultPort = case getScheme of
           Http -> 80
diff --git a/test/InstallationModeSpec.hs b/test/InstallationModeSpec.hs
--- a/test/InstallationModeSpec.hs
+++ b/test/InstallationModeSpec.hs
@@ -1,102 +1,65 @@
 module InstallationModeSpec where
 
+import Cachix.Client.InstallationMode
+import qualified Cachix.Client.NixConf as NixConf
 import Protolude
 import Test.Hspec
 
-import qualified Cachix.Client.NixConf as NixConf
-import           Cachix.Client.NixVersion ( NixVersion(..) )
-import           Cachix.Client.InstallationMode
-
-
 spec :: Spec
 spec = describe "getInstallationMode" $ do
-  it "NixOS with root prints configuration" $
-    let
-      nixenv = NixEnv
-        { nixVersion = Nix20 -- any except 1.0
-        , isTrusted = True -- any
-        , isRoot = True
-        , isNixOS = True
-        }
-    in getInstallationMode nixenv `shouldBe` EchoNixOS Nix20
-  it "NixOS without trust prints configuration plus trust" $
-    let
-      nixenv = NixEnv
-        { nixVersion = Nix201
-        , isTrusted = False
-        , isRoot = False
-        , isNixOS = True
-        }
-    in getInstallationMode nixenv `shouldBe` EchoNixOSWithTrustedUser Nix201
-  it "NixOS without trust prints configuration on older Nix" $
-    let
-      nixenv = NixEnv
-        { nixVersion = Nix20
-        , isTrusted = False
-        , isRoot = False
-        , isNixOS = True
-        }
-    in getInstallationMode nixenv `shouldBe` EchoNixOS Nix20
-  it "NixOS non-root trusted results into local install" $
-    let
-      nixenv = NixEnv
-        { nixVersion = Nix201
-        , isTrusted = True
-        , isRoot = False
-        , isNixOS = True
-        }
-    in getInstallationMode nixenv `shouldBe` Install Nix201 NixConf.Local
-  it "non-NixOS root results into global install" $
-    let
-      nixenv = NixEnv
-        { nixVersion = Nix201
-        , isTrusted = True
-        , isRoot = True
-        , isNixOS = False
-        }
-    in getInstallationMode nixenv `shouldBe` Install Nix201 NixConf.Global
-  it "non-NixOS with Nix 1.X root results into global install" $
-      let
-        nixenv = NixEnv
-          { nixVersion = Nix1XX
-          , isTrusted = True
-          , isRoot = True
-          , isNixOS = False -- any
-          }
-      in getInstallationMode nixenv `shouldBe` Install Nix1XX NixConf.Global
-  it "non-NixOS non-root trusted results into local install" $
-    let
-      nixenv = NixEnv
-        { nixVersion = Nix201
-        , isTrusted = True
-        , isRoot = False
-        , isNixOS = False
-        }
-    in getInstallationMode nixenv `shouldBe` Install Nix201 NixConf.Local
-  it "non-NixOS non-root non-trusted results into required sudo" $
-    let
-      nixenv = NixEnv
-        { nixVersion = Nix201
-        , isTrusted = False
-        , isRoot = False
-        , isNixOS = False
-        }
-    in getInstallationMode nixenv `shouldBe` UntrustedRequiresSudo
-  it "non-NixOS non-root non-trusted on Nix 2.0.0 results into required sudo with upgrade warning" $
-    let
-      nixenv = NixEnv
-        { nixVersion = Nix20
-        , isTrusted = False
-        , isRoot = False
-        , isNixOS = False
-        }
-    in getInstallationMode nixenv `shouldBe` Nix20RequiresSudo
-  it "non-NixOS non-root trusted on Nix 2.0.0 results into required sudo with upgrade warning" $
-    let
-      nixenv = NixEnv
-        { nixVersion = Nix20
-        , isTrusted = True
-        , isRoot = False
-        , isNixOS = False
-        }
-    in getInstallationMode nixenv `shouldBe` Nix20RequiresSudo
+  it "NixOS with root prints configuration"
+    $ let nixenv = NixEnv
+            { isTrusted = True, -- any
+              isRoot = True,
+              isNixOS = True
+              }
+     in getInstallationMode nixenv `shouldBe` WriteNixOS
+  it "NixOS without trust prints configuration plus trust"
+    $ let nixenv = NixEnv
+            { isTrusted = False,
+              isRoot = False,
+              isNixOS = True
+              }
+     in getInstallationMode nixenv `shouldBe` UntrustedRequiresSudo
+  it "NixOS without trust prints configuration on older Nix"
+    $ let nixenv = NixEnv
+            { isTrusted = False,
+              isRoot = False,
+              isNixOS = True
+              }
+     in getInstallationMode nixenv `shouldBe` UntrustedRequiresSudo
+  it "NixOS non-root trusted results into local install"
+    $ let nixenv = NixEnv
+            { isTrusted = True,
+              isRoot = False,
+              isNixOS = True
+              }
+     in getInstallationMode nixenv `shouldBe` Install NixConf.Local
+  it "non-NixOS root results into global install"
+    $ let nixenv = NixEnv
+            { isTrusted = True,
+              isRoot = True,
+              isNixOS = False
+              }
+     in getInstallationMode nixenv `shouldBe` Install NixConf.Global
+  it "non-NixOS with Nix 1.X root results into global install"
+    $ let nixenv = NixEnv
+            { isTrusted = True,
+              isRoot = True,
+              isNixOS = False -- any
+              }
+     in getInstallationMode nixenv `shouldBe` Install NixConf.Global
+  it "non-NixOS non-root trusted results into local install"
+    $ let nixenv = NixEnv
+            { isTrusted = True,
+              isRoot = False,
+              isNixOS = False
+              }
+     in getInstallationMode nixenv `shouldBe` Install NixConf.Local
+  it "non-NixOS non-root non-trusted results into required sudo"
+    $ let nixenv = NixEnv
+            { isTrusted = False,
+              isRoot = False,
+              isNixOS = False
+              }
+     in getInstallationMode nixenv `shouldBe` UntrustedRequiresSudo
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,12 +1,13 @@
 module Main where
 
 import Protolude
-import Test.Hspec.Runner
 import qualified Spec
+import Test.Hspec.Runner
 
 main :: IO ()
 main = hspecWith config Spec.spec
   where
-    config = defaultConfig
-      { configColorMode = ColorAlways
-      }
+    config =
+      defaultConfig
+        { configColorMode = ColorAlways
+          }
diff --git a/test/NetRcSpec.hs b/test/NetRcSpec.hs
--- a/test/NetRcSpec.hs
+++ b/test/NetRcSpec.hs
@@ -1,53 +1,47 @@
 module NetRcSpec where
 
-import Paths_cachix
+import Cachix.Api (BinaryCache (..))
+import Cachix.Client.Config (Config (..))
+import qualified Cachix.Client.NetRc as NetRc
 import Protolude
 import System.Directory (copyFile)
-import System.IO.Temp   (withSystemTempFile)
+import System.IO.Temp (withSystemTempFile)
 import Test.Hspec
 
-import           Cachix.Api (BinaryCache(..))
-import qualified Cachix.Client.NetRc as NetRc
-import           Cachix.Client.Config (Config(..))
-
-
 bc1 :: BinaryCache
 bc1 = BinaryCache
- { name = "name"
- , uri = "https://name.cachix.org"
- , publicSigningKeys = ["pub"]
- , isPublic = False
- , githubUsername = "foobar"
- }
+  { name = "name",
+    uri = "https://name.cachix.org",
+    publicSigningKeys = ["pub"],
+    isPublic = False,
+    githubUsername = "foobar"
+    }
 
 bc2 :: BinaryCache
 bc2 = BinaryCache
-  { name = "name2"
-  , uri = "https://name2.cachix.org"
-  , publicSigningKeys = ["pub2"]
-  , isPublic = False
-  , githubUsername = "foobar2"
-  }
+  { name = "name2",
+    uri = "https://name2.cachix.org",
+    publicSigningKeys = ["pub2"],
+    isPublic = False,
+    githubUsername = "foobar2"
+    }
 
 config :: Config
 config = Config
-  { authToken = "token123"
-  , binaryCaches = []
-  }
+  { authToken = "token123",
+    binaryCaches = []
+    }
 
 -- TODO: poor man's golden tests, use https://github.com/stackbuilders/hspec-golden
 test :: [BinaryCache] -> Text -> Expectation
 test binaryCaches goldenName = withSystemTempFile "hspec-netrc" $ \filepath _ -> do
-    input <- getInput
-    output <- getOutput
-    copyFile input filepath
-    NetRc.add config binaryCaches filepath
-    real <- readFile filepath
-    expected <- readFile output
-    real `shouldBe` expected
-  where
-    getInput = getDataFileName $ toS goldenName <> ".input"
-    getOutput = getDataFileName $ toS goldenName <> ".output"
+  let input = "test/data/" <> toS goldenName <> ".input"
+      output = "test/data/" <> toS goldenName <> ".output"
+  copyFile input filepath
+  NetRc.add config binaryCaches filepath
+  real <- readFile filepath
+  expected <- readFile output
+  real `shouldBe` expected
 
 spec :: Spec
 spec =
diff --git a/test/NixConfSpec.hs b/test/NixConfSpec.hs
--- a/test/NixConfSpec.hs
+++ b/test/NixConfSpec.hs
@@ -1,137 +1,148 @@
 {-# LANGUAGE QuasiQuotes #-}
+
 module NixConfSpec where
 
-import Protolude
+import Cachix.Api (BinaryCache (..))
+import Cachix.Client.NixConf as NixConf
 import Data.String.Here
+import Protolude
 import Test.Hspec
 
-import Cachix.Client.NixConf as NixConf
-import Cachix.Client.NixVersion (NixVersion(..))
-import Cachix.Api (BinaryCache(..))
-
-
 property :: Text -> Expectation
-property x = NixConf.render Nix20 <$> parse x `shouldBe` Right x
-
-propertyNix1 :: Text -> Expectation
-propertyNix1 x = NixConf.render Nix1XX <$> parse x `shouldBe` Right x
+property x = NixConf.render <$> parse x `shouldBe` Right x
 
 bc :: BinaryCache
 bc = BinaryCache
- { name = "name"
- , uri = "https://name.cachix.org"
- , isPublic = True
- , publicSigningKeys = ["pub"]
- , githubUsername = "foobar"
- }
+  { name = "name",
+    uri = "https://name.cachix.org",
+    isPublic = True,
+    publicSigningKeys = ["pub"],
+    githubUsername = "foobar"
+    }
 
 spec :: Spec
 spec = do
   describe "render . parse" $ do
-    it "handles single value substituters" $
-      property "substituters = a\n"
-    it "handles multi value substituters" $
-      property "substituters = a b c\n"
-    it "handles all known keys" $
-      property "substituters = a b c\ntrusted-users = him me\ntrusted-public-keys = a\n"
-    it "handles all known keys for Nix 1.0" $
-      propertyNix1 "binary-caches = a b c\ntrusted-users = him me\nbinary-cache-public-keys = a\n"
-    it "random content" $
-      property "blabla = foobar\nfoo = bar\n"
+    it "handles single value substituters"
+      $ property "substituters = a\n"
+    it "handles multi value substituters"
+      $ property "substituters = a b c\n"
+    it "handles all known keys"
+      $ property "substituters = a b c\ntrusted-users = him me\ntrusted-public-keys = a\n"
+    it "random content"
+      $ property "blabla = foobar\nfoo = bar\n"
   describe "add" $ do
-    it "merges binary caches from both files" $
-      let
-        globalConf = NixConf
-          [ Substituters ["bc1"]
-          , TrustedPublicKeys ["pub1"]
-          ]
-        localConf = NixConf
-          [ Substituters ["bc2"]
-          , TrustedPublicKeys ["pub2"]
-          ]
-        result = NixConf
-          [ Substituters [defaultPublicURI, "bc1", "bc2", "https://name.cachix.org"]
-          , TrustedPublicKeys [defaultSigningKey, "pub1", "pub2", "pub"]
-          ]
-      in add bc [globalConf, localConf] localConf `shouldBe` result
-    it "is noop if binary cache exists in one file" $
-      let
-        globalConf = NixConf
-          [ Substituters [defaultPublicURI, "https://name.cachix.org"]
-          , TrustedPublicKeys [defaultSigningKey, "pub"]
-          ]
-        localConf = NixConf [ ]
-        result = NixConf
-          [ Substituters [defaultPublicURI, "https://name.cachix.org"]
-          , TrustedPublicKeys [defaultSigningKey, "pub"]
-          ]
-      in add bc [globalConf, localConf] localConf `shouldBe` result
-    it "preserves other nixconf entries" $
-      let
-        globalConf = NixConf
-          [ Substituters ["http"]
-          , TrustedPublicKeys ["pub1"]
-          , TrustedUsers ["user"]
-          , Other "foo"
-          ]
-        localConf = NixConf
-          [ TrustedUsers ["user2"]
-          , Other "bar"
-          ]
-        result = NixConf
-          [ TrustedUsers ["user2"]
-          , Other "bar"
-          , Substituters [defaultPublicURI, "http", "https://name.cachix.org"]
-          , TrustedPublicKeys [defaultSigningKey, "pub1", "pub"]
-          ]
-      in add bc [globalConf, localConf] localConf `shouldBe` result
-    it "removed duplicates" $
-      let
-        globalConf = NixConf
-          [ Substituters ["bc1", "bc1"]
-          , TrustedPublicKeys ["pub1", "pub1"]
-          ]
-        localConf = NixConf
-          [ Substituters ["bc2", "bc2"]
-          , TrustedPublicKeys ["pub2", "pub2"]
-          ]
-        result = NixConf
-          [ Substituters [defaultPublicURI, "bc1", "bc2", "https://name.cachix.org"]
-          , TrustedPublicKeys [defaultSigningKey, "pub1", "pub2", "pub"]
-          ]
-      in add bc [globalConf, localConf] localConf `shouldBe` result
-    it "adds binary cache and defaults if no existing entries exist" $
-      let
-        globalConf = NixConf []
-        localConf = NixConf []
-        result = NixConf
-          [ Substituters [defaultPublicURI, "https://name.cachix.org"]
-          , TrustedPublicKeys [defaultSigningKey, "pub"]
-          ]
-      in add bc [globalConf, localConf] localConf `shouldBe` result
+    it "merges binary caches from both files"
+      $ let globalConf =
+              NixConf
+                [ Substituters ["bc1"],
+                  TrustedPublicKeys ["pub1"]
+                  ]
+            localConf =
+              NixConf
+                [ Substituters ["bc2"],
+                  TrustedPublicKeys ["pub2"]
+                  ]
+            result =
+              NixConf
+                [ Substituters [defaultPublicURI, "bc1", "bc2", "https://name.cachix.org"],
+                  TrustedPublicKeys [defaultSigningKey, "pub1", "pub2", "pub"]
+                  ]
+       in add bc [globalConf, localConf] localConf `shouldBe` result
+    it "is noop if binary cache exists in one file"
+      $ let globalConf =
+              NixConf
+                [ Substituters [defaultPublicURI, "https://name.cachix.org"],
+                  TrustedPublicKeys [defaultSigningKey, "pub"]
+                  ]
+            localConf = NixConf []
+            result =
+              NixConf
+                [ Substituters [defaultPublicURI, "https://name.cachix.org"],
+                  TrustedPublicKeys [defaultSigningKey, "pub"]
+                  ]
+       in add bc [globalConf, localConf] localConf `shouldBe` result
+    it "preserves other nixconf entries"
+      $ let globalConf =
+              NixConf
+                [ Substituters ["http"],
+                  TrustedPublicKeys ["pub1"],
+                  TrustedUsers ["user"],
+                  Other "foo"
+                  ]
+            localConf =
+              NixConf
+                [ TrustedUsers ["user2"],
+                  Other "bar"
+                  ]
+            result =
+              NixConf
+                [ TrustedUsers ["user2"],
+                  Other "bar",
+                  Substituters [defaultPublicURI, "http", "https://name.cachix.org"],
+                  TrustedPublicKeys [defaultSigningKey, "pub1", "pub"]
+                  ]
+       in add bc [globalConf, localConf] localConf `shouldBe` result
+    it "removed duplicates"
+      $ let globalConf =
+              NixConf
+                [ Substituters ["bc1", "bc1"],
+                  TrustedPublicKeys ["pub1", "pub1"]
+                  ]
+            localConf =
+              NixConf
+                [ Substituters ["bc2", "bc2"],
+                  TrustedPublicKeys ["pub2", "pub2"]
+                  ]
+            result =
+              NixConf
+                [ Substituters [defaultPublicURI, "bc1", "bc2", "https://name.cachix.org"],
+                  TrustedPublicKeys [defaultSigningKey, "pub1", "pub2", "pub"]
+                  ]
+       in add bc [globalConf, localConf] localConf `shouldBe` result
+    it "adds binary cache and defaults if no existing entries exist"
+      $ let globalConf = NixConf []
+            localConf = NixConf []
+            result =
+              NixConf
+                [ Substituters [defaultPublicURI, "https://name.cachix.org"],
+                  TrustedPublicKeys [defaultSigningKey, "pub"]
+                  ]
+       in add bc [globalConf, localConf] localConf `shouldBe` result
   describe "parse" $ do
-    it "parses substituters" $
-      parse "substituters = a\n" `shouldBe` (Right $ NixConf [Substituters ["a"]])
-    it "parses long key" $
-      parse "binary-caches-parallel-connections = 40\n" `shouldBe` (Right $ NixConf [Other "binary-caches-parallel-connections = 40"])
-    it "parses substituters with multiple values" $
-      parse "substituters = a b c\n" `shouldBe` (Right $ NixConf [Substituters ["a", "b", "c"]])
-    it "parses equal sign after the first key as literal" $
-      parse "substituters = a b c= d\n" `shouldBe` (Right $ NixConf [Substituters ["a", "b", "c=", "d"]])
-    it "parses with missing endline" $
-      parse "allowed-users = *"  `shouldBe` (Right $ NixConf [Other "allowed-users = *"])
-    it "parses a complex example" $
-      parse realExample `shouldBe` (Right $ NixConf [ Other ""
-                                                    , Substituters ["a","b","c"]
-                                                    , TrustedUsers ["him","me"]
-                                                    , TrustedPublicKeys ["a"]
-                                                    , Other "blabla =  asd"
-                                                    , Other "# comment"
-                                                    , Other ""
-                                                    , Other ""])
+    it "parses substituters"
+      $ parse "substituters = a\n"
+      `shouldBe` (Right $ NixConf [Substituters ["a"]])
+    it "parses long key"
+      $ parse "binary-caches-parallel-connections = 40\n"
+      `shouldBe` (Right $ NixConf [Other "binary-caches-parallel-connections = 40"])
+    it "parses substituters with multiple values"
+      $ parse "substituters = a b c\n"
+      `shouldBe` (Right $ NixConf [Substituters ["a", "b", "c"]])
+    it "parses equal sign after the first key as literal"
+      $ parse "substituters = a b c= d\n"
+      `shouldBe` (Right $ NixConf [Substituters ["a", "b", "c=", "d"]])
+    it "parses with missing endline"
+      $ parse "allowed-users = *"
+      `shouldBe` (Right $ NixConf [Other "allowed-users = *"])
+    it "parses a complex example"
+      $ parse realExample
+      `shouldBe` ( Right
+                     $ NixConf
+                         [ Other "",
+                           Substituters ["a", "b", "c"],
+                           TrustedUsers ["him", "me"],
+                           TrustedPublicKeys ["a"],
+                           Other "blabla =  asd",
+                           Other "# comment",
+                           Other "",
+                           Other ""
+                           ]
+                   )
 
 realExample :: Text
-realExample = [hereLit|
+realExample =
+  [hereLit|
 substituters = a  b c
 trusted-users = him me
 trusted-public-keys  =  a
diff --git a/test/NixVersionSpec.hs b/test/NixVersionSpec.hs
--- a/test/NixVersionSpec.hs
+++ b/test/NixVersionSpec.hs
@@ -1,21 +1,26 @@
 module NixVersionSpec where
 
+import Cachix.Client.NixVersion
 import Protolude
 import Test.Hspec
 
-import Cachix.Client.NixVersion
-
 spec :: Spec
 spec = describe "parseNixVersion" $ do
-  it "parses 'nix-env (Nix) 2.0' as Nix20" $
-    parseNixVersion "nix-env (Nix) 2.0" `shouldBe` Right Nix20
-  it "parses 'nix-env (Nix) 2.0.1' as Nix201" $
-    parseNixVersion "nix-env (Nix) 2.0.1" `shouldBe` Right Nix201
-  it "parses 'nix-env (Nix) 1.11.13' as Nix1XX" $
-    parseNixVersion "nix-env (Nix) 1.11.13" `shouldBe` Right Nix1XX
-  it "parses 'nix-env (Nix) 2.0.5' as Nix201" $
-    parseNixVersion "nix-env (Nix) 2.0.5" `shouldBe` Right Nix201
-  it "parses 'nix-env (Nix) 2.0.1pre6053_444b921' as Nix201" $
-    parseNixVersion "nix-env (Nix) 2.0.1pre6053_444b921" `shouldBe` Right Nix201
-  it "fails with unknown string 'foobar'" $
-    parseNixVersion "foobar" `shouldBe` Left "Couldn't parse 'nix-env --version' output: foobar"
+  it "parses 'nix-env (Nix) 2.0' as Nix20"
+    $ parseNixVersion "nix-env (Nix) 2.0"
+    `shouldBe` Left "Nix 2.0.2 or lower is not supported. Please upgrade: https://nixos.org/nix/"
+  it "parses 'nix-env (Nix) 2.0.1' as Nix201"
+    $ parseNixVersion "nix-env (Nix) 2.0.1"
+    `shouldBe` Right ()
+  it "parses 'nix-env (Nix) 1.11.13' as Nix1XX"
+    $ parseNixVersion "nix-env (Nix) 1.11.13"
+    `shouldBe` Left "Nix 2.0.2 or lower is not supported. Please upgrade: https://nixos.org/nix/"
+  it "parses 'nix-env (Nix) 2.0.5' as Nix201"
+    $ parseNixVersion "nix-env (Nix) 2.0.5"
+    `shouldBe` Right ()
+  it "parses 'nix-env (Nix) 2.0.1pre6053_444b921' as Nix201"
+    $ parseNixVersion "nix-env (Nix) 2.0.1pre6053_444b921"
+    `shouldBe` Right ()
+  it "fails with unknown string 'foobar'"
+    $ parseNixVersion "foobar"
+    `shouldBe` Left "Couldn't parse 'nix-env --version' output: foobar"
