diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,14 @@
+1.4.0
+
+* BREAKING CHANGE: Remove unnecessary `Maybe` from type of `single`
+* BREAKING CHANGE: Consolidate `searchable` and `executable`
+* `stream{,WithErr}` now throws an `ExitCode` on failure
+
+1.3.6
+
+* Build against `ghc-8.2`
+* Relax upper bound on `optparse-applicative` and `foldl`
+
 1.3.5
 
 * Increase upper bound on `foldl`
diff --git a/src/Turtle/Bytes.hs b/src/Turtle/Bytes.hs
--- a/src/Turtle/Bytes.hs
+++ b/src/Turtle/Bytes.hs
@@ -485,9 +485,18 @@
     -- ^ Chunks of bytes read from process output
 inshell cmd = stream (Process.shell (Data.Text.unpack cmd))
 
+waitForProcessThrows :: Process.ProcessHandle -> IO ()
+waitForProcessThrows ph = do
+    exitCode <- Process.waitForProcess ph
+    case exitCode of
+        ExitSuccess   -> return ()
+        ExitFailure _ -> Exception.throwIO exitCode
+
 {-| `stream` generalizes `inproc` and `inshell` by allowing you to supply your
     own custom `CreateProcess`.  This is for advanced users who feel comfortable
     using the lower-level @process@ API
+
+    Throws an `ExitCode` exception if the command returns a non-zero exit code
 -}
 stream
     :: Process.CreateProcess
@@ -527,11 +536,13 @@
         (Managed.managed (\k ->
             Exception.mask (\restore ->
                 Async.withAsync (feedIn restore) k ) ))
-    inhandle hOut <|> (liftIO (Process.waitForProcess ph *> halt a) *> empty)
+    inhandle hOut <|> (liftIO (waitForProcessThrows ph *> halt a) *> empty)
 
 {-| `streamWithErr` generalizes `inprocWithErr` and `inshellWithErr` by allowing
     you to supply your own custom `CreateProcess`.  This is for advanced users
     who feel comfortable using the lower-level @process@ API
+
+    Throws an `ExitCode` exception if the command returns a non-zero exit code
 -}
 streamWithErr
     :: Process.CreateProcess
@@ -611,12 +622,13 @@
             _ <- r
             return ()
     let waitAll = STM.atomically (Async.waitSTM a `also` (Async.waitSTM b `also` Async.waitSTM c))
-    drain <|> (liftIO (Process.waitForProcess ph *> waitAll) *> empty)
+    drain <|> (liftIO (waitForProcessThrows ph *> waitAll) *> empty)
 
 {-| Run a command using the shell, streaming @stdout@ and @stderr@ as chunks of
     `ByteString`.  Chunks from @stdout@ are wrapped in `Right` and chunks from
-    @stderr@ are wrapped in `Left`.  This does /not/ throw an exception if the
-    command returns a non-zero exit code
+    @stderr@ are wrapped in `Left`.
+
+    Throws an `ExitCode` exception if the command returns a non-zero exit code
 -}
 inprocWithErr
     :: Text
@@ -633,11 +645,12 @@
 
 {-| Run a command line using the shell, streaming @stdout@ and @stderr@ as
     chunks of `ByteString`.  Chunks from @stdout@ are wrapped in `Right` and
-    chunks from @stderr@ are wrapped in `Left`.  This does /not/ throw an
-    exception if the command returns a non-zero exit code
+    chunks from @stderr@ are wrapped in `Left`.
 
     This command is more powerful than `inprocWithErr`, but highly vulnerable to
     code injection if you template the command line with untrusted input
+
+    Throws an `ExitCode` exception if the command returns a non-zero exit code
 -}
 inshellWithErr
     :: Text
diff --git a/src/Turtle/Prelude.hs b/src/Turtle/Prelude.hs
--- a/src/Turtle/Prelude.hs
+++ b/src/Turtle/Prelude.hs
@@ -214,7 +214,7 @@
     , systemStrictWithErr
 
     -- * Permissions
-    , Permissions
+    , Permissions(..)
     , chmod
     , getmod
     , setmod
@@ -222,8 +222,7 @@
     , readable, nonreadable
     , writable, nonwritable
     , executable, nonexecutable
