diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -7,6 +7,7 @@
   Bool (True),
   Either (..),
   IO,
+  Maybe (Nothing),
   die,
   writeFile,
   ($),
@@ -49,7 +50,7 @@
 
           case configResult2 of
             Left error2 -> die $ T.pack $ prettyPrintParseException error2
-            Right configUser -> printOutput appName configUser
+            Right configUser -> printOutput appName Nothing configUser
         else die $ T.pack $ prettyPrintParseException error
     Right configUser ->
-      printOutput appName configUser
+      printOutput appName Nothing configUser
diff --git a/example-config.yaml b/example-config.yaml
deleted file mode 100644
--- a/example-config.yaml
+++ /dev/null
@@ -1,35 +0,0 @@
-tableName: tasks
-idWidth: 4
-idStyle: green
-priorityStyle: magenta
-dateStyle: dull black
-bodyStyle: white
-bodyClosedStyle: black
-closedStyle: dull black
-dueStyle: yellow
-overdueStyle: red
-tagStyle: blue
-utcFormat: YYYY-MM-DD H:MI:S
-#| FIXME: Blocked by https://github.com/vincenthz/hs-hourglass/issue
-# utcFormatShort: YYYY-DDD H:MI
-utcFormatShort: YYYY-MM-DD H:MI
-
-#| Optional, uses the XDG directory per default
-#| https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
-# dataDir: /custom/path/tasklite
-
-dbName: main.db
-dateWidth: 10
-bodyWidth: 10
-prioWidth: 4
-headCount: 20
-maxWidth: 120
-progressBarWidth: 24
-
-# hooks:
-#   #| Is per default the "hooks" directory in the `dataDir`
-#   # directory: /custom/path/hooks
-#   launch:
-#     pre:
-#       - interpreter: python3
-#         body: print('Python test')
diff --git a/tasklite.cabal b/tasklite.cabal
--- a/tasklite.cabal
+++ b/tasklite.cabal
@@ -1,11 +1,11 @@
 cabal-version: 2.2
 
--- This file has been generated from package.yaml by hpack version 0.36.0.
+-- This file has been generated from package.yaml by hpack version 0.38.0.
 --
 -- see: https://github.com/sol/hpack
 
 name:           tasklite
-version:        0.3.0.0
+version:        0.5.0.0
 synopsis:       CLI task / todo list manager with SQLite backend
 description:    TaskLite is a CLI task / todo list manager with a SQLite backend.
                 It is designed to be simple and easy to use,
@@ -24,8 +24,6 @@
 build-type:     Simple
 extra-source-files:
     README.md
-data-files:
-    example-config.yaml
 
 source-repository head
   type: git
@@ -52,12 +50,12 @@
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wno-orphans -Wredundant-constraints -Wunused-packages
   build-depends:
       base >=4.18 && <5
-    , directory
-    , filepath
-    , protolude >=0.3
+    , directory ==1.3.*
+    , filepath >=1.4 && <1.6
+    , protolude ==0.3.*
     , tasklite-core
-    , text
-    , yaml
+    , text >=1.2 && <2.2
+    , yaml ==0.11.*
   default-language: GHC2021
 
 test-suite tasklite-test
@@ -65,6 +63,7 @@
   main-is: Spec.hs
   other-modules:
       CliSpec
+      Utils
       Paths_tasklite
   autogen-modules:
       Paths_tasklite
@@ -83,8 +82,11 @@
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wno-orphans -Wredundant-constraints -Wunused-packages
   build-depends:
       base >=4.18 && <5
-    , hspec
-    , optparse-applicative
-    , protolude >=0.3
+    , directory
+    , hspec >=2.11 && <3.0
+    , optparse-applicative >=0.16 && <0.19
+    , protolude ==0.3.*
     , tasklite-core
+    , template-haskell >=2.17 && <2.23
+    , temporary ==1.3.*
   default-language: GHC2021
