diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,17 @@
 
 `hslua-module-system` uses [PVP Versioning][].
 
+## hslua-module-system-1.1.3
+
+Released 2025-05-21.
+
+-   Improved docs for the `os` field.
+
+-   Added new function `cmd` that runs system commands.
+
+-   Moved `CHANGELOG.md` to the `extra-doc-files` field in the
+    cabal file and also added `README.md` to that field.
+
 ## hslua-module-system-1.1.2
 
 Released 2024-05-28.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,312 @@
+HsLua Module: System
+====================
+
+This module provides access to system information and functionality 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.
+
+Example
+-------
+
+``` haskell
+loadProg :: Lua Status
+loadProg = do
+  openlibs
+  preloadModule documentedModule
+  -- create a temporary directory, print it's path, then delete it again.
+  dostring $ "system = require 'system'\n"
+          <> "system.with_tmpdir('.', 'foo', print)"
+```
+
+Documentation
+-------------
+
+### Fields
+
+#### arch
+
+The machine architecture on which the program is running.
+
+#### compiler_name
+
+The Haskell implementation with which the host program was compiled.
+
+#### compiler_version
+
+The version of `compiler_name` with which the host program was compiled.
+
+#### cputime_precision
+
+The smallest measurable difference in CPU time that the
+implementation can record, and is given as an integral number of
+picoseconds.
+
+#### os
+
+The operating system on which the program is running.
+
+### General Functions
+
+#### cmd
+
+`cmd (command, args[, input[, opts]])`
+
+Executes a system command with the given arguments and `input` on
+*stdin*.
+
+Parameters:
+
+`command`
+:   command to execute (string)
+
+`args`
+:   command arguments ({string,...})
+
+`input`
+:   input on stdin (string)
+
+`opts`
+:   process options (table)
+
+Returns:
+
+- exit code – `false` on success, an integer otherwise
+  (integer|boolean)
+
+- stdout (string)
+
+- stderr (string)
+
+
+#### cputime
+
+`cputime ()`
+
+Returns the number of picoseconds CPU time used by the current
+program. The precision of this result may vary in different
+versions and on different platforms. See also the field
+`cputime_precision`.
+
+#### env
+
+`env ()`
+
+Retrieve the entire environment.
+
+Returns:
+
+- A table mapping environment variables names to their string value
+  (table).
+
+#### getenv
+
+`getenv (var)`
+
+Return the value of the environment variable `var`, or `nil` if there
+is no such value.
+
+Parameters:
+
+`var`
+:   name of the environment variable (string)
+
+Returns:
+
+- value of the variable, or nil if the variable is not defined (string
+  or nil).
+
+#### getwd
+
+`getwd ()`
+
+Obtain the current working directory as an absolute path.
+
+Returns:
+
+- The current working directory (string).
+
+#### ls
+
+`ls ([directory])`
+
+List the contents of a directory.
+
+Parameters:
+
+`directory`
+:   Path of the directory whose contents should be listed (string).
+    Defaults to `.`.
+
+Returns:
+
+- A table of all entries in `directory` without the special entries (`.`
+  and `..`).
+
+#### mkdir
+
+`mkdir (dirname [, create_parent])`
+
+Create a new directory which is initially empty, or as near to
+empty as the operating system allows. The function throws an
+error if the directory cannot be created, e.g., if the parent
+directory does not exist or if a directory of the same name is
+already present.
+
+If the optional second parameter is provided and truthy, then all
+directories, including parent directories, are created as
+necessary.
+
+Parameters:
+
+`dirname`
+:   name of the new directory
+
+`create_parent`
+:   create parent directories if necessary
+
+#### rmdir
+
+`rmdir (dirname [, recursive])`
+
+Remove an existing, empty directory. If `recursive` is given,
+then delete the directory and its contents recursively.
+
+Parameters:
+
+`dirname`
+:   name of the directory to delete
+
+`recursive`
+:   delete content recursively
+
+#### setenv
+
+`setenv (var, value)`
+
+Set the specified environment variable to a new value.
+
+Parameters:
+
+`var`
+:   name of the environment variable (string).
+
+`value`
+:   new value (string).
+
+#### setwd
+
+`setwd (directory)`
+
+Change the working directory to the given path.
+
+Parameters:
+
+`directory`
+:   Path of the directory which is to become the new working
+    directory (string)
+
+#### tmpdirname
+
+`tmpdirname ()`
+
+Returns the current directory for temporary files.
+
+On Unix, `tmpdirname()` returns the value of the `TMPDIR` environment
+variable or "/tmp" if the variable isn't defined. On Windows, the
+function checks for the existence of environment variables in the
+following order and uses the first path found:
+
+- TMP environment variable.
+- TEMP environment variable.
+- USERPROFILE environment variable.
+- The Windows directory
+
+The operation may fail if the operating system has no notion of
+temporary directory.
+
+The function doesn't verify whether the path exists.
+
+Returns:
+
+- The current directory for temporary files (string).
+
+#### with\_env
+
+`with_env (environment, callback)`
+
+Run an action within a custom environment. Only the environment
+variables given by `environment` will be set, when `callback` is
+called. The original environment is restored after this function
+finishes, even if an error occurs while running the callback
+action.
+
+Parameters:
+
+`environment`
+:   Environment variables and their values to be set before
+    running `callback`. (table with string keys and string
+    values)
+
+`callback`
+:   Action to execute in the custom environment (function)
+
+Returns:
+
+- The result(s) of the call to `callback`
+
+#### with\_tmpdir
+
+`with_tmpdir ([parent_dir,] templ, callback)`
+
+Create and use a temporary directory inside the given directory.
+The directory is deleted after use.
+
+Parameters:
+
+`parent_dir`
+:   Parent directory to create the directory in (string). If this
+    parameter is omitted, the system's canonical temporary directory is
+    used.
+
+`templ`
+:   Directory name template (string).
+
+`callback`
+:   Function which takes the name of the temporary directory as its
+    first argument (function).
+
+Returns:
+
+- The result of the call to `callback`.
+
+#### with\_wd
+
+`with_wd (directory, callback)`
+
+Run an action within a different directory. This function will
+change the working directory to `directory`, execute `callback`,
+then switch back to the original working directory, even if an
+error occurs while running the callback action.
+
+Parameters:
+
+`directory`
+:   Directory in which the given `callback` should be executed
+    (string)
+
+`callback`
+:   Action to execute in the given directory (function)
+
+Returns:
+
+- The result(s) of the call to `callback`
+
+
+License
+-------
+
+This package is licensed under the MIT license. See [`LICENSE`](LICENSE)
+for details.
diff --git a/hslua-module-system.cabal b/hslua-module-system.cabal
--- a/hslua-module-system.cabal
+++ b/hslua-module-system.cabal
@@ -1,6 +1,6 @@
 cabal-version:       2.2
 name:                hslua-module-system