-    , searchable, nonsearchable
-    , ooo,roo,owo,oox,oos,rwo,rox,ros,owx,rwx,rws
+    , ooo,roo,owo,oox,rwo,rox,owx,rwx
 
     -- * File size
     , du
@@ -307,7 +306,7 @@
     lookupEnv,
 #endif
     getEnvironment )
-import System.Directory (Permissions)
+import qualified System.Directory
 import qualified System.Directory as Directory
 import System.Exit (ExitCode(..), exitWith)
 import System.IO (Handle, hClose)
@@ -669,9 +668,18 @@
     -- ^ Lines of standard output
 inshell cmd = stream (Process.shell (unpack cmd))
 
+waitForProcessThrows :: Process.ProcessHandle -> IO ()
+waitForProcessThrows ph = do
+    exitCode <- Process.waitForProcess ph
+    case exitCode of
+        ExitSuccess   -> return ()
+        ExitFailure _ -> Control.Exception.throwIO exitCode
+
 {-| `stream` generalizes `inproc` and `inshell` by allowing you to supply your
     own custom `CreateProcess`.  This is for advanced users who feel comfortable
     using the lower-level @process@ API
+
+    Throws an `ExitCode` exception if the command returns a non-zero exit code
 -}
 stream
     :: Process.CreateProcess
@@ -706,11 +714,13 @@
     a <- using
         (managed (\k ->
             mask (\restore -> withAsync (feedIn restore) (restore . k))))
-    inhandle hOut <|> (liftIO (Process.waitForProcess ph *> halt a) *> empty)
+    inhandle hOut <|> (liftIO (waitForProcessThrows ph *> halt a) *> empty)
 
 {-| `streamWithErr` generalizes `inprocWithErr` and `inshellWithErr` by allowing
     you to supply your own custom `CreateProcess`.  This is for advanced users
     who feel comfortable using the lower-level @process@ API
+
+    Throws an `ExitCode` exception if the command returns a non-zero exit code
 -}
 streamWithErr
     :: Process.CreateProcess
@@ -783,12 +793,13 @@
             _ <- r
             return ()
     let waitAll = STM.atomically (waitSTM a `also` (waitSTM b `also` waitSTM c))
-    drain <|> (liftIO (Process.waitForProcess ph *> waitAll) *> empty)
+    drain <|> (liftIO (waitForProcessThrows ph *> waitAll) *> empty)
 
 {-| Run a command using the shell, streaming @stdout@ and @stderr@ as lines of
     `Text`.  Lines from @stdout@ are wrapped in `Right` and lines from @stderr@
-    are wrapped in `Left`.  This does /not/ throw an exception if the command
-    returns a non-zero exit code
+    are wrapped in `Left`.
+
+    Throws an `ExitCode` exception if the command returns a non-zero exit code
 -}
 inprocWithErr
     :: Text
@@ -802,14 +813,14 @@
 inprocWithErr cmd args =
     streamWithErr (Process.proc (unpack cmd) (map unpack args))
 
-
 {-| Run a command line using the shell, streaming @stdout@ and @stderr@ as lines
     of `Text`.  Lines from @stdout@ are wrapped in `Right` and lines from
-    @stderr@ are wrapped in `Left`.  This does /not/ throw an exception if the
-    command returns a non-zero exit code
+    @stderr@ are wrapped in `Left`.
 
     This command is more powerful than `inprocWithErr`, but highly vulnerable to
     code injection if you template the command line with untrusted input
+
+    Throws an `ExitCode` exception if the command returns a non-zero exit code
 -}
 inshellWithErr
     :: Text
@@ -1109,11 +1120,64 @@
 #endif
         else output file empty )
 
