diff --git a/CHANGES.txt b/CHANGES.txt
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,7 @@
 Changelog for Weeder
 
+1.0.5, released 2018-05-05
+    #39, provide weeder as a library
 1.0.4, released 2018-05-02
     #38, make sure you parse bracketed version ranges properly
 1.0.3, released 2018-03-04
diff --git a/src/CmdLine.hs b/src/CmdLine.hs
--- a/src/CmdLine.hs
+++ b/src/CmdLine.hs
@@ -9,6 +9,7 @@
 import Paths_weeder
 import Data.Version
 import Data.Functor
+import System.Environment
 import Prelude
 
 
@@ -23,8 +24,8 @@
     ,cmdDistDir :: Maybe String
     } deriving (Show, Data, Typeable)
 
-getCmd :: IO Cmd
-getCmd = automatic <$> cmdArgsRun mode
+getCmd :: [String] -> IO Cmd
+getCmd args = withArgs args $ automatic <$> cmdArgsRun mode
 
 automatic :: Cmd -> Cmd
 automatic cmd
diff --git a/src/Main.hs b/src/Main.hs
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -1,79 +1,11 @@
-{-# LANGUAGE TupleSections, RecordWildCards, ScopedTypeVariables #-}
-
 module Main(main) where
 
-import Hi
-import Cabal
-import Stack
-import Data.List.Extra
-import Data.Functor
-import Data.Tuple.Extra
-import Control.Monad.Extra
-import System.Console.CmdArgs.Verbosity
+import Weeder
 import System.Exit
-import System.IO.Extra
-import qualified Data.HashMap.Strict as Map
-import System.Directory.Extra
-import System.FilePath
-import Check
-import Warning
-import CmdLine
-import Prelude
-
-
-data Result = Good | Bad deriving Eq
+import System.Environment
+import Control.Monad
 
 main :: IO ()
 main = do
-    cmd@Cmd{..} <- getCmd
-    res <- mapM (weedPath cmd) cmdProjects
-    when (Bad `elem` res) exitFailure
-
-
-weedPath :: Cmd -> FilePath -> IO Result
-weedPath Cmd{..} proj = do
-    -- project may either be a directory name, or a stack.yaml file
-    file <- do
-        isDir <- doesDirectoryExist proj
-        if isDir then findStack proj else return proj
-    whenLoud $ putStrLn $ "Running on Stack file " ++ file
-    when cmdBuild $ buildStack file
-    Stack{..} <- parseStack cmdDistDir file
-    cabals <- forM stackPackages $ \x -> do
-        file <- selectCabalFile x
-        (file,) <$> parseCabal file
-
-    ignore <- do
-        let x = takeDirectory file </> ".weeder.yaml"
-        b <- doesFileExist x
-        if not b then return [] else do
-            whenLoud $ putStrLn $ "Reading ignored warnings from " ++ x
-            readWarningsFile x
-    let quiet = cmdJson || cmdYaml
-
-    res <- forM cabals $ \(cabalFile, Cabal{..}) -> do
-        (fileToKey, keyToHi) <- hiParseDirectory $ takeDirectory cabalFile </> stackDistDir
-        let full = check (keyToHi Map.!) cabalName $
-                   map (id &&& selectHiFiles stackDistDir fileToKey) cabalSections
-        let warn = if cmdShowAll || cmdMatch then full else ignoreWarnings ignore full
-        unless quiet $
-            putStrLn $ unlines $ showWarningsPretty cabalName warn
-        return (length full - length warn, warn)
-    let (ignored, warns) = sum *** concat $ unzip res
-
-    when cmdJson $ putStrLn $ showWarningsJson warns
-    when cmdYaml $ putStrLn $ showWarningsYaml warns
-    if cmdMatch then
-        if sort ignore == sort warns then do
-            putStrLn "Warnings match"
-            return Good
-        else do
-            putStrLn "MISSING WARNINGS"
-            putStrLn $ unlines $ showWarningsPretty "" $ ignore \\ warns
-            putStrLn "EXTRA WARNINGS"
-            putStrLn $ unlines $ showWarningsPretty "" $ warns \\ ignore
-            return Bad
-     else do
-        when (ignored > 0 && not quiet) $
-            putStrLn $ "Ignored " ++ show ignored ++ " weeds (pass --show-all to see them)"
-        return $ if null warns then Good else Bad
+    bad <- weeder =<< getArgs
+    when (bad > 0) exitFailure
diff --git a/src/Weeder.hs b/src/Weeder.hs
new file mode 100644
--- /dev/null
+++ b/src/Weeder.hs
@@ -0,0 +1,80 @@
+{-# LANGUAGE TupleSections, RecordWildCards, ScopedTypeVariables #-}
+
+-- | Run the @weeder@ program as a direct dependency.
+--   You are encouraged to use the binary in preference to the library.
+module Weeder(weeder) where
+
+import Hi
+import Cabal
+import Stack
+import Data.List.Extra
+import Data.Functor
+import Data.Tuple.Extra
+import Control.Monad.Extra
+import System.Console.CmdArgs.Verbosity
+import System.IO.Extra
+import qualified Data.HashMap.Strict as Map
+import System.Directory.Extra
+import System.FilePath
+import Check
+import Warning
+import CmdLine
+import Prelude
+
+
+-- | Given the weeder command line arguments, return the number of warnings that were produced.
+--   If the number is @0@ that corresponds to a successful run.
+weeder :: [String] -> IO Int
+weeder args = do
+    cmd@Cmd{..} <- getCmd args
+    res <- mapM (weedPath cmd) cmdProjects
+    return $ sum res
+
+
+weedPath :: Cmd -> FilePath -> IO Int
+weedPath Cmd{..} proj = do
+    -- project may either be a directory name, or a stack.yaml file
+    file <- do
+        isDir <- doesDirectoryExist proj
+        if isDir then findStack proj else return proj
+    whenLoud $ putStrLn $ "Running on Stack file " ++ file
+    when cmdBuild $ buildStack file
+    Stack{..} <- parseStack cmdDistDir file
+    cabals <- forM stackPackages $ \x -> do
+        file <- selectCabalFile x
+        (file,) <$> parseCabal file
+
+    ignore <- do
+        let x = takeDirectory file </> ".weeder.yaml"
+        b <- doesFileExist x
+        if not b then return [] else do
+            whenLoud $ putStrLn $ "Reading ignored warnings from " ++ x
+            readWarningsFile x
+    let quiet = cmdJson || cmdYaml
+
+    res <- forM cabals $ \(cabalFile, Cabal{..}) -> do
+        (fileToKey, keyToHi) <- hiParseDirectory $ takeDirectory cabalFile </> stackDistDir
+        let full = check (keyToHi Map.!) cabalName $
+                   map (id &&& selectHiFiles stackDistDir fileToKey) cabalSections
+        let warn = if cmdShowAll || cmdMatch then full else ignoreWarnings ignore full
+        unless quiet $
+            putStrLn $ unlines $ showWarningsPretty cabalName warn
+        return (length full - length warn, warn)
+    let (ignored, warns) = sum *** concat $ unzip res
+
+    when cmdJson $ putStrLn $ showWarningsJson warns
+    when cmdYaml $ putStrLn $ showWarningsYaml warns
+    if cmdMatch then
+        if sort ignore == sort warns then do
+            putStrLn "Warnings match"
+            return 0
+        else do
+            putStrLn "MISSING WARNINGS"
+            putStrLn $ unlines $ showWarningsPretty "" $ ignore \\ warns
+            putStrLn "EXTRA WARNINGS"
+            putStrLn $ unlines $ showWarningsPretty "" $ warns \\ ignore
+            return 1
+     else do
+        when (ignored > 0 && not quiet) $
+            putStrLn $ "Ignored " ++ show ignored ++ " weeds (pass --show-all to see them)"
+        return $ length warns
diff --git a/weeder.cabal b/weeder.cabal
--- a/weeder.cabal
+++ b/weeder.cabal
@@ -1,7 +1,7 @@
 cabal-version:      >= 1.18
 build-type:         Simple
 name:               weeder
-version:            1.0.4
+version:            1.0.5
 license:            BSD3
 license-file:       LICENSE
 category:           Development
@@ -22,9 +22,8 @@
     type:     git
     location: https://github.com/ndmitchell/weeder.git
 
-executable weeder
+library
     default-language:   Haskell2010
-    main-is:            Main.hs
     hs-source-dirs:     src
     if impl(ghc < 8.0)
         build-depends: semigroups >= 0.18
@@ -45,6 +44,8 @@
         foundation >= 0.0.13,
         process >= 1.2.3.0,
         extra >= 1.6.4
+    exposed-modules:
+        Weeder
     other-modules:
         Cabal
         Hi
@@ -55,3 +56,10 @@
         CmdLine
         Str
         Paths_weeder
+
+executable weeder
+    default-language:   Haskell2010
+    main-is:            src/Main.hs
+    build-depends:
+        base,
+        weeder
