diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,4 @@
+# Revision history for cli-nix
+
+## 0.1.0.0
+* Initial release
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2020 Obsidian Systems LLC
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Obsidian Systems LLC nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/cli-nix.cabal b/cli-nix.cabal
new file mode 100644
--- /dev/null
+++ b/cli-nix.cabal
@@ -0,0 +1,28 @@
+cabal-version: >=1.10
+name: cli-nix
+version: 0.1.0.0
+license: BSD3
+license-file: LICENSE
+copyright: Obsidian Systems LLC 2020
+maintainer: maintainer@obsidian.systems
+author: Obsidian Systems LLC
+synopsis: Bindings to the nix command-line interface
+category: Nix, Bindings
+build-type: Simple
+extra-source-files:
+    CHANGELOG.md
+
+library
+    exposed-modules:
+        Bindings.Cli.Nix
+    hs-source-dirs: src
+    default-language: Haskell2010
+    build-depends:
+        base ==4.12.*,
+        cli-extras >=0.1.0.0 && <0.2,
+        lens >=4.17.1 && <4.18,
+        data-default >=0.7.1.1 && <0.8,
+        text >=1.2.3.1 && <1.3,
+        exceptions >=0.10.3 && <0.11,
+        mtl >=2.2.2 && <2.3,
+        logging-effect >=1.3.4 && <1.4
diff --git a/src/Bindings/Cli/Nix.hs b/src/Bindings/Cli/Nix.hs
new file mode 100644
--- /dev/null
+++ b/src/Bindings/Cli/Nix.hs
@@ -0,0 +1,238 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE ConstraintKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE Rank2Types #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE TypeApplications #-}
+module Bindings.Cli.Nix
+  ( Arg (..)
+  , NixBuildConfig (..)
+  , nixBuildConfig_common
+  , nixBuildConfig_outLink
+  , NixCmd (..)
+  , nixCmdConfig_args
+  , nixCmdConfig_builders
+  , nixCmdConfig_target
+  , NixCommonConfig (..)
+  , NixInstantiateConfig (..)
+  , nixInstantiateConfig_eval
+  , NixShellConfig (..)
+  , nixShellConfig_common
+  , nixShellConfig_pure
+  , nixShellConfig_run
+  , OutLink (..)
+  , Target (..)
+  , target_attr
+  , target_expr
+  , target_path
+
+  , boolArg
+  , nixCmd
+  , nixCmdProc
+  , nixCmdProc'
+  , rawArg
+  , runNixShellConfig
+  , strArg
+  ) where
+
+import Control.Lens
+import Control.Monad (guard)
+import Control.Monad.Catch (MonadMask)
+import Control.Monad.Except (MonadError)
+import Control.Monad.Fail
+import Control.Monad.IO.Class (MonadIO)
+import Control.Monad.Log (MonadLog)
+
+import Data.Bool (bool)
+import Data.Default
+import Data.List (intercalate)
+import Data.Maybe
+import Data.Monoid ((<>))
+import qualified Data.Text as T
+
+import Cli.Extras
+
+-- | What to build
+data Target = Target
+  { _target_path :: Maybe FilePath
+  , _target_attr :: Maybe String
+  , _target_expr :: Maybe String
+  }
+makeClassy ''Target
+
+instance Default Target where
+  def = Target
+    { _target_path = Just "."
+    , _target_attr = Nothing
+    , _target_expr = Nothing
+    }
+
+data Arg
+  = Arg_Str String String
+  | Arg_Expr String String
+  deriving (Eq, Show)
+
+strArg :: String -> String -> Arg
+strArg = Arg_Str
+
+rawArg :: String -> String -> Arg
+rawArg = Arg_Expr
+
+boolArg :: String -> Bool -> Arg
+boolArg k = Arg_Expr k . bool "false" "true"
+
+cliFromArgs :: [Arg] -> [String]
+cliFromArgs = concatMap $ \case
+  Arg_Str k v -> ["--argstr", k, v]
+  Arg_Expr k v -> ["--arg", k, v]
+
+data NixCommonConfig = NixCommonConfig
+  { _nixCmdConfig_target :: Target
+  , _nixCmdConfig_args :: [Arg]
+  , _nixCmdConfig_builders :: [String]
+  }
+makeClassy ''NixCommonConfig
+
+instance Default NixCommonConfig where
+  def = NixCommonConfig def mempty mempty
+
+runNixCommonConfig :: NixCommonConfig -> [String]
+runNixCommonConfig cfg = mconcat [maybeToList path, attrArg, exprArg, args, buildersArg]
+  where
+    path = _target_path $ _nixCmdConfig_target cfg
+    attr = _target_attr $ _nixCmdConfig_target cfg
+    expr = _target_expr $ _nixCmdConfig_target cfg
+    attrArg = case attr of
+      Nothing -> []
+      Just a -> ["-A", a]
+    exprArg = case expr of
+      Nothing -> []
+      Just a -> ["-E", a]
+    args = cliFromArgs $ _nixCmdConfig_args cfg
+    buildersArg = case _nixCmdConfig_builders cfg of
+      [] -> []
+      builders -> ["--builders", intercalate ";" builders]
+
+-- | Where to put nix-build output
+data OutLink
+  = OutLink_Default
+  | OutLink_None
+  | OutLink_IndirectRoot FilePath
+
+instance Default OutLink where
+  def = OutLink_Default
+
+data NixBuildConfig = NixBuildConfig
+  { _nixBuildConfig_common :: NixCommonConfig
+  , _nixBuildConfig_outLink :: OutLink
+  }
+makeLenses ''NixBuildConfig
+
+instance HasNixCommonConfig NixBuildConfig where
+  nixCommonConfig = nixBuildConfig_common
+
+instance Default NixBuildConfig where
+  def = NixBuildConfig def def
+
+runNixBuildConfig :: NixBuildConfig -> [String]
+runNixBuildConfig cfg = mconcat
+  [ runNixCommonConfig $ cfg ^. nixCommonConfig
+  , case _nixBuildConfig_outLink cfg of
+      OutLink_Default -> []
+      OutLink_None -> ["--no-out-link"]
+      OutLink_IndirectRoot l -> ["--out-link", l]
+  ]
+
+data NixInstantiateConfig = NixInstantiateConfig
+  { _nixInstantiateConfig_common :: NixCommonConfig
+  , _nixInstantiateConfig_eval :: Bool
+  }
+makeLenses ''NixInstantiateConfig
+
+instance HasNixCommonConfig NixInstantiateConfig where
+  nixCommonConfig = nixInstantiateConfig_common
+
+instance Default NixInstantiateConfig where
+  def = NixInstantiateConfig def False
+
+runNixInstantiateConfig :: NixInstantiateConfig -> [String]
+runNixInstantiateConfig cfg = mconcat
+  [ runNixCommonConfig $ cfg ^. nixCommonConfig
+  , "--eval" <$ guard (_nixInstantiateConfig_eval cfg)
+  ]
+
+data NixShellConfig = NixShellConfig
+  { _nixShellConfig_common :: NixCommonConfig
+  , _nixShellConfig_pure :: Bool
+  , _nixShellConfig_run :: Maybe String
+  }
+
+makeLenses ''NixShellConfig
+
+instance HasNixCommonConfig NixShellConfig where
+  nixCommonConfig = nixShellConfig_common
+
+instance Default NixShellConfig where
+  def = NixShellConfig def False Nothing
+
+data NixCmd
+  = NixCmd_Build NixBuildConfig
+  | NixCmd_Instantiate NixInstantiateConfig
+
+instance Default NixCmd where
+  def = NixCmd_Build def
+
+runNixShellConfig :: NixShellConfig -> [String]
+runNixShellConfig cfg = mconcat
+  [ runNixCommonConfig $ cfg ^. nixCommonConfig
+  , [ "--pure" | cfg ^. nixShellConfig_pure ]
+  ] ++ mconcat [
+    ["--run", run] | run <- maybeToList $ cfg ^. nixShellConfig_run
+  ]
+
+nixCmdProc :: NixCmd -> ProcessSpec
+nixCmdProc = fst . nixCmdProc'
+
+nixCmdProc' :: NixCmd -> (ProcessSpec, T.Text)
+nixCmdProc' cmdCfg = (proc (T.unpack cmd) options, cmd)
+  where
+    (cmd, options) = case cmdCfg of
+      NixCmd_Build cfg' ->
+        ( "nix-build"
+        , runNixBuildConfig cfg'
+        )
+      NixCmd_Instantiate cfg' ->
+        ( "nix-instantiate"
+        , runNixInstantiateConfig cfg'
+        )
+
+nixCmd
+  :: ( MonadIO m
+     , MonadMask m
+     , MonadLog Output m
+     , HasCliConfig m
+     , MonadError e m
+     , AsProcessFailure e
+     , MonadFail m
+     )
+  => NixCmd
+  -> m FilePath
+nixCmd cmdCfg = withSpinner' (T.unwords $ "Running" : cmd : desc) (Just $ const $ T.unwords $ "Built" : desc) $ do
+  output <- readProcessAndLogStderr Debug cmdProc
+  -- Remove final newline that Nix appends
+  Just (outPath, '\n') <- pure $ T.unsnoc output
+  pure $ T.unpack outPath
+  where
+    (cmdProc, cmd) = nixCmdProc' cmdCfg
+    commonCfg = case cmdCfg of
+      NixCmd_Build cfg' -> cfg' ^. nixCommonConfig
+      NixCmd_Instantiate cfg' -> cfg' ^. nixCommonConfig
+    path = commonCfg ^. nixCmdConfig_target . target_path
+    desc = concat $ catMaybes
+      [ (\x -> ["on", T.pack x]) <$> path
+      , (\a -> ["[" <> T.pack a <> "]"]) <$> (commonCfg ^. nixCmdConfig_target . target_attr)
+      ]
