diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## [2.9.0](https://github.com/haskell-github-trust/effectful-cli/compare/2.8.0...2.9.0) (2026-11-09)
+
+### Features
+
+* add config help command ([8776329](https://github.com/haskell-github-trust/effectful-cli/commit/87763296d29a30691671ccf0ed938312843a2302))
+
 ## [2.8.0](https://github.com/haskell-github-trust/effectful-cli/compare/2.7.0...2.8.0) (2026-11-09)
 
 ### Features
diff --git a/effectful-cli.cabal b/effectful-cli.cabal
--- a/effectful-cli.cabal
+++ b/effectful-cli.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           effectful-cli
-version:        0.2.8.0
+version:        0.2.9.0
 synopsis:       CLI helpers for the effectful ecosystem
 description:    Please see the README at https://github.com/haskell-github-trust/effectful-cli
 category:       CLI
diff --git a/src/Data/CLI/Config.hs b/src/Data/CLI/Config.hs
--- a/src/Data/CLI/Config.hs
+++ b/src/Data/CLI/Config.hs
@@ -42,6 +42,10 @@
   configKeys :: [Text]
   configKeys = []
 
+  -- | Description of each dotted key, printed by 'ConfigHelp'.
+  configDocs :: [(Text, Text)]
+  configDocs = (,"") <$> configKeys @c
+
 -- | Config file commands for config @c@ and application error @e@. IO failures
 -- are raised as 'InputError'.
 --
@@ -53,20 +57,24 @@
     ConfigGet Text
   | -- | Set a dotted key to a TOML literal, or a string if not one.
     ConfigSet Text Text
+  | -- | Describe every known key.
+    ConfigHelp
   deriving (Eq, Show)
 
 instance Bounded (ConfigCommand c e) where
   minBound = ConfigShow
-  maxBound = ConfigSet mempty mempty
+  maxBound = ConfigHelp
 
 instance Enum (ConfigCommand c e) where
   fromEnum ConfigShow = 0
   fromEnum (ConfigGet _) = 1
   fromEnum (ConfigSet _ _) = 2
+  fromEnum ConfigHelp = 3
 
   toEnum 0 = minBound
   toEnum 1 = ConfigGet mempty
-  toEnum 2 = maxBound
+  toEnum 2 = ConfigSet mempty mempty
+  toEnum 3 = maxBound
   toEnum _ = unboundEnumError "ConfigCommand"
 
   enumFrom = boundedEnumFrom
@@ -76,14 +84,18 @@
   cmdname ConfigShow = "show"
   cmdname (ConfigGet _) = "get"
   cmdname (ConfigSet _ _) = "set"
+  cmdname ConfigHelp = "help"
 
   doc ConfigShow = "Print the effective config (file merged over defaults)"
   doc (ConfigGet _) = "Print a dotted key's value (e.g. section.key)"
   doc (ConfigSet _ _) = "Set a dotted key to a TOML literal, or a string if not one"
+  doc ConfigHelp = "Describe every config key"
 
   parser ConfigShow = noArgs ConfigShow
   parser (ConfigGet _) = ConfigGet <$> keyArg (configKeys @c)
-  parser (ConfigSet _ _) = ConfigSet <$> keyArg (configKeys @c) <*> strArgument (metavar "VALUE")
+  parser (ConfigSet _ _) =
+    ConfigSet <$> keyArg (configKeys @c) <*> strArgument (metavar "VALUE")
+  parser ConfigHelp = noArgs ConfigHelp
 
   runner cmd = do
     path <- asks @c configFilePath
@@ -91,6 +103,15 @@
       ConfigShow -> printOutputLn =<< showConfig @(ConfigOf c) path
       ConfigGet k -> printOutputLn =<< getConfigKey path k
       ConfigSet k v -> setConfigKey @(ConfigOf c) (configKeys @c) path k v
+      ConfigHelp -> printOutputLn (helpText (configDocs @c))
+
+helpText :: [(Text, Text)] -> Text
+helpText docs =
+  T.intercalate
+    "\n"
+    [T.justifyLeft w ' ' k <> "  " <> d | (k, d) <- docs]
+ where
+  w = maximum (0 : fmap (T.length . fst) docs)
 
 keyArg
   :: [Text]
