diff --git a/shift.cabal b/shift.cabal
index 990a59770a0392c0814948a6a9a9baabdc86baae..dc3f7197e576e29bd571384fb2f24a82fecda437 100644
--- a/shift.cabal
+++ b/shift.cabal
@@ -17,6 +17,7 @@ library
   hs-source-dirs:      src
   exposed-modules:     Shift
                      , Shift.CLI
+                     , Shift.Generate
                      , Shift.Git
                      , Shift.Parsers
                      , Shift.Processing
diff --git a/src/Shift.hs b/src/Shift.hs
index 3abf5a72b5778ac55855890360ad0b62d68c0a97..8251db45e6dcc05457dc174523178b9e1a496744 100644
--- a/src/Shift.hs
+++ b/src/Shift.hs
@@ -42,7 +42,6 @@ import Shift.Git as X
     refsPairsFromRefs,
     renderDiff,
     renderToNode,
-    tempMain,
   )
 import Shift.Rendering as X
   ( bold,
@@ -115,6 +114,7 @@ import Shift.Types as X
     lookupUserOnGitHub,
     lookupUserOnGitHubCommit,
   )
+import Shift.Generate (runGenerate)
 
 -- | The main CLI entrypoint.
 shiftMain :: IO ()
@@ -122,7 +122,7 @@ shiftMain = do
   currentOptions <- execParser opts
 
   case currentOptions ^. soCommand of
-    GenerateCommand -> tempMain currentOptions
+    GenerateCommand {_gcOutputPath, ..} -> runGenerate _gcOutputPath currentOptions
     ServeCommand {_scPort, ..} -> runServer _scPort currentOptions
   where
     opts =
diff --git a/src/Shift/CLI.hs b/src/Shift/CLI.hs
index 0fe8b16e68789bad559136eada5c88c0f206886b..50724c5fffd153d5748f0490152d294ca7096a0e 100644
--- a/src/Shift/CLI.hs
+++ b/src/Shift/CLI.hs
@@ -36,7 +36,10 @@ data ShiftOptions = ShiftOptions
   }
   deriving (Show, Eq)
 
-data ShiftCommand = GenerateCommand | ServeCommand {_scPort :: Maybe Int} deriving (Show, Eq)
+data ShiftCommand
+  = GenerateCommand {_gcOutputPath :: Maybe FilePath}
+  | ServeCommand {_scPort :: Maybe Int}
+  deriving (Show, Eq)
 
 data HostingType = GitHubType | GitType deriving (Show, Eq, Enum)
 
@@ -97,7 +100,15 @@ shiftCommand =
     command
       "generate"
       ( info
-          (pure GenerateCommand)
+          ( GenerateCommand
+              <$> optional
+                ( strOption
+                    ( long "output-path" <> short 'o'
+                        <> metavar "PATH"
+                        <> help "File to write the report to."
+                    )
+                )
+          )
           (progDesc "Generate changelog")
       )
       <> command
diff --git a/src/Shift/Generate.hs b/src/Shift/Generate.hs
new file mode 100644
index 0000000000000000000000000000000000000000..e433cfc06947f74554a3369f77ffc4d4c38f7cfd
--- /dev/null
+++ b/src/Shift/Generate.hs
@@ -0,0 +1,17 @@
+module Shift.Generate where
+
+import CMarkGFM (nodeToCommonmark)
+import Control.Monad.IO.Class (MonadIO (liftIO))
+import qualified Data.Text.IO as TIO
+import Shift.CLI (ShiftOptions (ShiftOptions))
+import Shift.Git (renderToNode)
+
+runGenerate :: Maybe FilePath -> ShiftOptions -> IO ()
+runGenerate maybeOutputPath opts = do
+  node <- renderToNode opts
+
+  let output = nodeToCommonmark [] Nothing node
+
+  case maybeOutputPath of
+    Just outputPath -> liftIO $ TIO.writeFile outputPath output
+    Nothing -> liftIO $ TIO.putStr $ output
\ No newline at end of file
diff --git a/src/Shift/Git.hs b/src/Shift/Git.hs
index fdcf7504a221c2cb454966d7f080892664c531b5..2cbd9582a98f5c2eeb7a876536b6d5c79fd46c4d 100644
--- a/src/Shift/Git.hs
+++ b/src/Shift/Git.hs
@@ -55,12 +55,6 @@ parseTag ref = case versioning . cs . refNameRaw $ ref of
   Left e -> Left e
   Right v -> Right (TagRef ref (Just v))
 
-tempMain :: ShiftOptions -> IO ()
-tempMain opts = do
-  node <- renderToNode opts
-
-  liftIO $ TIO.putStr $ nodeToCommonmark [] Nothing node
-
 renderToNode :: ShiftOptions -> IO Node
 renderToNode opts = withRepo ".git" $ \repo -> do
   pairedTags <- pairedTagsFromOpts opts repo