diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,15 @@
 # encapsule releases
 
+## 0.6 (2026-10-10)
+- make passwordless sudo setup opt-in with `--sudo` (replaces `--no-sudo`)
+- add `--root` as shorthand for `--user root`
+- `enter`: add `--root/--user`
+- `commit`: use podman instead of buildah
+- install default capabilities in datadir/config.toml
+  (can be overriden by `XDG_CONFIG_DIRS` files and augmented by user config)
+- add `--network-host` to share host networking for local servers
+- add `pull` command (currently just a wrapper for `podman pull`)
+
 ## 0.5.1 (2026-09-15)
 - `enter` bugfix: -e=LANG must precede container
 - `enter`: allow an optional command like `run` (`enter TOOLBOX -- tmux`)
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -14,20 +14,26 @@
 Most encapsule subcommands act on an image.
 - If you wish to use an existing toolbox container as a starting point you can `commit` it to an "encapsule" container image.
   - Note your original toolbox container is left untouched: its system configuration and fs are just used as the base fs for the encapsule image (though its original bind mounts including $HOME will be not be included by default).
-- Alternatively you can roll your own image or run a vanilla image like `fedora` (`fedora:latest`), `fedora-toolbox:44` or `ubuntu:latest`, etc.
-  - However toolbox images or containers are recommended because they include `sudo` and `runuser`, but as such it doesn't have to be a toolbox container.
-  - For example since the fedora base container does not include runuser it runs as `--user root` by default (since as of 0.5 util-linux is no longer
-installed by default into encapsule containers: this may be addressed in future).
+- Alternatively you can roll your own image or run a vanilla image like `fedora` (`fedora:latest`), `fedora-toolbox:45`, `ubuntu:latest`, etc.
+  - Toolbox images or containers are generally recommended for development, but it doesn't have to be a toolbox container as such.
 
-Encapsule images and containers are prefixed by `encapsule-`.
-There is no need to use this prefix normally - it is implicit.
+A typical invocation might look like:
 
+```
+$ encapsule someimage:tag --cap my-config --home ~/isolated --project projects/abc
+```
+
+(directories can be relative).
+
+Encapsule images and containers are prefixed by `encapsule-`,
+but there is no need to use this prefix normally - it is implicit.
+
 ## Usage
 
 `$ encapsule --version`
 
 ```
-0.5.1
+0.6
 ```
 
 `$ encapsule --help`
@@ -47,6 +53,7 @@
 Available commands:
   list                     List encapsule images and containers
   list-caps                List available capabilities
+  pull                     Pull a container image with podman
   rm                       Remove an encapsule container
   rmi                      Remove an encapsule image
   stop                     Stop an encapsule container
@@ -62,20 +69,19 @@
 
 ### `run` command
 
-`run` starts a temporary encapsule container (removed on exit)
-from a (toolbox) image or container.
+`run` starts a temporary encapsule container (removed on exit) from an image.
 
 `$ encapsule run --help`
 
 ```
 Usage: encapsule run IMAGE [-v|--volume HOST:CONTAINER[:opts]]
                      [-e|--env KEY[=VALUE]] [--path DIR] [-i|--init CMD]
-                     [--cap NAME] [--pull] [--user USER]
+                     [--cap NAME] [--pull] [--root | --user USER]
                      [(-H|--home DIR[:opts]) [--backup-home]]
                      [(-p|--project DIR[:opts]) [--backup-project]]
-                     [-n|--name NAME] [--readonly] [--no-network] [--no-sudo]
-                     [--no-skel] [--podman-opt OPTION] [--debug] [--dryrun]
-                     [[--] CMD]
+                     [-n|--name NAME] [--readonly]
+                     [--no-network | --network-host] [--sudo] [--no-skel]
+                     [--podman-opt OPTION] [--debug] [--dryrun] [[--] CMD]
 
   Run a temporary encapsule container
 
@@ -88,6 +94,7 @@
                            container
   --cap NAME               Enable a capability from the config file
   --pull                   Pull newer container image
+  --root                   Run as root (shorthand for --user root)
   --user USER              Override container user [default: host/image user
                            with host UID]
   -H,--home DIR[:opts]     Mount a directory as a writable home (created if
@@ -100,7 +107,8 @@
                            skip 'encapsule-' prefix)
   --readonly               Make the encapsule container filesystem read-only
   --no-network             Disable network access
-  --no-sudo                Skip passwordless sudo setup
+  --network-host           Share the host network (including localhost ports)
+  --sudo                   Set up passwordless sudo for the container user
   --no-skel                Don't copy /etc/skel into an empty home
   --podman-opt OPTION      Pass an option directly to podman
   --debug                  Show debug output
@@ -108,6 +116,12 @@
   -h,--help                Show this help text
 ```
 
+Use `--network-host` with `run` (or `create`) to make servers on arbitrary
+container localhost ports reachable on the same host localhost ports.
+This also allows access to host-local services and shares the host's port
+space. It cannot be combined with `--no-network`. Without either option,
+Podman's default isolated container networking applies.
+
 ### `create` command
 `create` is similar but creates a reusable container for a project and/or temp home.
 
@@ -119,10 +133,35 @@
 $ encapsule enter my-toolbox -- tmux
 ```
 
+By default `enter` uses the user matching the host UID. Use `--root` (or
+`--user root`) when administrative access is needed:
+
+```bash
+$ encapsule enter --root my-toolbox
+```
+
 ### `commit` command
 `commit` saves a container as an encapsule image (`encapsule-CONTAINER` by default).
-Use `-n/--name NAME` for a custom image name (`encapsule-NAME`, or `^NAME` to skip the prefix).
+Use `-n/--name NAME` for a custom image name (becomes `encapsule-NAME`, or `^NAME` to skip the prefix).
 
+### `list` command
+Lists the encapsule images and containers:
+
+```
+$ ncpsl list
+localhost/encapsule-harness:latest  6.15 GB  12 days ago
+
+encapsule-harness-encapsule  Up 31 hours
+encapsule-fedora  Up 29 minutes
+encapsule-fedora-toolbox-45  Up 5 seconds
+```
+
+### `backup` command
+This is really independent of encapsule.
+It provides a simple way to create a backup tarball of a (git) project or dir.
+There are also runtime `--backup-project` and `--backup-home` options.
+
+
 ## Examples
 
 ```bash
