diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,8 @@
+# Changelog for polysemy-plugin
+
+## 0.1.0.0 (2019-04-27)
+
+- Initial release
+
+
+## Unreleased changes
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Sandy Maguire (c) 2019
+
+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 Sandy Maguire 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,77 @@
+# polysemy-plugin
+
+[![Build Status](https://api.travis-ci.org/isovector/polysemy.svg?branch=master)](https://travis-ci.org/isovector/polysemy)
+[![Hackage](https://img.shields.io/hackage/v/polysemy-plugin.svg?logo=haskell)](https://hackage.haskell.org/package/polysemy-plugin)
+
+## Dedication
+
+> It doesn't matter how beautiful your theory is, it doesn't matter how smart
+> you are. If it doesn't agree with experiment, it's wrong.
+>
+> Richard Feynman
+
+
+## Overview
+
+A typechecker plugin that can disambiguate "obvious" uses of effects in
+[`polysemy`](https://hackage.haskell.org/package/polysemy).
+
+
+## Example
+
+Consider the following program:
+
+```haskell
+foo :: Member (State Int) r => Sem r ()
+foo = put 10
+```
+
+What does this program do? Any human will tell you that it changes the state of
+the `Int` to 10, which is clearly what's meant.
+
+Unfortunately, `polysemy` can't work this out on its own. Its reasoning is
+"maybe you wanted to change some other `State` effect which is *also* a `Num`,
+but you just forgot to add a `Member` constraint for it."
+
+This is obviously insane, but it's the way the cookie crumbles.
+`polysemy-plugin` is a typechecker plugin which will disambiguate the above
+program (and others) so the compiler will do what you want.
+
+
+## Usage
+
+Add the following line to your package configuration:
+
+```
+ghc-options: -fplugin=Polysemy.Plugin
+```
+
+
+## Limitations
+
+The `polysemy-plugin` will only disambiguate effects if there is exactly one
+relevant constraint in scope. For example, it will *not* disambiguate the
+following program:
+
+```haskell
+bar :: Members '[ State Int
+                , State Double
+                ] r => Sem r ()
+bar = put 10
+```
+
+because it is now unclear whether you're attempting to set the `Int` or the
+`Double`. Instead, you can manually write a type application in this case.
+
+```haskell
+bar :: Members '[ State Int
+                , State Double
+                ] r => Sem r ()
+bar = put @Int 10
+```
+
+
+## Acknowledgments
+
+This plugin is copied almost verbatim from [`simple-effects`](https://hackage.haskell.org/package/simple-effects).
+
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/polysemy-plugin.cabal b/polysemy-plugin.cabal
new file mode 100644
--- /dev/null
+++ b/polysemy-plugin.cabal
@@ -0,0 +1,59 @@
+cabal-version: 1.12
+name: polysemy-plugin
+version: 0.1.0.0
+license: BSD3
+license-file: LICENSE
+copyright: 2019 Sandy Maguire
+maintainer: sandy@sandymaguire.me
+author: Sandy Maguire
+homepage: https://github.com/isovector/polysemy#readme
+bug-reports: https://github.com/isovector/polysemy/issues
+synopsis: Disambiguate obvious uses of effects.
+description:
+    Please see the README on GitHub at <https://github.com/isovector/polysemy/tree/master/polysemy-plugin#readme>
+category: Polysemy
+build-type: Simple
+extra-source-files:
+    README.md
+    ChangeLog.md
+
+source-repository head
+    type: git
+    location: https://github.com/isovector/polysemy
+
+library
+    exposed-modules:
+        Polysemy.Plugin
+    hs-source-dirs: src
+    other-modules:
+        Paths_polysemy_plugin
+    default-language: Haskell2010
+    default-extensions: DataKinds DeriveFunctor FlexibleContexts GADTs
+                        LambdaCase PolyKinds RankNTypes ScopedTypeVariables
+                        StandaloneDeriving TypeApplications TypeOperators TypeFamilies
+                        UnicodeSyntax
+    build-depends:
+        base >=4.7 && <5,
+        ghc >=8.6.3 && <8.7,
+        ghc-tcplugins-extra ==0.3.*,
+        polysemy >=0.1.2.0 && <0.2
+
+test-suite polysemy-plugin-test
+    type: exitcode-stdio-1.0
+    main-is: Spec.hs
+    hs-source-dirs: test
+    other-modules:
+        Paths_polysemy_plugin
+    default-language: Haskell2010
+    default-extensions: DataKinds DeriveFunctor FlexibleContexts GADTs
+                        LambdaCase PolyKinds RankNTypes ScopedTypeVariables
+                        StandaloneDeriving TypeApplications TypeOperators TypeFamilies
+                        UnicodeSyntax
+    ghc-options: -threaded -rtsopts -with-rtsopts=-N
+    build-depends:
+        base >=4.7 && <5,
+        ghc >=8.6.3 && <8.7,
+        ghc-tcplugins-extra ==0.3.*,
+        hspec >=2.6.0 && <2.7,
+        polysemy >=0.1.2.0 && <0.2,
+        polysemy-plugin -any
diff --git a/src/Polysemy/Plugin.hs b/src/Polysemy/Plugin.hs
new file mode 100644
--- /dev/null
+++ b/src/Polysemy/Plugin.hs
@@ -0,0 +1,182 @@
+{-# LANGUAGE NoMonomorphismRestriction #-}
+{-# LANGUAGE CPP #-}
+
+------------------------------------------------------------------------------
+-- The MIT License (MIT)
+--
+-- Copyright (c) 2017 Luka Horvat
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a copy
+-- of this software and associated documentation files (the "Software"), to
+-- deal in the Software without restriction, including without limitation the
+-- rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+-- sell copies of the Software, and to permit persons to whom the Software is
+-- furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+-- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+-- IN THE SOFTWARE.
+--
+------------------------------------------------------------------------------
+--
+-- This module is heavily based on 'Control.Effects.Plugin' from the
+-- 'simple-effects' package, originally by Luka Horvat.
+--
+-- https://gitlab.com/LukaHorvat/simple-effects/commit/966ce80b8b5777a4bd8f87ffd443f5fa80cc8845#f51c1641c95dfaa4827f641013f8017e8cd02aab
+
+
+------------------------------------------------------------------------------
+-- | A typechecker plugin that can disambiguate "obvious" uses of effects in
+-- Polysemy.
+--
+-- __Example:__
+--
+-- Consider the following program:
+--
+-- @
+-- foo :: 'Polysemy.Member' ('Polysemy.State.State' Int) r => 'Polysemy.Sem' r ()
+-- foo = 'Polysemy.State.put' 10
+-- @
+--
+-- What does this program do? Any human will tell you that it changes the state
+-- of the 'Int' to 10, which is clearly what's meant.
+--
+-- Unfortunately, Polysemy can't work this out on its own. Its reasoning is
+-- "maybe you wanted to change some other 'Polysemy.State.State' effect which
+-- is /also/ a 'Num', but you just forgot to add a 'Polysemy.Member' constraint
+-- for it."
+--
+-- This is obviously insane, but it's the way the cookie crumbles.
+-- 'Polysemy.Plugin' is a typechecker plugin which will disambiguate the above
+-- program (and others) so the compiler will do what you want.
+--
+-- __Usage:__
+--
+-- Add the following line to your package configuration:
+--
+-- @
+-- ghc-options: -fplugin=Polysemy.Plugin
+-- @
+--
+-- __Limitations:__
+--
+-- The 'Polysemy.Plugin' will only disambiguate effects if there is exactly one
+-- relevant constraint in scope. For example, it will /not/ disambiguate the
+-- following program:
+--
+-- @
+-- bar :: 'Polysemy.Members' \'[ 'Polysemy.State.State' Int
+--                 , 'Polysemy.State.State' Double
+--                 ] r => 'Polysemy.Sem' r ()
+-- bar = 'Polysemy.State.put' 10
+-- @
+--
+-- because it is now unclear whether you're attempting to set the 'Int' or the
+-- 'Double'. Instead, you can manually write a type application in this case.
+--
+-- @
+-- bar :: 'Polysemy.Members' \'[ 'Polysemy.State.State' Int
+--                 , 'Polysemy.State.State' Double
+--                 ] r => 'Polysemy.Sem' r ()
+-- bar = 'Polysemy.State.put' @Int 10
+-- @
+--
+module Polysemy.Plugin
+  ( plugin
+  ) where
+
+-- external
+import GHC.TcPluginM.Extra (lookupModule, lookupName)
+
+-- GHC API
+import FastString (fsLit)
+import Module     (mkModuleName)
+import OccName    (mkTcOcc)
+import Plugins    (Plugin (..), defaultPlugin
+#if __GLASGOW_HASKELL__ >= 806
+    , PluginRecompile(..)
+#endif
+    )
+import TcPluginM  (TcPluginM, tcLookupClass)
+import TcRnTypes
+import TyCoRep    (Type (..))
+import Control.Monad
+import Class
+import Type
+import Data.Maybe
+import TcSMonad hiding (tcLookupClass)
+import CoAxiom
+import Outputable
+
+
+plugin :: Plugin
+plugin = defaultPlugin
+    { tcPlugin = const (Just fundepPlugin)
+#if __GLASGOW_HASKELL__ >= 806
+    , pluginRecompile = const (return NoForceRecompile)
+#endif
+    }
+
+fundepPlugin :: TcPlugin
+fundepPlugin = TcPlugin
+    { tcPluginInit = do
+        md <- lookupModule (mkModuleName "Polysemy.Internal.Union") (fsLit "polysemy")
+        monadEffectTcNm <- lookupName md (mkTcOcc "Find")
+        tcLookupClass monadEffectTcNm
+    , tcPluginSolve = solveFundep
+    , tcPluginStop = const (return ()) }
+
+allMonadEffectConstraints :: Class -> [Ct] -> [(CtLoc, (Type, Type, Type))]
+allMonadEffectConstraints cls cts =
+    [ (ctLoc cd, (effName, eff, r))
+        | cd@CDictCan{cc_class = cls', cc_tyargs = [_, r, eff]} <- cts
+        , cls == cls'
+        , let effName = getEffName eff
+              ]
+
+singleListToJust :: [a] -> Maybe a
+singleListToJust [a] = Just a
+singleListToJust _ = Nothing
+
+findMatchingEffectIfSingular :: (Type, Type, Type) -> [(Type, Type, Type)] -> Maybe Type
+findMatchingEffectIfSingular (effName, _, mon) ts = singleListToJust
+    [ eff'
+        | (effName', eff', mon') <- ts
+        , eqType effName effName'
+        , eqType mon mon' ]
+
+getEffName :: Type -> Type
+getEffName t = fst $ splitAppTys t
+
+
+mkWanted :: CtLoc -> Type -> Type -> TcPluginM (Maybe Ct)
+mkWanted loc eff eff' = do
+  if eqType (getEffName eff) (getEffName eff')
+     then do
+       (ev, _) <- unsafeTcPluginTcM $ runTcSDeriveds $ newWantedEq loc Nominal eff eff'
+       pure $ Just (CNonCanonical ev)
+     else
+       pure Nothing
+
+
+solveFundep :: Class -> [Ct] -> [Ct] -> [Ct] -> TcPluginM TcPluginResult
+solveFundep effCls giv _ want = do
+    let wantedEffs = allMonadEffectConstraints effCls want
+    let givenEffs = snd <$> allMonadEffectConstraints effCls giv
+    eqs <- forM wantedEffs $ \(loc, e@(_, eff, r)) ->
+      case findMatchingEffectIfSingular e givenEffs of
+        Nothing -> do
+          case splitAppTys r of
+            (_, [_, eff', _]) -> mkWanted loc eff eff'
+            _                 -> pure Nothing
+        Just eff' -> mkWanted loc eff eff'
+
+    return (TcPluginOk [] (catMaybes eqs))
+
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,134 @@
+{-# LANGUAGE AllowAmbiguousTypes        #-}
+{-# LANGUAGE BlockArguments             #-}
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE OverloadedStrings          #-}
+
+{-# OPTIONS_GHC -fplugin=Polysemy.Plugin #-}
+
+module Main where
+
+import Data.Functor.Identity
+import GHC.Exts
+import Polysemy
+import Polysemy.Error
+import Polysemy.State
+import Test.Hspec
+
+
+
+idState :: Member (State s) r => Sem r ()
+idState = do
+  s <- get
+  put s
+
+intState :: Member (State Int) r => Sem r ()
+intState = put 10
+
+numState :: Num a => Member (State a) r => Sem r ()
+numState = put 10
+
+strState :: Member (State String) r => Sem r ()
+strState = put "hello"
+
+oStrState :: IsString a => Member (State a) r => Sem r ()
+oStrState = put "hello"
+
+
+err :: Member (Error e) r => Sem r Bool
+err =
+  catch
+    do
+      throw undefined
+    \_ -> pure True
+
+
+errState :: Num s => Members '[Error e, State s] r => Sem r Bool
+errState = do
+  numState
+  err
+
+
+lifted :: Monad m => Member (Lift m) r => Sem r ()
+lifted = sendM $ pure ()
+
+
+newtype MyString = MyString String
+  deriving (IsString, Eq, Show)
+
+
+main :: IO ()
+main = hspec $ do
+  describe "State effect" $ do
+    describe "get/put" $ do
+      it "should work in simple cases" $ do
+        flipShouldBe (True, ()) . run $ runState True idState
+
+      it "should, when polymorphic, eliminate the first matching effect" $ do
+        flipShouldBe (False, (True, ()))   . run $ runState False $ runState True idState
+
+      it "should, when polymorphic, not eliminate unmatching effects" $ do
+        flipShouldBe (True, Right @Int ()) . run $ runState True $ runError idState
+
+    describe "numbers" $ do
+      it "should interpret against concrete Int" $ do
+        flipShouldBe (10, ()) . run $ runState 0 intState
+
+      describe "polymorphic Num constraint" $ do
+        it "should interpret against Int" $ do
+          flipShouldBe (10 :: Int, ())     . run $ runState 0 numState
+
+        it "should interpret against Float" $ do
+          flipShouldBe (10 :: Float, ())   . run $ runState 0 numState
+
+        it "should interpret against Double" $ do
+          flipShouldBe (10 :: Double, ())  . run $ runState 0 numState
+
+        it "should interpret against Integer" $ do
+          flipShouldBe (10 :: Integer, ()) . run $ runState 0 numState
+
+    describe "strings" $ do
+      it "concrete interpret against concrete String" $ do
+        flipShouldBe ("hello", ()) . run $ runState "nothing" strState
+
+      describe "polymorphic IsString constraint" $ do
+        it "should interpret against String" $ do
+          flipShouldBe ("hello" :: String, ())   . run $ runState "nothing" oStrState
+
+        it "should interpret against MyString" $ do
+          flipShouldBe ("hello" :: MyString, ()) . run $ runState "nothing" oStrState
+
+
+  describe "Error effect" $ do
+    it "should interpret against Int" $ do
+      flipShouldBe (Right @Int True)  . run $ runError err
+    it "should interpret against Bool" $ do
+      flipShouldBe (Right @Bool True) . run $ runError err
+
+
+  describe "State/Error effect" $ do
+    it "should interpret against Int/String" $ do
+      flipShouldBe (10 :: Int, Right @String True)  . run $ runState 0 $ runError errState
+    it "should interpret against Float/Bool" $ do
+      flipShouldBe (10 :: Float, Right @Bool True)  . run $ runState 0 $ runError errState
+
+
+  describe "Error/State effect" $ do
+    it "should interpret against String/Int" $ do
+      flipShouldBe (Right @String (10 :: Int, True))  . run $ runError $ runState 0 errState
+    it "should interpret against Bool/Float" $ do
+      flipShouldBe (Right @Bool (10 :: Float, True))  . run $ runError $ runState 0 errState
+
+
+  describe "Lift effect" $ do
+    it "should interpret against IO" $ do
+      res <- runM lifted
+      res `shouldBe` ()
+
+    it "should interpret against Identity" $ do
+      let res = runM lifted
+      res `shouldBe` Identity ()
+
+
+flipShouldBe :: (Show a, Eq a) => a -> a -> Expectation
+flipShouldBe = flip shouldBe
+
