diff --git a/CHANGELOG.md b/CHANGELOG.md
deleted file mode 100644
--- a/CHANGELOG.md
+++ /dev/null
@@ -1,150 +0,0 @@
-## Changelog
-
-#### Upcoming
-
-#### v0.3.0.5
-
-*Revisions*
-
-* Fixed a bug causing prettifying to fail across filesystem boundaries. ([#72](https://github.com/hjwylde/git-fmt/issues/72))
-
-#### v0.3.0.4
-
-*Revisions*
-
-* Fixed a bug causing the program to hang when not in the root directory. ([#66](https://github.com/hjwylde/git-fmt/issues/66))
-* Fixed a bug that omitted searching the drive for a config file. ([#66](https://github.com/hjwylde/git-fmt/issues/66))
-* Fixed a bug where output files could be created outside of the temp directory. ([#68](https://github.com/hjwylde/git-fmt/issues/68))
-
-#### v0.3.0.3
-
-*Revisions*
-
-* Restricted use of `--operate-on-tracked` and `--operate-on REF` at the same time. ([#65](https://github.com/hjwylde/git-fmt/issues/65))
-* Removed long option for help text (as git overrides it for man pages). ([#65](https://github.com/hjwylde/git-fmt/issues/65))
-
-#### v0.3.0.2
-
-*Revisions*
-
-* Fixed a bug where passing arguments didn't properly narrow down the operation files. ([#64](https://github.com/hjwylde/git-fmt/issues/64))
-
-#### v0.3.0.1
-
-*Revisions*
-
-* Relaxed version constraints. ([#63](https://github.com/hjwylde/git-fmt/issues/63))
-
-#### v0.3.0.0
-
-*Major*
-
-* Restricted arguments to being inside the repository. ([#34](https://github.com/hjwylde/git-fmt/issues/34))
-* Refactored library to use pipes. ([#32](https://github.com/hjwylde/git-fmt/issues/32))
-* Made `Options` and `Version` modules private. ([#62](https://github.com/hjwylde/git-fmt/issues/62))
-* Renamed library modules to `Omnifmt`. ([#62](https://github.com/hjwylde/git-fmt/issues/62))
-* Refactored pipeline to feed and consume triples. ([#61](https://github.com/hjwylde/git-fmt/issues/61))
-* Set default `--operate-on` to `head`. ([#28](https://github.com/hjwylde/git-fmt/issues/28))
-
-*Minor*
-
-* Changed "not found" status to print as debug message. ([#61](https://github.com/hjwylde/git-fmt/issues/61))
-* Added "unsupported" status as debug message. ([#61](https://github.com/hjwylde/git-fmt/issues/61))
-* Added `--operate-on-tracked` and `--operate-on REF` options. ([#28](https://github.com/hjwylde/git-fmt/issues/28))
-
-#### v0.2.2.1
-
-*Revisions*
-
-* Fixed a bug causing prettifying to fail across filesystem boundaries. ([#72](https://github.com/hjwylde/git-fmt/issues/72))
-
-#### v0.2.2.0
-
-*Minor*
-
-* Added `--threads` option to change the number of threads for parallelisation. ([#54](https://github.com/hjwylde/git-fmt/issues/54))
-
-*Revisions*
-
-* Added quoting to the command variables during substitution. ([#59](https://github.com/hjwylde/git-fmt/issues/59))
-* Changed parallelisation to use the number of capabilities (and processors) for the number of
-  threads by default. ([#54](https://github.com/hjwylde/git-fmt/issues/54))
-
-#### v0.2.1.2
-
-*Revisions*
-
-* Fixed a bug causing prettifying to fail across filesystem boundaries. ([#72](https://github.com/hjwylde/git-fmt/issues/72))
-
-#### v0.2.1.1
-
-*Revisions*
-
-* Fixed a bug where passing arguments would only work when running in the git directory. ([#57](https://github.com/hjwylde/git-fmt/issues/57))
-
-#### v0.2.1.0
-
-*Minor*
-
-* Added default use of stdin and stdout when variables not specified in a program command. ([#49](https://github.com/hjwylde/git-fmt/issues/49))
-
-#### v0.2.0.2
-
-*Revisions*
-
-* Fixed a bug causing prettifying to fail across filesystem boundaries. ([#72](https://github.com/hjwylde/git-fmt/issues/72))
-
-#### v0.2.0.1
-
-*Revisions*
-
-* Fixed a bug where passing arguments would only work when running in the git directory. ([#57](https://github.com/hjwylde/git-fmt/issues/57))
-
-#### v0.2.0.0
-
-*Major*
-
-* Removed `--list-ugly` and `--dry-run` options. ([#29](https://github.com/hjwylde/git-fmt/issues/29))
-* Restricted use of `--quiet` and `--verbose` at the same time. ([#35](https://github.com/hjwylde/git-fmt/issues/35))
-* Updated project structure to delegate pretty printing to other binaries. ([#38](https://github.com/hjwylde/git-fmt/issues/38))
-* Added a `.omniyaml.yaml` config file. ([#38](https://github.com/hjwylde/git-fmt/issues/38))
-
-*Minor*
-
-* Added `--mode` option (either `normal` or `dry-run`). ([#29](https://github.com/hjwylde/git-fmt/issues/29))
-* Added `--null` option (use the null terminator as the delimiter for inputs). ([#27](https://github.com/hjwylde/git-fmt/issues/27))
-* Added support for directories as arguments (directories include all files within recursively). ([#30](https://github.com/hjwylde/git-fmt/issues/30))
-* Added parallelisation. ([#48](https://github.com/hjwylde/git-fmt/issues/48))
-
-*Revisions*
-
-* Added a warning for when files aren't found. ([#29](https://github.com/hjwylde/git-fmt/issues/29))
-* Updated internal use of `git ls-files` to use the null terminator option. ([#27](https://github.com/hjwylde/git-fmt/issues/27))
-* Fixed debug log messages to have timestamp and log level on all lines. ([#33](https://github.com/hjwylde/git-fmt/issues/33))
-* Tidied up error messages from git. ([#40](https://github.com/hjwylde/git-fmt/issues/40))
-* Tidied up error messages from parsing the config. ([#43](https://github.com/hjwylde/git-fmt/issues/43))
-
-#### v0.1.0.3
-
-*Revisions*
-
-* Fixed a bug where passing arguments would only work when running in the git directory. ([#57](https://github.com/hjwylde/git-fmt/issues/57))
-
-#### v0.1.0.2
-
-*Revisions*
-
-* Fixed a bug where UTF-8 characters in strings weren't printed properly. ([#26](https://github.com/hjwylde/git-fmt/issues/26))
-
-#### v0.1.0.1
-
-*Revisions*
-
-* Fixed a bug where integers were printed as rationals. ([#25](https://github.com/hjwylde/git-fmt/issues/25))
-
-#### v0.1.0.0
-
-This is first release of the `git-fmt` binary!
-It provides a basic syntax for formatting files in a git repository.
-Currently only JSON is supported.
-
diff --git a/README.md b/README.md
deleted file mode 100644
--- a/README.md
+++ /dev/null
@@ -1,110 +0,0 @@
-# git-fmt
-
-[![Project Status: Wip - Initial development is in progress, but there has not yet been a stable, usable release suitable for the public.](http://www.repostatus.org/badges/1.0.0/wip.svg)](http://www.repostatus.org/#wip)
-[![Build Status](https://travis-ci.org/hjwylde/git-fmt.svg?branch=master)](https://travis-ci.org/hjwylde/git-fmt)
-[![Release](https://img.shields.io/github/release/hjwylde/git-fmt.svg)](https://github.com/hjwylde/git-fmt/releases/latest)
-[![git-fmt on Stackage LTS](http://stackage.org/package/git-fmt/badge/lts)](http://stackage.org/lts/package/git-fmt)
-[![git-fmt on Stackage Nightly](http://stackage.org/package/git-fmt/badge/nightly)](http://stackage.org/nightly/package/git-fmt)
-
-(Side note: the formatting component of this project will eventually be split out and named omnifmt.)
-
-`git-fmt` was created to make prettifying code easy.
-It adds a custom (easy to use) command to Git that formats code through external pretty-printers.
-
-Formatted code is:
-
-* Easier to write: never worry about minor formatting concerns while hacking away.
-* Easier to read: when all code looks the same you need not mentally convert others' formatting
-  style into something you can understand.
-* Easier to maintain: mechanical changes to the source don't cause unrelated changes to the file's
-  formatting; diffs show only the real changes.
-* Uncontroversial: never have a debate about spacing or brace position ever again.
-
-(Bullet points taken from https://blog.golang.org/go-fmt-your-code.)
-
-### Installing
-
-Installing `git-fmt` is easiest done using either
-    [stack](https://github.com/commercialhaskell/stack) (recommended) or
-    [Cabal](https://github.com/haskell/cabal).
-
-**Using stack:**
-
-```bash
-stack install git-fmt
-export PATH=$PATH:~/.local/bin
-```
-
-**Using Cabal:**
-
-```bash
-cabal-install git-fmt
-export PATH=$PATH:~/.cabal/bin
-```
-
-### Usage
-
-The `git-fmt` binary wraps external pretty-printers and provides an interface for selecting files
-    to prettify and performing dry-runs.
-
-**The basics:**
-
-`git-fmt` operates only on tracked git files (thus it implicitly respects the .gitignore).
-By default it operates on files in the index (i.e., `--operate-on head`).
-It is possible to operate on all tracked files (`--operate-on-tracked`) or on a specific reference
-    (`--operate-on REF`).
-The `REF` argument is passed directly into `git diff REF --name-only`, so you can even play with
-    ranges such as `master...`.
-
-Passing arguments to `git-fmt` will narrow down the operation files.
-For example, `git-fmt --operate-on-tracked src/` will format all tracked files under `src/` and
-    `git-fmt --operate-on head src/` will format all files in the index under `src/`.
-
-`git-fmt` can run in two different modes, *normal* and *dry-run*.
-Normal mode (`--mode normal`) writes to (prettifies) all ugly files.
-Dry-run mode (`--mode dry-run`) outputs the ugly file paths to `stdout`.
-
-**NB:** a third mode, *patch*, is in the making, see
-    [#23](https://github.com/hjwylde/git-fmt/issues/23) for details.
-
-For the more uncommon usage options, have a read of `git fmt -h`.
-
-#### Configuration
-
-Configuration is done via an `.omnifmt.yaml` file in the git repository.
-The file contains a list of *programs* that link *extensions* to a prettifying *command*, e.g.,
-```yaml
-haskell:
-    extensions: ["hs", "lhs"]
-    command:    "stylish-haskell {{input}} > {{output}}"
-
-javascript:
-    extensions: ["js"]
-    command:    "js-beautify -f {{input}}"
-
-json:
-    extensions: ["json"]
-    command:    "json_pp"
-
-ruby:
-    extensions: ["rb"]
-    command:    "ruby-beautify"
-```
-
-Each command declares how to read the *input file* and how to write to the *output file*.
-If the input variable is omitted, the file contents are fed to the command through `stdin`.
-Likewise if the output variable is omitted, the pretty contents are read from `stdout`.
-The output file is used to compare whether the original was pretty or ugly before writing to it.
-
-The extensions field is pretty self explanatory, but if you use the same extension more than once
-    then precedence goes to the program defined first.
-
-#### Examples
-
-See the [docs/example-configs/](https://github.com/hjwylde/git-fmt/tree/master/docs/example-configs/)
-    directory for some common pretty-printers and their corresponding omnifmt config (pull requests
-    are welcome for adding more).
-Just don't forget to actually call the config file `.omnifmt.yaml`!
-
-**NB:** I haven't tested them fully, be careful in case one is buggy.
-
diff --git a/app/Git/Fmt/Options.hs b/app/Git/Fmt/Options.hs
--- a/app/Git/Fmt/Options.hs
+++ b/app/Git/Fmt/Options.hs
@@ -42,7 +42,7 @@
     deriving (Eq, Show)
 
 -- | Run mode.
-data Mode = Normal | DryRun
+data Mode = Normal | DryRun | Diff
     deriving (Eq, Show)
 
 -- | Operation files.
@@ -92,7 +92,7 @@
     <*> modeOption (mconcat [
         long "mode", short 'm', metavar "MODE",
         value Normal, showDefaultWith $ const "normal",
-        help "Specify the mode as either `normal' or `dry-run'"
+        help "Specify the mode as either `normal', `dry-run' or `diff'"
         ])
     <*> (
             flag' Tracked (mconcat [
@@ -120,5 +120,6 @@
         modeOption  = option $ readerAsk >>= \opt -> case opt of
             "normal"    -> return Normal
             "dry-run"   -> return DryRun
+            "diff"      -> return Diff
             _           -> readerError $ "unrecognised mode `" ++ opt ++ "'"
 
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -29,6 +29,7 @@
 
 import           Data.List.Extra    (dropEnd, intersect, linesBy, lower)
 import           Data.Maybe         (fromJust, fromMaybe, isNothing)
+import           Data.Text          (Text)
 import qualified Data.Text          as T
 import qualified Data.Text.Encoding as T
 import qualified Data.Text.IO       as T
@@ -44,9 +45,10 @@
 
 import Options.Applicative
 
-import Pipes
-import Pipes.Concurrent
-import Prelude          hiding (filter, log)
+import           Pipes
+import           Pipes.Concurrent
+import qualified Pipes.Prelude    as Pipes
+import           Prelude          hiding (filter, log)
 
 import System.Directory.Extra
 import System.Exit
@@ -63,9 +65,9 @@
 main :: IO ()
 main = do
     options     <- customExecParser gitFmtPrefs gitFmtInfo
-    let chatty  = optChatty options
+    let chatty  = if optMode options == Diff then Quiet else optChatty options
 
-    flip runLoggingT (log chatty) . filter chatty $ do
+    flip runLoggingT (log $ optChatty options) . filter chatty $ do
         checkGitRepository
 
         mFilePath <- Config.nearestConfigFile "."
@@ -84,12 +86,9 @@
 
 handle :: (MonadIO m, MonadLogger m, MonadMask m, MonadParallel m, MonadReader Config m) => Options -> m ()
 handle options = do
-    rootDir         <- liftIO getCurrentDirectory
-    filePaths       <- runPanic $ if null (argPaths options)
+    filePaths <- runPanic $ if null (argPaths options)
         then initialFilePaths options
         else liftM2 intersect (initialFilePaths options) (providedFilePaths options)
-    absFilePaths    <- forM filePaths $ \filePath -> ifM (liftIO $ doesFileExist filePath)
-        (liftIO $ canonicalizePath filePath) (return filePath)
 
     numThreads <- liftIO getNumCapabilities >>= \numCapabilities ->
         return $ fromMaybe numCapabilities (optThreads options)
@@ -97,12 +96,12 @@
     (output, input) <- liftIO $ spawn unbounded
 
     withSystemTempDirectory "git-fmt" $ \tmpDir -> do
-        runEffect $ each (map (\filePath -> omnifmt (makeRelative rootDir filePath) (tmpDir </> dropDrive filePath)) absFilePaths) >-> toOutput output
+        runEffect $ each (map (\filePath -> omnifmt filePath (tmpDir </> filePath)) filePaths) >-> toOutput output
 
         liftIO performGC
 
         Parallel.forM_ [1..numThreads] $ \_ ->
-            runEffect (fromInput input >-> pipeline >-> runner (optMode options) >-> statusPrinter) >>
+            runEffect (fromInput input >-> pipeline >-> runner (optMode options)) >>
             liftIO performGC
 
 providedFilePaths :: MonadIO m => Options -> m [FilePath]
@@ -124,9 +123,16 @@
 
             return item
 
-runner :: (MonadIO m, MonadLogger m) => Mode -> Pipe (Status, FilePath, FilePath) (Status, FilePath, FilePath) m ()
-runner Normal = commit
-runner DryRun = cat
+runner :: (MonadIO m, MonadLogger m) => Mode -> Consumer (Status, FilePath, FilePath) m ()
+runner Normal   = commit    >-> printFileStatus logFunction >-> Pipes.drain
+runner DryRun   = cat       >-> printFileStatus logFunction >-> Pipes.drain
+runner Diff     = diff      >-> Pipes.drain
+
+logFunction :: MonadLogger m => Status -> Text -> m ()
+logFunction status
+    | status `elem` [Unknown, Error, Timeout]       = logWarnN
+    | status `elem` [Unsupported, NotFound, Pretty] = logDebugN
+    | otherwise                                     = logInfoN
 
 filter :: Chatty -> LoggingT m a -> LoggingT m a
 filter Quiet    = filterLogger (\_ level -> level >= LevelError)
diff --git a/git-fmt.cabal b/git-fmt.cabal
--- a/git-fmt.cabal
+++ b/git-fmt.cabal
@@ -1,5 +1,5 @@
 name:           git-fmt
-version:        0.3.0.5
+version:        0.3.1.0
 
 author:         Henry J. Wylde
 maintainer:     public@hjwylde.com
@@ -16,8 +16,6 @@
 cabal-version:  >= 1.10
 category:       Development
 build-type:     Simple
-
-extra-source-files: CHANGELOG.md README.md
 
 source-repository head
     type:       git
diff --git a/src/Omnifmt/Config.hs b/src/Omnifmt/Config.hs
--- a/src/Omnifmt/Config.hs
+++ b/src/Omnifmt/Config.hs
@@ -66,10 +66,12 @@
     Right config    -> return $ Just config
 
 -- | Finds the nearest config file by searching from the given directory upwards.
+--
+--   TODO (hjw): fix the bug where it won't search the root directory.
 nearestConfigFile :: MonadIO m => FilePath -> m (Maybe FilePath)
-nearestConfigFile dir = liftIO (canonicalizePath dir) >>= \dir' -> findM (liftIO . doesFileExist) $ map (</> defaultFileName) (parents dir')
+nearestConfigFile dir = findM (liftIO . doesFileExist) $ map (</> defaultFileName) parents
     where
-        parents = reverse . scanl1 combine . splitDirectories
+        parents = takeWhile (\dir -> dir /= takeDrive dir) (iterate takeDirectory dir)
 
 -- | The file name of the default config, '.omnifmt.yaml'.
 defaultFileName :: FilePath
diff --git a/src/Omnifmt/Pipes.hs b/src/Omnifmt/Pipes.hs
--- a/src/Omnifmt/Pipes.hs
+++ b/src/Omnifmt/Pipes.hs
@@ -24,10 +24,8 @@
     omnifmt,
 
     -- * Transformers
-    select, checkFileSupported, checkFileExists, runProgram, checkFilePretty, commit,
-
-    -- * Consumers
-    statusPrinter,
+    select, checkFileSupported, checkFileExists, runProgram, checkFilePretty, commit, diff,
+    printFileStatus,
 ) where
 
 import Control.Monad.Except
@@ -36,11 +34,10 @@
 import Control.Monad.Reader
 
 import           Data.List.Extra  (lower)
+import           Data.Text        (Text)
 import qualified Data.Text        as T
 import           Data.Tuple.Extra (fst3)
 
-import GHC.IO.Exception (IOErrorType (..))
-
 import Omnifmt.Config
 import Omnifmt.Process
 
@@ -50,13 +47,13 @@
 import System.Directory.Extra
 import System.Exit
 import System.FilePath
-import System.IO.Error
 
 -- | A status for a file going through the omnifmt pipeline.
 data Status = Unknown       -- ^ The file has not been processed.
             | Error         -- ^ An error occurred somewhere.
             | Unsupported   -- ^ The file type is unsupported (i.e., no applicable 'Program').
             | NotFound      -- ^ The file could not be found.
+            | Timeout       -- ^ A command timed out.
             | Pretty        -- ^ The file is pretty.
             | Ugly          -- ^ The file is ugly.
             | Prettified    -- ^ The file is now pretty.
@@ -98,15 +95,17 @@
     config <- ask
     let program = unsafeProgramFor config (T.pack . drop 1 $ takeExtension uglyFilePath)
 
-    (exitCode, _, stderr) <- runCommand . T.unpack $ substitute (T.concat [command program, inputSuffix program, outputSuffix program]) [
+    (exitCode, _, stderr) <- runTimedCommand 5 . T.unpack $ substitute (T.concat [command program, inputSuffix program, outputSuffix program]) [
         (inputVariableName, T.pack uglyFilePath),
         (outputVariableName, T.pack prettyFilePath)
         ]
 
-    if exitCode == ExitSuccess
-        then return item
-        else logDebugN (T.pack stderr) >>
-             return (Error, uglyFilePath, prettyFilePath)
+    case exitCode of
+        ExitSuccess     -> return item
+        ExitFailure 124 -> return (Timeout, uglyFilePath, prettyFilePath)
+        ExitFailure 137 -> return (Timeout, uglyFilePath, prettyFilePath)
+        _               -> logDebugN (T.pack stderr) >>
+                           return (Error, uglyFilePath, prettyFilePath)
     where
         inputSuffix program
             | usesInputVariable (command program)   = T.empty
@@ -136,30 +135,24 @@
 --   This function updates the status to 'Prettified'.
 commit :: MonadIO m => Pipe (Status, FilePath, FilePath) (Status, FilePath, FilePath) m ()
 commit = select [Ugly] $ \(_, uglyFilePath, prettyFilePath) -> do
-    -- Try move the file, but if it's across a filesystem boundary then we may need to copy instead
-    liftIO $ renameFile prettyFilePath uglyFilePath `catchIOError` \e ->
-        if ioeGetErrorType e == UnsupportedOperation
-            then copyFile prettyFilePath uglyFilePath >> removeFile prettyFilePath
-            else ioError e
+    liftIO $ renameFile prettyFilePath uglyFilePath
 
     return (Prettified, uglyFilePath, prettyFilePath)
 
--- | Logs the status of each file.
---   'Unsupported', 'NotFound' and 'Pretty' are logged using 'logDebugN'.
---   'Ugly' and 'Prettified' are logged using 'logInfoN'.
---   'Unknown' and 'Error' are logged using 'logErrorN'.
-statusPrinter :: MonadLogger m => Consumer (Status, FilePath, FilePath) m ()
-statusPrinter = Pipes.mapM_ $ \(status, uglyFilePath, _) ->
-    logFunction status (T.pack $ uglyFilePath ++ ": " ++ showStatus status)
-    where
-        logFunction Unknown     = logWarnN
-        logFunction Unsupported = logDebugN
-        logFunction NotFound    = logDebugN
-        logFunction Error       = logWarnN
-        logFunction Pretty      = logDebugN
-        logFunction Ugly        = logInfoN
-        logFunction Prettified  = logInfoN
+-- | Prints out the diff of all ugly files to standard output.
+diff :: (MonadIO m, MonadLogger m) => Pipe (Status, FilePath, FilePath) (Status, FilePath, FilePath) m ()
+diff = select [Ugly] $ \item@(_, uglyFilePath, prettyFilePath) -> do
+    (_, stdout, _) <- runProcess "git" ["diff", "--no-index", "--", uglyFilePath, prettyFilePath]
 
+    liftIO $ putStr stdout
+
+    return item
+
+-- | Logs the status of each file using the given function.
+printFileStatus :: MonadLogger m => (Status -> Text -> m ()) -> Pipe (Status, FilePath, FilePath) (Status, FilePath, FilePath) m ()
+printFileStatus f = Pipes.mapM_ $ \(status, uglyFilePath, _) ->
+    f status (T.pack $ uglyFilePath ++ ": " ++ showStatus status)
+    where
         showStatus NotFound = "not found"
         showStatus status   = lower $ show status
 
diff --git a/src/Omnifmt/Process.hs b/src/Omnifmt/Process.hs
--- a/src/Omnifmt/Process.hs
+++ b/src/Omnifmt/Process.hs
@@ -15,7 +15,8 @@
 
 module Omnifmt.Process (
     -- * Run
-    runProcess, runProcess_, runCommand, runCommand_, runCreateProcess, runCreateProcess_,
+    runProcess, runProcess_, runTimedProcess, runCommand, runCommand_, runTimedCommand,
+    runCreateProcess, runCreateProcess_,
 ) where
 
 import Control.Monad.Except
@@ -39,6 +40,12 @@
 runProcess_ :: (MonadError ExitCode m, MonadIO m, MonadLogger m) => FilePath -> [String] -> m String
 runProcess_ cmd args = runCreateProcess_ (System.proc cmd args) ""
 
+-- | Runs the given executable with the arguments.
+--   Returns the exit code, stdout and stderr.
+--   The command is wrapped in a `timeout -k N*2 N` call.
+runTimedProcess :: (MonadIO m, MonadLogger m) => Int -> FilePath -> [String] -> m (ExitCode, String, String)
+runTimedProcess n cmd args = runCreateProcess (System.proc "timeout" $ "-k":show (n * 2):show n:cmd:args) ""
+
 -- | Runs the given command.
 --   Returns the exit code, stdout and stderr.
 runCommand :: (MonadIO m, MonadLogger m) => String -> m (ExitCode, String, String)
@@ -48,6 +55,12 @@
 --   Depending on the exit code, either logs the stderr and exits fast (128) or returns the stdout.
 runCommand_ :: (MonadError ExitCode m, MonadIO m, MonadLogger m) => String -> m String
 runCommand_ cmd = runCreateProcess_ (System.shell cmd) ""
+
+-- | Runs the given command.
+--   Returns the exit code, stdout and stderr.
+--   The command is wrapped in a `timeout -k N*2 N` call.
+runTimedCommand :: (MonadIO m, MonadLogger m) => Int -> String -> m (ExitCode, String, String)
+runTimedCommand n cmd = runCreateProcess (System.shell $ unwords ["timeout -k", show $ n * 2, show n, cmd]) ""
 
 -- | Runs the given 'CreateProcess'.
 --   Returns the exit code, stdout and stderr.
