diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+## self-extract 0.3.0
+
+Breaking fixes:
+* `bundle` now works standalone, without needing to be in a Cabal hook
+* Use `getExe` in Cabal hooks instead
+
+Other fixes:
+* Add tests
+* Add dev flag
+* Upgrade to ztar-0.1.0
+
 ## self-extract 0.2.0.0
 
 Fixed bug due to stripping the exes after bundling, which is only a problem when bundling multiple
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -4,56 +4,69 @@
 
 ## Usage
 
-### .cabal file
+### Basic
 
 ```
-...
-build-type: Custom
-...
+import Codec.SelfExtract (extractTo)
+import System.Environment (getArgs)
 
+main :: IO ()
+main = do
+  dir <- head <$> getArgs
+  extractTo dir
+```
+
+```
+$ stack ghc Example.hs
+$ mkdir artifacts && touch artifacts/hello.txt artifacts/world.txt
+$ stack build self-extract && stack exec -- self-bundle ./Example artifacts/
+$ ./Example dist
+$ ls dist
+hello.txt
+world.txt
+```
+
+### With Cabal hooks
+
+* Add `self-extract` to the Cabal file
+
+```
 custom-setup
   setup-depends: base, Cabal, self-extract
 
-...
-
 executable name-of-executable
-  build-depends: self-extract, ...
-  ...
+  build-depends: self-extract
 ```
 
-### Setup.hs
+* Call `bundle` in `Setup.hs`
 
 ```
-import Codec.SelfExtract.Distribution (bundle)
+import Codec.SelfExtract (bundle)
+import Codec.SelfExtract.Distribution (getExe)
 import Distribution.Simple
 
 main = defaultMainWithHooks simpleUserHooks
   { postCopy = \args cf pd lbi -> do
       postCopy simpleUserHooks args cf pd lbi
-      bundle "name-of-executable" "dir-to-bundle" lbi
+      exe <- getExe lbi "name-of-executable"
+      bundle exe "dir-to-bundle"
   }
 ```
 
-### Executable file
+* Call `extractTo` in the executable
 
 ```
-import Codec.SelfExtract (extractTo)
+import Codec.SelfExtract
 
 main = do
-  extractTo "dir" -- will extract to $CWD/dir
-  extractTo "/usr/local/lib"
-  ...
-```
-
-Extract to a temporary directory:
+  -- will extract to $CWD/dir
+  extractTo "dir"
 
-```
-import Codec.SelfExtract (withExtractToTemp)
-import System.Directory (removeDirectory)
+  -- will extract to /usr/local/lib
+  extractTo "/usr/local/lib"
 
-main = do
-  withExtractToTemp $ \tmp -> do
-    ...
+  -- will extract to a temporary directory
+  withExtractToTemp $ \dir -> ...
 ```
 
 ### Details
@@ -64,10 +77,9 @@
 When the executable containing `extractTo` is built, some space will be allocated to contain the
 size of the binary.
 
-`bundle` will find the executable from `LocalBuildInfo`'s `buildDir`. It will take the directory
-specified and run `tar` on it. It will also get the size of the executable and write the size into
-the space allocated by `extractTo`. Then `bundle` will replace the executable with the executable
-itself concatenated with the tar archive.
+`bundle` will take the directory specified and run `tar` on it. It will also get the size of the
+given executable and write the size into the space allocated by `extractTo`. Then `bundle` will
+replace the executable with the executable itself concatenated with the tar archive.
 
-When `extractTo` is called, it will read the size of the executable that was written in `bundle`.
+When `extractTo` is called, it will read the size of the executable that was written with `bundle`.
 After seeking to the size of the binary, the tar archive can be extracted to the desired directory.
