diff --git a/doctest/ghci-wrapper/src/Language/Haskell/GhciWrapper.hs b/doctest/ghci-wrapper/src/Language/Haskell/GhciWrapper.hs
deleted file mode 100644
--- a/doctest/ghci-wrapper/src/Language/Haskell/GhciWrapper.hs
+++ /dev/null
@@ -1,146 +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}
-  _ <- eval interpreter "import qualified System.IO"
-  _ <- eval interpreter "import qualified GHC.IO.Handle"
-  -- The buffering of stdout and stderr is NoBuffering
-  _ <- eval 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.
-  _ <- eval interpreter "GHC.IO.Handle.hSetBuffering System.IO.stdout GHC.IO.Handle.LineBuffering"
-  _ <- eval 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)
-  _ <- eval interpreter "GHC.IO.Handle.hSetEncoding System.IO.stdout GHC.IO.Handle.utf8"
-  _ <- eval interpreter "GHC.IO.Handle.hSetEncoding System.IO.stderr GHC.IO.Handle.utf8"
-
-  _ <- eval interpreter ":m - System.IO"
-  _ <- eval 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
-
-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 = (const $ 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/driver/sensei-web.hs b/driver/sensei-web.hs
--- a/driver/sensei-web.hs
+++ b/driver/sensei-web.hs
@@ -2,7 +2,10 @@
 
 import           System.Environment
 
+import           Paths_sensei
 import           Run
 
 main :: IO ()
-main = getArgs >>= runWeb
+main = do
+  startupFile <- getDataFileName "startup.ghci"
+  getArgs >>= runWeb startupFile
diff --git a/driver/sensei.hs b/driver/sensei.hs
--- a/driver/sensei.hs
+++ b/driver/sensei.hs
@@ -2,7 +2,10 @@
 
 import           System.Environment
 
+import           Paths_sensei
 import           Run
 
 main :: IO ()
-main = getArgs >>= run
+main = do
+  startupFile <- getDataFileName "startup.ghci"
+  getArgs >>= run startupFile
diff --git a/sensei.cabal b/sensei.cabal
--- a/sensei.cabal
+++ b/sensei.cabal
@@ -1,11 +1,11 @@
 cabal-version: 1.12
 
--- This file has been generated from package.yaml by hpack version 0.34.7.
+-- This file has been generated from package.yaml by hpack version 0.35.1.
 --
 -- see: https://github.com/sol/hpack
 
 name:           sensei
-version:        0.6.2
+version:        0.7.0
 synopsis:       Automatically run Hspec tests on file modifications
 category:       Development
 homepage:       https://github.com/hspec/sensei#readme
@@ -14,6 +14,8 @@
 license:        MIT
 license-file:   LICENSE
 build-type:     Simple
+data-files:
+    startup.ghci
 
 source-repository head
   type: git
@@ -23,7 +25,6 @@
   main-is: seito.hs
   hs-source-dirs:
       src
-      doctest/ghci-wrapper/src/
       driver
   ghc-options: -Wall
   build-depends:
@@ -32,7 +33,7 @@
     , bytestring
     , directory
     , filepath
-    , fsnotify
+    , fsnotify ==0.4.*
     , http-client >=0.5.0
     , http-types
     , network
@@ -48,12 +49,13 @@
       EventQueue
       HTTP
       Imports
+      Language.Haskell.GhciWrapper
       Options
+      ReadHandle
       Run
       Session
       Trigger
       Util
-      Language.Haskell.GhciWrapper
       Paths_sensei
   default-language: Haskell2010
 
@@ -61,7 +63,6 @@
   main-is: sensei.hs
   hs-source-dirs:
       src
-      doctest/ghci-wrapper/src/
       driver
   ghc-options: -Wall
   build-depends:
@@ -70,7 +71,7 @@
     , bytestring
     , directory
     , filepath
-    , fsnotify
+    , fsnotify ==0.4.*
     , http-client >=0.5.0
     , http-types
     , network
@@ -86,12 +87,13 @@
       EventQueue
       HTTP
       Imports
+      Language.Haskell.GhciWrapper
       Options
+      ReadHandle
       Run
       Session
       Trigger
       Util
-      Language.Haskell.GhciWrapper
       Paths_sensei
   default-language: Haskell2010
 
@@ -99,7 +101,6 @@
   main-is: sensei-web.hs
   hs-source-dirs:
       src
-      doctest/ghci-wrapper/src/
       driver
   ghc-options: -Wall
   build-depends:
@@ -108,7 +109,7 @@
     , bytestring
     , directory
     , filepath
-    , fsnotify
+    , fsnotify ==0.4.*
     , http-client >=0.5.0
     , http-types
     , network
@@ -124,12 +125,13 @@
       EventQueue
       HTTP
       Imports
+      Language.Haskell.GhciWrapper
       Options
+      ReadHandle
       Run
       Session
       Trigger
       Util
-      Language.Haskell.GhciWrapper
       Paths_sensei
   default-language: Haskell2010
 
@@ -138,20 +140,21 @@
   main-is: Spec.hs
   hs-source-dirs:
       src
-      doctest/ghci-wrapper/src/
       test
   ghc-options: -Wall
   cpp-options: -DTEST
   build-tool-depends:
       hspec-discover:hspec-discover
   build-depends:
-      ansi-terminal
+      QuickCheck
+    , ansi-terminal
     , base >=4.11 && <5
     , bytestring
     , directory
     , filepath
-    , fsnotify
+    , fsnotify ==0.4.*
     , hspec >=2.9.0
+    , hspec-contrib >=0.5.2
     , hspec-wai
     , http-client >=0.5.0
     , http-types
@@ -170,17 +173,20 @@
       EventQueue
       HTTP
       Imports
+      Language.Haskell.GhciWrapper
       Options
+      ReadHandle
       Run
       Session
       Trigger
       Util
-      Language.Haskell.GhciWrapper
       ClientSpec
       EventQueueSpec
       Helper
       HTTPSpec
+      Language.Haskell.GhciWrapperSpec
       OptionsSpec
+      ReadHandleSpec
       SessionSpec
       TriggerSpec
       UtilSpec
diff --git a/src/Imports.hs b/src/Imports.hs
--- a/src/Imports.hs
+++ b/src/Imports.hs
@@ -1,14 +1,29 @@
 module Imports (module Imports) where
 
+import           Control.Arrow as Imports ((>>>), (&&&))
 import           Control.Concurrent as Imports
 import           Control.Exception as Imports
 import           Control.Monad as Imports
+import           Data.Functor as Imports
 import           Data.Bifunctor as Imports
 import           Data.Char as Imports
 import           Data.List as Imports
 import           Data.Maybe as Imports
 import           Data.String as Imports
+import           Data.ByteString.Char8 as Imports (ByteString, pack, unpack)
 import           Data.Text.Lazy.Encoding as Imports (encodeUtf8)
 import           Data.Tuple as Imports
 import           System.IO.Error as Imports (isDoesNotExistError)
 import           Text.Read as Imports (readMaybe)
+
+pass :: Applicative m => m ()
+pass = pure ()
+
+while :: Monad m => m Bool -> m () -> m ()
+while p action = go
+  where
+    go = do
+      notDone <- p
+      when notDone $ do
+        action
+        go
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,141 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE NamedFieldPuns #-}
+module Language.Haskell.GhciWrapper (
+  Config(..)
+, Interpreter
+, withInterpreter
+, eval
+, evalVerbose
+) where
+
+import           Imports
+
+import qualified Data.ByteString as B
+import           Data.Text.Encoding (decodeUtf8)
+import qualified Data.Text as T
+import           System.IO hiding (stdin, stdout, stderr)
+import qualified System.IO as System
+import           System.Directory (doesFileExist)
+import           System.Process
+import           System.Exit (ExitCode(..))
+
+import qualified ReadHandle
+import           ReadHandle (ReadHandle, toReadHandle)
+
+data Config = Config {
+  configIgnoreDotGhci :: Bool
+, configVerbose :: Bool
+, configStartupFile :: FilePath
+} deriving (Eq, Show)
+
+data Interpreter = Interpreter {
+  hIn  :: Handle
+, hOut :: Handle
+, readHandle :: ReadHandle
+, process :: ProcessHandle
+}
+
+die :: String -> IO a
+die = throwIO . ErrorCall
+
+withInterpreter :: Config -> [String] -> (Interpreter -> IO r) -> IO r
+withInterpreter config args = bracket (new config args) (close $ verbosity config)
+
+new :: Config -> [String] -> IO Interpreter
+new config@Config{..} args_ = do
+
+  requireFile configStartupFile
+
+  (Just stdin_, Just stdout_, Nothing, processHandle ) <- createProcess (proc "ghci" args) {
+    std_in  = CreatePipe
+  , std_out = CreatePipe
+  , std_err = Inherit
+  }
+
+  setMode stdin_
+  readHandle <- toReadHandle stdout_ 1024
+
+  let
+    interpreter = Interpreter {
+      hIn = stdin_
+    , readHandle
+    , hOut = stdout_
+    , process = processHandle
+    }
+
+  _ <- printStartupMessages interpreter
+
+  evalThrow interpreter "GHC.IO.Handle.hDuplicateTo System.IO.stdout System.IO.stderr"
+
+  -- GHCi uses NoBuffering for stdout and stderr by default:
+  -- https://downloads.haskell.org/ghc/9.4.4/docs/users_guide/ghci.html
+  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"
+
+  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"
+
+  return interpreter
+  where
+    requireFile name = do
+      exists <- doesFileExist name
+      unless exists $ do
+        die $ "Required file " <> show name <> " does not exist!"
+
+    args = "-ghci-script" : configStartupFile : args_ ++ catMaybes [
+        if configIgnoreDotGhci then Just "-ignore-dot-ghci" else Nothing
+      ]
+
+    setMode h = do
+      hSetBinaryMode h False
+      hSetBuffering h LineBuffering
+      hSetEncoding h utf8
+
+    printStartupMessages interpreter = evalWith (verbosity config) interpreter ""
+
+    evalThrow :: Interpreter -> String -> IO ()
+    evalThrow interpreter expr = do
+      output <- eval interpreter expr
+      unless (null output) $ do
+        close (verbosity config) interpreter
+        die output
+
+close :: (ByteString -> IO ()) -> Interpreter -> IO ()
+close echo Interpreter{..} = do
+  hClose hIn
+  ReadHandle.drain readHandle echo
+  hClose hOut
+  e <- waitForProcess process
+  when (e /= ExitSuccess) $ do
+    throwIO (userError $ "Language.Haskell.GhciWrapper.close: Interpreter exited with an error (" ++ show e ++ ")")
+
+putExpression :: Interpreter -> String -> IO ()
+putExpression Interpreter{hIn = stdin} e = do
+  hPutStrLn stdin e
+  B.hPut stdin ReadHandle.marker
+  hFlush stdin
+
+getResult :: Interpreter -> (ByteString -> IO ()) -> IO String
+getResult Interpreter{readHandle = h} = fmap (T.unpack . decodeUtf8) . ReadHandle.getResult h
+
+verbosity :: Config -> ByteString -> IO ()
+verbosity config
+  | configVerbose config = verbose
+  | otherwise = silent
+
+verbose :: ByteString -> IO ()
+verbose string = B.putStr string >> hFlush System.stdout
+
+silent :: ByteString -> IO ()
+silent _ = pass
+
+evalWith :: (ByteString -> IO ()) -> Interpreter -> String -> IO String
+evalWith echo repl expr = do
+  putExpression repl expr
+  getResult repl echo
+
+eval :: Interpreter -> String -> IO String
+eval = evalWith silent
+
+evalVerbose :: Interpreter -> String -> IO String
+evalVerbose = evalWith verbose
diff --git a/src/ReadHandle.hs b/src/ReadHandle.hs
new file mode 100644
--- /dev/null
+++ b/src/ReadHandle.hs
@@ -0,0 +1,170 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE ViewPatterns #-}
+module ReadHandle (
+  ReadHandle(..)
+, toReadHandle
+, marker
+, getResult
+, drain
+#ifdef TEST
+, newEmptyBuffer
+#endif
+) where
+
+import           Imports
+
+import qualified Data.ByteString.Char8 as B
+import           Data.IORef
+import           System.IO hiding (stdin, stdout, stderr, isEOF)
+
+#if MIN_VERSION_bytestring(0,11,0)
+import           Data.ByteString (dropEnd)
+#else
+import qualified Data.ByteString.Internal as B
+dropEnd :: Int -> ByteString -> ByteString
+dropEnd n ps@(B.PS x offset len)
+    | n <= 0    = ps
+    | n >= len  = B.empty
+    | otherwise = B.PS x offset (len - n)
+#endif
+
+-- | 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 :: ByteString
+marker = pack (show @String "be77d2c8427d29cd1d62b7612d8e98cc") <> "\n"
+
+partialMarkers :: [ByteString]
+partialMarkers = reverse . drop 1 . init $ B.inits marker
+
+data ReadHandle = ReadHandle {
+  getChunk :: IO ByteString
+, buffer :: IORef Buffer
+}
+
+drain :: ReadHandle -> (ByteString -> IO ()) -> IO ()
+drain h echo = while (not <$> isEOF h) $ do
+  _ <- getResult h echo
+  pass
+
+isEOF :: ReadHandle -> IO Bool
+isEOF ReadHandle{..} = do
+  readIORef buffer <&> \ case
+    BufferEOF -> True
+    BufferEmpty -> False
+    BufferPartialMarker {}  -> False
+    BufferChunk {} -> False
+
+emptyBuffer :: Buffer -> Buffer
+emptyBuffer old = case old of
+  BufferEOF -> BufferEOF
+  BufferEmpty -> BufferEmpty
+  BufferPartialMarker {}  -> BufferEmpty
+  BufferChunk {} -> BufferEmpty
+
+mkBufferChunk :: ByteString -> Buffer
+mkBufferChunk chunk
+  | B.null chunk = BufferEmpty
+  | otherwise = BufferChunk chunk
+
+data Buffer =
+    BufferEOF
+  | BufferEmpty
+  | BufferPartialMarker !ByteString
+  | BufferChunk !ByteString
+
+toReadHandle :: Handle -> Int -> IO ReadHandle
+toReadHandle h n = do
+  hSetBinaryMode h True
+  ReadHandle (B.hGetSome h n) <$> newEmptyBuffer
+
+newEmptyBuffer :: IO (IORef Buffer)
+newEmptyBuffer = newIORef BufferEmpty
+
+getResult :: ReadHandle -> (ByteString -> IO ()) -> IO ByteString
+getResult h echo = mconcat <$> go
+  where
+    go :: IO [ByteString]
+    go = nextChunk h >>= \ case
+      Chunk chunk -> echo chunk >> (chunk :) <$> go
+      Marker -> return []
+      EOF -> return []
+
+data Chunk = Chunk ByteString | Marker | EOF
+
+nextChunk :: ReadHandle -> IO Chunk
+nextChunk ReadHandle {..} = go
+  where
+    takeBuffer :: IO Buffer
+    takeBuffer = atomicModifyIORef' buffer (emptyBuffer &&& id)
+
+    putBuffer :: Buffer -> IO ()
+    putBuffer = writeIORef buffer
+
+    putBuffer_ :: ByteString -> IO ()
+    putBuffer_ = putBuffer . mkBufferChunk
+
+    getSome :: IO (Maybe ByteString)
+    getSome = do
+      chunk <- getChunk
+      if B.null chunk then do
+        putBuffer BufferEOF
+        return Nothing
+      else do
+        return (Just chunk)
+
+    go :: IO Chunk
+    go = takeBuffer >>= \ case
+      BufferEOF -> return EOF
+      BufferEmpty -> getSome >>= \ case
+        Nothing -> return EOF
+        Just chunk -> processChunk chunk
+      BufferPartialMarker partialMarker -> getSome >>= \ case
+        Nothing -> return (Chunk partialMarker)
+        Just chunk -> processChunk (partialMarker <> chunk)
+      BufferChunk chunk -> processChunk chunk
+
+    processChunk :: ByteString -> IO Chunk
+    processChunk chunk = case stripMarker chunk of
+      StrippedMarker rest -> do
+        putBuffer_ rest
+        return Marker
+      PrefixBeforeMarker prefix rest -> do
+        putBuffer_ rest
+        return (Chunk prefix)
+      NoMarker -> case splitPartialMarker chunk of
+        Just (prefix, partialMarker) -> do
+          putBuffer (BufferPartialMarker partialMarker)
+          if B.null prefix then do
+            go
+          else do
+            return (Chunk prefix)
+        Nothing -> return (Chunk chunk)
+
+splitPartialMarker :: ByteString -> Maybe (ByteString, ByteString)
+splitPartialMarker chunk = split <$> findPartialMarker chunk
+  where
+    split partialMarker = (dropEnd (B.length partialMarker) chunk, partialMarker)
+
+findPartialMarker :: ByteString -> Maybe ByteString
+findPartialMarker chunk = find (`B.isSuffixOf` chunk) partialMarkers
+
+data StripMarker =
+    NoMarker
+  | PrefixBeforeMarker !ByteString !ByteString
+  | StrippedMarker !ByteString
+
+stripMarker :: ByteString -> StripMarker
+stripMarker input = case brakeAtMarker input of
+  (_, "") -> NoMarker
+  ("", dropMarker -> ys) -> StrippedMarker ys
+  (xs, ys) -> PrefixBeforeMarker xs ys
+  where
+    brakeAtMarker = B.breakSubstring marker
+    dropMarker = B.drop (B.length marker)
diff --git a/src/Run.hs b/src/Run.hs
--- a/src/Run.hs
+++ b/src/Run.hs
@@ -11,7 +11,7 @@
 
 import qualified HTTP
 import qualified Session
-import           Session (Session)
+import           Session (Session, Config(..))
 
 import           EventQueue
 import           Trigger
@@ -22,19 +22,24 @@
 
 watchFiles :: EventQueue -> IO ()
 watchFiles queue = do
-  watch $ emitEvent queue . \ case
-    Added file _ _ -> FileEvent FileAdded file
-    Removed file _ _ -> FileEvent FileRemoved file
-    Modified file _ _ -> FileEvent FileModified file
-    Unknown file _ _ -> FileEvent FileModified file
+  watch $ \ case
+    Added file _ _ -> emit $ FileEvent FileAdded file
+    Modified file _ _ -> emit $ FileEvent FileModified file
+    ModifiedAttributes _file _ _ -> pass
+    Removed file _ _ -> emit $ FileEvent FileRemoved file
+    WatchedDirectoryRemoved _file _ _ -> pass
+    CloseWrite file _ _ -> emit $ FileEvent FileModified file
+    Unknown file _ _ _ -> emit $ FileEvent FileModified file
   where
-    isInteresting = (&&) <$> not . eventIsDirectory <*> not . isBoring . eventPath
+    emit = emitEvent queue
 
     watch action = void . forkIO $ do
       withManager $ \ manager -> do
         _stopListening <- watchTree manager "." isInteresting action
         waitForever
-
+      where
+        isInteresting = (&&) <$> isFile <*> not . isBoring . eventPath
+        isFile = eventIsDirectory >>> (== IsFile)
 
 watchInput :: EventQueue -> IO ()
 watchInput queue = void . forkIO $ do
@@ -43,8 +48,8 @@
     emitEvent queue TriggerAll
   emitEvent queue Done
 
-run :: [String] -> IO ()
-run args = do
+run :: FilePath -> [String] -> IO ()
+run startupFile args = do
   queue <- newQueue
   watchFiles queue
   watchInput queue
@@ -55,7 +60,7 @@
       saveOutput action = modifyMVar_ lastOutput $ \_ -> action
 
       go = do
-        status <- withSession args $ \ session -> do
+        status <- withSession startupFile args $ \ session -> do
           let
             triggerAction = saveOutput (trigger session)
             triggerAllAction = saveOutput (triggerAll session)
@@ -66,16 +71,16 @@
           Terminate -> return ()
     go
 
-runWeb :: [String] -> IO ()
-runWeb args = do
-  withSession args $ \session -> do
+runWeb :: FilePath -> [String] -> IO ()
+runWeb startupFile args = do
+  withSession startupFile args $ \session -> do
     _ <- trigger session
     lock <- newMVar ()
     HTTP.withServer (withMVar lock $ \() -> trigger session) $ do
       waitForever
 
-withSession :: [String] -> (Session -> IO a) -> IO a
-withSession args action = do
+withSession :: FilePath -> [String] -> (Session -> IO a) -> IO a
+withSession startupFile args action = do
   check <- dotGhciWritableByOthers
   when check $ do
     putStrLn ".ghci is writable by others, you can fix this with:"
@@ -83,4 +88,11 @@
     putStrLn "    chmod go-w .ghci ."
     putStrLn ""
     exitFailure
-  bracket (Session.new args) Session.close action
+  Session.withSession config args action
+  where
+    config :: Config
+    config = Config {
+      configIgnoreDotGhci = False
+    , configVerbose = True
+    , configStartupFile = startupFile
+    }
diff --git a/src/Session.hs b/src/Session.hs
--- a/src/Session.hs
+++ b/src/Session.hs
@@ -1,9 +1,9 @@
 {-# LANGUAGE CPP #-}
 {-# LANGUAGE RecordWildCards #-}
 module Session (
-  Session(..)
-, new
-, close
+  Config(..)
+, Session(..)
+, withSession
 , reload
 
 , Summary(..)
@@ -27,8 +27,7 @@
 
 import           Data.IORef
 
-import qualified Language.Haskell.GhciWrapper as GhciWrapper
-import           Language.Haskell.GhciWrapper hiding (new, close)
+import           Language.Haskell.GhciWrapper
 
 import           Util
 import           Options
@@ -48,23 +47,17 @@
 hspecPreviousSummary :: Session -> IO (Maybe Summary)
 hspecPreviousSummary Session{..} = readIORef sessionHspecPreviousSummary
 
-new :: [String] -> IO Session
-new args = do
-  let (ghciArgs, hspecArgs) = splitArgs args
-  ghci <- GhciWrapper.new defaultConfig{configVerbose = True, configIgnoreDotGhci = False} ghciArgs
-  _ <- eval ghci (":set prompt " ++ show "")
-  _ <- eval ghci ("import qualified System.Environment")
-  _ <- eval ghci ("import qualified Test.Hspec.Runner")
-  _ <- eval ghci ("import qualified Test.Hspec.Meta")
-  _ <- eval ghci ("System.Environment.unsetEnv " ++ show hspecFailureEnvName)
-  ref <- newIORef (Just $ Summary 0 0)
-  return (Session ghci hspecArgs ref)
-
-close :: Session -> IO ()
-close = GhciWrapper.close . sessionInterpreter
+withSession :: Config -> [String] -> (Session -> IO r) -> IO r
+withSession config args action = do
+  withInterpreter config ghciArgs $ \ ghci -> do
+    _ <- eval ghci ("System.Environment.unsetEnv " ++ show hspecFailureEnvName)
+    ref <- newIORef (Just $ Summary 0 0)
+    action (Session ghci hspecArgs ref)
+  where
+    (ghciArgs, hspecArgs) = splitArgs args
 
 reload :: Session -> IO String
-reload Session{..} = evalEcho sessionInterpreter ":reload"
+reload Session{..} = evalVerbose sessionInterpreter ":reload"
 
 data Summary = Summary {
   summaryExamples :: Int
@@ -103,7 +96,7 @@
 runSpec command session@Session{..} = do
   failedPreviously <- isFailure <$> hspecPreviousSummary session
   let args = "--color" : (if failedPreviously then addRerun else id) sessionHspecArgs
-  r <- evalEcho sessionInterpreter $ "System.Environment.withArgs " ++ show args ++ " $ " ++ command
+  r <- evalVerbose sessionInterpreter $ "System.Environment.withArgs " ++ show args ++ " $ " ++ command
   writeIORef sessionHspecPreviousSummary (parseSummary r)
   return r
   where
diff --git a/src/Trigger.hs b/src/Trigger.hs
--- a/src/Trigger.hs
+++ b/src/Trigger.hs
@@ -41,12 +41,14 @@
       withColor Red $ putStrLn "RELOADING FAILED"
       return (False, "")
   where
+    hspec :: IO (Bool, String)
     hspec = do
       mRun <- Session.getRunSpec session
       case mRun of
         Just run -> runSpecs run
         Nothing -> return (True, "")
 
+    runSpecs :: IO String -> IO (Bool, String)
     runSpecs run = do
       failedPreviously <- isFailure <$> hspecPreviousSummary session
       (success, xs) <- runSpec run
@@ -54,6 +56,7 @@
         then runSpec run
         else return (success, "")
 
+    runSpec :: IO a -> IO (Bool, a)
     runSpec run = do
       xs <- run
       success <- isSuccess <$> hspecPreviousSummary session
diff --git a/startup.ghci b/startup.ghci
new file mode 100644
--- /dev/null
+++ b/startup.ghci
@@ -0,0 +1,4 @@
+:set prompt ""
+:unset +m +r +s +t +c
+:seti -XHaskell2010
+:seti -XNoOverloadedStrings
diff --git a/test/Helper.hs b/test/Helper.hs
--- a/test/Helper.hs
+++ b/test/Helper.hs
@@ -1,6 +1,6 @@
-{-# LANGUAGE QuasiQuotes #-}
 module Helper (
   module Imports
+, ghciConfig
 , withSession
 , withSomeSpec
 , passingSpec
@@ -16,16 +16,33 @@
 import           System.Process as Imports (readProcess, callCommand)
 import           Test.Hspec as Imports
 import           Test.Mockery.Directory as Imports
+import           Test.Hspec.Contrib.Mocks.V1 as Imports
 
 import           Run ()
 import qualified Session
 import           Session (Session)
+import           Language.Haskell.GhciWrapper (Config(..))
 
+startupFile :: FilePath
+startupFile = "startup.ghci"
+
+ghciConfig :: Config
+ghciConfig = Config {
+  configIgnoreDotGhci = True
+, configVerbose = False
+, configStartupFile = startupFile
+}
+
 withSession :: [String] -> (Session -> IO a) -> IO a
-withSession args action = bracket (Session.new $ "-ignore-dot-ghci" : args) Session.close action
+withSession = Session.withSession ghciConfig
 
 withSomeSpec :: IO a -> IO a
-withSomeSpec = (inTempDirectory .  (writeFile "Spec.hs" passingSpec >>))
+withSomeSpec action = do
+  startup <- readFile startupFile
+  inTempDirectory $ do
+    writeFile startupFile startup
+    writeFile "Spec.hs" passingSpec
+    action
 
 passingSpec :: String
 passingSpec = unlines [
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,89 @@
+{-# LANGUAGE CPP #-}
+module Language.Haskell.GhciWrapperSpec (main, spec) where
+
+import           Helper
+
+import           Language.Haskell.GhciWrapper (Config(..), Interpreter)
+import qualified Language.Haskell.GhciWrapper as Interpreter
+
+main :: IO ()
+main = hspec spec
+
+withInterpreter :: [String] -> (Interpreter -> IO a) -> IO a
+withInterpreter = Interpreter.withInterpreter ghciConfig
+
+withGhci :: ((String -> IO String) -> IO a) -> IO a
+withGhci action = withInterpreter [] $ action . Interpreter.eval
+
+spec :: Spec
+spec = do
+  describe "withInterpreter" $ do
+    context "on shutdown" $ do
+      it "drains `stdout` of the `ghci` process" $ do
+        result <- capture_ $ Interpreter.withInterpreter ghciConfig {configVerbose = True} [] $ \ _ghci -> do
+          pass
+        last (lines result) `shouldBe` "Leaving GHCi."
+
+  describe "evalVerbose" $ do
+    it "echos result to stdout" $ do
+      withInterpreter [] $ \ ghci -> do
+        capture (Interpreter.evalVerbose ghci $ "putStr" ++ show "foo\nbar") `shouldReturn` ("foo\nbar", "foo\nbar")
+
+  describe "eval" $ do
+    it "shows literals" $ withGhci $ \ ghci -> do
+      ghci "23" `shouldReturn` "23\n"
+
+    it "shows string literals containing Unicode" $ withGhci $ \ ghci -> do
+      ghci "\"λ\"" `shouldReturn` "\"\\955\"\n"
+
+    it "evaluates simple expressions" $ withGhci $ \ ghci -> do
+      ghci "23 + 42" `shouldReturn` "65\n"
+
+    it "uses LineBuffering for stdout and stderr" $ withGhci $ \ ghci -> do
+      ghci "GHC.IO.Handle.hGetBuffering System.IO.stdout" `shouldReturn` "LineBuffering\n"
+      ghci "GHC.IO.Handle.hGetBuffering System.IO.stderr" `shouldReturn` "LineBuffering\n"
+
+    it "supports let bindings" $ withGhci $ \ ghci -> do
+      ghci "let x = 10" `shouldReturn` ""
+      ghci "x" `shouldReturn` "10\n"
+
+    it "allows import statements" $ withGhci $ \ ghci -> do
+      ghci "import Data.Maybe" `shouldReturn` ""
+      ghci "fromJust (Just 20)" `shouldReturn` "20\n"
+
+    it "captures stdout" $ withGhci $ \ ghci -> do
+      ghci "putStr \"foo\"" `shouldReturn` "foo"
+
+    it "captures stdout (Unicode)" $ withGhci $ \ ghci -> do
+      ghci "putStrLn \"λ\"" `shouldReturn` "λ\n"
+
+    it "captures stdout (empty line)" $ withGhci $ \ ghci -> do
+      ghci "putStrLn \"\"" `shouldReturn` "\n"
+
+    it "captures stdout (multiple lines)" $ withGhci $ \ ghci -> do
+      ghci "putStrLn \"foo\" >> putStrLn \"bar\" >> putStrLn \"baz\""
+        `shouldReturn` "foo\nbar\nbaz\n"
+
+    it "captures stderr" $ withGhci $ \ ghci -> do
+      ghci "import System.IO" `shouldReturn` ""
+      ghci "hPutStrLn stderr \"foo\"" `shouldReturn` "foo\n"
+
+    it "captures stderr (Unicode)" $ withGhci $ \ ghci -> do
+      ghci "import System.IO" `shouldReturn` ""
+      ghci "hPutStrLn stderr \"λ\"" `shouldReturn` "λ\n"
+
+    it "shows exceptions" $ withGhci $ \ ghci -> do
+      ghci "import Control.Exception" `shouldReturn` ""
+      ghci "throwIO DivideByZero" `shouldReturn` "*** Exception: divide by zero\n"
+
+    it "shows exceptions (ExitCode)" $ withGhci $ \ 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" $ withGhci $ \ ghci -> do
+      ghci "foo" >>= (`shouldSatisfy` isInfixOf "Variable not in scope: foo")
+
+    context "with -XNoImplicitPrelude" $ do
+      it "works" $ withInterpreter ["-XNoImplicitPrelude"] $ \ ghci -> do
+        Interpreter.eval ghci "putStrLn \"foo\"" >>= (`shouldContain` "Variable not in scope: putStrLn")
+        Interpreter.eval ghci "23" `shouldReturn` "23\n"
diff --git a/test/ReadHandleSpec.hs b/test/ReadHandleSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/ReadHandleSpec.hs
@@ -0,0 +1,111 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ViewPatterns #-}
+module ReadHandleSpec (spec) where
+
+import           Helper
+import           Test.QuickCheck
+import qualified Data.ByteString as B
+
+import           ReadHandle
+
+chunkByteString :: (Int, Int) -> ByteString -> Gen [ByteString]
+chunkByteString size = go
+  where
+    go "" = return []
+    go xs = do
+      n <- chooseInt size
+      let (chunk, rest) = B.splitAt n xs
+      (chunk :) <$> go rest
+
+fakeHandle :: [ByteString] -> IO ReadHandle
+fakeHandle chunks = ReadHandle <$> stubAction chunks <*> newEmptyBuffer
+
+data ChunkSizes = SmallChunks | BigChunks
+
+withRandomChunkSizes :: [ByteString] -> (ReadHandle -> Expectation) -> Property
+withRandomChunkSizes (mconcat -> input) action = property $ do
+  chunkSizes <- elements [SmallChunks, BigChunks]
+  let
+    maxChunkSize = case chunkSizes of
+      SmallChunks -> 4
+      BigChunks -> B.length input
+
+  chunks <- chunkByteString (1, maxChunkSize) input
+  return $ fakeHandle chunks >>= action
+
+partialMarker :: ByteString
+partialMarker = B.take 5 marker
+
+spec :: Spec
+spec = do
+  describe "drain" $ do
+    it "drains all remaining input" $ do
+      h <- fakeHandle ["foo", marker, "bar", marker, "baz", marker, ""]
+      withSpy (drain h) `shouldReturn` ["foo", "bar", "baz"]
+
+  describe "getResult" $ do
+    context "with a single result" $ do
+      let input = ["foo", "bar", "baz", marker]
+
+      it "returns result" $ do
+        withSpy $ \ echo -> do
+          h <- fakeHandle input
+          getResult h echo `shouldReturn` "foobarbaz"
+        `shouldReturn` ["foo", "bar", "baz"]
+
+      context "with chunks of arbitrary size" $ do
+        it "returns result" $ do
+          withRandomChunkSizes input $ \ h -> do
+            fmap mconcat . withSpy $ \ echo -> do
+              getResult h echo `shouldReturn` "foobarbaz"
+            `shouldReturn` "foobarbaz"
+
+    context "with multiple results" $ do
+      let input = ["foo", marker, "bar", marker, "baz", marker]
+
+      it "returns one result at a time" $ do
+        withSpy $ \ echo -> do
+          h <- fakeHandle input
+          getResult h echo `shouldReturn` "foo"
+          getResult h echo `shouldReturn` "bar"
+          getResult h echo `shouldReturn` "baz"
+        `shouldReturn` ["foo", "bar", "baz"]
+
+      context "with chunks of arbitrary size" $ do
+        it "returns one result at a time" $ do
+          withRandomChunkSizes input $ \ h -> do
+            fmap mconcat . withSpy $ \ echo -> do
+              getResult h echo `shouldReturn` "foo"
+              getResult h echo `shouldReturn` "bar"
+              getResult h echo `shouldReturn` "baz"
+            `shouldReturn` "foobarbaz"
+
+    context "when a chunk that contains a marker ends with a partial marker" $ do
+      it "correctly gives the marker precedence over the partial marker" $ do
+        withSpy $ \ echo -> do
+          h <- fakeHandle ["foo" <> marker <> "bar" <> partialMarker, ""]
+          getResult h echo `shouldReturn` "foo"
+          getResult h echo `shouldReturn` ("bar" <> partialMarker)
+        `shouldReturn` ["foo", "bar", partialMarker]
+
+    context "on EOF" $ do
+      it "returns all remaining input" $ do
+        withSpy $ \ echo -> do
+          h <- fakeHandle ["foo", "bar", "baz", ""]
+          getResult h echo `shouldReturn` "foobarbaz"
+        `shouldReturn` ["foo", "bar", "baz"]
+
+      context "with a partialMarker at the end" $ do
+        it "includes the partial marker in the output" $ do
+          withSpy $ \ echo -> do
+            h <- fakeHandle ["foo", "bar", "baz", partialMarker, ""]
+            getResult h echo `shouldReturn` ("foobarbaz" <> partialMarker)
+          `shouldReturn` ["foo", "bar", "baz", partialMarker]
+
+      context "after a marker" $ do
+        it "returns all remaining input" $ do
+          withSpy $ \ echo -> do
+            h <- fakeHandle ["foo", "bar", "baz", marker, "qux", ""]
+            getResult h echo `shouldReturn` "foobarbaz"
+            getResult h echo `shouldReturn` "qux"
+          `shouldReturn` ["foo", "bar", "baz", "qux"]
diff --git a/test/SessionSpec.hs b/test/SessionSpec.hs
--- a/test/SessionSpec.hs
+++ b/test/SessionSpec.hs
@@ -7,16 +7,27 @@
 
 import           Language.Haskell.GhciWrapper (eval)
 import qualified Session
-import           Session (Session(..), Summary(..), hspecFailureEnvName, hspecPreviousSummary, hspecCommand)
+import           Session (Config(..), Session(..), Summary(..), hspecFailureEnvName, hspecPreviousSummary, hspecCommand)
 
 spec :: Spec
 spec = do
-  describe "new" $ do
+  describe "withSession" $ do
     it "unsets HSPEC_FAILURES" $ do
       setEnv hspecFailureEnvName "foo" True
-      withSession [] $ \Session{..} -> do
-        _ <- eval sessionInterpreter "import System.Environment"
-        eval sessionInterpreter ("lookupEnv " ++ show hspecFailureEnvName) `shouldReturn` "Nothing\n"
+      withSession [] $ \ Session{..} -> do
+        eval sessionInterpreter ("System.Environment.lookupEnv " ++ show hspecFailureEnvName) `shouldReturn` "Nothing\n"
+
+    context "with `:set +t +s`" $ do
+      it "works just fine" $ do
+        withSomeSpec $ do
+          writeFile ".ghci" ":set +t +s"
+          Session.withSession ghciConfig {configIgnoreDotGhci = False} [] $ \ Session{..} -> do
+            eval sessionInterpreter "23" `shouldReturn` "23\n"
+
+    context "with -XOverloadedStrings" $ do
+      it "works just fine" $ do
+        withSession ["-XOverloadedStrings", "-Wall", "-Werror"] $ \ Session{..} -> do
+          eval sessionInterpreter "23 :: Int" `shouldReturn` "23\n"
 
   describe "reload" $ do
     it "reloads" $ do
diff --git a/test/TriggerSpec.hs b/test/TriggerSpec.hs
--- a/test/TriggerSpec.hs
+++ b/test/TriggerSpec.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE CPP #-}
 module TriggerSpec (spec) where
 
 import           Helper
@@ -148,7 +149,11 @@
           writeFile "Spec.hs" passingSpec
           (True, xs) <- silence (trigger session)
           normalize xs `shouldBe` [
+#if __GLASGOW_HASKELL__ < 904
               "[1 of 1] Compiling Spec             ( Spec.hs, interpreted )"
+#else
+              "[1 of 1] Compiling Spec             ( Spec.hs, interpreted ) [Source file changed]"
+#endif
             , modulesLoaded Ok ["Spec"]
             , ""
             , "bar [✔]"
