diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for env-guard
+
+## 0.1 -- 2022-06-29
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Thomas Bidne
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,16 @@
+<div align="center">
+
+# env-guard
+
+[![Hackage](https://img.shields.io/hackage/v/env-guard)](https://hackage.haskell.org/package/env-guard)
+[![MIT](https://img.shields.io/github/license/tbidne/env-guard?color=blue)](https://opensource.org/licenses/MIT)
+
+[![nix](https://img.shields.io/github/workflow/status/tbidne/env-guard/nix/main?label=nix%209.2.3&&logo=nixos&logoColor=85c5e7&labelColor=2f353c)](https://github.com/tbidne/env-guard/actions/workflows/nix_ci.yaml)
+[![stack](https://img.shields.io/github/workflow/status/tbidne/env-guard/stack/main?label=stack%2019.4&logoColor=white&labelColor=2f353c)](https://github.com/tbidne/env-guard/actions/workflows/stack_ci.yaml)
+[![style](https://img.shields.io/github/workflow/status/tbidne/env-guard/style/main?label=style&logoColor=white&labelColor=2f353c)](https://github.com/tbidne/env-guard/actions/workflows/style_ci.yaml)
+
+[![8.10.7](https://img.shields.io/github/workflow/status/tbidne/env-guard/8.10.7/main?label=8.10.7&logo=haskell&logoColor=904d8c&labelColor=2f353c)](https://github.com/tbidne/env-guard/actions/workflows/ghc_8-10.yaml)
+[![9.0.2](https://img.shields.io/github/workflow/status/tbidne/env-guard/9.0.2/main?label=9.0.2&logo=haskell&logoColor=904d8c&labelColor=2f353c)](https://github.com/tbidne/env-guard/actions/workflows/ghc_9-0.yaml)
+[![9.2.3](https://img.shields.io/github/workflow/status/tbidne/env-guard/9.2.3/main?label=9.2.3&logo=haskell&logoColor=904d8c&labelColor=2f353c)](https://github.com/tbidne/env-guard/actions/workflows/ghc_9-2.yaml)
+
+</div>
diff --git a/env-guard.cabal b/env-guard.cabal
new file mode 100644
--- /dev/null
+++ b/env-guard.cabal
@@ -0,0 +1,46 @@
+cabal-version:      2.4
+name:               env-guard
+version:            0.1
+license:            MIT
+license-file:       LICENSE
+tested-with:        GHC ==8.10.7 || ==9.0.2 || ==9.2.3
+copyright:          2022 Thomas Bidne
+author:             Thomas Bidne
+maintainer:         tbidne@protonmail.com
+homepage:           https://github.com/tbidne/env-guard/
+bug-reports:        https://github.com/tbidne/env-guard/issues
+synopsis:
+  Conditionally running IO actions based on environment variables.
+
+description:
+  This package provides functions for validating presence / contents of
+  environment variables, and conditionally running IO actions based on the
+  result.
+
+category:           System, Environment
+extra-source-files:
+  CHANGELOG.md
+  README.md
+
+source-repository head
+  type:     git
+  location: https://github.com/tbidne/env-guard
+
+library
+  exposed-modules:
+    System.Environment.Guard
+    System.Environment.Guard.Lifted
+
+  build-depends:    base >=4.14.0.0 && <4.17
+  hs-source-dirs:   src
+  default-language: Haskell2010
+
+test-suite doctest
+  type:             exitcode-stdio-1.0
+  main-is:          Main.hs
+  build-depends:
+    , base
+    , doctest  >=0.16.3 && <0.21
+
+  hs-source-dirs:   test/doctest
+  default-language: Haskell2010
diff --git a/src/System/Environment/Guard.hs b/src/System/Environment/Guard.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Environment/Guard.hs
@@ -0,0 +1,126 @@
+{-# LANGUAGE LambdaCase #-}
+
+-- | Functions for conditionally running 'IO' actions based on an environment
+-- variable.
+--
+-- @since 0.1
+module System.Environment.Guard
+  ( -- * Checking environment variable is set
+    guardSet,
+    guardSet_,
+
+    -- * Checking environment variable match
+    guardExpected,
+    guardExpected_,
+
+    -- * Checking environment variable predicate
+    guardPredicate,
+    guardPredicate_,
+  )
+where
+
+import Control.Monad (void)
+import Data.Char (toLower)
+import System.Environment (lookupEnv)
+
+-- $setup
+-- >>> import Data.Functor (($>))
+-- >>> import System.Environment (setEnv)
+
+-- | @'guardSet' var io@ runs @io@ iff
+--
+-- 1. The environment variable @var@ is set.
+--
+-- @
+-- 'guardSet' var === 'guardPredicate' var ('const' 'True')
+-- @
+--
+-- ==== __Examples__
+--
+-- >>> guardSet "NOT_SET" (putStrLn "ran io" $> True)
+-- Nothing
+--
+-- >>> setEnv "SET" "foo"
+-- >>> guardSet "SET" (putStrLn "ran io" $> True)
+-- ran io
+-- Just True
+--
+-- @since 0.1
+guardSet :: String -> IO a -> IO (Maybe a)
+guardSet var = guardPredicate var (const True)
+
+-- | Variant of 'guardSet' that ignores the return value.
+--
+-- @since 0.1
+guardSet_ :: String -> IO a -> IO ()
+guardSet_ var = void . guardSet var
+
+-- | @'guardExpected' var expected io@ runs @io@ iff
+--
+-- 1. The environment variable @var@ is set.
+-- 2. @var@'s value equals @expected@. This is __case-insensitive__.
+--
+-- @
+-- 'guardExpected' var expected === 'guardPredicate' var (\\a b -> 'fmap' 'toLower' a == 'fmap' 'toLower' b)
+-- @
+--
+-- ==== __Examples__
+--
+-- >>> guardExpected "NOT_SET" "val" (putStrLn "ran io" $> True)
+-- Nothing
+--
+-- >>> setEnv "WRONG_VAL" "good_val"
+-- >>> guardExpected "WRONG_VAL" "bad_val" (putStrLn "ran io" $> True)
+-- Nothing
+--
+-- >>> setEnv "WILL_RUN" "val"
+-- >>> guardExpected "WILL_RUN" "VAL" (putStrLn "ran io" $> True)
+-- ran io
+-- Just True
+--
+-- @since 0.1
+guardExpected :: String -> String -> IO a -> IO (Maybe a)
+guardExpected var expected = guardPredicate var (eqCaseInsensitive expected)
+
+-- | Variant of 'guardExpected_' that ignores the return value.
+--
+-- @since 0.1
+guardExpected_ :: String -> String -> IO a -> IO ()
+guardExpected_ var expected = void . guardExpected var expected
+
+-- | Variant of 'guardPredicate' that ignores the return value.
+--
+-- @since 0.1
+guardPredicate_ :: String -> (String -> Bool) -> IO a -> IO ()
+guardPredicate_ var p = void . guardPredicate var p
+
+-- | This is the most general way to check an environment variable.
+-- @'guardPredicate' var p io@ runs @io@ iff
+--
+-- 1. The environment variable @var@ is set.
+-- 2. @var@'s value satisfies predicate @p@.
+--
+-- ==== __Examples__
+--
+-- >>> guardPredicate "NOT_SET" (const True) (putStrLn "ran io" $> True)
+-- Nothing
+--
+-- >>> setEnv "CASE_WRONG" "VAL"
+-- >>> guardPredicate "CASE_WRONG" (== "val") (putStrLn "ran io" $> True)
+-- Nothing
+--
+-- >>> setEnv "WILL_RUN" "VAL"
+-- >>> guardPredicate "WILL_RUN" (== "VAL") (putStrLn "ran io" $> True)
+-- ran io
+-- Just True
+--
+-- @since 0.1
+guardPredicate :: String -> (String -> Bool) -> IO a -> IO (Maybe a)
+guardPredicate var p io =
+  lookupEnv var
+    >>= \case
+      Just result | p result -> Just <$> io
+      _ -> pure Nothing
+
+eqCaseInsensitive :: String -> String -> Bool
+eqCaseInsensitive a b = fmap toLower a == fmap toLower b
diff --git a/src/System/Environment/Guard/Lifted.hs b/src/System/Environment/Guard/Lifted.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Environment/Guard/Lifted.hs
@@ -0,0 +1,126 @@
+{-# LANGUAGE LambdaCase #-}
+
+-- | "System.Environment.Guard" for 'MonadIO'.
+--
+-- @since 0.1
+module System.Environment.Guard.Lifted
+  ( -- * Checking environment variable is set
+    guardSet,
+    guardSet_,
+
+    -- * Checking environment variable match
+    guardExpected,
+    guardExpected_,
+
+    -- * Checking environment variable predicate
+    guardPredicate,
+    guardPredicate_,
+  )
+where
+
+import Control.Monad (void)
+import Control.Monad.IO.Class (MonadIO (..))
+import Data.Char (toLower)
+import System.Environment (lookupEnv)
+
+-- $setup
+-- >>> import Data.Functor (($>))
+-- >>> import System.Environment (setEnv)
+
+-- | @'guardSet' var io@ runs @io@ iff
+--
+-- 1. The environment variable @var@ is set.
+--
+-- @
+-- 'guardSet' var === 'guardPredicate' var ('const' 'True')
+-- @
+--
+-- ==== __Examples__
+--
+-- >>> guardSet "NOT_SET" (putStrLn "ran io" $> True)
+-- Nothing
+--
+-- >>> setEnv "SET" "foo"
+-- >>> guardSet "SET" (putStrLn "ran io" $> True)
+-- ran io
+-- Just True
+--
+-- @since 0.1
+guardSet :: MonadIO m => String -> m a -> m (Maybe a)
+guardSet var = guardPredicate var (const True)
+
+-- | Variant of 'guardSet' that ignores the return value.
+--
+-- @since 0.1
+guardSet_ :: MonadIO m => String -> m a -> m ()
+guardSet_ var = void . guardSet var
+
+-- | @'guardExpected' var expected io@ runs @io@ iff
+--
+-- 1. The environment variable @var@ is set.
+-- 2. @var@'s value equals @expected@. This is __case-insensitive__.
+--
+-- @
+-- 'guardExpected' var expected === 'guardPredicate' var (\\a b -> 'fmap' 'toLower' a == 'fmap' 'toLower' b)
+-- @
+--
+-- ==== __Examples__
+--
+-- >>> guardExpected "NOT_SET" "val" (putStrLn "ran io" $> True)
+-- Nothing
+--
+-- >>> setEnv "WRONG_VAL" "good_val"
+-- >>> guardExpected "WRONG_VAL" "bad_val" (putStrLn "ran io" $> True)
+-- Nothing
+--
+-- >>> setEnv "WILL_RUN" "val"
+-- >>> guardExpected "WILL_RUN" "VAL" (putStrLn "ran io" $> True)
+-- ran io
+-- Just True
+--
+-- @since 0.1
+guardExpected :: MonadIO m => String -> String -> m a -> m (Maybe a)
+guardExpected var expected = guardPredicate var (eqCaseInsensitive expected)
+
+-- | Variant of 'guardExpected_' that ignores the return value.
+--
+-- @since 0.1
+guardExpected_ :: MonadIO m => String -> String -> m a -> m ()
+guardExpected_ var expected = void . guardExpected var expected
+
+-- | Variant of 'guardPredicate' that ignores the return value.
+--
+-- @since 0.1
+guardPredicate_ :: MonadIO m => String -> (String -> Bool) -> m a -> m ()
+guardPredicate_ var p = void . guardPredicate var p
+
+-- | This is the most general way to check an environment variable.
+-- @'guardPredicate' var p io@ runs @io@ iff
+--
+-- 1. The environment variable @var@ is set.
+-- 2. @var@'s value satisfies predicate @p@.
+--
+-- ==== __Examples__
+--
+-- >>> guardPredicate "NOT_SET" (const True) (putStrLn "ran io" $> True)
+-- Nothing
+--
+-- >>> setEnv "CASE_WRONG" "VAL"
+-- >>> guardPredicate "CASE_WRONG" (== "val") (putStrLn "ran io" $> True)
+-- Nothing
+--
+-- >>> setEnv "WILL_RUN" "VAL"
+-- >>> guardPredicate "WILL_RUN" (== "VAL") (putStrLn "ran io" $> True)
+-- ran io
+-- Just True
+--
+-- @since 0.1
+guardPredicate :: MonadIO m => String -> (String -> Bool) -> m a -> m (Maybe a)
+guardPredicate var p io =
+  liftIO (lookupEnv var)
+    >>= \case
+      Just result | p result -> Just <$> io
+      _ -> pure Nothing
+
+eqCaseInsensitive :: String -> String -> Bool
+eqCaseInsensitive a b = fmap toLower a == fmap toLower b
diff --git a/test/doctest/Main.hs b/test/doctest/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/doctest/Main.hs
@@ -0,0 +1,21 @@
+module Main (main) where
+
+import System.Environment (lookupEnv)
+import Test.DocTest (doctest)
+import Prelude
+
+main :: IO ()
+main = do
+  shouldRun <- lookupEnv "RUN_DOCTEST"
+  case shouldRun of
+    Just _ -> doctest args
+    _ -> putStrLn "*** Doctests Disabled ***"
+  where
+    args = files
+
+files :: [String]
+files =
+  [ "-isrc",
+    "src/System/Environment/Guard.hs",
+    "src/System/Environment/Guard/Lifted.hs"
+  ]
