diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,7 @@
+# Revision history for takedouble
+
+## 0.1.0.0 -- 2022-06-22
+
+* First version. Released on an unsuspecting world.
+  In this first episode, files are lazily compared by filesize, then by first and last 4 kilobytes, and then by entire file contents.
+  Duplicates are reported as a list of results.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,11 @@
+Copyright 2022 Shae Erisson
+
+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,14 @@
+# takedouble
+TakeDouble is a duplicate file finder that reads and checks the filesize and first 4k and last 4k of a file and only then checks the full file to find duplicates.
+
+# How do I make it go?
+You can use nix or cabal to build this.
+
+`cabal build` should produce a binary. (use [ghcup](https://www.haskell.org/ghcup/) to install cabal and the latest GHC version).
+
+After that, `takedouble <dirname>` so you could use `takedouble ~/` for example.
+
+# Is it Fast?
+
+On my ThinkPad with six Xeon cores, 128GB RAM, and a 1TB Samsung 970 Pro NVMe (via PCIe 3.0), I can check 34393 uncached files in 6.4 seconds.
+A second run on the same directory takes 2.8 seconds due to file metainfo cached in memory.
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,23 @@
+module Main where
+
+import Data.List (sort)
+import System.Environment (getArgs, getProgName)
+import Takedouble (File (..), checkFullDuplicates, findPossibleDuplicates, getFileNames)
+import Text.Printf (printf)
+
+main :: IO ()
+main = do
+  args <- getArgs
+  case args of
+    [dir] ->
+      do
+        putStrLn $ "reading " <> dir
+        filenames <- getFileNames dir
+        putStrLn $ "comparing " <> show (length filenames) <> " files"
+        likelyDups <- findPossibleDuplicates filenames -- each element in the list is a list of files that are likely duplicates
+        let fnamesOnly = (filepath <$>) `fmap` likelyDups -- convert to a nested list of FilePath
+        dups <- mapM checkFullDuplicates fnamesOnly
+        mapM_ (\x -> putStr "\n" >> mapM_ putStrLn x) (sort $ concat dups)
+    _ -> do
+      name <- getProgName
+      printf "Something went wrong - please use ./%s <dir>\n" name
diff --git a/src/Takedouble.hs b/src/Takedouble.hs
new file mode 100644
--- /dev/null
+++ b/src/Takedouble.hs
@@ -0,0 +1,98 @@
+module Takedouble (findPossibleDuplicates, getFileNames, checkFullDuplicates, File (..)) where
+
+import Control.Monad.Extra (filterM, forM, ifM)
+import qualified Data.ByteString as BS
+import Data.List (group, sort, sortOn)
+import Data.List.Extra (groupOn)
+import System.Directory (doesDirectoryExist, doesPathExist, listDirectory)
+import System.FilePath.Posix ((</>))
+import System.IO
+  ( Handle,
+    IOMode (ReadMode),
+    SeekMode (SeekFromEnd),
+    hFileSize,
+    hSeek,
+    withFile,
+  )
+import System.Posix.Files
+  ( getFileStatus,
+    isDirectory,
+    isRegularFile,
+  )
+
+data File = File
+  { filepath :: FilePath,
+    filesize :: Integer,
+    firstchunk :: BS.ByteString,
+    lastchunk :: BS.ByteString
+  }
+
+-- | File needs a custom Eq instance so filepath is not part of the comparison
+instance Eq File where
+  (File _ fs1 firstchunk1 lastchunk1) == (File _ fs2 firstchunk2 lastchunk2) =
+    (fs1, firstchunk1, lastchunk1) == (fs2, firstchunk2, lastchunk2)
+
+instance Ord File where
+  compare (File _ fs1 firstchunk1 lastchunk1) (File _ fs2 firstchunk2 lastchunk2) =
+    compare (fs1, firstchunk1, lastchunk1) (fs2, firstchunk2, lastchunk2)
+
+instance Show File where
+  show (File fp _ _ _) = show fp
+
+-- | (hopefully) lazy comparison of files by size, first, and last chunk.
+findPossibleDuplicates :: [FilePath] -> IO [[File]]
+findPossibleDuplicates filenames = do
+  files <- mapM loadFile filenames
+  pure $ filter (\x -> 1 < length x) $ group (sort files)
+
+checkFullDuplicates :: [FilePath] -> IO [[FilePath]]
+checkFullDuplicates fps = do
+  allContents <- mapM BS.readFile fps
+  let pairs = zip fps allContents
+      sorted = sortOn snd pairs
+      dups = filter (\x -> length x > 1) $ groupOn snd sorted
+      res = (fst <$>) `fmap` dups
+  pure res
+
+loadFile :: FilePath -> IO File
+loadFile fp = do
+  (fsize, firstchunk, lastchunk) <- withFile fp ReadMode getChunks
+  pure $ File fp fsize firstchunk lastchunk
+
+-- | chunkSize is 4096 so NVMe drives will be especially happy
+chunkSize :: Int
+chunkSize = 4 * 1024
+
+-- | fetch the file size and first and last 4k chunks of the file
+getChunks :: Handle -> IO (Integer, BS.ByteString, BS.ByteString)
+getChunks h = do
+  fsize <- hFileSize h
+  begin <- BS.hGet h chunkSize
+  hSeek h SeekFromEnd (fromIntegral chunkSize) -- [TODO] needs to be read from filesize - filesize % 4096
+  end <- BS.hGet h chunkSize
+  pure (fsize, begin, end)
+
+-- | get all the FilePath values
+getFileNames :: FilePath -> IO [FilePath]
+getFileNames curDir = do
+  names <- listDirectory curDir
+  let names' = (curDir </>) <$> names
+  names'' <- filterM saneFile names'
+  files <- forM names'' $ \path -> do
+    let path' = curDir </> path
+    exists <- doesDirectoryExist path'
+    if exists
+      then getFileNames path'
+      else pure $ pure path'
+  pure $ concat files
+
+-- | Check if the file exists, and is not a fifo or broken symbolic link
+saneFile :: FilePath -> IO Bool
+saneFile fp =
+  ifM
+    (doesPathExist fp)
+    ( do
+        stat <- getFileStatus fp
+        pure $ isRegularFile stat || isDirectory stat
+    )
+    (pure False)
diff --git a/takedouble.cabal b/takedouble.cabal
new file mode 100644
--- /dev/null
+++ b/takedouble.cabal
@@ -0,0 +1,54 @@
+name:                takedouble
+synopsis:            duplicate file finder
+description:         takedouble is a fast duplicate file finder that filters by file size, first and last 4k chunks before checking the full contents of files that pass the filter.
+version:             0.0.1.1
+homepage:            https://github.com/shapr/takedouble
+license:             BSD3
+license-file:        LICENSE
+author:              Shae Erisson
+maintainer:          Shae Erisson
+copyright:           Shae Erisson
+category:            Utilities
+build-type:          Simple
+extra-source-files:
+                     CHANGELOG.md
+                     README.md
+cabal-version:       >=1.10
+source-repository head
+    type: git
+    location: https://github.com/shapr/takedouble.git
+
+library
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+  exposed-modules:     Takedouble
+  ghc-options:         -Wall -fno-warn-name-shadowing -O2
+  build-depends:       base >= 4.11 && < 5
+                     , bytestring
+                     , directory
+                     , extra
+                     , filepath
+                     , unix
+
+executable takedouble
+  main-is:            Main.hs
+  hs-source-dirs:     app
+  default-language:   Haskell2010
+  ghc-options:        -threaded -O2 -rtsopts "-with-rtsopts=-N -qg"
+  build-depends:      base
+                    , takedouble
+
+test-suite takedouble-tests
+  type:             exitcode-stdio-1.0
+  hs-source-dirs:   test
+  main-is:          Main.hs
+  default-language: Haskell2010
+  ghc-options:      -Wall -threaded
+  build-depends:    base >=4.11 && < 5
+                  , directory
+                  , extra
+                  , filepath
+                  , hedgehog
+                  , takedouble
+                  , temporary
+                  , unix
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,41 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TemplateHaskell #-}
+
+module Main where
+
+import Control.Monad
+import Control.Monad.IO.Class
+import Hedgehog
+import Hedgehog.Main
+-- import System.Directory
+import System.FilePath.Posix ((</>))
+import System.IO.Temp
+import Takedouble
+
+main :: IO ()
+main = defaultMain [checkParallel $$(discover)]
+
+prop_FileEq_no_check_filename :: Property
+prop_FileEq_no_check_filename = property $ do
+  File "a" 1024 "abc" "def" === File "b" 1024 "abc" "def"
+
+-- prop_Find_Duplicates :: Property
+-- prop_Find_Duplicates = property $
+--   do
+--     withTempDirectory "/tmp" "foo" $ do
+--       \dir -> do
+--         _ <- genTempFiles dir
+--         files <- findPossibleDuplicates [dir]
+--         if 1 < length files then pure () else error "no"
+
+-- dir <- liftIO $ genTempFiles
+-- files <- liftIO $ findPossibleDuplicates [dir]
+-- True === (1 < length files)
+
+genTempFiles :: FilePath -> IO FilePath
+genTempFiles dir = do
+  -- dir <- getTemporaryDirectory
+  writeFile (dir </> "dup1") "duplicate file"
+  writeFile (dir </> "dup2") "duplicate file"
+  writeFile (dir </> "uniq1") "uniq file"
+  pure dir
