diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for hslua-module-system
+
+## 0.1.0 -- 2019-04-26
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2019 Albert Krewinkel
+
+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/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/hslua-module-system.cabal b/hslua-module-system.cabal
new file mode 100644
--- /dev/null
+++ b/hslua-module-system.cabal
@@ -0,0 +1,49 @@
+name:                hslua-module-system
+version:             0.1.0
+synopsis:            Lua module wrapper around Haskell's System module.
+
+description:         Provides access to system information and functionality
+                     to Lua scripts via Haskell's `System` module.
+                     .
+                     Intended usage for this package is to preload it by adding
+                     the loader function to `package.preload`. Note that the
+                     Lua `package` library must have already been loaded before
+                     the loader can be added.
+homepage:            https://github.com/hslua/hslua-module-system
+license:             MIT
+license-file:        LICENSE
+author:              Albert Krewinkel
+maintainer:          albert+hslua@zeitkraut.de
+copyright:           Albert Krewinkel <albert+hslua@zeitkraut.de>
+category:            Foreign
+build-type:          Simple
+extra-source-files:  CHANGELOG.md
+cabal-version:       >=1.10
+
+source-repository head
+  type:              git
+  location:          https://github.com/hslua/hslua-module-system.git
+
+library
+  build-depends:       base       >= 4.9    && < 5
+                     , directory >= 1.3     && < 1.4
+                     , hslua      >= 1.0    && < 1.2
+                     , temporary  >= 1.2    && < 1.4
+  default-extensions:  LambdaCase
+  default-language:    Haskell2010
+  exposed-modules:     Foreign.Lua.Module.System
+  hs-source-dirs:      src
+  other-extensions:    OverloadedStrings
+
+test-suite test-hslua-module-system
+  default-language:    Haskell2010
+  type:                exitcode-stdio-1.0
+  main-is:             test-hslua-module-system.hs
+  hs-source-dirs:      test
+  ghc-options:         -Wall -threaded
+  build-depends:       base
+                     , hslua
+                     , hslua-module-system
+                     , tasty
+                     , tasty-hunit
+                     , text 
diff --git a/src/Foreign/Lua/Module/System.hs b/src/Foreign/Lua/Module/System.hs
new file mode 100644
--- /dev/null
+++ b/src/Foreign/Lua/Module/System.hs
@@ -0,0 +1,161 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-|
+Module      : Foreign.Lua.Module.System
+Copyright   : © 2019 Albert Krewinkel
+License     : MIT
+Maintainer  : Albert Krewinkel <albert+hslua@zeitkraut.de>
+Stability   : alpha
+Portability : Requires language extensions ForeignFunctionInterface,
+              OverloadedStrings.
+
+Provide a Lua module containing a selection of @'System'@ functions.
+-}
+module Foreign.Lua.Module.System
+  ( pushModule
+  , preloadModule
+  )
+where
+
+import Control.Applicative ((<$>))
+import Control.Exception (IOException, catch, evaluate, try)
+import Data.Maybe (fromMaybe)
+import Foreign.Lua (Lua, NumResults(..), Optional (..), Peekable, Pushable,
+                    StackIndex, ToHaskellFunction)
+import System.IO.Error (IOError, isDoesNotExistError)
+
+import qualified Foreign.Lua as Lua
+import qualified System.Directory as Directory
+import qualified System.Environment as Env
+import qualified System.IO.Temp as Temp
+
+-- | Pushes the @text@ module to the lua stack.
+pushModule :: Lua NumResults
+pushModule = do
+  Lua.newtable
+  addFunction "chdir" chdir
+  addFunction "currentdir" currentdir
+  addFunction "env" env
+  addFunction "getenv" getenv
+  addFunction "ls" ls
+  addFunction "pwd" currentdir
+  addFunction "setenv" setenv
+  addFunction "tmpdirname" tmpdirname
+  addFunction "with_tmpdir" with_tmpdir
+  return 1
+
+-- | Add the text module under the given name to the table of preloaded
+-- packages.
+preloadModule :: String -> Lua ()
+preloadModule = flip addPackagePreloader pushModule
+
+-- | Registers a preloading function. Takes an module name and the Lua
+-- operation which produces the package.
+addPackagePreloader :: String -> Lua NumResults -> Lua ()
+addPackagePreloader name modulePusher = do
+  Lua.getfield Lua.registryindex Lua.preloadTableRegistryField
+  Lua.pushHaskellFunction modulePusher
+  Lua.setfield (-2) name
+  Lua.pop 1
+
+-- | Attach a function to the table at the top of the stack, using the
+-- given name.
+addFunction :: ToHaskellFunction a => String -> a -> Lua ()
+addFunction name fn = do
+  Lua.push name
+  Lua.pushHaskellFunction fn
+  Lua.rawset (-3)
+
+-- | Lua callback function
+newtype Callback = Callback { callbackStackIndex :: StackIndex }
+
+instance Peekable Callback where
+  peek idx = do
+    isFn <- Lua.isfunction idx
+    if isFn
+      then return (Callback idx)
+      else Lua.throwException "Function expected"
+
+instance Pushable Callback where
+  push (Callback idx) = Lua.pushvalue idx
+
+
+-- | Any value of unknown type
+newtype AnyValue = AnyValue { fromAnyValue :: StackIndex }
+
+instance Peekable AnyValue where
+  peek = return . AnyValue
+
+instance Pushable AnyValue where
+  push (AnyValue idx) = Lua.pushvalue idx
+
+with_tmpdir :: String            -- ^ parent dir or template
+            -> AnyValue          -- ^ template or callback
+            -> Optional Callback -- ^ callback or nil
+            -> Lua NumResults
+with_tmpdir parentDir tmpl callback = do
+  case fromOptional callback of
+    Nothing -> do
+      -- At most two args. The first arg (parent dir) has probably been
+      -- omitted, so we shift arguments and use the system's canonical
+      -- temporary directory.
+      let tmpl' = parentDir
+      callback' <- Lua.peek (fromAnyValue tmpl)
+      Temp.withSystemTempDirectory tmpl' (callWithFilename callback')
+    Just callback' -> do
+      -- all args given. Second value must be converted to a string.
+      tmpl' <- Lua.peek (fromAnyValue tmpl)
+      Temp.withTempDirectory parentDir tmpl' (callWithFilename callback')
+
+-- | Call Lua callback function with the given filename as its argument.
+callWithFilename :: Callback -> FilePath -> Lua NumResults
+callWithFilename callback filename = do
+  oldTop <- Lua.gettop
+  Lua.push callback
+  Lua.push filename
+  Lua.call (Lua.NumArgs 1) Lua.multret
+  newTop <- Lua.gettop
+  return . NumResults . fromIntegral . Lua.fromStackIndex $
+    newTop - oldTop
+
+-- | List the contents of a directory.
+ls :: Optional FilePath -> Lua [FilePath]
+ls fp = do
+  let fp' = fromMaybe "." (fromOptional fp)
+  ioToLua (Directory.listDirectory fp')
+
+-- | Change current working directory.
+chdir :: FilePath -> Lua ()
+chdir fp = ioToLua $ Directory.setCurrentDirectory fp
+
+-- | Return the current working directory.
+currentdir :: Lua FilePath
+currentdir = ioToLua Directory.getCurrentDirectory
+
+-- | Retrieve the entire environment
+env :: Lua NumResults
+env = do
+  kvs <- ioToLua Env.getEnvironment
+  let addValue (k, v) = Lua.push k *> Lua.push v *> Lua.rawset (-3)
+  Lua.newtable
+  mapM_ addValue kvs
+  return (NumResults 1)
+
+-- | Returns the value of an environment variable
+getenv :: String -> Lua (Optional String)
+getenv name = ioToLua (Optional <$> Env.lookupEnv name)
+
+-- | Set the specified environment variable to a new value.
+setenv :: String -> String -> Lua ()
+setenv name value = ioToLua (Env.setEnv name value)
+
+-- | Get the current directory for temporary files.
+tmpdirname :: Lua FilePath
+tmpdirname = ioToLua Directory.getTemporaryDirectory
+
+-- | Convert a System IO operation to a Lua operation.
+ioToLua :: IO a -> Lua a
+ioToLua action = do
+  result <- Lua.liftIO (try action)
+  case result of
+    Right result' -> return result'
+    Left err      -> Lua.throwException (show (err :: IOException))
diff --git a/test/test-hslua-module-system.hs b/test/test-hslua-module-system.hs
new file mode 100644
--- /dev/null
+++ b/test/test-hslua-module-system.hs
@@ -0,0 +1,54 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-|
+Module      : Main
+Copyright   : © 2019 Albert Krewinkel
+License     : MIT
+Maintainer  : Albert Krewinkel <albert+hslua@zeitkraut.de>
+Stability   : alpha
+Portability : Requires language extensions ForeignFunctionInterface,
+              OverloadedStrings.
+
+Tests for the `system` Lua module.
+-}
+
+import Control.Monad (void, when)
+import Foreign.Lua (Lua)
+import Foreign.Lua.Module.System (preloadModule, pushModule)
+import Test.Tasty (TestTree, defaultMain, testGroup)
+import Test.Tasty.HUnit (assertEqual, testCase)
+
+import qualified Foreign.Lua as Lua
+
+main :: IO ()
+main = defaultMain $ testGroup "hslua-module-system" [tests]
+
+-- | HSpec tests for the Lua 'system' module
+tests :: TestTree
+tests = testGroup "HsLua System module"
+  [ testCase "system module can be pushed to the stack" $
+      Lua.run (void pushModule)
+
+  , testCase "text module can be added to the preloader" . Lua.run $ do
+      Lua.openlibs
+      preloadModule "system"
+      assertEqual' "function not added to preloader" Lua.TypeFunction =<< do
+        Lua.getglobal' "package.preload.system"
+        Lua.ltype (-1)
+
+  , testCase "text module can be loaded as hstext" . Lua.run $ do
+      Lua.openlibs
+      preloadModule "system"
+      assertEqual' "loading the module fails " Lua.OK =<<
+        Lua.dostring "require 'system'"
+
+  , testCase "Lua tests pass" . Lua.run $ do
+      Lua.openlibs
+      preloadModule "system"
+      assertEqual' "error while running lua tests" Lua.OK =<< do
+        st <- Lua.loadfile "test/system-module-tests.lua"
+        when (st == Lua.OK) $ Lua.call 0 0
+        return st
+  ]
+
+assertEqual' :: (Show a, Eq a) => String -> a -> a -> Lua ()
+assertEqual' msg expected = Lua.liftIO . assertEqual msg expected
