diff --git a/projects/cram/BUILD b/projects/cram/BUILD
index 6723694..e2c8edd 100644
--- a/projects/cram/BUILD
+++ b/projects/cram/BUILD
@@ -5,6 +5,7 @@ zapp_binary(
         "src/python"
     ],
     deps = [
+        "//projects/vfs",
         py_requirement("toposort"),
     ]
 )
diff --git a/projects/cram/README.md b/projects/cram/README.md
index e5d6166..47824b7 100644
--- a/projects/cram/README.md
+++ b/projects/cram/README.md
@@ -2,35 +2,32 @@
 
 > To force (people or things) into a place or container that is or appears to be too small to contain them.
 
-An alternative to GNU Stow, which critically supports jinja2 templating and some other niceties.
+An alternative to GNU Stow, more some notion of packages with dependencies and install scripts.
 
 ## Usage
 
 ```
-$ cram.zapp [hostname]
+# cram [--dry-run | --execute] <configdir> <destdir>
+$ cram ~/conf ~/  # --dry-run is the default
 ```
 
-
-Cram consumes a directory tree of the following structure:
+Cram operates in terms of packages, which are directories with the following structure -
 
 ```
-# Hosts
-./hosts.d/<hostname>/
-./hosts.d/<hostname>/REQUIRES
-./hosts.d/<hostname>/PRE_INSTALL
-./hosts.d/<hostname>/INSTALL
-./hosts.d/<hostname>/POST_INSTALL
+/REQUIRES      # A list of other packages this one requires
+/BUILD         # 1. Perform any compile or package management tasks
+/PRE_INSTALL   # 2. Any other tasks required before installation occurs
+/INSTALL       # 3. Do whatever constitutes installation
+/POST_INSTALL  # 4. Any cleanup or other tasks following installation
 
-# Profiles
-./profiles.d/<profilename>/
-./profiles.d/<profilename>/REQUIRES
-./profiles.d/<profilename>/PRE_INSTALL
-./profiles.d/<profilename>/INSTALL
-./profiles.d/<profilename>/POST_INSTALL
+...            # Any other files are treated as package contents
 
-# Packages
-./packages.d/<packagename>/
-./packages.d/<packagename>/REQUIRES
-./packages.d/<packagename>/PRE_INSTALL
-./packages.d/<packagename>/POST_INSTALL
 ```
+
+Cram reads a config dir with three groups of packages
+- `packages.d/<packagename>` contains a package that installs but probably shouldn't configure a given tool, package or group of files.
+  Configuration should be left to profiles.
+- `profiles.d/<profilename>` contains a profile; a group of related profiles and packages that should be installed together.
+- `hosts.d/<hostname>` contains one package for each host, and should pull in a list of profiles.
+
+The intent of this tool is to keep GNU Stow's intuitive model of deploying configs via symlinks, and augment it with a useful pattern for talking about "layers" / "packages" of related configs.
diff --git a/projects/cram/src/python/cram/__main__.py b/projects/cram/src/python/cram/__main__.py
index c6f95ef..1f06f95 100644
--- a/projects/cram/src/python/cram/__main__.py
+++ b/projects/cram/src/python/cram/__main__.py
@@ -2,80 +2,25 @@
 
 import argparse
 from itertools import chain
+import logging
 import os
 from pathlib import Path
-from subprocess import run
 import sys
 from typing import NamedTuple
 
 from toposort import toposort_flatten
+from vfs import Vfs
 
+log = logging.getLogger(__name__)
 
 parser = argparse.ArgumentParser()
 parser.add_argument("-x", "--execute", dest="execute", action="store_true", default=False)
 parser.add_argument("-d", "--dry-run", dest="execute", action="store_false")
-parser.add_argument("confdir", default="~/conf", type=Path)
-parser.add_argument("destdir", default="~/", type=Path)
+parser.add_argument("confdir", type=Path)
+parser.add_argument("destdir", type=Path)
 
 
-class Fs(object):
-    """An abstract filesystem device which can accumulate changes, and apply them in a batch."""
-
-    def __init__(self):
-        self._log = []
-
-    def execute(self, execute=False):
-        for e in self._log:
-            print(e)
-
-            if not execute:
-                continue
-
-            elif e[0] == "exec":
-                _, dir, cmd = e
-                run(cmd, cwd=str(dir))
-
-            elif e[0] == "link":
-                _, src, dest = e
-                if dest.exists() and dest.is_symlink() and dest.readlink() == dest:
-                    continue
-                else:
-                    if dest.exists():
-                        dest.unlink()
-                    dest.symlink_to(src)
-
-            elif e[0] == "copy":
-                raise NotImplementedError()
-
-            elif e[0] == "chmod":
-                _, dest, mode = e
-                dest.chmod(mode)
-
-            elif e[0] == "mkdir":
-                _, dest = e
-                dest.mkdir(exist_ok=True)
-
-
-    def _append(self, msg):
-        self._log.append(msg)
-
-    def link(self, src, dest):
-        self._append(("link", src, dest))
-
-    def copy(self, src, dest):
-        self._append(("copy", src, dest))
-
-    def chmod(self, dest, mode):
-        self._append(("chmod", dest, mode))
-
-    def mkdir(self, dest):
-        self._append(("mkdir", dest))
-
-    def exec(self, dest, cmd):
-        self._append(("exec", dest, cmd))
-
-
-def stow(fs: Fs, src_dir: Path, dest_dir: Path, skip=[]):
+def stow(fs: Vfs, src_dir: Path, dest_dir: Path, skip=[]):
     """Recursively 'stow' (link) the contents of the source into the destination."""
 
     dest_root = Path(dest_dir)
