diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for cow
+
+## 0.1.0.0 -- 2026-04-30
+
+* Linux, Windows, and Darwin support.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2026 AgentM
+
+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.
diff --git a/cow.cabal b/cow.cabal
new file mode 100644
--- /dev/null
+++ b/cow.cabal
@@ -0,0 +1,41 @@
+cabal-version:      3.0
+name:               cow
+version:            0.1.0.0
+synopsis:           Cross-platform file copy-on-write
+-- description:
+homepage:           https://github.com/agentm/cow
+license:            MIT
+license-file:       LICENSE
+author:             AgentM
+maintainer:         agentm@themactionfaction.com
+category:           System
+build-type:         Simple
+extra-doc-files:    CHANGELOG.md
+
+common warnings
+    ghc-options: -Wall
+
+library
+    import:           warnings
+    exposed-modules:  System.IO.Copy
+    build-depends:    base ^>=4.18.3.0
+    if os(linux)
+       Build-Depends: unix
+    if os(windows)
+      Build-Depends: Win32 >= 2.12
+    hs-source-dirs:   src
+    default-language: Haskell2010
+
+test-suite cow-test
+    import:           warnings
+    default-language: Haskell2010
+    type:             exitcode-stdio-1.0
+    hs-source-dirs:   test
+    main-is:          Main.hs
+    build-depends:
+        base ^>=4.18.3.0,
+        cow,
+        tasty,
+        tasty-hunit,
+        directory
+
diff --git a/src/System/IO/Copy.hs b/src/System/IO/Copy.hs
new file mode 100644
--- /dev/null
+++ b/src/System/IO/Copy.hs
@@ -0,0 +1,168 @@
+{-# LANGUAGE CPP #-}
+-- Cross-platform copy-on-write file semantics.
+module System.IO.Copy 
+  (copyFile
+#if linux_HOST_OS || darwin_HOST_OS
+   ,copyFileOnlyIfCopyOnWrite
+   ,CopyError(..)
+#endif
+  ) where
+import Foreign.C.Error (Errno(..))
+#if defined(mingw32_HOST_OS)
+-- WINDOWS
+import qualified System.Win32.File as Win32
+# if defined(i386_HOST_ARCH)
+#  define WINDOWS_CCONV stdcall
+# elif defined(x86_64_HOST_ARCH)
+#  define WINDOWS_CCONV ccall
+# endif
+#else
+-- LINUX
+import Foreign.Ptr (Ptr, nullPtr)
+import Foreign.C.Types
+import System.IO (Handle, IOMode(..), openFile)
+import Control.Exception (throwIO)
+import System.Posix.Types (Fd(..))
+import System.Posix.IO (handleToFd, closeFd)
+import Foreign.C.Error (getErrno, eBADF, eFAULT, eNOTTY, eTXTBSY, eISDIR, eXDEV, eOPNOTSUPP, ePERM, eINVAL, errnoToIOError)
+#endif
+
+#if linux_HOST_OS
+-- | True if the function should fallback on non-copy-on-write semantics if copy-on-write is not possible.
+type CopyFallback = Bool
+
+-- | Copy data from one handle to the other hopefully using copy-on-write semantics, but fallback to generic copy if copy-on-write fails.
+copyHandle :: CopyFallback -> Handle -> Handle -> IO (Either CopyError ())
+copyHandle fallback source dest = do
+  sourceFd <- handleToFd source
+  destFd <- handleToFd dest
+  ret <- ficlone destFd sourceFd
+  errno <- getErrno
+  let convertError errno'
+        | errno' `elem` [eBADF, ePERM, eTXTBSY] =
+          InvalidFileDescriptorError errno'
+      convertError errno'
+        | errno' `elem` [eXDEV] =
+          CrossFSWriteError errno'
+      convertError errno'
+        | errno' `elem` [eINVAL, eISDIR, eOPNOTSUPP] =
+          NotSupportedError errno'
+      convertError errno' =
+        InvalidFileDescriptorError errno'
+      cleanup = do
+        closeFd sourceFd
+        closeFd destFd
+  if ret /= 0 then do
+    if fallback then do
+      if errno `elem` [eBADF, eFAULT, eNOTTY, eTXTBSY, eISDIR, ePERM] then do
+        cleanup
+        pure (Left (convertError errno))
+      else do --EINVAL or EOPNOTSUPP or EXDEV, so perform fallback
+        result <- copy_file_range sourceFd destFd
+        if result < 0 then do
+          err <- getErrno
+          cleanup
+          pure (Left (FallbackCopyError err))
+        else do
+          cleanup
+          pure (Right ())
+    else do
+      cleanup
+      pure (Left (convertError errno))
+  else do
+    cleanup
+    pure (Right ())
+#elif darwin_HOST_OS
+  res <- clonefile source dest
+  if res < 0 then do
+    -- on darwin, there is no fallback- we just assume APFS as the filesystem
+    pure (Left (FallbackCopyError (Errno 0)))
+  else
+    pure (Right ())
+#elif mingw32_HOST_OS
+#endif
+
+copyFile :: FilePath -> FilePath -> IO ()
+copyFile sourcePath destPath = do
+  -- call copyHandle on Linux and Darwin
+#if linux_HOST_OS || darwin_HOST_OS
+  sourceHandle <- openFile sourcePath ReadMode 
+  destHandle <- openFile destPath WriteMode 
+  res <- copyHandle True sourceHandle destHandle
+  case res of
+    Left err -> throwIO $ errnoToIOError "copyFile (CoW)" (errnoFromCopyError err) Nothing Nothing
+    Right () -> pure ()
+#else 
+  -- on Windows, copyFile will use CoW where available which is only on ReFS
+  Win32.copyFile sourcePath destPath False
+#endif
+  
+data CopyError =
+  CrossFSWriteError Errno |
+  InvalidFileDescriptorError Errno |
+  NotSupportedError Errno |
+  FallbackCopyError Errno
+  deriving (Eq)
+
+instance Show CopyError where
+  show e =
+    case e of
+      CrossFSWriteError (Errno errno) -> "CrossFSWriteError " <> show errno
+      InvalidFileDescriptorError (Errno errno) -> "InvalidFileDescriptorError " <> show errno
+      NotSupportedError (Errno errno) -> "NotSupportedError " <> show errno
+      FallbackCopyError (Errno errno) -> "FallbackCopyError " <> show errno
+
+#if linux_HOST_OS || darwin_HOST_OS
+errnoFromCopyError :: CopyError -> Errno
+errnoFromCopyError e =
+  case e of
+    CrossFSWriteError e' -> e'
+    InvalidFileDescriptorError e' -> e'
+    NotSupportedError e' -> e'
+    FallbackCopyError e' -> e'
+
+copyFileOnlyIfCopyOnWrite :: FilePath -> FilePath -> IO (Either CopyError ())
+copyFileOnlyIfCopyOnWrite sourcePath destPath = do
+  sourceHandle <- openFile sourcePath ReadMode 
+  destHandle <- openFile destPath WriteMode 
+  copyHandle False sourceHandle destHandle
+#endif
+
+#ifdef linux_HOST_OS
+-- LINUX ONLY
+-- FICLONE ioctl number. Common value for x86_64 Linux (from linux/fs.h):
+-- #define FICLONE        _IOW(0x94, 9, int)
+-- Encoded here as 0x40049409 (unsigned long)
+-- If this doesn't work on your architecture, check <linux/fs.h>.
+ficlone_ioctl :: CULong
+ficlone_ioctl = 0x40049409
+
+foreign import ccall unsafe "ioctl"
+  c_ioctl :: CInt -> CULong -> CInt -> IO CInt
+
+-- wrapper: perform ioctl with fdDst, FICLONE, fdSrc
+ficlone :: Fd -> Fd -> IO CInt
+ficlone (Fd fdDst) (Fd fdSrc) = do
+  -- third arg should be the source fd cast to unsigned long / int as expected by kernel
+  let arg = fromIntegral fdSrc :: CInt
+  --throwErrnoIfMinus1_ "ioctl(FICLONE)" $
+  c_ioctl (fromIntegral fdDst) ficlone_ioctl arg
+
+foreign import ccall unsafe "copy_file_range"
+  c_copy_file_range :: CInt -> Ptr CULong -> CInt -> Ptr CULong -> CULong -> CUInt -> IO CLong
+
+copy_file_range :: Fd -> Fd -> IO CLong
+copy_file_range (Fd fdSource) (Fd fdDest) =
+  c_copy_file_range (fromIntegral fdSource) nullPtr (fromIntegral fdDest) nullPtr maxBound 0
+
+#elif darwin_HOST_OS
+foreign import ccall unsafe "clonefile"
+  c_clonefile :: CString -> CString -> IO CInt
+
+clonefile :: FilePath -> FilePath -> IO CInt
+clonefile sourcePath destPath =
+  withCString sourcePath $ \cSourcePath ->
+    withCString destPath $ \cDestPath ->
+      c_clonefile cSourcePath cDestPath
+
+#endif
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,58 @@
+{-# LANGUAGE CPP #-}
+import Test.Tasty
+import Test.Tasty.HUnit
+import System.IO.Copy
+#if linux_HOST_OS || darwin_HOST_OS
+import Foreign.C.Error (Errno(..))
+#endif
+import System.Directory (removeFile)
+
+main :: IO ()
+main = defaultMain tests
+
+tests :: TestTree
+#if linux_HOST_OS || darwin_HOST_OS
+tests = testGroup "CoW" [
+  testCase "mandatory CoW" testMandatoryCoW,
+  testCase "optional CoW" testOptionalCoW
+  ]
+#else
+tests = testGroup "CoW" [
+  testCase "optional CoW" testOptionalCoW
+  ]
+#endif
+
+#if linux_HOST_OS || darwin_HOST_OS
+testMandatoryCoW :: Assertion
+testMandatoryCoW = do
+  -- btrfs/xfs
+  let source1 = "test/cow_source.txt"
+      dest1 = "test/cow_dest.txt"
+      cow_data = "COW DATA"
+  writeFile source1 cow_data
+  result1 <- copyFileOnlyIfCopyOnWrite source1 dest1
+  assertEqual "copyFileOnlyIfCopyOnWrite" (Right ()) result1
+  dest1Data <- readFile dest1
+  assertEqual "mandatory cow data" cow_data dest1Data
+  removeFile source1
+  removeFile dest1
+
+  -- ext4
+  let source2 = "/tmp/cow_source.txt"
+      dest2 = "/tmp/cow_dest.txt"
+  writeFile source2 "COW DATA"
+  result2 <- copyFileOnlyIfCopyOnWrite source2 dest2
+  assertEqual "copyFileOnlyIfCopyOnWrite expected failure" (Left (NotSupportedError (Errno 95))) result2  
+#endif
+
+testOptionalCoW :: Assertion
+testOptionalCoW = do
+  let source1 = "cow_source_opt.txt"
+      dest1 = "cow_dest_opt.txt"
+      cow_data = "COW_DATA"
+  writeFile source1 cow_data
+  copyFile source1 dest1
+  dest1Data <- readFile dest1
+  assertEqual "optional cow data" cow_data dest1Data
+  removeFile source1
+  removeFile dest1
