diff --git a/CHANGES.markdown b/CHANGES.markdown
--- a/CHANGES.markdown
+++ b/CHANGES.markdown
@@ -1,3 +1,6 @@
+Changes in 0.21.1
+  - GHC 9.6 compatibility.
+
 Changes in 0.21.0
   - Accept `--fast`, `--preserve-it` and `--verbose` via `--repl-options`
 
diff --git a/doctest.cabal b/doctest.cabal
--- a/doctest.cabal
+++ b/doctest.cabal
@@ -1,11 +1,11 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.35.1.
+-- This file has been generated from package.yaml by hpack version 0.35.2.
 --
 -- see: https://github.com/sol/hpack
 
 name:           doctest
-version:        0.21.0
+version:        0.21.1
 synopsis:       Test interactive Haskell examples
 description:    `doctest` is a tool that checks [examples](https://www.haskell.org/haddock/doc/html/ch03s08.html#idm140354810775744)
                 and [properties](https://www.haskell.org/haddock/doc/html/ch03s08.html#idm140354810771856)
@@ -111,7 +111,6 @@
   ghc-options: -Wall
   hs-source-dirs:
       src
-      ghci-wrapper/src
   exposed-modules:
       Test.DocTest
   other-modules:
@@ -119,6 +118,7 @@
       GhcUtil
       Info
       Interpreter
+      Language.Haskell.GhciWrapper
       Location
       Options
       PackageDBs
@@ -128,7 +128,6 @@
       Runner
       Runner.Example
       Util
-      Language.Haskell.GhciWrapper
       Paths_doctest
   build-depends:
       base >=4.5 && <5
@@ -138,7 +137,7 @@
     , directory
     , exceptions
     , filepath
-    , ghc >=8.0 && <9.5
+    , ghc >=8.0 && <9.7
     , ghc-paths >=0.1.0.9
     , process
     , syb >=0.3
@@ -161,7 +160,7 @@
     , doctest
     , exceptions
     , filepath
-    , ghc >=8.0 && <9.5
+    , ghc >=8.0 && <9.7
     , ghc-paths >=0.1.0.9
     , process
     , syb >=0.3
@@ -174,6 +173,7 @@
       ExtractSpec
       InfoSpec
       InterpreterSpec
+      Language.Haskell.GhciWrapperSpec
       LocationSpec
       MainSpec
       OptionsSpec
@@ -189,6 +189,7 @@
       GhcUtil
       Info
       Interpreter
+      Language.Haskell.GhciWrapper
       Location
       Options
       PackageDBs
@@ -199,7 +200,6 @@
       Runner.Example
       Test.DocTest
       Util
-      Language.Haskell.GhciWrapper
       Paths_doctest
   type: exitcode-stdio-1.0
   ghc-options: -Wall -threaded
@@ -207,7 +207,6 @@
   hs-source-dirs:
       test
       src
-      ghci-wrapper/src
   c-sources:
       test/integration/with-cbits/foo.c
   build-tool-depends:
@@ -222,7 +221,7 @@
     , directory
     , exceptions
     , filepath
-    , ghc >=8.0 && <9.5
+    , ghc >=8.0 && <9.7
     , ghc-paths >=0.1.0.9
     , hspec >=2.3.0
     , hspec-core >=2.3.0
diff --git a/ghci-wrapper/src/Language/Haskell/GhciWrapper.hs b/ghci-wrapper/src/Language/Haskell/GhciWrapper.hs
deleted file mode 100644
--- a/ghci-wrapper/src/Language/Haskell/GhciWrapper.hs
+++ /dev/null
@@ -1,159 +0,0 @@
-{-# LANGUAGE RecordWildCards #-}
-module Language.Haskell.GhciWrapper (
-  Interpreter
-, Config(..)
-, defaultConfig
-, new
-, close
-, eval
-, evalIt
-, evalEcho
-) where
-
-import           System.IO hiding (stdin, stdout, stderr)
-import           System.Process
-import           System.Exit
-import           Control.Monad
-import           Control.Exception
-import           Data.List (isSuffixOf)
-import           Data.Maybe
-
-data Config = Config {
-  configGhci :: String
-, configVerbose :: Bool
-, configIgnoreDotGhci :: Bool
-} deriving (Eq, Show)
-
-defaultConfig :: Config
-defaultConfig = Config {
-  configGhci = "ghci"
-, configVerbose = False
-, configIgnoreDotGhci = True
-}
-
--- | Truly random marker, used to separate expressions.
---
--- IMPORTANT: This module relies upon the fact that this marker is unique.  It
--- has been obtained from random.org.  Do not expect this module to work
--- properly, if you reuse it for any purpose!
-marker :: String
-marker = show "dcbd2a1e20ae519a1c7714df2859f1890581d57fac96ba3f499412b2f5c928a1"
-
-itMarker :: String
-itMarker = "d42472243a0e6fc481e7514cbc9eb08812ed48daa29ca815844d86010b1d113a"
-
-data Interpreter = Interpreter {
-  hIn  :: Handle
-, hOut :: Handle
-, process :: ProcessHandle
-}
-
-new :: Config -> [String] -> IO Interpreter
-new Config{..} args_ = do
-  (Just stdin_, Just stdout_, Nothing, processHandle ) <- createProcess (proc configGhci args) {
-    std_in  = CreatePipe
-  , std_out = CreatePipe
-  , std_err = Inherit
-  }
-  setMode stdin_
-  setMode stdout_
-  let interpreter = Interpreter {hIn = stdin_, hOut = stdout_, process = processHandle}
-  evalThrow interpreter "import qualified System.IO"
-  evalThrow interpreter "import qualified GHC.IO.Encoding"
-  evalThrow interpreter "import qualified GHC.IO.Handle"
-  -- The buffering of stdout and stderr is NoBuffering
-  evalThrow interpreter "GHC.IO.Handle.hDuplicateTo System.IO.stdout System.IO.stderr"
-  -- Now the buffering of stderr is BlockBuffering Nothing
-  -- In this situation, GHC 7.7 does not flush the buffer even when
-  -- error happens.
-  evalThrow interpreter "GHC.IO.Handle.hSetBuffering System.IO.stdout GHC.IO.Handle.LineBuffering"
-  evalThrow interpreter "GHC.IO.Handle.hSetBuffering System.IO.stderr GHC.IO.Handle.LineBuffering"
-
-  -- this is required on systems that don't use utf8 as default encoding (e.g.
-  -- Windows)
-  evalThrow interpreter "GHC.IO.Handle.hSetEncoding System.IO.stdout GHC.IO.Encoding.utf8"
-  evalThrow interpreter "GHC.IO.Handle.hSetEncoding System.IO.stderr GHC.IO.Encoding.utf8"
-
-  evalThrow interpreter ":m - System.IO"
-  evalThrow interpreter ":m - GHC.IO.Encoding"
-  evalThrow interpreter ":m - GHC.IO.Handle"
-
-  return interpreter
-  where
-    args = args_ ++ catMaybes [
-        if configIgnoreDotGhci then Just "-ignore-dot-ghci" else Nothing
-      , if configVerbose then Nothing else Just "-v0"
-      ]
-    setMode h = do
-      hSetBinaryMode h False
-      hSetBuffering h LineBuffering
-      hSetEncoding h utf8
-
-    evalThrow :: Interpreter -> String -> IO ()
-    evalThrow interpreter expr = do
-      output <- eval interpreter expr
-      unless (null output || configVerbose) $ do
-        close interpreter
-        throwIO (ErrorCall output)
-
-close :: Interpreter -> IO ()
-close repl = do
-  hClose $ hIn repl
-
-  -- It is crucial not to close `hOut` before calling `waitForProcess`,
-  -- otherwise ghci may not cleanly terminate on SIGINT (ctrl-c) and hang
-  -- around consuming 100% CPU.  This happens when ghci tries to print
-  -- something to stdout in its signal handler (e.g. when it is blocked in
-  -- threadDelay it writes "Interrupted." on SIGINT).
-  e <- waitForProcess $ process repl
-  hClose $ hOut repl
-
-  when (e /= ExitSuccess) $ do
-    throwIO (userError $ "Language.Haskell.GhciWrapper.close: Interpreter exited with an error (" ++ show e ++ ")")
-
-putExpression :: Interpreter -> Bool -> String -> IO ()
-putExpression Interpreter{hIn = stdin} preserveIt e = do
-  hPutStrLn stdin e
-  when preserveIt $ hPutStrLn stdin $ "let " ++ itMarker ++ " = it"
-  hPutStrLn stdin (marker ++ " :: Data.String.String")
-  when preserveIt $ hPutStrLn stdin $ "let it = " ++ itMarker
-  hFlush stdin
-
-getResult :: Bool -> Interpreter -> IO String
-getResult echoMode Interpreter{hOut = stdout} = go
-  where
-    go = do
-      line <- hGetLine stdout
-      if marker `isSuffixOf` line
-        then do
-          let xs = stripMarker line
-          echo xs
-          return xs
-        else do
-          echo (line ++ "\n")
-          result <- go
-          return (line ++ "\n" ++ result)
-    stripMarker l = take (length l - length marker) l
-
-    echo :: String -> IO ()
-    echo
-      | echoMode = putStr
-      | otherwise = \ _ -> return ()
-
--- | Evaluate an expression
-eval :: Interpreter -> String -> IO String
-eval repl expr = do
-  putExpression repl False expr
-  getResult False repl
-
--- | Like 'eval', but try to preserve the @it@ variable
-evalIt :: Interpreter -> String -> IO String
-evalIt repl expr = do
-  putExpression repl True expr
-  getResult False repl
-
--- | Evaluate an expression
-evalEcho :: Interpreter -> String -> IO String
-evalEcho repl expr = do
-  putExpression repl False expr
-  getResult True repl
diff --git a/src/Extract.hs b/src/Extract.hs
--- a/src/Extract.hs
+++ b/src/Extract.hs
@@ -221,8 +221,10 @@
     -- We process header, exports and declarations separately instead of
     -- traversing the whole source in a generic way, to ensure that we get
     -- everything in source order.
-#if __GLASGOW_HASKELL__ >= 904
-    header  = [(Nothing, hsDocString <$> x) | Just x <- [hsmodHaddockModHeader source]]
+#if __GLASGOW_HASKELL__ >= 906
+    header  = [(Nothing, hsDocString <$> x) | Just x <- [hsmodHaddockModHeader (hsmodExt source)]]
+#elif __GLASGOW_HASKELL__ >= 904
+    header  = [(Nothing, hsDocString <$> x) | Just x <- [hsmodHaddockModHeader (source)]]
 #else
     header  = [(Nothing, x) | Just x <- [hsmodHaddockModHeader source]]
 #endif
diff --git a/src/GhcUtil.hs b/src/GhcUtil.hs
--- a/src/GhcUtil.hs
+++ b/src/GhcUtil.hs
@@ -69,7 +69,9 @@
 
 setHaddockMode :: DynFlags -> DynFlags
 setHaddockMode dynflags = (gopt_set dynflags Opt_Haddock) {
-#if __GLASGOW_HASKELL__ >= 901
+#if __GLASGOW_HASKELL__ >= 906
+      backend   = noBackend
+#elif __GLASGOW_HASKELL__ >= 901
       backend   = NoBackend
 #else
       hscTarget = HscNothing
diff --git a/src/Language/Haskell/GhciWrapper.hs b/src/Language/Haskell/GhciWrapper.hs
new file mode 100644
--- /dev/null
+++ b/src/Language/Haskell/GhciWrapper.hs
@@ -0,0 +1,159 @@
+{-# LANGUAGE RecordWildCards #-}
+module Language.Haskell.GhciWrapper (
+  Interpreter
+, Config(..)
+, defaultConfig
+, new
+, close
+, eval
+, evalIt
+, evalEcho
+) where
+
+import           System.IO hiding (stdin, stdout, stderr)
+import           System.Process
+import           System.Exit
+import           Control.Monad
+import           Control.Exception
+import           Data.List (isSuffixOf)
+import           Data.Maybe
+
+data Config = Config {
+  configGhci :: String
+, configVerbose :: Bool
+, configIgnoreDotGhci :: Bool
+} deriving (Eq, Show)
+
+defaultConfig :: Config
+defaultConfig = Config {
+  configGhci = "ghci"
+, configVerbose = False
+, configIgnoreDotGhci = True
+}
+
+-- | Truly random marker, used to separate expressions.
+--
+-- IMPORTANT: This module relies upon the fact that this marker is unique.  It
+-- has been obtained from random.org.  Do not expect this module to work
+-- properly, if you reuse it for any purpose!
+marker :: String
+marker = show "dcbd2a1e20ae519a1c7714df2859f1890581d57fac96ba3f499412b2f5c928a1"
+
+itMarker :: String
+itMarker = "d42472243a0e6fc481e7514cbc9eb08812ed48daa29ca815844d86010b1d113a"
+
+data Interpreter = Interpreter {
+  hIn  :: Handle
+, hOut :: Handle
+, process :: ProcessHandle
+}
+
+new :: Config -> [String] -> IO Interpreter
+new Config{..} args_ = do
+  (Just stdin_, Just stdout_, Nothing, processHandle ) <- createProcess (proc configGhci args) {
+    std_in  = CreatePipe
+  , std_out = CreatePipe
+  , std_err = Inherit
+  }
+  setMode stdin_
+  setMode stdout_
+  let interpreter = Interpreter {hIn = stdin_, hOut = stdout_, process = processHandle}
+  evalThrow interpreter "import qualified System.IO"
+  evalThrow interpreter "import qualified GHC.IO.Encoding"
+  evalThrow interpreter "import qualified GHC.IO.Handle"
+  -- The buffering of stdout and stderr is NoBuffering
+  evalThrow interpreter "GHC.IO.Handle.hDuplicateTo System.IO.stdout System.IO.stderr"
+  -- Now the buffering of stderr is BlockBuffering Nothing
+  -- In this situation, GHC 7.7 does not flush the buffer even when
+  -- error happens.
+  evalThrow interpreter "GHC.IO.Handle.hSetBuffering System.IO.stdout GHC.IO.Handle.LineBuffering"
+  evalThrow interpreter "GHC.IO.Handle.hSetBuffering System.IO.stderr GHC.IO.Handle.LineBuffering"
+
+  -- this is required on systems that don't use utf8 as default encoding (e.g.
+  -- Windows)
+  evalThrow interpreter "GHC.IO.Handle.hSetEncoding System.IO.stdout GHC.IO.Encoding.utf8"
+  evalThrow interpreter "GHC.IO.Handle.hSetEncoding System.IO.stderr GHC.IO.Encoding.utf8"
+
+  evalThrow interpreter ":m - System.IO"
+  evalThrow interpreter ":m - GHC.IO.Encoding"
+  evalThrow interpreter ":m - GHC.IO.Handle"
+
+  return interpreter
+  where
+    args = args_ ++ catMaybes [
+        if configIgnoreDotGhci then Just "-ignore-dot-ghci" else Nothing
+      , if configVerbose then Nothing else Just "-v0"
+      ]
+    setMode h = do
+      hSetBinaryMode h False
+      hSetBuffering h LineBuffering
+      hSetEncoding h utf8
+
+    evalThrow :: Interpreter -> String -> IO ()
+    evalThrow interpreter expr = do
+      output <- eval interpreter expr
+      unless (null output || configVerbose) $ do
+        close interpreter
+        throwIO (ErrorCall output)
+
+close :: Interpreter -> IO ()
+close repl = do
+  hClose $ hIn repl
+
+  -- It is crucial not to close `hOut` before calling `waitForProcess`,
+  -- otherwise ghci may not cleanly terminate on SIGINT (ctrl-c) and hang
+  -- around consuming 100% CPU.  This happens when ghci tries to print
+  -- something to stdout in its signal handler (e.g. when it is blocked in
+  -- threadDelay it writes "Interrupted." on SIGINT).
+  e <- waitForProcess $ process repl
+  hClose $ hOut repl
+
+  when (e /= ExitSuccess) $ do
+    throwIO (userError $ "Language.Haskell.GhciWrapper.close: Interpreter exited with an error (" ++ show e ++ ")")
+
+putExpression :: Interpreter -> Bool -> String -> IO ()
+putExpression Interpreter{hIn = stdin} preserveIt e = do
+  hPutStrLn stdin e
+  when preserveIt $ hPutStrLn stdin $ "let " ++ itMarker ++ " = it"
+  hPutStrLn stdin (marker ++ " :: Data.String.String")
+  when preserveIt $ hPutStrLn stdin $ "let it = " ++ itMarker
+  hFlush stdin
+
+getResult :: Bool -> Interpreter -> IO String
+getResult echoMode Interpreter{hOut = stdout} = go
+  where
+    go = do
+      line <- hGetLine stdout
+      if marker `isSuffixOf` line
+        then do
+          let xs = stripMarker line
+          echo xs
+          return xs
+        else do
+          echo (line ++ "\n")
+          result <- go
+          return (line ++ "\n" ++ result)
+    stripMarker l = take (length l - length marker) l
+
+    echo :: String -> IO ()
+    echo
+      | echoMode = putStr
+      | otherwise = \ _ -> return ()
+
+-- | Evaluate an expression
+eval :: Interpreter -> String -> IO String
+eval repl expr = do
+  putExpression repl False expr
+  getResult False repl
+
+-- | Like 'eval', but try to preserve the @it@ variable
+evalIt :: Interpreter -> String -> IO String
+evalIt repl expr = do
+  putExpression repl True expr
+  getResult False repl
+
+-- | Evaluate an expression
+evalEcho :: Interpreter -> String -> IO String
+evalEcho repl expr = do
+  putExpression repl False expr
+  getResult True repl
diff --git a/test/Language/Haskell/GhciWrapperSpec.hs b/test/Language/Haskell/GhciWrapperSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Language/Haskell/GhciWrapperSpec.hs
@@ -0,0 +1,114 @@
+{-# LANGUAGE CPP #-}
+module Language.Haskell.GhciWrapperSpec (main, spec) where
+
+import           Test.Hspec
+import           System.IO.Silently
+
+import           Control.Exception
+import           Data.List
+
+import           Language.Haskell.GhciWrapper (Interpreter, Config(..), defaultConfig)
+import qualified Language.Haskell.GhciWrapper as Interpreter
+
+main :: IO ()
+main = hspec spec
+
+withInterpreterConfig :: Config -> [String] -> (Interpreter -> IO a) -> IO a
+withInterpreterConfig config args = bracket (Interpreter.new config args) Interpreter.close
+
+withInterpreterArgs :: [String] -> ((String -> IO String) -> IO a) -> IO a
+withInterpreterArgs args action = withInterpreterConfig defaultConfig args $ action . Interpreter.eval
+
+withInterpreter :: ((String -> IO String) -> IO a) -> IO a
+withInterpreter = withInterpreterArgs []
+
+spec :: Spec
+spec = do
+  describe "evalEcho" $ do
+    it "prints result to stdout" $ do
+      withInterpreterConfig defaultConfig [] $ \ghci -> do
+        (capture $ Interpreter.evalEcho ghci ("putStr" ++ show "foo\nbar")) `shouldReturn` ("foo\nbar", "foo\nbar")
+
+  describe "evalIt" $ do
+    it "preserves it" $ do
+      withInterpreterConfig defaultConfig [] $ \ghci -> do
+        Interpreter.evalIt ghci "23" `shouldReturn` "23\n"
+        Interpreter.eval ghci "it" `shouldReturn` "23\n"
+
+  describe "eval" $ do
+    it "shows literals" $ withInterpreter $ \ghci -> do
+      ghci "23" `shouldReturn` "23\n"
+
+    it "shows string literals containing Unicode" $ withInterpreter $ \ghci -> do
+      ghci "\"λ\"" `shouldReturn` "\"\\955\"\n"
+
+    it "evaluates simple expressions" $ withInterpreter $ \ghci -> do
+      ghci "23 + 42" `shouldReturn` "65\n"
+
+    it "supports let bindings" $ withInterpreter $ \ghci -> do
+      ghci "let x = 10" `shouldReturn` ""
+      ghci "x" `shouldReturn` "10\n"
+
+    it "allows import statements" $ withInterpreter $ \ghci -> do
+      ghci "import Data.Maybe" `shouldReturn` ""
+      ghci "fromJust (Just 20)" `shouldReturn` "20\n"
+
+    it "captures stdout" $ withInterpreter $ \ghci -> do
+      ghci "putStr \"foo\"" `shouldReturn` "foo"
+
+    it "captures stdout (Unicode)" $ withInterpreter $ \ghci -> do
+      ghci "putStrLn \"λ\"" `shouldReturn` "λ\n"
+
+    it "captures stdout (empty line)" $ withInterpreter $ \ghci -> do
+      ghci "putStrLn \"\"" `shouldReturn` "\n"
+
+    it "captures stdout (multiple lines)" $ withInterpreter $ \ghci -> do
+      ghci "putStrLn \"foo\" >> putStrLn \"bar\" >> putStrLn \"baz\""
+        `shouldReturn` "foo\nbar\nbaz\n"
+
+    it "captures stderr" $ withInterpreter $ \ghci -> do
+      ghci "import System.IO" `shouldReturn` ""
+      ghci "hPutStrLn stderr \"foo\"" `shouldReturn` "foo\n"
+
+    it "captures stderr (Unicode)" $ withInterpreter $ \ghci -> do
+      ghci "import System.IO" `shouldReturn` ""
+      ghci "hPutStrLn stderr \"λ\"" `shouldReturn` "λ\n"
+
+    it "shows exceptions" $ withInterpreter $ \ghci -> do
+      ghci "import Control.Exception" `shouldReturn` ""
+      ghci "throwIO DivideByZero" `shouldReturn` "*** Exception: divide by zero\n"
+
+    it "shows exceptions (ExitCode)" $ withInterpreter $ \ghci -> do
+      ghci "import System.Exit" `shouldReturn` ""
+      ghci "exitWith $ ExitFailure 10" `shouldReturn` "*** Exception: ExitFailure 10\n"
+
+    it "gives an error message for identifiers that are not in scope" $ withInterpreter $ \ghci -> do
+#if __GLASGOW_HASKELL__ >= 800
+      ghci "foo" >>= (`shouldSatisfy` isInfixOf "Variable not in scope: foo")
+#elif __GLASGOW_HASKELL__ >= 707
+      ghci "foo" >>= (`shouldSatisfy` isSuffixOf "Not in scope: \8216foo\8217\n")
+#else
+      ghci "foo" >>= (`shouldSatisfy` isSuffixOf "Not in scope: `foo'\n")
+#endif
+    context "when configVerbose is True" $ do
+      it "prints prompt" $ do
+        withInterpreterConfig defaultConfig{configVerbose = True} [] $ \ghci -> do
+          Interpreter.eval ghci "print 23" >>= (`shouldSatisfy`
+            (`elem` [ "Prelude> 23\nPrelude> "
+                    ,  "ghci> 23\nghci> "
+                    ]))
+
+    context "with -XOverloadedStrings, -Wall and -Werror" $ do
+      it "does not fail on marker expression (bug fix)" $ withInterpreter $ \ghci -> do
+        ghci ":set -XOverloadedStrings -Wall -Werror" `shouldReturn` ""
+        ghci "putStrLn \"foo\"" `shouldReturn` "foo\n"
+
+    context "with NoImplicitPrelude" $ do
+      it "works" $ withInterpreterArgs ["-XNoImplicitPrelude"] $ \ghci -> do
+        ghci "putStrLn \"foo\"" >>= (`shouldContain` "Variable not in scope: putStrLn")
+        ghci "23" `shouldReturn` "23\n"
+
+    context "with a strange String type" $ do
+      it "works" $ withInterpreter $ \ghci -> do
+        ghci "type String = Int" `shouldReturn` ""
+        ghci "putStrLn \"foo\"" `shouldReturn` "foo\n"
diff --git a/test/RunSpec.hs b/test/RunSpec.hs
--- a/test/RunSpec.hs
+++ b/test/RunSpec.hs
@@ -128,7 +128,15 @@
 
       it "prints a useful error message" $ do
         (r, _) <- hCapture [stderr] (E.try action :: IO (Either ExitCode Summary))
-        stripAnsiColors (removeLoadedPackageEnvironment r) `shouldBe` "\nFoo.hs:6:1: error:\n    parse error (possibly incorrect indentation or mismatched brackets)\n"
+        stripAnsiColors (removeLoadedPackageEnvironment r) `shouldBe` unlines [
+            ""
+#if __GLASGOW_HASKELL__ >= 906
+          , "Foo.hs:6:1: error: [GHC-58481]"
+#else
+          , "Foo.hs:6:1: error:"
+#endif
+          , "    parse error (possibly incorrect indentation or mismatched brackets)"
+          ]
 
   describe "expandDirs" $ do
     it "expands a directory" $ do