diff --git a/exe/Bundle.hs b/exe/Bundle.hs
new file mode 100644
--- /dev/null
+++ b/exe/Bundle.hs
@@ -0,0 +1,14 @@
+{-# LANGUAGE LambdaCase #-}
+
+import Codec.SelfExtract (bundle)
+import System.Environment (getArgs)
+
+-- | Given an executable and a directory, bundle the directory into the executable to be extracted
+-- when the executable is run.
+--
+-- The executable should import `extractTo` from `Codec.SelfExtract` to extract the directory
+-- bundled with it.
+main :: IO ()
+main = getArgs >>= \case
+  [exe, dir] -> bundle exe dir
+  _ -> fail "Usage: self-bundle EXE DIR"
diff --git a/self-extract.cabal b/self-extract.cabal
--- a/self-extract.cabal
+++ b/self-extract.cabal
@@ -1,5 +1,5 @@
 name:                self-extract
-version:             0.2.1
+version:             0.3.0
 license:             BSD3
 license-file:        LICENSE.md
 author:              Brandon Chinn <brandonchinn178@gmail.com>
@@ -15,12 +15,16 @@
   type: git
   location: https://github.com/brandonchinn178/self-extract.git
 
+flag dev
+  description:        Turn on development settings.
+  manual:             True
+  default:            False
+
 library
   hs-source-dirs:      src
   default-language:    Haskell2010
   exposed-modules:     Codec.SelfExtract
                        Codec.SelfExtract.Distribution
-                       Codec.SelfExtract.Tar
   build-depends:       base >= 4.7 && < 5
                      , Cabal >= 2.0 && < 3
                      , binary >= 0.8.5 && < 0.9
@@ -31,6 +35,29 @@
                      , path-io >= 1.3 && < 1.4
                      , process >= 1.6 && < 1.7
                      , unix-compat >= 0.5 && < 0.6
-  ghc-options:
-    -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -Wcompat
-    -Wredundant-constraints -Wnoncanonical-monad-instances
+                     , ztar >= 0.1 && < 0.2
+  ghc-options: -Wall
+  if flag(dev)
+    ghc-options:      -Werror
+  if flag(dev) && impl(ghc >= 8.0)
+    ghc-options:      -Wcompat
+                      -Wincomplete-record-updates
+                      -Wincomplete-uni-patterns
+                      -Wnoncanonical-monad-instances
+                      -Wnoncanonical-monadfail-instances
+
+executable self-bundle
+  main-is:              Bundle.hs
+  hs-source-dirs:       exe
+  default-language:     Haskell2010
+  build-depends:        base
+                      , self-extract
+  ghc-options: -Wall
+  if flag(dev)
+    ghc-options:      -Werror
+  if flag(dev) && impl(ghc >= 8.0)
+    ghc-options:      -Wcompat
+                      -Wincomplete-record-updates
+                      -Wincomplete-uni-patterns
+                      -Wnoncanonical-monad-instances
+                      -Wnoncanonical-monadfail-instances
diff --git a/src/Codec/SelfExtract.hs b/src/Codec/SelfExtract.hs
--- a/src/Codec/SelfExtract.hs
+++ b/src/Codec/SelfExtract.hs
@@ -7,27 +7,45 @@
 Defines functions that should be used in a self-extractable executable.
 -}
 