-version:             1.1.2
+version:             1.1.3
 synopsis:            Lua module wrapper around Haskell's System module.
 
 description:         Provides access to system information and
@@ -15,10 +15,11 @@
 license-file:        LICENSE
 author:              Albert Krewinkel
 maintainer:          tarleb@hslua.org
-copyright:           © 2019-2024 Albert Krewinkel <tarleb@hslua.org>
+copyright:           © 2019-2025 Albert Krewinkel <tarleb@hslua.org>
 category:            Foreign
-extra-source-files:  CHANGELOG.md
-                   , test/test-system.lua
+extra-source-files:  test/test-system.lua
+extra-doc-files:     CHANGELOG.md
+                   , README.md
 tested-with:         GHC == 8.4.4
                    , GHC == 8.6.5
                    , GHC == 8.8.3
@@ -61,6 +62,7 @@
   build-depends:       directory            >= 1.3    && < 1.4
                      , exceptions           >= 0.8    && < 0.11
                      , hslua-marshalling    >= 2.1    && < 2.4
+                     , process              >= 1.2.3  && < 1.7
                      , temporary            >= 1.2    && < 1.4
   exposed-modules:     HsLua.Module.System
   other-modules:       HsLua.Module.SystemUtils
diff --git a/src/HsLua/Module/System.hs b/src/HsLua/Module/System.hs
--- a/src/HsLua/Module/System.hs
+++ b/src/HsLua/Module/System.hs
@@ -1,6 +1,6 @@
 {-|
 Module      : HsLua.Module.System
-Copyright   : © 2019-2024 Albert Krewinkel
+Copyright   : © 2019-2025 Albert Krewinkel
 License     : MIT
 Maintainer  : Albert Krewinkel <tarleb@hslua.org>
 Stability   : alpha
@@ -27,6 +27,7 @@
   , ls
   , mkdir
   , rmdir
+  , run
   , setenv
   , setwd
   , tmpdirname
@@ -50,8 +51,10 @@
 import qualified System.CPUTime as CPUTime
 import qualified System.Directory as Directory
 import qualified System.Environment as Env
+import qualified System.Exit as Exit
 import qualified System.Info as Info
 import qualified System.IO.Temp as Temp
+import qualified System.Process as Process
 
 -- | The "system" module.
 documentedModule :: LuaError e => Module e
@@ -65,7 +68,8 @@
       , os
       ]
   , moduleFunctions =