@@ -175,7 +214,8 @@
 ## Capabilities
 
 Capabilities define reusable groups of volumes, environment variables,
-PATH entries, and init commands in `~/.config/encapsule/config.toml`:
+PATH entries, and init commands in `/usr/share/encapsule/config.toml`
+and `~/.config/encapsule/config.toml`:
 
 ```toml
 [capabilities.ssh]
@@ -193,6 +233,10 @@
 path = ["~/.cargo/bin"]
 ```
 
+There is an example user config file: `example/config.toml`.
+
+Use `encapsule list-caps` to list all the available defined capabilities.
+
 Each capability can define:
 
 - `volumes` : list of bind mount specs
@@ -205,26 +249,43 @@
 If the host and container paths are the same, you can use the shorthand
 `PATH[:opts]` instead of `PATH:PATH[:opts]`.
 
+The default defined capabilities live in the bundled `data/config.toml` file.
+These capabilities can be overridden by XDG system config files
+(under `XDG_CONFIG_DIRS`): by default `/etc/xdg/encapsule/config.toml`.
+User settings (under `XDG_CONFIG_HOME`) take precedence over these settings;
+earlier directories in `XDG_CONFIG_DIRS` take precedence over later ones.
+Nested tables are merged, so overriding one field of a capability preserves
+its other system defaults. Arrays are replaced rather than appended;
+use an empty array to clear a default.
+
+Note that an existing XDG system config, even an empty file,
+replaces the bundled default definitions entirely,
+whereas the user config will augment or modify them.
+
+To be quite clear, no capabilities are used by default:
+they need to be explicitly enabled on the commandline.
+The definitions just make them available to use.
+
 ## How it works
 
-0. Commits the named toolbox container to an encapsule image using `buildah commit`.
+0. Optionally commits the named toolbox container to an encapsule image (using `podman commit`).
 1. Runs `podman run` with `--userns=keep-id` so you are your own user, not root
