diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,11 @@
+# Changelog for `core-of-name`
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to the
+[Haskell Package Versioning Policy](https://pvp.haskell.org/).
+
+## Unreleased
+
+## 0.1.0.0 - YYYY-MM-DD
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2026 Author name here
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1.  Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+
+2.  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.
+
+3.  Neither the name of the copyright holder nor the names of its 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 HOLDER 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,71 @@
+# core-of-name
+
+[![CI](https://github.com/ocramz/ghc-plugin-core-of-name/actions/workflows/ci.yml/badge.svg)](https://github.com/ocramz/ghc-plugin-core-of-name/actions/workflows/ci.yml)
+
+GHC plugin that prints the Core intermediate representation of annotated Haskell bindings during compilation.
+
+Based on the technique described in
+[Finding the Core of an expression using Template Haskell and a custom GHC Core plugin](https://ocramz.github.io/posts/2021-06-22-finding-core-th.html), which in turn was inspired by [`inspection-testing`](https://hackage.haskell.org/package/inspection-testing)
+
+## Usage
+
+In the module whose bindings you want to inspect, enable `TemplateHaskell` and the plugin, then call `coreOf` or `coreOfWith` after the binding:
+
+### `coreOf` — print Core to stdout
+
+```haskell
+{-# LANGUAGE TemplateHaskell #-}
+{-# OPTIONS_GHC -fplugin=CoreOfName.Plugin #-}
+module MyModule where
+
+import CoreOfName.Types (coreOf)
+
+f :: Double -> Double -> Double
+f = \x y -> sqrt x + y
+
+coreOf 'f
+```
+
+The Core representation of `f` will be printed interleaved with the normal compiler output during `stack build` / `cabal build`.
+
+### `coreOfWith` — write Core to a file
+
+Use `coreOfWith` to direct the output to a file instead of stdout with this shorthand:
+
+```haskell
+{-# LANGUAGE TemplateHaskell #-}
+{-# language OverloadedStrings #-}
+{-# OPTIONS_GHC -fplugin=CoreOfName.Plugin #-}
+module MyModule where
+
+import CoreOfName.Types (coreOfWith)
+
+f :: Double -> Double -> Double
+f = \x y -> sqrt x + y
+
+coreOfWith "output.core" 'f
+```
+
+NB if two or more declarations use the same output file, the file will be _overwritten_. It is best to assign one Core output file per function.
+
+The string literal is an `Options` value via the `IsString` instance and is equivalent to `OToFile "output.core"`.
+
+### Explicit `Options`
+
+You can also pass `Options` values directly:
+
+```haskell
+import CoreOfName.Types (coreOfWith, Options(..))
+
+-- print to stdout (same as coreOf)
+coreOfWith OPrintCore 'f
+
+-- write to a file
+coreOfWith (OToFile "output.core") 'f
+```
+
+## Building
+
+```
+stack clean && stack build
+```
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/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,23 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# language OverloadedStrings #-}
+{-# OPTIONS_GHC -fplugin=CoreOfName.Plugin #-}
+module Main (main) where
+
+import CoreOfName.Types (coreOf, coreOfWith, Options(..))
+
+g :: Double -> Double
+g = \x -> x + 1
+-- {-# NOINLINE g #-}
+
+coreOfWith "g.core" 'g
+
+-- Try building with either type signature for different Core output:
+f :: Double -> Double -> Double   -- gives unboxed primops
+-- f :: Floating a => a -> a -> a    -- gives dictionary-passing style
+f = \x y -> g x + y
+
+coreOfWith "f.core" 'f
+-- coreOf 'f
+
+main :: IO ()
+main = putStrLn "Plugin ran at compile time — see build output for Core of 'f'"
diff --git a/core-of-name.cabal b/core-of-name.cabal
new file mode 100644
--- /dev/null
+++ b/core-of-name.cabal
@@ -0,0 +1,47 @@
+cabal-version:       2.2
+
+name:                core-of-name
+version:             0.1.0.0
+synopsis:            Print the Core representation of a binding with a GHC plugin.
+description:         GHC plugin that prints the Core representation of a given binding, either to the console or to a file. Useful for debugging and learning about GHC's Core language.
+homepage:            https://github.com/ocramz/ghc-plugin-core-of-name
+license:             BSD-3-Clause
+license-file:        LICENSE
+author:              Marco Zocca
+maintainer:          ocramz
+copyright:           2026 Marco Zocca
+category:            Development
+build-type:          Simple
+tested-with:          GHC == 9.10.3
+extra-source-files:  README.md
+extra-doc-files:    CHANGELOG.md
+
+library
+  default-language:    Haskell2010
+  hs-source-dirs:      src
+  exposed-modules:     CoreOfName.Types
+                     , CoreOfName.Plugin
+  build-depends:       base >= 4.7 && < 5
+                     , ghc >= 9.8 && < 9.14
+                     , template-haskell >= 2.15 && < 2.23
+  ghc-options:         -Wall
+                       -Wcompat
+                       -Widentities
+                       -Wincomplete-record-updates
+                       -Wincomplete-uni-patterns
+                       -Wmissing-home-modules
+                       -Wpartial-fields
+                       -Wredundant-constraints
+
+executable core-of-name
+  default-language:    Haskell2010
+  hs-source-dirs:      app
+  main-is:             Main.hs
+  build-depends:       base
+                     , core-of-name
+  ghc-options:         -Wall
+
+
+source-repository head
+  type:     git
+  location: https://github.com/ocramz/ghc-plugin-core-of-name
diff --git a/src/CoreOfName/Plugin.hs b/src/CoreOfName/Plugin.hs
new file mode 100644
--- /dev/null
+++ b/src/CoreOfName/Plugin.hs
@@ -0,0 +1,93 @@
+{-# LANGUAGE LambdaCase #-}
+module CoreOfName.Plugin
+  ( plugin
+  ) where
+
+import Control.Monad.IO.Class (liftIO)
+import Data.Foldable (traverse_)
+
+import GHC.Plugins
+  ( Plugin(..), defaultPlugin, CommandLineOption
+  , fromSerialized, deserializeWithData
+  , ModGuts(..), Name, CoreExpr, Var
+  , flattenBinds, getName, thNameToGhcName
+  , PluginRecompile(..)
+  )
+import GHC.Core.Opt.Monad (CoreM, putMsgS, getDynFlags)
+import GHC.Core.Opt.Pipeline.Types (CoreToDo(..))
+import GHC.Driver.Ppr (showSDoc)
+import GHC.Types.Annotations (Annotation(..))
+import GHC.Utils.Outputable (ppr)
+import qualified Language.Haskell.TH.Syntax as TH (Name)
+
+import CoreOfName.Types (Target(..), Options(..))
+
+-- | The GHC plugin entry point. Appends a Core-to-Core pass that
+--   finds annotated bindings and prints their Core representation.
+plugin :: Plugin
+plugin = defaultPlugin
+  { installCoreToDos = install
+  , pluginRecompile  = \_ -> pure NoForceRecompile
+  }
+
+-- | Append our plugin pass at the end of the Core pipeline
+install :: [CommandLineOption] -> [CoreToDo] -> CoreM [CoreToDo]
+install cliOpts todos = pure (todos ++ [pass])
+  where
+    pass = CoreDoPluginPass "CoreOfName" $ \guts -> do
+      let (guts', targets) = extractTargets guts
+      liftIO $ putStrLn $ unwords ["CLI options:", show cliOpts, "| Found", show (length targets), "targets"]
+      traverse_ (printCore guts') targets
+      pure guts'
+
+
+-- | Pretty-print the Core representation of the binding with the
+--   given TH 'Name'.
+printCore :: ModGuts -> Target -> CoreM ()
+printCore guts target = do
+  let 
+    thn = tgName target
+    thOpts = tgOptions target
+  mn <- lookupTHName guts thn
+  case mn of
+    Just (_, coreExpr) -> do
+      dflags <- getDynFlags
+      case thOpts of
+        OPrintCore -> putMsgS $ showSDoc dflags (ppr coreExpr)
+        OToFile fp -> liftIO $ writeFile fp (showSDoc dflags (ppr coreExpr))
+    Nothing ->
+      putMsgS $ "CoreOfName: Cannot find name " ++ show thn
+
+-- | Resolve a TH 'Name' to a GHC 'Name' and look it up in the
+--   module's Core bindings.
+lookupTHName :: ModGuts -> TH.Name -> CoreM (Maybe (Var, CoreExpr))
+lookupTHName guts thn = thNameToGhcName thn >>= \case
+  Nothing -> do
+    putMsgS $ "CoreOfName: Could not resolve TH name " ++ show thn
+    pure Nothing
+  Just n -> pure $ lookupNameInGuts n
+  where
+    lookupNameInGuts :: Name -> Maybe (Var, CoreExpr)
+    lookupNameInGuts n =
+      case [ (v, e) | (v, e) <- flattenBinds (mg_binds guts), getName v == n ] of
+        (hit:_) -> Just hit
+        []      -> Nothing
+
+-- | Decode 'Target' annotations from the module guts, returning
+--   cleaned guts (annotations consumed) and the list of targets.
+extractTargets :: ModGuts -> (ModGuts, [Target])
+extractTargets guts = (guts', targets)
+  where
+    (anns_clean, targets) = partitionMaybe findTargetAnn (mg_anns guts)
+    guts' = guts { mg_anns = anns_clean }
+    findTargetAnn (Annotation _ payload) =
+      fromSerialized deserializeWithData payload
+
+-- | Partition a list using a function that returns 'Just' for
+--   elements to collect and 'Nothing' for elements to keep.
+partitionMaybe :: (a -> Maybe b) -> [a] -> ([a], [b])
+partitionMaybe _ [] = ([], [])
+partitionMaybe f (x:xs) = case f x of
+  Nothing -> (x : as, bs)
+  Just b  -> (as, b : bs)
+  where (as, bs) = partitionMaybe f xs
diff --git a/src/CoreOfName/Types.hs b/src/CoreOfName/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/CoreOfName/Types.hs
@@ -0,0 +1,50 @@
+{-# LANGUAGE DeriveDataTypeable #-}
+module CoreOfName.Types
+  ( Target(..)
+  , coreOf
+  , Options(..)
+  , defaultOptions
+  , coreOfWith
+  ) where
+
+import Data.Data (Data)
+import Data.String (IsString(..))
+import Language.Haskell.TH (Name, AnnTarget(..), Pragma(..), Dec(..), Q)
+import Language.Haskell.TH.Syntax (liftData)
+
+-- | Annotation payload carrying the TH 'Name' of a binding whose
+--   GHC Core representation we want to inspect.
+data Target = MkTarget { 
+  tgOptions :: Options
+, tgName :: Name
+} deriving (Data)
+
+data Options = OPrintCore 
+             | OToFile FilePath 
+             deriving (Data)
+instance IsString Options where
+  fromString s = OToFile s
+
+defaultOptions :: Options
+defaultOptions = OPrintCore
+
+-- | Template Haskell splice that attaches a module-level annotation
+--   carrying the given 'Name'. Usage (in client module):
+--
+-- @
+-- {-\# LANGUAGE TemplateHaskell \#-}
+-- f :: Double -> Double -> Double
+-- f = \\x y -> sqrt x + y
+--
+-- coreOf \'f
+-- @
+coreOf :: Name -> Q [Dec]
+coreOf = coreOfWith defaultOptions
+
+coreOfWith :: Options -> Name -> Q [Dec]
+coreOfWith opts n = do
+  annExpr <- liftData (MkTarget opts n)
+  pure [PragmaD (AnnP ModuleAnnotation annExpr)]
+
+
+