-      [ cputime
+      [ cmd
+      , cputime
       , env
       , getenv
       , getwd
@@ -143,7 +147,11 @@
 os = Field
   { fieldName = "os"
   , fieldType = "string"
-  , fieldDescription = "The operating system on which the program is running."
+  , fieldDescription = T.unlines
+    [ "The operating system on which the program is running."
+    , "The most common values are `darwin` (macOS), `freebsd`, `linux`,"
+    , "`linux-android`, `mingw32` (Windows), `netbsd`, `openbsd`."
+    ]
   , fieldPushValue = pushString Info.os
   }
 
@@ -152,6 +160,29 @@
 -- Functions
 --
 
+-- | Run a system command
+cmd :: LuaError e => DocumentedFunction e
+cmd = defun "cmd"
+  ### (\command args minput opts -> do
+          let input = fromMaybe "" minput
+          let cp_opts = (Process.proc command args)
+                        { Process.env = processOptsEnv =<< opts
+                        , Process.cwd = processOptsCwd =<< opts
+                        }
+          liftIO $ Process.readCreateProcessWithExitCode cp_opts input)
+  <#> filepathParam "command" "command to execute"
+  <#> parameter (peekList peekString) "{string,...}" "args" "command arguments"
+  <#> opt (parameter peekString "string" "input" "input on stdin")
+  <#> opt (parameter peekProcessOptions "table" "opts" "process options")
+  =#> (functionResult (pushExitCode . (\(a,_,_) -> a)) "integer|boolean"
+                      "exit code – `false` on success, an integer otherwise" <>
+       functionResult (pushString . \(_,b,_) -> b) "string" "stdout" <>
+       functionResult (pushString . \(_,_,c) -> c) "string" "stderr")
+  #? T.unlines
+     [ "Executes a system command with the given arguments and `input`"
+     , "on *stdin*."
+     ]
+
 -- | Access the CPU time, e.g. for benchmarking.
 cputime :: LuaError e => DocumentedFunction e
 cputime = defun "cputime"
@@ -385,3 +416,32 @@
 filepathResult :: Text -- ^ Description
                -> [FunctionResult e FilePath]
 filepathResult = functionResult pushString "string"
+
+--
+-- Process parameters
+--
+
+-- | Process options
+data ProcessOpts = ProcessOpts
+  { processOptsEnv  :: Maybe [(String, String)]
+  , processOptsCwd  :: Maybe FilePath
+  }
+
+-- | Peek process creation options
+peekProcessOptions :: LuaError e => Peeker e ProcessOpts
+peekProcessOptions = typeChecked "table" istable $ \idx -> do
+  let peekEnv = peekKeyValuePairs peekString peekString
+  env' <- peekFieldRaw (peekNilOr peekEnv)    "env" idx
+  cwd' <- peekFieldRaw (peekNilOr peekString) "cwd" idx
+  return $ ProcessOpts
+    { processOptsEnv = env'
+    , processOptsCwd = cwd'
+    }
+
+-- | Pushes an exit code; failure codes are pushed as integers, and
+-- success is pushed as `false`. This means that the value can be
+-- interpreted as a boolean `failed` value.
+pushExitCode :: Pusher e Exit.ExitCode
+pushExitCode = \case
+  Exit.ExitSuccess   -> pushBool False
+  Exit.ExitFailure n -> pushIntegral n
diff --git a/src/HsLua/Module/SystemUtils.hs b/src/HsLua/Module/SystemUtils.hs
--- a/src/HsLua/Module/SystemUtils.hs
+++ b/src/HsLua/Module/SystemUtils.hs
@@ -1,6 +1,6 @@
 {-|
 Module      : HsLua.Module.SystemUtils
-Copyright   : © 2019-2024 Albert Krewinkel
+Copyright   : © 2019-2025 Albert Krewinkel
 License     : MIT
 Maintainer  : Albert Krewinkel <tarleb@hslua.org>
 
