diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2017 Nicolas Mattia
+
+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,28 @@
+# Snipcheck
+
+Snipcheck makes sure that the code snippets in markdown files are up-to-date.
+
+This is very much a work in progress. The only function currently available is
+
+``` haskell
+checkMarkdownFile :: FilePath -> IO ()
+```
+
+that will run shell snippets and errored out if the output doesn't match the
+snippet. You can skip some of the output with `...`.
+
+## Example:
+
+    # Some title
+
+    some markdown content
+
+    ``` shell
+    $ echo foo; echo bar; echo baz; echo qux
+    foo
+    ...
+    qux
+    ```
+
+    some more content
+
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/snipcheck.cabal b/snipcheck.cabal
new file mode 100644
--- /dev/null
+++ b/snipcheck.cabal
@@ -0,0 +1,28 @@
+name: snipcheck
+version: 0.1.0.0
+cabal-version: >=1.10
+build-type: Simple
+license: MIT
+license-file: LICENSE
+copyright: 2017 Nicolas Mattia
+maintainer: nicolas@nmattia.com
+homepage: https://github.com/nmattia/snipcheck#readme
+synopsis: Markdown tester
+description:
+    Markdown snippet runner and checker
+category: Development
+author: Nicolas Mattia
+extra-source-files:
+    README.md
+
+library
+    exposed-modules:
+        Snipcheck
+    build-depends:
+        base >=4.7 && <5,
+        pandoc >=1.19.2.1 && <1.20,
+        process >=1.4.3.0 && <1.5
+    default-language: Haskell2010
+    hs-source-dirs: src
+    ghc-options: -Wall
+
diff --git a/src/Snipcheck.hs b/src/Snipcheck.hs
new file mode 100644
--- /dev/null
+++ b/src/Snipcheck.hs
@@ -0,0 +1,70 @@
+{-# LANGUAGE DeriveFunctor #-}
+
+module Snipcheck where
+
+import Control.Monad
+import Data.Maybe
+import Data.Monoid
+import System.Process(readCreateProcess, shell)
+import Text.Pandoc (Block(..))
+
+import qualified Text.Pandoc as Pandoc
+
+data Sloppy a = Skip | Must a deriving (Show, Functor)
+
+sloppyString :: String -> Sloppy String
+sloppyString "..." = Skip
+sloppyString str = Must str
+
+checkSloppy :: Eq a => [a] -> [Sloppy a] -> Bool
+checkSloppy (a:as) ((Must a'):as')
+  | a == a' = checkSloppy as as'
+  | otherwise = False
+checkSloppy (a:as) as'@(Skip:(Must a'):as'')
+  | a == a' = checkSloppy as as''
+  | otherwise = checkSloppy as as'
+checkSloppy as (Skip:Skip:as') = checkSloppy as (Skip:as')
+checkSloppy [] ((Must _):_) = False
+checkSloppy [] (Skip:as') = checkSloppy [] as'
+checkSloppy [] [] = True
+checkSloppy (_:_) [] = False
+checkSloppy _ [Skip] = True
+
+checkMarkdownFile :: FilePath -> IO ()
+checkMarkdownFile fp = do
+  content <- readFile fp
+  let (Right (Pandoc.Pandoc _meta blocks)) = Pandoc.readMarkdown Pandoc.def content
+  forM_ blocks check
+
+check :: Pandoc.Block -> IO ()
+check (CodeBlock (typ, classes, kvs) content)
+  | "shell" `elem` classes = do
+      let Right cmds = extractCommands content
+      forM_ cmds $ \(cmd, expected) -> do
+        actual <- lines <$> readCreateProcess (shell cmd) ""
+        let expected' = sloppyString <$> expected
+        unless (checkSloppy actual expected') $ error $ mconcat
+          [ "Couldnt match expected ", show expected'
+          , " with " <> show actual
+          ]
+  | otherwise = print (typ, classes, kvs)
+check _ = return ()
+
+extractCommands :: String -> Either String [(String, [String])]
+extractCommands str = go (lines str)
+  where
+    go :: [String] -> Either String [(String, [String])]
+    go (l:ls) | Just cmd <- toCommand l =
+      let (output, rest) = span (not . isCommand) ls
+      in ((cmd,output):) <$> (go rest)
+              | otherwise = Left $ "Expected a command, got " <> l
+    go [] = Right []
+    toCommand :: String -> Maybe String
+    toCommand ('$':cmd) = Just cmd
+    toCommand _ = Nothing
+    isCommand :: String -> Bool
+    isCommand = isJust . toCommand
+
+
+someFunc :: IO ()
+someFunc = putStrLn "someFunc"
