diff --git a/LICENSE.markdown b/LICENSE.markdown
new file mode 100644
--- /dev/null
+++ b/LICENSE.markdown
@@ -0,0 +1,13 @@
+Copyright 2020 Taylor Fausak
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted, provided that the above
+copyright notice and this permission notice appear in all copies.
+
+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+PERFORMANCE OF THIS SOFTWARE.
diff --git a/README.markdown b/README.markdown
new file mode 100644
--- /dev/null
+++ b/README.markdown
@@ -0,0 +1,102 @@
+# Splint
+
+[![Travis CI](https://travis-ci.org/tfausak/splint.svg?branch=master)](https://travis-ci.org/tfausak/splint)
+[![Hackage](https://img.shields.io/hackage/v/splint)](https://hackage.haskell.org/package/splint)
+[![Stackage](https://www.stackage.org/package/splint/badge/nightly?label=stackage)](https://www.stackage.org/package/splint)
+
+Splint is a proof of concept, showing how to use [HLint 3][] as a [GHC source
+plugin][]. It is similar to [hlint-source-plugin][] by Ollie Charles, except
+that it doesn't have to re-parse the module in order to lint it.
+
+To use Splint, pass `-fplugin=Splint` to GHC. Any ideas suggested by HLint will
+be reported as warnings by GHC. For example, if you define `Main.hs` as:
+
+``` hs
+main = print . concat $ map pure [ 'a' .. 'z' ]
+```
+
+You would expect HLint to tell you to use `concatMap`. Normally you would need
+to both compile your module with GHC and lint it with HLint. However with
+Splint you can compile it and get suggestions from HLint all at once by
+running:
+
+``` sh
+ghc -fplugin=Splint Main.hs
+```
+
+Among all the usual output from GHC, you should see this new warning:
+
+```
+Main.hs:1:8: warning:
+    Use concatMap
+    Perhaps: print (concatMap pure ['a' .. 'z'])
+  |
+1 | main = print . concat $ map pure [ 'a' .. 'z' ]
+  |        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+```
+
+And that's all there is to it! HLint suggestions as part of your normal build
+process. What a time to be alive.
+
+If you want to pass arguments through to HLint, you can use
+`-fplugin-opt=Splint:arg`. For example you can ignore the warning above with
+`-fplugin-opt=Splint:'--ignore=Use concatMap'`.
+
+## Trade offs
+
+Running HLint as a GHC source plugin has some upsides:
+
+-   Modules are only linted when they're compiled, so if they haven't changed
+    they won't be linted again.
+
+-   HLint's suggestions are reported just like GHC's warnings. They're
+    formatted the same way and they're reported at the same time.
+
+-   Each module is only parsed once.
+
+-   Parsing is done by GHC instead of something like `haskell-src-exts`. HLint
+    already works like this, but by using a plugin you can be sure that all of
+    the versions and options line up correctly.
+
+However it's also got some downsides:
+
+-   Using Splint means adding it as a dependency to the targets you want to
+    lint. Normally HLint is either a test dependency or just installed on the
+    system.
+
+    You may be able to lessen the impact of this by providing a flag to control
+    linting. That way you can enable it locally and in CI, but not require
+    everything downstream of you to depend on HLint.
+
+    ``` cabal
+    flag lint
+      default: False
+      manual: True
+    library
+      if flag(lint)
+        build-depends: splint
+        ghc-options: -fplugin=Splint
+    ```
+
+-   It's slower. I've found that it adds about a tenth of a second per module.
+
+-   You can't use the automated refactorings that HLint provides.
+
+-   Using plugins marks every module as unsafe.
+
+## To do
+
+As stated, Splint is basically a tech demo. Although it's usable in its current
+form, there is some low hanging fruit to fix before it should be considered
+ready for production:
+
+- [ ] Work with versions of GHC older than 8.10.
+- [x] Accept command-line options using `-fplugin-opt=Splint:something`.
+- [x] Reliably read HLint configuration.
+- [x] Avoid re-reading HLint config for each source file.
+- [x] Figure out a good output format for the warnings.
+- [x] Publish to Hackage.
+
+[HLint 3]: https://neilmitchell.blogspot.com/2020/05/hlint-30.html
+[GHC source plugin]: https://downloads.haskell.org/~ghc/8.10.1/docs/html/users_guide/extending_ghc.html#source-plugins
+[hlint-source-plugin]: https://github.com/ocharles/hlint-source-plugin
diff --git a/splint.cabal b/splint.cabal
new file mode 100644
--- /dev/null
+++ b/splint.cabal
@@ -0,0 +1,27 @@
+name: splint
+version: 1.0.0.0
+
+build-type: Simple
+cabal-version: >= 1.10
+category: Development
+description: Splint is HLint as a GHC source plugin.
+extra-source-files: README.markdown
+license-file: LICENSE.markdown
+license: ISC
+maintainer: Taylor Fausak
+synopsis: HLint as a GHC source plugin.
+
+library
+  build-depends:
+    base >= 4.14.0 && < 4.15
+    , ghc >= 8.10.1 && < 8.11
+    , hlint >= 3.0 && < 3.2
+  default-language: Haskell2010
+  exposed-modules: Splint
+  ghc-options:
+    -Weverything
+    -Wno-implicit-prelude
+    -Wno-missing-safe-haskell-mode
+    -Wno-prepositive-qualified-module
+    -Wno-unsafe
+  hs-source-dirs: src/lib
diff --git a/src/lib/Splint.hs b/src/lib/Splint.hs
new file mode 100644
--- /dev/null
+++ b/src/lib/Splint.hs
@@ -0,0 +1,69 @@
+module Splint ( plugin ) where
+
+import qualified Bag as GHC
+import qualified Data.IORef as IORef
+import qualified ErrUtils as GHC
+import qualified GhcPlugins as GHC
+import qualified Language.Haskell.HLint as HLint
+import qualified System.IO.Unsafe as Unsafe
+
+plugin :: GHC.Plugin
+plugin = GHC.defaultPlugin
+  { GHC.parsedResultAction = action
+  , GHC.pluginRecompile = GHC.purePlugin
+  }
+
+action
+  :: [GHC.CommandLineOption]
+  -> GHC.ModSummary
+  -> GHC.HsParsedModule
+  -> GHC.Hsc GHC.HsParsedModule
+action commandLineOptions _modSummary hsParsedModule = do
+  dynFlags <- GHC.getDynFlags
+  GHC.liftIO $ do
+    (_parseFlags, classifies, hint) <- getSettings commandLineOptions
+    let
+      apiAnns = GHC.hpm_annotations hsParsedModule
+      hsModule = GHC.hpm_module hsParsedModule
+      moduleEx = HLint.createModuleEx apiAnns hsModule
+      ideas = HLint.applyHints classifies hint [moduleEx]
+    GHC.printOrThrowWarnings dynFlags
+      . GHC.listToBag
+      . fmap (ideaToWarnMsg dynFlags)
+      $ filter ((/= HLint.Ignore) . HLint.ideaSeverity) ideas
+  pure hsParsedModule
+
+type Settings = (HLint.ParseFlags, [HLint.Classify], HLint.Hint)
+
+getSettings :: [GHC.CommandLineOption] -> IO Settings
+getSettings commandLineOptions = do
+  maybeSettings <- IORef.readIORef settingsRef
+  case maybeSettings of
+    Just settings -> pure settings
+    Nothing -> do
+      settings <- HLint.argsSettings commandLineOptions
+      IORef.writeIORef settingsRef $ Just settings
+      pure settings
+
+{-# NOINLINE settingsRef #-}
+settingsRef :: IORef.IORef (Maybe Settings)
+settingsRef = Unsafe.unsafePerformIO $ IORef.newIORef Nothing
+
+ideaToWarnMsg :: GHC.DynFlags -> HLint.Idea -> GHC.WarnMsg
+ideaToWarnMsg dynFlags idea =
+  let
+    mkErrMsg = case HLint.ideaSeverity idea of
+      HLint.Error -> GHC.mkPlainErrMsg
+      _ -> GHC.mkPlainWarnMsg
+    srcSpan = HLint.ideaSpan idea
+    msgDoc = ideaToMsgDoc idea
+  in mkErrMsg dynFlags srcSpan msgDoc
+
+ideaToMsgDoc :: HLint.Idea -> GHC.MsgDoc
+ideaToMsgDoc idea = GHC.vcat
+  [ GHC.text $ HLint.ideaHint idea
+  , case HLint.ideaTo idea of
+    Just to | not $ null to -> GHC.text $ "Perhaps: " <> to
+    _ -> GHC.empty
+  , GHC.vcat . fmap (GHC.text . mappend "Note: " . show) $ HLint.ideaNote idea
+  ]
