diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2017 Richard Cook
+
+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,11 @@
+# `hidden-char`
+
+Provides a `getHiddenChar` function that works on Windows, Linux and macOS
+
+## Licence
+
+[MIT License][licence]
+
+Copyright &copy; 2017, Richard Cook.
+
+[licence]: LICENSE
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,16 @@
+{-|
+Module      : Main
+Description : Setup entrypoint for @hidden-char@
+Copyright   : (C) Richard Cook, 2017
+Licence     : MIT
+Maintainer  : rcook@rcook.org
+Stability   : stable
+Portability : portable
+-}
+
+module Main (main) where
+
+import           Distribution.Simple
+
+main :: IO ()
+main = defaultMain
diff --git a/hidden-char.cabal b/hidden-char.cabal
new file mode 100644
--- /dev/null
+++ b/hidden-char.cabal
@@ -0,0 +1,45 @@
+name:                               hidden-char
+version:                            0.1.0.0
+synopsis:                           Provides getHiddenChar function
+description:                        Provides a @getHiddenChar@ function that works on Windows, Linux and macOS
+homepage:                           https://github.com/rcook/hidden-char#readme
+license:                            MIT
+license-file:                       LICENSE
+author:                             Richard Cook
+maintainer:                         rcook@rcook.org
+copyright:                          2017 Richard Cook
+category:                           Command Line Tool
+build-type:                         Simple
+cabal-version:                      >= 1.10
+extra-source-files:                 README.md
+
+library
+  default-language:                 Haskell2010
+  ghc-options:                      -W -Wall
+  hs-source-dirs:                   src
+  build-depends:                    base >= 4.7 && < 5
+  exposed-modules:                  System.IO.HiddenChar
+  if os(linux)
+    cpp-options:                    -DOS_LINUX
+    other-modules:                  System.IO.HiddenChar.Posix
+  if os(windows)
+    cpp-options:                    -DOS_WINDOWS
+    other-modules:                  System.IO.HiddenChar.Windows
+  if os(darwin)
+    cpp-options:                    -DOS_MACOS
+    other-modules:                  System.IO.HiddenChar.Posix
+
+test-suite hidden-char-test
+  type:                             exitcode-stdio-1.0
+  default-language:                 Haskell2010
+  ghc-options:                      -W -Wall
+  hs-source-dirs:                   test
+  main-is:                          Main.hs
+  build-depends:                    base >= 4.7 && < 5
+                                  , hspec
+  if os(linux)
+    cpp-options:                    -DOS_LINUX
+  if os(windows)
+    cpp-options:                    -DOS_WINDOWS
+  if os(darwin)
+    cpp-options:                    -DOS_MACOS
diff --git a/src/System/IO/HiddenChar.hs b/src/System/IO/HiddenChar.hs
new file mode 100644
--- /dev/null
+++ b/src/System/IO/HiddenChar.hs
@@ -0,0 +1,26 @@
+{-|
+Module      : System.IO.HiddenChar
+Description : Umbrella module for @System.IO.HiddenChar@
+Copyright   : (C) Richard Cook, 2017
+Licence     : MIT
+Maintainer  : rcook@rcook.org
+Stability   : stable
+Portability : portable
+
+This package provides a @getHiddenChar@ function which works reasonably
+consistently across the Windows, Linux and macOS platforms. @getHiddenChar@
+yields a single character from the standard input device with buffering and
+echoing to standard output disabled.
+-}
+
+{-# LANGUAGE CPP #-}
+
+module System.IO.HiddenChar (getHiddenChar) where
+
+#if defined(OS_LINUX) || defined(OS_MACOS)
+import           System.IO.HiddenChar.Posix
+#elif defined(OS_WINDOWS)
+import           System.IO.HiddenChar.Windows
+#else
+#error Unsupported platform
+#endif
diff --git a/src/System/IO/HiddenChar/Posix.hs b/src/System/IO/HiddenChar/Posix.hs
new file mode 100644
--- /dev/null
+++ b/src/System/IO/HiddenChar/Posix.hs
@@ -0,0 +1,41 @@
+{-|
+Module      : System.IO.HiddenChar.Posix
+Description : Posix implementation of @getHiddenChar@
+Copyright   : (C) Richard Cook, 2017
+Licence     : MIT
+Maintainer  : rcook@rcook.org
+Stability   : stable
+Portability : portable
+-}
+
+module System.IO.HiddenChar.Posix (getHiddenChar) where
+
+import           Control.Exception
+import           System.IO
+
+data HandleState = HandleState BufferMode Bool
+
+hGetState :: Handle -> IO HandleState
+hGetState h = do
+    bufferMode <- hGetBuffering h
+    isEcho <- hGetEcho h
+    return $ HandleState bufferMode isEcho
+
+hSetState :: Handle -> HandleState -> IO ()
+hSetState h (HandleState mode isEcho) = do
+    hSetEcho h isEcho
+    hSetBuffering h mode
+
+bracketHandle :: Handle -> (IO a -> IO a)
+bracketHandle h action = bracket
+    (hGetState h)
+    (hSetState h)
+    (const action)
+
+-- | Read a character from the standard input device with buffering and echoing disabled
+getHiddenChar ::
+    IO Char -- ^ returned character
+getHiddenChar = bracketHandle stdin $ do
+    hSetBuffering stdin NoBuffering
+    hSetEcho stdin False
+    getChar
diff --git a/src/System/IO/HiddenChar/Windows.hs b/src/System/IO/HiddenChar/Windows.hs
new file mode 100644
--- /dev/null
+++ b/src/System/IO/HiddenChar/Windows.hs
@@ -0,0 +1,24 @@
+{-|
+Module      : System.IO.HiddenChar.Windows
+Description : Windows implementation of @getHiddenChar@
+Copyright   : (C) Richard Cook, 2017
+Licence     : MIT
+Maintainer  : rcook@rcook.org
+Stability   : stable
+Portability : portable
+-}
+
+{-# LANGUAGE ForeignFunctionInterface #-}
+
+module System.IO.HiddenChar.Windows (getHiddenChar) where
+
+import           Data.Char
+import           Foreign.C.Types
+
+foreign import ccall unsafe "conio.h getch" c_getch :: IO CInt
+
+-- | Read a character from the standard input device with buffering and echoing disabled
+-- Hack based on http://stackoverflow.com/questions/2983974/haskell-read-input-character-from-console-immediately-not-after-newline
+getHiddenChar ::
+    IO Char -- ^ returned character
+getHiddenChar = fmap (chr . fromEnum) c_getch
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,11 @@
+{-|
+Module      : Main
+Description : Main entrypoint for @hidden-char@ tests
+Copyright   : (C) Richard Cook, 2017
+Licence     : MIT
+Maintainer  : rcook@rcook.org
+Stability   : stable
+Portability : portable
+-}
+
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