diff --git a/test/CliSpec.hs b/test/CliSpec.hs
--- a/test/CliSpec.hs
+++ b/test/CliSpec.hs
@@ -1,6 +1,8 @@
+{-# LANGUAGE QuasiQuotes #-}
+
 module CliSpec where
 
-import Protolude (($), (&))
+import Protolude (Maybe (Just, Nothing), ($), (&), (<>))
 import Protolude qualified as P
 
 import Options.Applicative (
@@ -11,16 +13,36 @@
   parserFailure,
   renderFailure,
  )
-import Test.Hspec (Spec, describe, it, shouldContain)
+import Test.Hspec (Spec, describe, it, shouldBe, shouldContain)
 
-import Cli (commandParserInfo)
-import Config (defaultConfig)
+import Cli (commandParserInfo, printOutput)
+import Config (Config, Hook (Hook), HookSet (HookSet), defaultConfig)
+import Config qualified
+import System.Directory (
+  Permissions (executable, readable),
+  emptyPermissions,
+  setPermissions,
+ )
+import Utils (raw)
 
 
-spec :: Spec
-spec = do
+createHook :: Config -> P.FilePath -> P.Text -> P.IO ()
+createHook conf name content = do
+  let filePath = conf.hooks.directory <> "/" <> name
+  P.writeFile filePath content
+  setPermissions
+    filePath
+    ( emptyPermissions
+        { executable = P.True
+        , readable = P.True
+        }
+    )
+
+
+spec :: P.FilePath -> Spec
+spec tmpDirPath = do
   describe "CLI" $ do
-    it "should include header, body, and footer in help output" $ do
+    it "includes header, body, and footer in help output" $ do
       let
         failure :: ParserFailure ParserHelp =
           parserFailure
@@ -33,3 +55,115 @@
 
       helpText `shouldContain` "Usage: xxx"
       helpText `shouldContain` "developed by"
+
+    it "prints current version" $ do
+      _ <- printOutput "test-app" (Just ["version"]) defaultConfig
+      () `shouldBe` ()
+
+    it "calls task lifecycle hooks (add, modify) stored in config" $ do
+      let
+        getLuaHook body =
+          Hook
+            { Config.filePath = Nothing
+            , Config.interpreter = "lua"
+            , Config.body = body
+            }
+        preAddHook =
+          getLuaHook
+            [raw|
+              io.stderr:write("🏃 Executing pre-add script …\n")
+              io.stderr:write(
+                "ℹ️ Receives an object with arguments:\n",
+                io.read("*a"),
+                "\n"
+              )
+              -- print("{}")
+            |]
+        postAddHook =
+          getLuaHook
+            [raw|
+              io.stderr:write("🏃 Executing post-add script …\n")
+              io.stderr:write(
+                "ℹ️ Receives an object with arguments:\n",
+                  io.read("*a"),
+                  "\n"
+              )
+              -- print("{}")
+            |]
+        preModifyHook =
+          getLuaHook
+            [raw|
+              io.stderr:write("🏃 Executing pre-modify script …\n")
+              io.stderr:write(
+                "ℹ️ Receives an object with arguments:\n",
+                io.read("*a"),
+                "\n"
+              )
+              -- print("{}")
+            |]
+        postModifyHook =
+          getLuaHook
+            [raw|
+              io.stderr:write("🏃 Executing post-modify script …\n")
+              io.stderr:write(
+                "ℹ️ Receives an object with arguments:\n",
+                io.read("*a"),
+                "\n"
+              )
+              -- print("{}")
+            |]
+        testConf =
+          defaultConfig
+            { Config.hooks =
+                defaultConfig.hooks
+                  { Config.add =
+                      HookSet
+                        { pre = [preAddHook]
+                        , post = [postAddHook]
+                        }
+                  , Config.modify =
+                      HookSet
+                        { pre = [preModifyHook]
+                        , post = [postModifyHook]
+                        }
+                  }
+            }
+
+      _ <- printOutput "test-app" (Just ["add", "buy milk"]) testConf
+
+      () `shouldBe` ()
+
+    it "calls launch hooks stored in files" $ do
+      let
+        testConf =
+          defaultConfig
+            { Config.hooks =
+                defaultConfig.hooks{Config.directory = tmpDirPath}
+            }
+        hookFor = createHook testConf
+
+      hookFor
+        "pre-launch.lua"
+        [raw|
+          io.stderr:write("🏃 Executing pre-launch script …\n")
+          io.stderr:write(
+            "ℹ️ Receives no input:",
+            io.read("*a"),
+            "\n"
+          )
+        |]
+
+      hookFor
+        "post-launch.lua"
+        [raw|
+          io.stderr:write("🏃 Executing post-launch script …\n")
+          io.stderr:write(
+            "ℹ️ Receives an object with arguments:",
+            io.read("*a"),
+            "\n"
+          )
+        |]
+
+      _ <- printOutput "test-app" (Just ["head"]) testConf
+
+      () `shouldBe` ()
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -1,4 +1,5 @@
-import Protolude (IO)
+import Protolude (IO, ($))
+import System.IO.Temp (withSystemTempDirectory)
 import Test.Hspec (hspec)
 
 import CliSpec qualified
@@ -6,4 +7,5 @@
 
 main :: IO ()
 main = do
-  hspec CliSpec.spec
+  withSystemTempDirectory "tasklite-test" $ \dirPath -> do
+    hspec $ CliSpec.spec dirPath
diff --git a/test/Utils.hs b/test/Utils.hs
new file mode 100644
--- /dev/null
+++ b/test/Utils.hs
@@ -0,0 +1,30 @@
+{-# OPTIONS_GHC -Wno-deprecations #-}
+
+module Utils where
+
+import Protolude (pure, (.))
+import Protolude.Error (error)
+
+import Language.Haskell.TH (Exp (LitE), Lit (StringL))
+import Language.Haskell.TH.Quote (
+  QuasiQuoter (QuasiQuoter, quoteDec, quoteExp, quotePat, quoteType),
+ )
+
+
+raw :: QuasiQuoter
+raw =
+  QuasiQuoter
+    { quoteExp = pure . LitE . StringL
+    , quotePat = \_ ->
+        error
+          "Illegal raw string QuasiQuote \
+          \(allowed as expression only, used as a pattern)"
+    , quoteType = \_ ->
+        error
+          "Illegal raw string QuasiQuote \
+          \(allowed as expression only, used as a type)"
+    , quoteDec = \_ ->
+        error
+          "Illegal raw string QuasiQuote \
+          \(allowed as expression only, used as a declaration)"
+    }
