diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+# Changelog
+
+## v0.1.0.0
+
+Initial version.
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Daniël van de Giessen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,55 @@
+# dbus-app-launcher
+
+A simple service that allows executing a program via D-Bus.
+
+Note: Allowing D-Bus clients to execute arbitrary programs may pose a security
+risk. Use carefully.
+
+## Installation
+
+To build the code, you will need Haskell and the `stack` command line utility.
+Get it [here](https://docs.haskellstack.org/en/stable/).
+
+To build and install, run `stack install`. This will build the code and copy the
+executable to `~/.local/bin`.
+
+Next, set up a D-Bus service description that will automatically launch the new
+service whenever it is accessed:
+
+`/usr/share/dbus-1/services/nl.dvdgiessen.dbusapplauncher.service`
+
+```ini
+[D-BUS Service]
+Name=nl.dvdgiessen.dbusapplauncher
+Exec=/home/USERNAME/.local/bin/dbus-app-launcher
+```
+
+## Using the service
+
+The service currently exports one interface, `Exec`. Calling any of the methods
+of this interface will start the program using the `exec()` syscall.
+
+- Service name: `nl.dvdgiessen.dbusapplauncher`
+- Object path: `/nl/dvdgiessen/DBusAppLauncher`
+- Interface: `nl.dvdgiessen.dbusapplauncher.Exec`
+- Methods:
+  - `Cmd(String cmd)`
+  - `CmdArgs(String cmd, List<String> args)`
+  - `CmdArgsEnv(String cmd, List<String> args, Map<String,String> env)`
+
+## Issues and contributing
+
+This is my first Haskell program, so there's probably plenty that could be
+improved upon. Feel free to fork it or open pull requests if you find a bug.
+
+I might add more functionality in the future, for example for running programs
+and returning their exit code and stdout/stderr output. Currently my own use
+case for this program is for starting applications from KWin scripts (see
+[kwin-toggleterminal](https://github.com/DvdGiessen/kwin-toggleterminal) if
+you're interested in that sort of thing), so adding more functionality probably
+won't be a priority until I have a need for it myself.
+
+## License
+
+`dbus-app-launcher` is freely distributable under the terms of the
+[MIT license](https://github.com/DvdGiessen/dbus-app-launcher/blob/master/LICENSE).
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,58 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main (main) where
+
+import           Control.Concurrent (threadDelay)
+import           Control.Concurrent.Chan
+import           Control.Exception.Extra (ignore)
+import           Control.Monad
+import           Data.Map
+import           DBus.Client
+import           System.Exit
+import           System.Posix.Process
+
+main :: IO ()
+main = do
+    -- Connect to D-Bus
+    client <- connectSession
+
+    -- Channel for transfering exec parameters from the callback thread
+    channel <- newChan
+    
+    -- Export object used for launching programs
+    export client "/nl/dvdgiessen/DBusAppLauncher" defaultInterface
+             { interfaceName = "nl.dvdgiessen.dbusapplauncher.Exec"
+             , interfaceMethods =
+               [ autoMethod "Cmd" (\ cmd -> (writeChan channel (cmd, [], Nothing)))
+               , autoMethod "CmdArgs" (\ cmd args -> (writeChan channel (cmd, args, Nothing)))
+               , autoMethod "CmdArgsEnv" (\ cmd args env -> (writeChan channel (cmd, args, Just (toList env))))
+               ]
+             }
+
+    -- Register our service
+    requestResult <- requestName client "nl.dvdgiessen.dbusapplauncher" []
+    when (requestResult /= NamePrimaryOwner) $ do
+        putStrLn "Another service owns the \"nl.dvdgiessen.dbusapplauncher\" bus name"
+        exitFailure
+
+    -- Wait for the callback thread to return the exec parameters
+    (cmd, args, env) <- readChan channel
+
+    -- Do not accept any additional calls
+    unexport client "/nl/dvdgiessen/DBusAppLauncher"
+
+    -- Give the callback thread one millisecond to return its result before we kill it
+    threadDelay 1000
+
+    -- Disconnect from D-Bus, killing the callback thread
+    disconnect client
+
+    -- Make sure we are in our own process group and session
+    ignore (do _ <- createSession; return ())
+
+    -- Exec with the requested parameters
+    _ <- executeFile cmd True args env
+
+    -- Never reached
+    return ()
+
diff --git a/dbus-app-launcher.cabal b/dbus-app-launcher.cabal
new file mode 100644
--- /dev/null
+++ b/dbus-app-launcher.cabal
@@ -0,0 +1,41 @@
+cabal-version:      1.12
+name:               dbus-app-launcher
+version:            0.1.0.0
+license:            MIT
+license-file:       LICENSE
+copyright:          2024 Daniël van de Giessen
+maintainer:         daniel@dvdgiessen.nl
+author:             Daniël van de Giessen
+homepage:           https://github.com/DvdGiessen/dbus-app-launcher#readme
+bug-reports:        https://github.com/DvdGiessen/dbus-app-launcher/issues
+synopsis:           Simple app launcher for D-Bus
+description:
+    See the README at <https://github.com/DvdGiessen/dbus-app-launcher>
+
+category:           System
+build-type:         Simple
+extra-source-files:
+    README.md
+    CHANGELOG.md
+
+source-repository head
+    type:     git
+    location: https://github.com/DvdGiessen/dbus-app-launcher
+
+executable dbus-app-launcher
+    main-is:          Main.hs
+    hs-source-dirs:   app
+    other-modules:    Paths_dbus_app_launcher
+    default-language: Haskell2010
+    ghc-options:
+        -Wall -Wcompat -Widentities -Wincomplete-record-updates
+        -Wincomplete-uni-patterns -Wmissing-export-lists
+        -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints
+        -threaded -rtsopts -with-rtsopts=-N
+
+    build-depends:
+        base >=4.7 && <5,
+        containers >=0.6 && <0.7,
+        dbus >=1.3 && <1.4,
+        extra >=1.7 && <1.8,
+        unix >=2.8 && <2.9