+{-| This type is the same as @"System.Directory".`System.Directory.Permissions`@
+    type except combining the `System.Directory.executable` and
+    `System.Directory.searchable` fields into a single `executable` field for
+    consistency with the Unix @chmod@.  This simplification is still entirely
+    consistent with the behavior of "System.Directory", which treats the two
+    fields as interchangeable.
+-}
+data Permissions = Permissions
+    { _readable   :: Bool
+    , _writable   :: Bool
+    , _executable :: Bool
+    } deriving (Eq, Read, Ord, Show)
+
+{-| Under the hood, "System.Directory" does not distinguish between
+    `System.Directory.executable` and `System.Directory.searchable`.  They both
+    translate to the same `System.Posix.ownerExecuteMode` permission.  That
+    means that we can always safely just set the `System.Directory.executable`
+    field and safely leave the `System.Directory.searchable` field as `False`
+    because the two fields are combined with (`||`) to determine whether to set
+    the executable bit.
+-}
+toSystemDirectoryPermissions :: Permissions -> System.Directory.Permissions
+toSystemDirectoryPermissions p =
+    ( System.Directory.setOwnerReadable   (_readable   p)
+    . System.Directory.setOwnerWritable   (_writable   p)
+    . System.Directory.setOwnerExecutable (_executable p)
+    ) System.Directory.emptyPermissions
+
+fromSystemDirectoryPermissions :: System.Directory.Permissions -> Permissions
+fromSystemDirectoryPermissions p = Permissions
+    { _readable   = System.Directory.readable p
+    , _writable   = System.Directory.writable p
+    , _executable =
+        System.Directory.executable p || System.Directory.searchable p
+    }
+
 {-| Update a file or directory's user permissions
 
-> chmod rwo        "foo.txt"  -- chmod u=rw foo.txt
-> chmod executable "foo.txt"  -- chmod u+x foo.txt
-> chmod nonwritable "foo.txt" -- chmod u-w foo.txt
+> chmod rwo         "foo.txt"  -- chmod u=rw foo.txt
+> chmod executable  "foo.txt"  -- chmod u+x foo.txt
+> chmod nonwritable "foo.txt"  -- chmod u-w foo.txt
+
+    The meaning of each permission is:
+
+    * `readable` (@+r@ for short): For files, determines whether you can read
+      from that file (such as with `input`).  For directories, determines
+      whether or not you can list the directory contents (such as with `ls`).
+      Note: if a directory is not readable then `ls` will stream an empty list
+      of contents
+
+    * `writable` (@+w@ for short): For files, determines whether you can write
+      to that file (such as with `output`).  For directories, determines whether
+      you can create a new file underneath that directory.
+
+    * `executable` (@+x@ for short): For files, determines whether or not that
+      file is executable (such as with `proc`).  For directories, determines
+      whether or not you can read or execute files underneath that directory
+      (such as with `input` or `proc`)
 -}
 chmod
     :: MonadIO io
