diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## [2.1.0](https://github.com/haskell-github-trust/effectful-cli/compare/2.0.0...2.1.0) (2026-10-26)
+
+### Features
+
+* add io exception wrapper ([63d7f60](https://github.com/haskell-github-trust/effectful-cli/commit/63d7f604d6c93dc7d85abf3c4d316637e9801e19))
+
 ## [2.0.0](https://github.com/haskell-github-trust/effectful-cli/compare/1.0.2...2.0.0) (2026-10-26)
 
 ### ⚠ BREAKING CHANGES
diff --git a/effectful-cli.cabal b/effectful-cli.cabal
--- a/effectful-cli.cabal
+++ b/effectful-cli.cabal
@@ -5,7 +5,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           effectful-cli
-version:        0.2.0.0
+version:        0.2.1.0
 synopsis:       CLI helpers for the effectful ecosystem
 description:    Please see the README at https://github.com/haskell-github-trust/effectful-cli
 category:       CLI
@@ -28,6 +28,7 @@
       Data.Error.CLI
       Effectful.CLI
       Effectful.CLI.Conc
+      Effectful.CLI.Exception
       Effectful.CLI.IO
       Effectful.CLI.Sleep
       Effectful.CLI.Time
diff --git a/src/Effectful/CLI/Exception.hs b/src/Effectful/CLI/Exception.hs
new file mode 100644
--- /dev/null
+++ b/src/Effectful/CLI/Exception.hs
@@ -0,0 +1,15 @@
+-- | Bubble up IO exceptions as application errors.
+module Effectful.CLI.Exception (
+  rethrowIO,
+) where
+
+import Control.Exception (IOException)
+import Effectful (Eff, (:>))
+import Effectful.Error.Static (Error, throwError)
+import Effectful.Exception (handle)
+
+-- | Catch 'IOException's raised by an action and rethrow them as @e@ via the given constructor.
+--
+-- @since 0.2.1.0
+rethrowIO :: Error e :> es => Show e => (IOException -> e) -> Eff es a -> Eff es a
+rethrowIO f = handle (throwError . f)
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -1,11 +1,14 @@
+{-# LANGUAGE DataKinds #-}
 {-# LANGUAGE OverloadedStrings #-}
 
 import Data.CLI.Impl (fromParsec)
 import Data.Error.CLI (CLIError (..), displayCLIError)
-import Effectful (runEff)
+import Effectful (Eff, IOE, liftIO, runEff)
 import Effectful.CLI.Conc (batchConcurrentlyWithIndex)
+import Effectful.CLI.Exception (rethrowIO)
 import Effectful.CLI.IO (resolveIndex)
 import Effectful.Concurrent (runConcurrent)
+import Effectful.Error.Static (Error, runErrorNoCallStack)
 import Options.Applicative
 import Test.Hspec
 import Text.Megaparsec.Char (string)
@@ -26,6 +29,14 @@
 
     it "works with a batch smaller than the list" $
       run 2 `shouldReturn` expected
+
+  describe "rethrowIO"
+    $ it "rethrows IOException as app error"
+    $ runEff
+      ( runErrorNoCallStack
+          (rethrowIO show (liftIO (ioError (userError "boom")) :: Eff '[Error String, IOE] ()))
+      )
+      `shouldReturn` Left "user error (boom)"
 
   describe "resolveIndex" $ do
     let len = 4
