diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+## [_Unreleased_](https://github.com/pbrisbin/ronn/compare/ronn-opt-env-conf-v1.0.0.0...main)
+
+## [v1.0.0.0](https://github.com/pbrisbin/ronn/tree/ronn-opt-env-conf-v1.0.0.0)
+
+First tagged pre-release.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
diff --git a/ronn-opt-env-conf.cabal b/ronn-opt-env-conf.cabal
new file mode 100644
--- /dev/null
+++ b/ronn-opt-env-conf.cabal
@@ -0,0 +1,88 @@
+cabal-version:   1.18
+name:            ronn-opt-env-conf
+version:         1.0.0.0
+license:         AGPL-3
+maintainer:      Pat Brisbin
+homepage:        https://github.com/pbrisbin/ronn#readme
+bug-reports:     https://github.com/pbrisbin/ronn/issues
+synopsis:        Produce Ronn from OptEnvConf
+description:     Please see README.md
+category:        Documentation
+build-type:      Simple
+extra-doc-files:
+    README.md
+    CHANGELOG.md
+
+source-repository head
+    type:     git
+    location: https://github.com/pbrisbin/ronn
+
+library
+    exposed-modules:
+        Ronn.OptEnvConf
+        Ronn.OptEnvConf.Env
+        Ronn.OptEnvConf.Opt
+
+    hs-source-dirs:     src
+    other-modules:      Paths_ronn_opt_env_conf
+    default-language:   GHC2021
+    default-extensions:
+        DataKinds DeriveAnyClass DerivingStrategies DerivingVia
+        DuplicateRecordFields GADTs LambdaCase NoFieldSelectors
+        NoImplicitPrelude NoMonomorphismRestriction OverloadedRecordDot
+        OverloadedStrings RecordWildCards TypeFamilies
+
+    ghc-options:
+        -fignore-optim-changes -fwrite-ide-info -Weverything
+        -Wno-all-missed-specialisations -Wno-missing-exported-signatures
+        -Wno-missing-import-lists -Wno-missing-kind-signatures
+        -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode
+        -Wno-monomorphism-restriction -Wno-prepositive-qualified-module
+        -Wno-safe -Wno-unsafe
+
+    build-depends:
+        base >=4.16.4.0 && <5,
+        opt-env-conf,
+        ronn >=1.1.0.0,
+        text >=1.2.5.0
+
+    if impl(ghc >=9.8)
+        ghc-options:
+            -Wno-missing-role-annotations -Wno-missing-poly-kind-signatures
+
+test-suite spec
+    type:               exitcode-stdio-1.0
+    main-is:            Main.hs
+    hs-source-dirs:     tests
+    other-modules:
+        Ronn.OptEnvConfSpec
+        Paths_ronn_opt_env_conf
+
+    default-language:   GHC2021
+    default-extensions:
+        DataKinds DeriveAnyClass DerivingStrategies DerivingVia
+        DuplicateRecordFields GADTs LambdaCase NoFieldSelectors
+        NoImplicitPrelude NoMonomorphismRestriction OverloadedRecordDot
+        OverloadedStrings RecordWildCards TypeFamilies
+
+    ghc-options:
+        -fignore-optim-changes -fwrite-ide-info -Weverything
+        -Wno-all-missed-specialisations -Wno-missing-exported-signatures
+        -Wno-missing-import-lists -Wno-missing-kind-signatures
+        -Wno-missing-local-signatures -Wno-missing-safe-haskell-mode
+        -Wno-monomorphism-restriction -Wno-prepositive-qualified-module
+        -Wno-safe -Wno-unsafe -threaded -rtsopts -with-rtsopts=-N
+
+    build-depends:
+        base >=4.16.4.0 && <5,
+        filepath >=1.4.2.2,
+        hspec >=2.9.7,
+        hspec-golden >=0.2.1.0,
+        opt-env-conf,
+        ronn >=1.1.0.0,
+        ronn-opt-env-conf,
+        text >=1.2.5.0
+
+    if impl(ghc >=9.8)
+        ghc-options:
+            -Wno-missing-role-annotations -Wno-missing-poly-kind-signatures
diff --git a/src/Ronn/OptEnvConf.hs b/src/Ronn/OptEnvConf.hs
new file mode 100644
--- /dev/null
+++ b/src/Ronn/OptEnvConf.hs
@@ -0,0 +1,14 @@
+-- |
+--
+-- Module      : Ronn.OptEnvConf
+-- Copyright   : (c) 2024 Patrick Brisbin
+-- License     : AGPL-3
+-- Maintainer  : pbrisbin@gmail.com
+-- Stability   : experimental
+-- Portability : POSIX
+module Ronn.OptEnvConf
+  ( module X
+  ) where
+
+import Ronn.OptEnvConf.Env as X
+import Ronn.OptEnvConf.Opt as X
diff --git a/src/Ronn/OptEnvConf/Env.hs b/src/Ronn/OptEnvConf/Env.hs
new file mode 100644
--- /dev/null
+++ b/src/Ronn/OptEnvConf/Env.hs
@@ -0,0 +1,50 @@
+-- |
+--
+-- Module      : Ronn.OptEnvConf.Env
+-- Copyright   : (c) 2024 Patrick Brisbin
+-- License     : AGPL-3
+-- Maintainer  : pbrisbin@gmail.com
+-- Stability   : experimental
+-- Portability : POSIX
+module Ronn.OptEnvConf.Env
+  ( envDefinitions
+  ) where
+
+import Prelude
+
+import Data.Foldable (toList)
+import Data.List (intersperse)
+import Data.String (IsString (..))
+import OptEnvConf (Parser)
+import OptEnvConf.Doc (AnyDocs (..), EnvDoc (..), parserEnvDocs)
+import Ronn.AST
+
+envDefinitions :: Parser a -> [Definition]
+envDefinitions = go . parserEnvDocs
+ where
+  go :: AnyDocs EnvDoc -> [Definition]
+  go = \case
+    AnyDocsCommands {} -> [] -- TODO
+    AnyDocsAnd ds -> concatMap go ds
+    AnyDocsOr ds -> concatMap go ds
+    AnyDocsSingle d -> [envDocDefinition d]
+
+envDocDefinition :: EnvDoc -> Definition
+envDocDefinition doc =
+  Definition
+    { name
+    , description = Line $ maybe [] (pure . fromString) $ envDocHelp doc
+    , content = Nothing
+    }
+ where
+  name =
+    Concat $
+      addArgument $
+        intersperse "|" $
+          map (Code . fromString) $
+            toList $
+              envDocVars doc
+
+  addArgument = case envDocMetavar doc of
+    Nothing -> id
+    Just arg -> (<> ["=", Variable $ fromString arg])
diff --git a/src/Ronn/OptEnvConf/Opt.hs b/src/Ronn/OptEnvConf/Opt.hs
new file mode 100644
--- /dev/null
+++ b/src/Ronn/OptEnvConf/Opt.hs
@@ -0,0 +1,111 @@
+-- |
+--
+-- Module      : Ronn.OptEnvConf.Opt
+-- Copyright   : (c) 2024 Patrick Brisbin
+-- License     : AGPL-3
+-- Maintainer  : pbrisbin@gmail.com
+-- Stability   : experimental
+-- Portability : POSIX
+module Ronn.OptEnvConf.Opt
+  ( optSynopsis
+  , optDefinitions
+  ) where
+
+import Prelude
+
+import Data.Foldable (toList)
+import Data.List (intersperse)
+import Data.String (IsString (..))
+import Data.Text (pack)
+import Data.Text qualified as T
+import OptEnvConf (Parser)
+import OptEnvConf.Args (Dashed (..))
+import OptEnvConf.Doc (AnyDocs (..), OptDoc (..), parserOptDocs)
+import Ronn.AST
+
+optSynopsis :: Parser a -> [Part]
+optSynopsis = go False . parserOptDocs
+ where
+  go :: Bool -> AnyDocs (Maybe OptDoc) -> [Part]
+  go nested = \case
+    AnyDocsCommands {} -> [] -- TODO
+    AnyDocsAnd ds -> concatMap (go True) ds
+    AnyDocsOr [AnyDocsSingle (Just d)] -> [Brackets $ optDocPart d] -- optional d
+    AnyDocsOr ds ->
+      [ (if nested then Parens else id) $
+          Concat $
+            intersperse " \\| " $
+              concatMap (go True) ds
+      ]
+    AnyDocsSingle Nothing -> []
+    AnyDocsSingle (Just d) -> [optDocPart d]
+
+optDocPart :: OptDoc -> Part
+optDocPart doc = bracketize go
+ where
+  go
+    | null (optDocDasheds doc)
+    , Just mv <- optDocMetavar doc =
+        case optDocDefault doc of
+          Nothing -> Variable $ fromString mv
+          Just {} -> Brackets $ fromString mv
+    | otherwise =
+        Concat $
+          intersperse "\\|" $
+            optDashedParts doc
+
+  bracketize = case optDocDefault doc of
+    Nothing -> id
+    Just {} -> Brackets
+
+optDefinitions :: Parser a -> [Definition]
+optDefinitions = go . parserOptDocs
+ where
+  go :: AnyDocs (Maybe OptDoc) -> [Definition]
+  go = \case
+    AnyDocsCommands {} -> [] -- TODO
+    AnyDocsAnd ds -> concatMap go ds
+    AnyDocsOr ds -> concatMap go ds
+    AnyDocsSingle Nothing -> []
+    AnyDocsSingle (Just d) -> [optDocDefinition d]
+
+optDocDefinition :: OptDoc -> Definition
+optDocDefinition doc =
+  Definition
+    { name
+    , description =
+        Line $
+          concat
+            [ maybe [] (pure . fromString) $ optDocHelp doc
+            , maybe [] (\d -> pure $ Parens $ "default " <> fromString d) $ optDocDefault doc
+            ]
+    , content = Nothing
+    }
+ where
+  name
+    | null (optDocDasheds doc)
+    , Just mv <- optDocMetavar doc =
+        Code $ fromString mv
+    | otherwise = Concat $ intersperse ", " $ optDashedParts doc
+
+optDashedParts :: OptDoc -> [Part]
+optDashedParts doc =
+  map (\c -> Concat [Code $ "-" <> Raw (T.singleton c)]) shorts
+    <> map
+      (\n -> Concat $ addArgument (optDocMetavar doc) [Code $ "--" <> Raw (pack n)])
+      longs
+ where
+  (shorts, longs) = partitionDasheds $ optDocDasheds doc
+
+addArgument :: Maybe String -> [Part] -> [Part]
+addArgument = \case
+  Nothing -> id
+  Just arg -> (<> ["=", Variable $ fromString arg])
+
+partitionDasheds :: [Dashed] -> ([Char], [String])
+partitionDasheds = go ([], [])
+ where
+  go acc@(shorts, longs) = \case
+    [] -> acc
+    (DashedShort c : ds) -> go (shorts <> [c], longs) ds
+    (DashedLong cs : ds) -> go (shorts, longs <> [toList cs]) ds
diff --git a/tests/Main.hs b/tests/Main.hs
new file mode 100644
--- /dev/null
+++ b/tests/Main.hs
@@ -0,0 +1,9 @@
+-- |
+--
+-- Module      : Main
+-- Copyright   : (c) 2024 Patrick Brisbin
+-- License     : AGPL-3
+-- Maintainer  : pbrisbin@gmail.com
+-- Stability   : experimental
+-- Portability : POSIX
+{-# OPTIONS_GHC -F -pgmF hspec-discover -Wno-missing-export-lists #-}
diff --git a/tests/Ronn/OptEnvConfSpec.hs b/tests/Ronn/OptEnvConfSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Ronn/OptEnvConfSpec.hs
@@ -0,0 +1,88 @@
+-- |
+--
+-- Module      : Ronn.OptEnvConfSpec
+-- Copyright   : (c) 2024 Patrick Brisbin
+-- License     : AGPL-3
+-- Maintainer  : pbrisbin@gmail.com
+-- Stability   : experimental
+-- Portability : POSIX
+module Ronn.OptEnvConfSpec
+  ( spec
+  ) where
+
+import Prelude
+
+import Data.Text (Text, unpack)
+import Data.Text.IO qualified as T
+import OptEnvConf
+import Ronn
+import Ronn.OptEnvConf
+import System.FilePath ((</>))
+import Test.Hspec
+import Test.Hspec.Golden
+
+spec :: Spec
+spec = do
+  specify "complete example" $
+    let
+      p :: Parser (Maybe Bool, FilePath, FilePath)
+      p =
+        (,,)
+          <$> optional
+            ( setting
+                [ help "Enable debug"
+                , long "debug"
+                , switch True
+                , env "DEBUG"
+                , env "VERBOSE"
+                ]
+            )
+          <*> setting
+            [ help "Output file"
+            , short 'o'
+            , long "output"
+            , reader str
+            , metavar "FILE"
+            , option
+            , env "OUTPUT"
+            , value "-"
+            ]
+          <*> setting
+            [ help "Input file"
+            , reader str
+            , metavar "FILE"
+            , argument
+            , env "INPUT"
+            ]
+    in
+      ronnGolden $
+        Ronn
+          { name = ManRef "ronn-opt-env-conf" ManSection1
+          , description = ["example Ronn from opt-env-conf"]
+          , sections =
+              [ Section
+                  { name = "SYNOPSIS"
+                  , content = [Groups [Lines [Line $ Code "ronn-opt-env-conf" : optSynopsis p]]]
+                  }
+              , Section
+                  { name = "OPTIONS"
+                  , content = [Definitions $ optDefinitions p]
+                  }
+              , Section
+                  { name = "ENVIRONMENT"
+                  , content = [Definitions $ envDefinitions p]
+                  }
+              ]
+          }
+
+ronnGolden :: Ronn -> Golden Text
+ronnGolden ronn =
+  Golden
+    { output = ronnToText ronn
+    , encodePretty = unpack
+    , writeToFile = T.writeFile
+    , readFromFile = T.readFile
+    , goldenFile = "../doc" </> ronnFilePath ronn
+    , actualFile = Nothing
+    , failFirstTime = False
+    }