@@ -100,22 +45,26 @@ class PackageV0(NamedTuple):
     root: Path
     name: str
 
-    SPECIAL_FILES = ["BUILD", "INSTALL", "POST_INSTALL", "requires"]
+    SPECIAL_FILES = ["BUILD", "PRE_INSTALL", "INSTALL", "POST_INSTALL", "REQUIRES"]
 
     def requires(self):
         """Get the dependencies of this package."""
-        requiresf = self.root / "requires"
+        requiresf = self.root / "REQUIRES"
         if requiresf.exists():
             with open(requiresf) as fp:
                 return [l.strip() for l in fp]
         return []
 
-    def install(self, fs, dest):
+    def install(self, fs: Vfs, dest):
         """Install this package."""
         buildf = self.root / "BUILD"
         if buildf.exists():
             fs.exec(self.root, ["bash", str(buildf)])
 
+        pref = self.root / "PRE_INSTALL"
+        if pref.exists():
+            fs.exec(self.root, ["bash", str(pref)])
+
         installf = self.root / "INSTALL"
         if installf.exists():
             fs.exec(self.root, ["bash", str(installf)])
@@ -132,6 +81,11 @@ def main():
 
     opts, args = parser.parse_known_args()
 
+    logging.basicConfig(
+        level=logging.DEBUG,
+        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+    )
+
     root = opts.confdir
 
     packages = {str(p.relative_to(root)): PackageV0(p, str(p))
@@ -155,7 +109,7 @@ def main():
 
     # Compute the topsort graph
     requirements = {r: packages[r].requires() for r in requirements}
-    fs = Fs()
+    fs = Vfs()
 
     for r in toposort_flatten(requirements):
         r = packages[r]
diff --git a/projects/vfs/BUILD b/projects/vfs/BUILD
new file mode 100644
index 0000000..af8fe55
--- /dev/null
+++ b/projects/vfs/BUILD
@@ -0,0 +1,3 @@
+py_project(
+    name = "vfs"
+)
diff --git a/projects/vfs/src/python/vfs/__init__.py b/projects/vfs/src/python/vfs/__init__.py
new file mode 100644
index 0000000..fdf6cb7
--- /dev/null
+++ b/projects/vfs/src/python/vfs/__init__.py
@@ -0,0 +1,5 @@
+"""
+The published interface of the VFS package.
+"""
+
+from .impl import Vfs  # noqa
diff --git a/projects/vfs/src/python/vfs/impl.py b/projects/vfs/src/python/vfs/impl.py
new file mode 100644
index 0000000..82138cc
--- /dev/null
+++ b/projects/vfs/src/python/vfs/impl.py
@@ -0,0 +1,65 @@
+"""
+The implementation.
+"""
+
+import logging
+from subprocess import run
+
+
+_log = logging.getLogger(__name__)
+
+
+class Vfs(object):
+    """An abstract filesystem device which can accumulate changes, and apply them in a batch."""
+
+    def __init__(self):
+        self._log = []
+
+    def execute(self, execute=False):
+        for e in self._log:
+            _log.debug(e)
+
+            if not execute:
+                continue
+
+            elif e[0] == "exec":
+                _, dir, cmd = e
+                run(cmd, cwd=str(dir))
+
+            elif e[0] == "link":
+                _, src, dest = e
+                if dest.exists() and dest.is_symlink() and dest.readlink() == dest:
+                    continue
+                else:
+                    if dest.exists():
+                        dest.unlink()
+                    dest.symlink_to(src)
+
+            elif e[0] == "copy":
+                raise NotImplementedError()
+
+            elif e[0] == "chmod":
+                _, dest, mode = e
+                dest.chmod(mode)
+
+            elif e[0] == "mkdir":
+                _, dest = e
+                dest.mkdir(exist_ok=True)
+
+    def _append(self, msg):
+        self._log.append(msg)
+
+    def link(self, src, dest):
+        self._append(("link", src, dest))
+
+    def copy(self, src, dest):
+        self._append(("copy", src, dest))
+
+    def chmod(self, dest, mode):
+        self._append(("chmod", dest, mode))
+
+    def mkdir(self, dest):
+        self._append(("mkdir", dest))
+
+    def exec(self, dest, cmd):
+        self._append(("exec", dest, cmd))