diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for pinned-warnings
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Aaron Allen (c) 2021
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Aaron Allen nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,40 @@
+# Pinned Warnings
+
+### The problem
+When working in GHCi, it's easy to lose track of warnings from modules other
+than the one you are currently editing. Having to reload the project to catch
+any warnings you might have missed is painful.
+
+### The solution
+This package has a GHC plugin that allows you to see all the current warnings
+from all modules in GHCi by calling a special `showWarnings` function.
+
+### Usage
+You can start GHCi in your project with one of the following commands to enable
+the necessary plugin. You can add `pinned-warnings` as a package dependency to
+avoid having to include the additional argument.
+```
+cabal update
+cabal new-repl -b pinned-warnings --ghc-options="-fplugin PinnedWarnings"
+
+stack update
+stack repl --package pinned-warnings --ghci-options "-fplugin PinnedWarnings"
+```
+Then you must add the `ShowWarnings` module to the GHCi context:
+```
+:m + ShowWarnings
+```
+Now all active warnings can be viewed by evaluating `showWarnings`.
+
+### Tip
+You can define a custom GHCi command in your `.ghci` file that adds the
+`ShowWarnings` module and calls `showWarnings`:
+```
+:def sw (\_ -> pure ":m + ShowWarnings \n showWarnings")
+```
+With this you can simply use `:sw` to view warnings.
+
+### Known limitations
+- Warnings produced outside of module type checking are not captured,
+  for example those related to `.cabal` file omissions.
+- Only the specified versions of GHC are supported
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/pinned-warnings.cabal b/pinned-warnings.cabal
new file mode 100644
--- /dev/null
+++ b/pinned-warnings.cabal
@@ -0,0 +1,34 @@
+cabal-version:       2.4
+-- Initial package description 'pinned-warnings.cabal' generated by 'cabal
+-- init'.  For further documentation, see
+-- http://haskell.org/cabal/users-guide/
+
+name:                pinned-warnings
+version:             0.1.0.0
+synopsis: Preserve warnings in a GHCi session
+description: Please see the README on GitHub at <https://github.com/aaronallen8455/pinned-warnings#readme>
+homepage:       https://github.com/aaronallen8455/pinned-warnings#readme
+bug-reports:    https://github.com/aaronallen8455/pinned-warnings/issues
+license:        BSD-3-Clause
+license-file:   LICENSE
+
+author: Aaron Allen
+maintainer:          aaronallen8455@gmail.com
+copyright: 2021 Aaron Allen
+category: Utility
+extra-source-files:
+  README.md
+  CHANGELOG.md
+tested-with: GHC ==8.8.4 || ==8.10.4 || ==9.0.1
+
+library
+  exposed-modules:     PinnedWarnings, ShowWarnings
+  other-modules:       GhcFacade
+  --default-extensions:  ImportQualifiedPost
+  build-depends:       ghc              >= 8.8 && < 9.1,
+                       base             >= 4.7 && < 5,
+                       bytestring       >= 0.10.10 && < 0.11,
+                       containers       >= 0.6.2 && < 0.7,
+                       directory        >= 1.3.6 && < 1.4,
+  hs-source-dirs:      src
+  default-language:    Haskell2010
diff --git a/src/GhcFacade.hs b/src/GhcFacade.hs
new file mode 100644
--- /dev/null
+++ b/src/GhcFacade.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE PatternSynonyms #-}
+module GhcFacade
+  ( module X
+  , pattern RealSrcSpan'
+  , bytesFS'
+  ) where
+
+import qualified Data.ByteString.Char8 as BS
+
+#if MIN_VERSION_ghc(9,0,0)
+import GHC.Data.Bag as X
+import GHC.Tc.Types as X
+import GHC.Tc.Plugin as X
+import GHC.Driver.Plugins as X hiding (TcPlugin)
+import GHC.Data.IOEnv as X
+import GHC.Core.Make as X
+import GHC.Tc.Types.Evidence as X
+import GHC as X
+import GHC.Data.FastString as X
+import GHC.Utils.Error as X
+import GHC.Core.Class as X
+import GHC.Tc.Types.Constraint as X
+import GHC.Types.Name.Occurrence as X
+
+#elif MIN_VERSION_ghc(8,8,0)
+import Bag as X
+import Class as X
+import DynFlags as X
+import ErrUtils as X
+import FastString as X
+import GHC as X
+import HscMain as X
+import HscTypes as X
+import IOEnv as X
+import MkCore as X
+import OccName as X
+import Outputable as X
+import Plugins as X hiding (TcPlugin)
+import TcEvidence as X
+import TcPluginM as X
+import TcRnTypes as X
+#endif
+
+#if MIN_VERSION_ghc(8,10,0) && __GLASGOW_HASKELL__ < 900
+import Constraint as X
+#endif
+
+pattern RealSrcSpan' :: RealSrcSpan -> SrcSpan
+pattern RealSrcSpan' s <-
+#if MIN_VERSION_ghc(9,0,1)
+    RealSrcSpan s x
+#elif MIN_VERSION_ghc(8,8,0)
+    RealSrcSpan s
+#endif
+
+bytesFS' :: FastString -> BS.ByteString
+bytesFS' =
+#if MIN_VERSION_ghc(8,10,0)
+  bytesFS
+#elif MIN_VERSION_ghc(8,8,0)
+  BS.pack . unpackFS
+#endif
diff --git a/src/PinnedWarnings.hs b/src/PinnedWarnings.hs
new file mode 100644
--- /dev/null
+++ b/src/PinnedWarnings.hs
@@ -0,0 +1,132 @@
+{-# LANGUAGE OverloadedStrings #-}
+module PinnedWarnings
+  ( plugin
+  ) where
+
+import           Control.Concurrent.MVar
+import           Control.Monad
+import           Control.Monad.IO.Class
+import qualified Data.ByteString.Char8 as BS
+import           Data.IORef
+import           Data.List
+import qualified Data.Map.Strict as M
+import           Data.Maybe
+import qualified Data.Set as S
+import qualified System.Directory as Dir
+import           System.IO.Unsafe (unsafePerformIO)
+
+import qualified GhcFacade as Ghc
+
+type ModuleFile = BS.ByteString
+
+-- The infamous mutable global trick.
+-- Needed to track the pinned warnings during and after compilation.
+globalState :: MVar (M.Map ModuleFile Ghc.WarningMessages)
+globalState = unsafePerformIO $ newMVar mempty
+{-# NOINLINE globalState #-}
+
+plugin :: Ghc.Plugin
+plugin =
+  Ghc.defaultPlugin
+    { Ghc.tcPlugin = const $ Just tcPlugin
+    , Ghc.typeCheckResultAction = const insertModuleWarnings
+    }
+
+tcPlugin :: Ghc.TcPlugin
+tcPlugin =
+  Ghc.TcPlugin
+    { Ghc.tcPluginInit  = initTcPlugin
+    , Ghc.tcPluginSolve = \(sw, counterRef) _ _ wanteds ->
+        checkWanteds sw counterRef wanteds
+    , Ghc.tcPluginStop  = const $ pure ()
+    }
+
+initTcPlugin :: Ghc.TcPluginM (Ghc.TyCon, IORef Int)
+initTcPlugin =
+  (,) <$> lookupShowWarnings
+      <*> Ghc.tcPluginIO (newIORef 0)
+
+-- | Gets a reference to the 'ShowWarnings' constraint
+lookupShowWarnings :: Ghc.TcPluginM Ghc.TyCon
+lookupShowWarnings = do
+  result <- Ghc.findImportedModule
+              (Ghc.mkModuleName "ShowWarnings")
+              (Just  "pinned-warnings")
+
+  case result of
+    Ghc.Found _ mod -> do
+      name <- Ghc.lookupOrig mod $ Ghc.mkTcOcc "ShowWarnings"
+      Ghc.classTyCon <$> Ghc.tcLookupClass name
+
+    _ -> error "ShowWarnings module not found"
+
+-- | If any wanted constraints are for 'ShowWarnings', then inject any pinned
+-- warnings into GHC.
+checkWanteds :: Ghc.TyCon
+             -> IORef Int
+             -> [Ghc.Ct]
+             -> Ghc.TcPluginM Ghc.TcPluginResult
+checkWanteds sw counterRef
+    = fmap (flip Ghc.TcPluginOk [] . catMaybes)
+    . traverse go
+  where
+    go ct@Ghc.CDictCan { Ghc.cc_class = cls }
+      | Ghc.classTyCon cls == sw = do
+          counter <- Ghc.tcPluginIO $ readIORef counterRef
+
+          -- for some reason warnings only appear if they are added on
+          -- particular iterations.
+          when (counter == 2) addWarningsToContext
+
+          Ghc.tcPluginIO $ modifyIORef' counterRef succ
+
+          pure $ Just (Ghc.EvExpr Ghc.unitExpr, ct)
+
+    go _ = pure Nothing
+
+-- | Add warnings from the global state back into the GHC context
+addWarningsToContext :: Ghc.TcPluginM ()
+addWarningsToContext = do
+  errsRef <- Ghc.tcl_errs . snd <$> Ghc.getEnvs
+
+  pruneDeleted
+  pinnedWarns <- Ghc.listToBag
+               . foldMap Ghc.bagToList
+             <$> Ghc.tcPluginIO (readMVar globalState)
+
+  Ghc.tcPluginIO . atomicModifyIORef' errsRef
+    $ \(warnings, errors) ->
+        ((Ghc.unionBags pinnedWarns warnings, errors), ())
+
+-- | Remove warnings for modules that no longer exist
+pruneDeleted :: Ghc.TcPluginM ()
+pruneDeleted = Ghc.tcPluginIO . modifyMVar_ globalState $ \warns -> do
+  let mods = M.keys warns
+
+  deletedMods <-
+    filterM (fmap not . Dir.doesFileExist . BS.unpack)
+            mods
+
+  pure $ foldl' (flip M.delete) warns deletedMods
+
+-- | After type checking a module, pin any warnings pertaining to it.
+insertModuleWarnings :: Ghc.ModSummary -> Ghc.TcGblEnv -> Ghc.TcM Ghc.TcGblEnv
+insertModuleWarnings modSummary tcGblEnv = do
+  lclErrsRef <- Ghc.tcl_errs . Ghc.env_lcl <$> Ghc.getEnv
+  (warns, _) <- liftIO $ readIORef lclErrsRef
+
+  let modFile = BS.pack $ Ghc.ms_hspp_file modSummary
+      onlyThisMod w =
+        case Ghc.errMsgSpan w of
+          Ghc.RealSrcSpan' span ->
+            Ghc.bytesFS' (Ghc.srcSpanFile span) == modFile
+          _ -> False
+
+      warnsForMod = Ghc.filterBag onlyThisMod warns
+
+  -- Replace any existing pinned warnings with new ones for this module
+  liftIO . modifyMVar_ globalState
+    $ pure . M.insert modFile warnsForMod
+
+  pure tcGblEnv
+
diff --git a/src/ShowWarnings.hs b/src/ShowWarnings.hs
new file mode 100644
--- /dev/null
+++ b/src/ShowWarnings.hs
@@ -0,0 +1,9 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+module ShowWarnings
+  ( ShowWarnings
+  , showWarnings
+  ) where
+
+class ShowWarnings where
+  showWarnings :: ()
