diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,8 @@
+# Changelog for ghc-clippy-plugin
+
+## 0.0.0.1
+
+Initial release:
+ - allows overriding warning and error messages emitted by ghc
+ - works with stack and ghcid
+ - configured using ./.clippy.dhall
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Author name here (c) 2020
+
+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 Author name here 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,189 @@
+# GHC Clippy Plugin
+
+[![Build status](https://img.shields.io/travis/ArturGajowy/ghc-clippy-plugin.svg?logo=travis)](https://travis-ci.org/ArturGajowy/ghc-clippy-plugin)
+[![License](https://img.shields.io/badge/license-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
+
+A helpful companion to GHC.
+
+Overrides GHC error and warning messages, to the user's liking.
+
+Configured using (how else!) regexps. Tested with stack and ghcid.
+
+## Showcase
+
+<img align="right" width="430" src="https://i.imgur.com/Beay7p4.png">
+<img width="430" src="https://i.imgur.com/CsYulBQ.png">
+
+Left: without Clippy.
+
+Right: with Clippy, using the sample config.
+
+## But, why?
+
+For all kinds of reasons:
+ - making GHC messages more terse or more verbose
+ - adding more context for beginners
+ - stripping confusing / duplicated / rarely useful context for everyone
+ - prototyping improvements for ghc error output
+ - ever wanted GHC to speak in emoji? :smiling_imp:
+ - ... or in your mother tongue?
+ - ... or in mathematical notation?
+
+## Usage
+
+1. Add the `ghc-clippy-plugin` dependency to your project
+2. Pass `-fplugin=Clippy` in GHC options
+
+E.g. for stack:
+
+```yaml
+# file: package.yaml
+dependencies:
+ - ghc-clippy-plugin
+
+ghc-options:
+- -fplugin=Clippy
+```
+
+3. When building, you should see the following warning:
+
+```
+ghc-clippy-plugin: warning:
+    Clippy plugin couldn't start. Cause:
+    ./.clippy.dhall: openFile: does not exist (No such file or directory)
+```
+
+Make sure there was anything to compile (change a .hs file) if the warning wasn't there.
+
+Save the [sample config](/.clippy-terse.dhall) as `.clippy.dhall` in the project's root dir
+(or - more precisely - the `current directory` GHC is going to use)
+
+4. Put an error somewhere:
+
+```haskell
+oops = print mempty
+```
+
+With the sample config, this should output:
+
+```
+./app/Main.hs:19:14-19: error:
+    Type variable ‘a0’ is ambiguous in ‘mempty’.
+    Can't pick an instance for ‘(Monoid a0)’.
+    ---
+    Maybe-fix: add type annotations to disambiguate.
+    More info: compile with -fprint-potential-instances.
+   |
+19 | oops = print mempty
+   |
+```
+
+5. Enjoy the much terser output and tweak it to your heart's content! :grin:
+
+
+## Tweaking the config
+
+### Stack
+
+For `--file-watch` to pick up config changes, add in `package.yaml`:
+```yaml
+extra-source-files:
+- .clippy.dhall
+```
+
+Use `stack build --file-watch`. Make sure to have some errors handy! :)
+
+### Ghcid
+
+For ghcid to reload after config change, run it with `--reload=.clippy.dhall`.
+I tend to put the above line in `./.ghcid`.
+
+Ghcid may terminate if there are compile errors on startup.
+If that's the case, remove your errors until ghcid starts successfully :)
+
+With the above, for error messages, ghcid picks up `.clippy.dhall` changes immediately.
+
+For warnings, one needs to trigger recompilation of the file triggering them.
+One way around that is to enable `-Werror` for the period of config tweaking.
+(I put mine in `./.ghci`.)
+
+### Error message structure, section markers
+
+In GHC, each error/warning message contains 3 sections in its [ErrDoc](https://hackage.haskell.org/package/ghc-8.10.1/docs/src/ErrUtils.html#ErrDoc)
+('Important', 'Context', and 'Supplementary'). Each section contains a list of `MsgDoc`-s.
+
+Before replacing the message text, Clippy wraps each section, and each of their `MsgDoc`-s, with
+markers. This allows for more precise match targeting, including MsgDoc/section ends.
+
+To see the structure of the replaced error message, comment out the `marker removing rule` in the
+config.
+
+```dhall
+  rules =
+    [ -- rule "(>>[ICS]>)|(<[ICS]<<)|(>[ICS]>)|(<[ICS]<)" ""
+    , rule "..." "..."
+    -- ...
+    ]
+```
+
+Comment out all rules to see the structure of the original message. Sample result:
+
+```
+./app/Main.hs:27:11: error:
+    • >>I>
+      >I>
+      No instance for (Num a) arising from a use of ‘+’
+      Possible fix:
+        add (Num a) to the context of
+          the type signature for:
+            bar :: forall a. a -> a -> a
+      <I<
+      <I<<
+    • >>C>
+      >C>
+      In the expression: a + b
+      In an equation for ‘bar’: bar a b = a + b
+      <C<
+      <C<<
+    • >>S>
+      <S<<
+   |
+27 | bar a b = a + b
+   |
+```
+
+Replace rules can span across multiple messages in a section, but can't cross section boundaries.
+For example, the following rule will remove the entire 'Context' section:
+
+```dhall
+  rule "(?s)>>C>.*?<C<<" "" -- notice the (?s) - "dot-all" regex flag
+```
+
+All-whitespace lines are removed from all the messages.
+
+### Rule matching order
+
+Replacement rules are applied in reverse order of the `rules` list in config.
+This means the most generic and least selective rules should go at the top of the file.
+In particular, the `marker removing rule` - which should be applied last - should be
+the first rule in every config.
+
+
+## Acknowledgements
+
+I'd like to thank the authors of
+ - the [Scala Clippy Plugin](https://scala-clippy.org/) for the name and the idea
+ - [Error Messages in Haskell, and how to Improve
+   them](https://anthony.noided.media/blog/haskell/programming/2020/05/14/haskell-errors.html) for
+   inspiration and some initial test cases
+ - [Understanding Basic Haskell Error
+Messages](http://ics.p.lodz.pl/~stolarek/_media/pl:research:stolarek_understanding_basic_haskell_error_messages.pdf)
+   for test cases as well
+ - the wonderful folk working on GHC, for making this possible :heart:
+
+## Roadmap
+
+- resolve the config from the first directory above that contains `.clippy.dhall`
+- fall back to `~/.config/clippy.dhall` and then to `~/.clippy.dhall`
+- cache the config instead of re-parsing for every module
+- ...?
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/ghc-clippy-plugin.cabal b/ghc-clippy-plugin.cabal
new file mode 100644
--- /dev/null
+++ b/ghc-clippy-plugin.cabal
@@ -0,0 +1,45 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.33.0.
+--
+-- see: https://github.com/sol/hpack
+--
+-- hash: ecc0ca90c321825f155dc23884f20f462b01edc11978e490cca555c329e8fde5
+
+name:           ghc-clippy-plugin
+version:        0.0.0.1
+synopsis:       Override GHC error messages to the user's liking
+description:    Please see the README on GitHub at <https://github.com/ArturGajowy/ghc-clippy-plugin#readme>
+category:       Development, Compiler Plugin
+homepage:       https://github.com/ArturGajowy/ghc-clippy-plugin#readme
+bug-reports:    https://github.com/ArturGajowy/ghc-clippy-plugin/issues
+author:         Artur Gajowy
+maintainer:     artur.gajowy@gmail.com
+copyright:      2020 Artur Gajowy
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    ChangeLog.md
+
+source-repository head
+  type: git
+  location: https://github.com/ArturGajowy/ghc-clippy-plugin
+
+library
+  exposed-modules:
+      Clippy
+  other-modules:
+      Paths_ghc_clippy_plugin
+  hs-source-dirs:
+      src
+  ghc-options: -fobject-code -Wall
+  build-depends:
+      base >=4.7 && <5
+    , dhall >=1.30.0 && <1.33
+    , ghc >=8.8.2 && <8.11
+    , text >=1.2.3.2 && <1.3
+    , text-icu >=0.7.0 && <0.8
+    , text-regex-replace >=0.1.1 && <0.2
+  default-language: Haskell2010
diff --git a/src/Clippy.hs b/src/Clippy.hs
new file mode 100644
--- /dev/null
+++ b/src/Clippy.hs
@@ -0,0 +1,108 @@
+{-# LANGUAGE DeriveGeneric     #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeApplications  #-}
+
+module Clippy
+  ( plugin
+  )
+where
+
+import           Bag
+import           Control.Exception     (SomeException, tryJust)
+import           Data.Bifunctor
+import           Data.Function
+import           Data.IORef
+import           Data.String           (fromString)
+import qualified Data.Text             as T
+import           Data.Text.ICU         (regex)
+import           Data.Text.ICU.Replace (replaceAll)
+import           Dhall
+import           ErrUtils
+import           GhcPlugins            hiding (Rule, (<>))
+import           Prelude               hiding (print)
+import           TcPluginM
+import           TcRnTypes
+
+plugin :: Plugin
+plugin =
+  defaultPlugin
+  { tcPlugin = const $ Just $
+      TcPlugin
+      { tcPluginInit  = pure ()
+      , tcPluginSolve = \_ _ _ _ -> pure $ TcPluginOk [] []
+      , tcPluginStop  = const $ loadConfig >>= either cantInitializeWarning replaceMessages
+      }
+  , pluginRecompile = purePlugin
+  }
+
+newtype Config = Config { rules :: [Rule] } deriving Generic
+instance FromDhall Config
+
+data Rule = Rule
+    { match :: Text
+    , print :: Text
+    }
+    deriving Generic
+instance FromDhall Rule
+
+data PEnv = PEnv
+    { showMsgDoc :: MsgDoc -> Text
+    , config     :: Config
+    }
+
+loadConfig :: TcPluginM (Either String Config)
+loadConfig = tcPluginIO
+  . tryJust (Just . show @SomeException)
+  $ inputFile auto "./.clippy.dhall"
+
+cantInitializeWarning :: String -> TcPluginM ()
+cantInitializeWarning cause = do
+  dynFlags <- hsc_dflags <$> getTopEnv
+  let warning = mkPlainWarnMsg dynFlags _span msgDoc
+      _span   = mkGeneralSrcSpan $ mkFastString "ghc-clippy-plugin"
+      msgDoc  = text "Clippy plugin couldn't start. Cause:" $$ text cause
+  tcPluginIO $ printOrThrowWarnings dynFlags $ unitBag warning
+
+replaceMessages :: Config -> TcPluginM ()
+replaceMessages conf = do
+  errsRef  <- tcl_errs . snd <$> getEnvs
+  dynFlags <- hsc_dflags <$> getTopEnv
+  let _showMsgDoc = T.pack . showSDoc dynFlags
+      replaceErrMsgs = fmap $ replaceErrMsgDoc $ PEnv _showMsgDoc conf
+  tcPluginIO $ modifyIORef errsRef (bimap replaceErrMsgs replaceErrMsgs)
+
+replaceErrMsgDoc :: PEnv -> ErrMsg -> ErrMsg
+replaceErrMsgDoc env e = e { errMsgDoc = replaceMsgDocs env (errMsgDoc e) }
+
+replaceMsgDocs :: PEnv -> ErrDoc -> ErrDoc
+replaceMsgDocs env e = e
+  { errDocImportant     = replaceMsgDocsGroup env "I" (errDocImportant e)
+  , errDocContext       = replaceMsgDocsGroup env "C" (errDocContext e)
+  , errDocSupplementary = replaceMsgDocsGroup env "S" (errDocSupplementary e)
+  }
+
+replaceMsgDocsGroup :: PEnv -> Text -> [MsgDoc] -> [MsgDoc]
+replaceMsgDocsGroup env label msgDocs = text . T.unpack <$> filtered
+ where
+  filtered = filter (not . T.null . T.strip) (T.lines replaced)
+  replaced = replaceText env wrapped
+  wrapped  = (env & showMsgDoc) . vcat $ wrapGroup label msgDocs
+
+replaceText :: PEnv -> Text -> Text
+replaceText env t = foldr replaceRule t (rules . config $ env)
+
+replaceRule :: Rule -> Text -> Text
+replaceRule rule = replaceAll (regex [] $ rule & match) (fromString . T.unpack $ rule & print)
+
+wrapGroup :: Text -> [MsgDoc] -> [MsgDoc]
+wrapGroup label group =
+  [open $ ">" <> label] ++ (wrapDoc label <$> group) ++ [close $ label <> "<"]
+
+wrapDoc :: Text -> MsgDoc -> MsgDoc
+wrapDoc label doc = vcat [open label, doc, close label]
+
+open :: Text -> MsgDoc
+open label = text . T.unpack $ ">" <> label <> ">"
+
+close :: Text -> MsgDoc
+close label = text . T.unpack $ "<" <> label <> "<"