-2. Drops from root with `runuser` if present, otherwise `sudo -u`
-   (`enter` uses `podman exec --user`)
-3. Sets up passwordless `sudo` inside the encapsule container (unless `--no-sudo`)
+2. Drops from root with `runuser` if present, otherwise `sudo -u`. Explicit
+   `--user root`/`--root` runs directly as root (`enter` uses
+   `podman exec --user`)
+3. Does not grant privilege escalation by default. `--sudo` opts into a
+   passwordless sudoers entry when the image contains `sudo`
 4. Bind mounts get SELinux `:z` (shared) labels automatically,
    so multiple containers can safely access the same directories
 5. When `-p/--project DIR` is used (and `--name` isn't), the container name
    includes the project directory's name (e.g. `encapsule-mytoolbox-myproject`),
    so you can run the same toolbox against different projects at the same time
-   in separate encapsule containers. Though for different project paths with
-   the same directory name the container name will not be differentiated.
+   in separate encapsule containers. For invocations with the same name,
+   the container name gets differentiated by pid.
 
 ## Installation
 
-A copr repo is available for Fedora and EPEL 10:
-
-<https://copr.fedorainfracloud.org/coprs/petersen/encapsule/>
+Encapsule is packaged in Fedora: <https://src.fedoraproject.org/rpms/encapsule>
 
 ## Building from source
 
@@ -251,8 +312,8 @@
 (`--dryrun` against local images, plus an optional live `run`).
 It needs podman and skips missing images.
 
-Default images are `ubuntu:latest` and `fedora:latest`.
-Override with `ENCAPSULE_TEST_UBUNTU` and `ENCAPSULE_TEST_FEDORA`.
+Default images are `fedora:latest` and `ubuntu:latest`.
+Override with `ENCAPSULE_TEST_FEDORA` and `ENCAPSULE_TEST_UBUNTU`.
 Live tests need a TTY, or set `ENCAPSULE_LIVE=1` to try without one.
 `ENCAPSULE` selects a different encapsule binary.
 
@@ -271,9 +332,10 @@
 
 ## Runtime Requirements
 
-- [podman](https://podman.io/) and [buildah](https://buildah.io/)
-- An existing (toolbox) container (created with `toolbox create`) or an image.
-- Alternatively other non-toolbox container/images can also work.
+- [podman](https://podman.io/)
+- An existing (toolbox) image (optionally created from a container with `toolbox create`).
+  - Use `encapsule pull` (or `podman pull`) to get an image first.
+- Encapsule has only been tested on Linux.
 
 ## Related projects
 
@@ -283,7 +345,7 @@
 
 Another somewhat related project is [podenv](https://github.com/podenv/podenv), which "provides a declarative interface to manage containerized applications."
 
-For stronger sandboxing and isolation, specially network, consider using [OpenShell](https://github.com/NVIDIA/OpenShell/). At some point this project might move to wrapping or supporting openshell possibly.
+For stronger sandboxing and isolation, specially network, consider using [OpenShell](https://github.com/NVIDIA/OpenShell/). At some point this project might support openshell perhaps.
 
 There is also [litterbox](https://github.com/Gerharddc/litterbox) which has quite a lot of features and though somewhat opinionated, for example like openshell also supports landlock confinement.
 
diff --git a/data/config.toml b/data/config.toml
new file mode 100644
--- /dev/null
+++ b/data/config.toml
@@ -0,0 +1,31 @@
+# table fields can be overriden by
+# - /etc/xdg/encapsule/config.toml (XDG_CONFIG_DIRS)
+# - ~/.config/encapsule/config.toml (XDG_CONFIG_HOME)
+
+[capabilities.ssh]
+volumes = ["~/.ssh:ro"]
+
+[capabilities.git]
+volumes = ["~/.gitconfig:ro"]
+
+# gtk4 needs libglvnd-gles
+[capabilities.wayland]
+env = ["WAYLAND_DISPLAY", "XDG_RUNTIME_DIR"]
+volumes = ["$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY"]
+security_opts = ["label=disable"]
+
+[capabilities.dbus]
+volumes = ["/run/dbus/system_bus_socket"]
+
+[capabilities.dconf]
+volumes = ["$XDG_RUNTIME_DIR/dconf"]
+
+[capabilities.machine-id]
+volumes = ["/etc/machine-id:ro"]
+
+[capabilities.rust]
+path = ["~/.cargo/bin"]
+
+[capabilities.ssh-agent]
+env = ["XDG_RUNTIME_DIR"]
+volumes = ["$XDG_RUNTIME_DIR/ssh-agent.sock"]
diff --git a/encapsule.cabal b/encapsule.cabal
--- a/encapsule.cabal
+++ b/encapsule.cabal
@@ -1,12 +1,12 @@
 cabal-version:       2.2
 name:                encapsule
-version:             0.5.1
+version:             0.6
 synopsis:            Run isolated toolbox containers with podman
 description:
         This tool (originally based on the toolbox-constrained project)
         allows running isolated toolbox containers with podman.
         Mounting of home and host integration are not enabled by default,
-        but one can choose options to do so including capabilities
+        but one can use options to do so including capabilities
         specified in a toml configuration file.
 license:             Apache-2.0
 license-file:        LICENSE
@@ -17,6 +17,7 @@
 homepage:            https://github.com/juhp/encapsule
 bug-reports:         https://github.com/juhp/encapsule/issues
 build-type:          Simple
+data-files:          data/config.toml
 extra-doc-files:     README.md
                      ChangeLog.md
                      example/config.toml
@@ -84,13 +85,22 @@
   type:                exitcode-stdio-1.0
   main-is:             Spec.hs
   other-modules:       EncapsuleTest
-  hs-source-dirs:      test
+                       ConfigSpec
+                       Config
+                       Paths_encapsule
+  autogen-modules:     Paths_encapsule
+  hs-source-dirs:      test src
   build-depends:       base < 5
+                     , containers
                      , directory
                      , filepath
                      , hspec
                      , process
+                     , simple-cmd
+                     , text
+                     , toml-reader
                      , unix
+                     , xdg-basedir
   build-tool-depends:  encapsule:encapsule
   default-language:    Haskell2010
   ghc-options:         -Wall -threaded
diff --git a/example/config.toml b/example/config.toml
--- a/example/config.toml
+++ b/example/config.toml
@@ -1,30 +1,10 @@
-[capabilities.ssh]
-volumes = ["~/.ssh:~/.ssh:ro"]
-
-[capabilities.git]
-volumes = ["~/.gitconfig:ro"]
-
-# gtk4 needs libglvnd-gles
-[capabilities.wayland]
-env = ["WAYLAND_DISPLAY", "XDG_RUNTIME_DIR"]
-volumes = ["$XDG_RUNTIME_DIR/$WAYLAND_DISPLAY"]
-security_opts = ["label=disable"]
-
-[capabilities.dbus]
-volumes = ["/run/dbus/system_bus_socket"]
-
-[capabilities.dconf]
-volumes = ["$XDG_RUNTIME_DIR/dconf"]
-
-[capabilities.machine-id]
-volumes = ["/etc/machine-id:ro"]
-
-[capabilities.rust]
-path = ["~/.cargo/bin"]
+# example ~/.config/encapsule/config.toml
 
 [capabilities.isolation]
 volumes = ["~/isolation:~:rw"]
 
-[capabilities.ssh-agent]
-env = ["XDG_RUNTIME_DIR"]
-volumes = ["$XDG_RUNTIME_DIR/ssh-agent.sock"]
+[capabilities.agents]
+volumes = ["~/AGENTS.md:ro"]
+
+[capabilities.unconfined]
+security_opts = ["label=disable"]
diff --git a/src/Backup.hs b/src/Backup.hs
--- a/src/Backup.hs
+++ b/src/Backup.hs
@@ -22,6 +22,7 @@
 import Expand
 import ShellQuote
 
+-- FIXME add --output-dir
 backupCmd :: Bool -> Bool -> Maybe FilePath -> FilePath -> IO ()
 backupCmd dryrun yes moutput dir = do
   homedir <- getHomeDirectory >>= canonicalizePath
@@ -55,7 +56,7 @@
     else do
     checkSize yes Nothing src
     (if dryrun then cmdN else cmd_) "tar" $ map shellQuote args ++ [base]
-  putStrLn $ "Wrote" +-+ tarball
+  putStrLn $ (if dryrun then "Would write" else "Wrote") +-+ tarball
 
 -- Prompt when backing up more than this many bytes.
 largeBackupBytes :: Integer
diff --git a/src/Config.hs b/src/Config.hs
--- a/src/Config.hs
+++ b/src/Config.hs
@@ -9,29 +9,52 @@
   )
 where
 
-import Data.List (intercalate)
+import Control.Monad (filterM)
+import Data.List (delete, intercalate)
 import qualified Data.Map.Strict as Map
 import Data.Maybe (mapMaybe)
 import qualified Data.Text as T
 import SimpleCmd (error', (+-+))
 import System.Directory (doesFileExist)
-import System.Environment.XDG.BaseDir (getUserConfigFile)
+import System.Environment.XDG.BaseDir (getAllConfigFiles, getUserConfigFile)
 import TOML (Value(..), Table, renderTOMLError, decodeFile)
 
+import Paths_encapsule (getDataFileName)
+
 progname :: String
 progname = "encapsule"
 
 loadConfig :: IO (Maybe Table)
 loadConfig = do
-  path <- getUserConfigFile progname "config.toml"
-  exists <- doesFileExist path
-  if not exists
+  userPath <- getUserConfigFile progname "config.toml"
+  configPaths <- getAllConfigFiles progname "config.toml" >>= filterM doesFileExist
+  paths <- if null $ delete userPath configPaths
+           then do
+             bundledPath <- getDataFileName "data/config.toml"
+             exists <- doesFileExist bundledPath
+             return $ configPaths ++ [bundledPath | exists]
+           else return configPaths
+  if null paths
     then return Nothing
     else do
+      tables <- mapM loadTable paths
+      return $ Just $ foldr mergeTables Map.empty tables
+  where
+    loadTable path = do
       result <- decodeFile path
       case result of
-        Left e -> error' $ "config parse error:" +-+ T.unpack (renderTOMLError e)
-        Right table -> return (Just table)
+        Left e -> error' $ "config parse error in" +-+ path ++ ":" +-+
+                          T.unpack (renderTOMLError e)
+        Right table -> return table
+
+-- XDG lists the user file first, followed by system files in priority order.
+-- Merge nested tables, but replace other values (including arrays).
+mergeTables :: Table -> Table -> Table
+mergeTables = Map.unionWith mergeValue
+  where
+    mergeValue (Table preferred) (Table fallback) =
+      Table $ mergeTables preferred fallback
+    mergeValue preferred _ = preferred
 
 getCapabilities :: Maybe Table -> Table
 getCapabilities Nothing = Map.empty
diff --git a/src/Enter.hs b/src/Enter.hs
--- a/src/Enter.hs
+++ b/src/Enter.hs
@@ -10,7 +10,7 @@
 where
 
 import Control.Monad (unless, when)
-import Data.Maybe (fromMaybe, isNothing)
+import Data.Maybe (fromMaybe, isJust, isNothing)
 import SimpleCmd (cmd, cmd_, cmdFull)
 import System.Directory (canonicalizePath, getHomeDirectory)
 import System.Exit (exitWith)
@@ -20,13 +20,13 @@
 
 import ShellQuote
 
-enterContainer :: Bool -> Bool -> Bool -> String -> [String] -> IO ()
-enterContainer dryrun debug running container command = do
+enterContainer :: Bool -> Bool -> Bool -> Maybe String -> String -> [String] -> IO ()
+enterContainer dryrun debug running muser container command = do
   hostHome <- getHomeDirectory >>= canonicalizePath
   unless running $ do
     putStr "start "
     cmd_ "podman" ["start", container]
-  (username, mPasswdHome) <- lookupContainerUser container
+  (username, mPasswdHome) <- lookupContainerUser container muser
   wd <- containerWorkdir container
   let userCmd = if null command then ["bash"] else command
       homeDir = fromMaybe hostHome mPasswdHome
@@ -38,7 +38,9 @@
           "/" -> homeDir
           d -> d
       homeEnv =
-        if isNothing mPasswdHome then ["env", "HOME=" ++ homeDir] else []
+        if isJust muser || isNothing mPasswdHome
+        then ["env", "HOME=" ++ homeDir]
+        else []
       execArgs = ["exec", "-it", "--user", username]
                  ++ langEnvArgs
                  ++ ["--workdir", workdir, container]
@@ -72,17 +74,17 @@
   | isAbsolute h = Just h
   | otherwise = Nothing
 
-lookupContainerUser :: String -> IO (String, Maybe FilePath)
-lookupContainerUser container = do
-  hostName <- getEffectiveUserName
+lookupContainerUser :: String -> Maybe String -> IO (String, Maybe FilePath)
+lookupContainerUser container muser = do
+  hostUserName <- getEffectiveUserName
   uid <- getEffectiveUserID
   let uidStr = show (fromIntegral uid :: Integer)
-      sh = passwdEntryForUidSh uidStr
+      sh = maybe (passwdEntryForUidSh uidStr) passwdEntryForNameSh muser
   (_, out, _) <- cmdFull "podman" ["exec", container, "/bin/sh", "-c", sh] ""
   case lines out of
     (n:h:_) | not (null n) -> return (n, usablePasswdHome h)
     (n:_) | not (null n) -> return (n, Nothing)
-    _ -> return (hostName, Nothing)
+    _ -> return (fromMaybe hostUserName muser, Nothing)
 
 containerWorkdir :: String -> IO String
 containerWorkdir container =
diff --git a/src/Main.hs b/src/Main.hs
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -28,6 +28,8 @@
       pure listCmd
     , Subcommand "list-caps" "List available capabilities" $
       pure listCapsCmd
+    , Subcommand "pull" "Pull a container image with podman" $
+      pullCmd <$> strArg "IMAGE"
     , Subcommand "rm" "Remove an encapsule container" $
       removeCmd
       <$> strArg "TOOLBOX"
@@ -60,6 +62,7 @@
       <*> pure True
       <*> optional (strArg "TOOLBOX")
       <*> optional projectNameOpt
+      <*> optional userNameOpt
       <*> many (strArg "[--] CMD")
     , Subcommand "run" "Run a temporary encapsule container" $
       runCmd <$> runOpts False True
@@ -74,7 +77,7 @@
     projectNameOpt = Project <$> projectOpt "Project name or path" <|>
                      Name <$> nameOpt
 
-    backupDirOpt s l m h =
+    dirOptBackup s l m h =
       let pair fs sn = (fs,sn) in
         pair
         <$> strOptionWith s l m h
@@ -82,6 +85,10 @@
 
     debugOpt = switchLongWith "debug" "Show debug output"
 
+    userNameOpt =
+      flagLongWith' "root" "root" "Run as root (shorthand for --user root)" <|>
+      strOptionLongWith "user" "USER" "Override container user [default: host/image user with host UID]"
+
     runOpts keep unique =
       Run.RunOpts
       <$> strArg "IMAGE"
@@ -91,14 +98,16 @@
       <*> many (strOptionWith 'i' "init" "CMD" "A bash snippet run when creating the encapsule container")
       <*> many (strOptionLongWith "cap" "NAME" "Enable a capability from the config file")
       <*> switchLongWith "pull" "Pull newer container image"
-      <*> optional (strOptionLongWith "user" "USER" "Override container user [default: host/image user with host UID]")
-      <*> optional (backupDirOpt 'H' "home" "DIR[:opts]" "Mount a directory as a writable home (created if missing; use DIR:O to overlay)")
-      <*> optional (backupDirOpt 'p' "project" "DIR[:opts]" "Mount a (project) directory as workdir (use DIR:O to overlay)")
+      <*> optional userNameOpt
+      <*> optional (dirOptBackup 'H' "home" "DIR[:opts]" "Mount a directory as a writable home (created if missing; use DIR:O to overlay)")
+      <*> optional (dirOptBackup 'p' "project" "DIR[:opts]" "Mount a (project) directory as workdir (use DIR:O to overlay)")
       <*> optional nameOpt
       <*> pure keep
       <*> switchLongWith "readonly" "Make the encapsule container filesystem read-only"
-      <*> switchLongWith "no-network" "Disable network access"
-      <*> switchLongWith "no-sudo" "Skip passwordless sudo setup"
+      <*> optional
+          (flagLongWith' NetNone "no-network" "Disable network access" <|>
+           flagLongWith' NetHost "network-host" "Share the host network (including localhost ports)")
+      <*> switchLongWith "sudo" "Set up passwordless sudo for the container user"
       <*> switchLongWith "no-skel" "Don't copy /etc/skel into an empty home"
       <*> pure unique
       <*> many (strOptionLongWith "podman-opt" "OPTION" "Pass an option directly to podman")
@@ -157,8 +166,9 @@
     else warning $ "container" +-+ containerName +-+ "not found"
 
 enterCmd :: Bool -> Bool -> Bool -> Maybe String -> Maybe ProjectName
+         -> Maybe String
          -> [String] -> IO ()
-enterCmd dryrun debug running mbase mprojectname command = do
+enterCmd dryrun debug running mbase mprojectname muser command = do
   regexp <-
     case mprojectname of
       Nothing -> return $ progname +=+ fromMaybe "" mbase
@@ -175,16 +185,21 @@
     [] ->
       if running
       then do
-        enterCmd dryrun debug False mbase mprojectname command
+        enterCmd dryrun debug False mbase mprojectname muser command
       else error' "encapsule container not found"
     [c] -> do
       unless running $
         warning "no running encapsule container found"
-      enterContainer dryrun debug True c command
+      enterContainer dryrun debug True muser c command
     _ -> error' $ "multiple" +-+ (if running then  "running" else "") +-+ "containers match:\n" ++ unlines ps
 
 -- image management
 
+pullCmd :: String -> IO ()
+pullCmd image = do
+  needPodman
+  cmd_ "podman" ["pull", image]
+
 commitCmd :: Bool -> Maybe String -> String -> IO ()
 commitCmd dryrun mname toolbox = do
   needPodman
@@ -197,9 +212,7 @@
   imageExists <- cmdBool "podman" ["image", "exists", image]
   unless imageExists $
     putStrLn $ "creating new image:" +-+ image
-  let buildah_args = ["commit", "--disable-compression", toolbox, image]
-  if dryrun
-    then cmdN "buildah" buildah_args
-    else do
-      putStr "writing image "
-      cmd_ "buildah" buildah_args
+  let commit_args = ["commit", toolbox, image]
+  unless dryrun $
+    putStr "writing image "
+  (if dryrun then cmdN else cmd_) "podman" commit_args
diff --git a/src/Run.hs b/src/Run.hs
--- a/src/Run.hs
+++ b/src/Run.hs
@@ -4,6 +4,7 @@
 
 module Run (
   ProjectName(..),
+  NetworkMode(..),
   RunOpts(..),
   runCmd,
   (+=+),
@@ -46,6 +47,8 @@
 
 data ProjectName = Project FilePath | Name String
 
+data NetworkMode = NetNone | NetHost
+
 data RunOpts = RunOpts
   { toolbox :: String
   , vols :: [String]
@@ -60,8 +63,8 @@
   , mname :: Maybe String
   , keep :: Bool
   , readonly :: Bool
-  , nonetwork :: Bool
-  , nosudo :: Bool
+  , networkMode :: Maybe NetworkMode
+  , sudoEnable :: Bool
   , noskel :: Bool
   , unique :: Bool
   , podmanopts :: [String]
@@ -122,15 +125,15 @@
             , isNothing muser
             , not keep
             , not readonly
-            , not nonetwork
-            , not nosudo
+            , isNothing networkMode
+            , not sudoEnable
             , not noskel
             , null podmanopts
             ]
       unless noopts $
         error' "cannot give options for an existing container!"
       warning "Entering existing container"
-      enterContainer dryrun debugging True container command
+      enterContainer dryrun debugging True Nothing container command
     else do
       when backupHome $
         whenJust mhomeDir $ backupCmd dryrun False Nothing
@@ -184,16 +187,20 @@
           Nothing -> maybe getEffectiveUserName return mImageUser
       debug $ "user:" +-+ username
 
-      let switch = chooseSwitchUser haveRunuser haveSudo
+      let mswitch =
+            if username == "root"
+            then Nothing
+            else chooseSwitchUser haveRunuser haveSudo
           startAsRoot =
-            canSwitchUser switch
+            username == "root"
+            || isJust mswitch
             || isNothing mhome && isNothing muser && isNothing mImageUser
-          stayAsRoot = startAsRoot && not (canSwitchUser switch)
+          stayAsRoot = startAsRoot && isNothing mswitch
           (containerHome, overrideHome) =
             if stayAsRoot
             then ("/root", False)
             else (fromMaybe hostHome mPasswdHome, isNothing mPasswdHome)
-      debug $ "switch:" +-+ switchLabel switch
+      debug $ "switch:" +-+ maybe "none" switchLabel mswitch
       debug $ "container home:" +-+ containerHome
 
       homeVol <-
@@ -250,14 +257,14 @@
           -- mkdir+chown when not bind-mounting --home. A passwd home may
           -- already exist but not be writable (committed toolbox image).
           setupArgs =
-            Setup nosudo noskel (TL.pack username) progname (isNothing mhome) (TL.pack containerHome) mprojectDir
+            Setup sudoEnable noskel (TL.pack username) progname (isNothing mhome) (TL.pack containerHome) mprojectDir
 
           setupParts =
-            let setup = setupScript debugging switch haveSudo setupArgs
+            let setup = setupScript debugging mswitch haveSudo setupArgs
             in [setup | not (null setup)] ++
                [mkInitSetup allinits | not (null allinits)]
           finalCmd =
-            case switchUserArgs switch username of
+            case maybe [] (switchUserArgs username) mswitch of
               []   -> "exec" +-+ userCmd
               args -> "exec" +-+ unwords args +-+ userCmd
           execScript =
@@ -322,7 +329,10 @@
                               Nothing -> ["--tmpfs", containerHome]
                               Just _ -> []
                     else [])
-                ++ (if nonetwork then ["--net", "none"] else [])
+                ++ (case networkMode of
+                      Nothing -> []
+                      Just NetNone -> ["--net", "none"]
+                      Just NetHost -> ["--net", "host"])
                 ++ concatMap (\s -> ["--security-opt", s]) securityOpts
                 ++ concatMap (\m -> ["-v", m]) (tzMounts ++ mounts)
                 ++ concatMap (\e -> ["-e", e]) envVars
diff --git a/src/Script.hs b/src/Script.hs
--- a/src/Script.hs
+++ b/src/Script.hs
@@ -8,7 +8,6 @@
 module Script (
   SwitchUser(..),
   chooseSwitchUser,
-  canSwitchUser,
   switchUserArgs,
   switchLabel,
   setupScript,
@@ -18,37 +17,31 @@
 
 import Control.Monad (unless, when)
 import Control.Monad.Shell
-import Data.Maybe (isNothing)
+import Data.Maybe (isJust, isNothing)
 import qualified Data.Text.Lazy as T
 import System.FilePath ((</>))
 import System.Posix.IO
 
 default (T.Text)
 
-data SwitchUser = Runuser | Sudo | None
-
-chooseSwitchUser :: Bool -> Bool -> SwitchUser
-chooseSwitchUser True _     = Runuser
-chooseSwitchUser False True = Sudo
-chooseSwitchUser _ _        = None
+data SwitchUser = Runuser | Sudo
 
-canSwitchUser :: SwitchUser -> Bool
-canSwitchUser None = False
-canSwitchUser _    = True
+chooseSwitchUser :: Bool -> Bool -> Maybe SwitchUser
+chooseSwitchUser True _     = Just Runuser
+chooseSwitchUser False True = Just Sudo
+chooseSwitchUser _ _        = Nothing
 
 -- argv prefix; empty for None
-switchUserArgs :: SwitchUser -> String -> [String]
-switchUserArgs Runuser u = ["runuser", "-u", u, "--"]
-switchUserArgs Sudo    u = ["sudo", "-n", "--preserve-env", "-u", u, "--"]
-switchUserArgs None    _ = []
+switchUserArgs :: String -> SwitchUser -> [String]
+switchUserArgs u Runuser = ["runuser", "-u", u, "--"]
+switchUserArgs u Sudo = ["sudo", "-n", "--preserve-env", "-u", u, "--"]
 
 switchLabel :: SwitchUser -> String
 switchLabel Runuser = "runuser"
 switchLabel Sudo    = "sudo"
-switchLabel None    = "none"
 
 data Setup = Setup
-  { nosudo :: Bool
+  { sudoEnable :: Bool
   , noskel :: Bool
   , username :: T.Text
   , program :: String
@@ -57,8 +50,8 @@
   , mprojectDir :: Maybe FilePath
   }
 
-setupScript :: Bool -> SwitchUser -> Bool -> Setup -> String
-setupScript dbg switch haveSudo (Setup {..}) =
+setupScript :: Bool -> Maybe SwitchUser -> Bool -> Setup -> String
+setupScript dbg mswitch haveSudo (Setup {..}) =
   T.unpack . T.replace "\t" " " . linearScript $
   sudoSetup >> homeSetup
   where
@@ -69,24 +62,26 @@
 
     sudoSetup =
       when haveSudo $
-      unless nosudo $
-      let sudoers = "/etc/sudoers.d" in
-          whenCmd (test $ TDirExists sudoers) $ do
-          runHide "echo" [username, "ALL=(ALL) NOPASSWD:ALL"] `redir` (sudoers </> program)
-          runHide "chmod" ["440", T.pack sudoers]
+      when sudoEnable $
+      -- FIXME handle no sudoers.d?
+      let sudoersd = "/etc/sudoers.d" in
+        whenCmd (test $ TDirExists sudoersd) $ do
+        let sudoersfile = sudoersd </> program
+        runHide "echo" [username, "ALL=(ALL) NOPASSWD:ALL"] `redir` sudoersfile
+        runHide "chmod" ["440", T.pack sudoersfile]
 
     homeSetup = do
       when createhome $ do
         runHide "mkdir" ["-p", homedir]
         runHide "chown" [username, homedir]
       unless noskel $
-        when (canSwitchUser switch) $
+        when (isJust mswitch) $
         whenCmd
         (test (TDirExists homedir)
          -&&-
          test (TDirExists (T.pack "/etc/skel"))) $
         let cpArgs = ["-a", "--update=none", "/etc/skel/.", homedir <> "/"]
-        in case map T.pack $ switchUserArgs switch (T.unpack username) of
+        in case map T.pack $ maybe [] (switchUserArgs (T.unpack username)) mswitch of
              prog:args -> runHide prog $ args ++ "cp" : cpArgs
              [] -> return ()
       when (isNothing mprojectDir && createhome) $
diff --git a/test/ConfigSpec.hs b/test/ConfigSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/ConfigSpec.hs
@@ -0,0 +1,160 @@
+-- SPDX-License-Identifier: Apache-2.0
+
+module ConfigSpec (spec) where
+
+import Control.Exception (bracket)
+import qualified Data.Map.Strict as Map
+import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive)
+import System.Environment (lookupEnv, setEnv, unsetEnv)
+import System.FilePath ((</>), takeDirectory)
+import System.Posix.Temp (mkdtemp)
+import Test.Hspec
+
+import Config (getCapabilities, loadConfig, resolveCapabilities)
+import EncapsuleTest (encapsule)
+
+spec :: Spec
+spec = describe "config" $ do
+  it "returns Nothing when no config files exist, including bundled defaults" $
+    withConfigDirs $ \_ _ _ ->
+      loadConfig `shouldReturn` Nothing
+
+  it "loads bundled defaults when no user or system config exists" $
+    withConfigDirs $ \user _ _ -> do
+      writeBundledConfig user "[capabilities.bundled]\nenv = ['BUNDLED']\n"
+      resolve ["bundled"] `shouldReturn` ([], ["BUNDLED"], [], [], [])
+      out <- encapsule ["list-caps"]
+      out `shouldBe` "Available capabilities:\n  bundled\n"
+
+  it "merges user overrides over bundled defaults without a system config" $
+    withConfigDirs $ \user _ _ -> do
+      writeBundledConfig user $ unlines
+        [ "[capabilities.shared]"
+        , "env = ['BUNDLED']"
+        , "path = ['/bundled/bin']"
+        , "[capabilities.bundled]"
+        , "volumes = ['bundled-volume']"
+        ]
+      writeConfig user "[capabilities.shared]\nenv = ['USER']\n"
+      resolve ["shared", "bundled"] `shouldReturn`
+        (["bundled-volume"], ["USER"], ["/bundled/bin"], [], [])
+
+  it "ignores bundled defaults when a system config exists" $
+    withConfigDirs $ \user _ system -> do
+      writeBundledConfig user "[capabilities.bundled]\nenv = ['BUNDLED']\n"
+      writeConfig system "[capabilities.system]\nenv = ['SYSTEM']\n"
+      out <- encapsule ["list-caps"]
+      out `shouldBe` "Available capabilities:\n  system\n"
+
+  it "lets an empty system config suppress bundled defaults" $
+    withConfigDirs $ \user system _ -> do
+      writeBundledConfig user "[capabilities.bundled]\nenv = ['BUNDLED']\n"
+      writeConfig system ""
+      (getCapabilities <$> loadConfig) `shouldReturn` Map.empty
+
+  it "reports malformed bundled defaults when no system config exists" $
+    withConfigDirs $ \user _ _ -> do
+      writeBundledConfig user "[broken\n"
+      out <- encapsule ["list-caps"]
+      out `shouldContain` "config parse error in"
+      out `shouldContain` (takeDirectory user </> "bundled" </> "data" </> "config.toml")
+
+  it "ignores malformed bundled defaults when a system config exists" $
+    withConfigDirs $ \user system _ -> do
+      writeBundledConfig user "[broken\n"
+      writeConfig system "[capabilities.system]\nenv = ['SYSTEM']\n"
+      resolve ["system"] `shouldReturn` ([], ["SYSTEM"], [], [], [])
+
+  it "loads a user config without a system config" $
+    withConfigDirs $ \user _ _ -> do
+      writeConfig user "[capabilities.user]\nenv = ['USER_ONLY']\n"
+      resolve ["user"] `shouldReturn` ([], ["USER_ONLY"], [], [], [])
+
+  it "loads a system config without a user config" $
+    withConfigDirs $ \_ system _ -> do
+      writeConfig system "[capabilities.system]\nenv = ['SYSTEM_ONLY']\n"
+      resolve ["system"] `shouldReturn` ([], ["SYSTEM_ONLY"], [], [], [])
+
+  it "merges capability fields and replaces arrays and scalar values" $
+    withConfigDirs $ \user system _ -> do
+      writeConfig system $ unlines
+        [ "[capabilities.shared]"
+        , "volumes = ['system-volume']"
+        , "env = ['SYSTEM_ENV']"
+        , "path = ['/system/bin']"
+        , "init = 'system-init'"
+        , "security_opts = ['label=disable']"
+        , "[capabilities.system]"
+        , "env = ['SYSTEM_ONLY']"
+        ]
+      writeConfig user $ unlines
+        [ "[capabilities.shared]"
+        , "volumes = []"
+        , "env = ['USER_ENV']"
+        , "init = 'user-init'"
+        , "[capabilities.user]"
+        , "env = ['USER_ONLY']"
+        ]
+      resolve ["shared"] `shouldReturn`
+        ([], ["USER_ENV"], ["/system/bin"], ["user-init"], ["label=disable"])
+      resolve ["system", "user"] `shouldReturn`
+        ([], ["SYSTEM_ONLY", "USER_ONLY"], [], [], [])
+
+  it "respects system directory order and user precedence" $
+    withConfigDirs $ \user first second -> do
+      writeConfig second "[capabilities.shared]\nenv = ['SECOND']\npath = ['/second/bin']\n"
+      writeConfig first "[capabilities.shared]\nenv = ['FIRST']\n"
+      resolve ["shared"] `shouldReturn` ([], ["FIRST"], ["/second/bin"], [], [])
+      writeConfig user "[capabilities.shared]\nenv = ['USER']\n"
+      resolve ["shared"] `shouldReturn` ([], ["USER"], ["/second/bin"], [], [])
+
+  it "lets a user value replace a system table" $
+    withConfigDirs $ \user system _ -> do
+      writeConfig system "[capabilities.shared]\nenv = ['SYSTEM']\n"
+      writeConfig user "capabilities = []\n"
+      (getCapabilities <$> loadConfig) `shouldReturn` Map.empty
+
+  it "reports the path of a malformed system config even with a user config" $
+    withConfigDirs $ \user system _ -> do
+      writeConfig user "[capabilities.user]\nenv = ['USER']\n"
+      writeConfig system "[broken\n"
+      out <- encapsule ["list-caps"]
+      out `shouldContain` "config parse error in"
+      out `shouldContain` (system </> "encapsule" </> "config.toml")
+
+resolve :: [String] -> IO ([String], [String], [String], [String], [String])
+resolve names = do
+  caps <- getCapabilities <$> loadConfig
+  resolveCapabilities caps names
+
+writeConfig :: FilePath -> String -> IO ()
+writeConfig dir contents = do
+  let appDir = dir </> "encapsule"
+  createDirectoryIfMissing True appDir
+  writeFile (appDir </> "config.toml") contents
+
+writeBundledConfig :: FilePath -> String -> IO ()
+writeBundledConfig user contents = do
+  let dataDir = takeDirectory user </> "bundled" </> "data"
+  createDirectoryIfMissing True dataDir
+  writeFile (dataDir </> "config.toml") contents
+
+withConfigDirs :: (FilePath -> FilePath -> FilePath -> IO a) -> IO a
+withConfigDirs action =
+  bracket (mkdtemp "/tmp/encapsule-config-test-XXXXXX") removeDirectoryRecursive $ \tmp -> do
+    let user = tmp </> "user"
+        first = tmp </> "system-first"
+        second = tmp </> "system-second"
+    withEnv "XDG_CONFIG_HOME" user $
+      withEnv "XDG_CONFIG_DIRS" (first ++ ":" ++ second) $
+        withEnv "encapsule_datadir" (tmp </> "bundled") $
+          action user first second
+
+withEnv :: String -> String -> IO a -> IO a
+withEnv key value action =
+  bracket (lookupEnv key) restore $ \_ -> do
+    setEnv key value
+    action
+  where
+    restore Nothing = unsetEnv key
+    restore (Just old) = setEnv key old
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -16,13 +16,20 @@
 import Test.Hspec
 
 import EncapsuleTest
+import qualified ConfigSpec
 
 main :: IO ()
 main = hspec spec
 
 spec :: Spec
 spec = do
+  ConfigSpec.spec
   describe "dryrun" $ do
+    it "shares host networking with --network-host" $
+      withGenericImage $ \img -> do
+        out <- dryrun ["--network-host", img]
+        out `shouldContain` "--net host"
+
     it "podman run has keep-id, TERM & LANG" $
       withGenericImage $ \img -> do
         out <- dryrun [img]
@@ -53,15 +60,24 @@
           out `shouldContain` "--name encapsule-"
           out `shouldContain` "-proj"
 
-    it "uses runuser or sudo to switch with --user root" $
+    it "runs directly as root with --user root" $
       withGenericImage $ \img -> do
-        out <- dryrun [img]
-        case debugField out "switch" of
-          Just "none" -> pendingWith $ img ++ " has no runuser or sudo"
-          _ -> do
-            out' <- dryrun ["--user", "root", img]
-            assertUserSwitch out' "root"
+        out <- dryrun ["--user", "root", img]
+        debugField out "user" `shouldBe` Just "root"
+        debugField out "switch" `shouldBe` Just "none"
+        debugField out "container home" `shouldBe` Just "/root"
+        out `shouldContain` "--user=root"
+        out `shouldNotContain` "runuser -u root"
+        out `shouldNotContain` "sudo -n --preserve-env -u root"
 
+    it "treats --root as shorthand for --user root" $
+      withGenericImage $ \img -> do
+        out <- dryrun ["--root", img]
+        debugField out "user" `shouldBe` Just "root"
+        debugField out "switch" `shouldBe` Just "none"
+        debugField out "container home" `shouldBe` Just "/root"
+        out `shouldContain` "--user=root"
+
   describe "ubuntu" $ do
     it "uses ubuntu user and passwd home for UID 1000" $ do
       img <- ubuntuImg
@@ -157,18 +173,17 @@
           pendingWith $ "image has uid user " ++ fromMaybe "unknown" other
 
   describe "sudo" $ do
-    it "writes sudoers when sudo is present, skips when not" $
+    it "only writes sudoers when --sudo is given and sudo is present" $
       withGenericImage $ \img -> do
         out <- dryrun [img]
+        out `shouldNotContain` "NOPASSWD:ALL"
         case debugField out "sudo" of
           Just "True" -> do
-            out `shouldContain` "NOPASSWD:ALL"
-            outNo <- dryrun ["--no-sudo", img]
-            outNo `shouldNotContain` "NOPASSWD:ALL"
+            outSudo <- dryrun ["--sudo", img]
+            outSudo `shouldContain` "NOPASSWD:ALL"
           Just "False" -> do
-            out `shouldNotContain` "NOPASSWD:ALL"
-            outNo <- dryrun ["--no-sudo", img]
-            outNo `shouldNotContain` "NOPASSWD:ALL"
+            outSudo <- dryrun ["--sudo", img]
+            outSudo `shouldNotContain` "NOPASSWD:ALL"
           other ->
             expectationFailure $
               "debug sudo line for " ++ img ++ " (got: " ++
@@ -179,6 +194,12 @@
       out <- encapsule ["enter", "--help"]
       out `shouldContain` "[--] CMD"
 
+    it "offers explicit user and root selection" $ do
+      out <- encapsule ["enter", "--help"]
+      out `shouldContain` "[--root | --user USER]"
+      out `shouldContain` "--user USER"
+      out `shouldContain` "--root"
+
   describe "commit" $ do
     it "offers --name" $ do
       out <- encapsule ["commit", "--help"]
@@ -187,7 +208,7 @@
     it "names the image encapsule-CONTAINER by default" $
       withScratchContainer $ \cname -> do
         out <- encapsule ["commit", "--dryrun", cname]
-        out `shouldContain` "buildah commit"
+        out `shouldContain` "podman commit"
         out `shouldContain` ("encapsule-" ++ cname)
 
     it "applies --name to the encapsule image" $
