diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for EVP
+
+## 0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/EVP.cabal b/EVP.cabal
new file mode 100644
--- /dev/null
+++ b/EVP.cabal
@@ -0,0 +1,42 @@
+cabal-version:      3.0
+name:               EVP
+version:            0
+synopsis:           Environment Variable Parser
+description:        See README.md
+homepage:           https://github.com/fumieval/EVP
+license:            BSD-3-Clause
+license-file:       LICENSE
+author:             Fumiaki Kinoshita
+maintainer:         fumiexcel@gmail.com
+copyright:          Copyright (c) 2023 Fumiaki Kinoshita
+category:           System
+build-type:         Simple
+extra-doc-files:    CHANGELOG.md
+extra-source-files:
+    README.md
+
+common warnings
+    ghc-options: -Wall
+
+source-repository head
+  type: git
+  location: https://github.com/fumieval/EVP.git
+
+library
+    import:           warnings
+    exposed-modules:  EVP
+    build-depends:    base >=4.16, containers, data-default-class, text, yaml
+    hs-source-dirs:   src
+    default-language: GHC2021
+
+test-suite test
+  type: exitcode-stdio-1.0
+  main-is: test.hs
+  hs-source-dirs:
+      tests
+  build-depends:
+      base >=4.7 && <5
+    , EVP
+    , text
+  default-language: GHC2021
+  ghc-options: -Wall -Wcompat
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright (c) 2023, Fumiaki Kinoshita
+
+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 Fumiaki Kinoshita 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/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,84 @@
+EVP: Environment Variable Parser
+====
+
+EVP is a simple environment parser which focues on these three aspects:
+
+* Ease of use: no complicated machinery is needed
+* Observability: for each environment variable, EVP logs the parsed value or an error. An error does not interrupt the parsing process and it checks all the variables exhaustively.
+* Composability: environment parsers can be composed via the Applicative structure.
+
+Example
+----
+
+The following code is a complete example demonstrating how to use EVP:
+
+```haskell
+{-# LANGUAGE ApplicativeDo #-}
+
+import EVP qualified
+
+main :: IO ()
+main = EVP.scan parser
+
+-- ApplicativeDo is important here because Scan is not a monad.
+parser :: EVP.Scan ()
+parser = do
+    -- @secret@ masks the parsed value
+    _token <- EVP.secret $ EVP.string "API_TOKEN"
+    -- parse the environment variable as a YAML value
+    _port <- EVP.yaml "HTTP_PORT"
+    -- obtain the environment variable as is
+    _foo <- EVP.string "FOO"
+    -- you can also provide a default value
+    _debug <- EVP.yamlDefault "DEBUG_MODE" False
+    pure ()
+```
+
+Running this code produces the following output.
+
+```
+[EVP Info] API_TOKEN: <REDACTED>
+[EVP Info] HTTP_PORT: 8080
+[EVP Info] FOO: foo
+[EVP Info] DEBUG_MODE: False (default)
+```
+
+Revealing unused variables
+----
+
+EVP has a mechanism to detect unused variables.
+
+If your application's environment variables has a common prefix `MYAPP_`, you can set `assumePrefix` as a `unusedLogger`.
+
+```haskell
+EVP.scanWith EVP.def
+    { EVP.unusedLogger = EVP.assumePrefix "MYAPP_" }
+    parser
+```
+
+If an environment variable prefixed by `MYAPP_` is set but not referred to, EVP prints the following warning. This is useful for detecting typos too.
+
+```
+[EVP Warn] MYAPP_OBSOLETE_FLAG is set but not used
+```
+
+Alternatively, you can also name unwanted variables individually:
+
+```haskell
+EVP.scanWith EVP.def
+    { EVP.unusedLogger = EVP.obsolete ["OBSOLETE_VAR"] }
+```
+
+These can be combined using the `Semigroup` instance.
+
+```haskell
+EVP.scanWith EVP.def
+    { EVP.unusedLogger = EVP.assumePrefix "MYAPP_" <> EVP.obsolete ["OBSOLETE_VAR"] }
+    parser
+```
+
+Design context
+----
+
+* If `Scan` were a monad, the parsing process would be non-deterministic. This might cause a burden when there are two or more problems in the environment variables, because it is not possible to reveal all problems in a non-deterministic context.
+* It is recommended to avoid falling back to default values expect for debugging features. If the application configuration has an error such as a typo, it is much safer to exit than launching anyway with a default value.
diff --git a/src/EVP.hs b/src/EVP.hs
new file mode 100644
--- /dev/null
+++ b/src/EVP.hs
@@ -0,0 +1,164 @@
+{-# LANGUAGE BangPatterns #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE RecordWildCards #-}
+module EVP
+  ( Name
+  , Error(..)
+  -- * Parsers
+  , string
+  , yaml
+  , parse
+  , secret
+  -- * Providing a default value
+  , stringDefault
+  , yamlDefault
+  , parseDefault
+  -- * Runner
+  , Settings(..)
+  , def
+  , scan
+  , scanWith
+  , enumerate
+  -- * Logger
+  , assumePrefix
+  , obsolete
+  -- * Internal
+  , Scan(..)
+  ) where
+
+import Control.Monad
+import Data.Bifunctor
+import Data.Default.Class
+import Data.List (isPrefixOf)
+import Data.Map.Strict qualified as Map
+import Data.Set qualified as Set
+import Data.String
+import Data.Yaml qualified as Yaml
+import Data.Text.Encoding
+import System.Environment
+import System.Exit
+import System.IO
+
+type Name = String
+
+data Error = Missing Name
+  | ParseError Name String
+  deriving Show
+
+-- | Obtain the environment variable.
+string :: (IsString a) => Name -> Scan a
+string v = Var v (\case
+  Nothing -> Left (Missing v)
+  Just x -> Right (x, fromString x)) (Pure id)
+
+stringDefault :: (IsString a) => Name -> String -> Scan a
+stringDefault v d = Var v (\case
+  Nothing -> Right (d <> " (default)", fromString d)
+  Just x -> Right (x, fromString x)) (Pure id)
+
+-- | Parse the environment variable as a YAML value.
+yaml :: (Show a, Yaml.FromJSON a) => Name -> Scan a
+yaml v = parse v decodeYaml
+
+-- | Parse the environment variable as a YAML value.
+yamlDefault :: (Show a, Yaml.FromJSON a) => Name -> a -> Scan a
+yamlDefault v d = parseDefault v d decodeYaml
+
+decodeYaml :: Yaml.FromJSON a => String -> Either String a
+decodeYaml = first show . Yaml.decodeEither' . encodeUtf8 . fromString
+
+parse :: (Show a) => Name -> (String -> Either String a) -> Scan a
+parse v f = Var v (\case
+  Nothing -> Left (Missing v)
+  Just x -> bimap (ParseError v) withShow $ f x) (Pure id)
+
+parseDefault :: (Show a) => Name -> a -> (String -> Either String a) -> Scan a
+parseDefault v d f = Var v (\case
+  Nothing -> Right (show d <> " (default)", d)
+  Just x -> bimap (ParseError v) withShow $ f x) (Pure id)
+
+-- | Disable logging of parsed values.
+secret :: Scan a -> Scan a
+secret (Pure a) = Pure a
+secret (Var v f k) = Var v (fmap (first (const "<REDACTED>")) . f) (secret k)
+
+withShow :: Show a => a -> (String, a)
+withShow x = (show x, x)
+  
+data Scan a where
+  Pure :: a -> Scan a
+  Var :: Name -> (Maybe String -> Either Error (String, a)) -> Scan (a -> b) -> Scan b
+
+instance Functor Scan where
+  fmap f (Pure a) = Pure (f a)
+  fmap f (Var k g c) = Var k g (fmap (f.) c)
+
+instance Applicative Scan where
+  pure = Pure
+  Pure f <*> k = f <$> k
+  Var k f c <*> r = Var k f (flip <$> c <*> r)
+   
+type EnvMap = Map.Map String String
+
+data Settings = Settings
+  { parseLogger :: Name -> String -> IO ()
+  , errorLogger :: Error -> IO ()
+  , unusedLogger :: Name -> Maybe (IO ())
+  , pedantic :: Bool -- ^ exit on warning
+  }
+  
+instance Default Settings where
+  def = Settings
+    { parseLogger = \name value -> putStrLn $ unwords ["[EVP Info]", name <> ":", value]
+    , errorLogger = \e -> hPutStrLn stderr $ unwords ["[EVP Error]", show e]
+    , unusedLogger = mempty
+    , pedantic = False
+    }
+
+-- | Custom logging function for 'unusedLogger'.
+-- @'assumePrefix' p@ prints a warning for each unused environment variable prefixed by @p@.
+assumePrefix :: String -> Name -> Maybe (IO ())
+assumePrefix prefix name
+  | isPrefixOf prefix name = Just $ hPutStrLn stderr $ unwords ["[EVP Warn]", name, "is set but not used"]
+  | otherwise = Nothing
+
+-- | @'obsolete' names@ prints a warning if any of the @names@ is set but not used.
+obsolete :: [Name] -> Name -> Maybe (IO ())
+obsolete nameSet name
+  | elem name nameSet = Just $ hPutStrLn stderr $ unwords ["[EVP Warn]", name, "is obsolete"]
+  | otherwise = Nothing
+
+-- | Enumerate the names of the variables it would parse.
+enumerate :: Scan a -> [Name]
+enumerate m = Set.toList $ go Set.empty m where
+  go :: Set.Set Name -> Scan a -> Set.Set Name
+  go !s (Pure _) = s
+  go !s (Var k _ cont) = go (Set.insert k s) cont
+
+scan :: Scan a -> IO a
+scan = scanWith def
+
+scanWith :: Settings -> Scan a -> IO a
+scanWith Settings{..} action = do
+  envs0 <- Map.fromList <$> getEnvironment
+  (remainder, errors, result) <- go envs0 envs0 action
+  mapM_ errorLogger errors
+  case foldMap unusedLogger $ Map.keys remainder of
+    Nothing -> pure ()
+    Just m -> do
+      m
+      when pedantic exitFailure
+  case result of
+    Nothing -> exitFailure
+    Just a -> pure a
+  where
+    go :: EnvMap -> EnvMap -> Scan a -> IO (EnvMap, [Error], Maybe a)
+    go _ envs (Pure a) = pure (envs, [], Just a)
+    go allEnvs envs (Var name parser cont) = case parser (Map.lookup name allEnvs) of
+      Left e -> do
+        (remainder, errors, _) <- go allEnvs (Map.delete name envs) cont
+        pure (remainder, e : errors, Nothing)
+      Right (display, v) -> do
+        parseLogger name display
+        (remainder, errors, func) <- go allEnvs (Map.delete name envs)  cont
+        pure (remainder, errors, ($ v) <$> func)
diff --git a/tests/test.hs b/tests/test.hs
new file mode 100644
--- /dev/null
+++ b/tests/test.hs
@@ -0,0 +1,30 @@
+{-# LANGUAGE ApplicativeDo #-}
+{-# LANGUAGE GADTs #-}
+
+import Data.Text (Text)
+import EVP qualified
+
+main :: IO ()
+main = EVP.scanWith EVP.def
+    { EVP.unusedLogger = EVP.assumePrefix "MYAPP_" <> EVP.obsolete ["OBSOLETE_VAR"] }
+    parser
+
+-- ApplicativeDo is important here because Scan is not a monad.
+parser :: EVP.Scan ()
+parser = do
+    -- @secret@ disables logging
+    _token :: String <- EVP.secret $ EVP.string "API_TOKEN"
+    -- parse the environment variable as a YAML value
+    _port :: Int <- EVP.yaml "HTTP_PORT"
+    -- obtain the environment variable as is
+    _ :: Text <- EVP.string "FOO"
+    -- you can also provide a default value
+    _ :: Bool <- EVP.yamlDefault "DEBUG_MODE" False
+    pure ()
+
+{-
+[EVP Info] API_TOKEN: <REDACTED>
+[EVP Info] HTTP_PORT: 8080
+[EVP Info] FOO: foo
+[EVP Info] DEBUG_MODE: False (default)
+-}
