diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,4 @@
+# Change log
+
+Heck follows the [Package Versioning Policy](https://pvp.haskell.org). You can
+find release notes [on GitHub](https://github.com/tfausak/heck/releases).
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,14 @@
+BSD Zero Clause License
+
+Copyright (c) 2025 Taylor Fausak
+
+Permission to use, copy, modify, and/or distribute this software for any
+purpose with or without fee is hereby granted.
+
+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.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,17 @@
+# Heck
+
+Heck is a Haskell library that provides a abstract unit test interface. It can
+be used to write tests without depending on any particular test library.
+
+## Example
+
+``` hs
+import Heck
+
+spec :: (Applicative m, Monad n) => Test m n -> n ()
+spec t = describe t "something" $ do
+  it t "works" $ do
+    assertEq t 'b' (max 'a' 'b')
+  it t "fails" $ do
+    assertEq t "expected" "actual"
+```
diff --git a/heck.cabal b/heck.cabal
new file mode 100644
--- /dev/null
+++ b/heck.cabal
@@ -0,0 +1,40 @@
+cabal-version: 3.0
+name: heck
+version: 0.2025.5.22
+synopsis: Abstract unit test interface
+description: Heck provides an abstract unit test interface.
+category: Testing
+extra-doc-files:
+  CHANGELOG.md
+  README.md
+
+license: 0BSD
+license-file: LICENSE.txt
+maintainer: Taylor Fausak
+
+source-repository head
+  location: https://github.com/tfausak/heck
+  type: git
+
+flag pedantic
+  default: False
+  manual: True
+
+library
+  build-depends: base ^>={4.19, 4.20, 4.21}
+  default-language: Haskell2010
+  exposed-modules: Heck
+  ghc-options:
+    -Weverything
+    -Wno-implicit-prelude
+    -Wno-missing-export-lists
+    -Wno-missing-kind-signatures
+    -Wno-missing-role-annotations
+    -Wno-missing-safe-haskell-mode
+    -Wno-prepositive-qualified-module
+    -Wno-safe
+
+  hs-source-dirs: source/library
+
+  if flag(pedantic)
+    ghc-options: -Werror
diff --git a/source/library/Heck.hs b/source/library/Heck.hs
new file mode 100644
--- /dev/null
+++ b/source/library/Heck.hs
@@ -0,0 +1,25 @@
+{-# LANGUAGE RankNTypes #-}
+
+module Heck where
+
+import qualified Control.Monad as Monad
+import qualified GHC.Stack as Stack
+
+data Test m n = MkTest
+  { assertFailure :: forall x. (Stack.HasCallStack) => String -> m x,
+    describe :: String -> n () -> n (),
+    it :: String -> m () -> n ()
+  }
+
+assertEq ::
+  (Stack.HasCallStack, Applicative m, Eq a, Show a) =>
+  Test m n ->
+  -- | expected
+  a ->
+  -- | actual
+  a ->
+  m ()
+assertEq test expected actual =
+  Monad.when (expected /= actual)
+    . assertFailure test
+    $ "expected " <> show expected <> " but got " <> show actual