+{-# LANGUAGE QuasiQuotes #-}
 {-# LANGUAGE OverloadedStrings #-}
 {-# LANGUAGE TemplateHaskell #-}
 
 module Codec.SelfExtract
   ( extractTo
   , withExtractToTemp
+  , bundle
   , extractTo'
   , withExtractToTemp'
+  , bundle'
   ) where
 
+import Codec.Archive.ZTar (Compression(..), create, extract)
 import Control.Monad ((>=>))
-import Data.Binary (Word32, decode)
+import Control.Monad.Extra (unlessM)
+import Data.Binary (Word32, decode, encode)
 import qualified Data.ByteString as BS
 import qualified Data.ByteString.Lazy as LBS
-import Data.FileEmbed (dummySpaceWith)
-import Path (Abs, Dir, Path, fromAbsDir, fromAbsFile, parseAbsFile)
-import Path.IO (resolveDir', withSystemTempDir, withSystemTempFile)
+import Data.FileEmbed (dummySpaceWith, injectFileWith)
+import Path
+    ( Abs
+    , Dir
+    , File
+    , Path
+    , fromAbsDir
+    , fromAbsFile
+    , parseAbsFile
+    , relfile
+    , toFilePath
+    , (</>)
+    )
+import Path.IO
+    (doesFileExist, renameFile, resolveDir', resolveFile', withSystemTempDir, withSystemTempFile)
 import System.Environment (getExecutablePath)
 import System.IO (IOMode(..), SeekMode(..), hClose, hSeek, withFile)
+import qualified System.PosixCompat.Files as Posix
 
-import Codec.SelfExtract.Tar (untar)
+{- With FilePaths -}
 
 -- | Extract the self-bundled executable to the given path.
 --
@@ -39,14 +57,24 @@
 extractTo = resolveDir' >=> extractTo'
 
 -- | Extract the self-bundled executable to a temporary path.
+withExtractToTemp :: (FilePath -> IO ()) -> IO ()
+withExtractToTemp action = withExtractToTemp' (action . fromAbsDir)
+
+-- | Bundle the given directory into the executable with the given name.
 --
+-- For example, to bundle the @static/@ directory in the executable named @install-files@:
+--
 -- @
--- withExtractToTemp $ \tmp -> do
---   ...
+-- bundle "./install-files" ".\/static\/"
 -- @
-withExtractToTemp :: (FilePath -> IO ()) -> IO ()
-withExtractToTemp action = withExtractToTemp' (action . fromAbsDir)
+bundle :: FilePath -> FilePath -> IO ()
+bundle exe dir = do
+  exe' <- resolveFile' exe
+  dir' <- resolveDir' dir
+  bundle' exe' dir'
 
+{- With Paths -}
+
 -- | Same as 'extractTo', except using the 'Path' library.
 --
 -- @
@@ -62,12 +90,57 @@
       BS.hGetContents hSelf >>= BS.hPut hTemp
 
     hClose hTemp
-    untar archive dir
+    extract (toFilePath dir) $ fromAbsFile archive
 
 -- | Same as 'withExtractToTemp', except using the 'Path' library.
 withExtractToTemp' :: (Path Abs Dir -> IO ()) -> IO ()
 withExtractToTemp' action = withSystemTempDir "" $ \dir -> extractTo' dir >> action dir
 
+-- | Same as 'bundle', except using the 'Path' library.
+--
+-- @
+-- bundle' [relfile|install-files|] [reldir|static|]
+-- @
+bundle' :: Path b File -> Path b Dir -> IO ()
+bundle' exe dir = do
+  unlessM (doesFileExist exe) $ error $ "Executable does not exist: " ++ toFilePath exe
+
+  size <- getFileSize exe
+
+  withSystemTempDir "self-extract" $ \tempDir -> do
+    let exeWithSize = tempDir </> [relfile|exe_with_size|]
+    injectFileWith "self-extract"
+      (LBS.toStrict $ encode size)
+      (toFilePath exe)
+      (fromAbsFile exeWithSize)
+
+    let archive = tempDir </> [relfile|bundle.tar.gz|]
+    create GZip (fromAbsFile archive) $ toFilePath dir
+
+    let combined = tempDir </> [relfile|exe_and_bundle|]
+    cat [exeWithSize, archive] combined
+
+    renameFile combined exe
+
+  Posix.setFileMode (toFilePath exe) executeMode
+  where
+    -- 755 permissions
+    executeMode = Posix.unionFileModes Posix.stdFileMode Posix.ownerExecuteMode
+
+{- Helpers -}
+
 -- | The size of executable that will be rewritten by `bundle`.
 exeSize :: Word32
 exeSize = decode $ LBS.fromStrict $(dummySpaceWith "self-extract" 32)
+
+-- | Get the size of the given file.
+getFileSize :: Path b File -> IO Word32
+getFileSize = fmap getSize . Posix.getFileStatus . toFilePath
+  where
+    getSize = fromIntegral . Posix.fileSize
+
+-- | Concatenate the given files and write to the given file.
+cat :: [Path b File] -> Path b File -> IO ()
+cat srcs dest = do
+  contents <- BS.concat <$> mapM (BS.readFile . toFilePath) srcs
+  BS.writeFile (toFilePath dest) contents
diff --git a/src/Codec/SelfExtract/Distribution.hs b/src/Codec/SelfExtract/Distribution.hs
--- a/src/Codec/SelfExtract/Distribution.hs
+++ b/src/Codec/SelfExtract/Distribution.hs
@@ -12,101 +12,39 @@
 {-# LANGUAGE QuasiQuotes #-}
 
 module Codec.SelfExtract.Distribution
-  ( bundle
-  , bundle'
+  ( getExe
+  , getExe'
   ) where
 
-import Control.Monad.Extra (unlessM)
-import Data.Binary (Word32, encode)
-import Data.ByteString as BS
-import Data.ByteString.Lazy as LBS
-import Data.FileEmbed (injectFileWith)
 import Distribution.Simple.LocalBuildInfo (InstallDirs(..), LocalBuildInfo(..), fromPathTemplate)
 import Distribution.Simple.Setup (ConfigFlags(..), fromFlag)
-import Path
-    ( Abs
-    , Dir
-    , File
-    , Path
-    , fromAbsFile
-    , parseAbsDir
-    , parseRelFile
-    , relfile
-    , toFilePath
-    , (</>)
-    )
-import Path.IO (doesFileExist, renameFile, resolveDir', withSystemTempDir)
-import qualified System.PosixCompat.Files as Posix
-
-import Codec.SelfExtract.Tar (tar)
+import Path (Abs, File, Path, parseAbsDir, parseRelFile, toFilePath, (</>))
 
--- | Bundle the given directory into the executable with the given name.
---
--- For example, to bundle the @static/@ directory in the executable named @install-files@:
+-- | Get the executable with the given name with the given LocalBuildInfo.
 --
 -- @
 -- main = defaultMainWithHooks simpleUserHooks
 --   { postCopy = \args cf pd lbi -> do
 --       postCopy simpleUserHooks args cf pd lbi
---       bundle "install-files" ".\/static\/" lbi
+--       exe <- getExe lbi "name-of-executable"
+--       bundle exe ".\/static\/"
 --   }
 -- @
-bundle :: String -> FilePath -> LocalBuildInfo -> IO ()
-bundle exe dir lbi = do
-  dir' <- resolveDir' dir
-  bundle' exe dir' lbi
+getExe :: LocalBuildInfo -> String -> IO FilePath
+getExe lbi exeName = toFilePath <$> getExe' lbi exeName
 
--- | Same as 'bundle', except using the 'Path' library.
+-- | Same as 'getExe', except using the 'Path' library.
 --
 -- @
 -- main = defaultMainWithHooks simpleUserHooks
 --   { postCopy = \args cf pd lbi -> do
 --       postCopy simpleUserHooks args cf pd lbi
---       bundle' "install-files" [reldir|.\/static\/|] lbi
+--       exe <- getExe' lbi "name-of-executable"
+--       bundle' exe [reldir|.\/static\/|]
 --   }
 -- @
-bundle' :: String -> Path b Dir -> LocalBuildInfo -> IO ()
-bundle' exeName dir lbi = do
-  exe <- getExe lbi exeName
-  unlessM (doesFileExist exe) $ error $ "Executable does not exist: " ++ exeName
-
-  size <- getFileSize exe
-
-  withSystemTempDir "self-extract" $ \tempDir -> do
-    let exeWithSize = tempDir </> [relfile|exe_with_size|]
-    injectFileWith "self-extract"
-      (LBS.toStrict $ encode size)
-      (fromAbsFile exe)
-      (fromAbsFile exeWithSize)
-
-    let zippedDir = tempDir </> [relfile|bundle.tar.gz|]
-    tar dir zippedDir
-
-    let combined = tempDir </> [relfile|exe_and_bundle|]
-    cat [exeWithSize, zippedDir] combined
-
-    renameFile combined exe
-
-  Posix.setFileMode (fromAbsFile exe) executeMode
-  where
-    -- 755 permissions
-    executeMode = Posix.unionFileModes Posix.stdFileMode Posix.ownerExecuteMode
-
--- | Get the executable to be made self-extracting.
-getExe :: LocalBuildInfo -> String -> IO (Path Abs File)
-getExe LocalBuildInfo{configFlags} exeName = do
+getExe' :: LocalBuildInfo -> String -> IO (Path Abs File)
+getExe' LocalBuildInfo{configFlags} exeName = do
   binDir <- parseAbsDir $ fromPathTemplate $ fromFlag $ bindir $ configInstallDirs configFlags
   exe <- parseRelFile exeName
   return $ binDir </> exe
-
--- | Get the size of the given file.
-getFileSize :: Path b File -> IO Word32
-getFileSize = fmap getSize . Posix.getFileStatus . toFilePath
-  where
-    getSize = fromIntegral . Posix.fileSize
-
--- | Concatenate the given files and write to the given file.
-cat :: [Path b File] -> Path b File -> IO ()
-cat srcs dest = do
-  contents <- BS.concat <$> mapM (BS.readFile . toFilePath) srcs
-  BS.writeFile (toFilePath dest) contents
diff --git a/src/Codec/SelfExtract/Tar.hs b/src/Codec/SelfExtract/Tar.hs
deleted file mode 100644
--- a/src/Codec/SelfExtract/Tar.hs
+++ /dev/null
@@ -1,30 +0,0 @@
-{-|
-Module      :  Codec.SelfExtract.Tar
-Maintainer  :  Brandon Chinn <brandonchinn178@gmail.com>
-Stability   :  experimental
-Portability :  portable
-
-Defines utilities for creating/extracting TAR archives.
-
-These functions shell out to the 'tar' process because Haskell packages that implement Tar would
-need some C header files, which we don't want to require on the user end.
--}
-
-module Codec.SelfExtract.Tar
-  ( tar
-  , untar
-  ) where
-
-import Path (Dir, File, Path, toFilePath)
-import Path.IO (ensureDir)
-import System.Process (callProcess)
-
--- | Zip the given directory into the given archive.
-tar :: Path b0 Dir -> Path b1 File -> IO ()
-tar src archive = callProcess "tar" ["-czf", toFilePath archive, "-C", toFilePath src, "."]
-
--- | Extract the given archive to the given directory.
-untar :: Path b0 File -> Path b1 Dir -> IO ()
-untar archive dest = do
-  ensureDir dest
-  callProcess "tar" ["-xzf", toFilePath archive, "-C", toFilePath dest]
