diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,12 @@
+# Changelog
+
+`hslua-repl` uses [PVP Versioning](https://pvp.haskell.org).
+
+## hslua-repl-0.1.0
+
+Release pending.
+
+-   A new package begins its way into the undiscovered land. May
+    it live long and prosper.
+
+  [PVP Versioning]: https://pvp.haskell.org
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright © 2023 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/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+hslua-repl
+==========
+
+An embeddable, isocline-based Lua REPL.
+
+Example
+-------
+
+``` haskell
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeApplications  #-}
+import HsLua.Core  as Lua (Exception, openlibs, run)
+import HsLua.REPL (defaultREPLConfig, startREPL)
+
+-- | Run a default Lua interpreter.
+main :: IO ()
+main = run @Lua.Exception $ do
+  startREPL defaultREPLConfig
+```
diff --git a/app/luarepl.hs b/app/luarepl.hs
new file mode 100644
--- /dev/null
+++ b/app/luarepl.hs
@@ -0,0 +1,28 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeApplications  #-}
+{- |
+Module      : Main
+Copyright   : © 2022-2023 Albert Krewinkel
+License     : MIT
+Maintainer  : Albert Krewinkel <tarleb@hslua.org>
+
+A simple Lua REPL.
+-}
+module Main (main) where
+import Control.Monad (void)
+import HsLua.Core  as Lua (Exception, openlibs, run)
+import HsLua.REPL (Config (..), defaultConfig, repl, setup)
+import qualified Data.Text as T
+
+-- | Run a default Lua interpreter.
+main :: IO ()
+main = run @Lua.Exception $ do
+  let config = Config
+        { replPrompt = "luarepl"
+        , replInfo = replInfo defaultConfig `T.append`
+                     "\nREPL: © 2023 Albert Krewinkel"
+        , replHistory = Just ".luarepl-history"
+        }
+  openlibs
+  setup config
+  void repl
diff --git a/hslua-repl.cabal b/hslua-repl.cabal
new file mode 100644
--- /dev/null
+++ b/hslua-repl.cabal
@@ -0,0 +1,78 @@
+cabal-version:       2.2
+name:                hslua-repl
+version:             0.1.0
+synopsis:            Isocline-based Lua REPL
+description:         An embeddable Lua REPL built with Isocline and HsLua.
+homepage:            https://hslua.org/
+bug-reports:         https://github.com/hslua/hslua/issues
+license:             MIT
+license-file:        LICENSE
+author:              Albert Krewinkel
+maintainer:          Albert Krewinkel <tarleb@hslua.org>
+copyright:           © lbert Krewinkel
+category:            Foreign
+build-type:          Simple
+extra-doc-files:     README.md
+                     CHANGELOG.md
+tested-with:         GHC == 8.4.4
+                   , GHC == 8.6.5
+                   , GHC == 8.8.4
+                   , GHC == 8.10.7
+                   , GHC == 9.0.2
+                   , GHC == 9.2.5
+                   , GHC == 9.4.4
+
+source-repository head
+  type:                git
+  location:            https://github.com/hslua/hslua
+  subdir:              hslua-repl
+
+Flag executable
+  description:         Compile `luarepl` standalone executable.
+  default:             False
+
+common common-options
+  build-depends:       base              >= 4.9.1  && < 5
+                     , hslua-core        >= 2.3    && < 2.4
+                     , text              >= 1.2    && < 2.1
+
+  ghc-options:         -Wall
+                       -Wcpp-undef
+                       -Widentities
+                       -Wincomplete-record-updates
+                       -Wincomplete-uni-patterns
+                       -Wmissing-export-lists
+                       -Wmissing-signatures
+                       -Wnoncanonical-monad-instances
+                       -Wpartial-fields
+                       -Wredundant-constraints
+                       -fhide-source-paths
+
+  if impl(ghc >= 8.10)
+    ghc-options:       -Wunused-packages
+
+  if impl(ghc >= 9.0)
+    ghc-options:       -Winvalid-haddock
+
+
+  default-language:    Haskell2010
+
+library
+  import:              common-options
+  hs-source-dirs:      src
+  exposed-modules:     HsLua.REPL
+  build-depends:       base              >= 4.11   && < 5
+                     , bytestring        >= 0.10   && < 0.12
+                     , isocline          >= 1.0    && < 1.1
+                     , lua               >= 2.3    && < 2.4
+
+executable luarepl
+  import:              common-options
+  hs-source-dirs:      app
+  main-is:             luarepl.hs
+  ghc-options:         -rtsopts -with-rtsopts=-A8m -threaded
+  if flag(executable)
+    buildable:           True
+    build-depends:       hslua-repl
+  else
+   buildable:            False
diff --git a/src/HsLua/REPL.hs b/src/HsLua/REPL.hs
new file mode 100644
--- /dev/null
+++ b/src/HsLua/REPL.hs
@@ -0,0 +1,123 @@
+{-# LANGUAGE CPP               #-}
+{-# LANGUAGE LambdaCase        #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE PatternSynonyms   #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{- |
+Module      : HsLua.REPL
+Copyright   : Copyright © 2017-2023 Albert Krewinkel
+License     : MIT
+Maintainer  : Albert Krewinkel <tarleb@hslua.org>
+
+Embeddable Lua interpreter interface.
+-}
+module HsLua.REPL
+  ( -- * Run scripts as program
+    repl
+  , setup
+  , Config (..)
+  , defaultConfig
+  ) where
+
+import Control.Applicative ((<|>))
+import Control.Monad (void, when)
+import Data.ByteString (ByteString)
+import Data.Text (Text)
+import Lua (pattern LUA_COPYRIGHT)
+import HsLua.Core
+import System.Console.Isocline
+import qualified Data.ByteString.Char8 as Char8
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
+import qualified HsLua.Core.Utf8 as UTF8
+
+-- | Lua runner command line options.
+data Config = Config
+  { replPrompt     :: Text
+  , replInfo       :: Text
+  , replHistory    :: Maybe FilePath
+  }
+
+defaultConfig :: Config
+defaultConfig = Config
+  { replPrompt = ""
+  , replInfo = T.pack LUA_COPYRIGHT
+  , replHistory = Nothing
+  }
+
+-- | Setup a new repl. Prints the version and extra info before the
+-- first prompt.
+setup :: Config -> LuaE e ()
+setup config = do
+  liftIO $ T.putStrLn (replInfo config)
+  case replHistory config of
+    Just histfile -> liftIO $ setHistory histfile 200
+    Nothing -> pure ()
+
+-- | Checks if the error message hints at incomplete input. Removes the
+-- message from the stack in that case.
+incomplete :: LuaError e => LuaE e Bool
+incomplete = do
+  let eofmark = "<eof>"
+  msg <- tostring' top
+  if eofmark `Char8.isSuffixOf` msg
+    then True  <$ pop 2  -- error message (duplicated by tostring')
+    else False <$ pop 1  -- value pushed by tostring'
+
+-- | Load an input string, mark it as coming from @stdin@.
+loadinput :: ByteString -> LuaE e Status
+loadinput inp = loadbuffer inp "=stdin"
+
+-- | Try to load input while prepending a @return@ statement.
+loadExpression :: LuaError e => ByteString -> LuaE e ()
+loadExpression input = loadinput ("return " <> input) >>= \case
+  OK -> pure ()  -- yep, that worked
+  _err   -> throwErrorAsException
+
+-- | Load a multiline statement; prompts for more lines if the statement
+-- looks incomplete.
+loadStatement :: LuaError e
+              => [ByteString]      -- ^ input lines
+              -> LuaE e ()
+loadStatement lns = do
+  loadinput (Char8.unlines $ reverse lns) >>= \case
+    OK -> pure ()
+    ErrSyntax -> incomplete >>= \isincmplt ->
+      if isincmplt
+      then liftIO (readlineMaybe ">") >>= \case
+        Nothing    -> failLua "Multiline input aborted"
+        Just input -> loadStatement (UTF8.fromString input : lns)
+      else throwErrorAsException
+    _ -> throwErrorAsException
+
+-- | Run a Lua repl.
+repl :: LuaError e => LuaE e NumResults
+repl = try repl' >>= \case
+  Right n  -> pure n -- Ctrl-D or Ctrl-C
+  Left err -> do
+    -- something went wrong: report error, reset stack and try again
+    void $ getglobal "print"
+    pushException err
+    call 1 0
+    settop 0
+    repl
+ where
+  repl' :: LuaError e => LuaE e NumResults
+  repl' = liftIO (readlineMaybe "") >>= \case
+    Nothing ->
+      -- Return all values left on the stack as results
+      NumResults . fromStackIndex <$> gettop
+    Just inputStr -> do
+      settop 0  -- reset stack
+      let input = UTF8.fromString inputStr
+      loadExpression input <|> loadStatement [input]
+      -- run loaded input
+      callTrace 0 multret
+      nvalues <- gettop
+      -- duplicate everything and call print on the results
+      checkstack' (fromIntegral (fromStackIndex nvalues) + 1) "repl'"
+      when (nvalues > 0) $ do
+        void $ getglobal "print"
+        mapM_ pushvalue [1..nvalues]
+        call (fromIntegral $ fromStackIndex nvalues) 0
+      repl'