@@ -1126,22 +1190,25 @@
 chmod modifyPermissions path = liftIO (do
     let path' = deslash (Filesystem.encodeString path)
     permissions <- Directory.getPermissions path'
-    let permissions' = modifyPermissions permissions
-        changed = permissions /= permissions'
-    when changed (Directory.setPermissions path' permissions')
+    let permissions' = fromSystemDirectoryPermissions permissions
+    let permissions'' = modifyPermissions permissions'
+        changed = permissions' /= permissions''
+    let permissions''' = toSystemDirectoryPermissions permissions'
+    when changed (Directory.setPermissions path' permissions''')
     return permissions' )
 
 -- | Get a file or directory's user permissions
 getmod :: MonadIO io => FilePath -> io Permissions
 getmod path = liftIO (do
     let path' = deslash (Filesystem.encodeString path)
-    Directory.getPermissions path' )
+    permissions <- Directory.getPermissions path'
+    return (fromSystemDirectoryPermissions permissions))
 
 -- | Set a file or directory's user permissions
 setmod :: MonadIO io => Permissions -> FilePath -> io ()
 setmod permissions path = liftIO (do
     let path' = deslash (Filesystem.encodeString path)
-    Directory.setPermissions path' permissions )
+    Directory.setPermissions path' (toSystemDirectoryPermissions permissions) )
 
 -- | Copy a file or directory's permissions (analogous to @chmod --reference@)
 copymod :: MonadIO io => FilePath -> FilePath -> io ()
@@ -1152,39 +1219,35 @@
 
 -- | @+r@
 readable :: Permissions -> Permissions
-readable = Directory.setOwnerReadable True
+readable p = p { _readable = True }
 
 -- | @-r@
 nonreadable :: Permissions -> Permissions
-nonreadable = Directory.setOwnerReadable False
+nonreadable p = p { _readable = False }
 
 -- | @+w@
 writable :: Permissions -> Permissions
-writable = Directory.setOwnerWritable True
+writable p = p { _writable = True }
 
 -- | @-w@
 nonwritable :: Permissions -> Permissions
-nonwritable = Directory.setOwnerWritable False
+nonwritable p = p { _writable = False }
 
 -- | @+x@
 executable :: Permissions -> Permissions
-executable = Directory.setOwnerExecutable True
+executable p = p { _executable = True }
 
 -- | @-x@
 nonexecutable :: Permissions -> Permissions
-nonexecutable = Directory.setOwnerExecutable False
-
--- | @+s@
-searchable :: Permissions -> Permissions
-searchable = Directory.setOwnerSearchable True
-
--- | @-s@
-nonsearchable :: Permissions -> Permissions
-nonsearchable = Directory.setOwnerSearchable False
+nonexecutable p = p { _executable = False }
 
 -- | @-r -w -x@
 ooo :: Permissions -> Permissions
-ooo = const Directory.emptyPermissions
+ooo _ = Permissions
+    { _readable   = False
+    , _writable   = False
+    , _executable = False
+    }
 
 -- | @+r -w -x@
 roo :: Permissions -> Permissions
@@ -1198,10 +1261,6 @@
 oox :: Permissions -> Permissions
 oox = executable . ooo
 
--- | @-r -w +s@
-oos :: Permissions -> Permissions
-oos = searchable . ooo
-
 -- | @+r +w -x@
 rwo :: Permissions -> Permissions
 rwo = readable . writable . ooo
@@ -1210,10 +1269,6 @@
 rox :: Permissions -> Permissions
 rox = readable . executable . ooo
 
--- | @+r -w +s@
-ros :: Permissions -> Permissions
-ros = readable . searchable . ooo
-
 -- | @-r +w +x@
 owx :: Permissions -> Permissions
 owx = writable . executable . ooo
@@ -1222,10 +1277,6 @@
 rwx :: Permissions -> Permissions
 rwx = readable . writable . executable . ooo
 
--- | @+r +w +s@
-rws :: Permissions -> Permissions
-rws = readable . writable . searchable . ooo
-
 {-| Time how long a command takes in monotonic wall clock time
 
     Returns the duration alongside the return value
@@ -1256,15 +1307,16 @@
 
   True <- testfile path'
 
-  let handler :: IOError -> IO Permissions
+  let handler :: IOError -> IO Bool
       handler e =
           if isPermissionError e || isDoesNotExistError e
-              then return Directory.emptyPermissions
+              then return False
               else throwIO e
 
-  perms <- liftIO (getmod path' `catchIOError` handler)
+  let getIsExecutable = fmap _executable (getmod path')
+  isExecutable <- liftIO (getIsExecutable `catchIOError` handler)
 
-  guard (Directory.executable perms)
+  guard isExecutable
   return path'
 
 {-| Sleep for the given duration
@@ -1872,11 +1924,11 @@
 -- > main = do
 -- >   Just directory <- single (inshell "pwd" empty)
 -- >   print directory
-single :: MonadIO io => Shell a -> io (Maybe a)
+single :: MonadIO io => Shell a -> io a
 single s = do
     as <- fold s Control.Foldl.list
     case as of
-        [a] -> return (Just a)
+        [a] -> return a
         _   -> do
             let msg = format ("single: expected 1 line of input but there were "%d%" lines of input") (length as)
             die msg
diff --git a/src/Turtle/Tutorial.hs b/src/Turtle/Tutorial.hs
--- a/src/Turtle/Tutorial.hs
+++ b/src/Turtle/Tutorial.hs
@@ -1818,3 +1818,10 @@
 -- /Answer:/ Use `runManaged`, `sh`, or (`<|>`) (all resources acquired in the
 -- left stream will close before beginning the right stream).  Alternatively,
 -- use `with` to acquire a resource for a limited scope.
+--
+-- /Question:/ How do I use @turtle@ to run another shell as a subprocess?
+--
+-- /Answer:/ Use `Turtle.system` in conjunction with the `process` library,
+-- like this:
+--
+-- > Turtle.system (System.Process.proc "/bin/sh" []) empty
diff --git a/turtle.cabal b/turtle.cabal
--- a/turtle.cabal
+++ b/turtle.cabal
@@ -1,5 +1,5 @@
 Name: turtle
-Version: 1.3.6
+Version: 1.4.0
 Cabal-Version: >=1.10
 Build-Type: Simple
 License: BSD3
