diff --git a/core/HaskellWorks/Polysemy/Prelude.hs b/core/HaskellWorks/Polysemy/Prelude.hs
--- a/core/HaskellWorks/Polysemy/Prelude.hs
+++ b/core/HaskellWorks/Polysemy/Prelude.hs
@@ -63,6 +63,8 @@
   , ($!)
   , ($)
   , (&)
+  , (&&)
+  , (||)
   , (.)
   , (</>)
 
diff --git a/core/HaskellWorks/Polysemy/Stack.hs b/core/HaskellWorks/Polysemy/Stack.hs
new file mode 100644
--- /dev/null
+++ b/core/HaskellWorks/Polysemy/Stack.hs
@@ -0,0 +1,11 @@
+module HaskellWorks.Polysemy.Stack
+  ( callerModuleName
+  ) where
+
+import           GHC.Stack                     (callStack, getCallStack,
+                                                srcLocModule)
+import           HaskellWorks.Polysemy.Prelude
+
+-- | Get the module name of the caller.
+callerModuleName :: HasCallStack => String
+callerModuleName = maybe "<no-module>" (srcLocModule . snd) (listToMaybe (getCallStack callStack))
diff --git a/core/HaskellWorks/Polysemy/System/IO/Temp.hs b/core/HaskellWorks/Polysemy/System/IO/Temp.hs
new file mode 100644
--- /dev/null
+++ b/core/HaskellWorks/Polysemy/System/IO/Temp.hs
@@ -0,0 +1,26 @@
+module HaskellWorks.Polysemy.System.IO.Temp
+  ( createTempDirectory,
+    getCanonicalTemporaryDirectory,
+  ) where
+
+import           HaskellWorks.Polysemy.Prelude
+import           Polysemy
+import qualified System.IO.Temp                as IO
+
+createTempDirectory :: ()
+  => HasCallStack
+  => Member (Embed IO) r
+  => FilePath
+  -> String
+  -> Sem r FilePath
+createTempDirectory fp template = withFrozenCallStack $ do
+  embed $ IO.createTempDirectory fp template
+{-# INLINE createTempDirectory #-}
+
+getCanonicalTemporaryDirectory :: ()
+  => HasCallStack
+  => Member (Embed IO) r
+  => Sem r FilePath
+getCanonicalTemporaryDirectory = withFrozenCallStack $ do
+  embed IO.getCanonicalTemporaryDirectory
+{-# INLINE getCanonicalTemporaryDirectory #-}
diff --git a/hedgehog/HaskellWorks/Polysemy/Hedgehog/Workspace.hs b/hedgehog/HaskellWorks/Polysemy/Hedgehog/Workspace.hs
new file mode 100644
--- /dev/null
+++ b/hedgehog/HaskellWorks/Polysemy/Hedgehog/Workspace.hs
@@ -0,0 +1,65 @@
+module HaskellWorks.Polysemy.Hedgehog.Workspace
+  ( workspace
+  , moduleWorkspace
+  ) where
+
+import           HaskellWorks.Polysemy.Hedgehog
+import           HaskellWorks.Polysemy.Prelude
+import           HaskellWorks.Polysemy.Stack
+import           HaskellWorks.Polysemy.System.Directory
+import           HaskellWorks.Polysemy.System.Environment
+import           HaskellWorks.Polysemy.System.IO.Temp
+import           Polysemy
+import           Polysemy.Error
+import           Polysemy.Log
+import           System.Info
+
+import qualified HaskellWorks.Polysemy.System.IO          as PIO
+
+-- | Create a workspace directory which will exist for at least the duration of
+-- the supplied block.
+--
+-- The directory will have the supplied prefix but contain a generated random
+-- suffix to prevent interference between tests
+--
+-- The directory will be deleted if the block succeeds, but left behind if
+-- the block fails.
+workspace :: ()
+  => Member Hedgehog r
+  => Member Log r
+  => Member (Embed IO) r
+  => Member (Error IOException) r
+  => HasCallStack
+  => FilePath
+  -> (FilePath -> Sem r ())
+  -> Sem r ()
+workspace prefixPath f = withFrozenCallStack $ do
+  systemTemp <- getCanonicalTemporaryDirectory
+  maybeKeepWorkspace <- lookupEnv "KEEP_WORKSPACE"
+  ws <- createTempDirectory systemTemp $ prefixPath <> "-test"
+  jot_ $ "Workspace: " <> ws
+  PIO.writeFile (ws </> "module") callerModuleName
+  f ws
+  when (os /= "mingw32" && maybeKeepWorkspace /= Just "1") $ do
+    removePathForcibly ws
+
+-- | Create a workspace directory which will exist for at least the duration of
+-- the supplied block.
+--
+-- The directory will have the prefix as "$prefixPath/$moduleName" but contain a generated random
+-- suffix to prevent interference between tests
+--
+-- The directory will be deleted if the block succeeds, but left behind if
+-- the block fails.
+--
+-- The 'prefix' argument should not contain directory delimeters.
+moduleWorkspace ::  ()
+  => Member Hedgehog r
+  => Member Log r
+  => Member (Embed IO) r
+  => Member (Error IOException) r
+  => String
+  -> (FilePath -> Sem r ())
+  -> Sem r ()
+moduleWorkspace prefix f = withFrozenCallStack $
+  workspace (prefix <> "-" <> callerModuleName) f
diff --git a/hw-polysemy.cabal b/hw-polysemy.cabal
--- a/hw-polysemy.cabal
+++ b/hw-polysemy.cabal
@@ -1,6 +1,6 @@
 cabal-version:          3.4
 name:                   hw-polysemy
-version:                0.2.2.0
+version:                0.2.3.0
 synopsis:               Opinionated polysemy library
 description:            Opinionated polysemy library.
 license:                Apache-2.0
@@ -40,6 +40,7 @@
 common stm                        { build-depends: stm                                         < 2.6    }
 common tasty                      { build-depends: tasty                                       < 1.6    }
 common tasty-hedgehog             { build-depends: tasty-hedgehog                              < 1.5    }
+common temporary                  { build-depends: temporary                                   < 1.4    }
 common text                       { build-depends: text                                        < 3      }
 common time                       { build-depends: time                                        < 2      }
 common unliftio                   { build-depends: unliftio                                    < 0.3    }
@@ -83,6 +84,7 @@
                         resourcet,
                         stm,
                         text,
+                        temporary,
                         time,
                         unliftio,
                         yaml,
@@ -116,10 +118,12 @@
                         HaskellWorks.Polysemy.FilePath
                         HaskellWorks.Polysemy.OS
                         HaskellWorks.Polysemy.Prelude
+                        HaskellWorks.Polysemy.Stack
                         HaskellWorks.Polysemy.String
                         HaskellWorks.Polysemy.System.Directory
                         HaskellWorks.Polysemy.System.Environment
                         HaskellWorks.Polysemy.System.IO
+                        HaskellWorks.Polysemy.System.IO.Temp
                         HaskellWorks.Polysemy.System.Process
   hs-source-dirs:       core
   default-language:     GHC2021
@@ -154,6 +158,7 @@
                         HaskellWorks.Polysemy.Hedgehog.Process
                         HaskellWorks.Polysemy.Hedgehog.Process.Internal
                         HaskellWorks.Polysemy.Hedgehog.Property
+                        HaskellWorks.Polysemy.Hedgehog.Workspace
   hs-source-dirs:       hedgehog
   default-language:     GHC2021
 
@@ -193,6 +198,7 @@
                         HaskellWorks.Polysemy.Hedgehog.Process,
                         HaskellWorks.Polysemy.Hedgehog.Process.Internal,
                         HaskellWorks.Polysemy.Hedgehog.Property,
+                        HaskellWorks.Polysemy.Hedgehog.Workspace,
                         HaskellWorks.Polysemy.Prelude,
                         HaskellWorks.Polysemy.String,
                         HaskellWorks.Polysemy.System.Directory,
