diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Daniel Bergey (c) 2015
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Daniel Bergey nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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/app/Main.hs b/app/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/Main.hs
@@ -0,0 +1,22 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import           VC.Tree
+
+import           Shelly
+
+import           Control.Monad
+import           Data.Foldable
+import qualified Data.Text          as T
+import           System.Environment
+
+main :: IO ()
+main = do
+  args <- getArgs
+  let startingPath = case args of
+        [] -> "."
+        (d:_) -> fromText $ T.pack d
+  shelly $ do
+    fps <- directories <$> check startingPath
+    traverse_ (echo <=< toTextWarn) fps
diff --git a/src/VC/Tree.hs b/src/VC/Tree.hs
new file mode 100644
--- /dev/null
+++ b/src/VC/Tree.hs
@@ -0,0 +1,74 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE CPP #-}
+
+module VC.Tree where
+
+import           Filesystem.Path     (basename)
+import           Shelly
+
+import           Control.Monad.Extra
+
+import           Prelude             hiding (FilePath)
+
+#if __GLASGOW_HASKELL__ < 710
+import           Data.Traversable
+#endif
+
+check :: FilePath -> Sh Managed
+check dir = do
+  names <- ls dir
+  if checkVCDir names
+    then return Managed
+    else do
+    -- TODO exclude symlinks from files
+         (subdirs, files) <- partitionDirs =<< ls dir
+         managed <- traverse check subdirs
+         return $ summary dir (files == []) managed
+
+checkVCDir :: [FilePath] -> Bool
+checkVCDir xs = let names = map basename xs in
+  any (`elem` names)
+  [ ".git", "_darcs", ".hg", ".svn", "CVS", ".fslckout"]
+
+data Managed =
+  UnManaged FilePath
+  | Managed
+  | Empty
+  | SomeManaged [FilePath]
+
+summary :: FilePath -> Bool -> [Managed] -> Managed
+summary dir nofiles xs
+  | all isEmpty xs = if nofiles
+                     then Empty
+                     else UnManaged dir
+  | all isManaged xs = if nofiles then Managed
+                       else UnManaged dir
+  | all isUnmanaged xs = UnManaged dir
+  | otherwise = SomeManaged $ if nofiles
+                then concatMap directories xs
+                else dir : concatMap directories xs
+
+partitionDirs :: [FilePath] -> Sh ([FilePath], [FilePath])
+partitionDirs paths = do
+  (dirs, nondirs) <- partitionM test_d paths
+  files <- filterM (fmap not . test_s) nondirs -- exclude symlinks
+  return (dirs, files)
+
+isEmpty :: Managed -> Bool
+isEmpty Empty = True
+isEmpty _ = False
+
+isManaged :: Managed-> Bool
+isManaged Managed = True
+isManaged Empty = True
+isManaged _ = False
+
+isUnmanaged :: Managed -> Bool
+isUnmanaged (UnManaged _) = True
+isUnmanaged Empty = True
+isUnmanaged _ = False
+
+directories :: Managed -> [FilePath]
+directories (SomeManaged ds) = ds
+directories (UnManaged d) = [d]
+directories _ = []
diff --git a/vcatt.cabal b/vcatt.cabal
new file mode 100644
--- /dev/null
+++ b/vcatt.cabal
@@ -0,0 +1,37 @@
+name:                vcatt
+version:             0.1
+synopsis:            Recursively check that a directory is under version control.
+description:         Version control all the things.  Please see README.md
+homepage:            http://github.com/bergey/vcatt
+license:             BSD3
+license-file:        LICENSE
+author:              Daniel Bergey
+maintainer:          bergey@teallabs.org
+copyright:           BSD3
+category:            Web
+build-type:          Simple
+-- extra-source-files:
+cabal-version:       >=1.10
+
+library
+  hs-source-dirs:      src
+  exposed-modules:     VC.Tree
+  build-depends:       base >= 4.6 && < 5
+                     , shelly >= 1.6 && < 1.7
+                     , extra >= 1.0 && < 1.5
+                     , system-filepath >= 0.4.9 && < 0.5
+  default-language:    Haskell2010
+
+executable vcatt
+  hs-source-dirs:      app
+  main-is:             Main.hs
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
+  build-depends:       base
+                     , vcatt
+                     , shelly
+                     , text >= 1.2 && < 1.3
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/bergey/vcatt
