diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2014, Seth Fowler
+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 the {organization} nor the names of its
+  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 HOLDER 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/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+hslibvoyeur
+===========
+
+Haskell bindings for libvoyeur
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,159 @@
+{-# LANGUAGE RecordWildCards #-}
+{-# LANGUAGE TupleSections #-}
+
+import Control.Applicative
+import Control.Exception
+import Control.Monad
+import Data.List
+import Data.Traversable (for)
+import Distribution.PackageDescription
+import Distribution.Simple
+import Distribution.Simple.BuildPaths
+import Distribution.Simple.LocalBuildInfo
+import Distribution.Simple.Setup
+import Distribution.Simple.Utils
+import Distribution.Simple.Program
+import qualified Distribution.Verbosity as Verbosity
+import System.Directory
+import System.FilePath
+import System.Info
+
+main :: IO ()
+main = defaultMainWithHooks simpleUserHooks
+    { confHook  = customConfHook
+    , buildHook = customBuildHook
+    , copyHook  = customCopyHook
+    , cleanHook = customCleanHook
+
+    , hookedPrograms = hookedPrograms simpleUserHooks
+                    ++ [ makeProgram ]
+    }
+
+customConfHook :: (GenericPackageDescription, HookedBuildInfo) -> ConfigFlags
+               -> IO LocalBuildInfo
+customConfHook (pkg, pbi) flags = do
+  (_, includeDir, _) <- libvoyeurPaths
+  let addIncludeDirs = (onLocalLibBuildInfo . onIncludeDirs) (++ [".", includeDir])
+      addLibs = if os == "darwin"
+                  then id
+                  else (onLocalLibBuildInfo . onLdOptions) (++ ["-lbsd"])
+
+  lbi <- confHook simpleUserHooks (pkg, pbi) flags
+  return $ (addLibs . addIncludeDirs) lbi
+
+customBuildHook :: PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()
+customBuildHook pkg lbi usrHooks flags = do
+  putStrLn "Building libvoyeur..."
+  
+  (libvoyeurDir, _, libDir) <- libvoyeurPaths
+  let verbosity = fromFlag (buildVerbosity flags)
+      runMake = runDbProgram verbosity makeProgram (withPrograms lbi)
+  
+  inDir libvoyeurDir $
+    runMake []
+  
+  buildHook simpleUserHooks pkg lbi usrHooks flags
+  
+  notice verbosity "Relinking libvoyeur.."
+  
+  let libObjs = map (libObjPath libDir) [ "voyeur"
+                                        , "net"
+                                        , "env"
+                                        , "event"
+                                        , "util"
+                                        ]
+  
+      componentLibs = concatMap componentLibNames $ componentsConfigs lbi
+      addStaticObjectFile libName objName = runAr ["r", libName, objName]
+      runAr = runDbProgram verbosity arProgram (withPrograms lbi)
+  
+  forM_ componentLibs $ \componentLib -> do
+    when (withVanillaLib lbi) $
+         let libName = buildDir lbi </> mkLibName componentLib
+         in mapM_ (addStaticObjectFile libName) libObjs
+  
+    when (withProfLib lbi) $
+         let libName = buildDir lbi </> mkProfLibName componentLib
+         in mapM_ (addStaticObjectFile libName) libObjs
+  
+    when (withSharedLib lbi) $
+         let libName = buildDir lbi </> mkSharedLibName buildCompilerId componentLib
+         in mapM_ (addStaticObjectFile libName) libObjs
+
+customCopyHook :: PackageDescription -> LocalBuildInfo -> UserHooks -> CopyFlags -> IO ()
+customCopyHook pkg lbi hooks flags = do
+  let verb = fromFlagOrDefault Verbosity.normal $ copyVerbosity flags
+
+  copyHook simpleUserHooks pkg lbi hooks flags
+
+  putStrLn "Installing libvoyeur helper libraries..."
+
+  let helperLibs = [ "exec", "exit", "open", "close" ]
+      helperLibFiles = map (("libvoyeur-" ++) . (<.> dllExtension)) helperLibs
+      helperLibDir = datadir (absoluteInstallDirs pkg lbi NoCopyDest)
+
+  (_, _, libDir) <- libvoyeurPaths
+  copyFiles verb helperLibDir $ map (libDir,) helperLibFiles
+      
+customCleanHook :: PackageDescription -> () -> UserHooks -> CleanFlags -> IO ()
+customCleanHook pkg v hooks flags = do
+  putStrLn "Cleaning libvoyeur..."
+  
+  let verb = fromFlagOrDefault Verbosity.normal $ cleanVerbosity flags
+  pgmConf <- configureProgram verb (simpleProgram "make") defaultProgramConfiguration
+  
+  (libvoyeurDir, _, _) <- libvoyeurPaths
+  inDir libvoyeurDir $
+    runDbProgram verb makeProgram pgmConf ["clean"]
+  
+  cleanHook simpleUserHooks pkg v hooks flags
+
+libvoyeurPaths :: IO (FilePath, FilePath, FilePath)
+libvoyeurPaths = do
+  curDir <- getCurrentDirectory
+  return (curDir </> "libvoyeur",
+          curDir </> "libvoyeur" </> "include",
+          curDir </> "libvoyeur" </> "build")
+  
+componentLibNames :: (ComponentName, ComponentLocalBuildInfo, [ComponentName]) -> [LibraryName]
+componentLibNames (_, LibComponentLocalBuildInfo {..}, _) = componentLibraries
+componentLibNames _                                       = []
+
+makeProgram :: Program
+makeProgram = simpleProgram "make"
+
+libObjPath :: FilePath -> FilePath -> FilePath
+libObjPath dir name = dir </> name <.> objExtension
+
+inDir :: FilePath -> IO a -> IO a
+inDir dir act = do
+  curDir <- getCurrentDirectory
+  bracket_ (setCurrentDirectory dir)
+           (setCurrentDirectory curDir)
+           act
+
+type Lifter a b = (a -> a) -> b -> b
+
+onLocalPkgDescr :: Lifter PackageDescription LocalBuildInfo
+onLocalPkgDescr f lbi = lbi { localPkgDescr = f (localPkgDescr lbi) }
+
+onLibrary :: Lifter Library PackageDescription
+onLibrary f lpd = lpd { library = f <$> library lpd }
+
+onLibBuildInfo :: Lifter BuildInfo Library
+onLibBuildInfo f lib = lib { libBuildInfo = f (libBuildInfo lib) }
+
+onLocalLibBuildInfo :: Lifter BuildInfo LocalBuildInfo
+onLocalLibBuildInfo = onLocalPkgDescr . onLibrary . onLibBuildInfo
+
+onIncludeDirs :: Lifter [FilePath] BuildInfo
+onIncludeDirs f libbi = libbi { includeDirs = f (includeDirs libbi) }
+
+onLdOptions :: Lifter [FilePath] BuildInfo
+onLdOptions f libbi = libbi { ldOptions = f (ldOptions libbi) }
+
+onPkgDescr :: Lifter PackageDescription GenericPackageDescription
+onPkgDescr f gpd = gpd { packageDescription = f (packageDescription gpd) }
+
+onExtraSrcFiles :: Lifter [FilePath] PackageDescription
+onExtraSrcFiles f pd = pd { extraSrcFiles = f (extraSrcFiles pd) }
diff --git a/changelog b/changelog
new file mode 100644
--- /dev/null
+++ b/changelog
@@ -0,0 +1,3 @@
+0.1.0.0
+
+	* Initial release.
diff --git a/libvoyeur/LICENSE b/libvoyeur/LICENSE
new file mode 100644
--- /dev/null
+++ b/libvoyeur/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2014, Seth Fowler
+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 the {organization} nor the names of its
+  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 HOLDER 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/libvoyeur/Makefile b/libvoyeur/Makefile
new file mode 100644
--- /dev/null
+++ b/libvoyeur/Makefile
@@ -0,0 +1,152 @@
+###############################################################################
+# Customizable values
+###############################################################################
+
+CC?=clang
+CFLAGS?=
+LDFLAGS?=
+PREFIX?=/usr/local/bin
+
+CFLAGS+=-I./include -std=c99
+
+ifdef DEBUG
+CFLAGS+=-g -DDEBUG
+endif
+
+
+###############################################################################
+# Outputs
+###############################################################################
+
+MAINLIBNAME=libvoyeur
+LIBNAMES=libvoyeur-exec libvoyeur-exit libvoyeur-open libvoyeur-close
+TESTNAMES=test-exec test-exec-recursive test-open test-exec-and-open test-open-and-close test-exec-variants
+TESTHARNESSNAME=voyeur-test
+LIBNULLNAME=libnull
+EXAMPLENAMES=voyeur-watch-exec voyeur-watch-open
+
+
+###############################################################################
+# Computed values
+###############################################################################
+
+UNAME := $(shell uname -s)
+ifeq ($(UNAME), Darwin)
+  LIBSUFFIX=dylib
+else
+  LIBSUFFIX=so
+  CFLAGS+=-fPIC -pthread
+  LDFLAGS+=-lbsd -ldl
+endif
+
+HEADERS=$(wildcard src/*.h) $(wildcard include/*.h)
+LIBSOURCES=$(wildcard src/*.c)
+LIBOBJECTS=$(patsubst src/%.c, build/%.o, $(LIBSOURCES))
+TESTSOURCES=$(wildcard test/*.c)
+TESTOBJECTS=$(patsubst test/%.c, build/%.o, $(TESTSOURCES))
+OBJECTS=$(LIBOBJECTS)
+LIBS=$(addprefix build/, $(addsuffix .$(LIBSUFFIX), $(LIBNAMES)))
+MAINLIB=$(addprefix build/, $(addsuffix .$(LIBSUFFIX), $(MAINLIBNAME)))
+MAINSTATICLIB=$(addprefix build/, $(addsuffix .a, $(MAINLIBNAME)))
+TESTS=$(addprefix build/, $(TESTNAMES))
+TESTHARNESS=$(addprefix build/, $(TESTHARNESSNAME))
+LIBNULL=$(addprefix build/, $(addsuffix .$(LIBSUFFIX), $(LIBNULLNAME)))
+EXAMPLES=$(addprefix build/, $(EXAMPLENAMES))
+BUILDDIR=$(realpath build/)
+
+ifeq ($(UNAME), Darwin)
+  define make-dynamic-lib
+  $(CC) $(CFLAGS) $^ -dynamiclib -install_name lib$*.$(LIBSUFFIX) -o $@ $(LDFLAGS)
+  endef
+
+  define make-exec
+  $(CC) $(CFLAGS) $< -o $@ -Lbuild -lvoyeur $(LDFLAGS)
+  endef
+
+  define make-test
+  $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
+  endef
+else
+  define make-dynamic-lib
+  $(CC) $(CFLAGS) $^ -shared -Wl,-soname,lib$*.$(LIBSUFFIX) -o $@ $(LDFLAGS)
+  endef
+
+  define make-exec
+  $(CC) $(CFLAGS) -Wl,-rpath '-Wl,$$ORIGIN' $< -o $@ -Lbuild -lvoyeur $(LDFLAGS)
+  endef
+
+  define make-test
+  $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
+  endef
+endif
+
+.PHONY: default check examples install clean
+
+
+###############################################################################
+# Library targets
+###############################################################################
+
+default: build $(LIBS) $(MAINLIB) $(MAINSTATICLIB)
+
+build:
+	mkdir -p $@
+
+$(OBJECTS): build/%.o : src/%.c $(HEADERS)
+	$(CC) $(CFLAGS) -c $< -o $@
+
+$(LIBS): build/lib%.$(LIBSUFFIX) : build/%.o build/net.o build/env.o build/event.o build/util.o
+	$(make-dynamic-lib)
+
+$(MAINLIB): build/lib%.$(LIBSUFFIX) : build/%.o build/net.o build/env.o build/event.o build/util.o
+	$(make-dynamic-lib)
+
+$(MAINSTATICLIB): build/lib%.a : build/%.o build/net.o build/env.o build/event.o build/util.o
+	$(AR) rcs $@ $^
+
+
+###############################################################################
+# Test targets
+###############################################################################
+
+check: default $(TESTHARNESS) $(TESTS) $(LIBNULL)
+	cd build && ./$(TESTHARNESSNAME)
+
+$(TESTHARNESS): build/% : test/%.c $(LIBS)
+	$(make-exec)
+
+$(TESTS): build/% : test/%.c $(LIBS)
+	$(make-test)
+
+$(LIBNULL): build/lib%.$(LIBSUFFIX) : test/%.c
+	$(make-dynamic-lib)
+
+
+###############################################################################
+# Example targets
+###############################################################################
+
+examples: default $(EXAMPLES)
+
+$(EXAMPLES): build/% : examples/%.c $(HEADERS)
+	$(make-exec)
+
+
+###############################################################################
+# Install targets
+###############################################################################
+
+install: default
+	install -c $(MAINLIB) $(MAINSTATICLIB) $(LIBS) $(PREFIX)
+
+uninstall:
+	rm $(addprefix $(PREFIX)/, $(notdir $(MAINLIB)))
+	rm $(addprefix $(PREFIX)/, $(notdir $(MAINSTATICLIB)))
+	rm $(addprefix $(PREFIX)/, $(notdir $(LIBS)))
+
+###############################################################################
+# Utility targets
+###############################################################################
+
+clean:
+	rm -rf build
diff --git a/libvoyeur/README.md b/libvoyeur/README.md
new file mode 100644
--- /dev/null
+++ b/libvoyeur/README.md
@@ -0,0 +1,38 @@
+libvoyeur
+=========
+
+A BSD-licensed library for observing the private behavior of a child process.
+
+Mac OS X and Linux are supported.
+
+The following calls are currently observable:
+
+- Process creation with `exec*`
+- File access with `open` and `close`
+
+More are planned. The library has been designed so that adding support for new
+calls is easy.
+
+There is a significant performance cost to the observed process, because the
+details of the calls are sent to the observing process over IPC, and locking is
+required. However, libvoyeur tries to be as efficient as possible: you only pay
+a performance penalty for the calls that you actually want to observe.
+
+Compilation
+===========
+
+Just run `make`. You can check that everything built correctly with `make check`.
+
+Usage
+=====
+
+The program doing the observing must link against `libvoyeur`.
+
+If you link statically, or if the various helper libraries like `libvoyeur-exec`
+are not in the same directory as `libvoyeur`, you'll need to tell the library
+where to find them using `voyeur_set_resource_path`.
+
+The public API is documented in [voyeur.h](include/voyeur.h).
+
+The [examples directory](examples/) contains some sample programs built using
+`libvoyeur`. You can build the examples with `make examples`.
diff --git a/libvoyeur/include/voyeur.h b/libvoyeur/include/voyeur.h
new file mode 100644
--- /dev/null
+++ b/libvoyeur/include/voyeur.h
@@ -0,0 +1,205 @@
+#ifndef LIBVOYEUR_VOYEUR_H
+#define LIBVOYEUR_VOYEUR_H
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <sys/types.h>
+
+//////////////////////////////////////////////////
+// Overview of libvoyeur.
+//////////////////////////////////////////////////
+
+// Libvoyeur is a library for observing the private activity of a
+// child process. It does this by using the dynamic linker to override
+// the implementations of standard library functions. When those
+// functions are called, libvoyeur generates events, and by
+// registering callbacks you can observe those events and take
+// whatever action you want.
+
+// The flow of using libvoyeur is simple. Start by creating a context:
+//
+// > voyeur_context_t ctx = voyeur_context_create();
+//
+// Then register to observe some events:
+//
+// > voyeur_observe_exec(ctx,
+// >                     OBSERVE_EXEC_CMD | OBSERVE_EXEC_ENV,
+// >                     my_exec_callback,
+// >                     NULL);
+//
+// Use voyeur_exec() to start and observe the child process:
+//
+// > voyeur_exec(ctx, "/usr/bin/program", argv, envp);
+//
+// Finally, destroy the context:
+//
+// > voyeur_context_destroy(ctx);
+
+// Users with more complex needs or who intend to provide bindings to
+// languages other than C will probably want to use voyeur_prepare()
+// and voyeur_start() rather than voyeur_exec(). Their documentation
+// is below.
+
+
+//////////////////////////////////////////////////
+// Creating and destroying libvoyeur contexts.
+//////////////////////////////////////////////////
+
+typedef void* voyeur_context_t;
+
+// Creates a context used to specify which events you want to observe
+// and as an owner for resources allocated by voyeur_prepare().
+voyeur_context_t voyeur_context_create();
+
+// Destroys a context. This must always be called after voyeur_start()
+// or voyeur_exec() is finished to release the resources libvoyeur has allocated.
+// After destroying a context, it is invalid and should not be used again.
+void voyeur_context_destroy(voyeur_context_t ctx);
+
+
+//////////////////////////////////////////////////
+// Registering callbacks for particular events.
+//////////////////////////////////////////////////
+
+// Observing exec*() calls.
+typedef void (*voyeur_exec_callback)(const char* file,
+                                     char* const argv[],
+                                     char* const envp[],
+                                     const char* path,
+                                     const char* cwd,
+                                     pid_t pid,
+                                     pid_t ppid,
+                                     void* userdata);
+typedef enum {
+  OBSERVE_EXEC_DEFAULT  = 0,
+  OBSERVE_EXEC_CWD      = 1 << 0,  // Include 'cwd' (working directory).
+  OBSERVE_EXEC_ENV      = 1 << 1,  // Include 'envp' (environment).
+  OBSERVE_EXEC_PATH     = 1 << 2,  // Include the value of 'PATH'.
+  OBSERVE_EXEC_NOACCESS = 1 << 3,  // Include exec calls for paths
+                                   // that don't exist or aren't executable.
+} voyeur_exec_options;
+void voyeur_observe_exec(voyeur_context_t ctx,
+                         uint8_t opts,
+                         voyeur_exec_callback callback,
+                         void* userdata);
+
+// Observing when processes exit.
+typedef void (*voyeur_exit_callback)(int status,
+                                     pid_t pid,
+                                     pid_t ppid,
+                                     void* userdata);
+typedef enum {
+  OBSERVE_EXIT_DEFAULT  = 0,
+} voyeur_exit_options;
+void voyeur_observe_exit(voyeur_context_t ctx,
+                         uint8_t opts,
+                         voyeur_exit_callback callback,
+                         void* userdata);
+
+// Observing open() calls.
+typedef void (*voyeur_open_callback)(const char* path,
+                                     int oflag,
+                                     mode_t mode,
+                                     const char* cwd,
+                                     int retval,
+                                     pid_t pid,
+                                     void* userdata);
+typedef enum {
+  OBSERVE_OPEN_DEFAULT = 0,
+  OBSERVE_OPEN_CWD     = 1 << 0,  // Include 'cwd' (working directory).
+} voyeur_open_options;
+void voyeur_observe_open(voyeur_context_t ctx,
+                         uint8_t opts,
+                         voyeur_open_callback callback,
+                         void* userdata);
+
+// Observing close() calls.
+typedef void (*voyeur_close_callback)(int fd,
+                                      int retval,
+                                      pid_t pid,
+                                      void* userdata);
+typedef enum {
+  OBSERVE_CLOSE_DEFAULT = 0,
+} voyeur_close_options;
+void voyeur_observe_close(voyeur_context_t ctx,
+                          uint8_t opts,
+                          voyeur_close_callback callback,
+                          void* userdata);
+
+
+//////////////////////////////////////////////////
+// Other context configuration options.
+//////////////////////////////////////////////////
+
+// Set the path where libvoyeur should look for its resources.
+//
+// To inject code into child processes, libvoyeur uses a set
+// of helper dynamic libraries. By default, libvoyeur assumes
+// that those libraries are located in the same directory as
+// libvoyeur itself, which is to say in the same directory as
+// libvoyeur.so/.dylib if using dynamic linking, or in the same
+// directory as the main program if using static linking. If
+// this assumption doesn't hold, this function can be used to
+// provide the path where libvoyeur should look for these files.
+//
+// The provided path must have a trailing path separator -
+// in other words, it must end with a '/'.
+void voyeur_set_resource_path(voyeur_context_t ctx,
+                              const char* path);
+
+
+//////////////////////////////////////////////////
+// Observing processes.
+//////////////////////////////////////////////////
+
+// Prepare to observe a child process.
+//
+// This will acquire resources that libvoyeur needs to do its work and
+// create a modified environment, based upon the provided template,
+// that should be passed to the child process when it gets exec'd.
+// It's the caller's responsibility to free this environment. (Note
+// that unlike other resources acquired by libvoyeur, the environment
+// is not freed when voyeur_context_destroy() is called.)
+//
+// This function should be called before forking. After calling
+// voyeur_prepare(), it isn't safe to call voyeur_observe_* functions
+// on the same context anymore.
+//
+// Returns NULL on failure.
+char** voyeur_prepare(voyeur_context_t ctx, char* const envp[]);
+
+// Start observing a child process.
+//
+// This will block until the child process completes, at which time
+// it will return its exit status. While the child process is running,
+// the callbacks you've registered with the voyeur_observe_* functions
+// will be called.
+//
+// Call this after forking, in the parent process. After
+// voyeur_start() returns, you should use voyeur_context_destroy()
+// to release the resources libvoyeur has acquired.
+int voyeur_start(voyeur_context_t ctx, pid_t child_pid);
+
+// Create and observe a new child process.
+//
+// voyeur_exec() is a convenience function that behaves just as if
+// you had made this sequence of calls:
+//
+// > voyeur_envp = voyeur_prepare(ctx, envp);
+// > posix_spawn(&pid, path, NULL, NULL, argv, voyeur_envp);
+// > voyeur_start(ctx, pid);
+// > free(voyeur_envp);
+//
+// Like voyeur_start(), it returns the exit status of the child. After
+// voyeur_exec() returns, you should use voyeur_context_destroy() to
+// release the resources libvoyeur has acquired.
+//
+// voyeur_exec() may fail, in which case it will return -1. If you need
+// to distinguish between a child process exit status of -1 and a failure
+// inside libvoyeur, use voyeur_prepare() and voyeur_start().
+int voyeur_exec(voyeur_context_t ctx,
+                const char* path,
+                char* const argv[],
+                char* const envp[]);
+
+#endif
diff --git a/libvoyeur/src/dyld.h b/libvoyeur/src/dyld.h
new file mode 100644
--- /dev/null
+++ b/libvoyeur/src/dyld.h
@@ -0,0 +1,30 @@
+#ifndef VOYEUR_DYLD_H
+#define VOYEUR_DYLD_H
+
+#include <dlfcn.h>
+
+#ifdef __APPLE__
+
+#define VOYEUR_FUNC(_foo) voyeur_##_foo
+#define VOYEUR_DECLARE_NEXT(_foo_t, _foo)
+#define VOYEUR_STATIC_DECLARE_NEXT(_foo_t, _foo)
+#define VOYEUR_LOOKUP_NEXT(_foo_t, _foo)
+#define VOYEUR_CALL_NEXT(_foo, ...) _foo(__VA_ARGS__)
+
+// Based on DYLD_INTERPOSE in dyld-interposing.h:
+#define VOYEUR_INTERPOSE(_replacee) \
+  __attribute__((used)) static struct{ const void* replacment; const void* replacee; } _interpose_##_replacee \
+  __attribute__ ((section ("__DATA,__interpose"))) = { (const void*)(unsigned long)&VOYEUR_FUNC(_replacee), (const void*)(unsigned long)&_replacee }; 
+
+#else
+
+#define VOYEUR_FUNC(_foo) _foo
+#define VOYEUR_DECLARE_NEXT(_foo_t, _foo) _foo_t voyeur_##_foo##_next = NULL;
+#define VOYEUR_STATIC_DECLARE_NEXT(_foo_t, _foo) static _foo_t voyeur_##_foo##_next = NULL;
+#define VOYEUR_LOOKUP_NEXT(_foo_t, _foo) voyeur_##_foo##_next = (_foo_t) dlsym(RTLD_NEXT, #_foo)
+#define VOYEUR_CALL_NEXT(_foo, ...) voyeur_##_foo##_next(__VA_ARGS__)
+#define VOYEUR_INTERPOSE(_replacee)
+
+#endif
+
+#endif
diff --git a/libvoyeur/src/env.c b/libvoyeur/src/env.c
new file mode 100644
--- /dev/null
+++ b/libvoyeur/src/env.c
@@ -0,0 +1,110 @@
+#ifdef __linux__
+#define _GNU_SOURCE
+#endif
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#ifdef __linux__
+#include <bsd/bsd.h>
+#endif
+
+#include "env.h"
+
+#ifdef __APPLE__
+#define INSERT_LIBS "DYLD_INSERT_LIBRARIES="
+#else
+#define INSERT_LIBS "LD_PRELOAD="
+#endif
+
+typedef char env_buf[2048];
+
+char** voyeur_augment_environment(char* const* envp,
+                                  const char* voyeur_libs,
+                                  const char* voyeur_opts,
+                                  const char* sockpath,
+                                  void** buf_out)
+{
+  // Determine the size of the original environment.
+  unsigned envlen = 0;
+  unsigned existing_insert_idx = 0;
+  char* existing_insert = NULL;
+  for ( ; envp[envlen] != NULL ; ++envlen) {
+    if (strncmp(envp[envlen], INSERT_LIBS, sizeof(INSERT_LIBS) - 1) == 0) {
+      existing_insert = envp[envlen] +
+                             sizeof(INSERT_LIBS) - 1;
+      existing_insert_idx = envlen;
+    }
+  }
+
+  char must_add_voyeur_libs = 1;
+  if (existing_insert && strnstr(existing_insert, voyeur_libs, sizeof(env_buf) * 2)) {
+    must_add_voyeur_libs = 0;
+  }
+  if (must_add_voyeur_libs &&
+      (strlen(voyeur_libs) + (existing_insert ? strlen(existing_insert) : 0)) > sizeof(env_buf)) {
+    fprintf(stderr, "Not enough space to insert libvoyeur into " INSERT_LIBS);
+    must_add_voyeur_libs = 0;
+  }
+
+  // Allocate the new environment variables.
+  env_buf* buf = malloc(sizeof(env_buf) * 4);
+  *buf_out = (void*) buf;
+
+  strlcpy(buf[0], "LIBVOYEUR_LIBS=", sizeof(env_buf));
+  strlcat(buf[0], voyeur_libs, sizeof(env_buf));
+
+  strlcpy(buf[1], "LIBVOYEUR_OPTS=", sizeof(env_buf));
+  strlcat(buf[1], voyeur_opts, sizeof(env_buf));
+
+  strlcpy(buf[2], "LIBVOYEUR_SOCKET=", sizeof(env_buf));
+  strlcat(buf[2], sockpath, sizeof(env_buf));
+
+  strlcpy(buf[3], INSERT_LIBS, sizeof(env_buf));
+  if (must_add_voyeur_libs) {
+    strlcat(buf[3], voyeur_libs, sizeof(env_buf));
+    if (existing_insert) {
+      strlcat(buf[3], ":", sizeof(env_buf));
+      strlcat(buf[3], existing_insert, sizeof(env_buf));
+    }
+  } else if (existing_insert) {
+    strlcat(buf[3], existing_insert, sizeof(env_buf));
+  }
+
+  // Allocate a new environment, including additional space for the 4
+  // extra environment variables we'll add and a terminating NULL.
+  char** newenvp = malloc(sizeof(char*) * (envlen + 5));
+  memcpy(newenvp, envp, sizeof(char*) * envlen);
+  newenvp[envlen + 0] = buf[0];
+  newenvp[envlen + 1] = buf[1];
+  newenvp[envlen + 2] = buf[2];
+
+  if (existing_insert) {
+    // Make sure we don't have two INSERT_LIBS environment variables.
+    newenvp[existing_insert_idx] = buf[3];
+    newenvp[envlen + 3] = NULL;
+  } else {
+    newenvp[envlen + 3] = buf[3];
+    newenvp[envlen + 4] = NULL;
+  }
+
+  return newenvp;
+}
+
+char voyeur_encode_options(uint8_t opts)
+{
+  // Stripping all but the last 5 bits and bitwise-or'ing with '@' will always
+  // result in a printable character. The downside is that we can only
+  // support 5 flags this way.
+  return '@' | ((char) opts & 0x1F);
+}
+
+uint8_t voyeur_decode_options(const char* opts, uint8_t offset)
+{
+  if (!opts || offset > strnlen(opts, 8)) {
+    return 0;
+  }
+
+  return (uint8_t) (opts[offset] & 0x1F);
+}
diff --git a/libvoyeur/src/env.h b/libvoyeur/src/env.h
new file mode 100644
--- /dev/null
+++ b/libvoyeur/src/env.h
@@ -0,0 +1,20 @@
+#ifndef VOYEUR_ENV_H
+#define VOYEUR_ENV_H
+
+#include <voyeur.h>
+
+// Augments the provided list of environment variables with the
+// variables required for libvoyeur to observe a process. If the
+// caller doesn't fork, then after calling exec() they should free
+// both the returned environment and the buffer returned in buf_out.
+char** voyeur_augment_environment(char* const* envp,
+                                  const char* voyeur_libs,
+                                  const char* voyeur_opts,
+                                  const char* sockpath,
+                                  void** buf_out);
+
+// Encoding and decoding options.
+char voyeur_encode_options(uint8_t opts);
+uint8_t voyeur_decode_options(const char* opts, uint8_t offset);
+
+#endif
diff --git a/libvoyeur/src/event.c b/libvoyeur/src/event.c
new file mode 100644
--- /dev/null
+++ b/libvoyeur/src/event.c
@@ -0,0 +1,307 @@
+#ifdef __linux__
+#define _GNU_SOURCE
+#endif
+
+#include <dlfcn.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#ifdef __linux__
+#include <bsd/bsd.h>
+#endif
+
+#include "env.h"
+#include "event.h"
+#include "net.h"
+#include "util.h"
+#include <voyeur.h>
+
+// Note that in event handlers, we always have to read the data,
+// even if the callback isn't present, so we can move on to the
+// next event in the stream. In practice the callback should always be
+// present, so it's not worth worrying about.
+
+static void handle_exec(voyeur_context* context, int fd)
+{
+  // Read the path.
+  char* file;
+  RETURN_ON_FAIL(voyeur_read_string, fd, &file, 0);
+
+  // Read the arguments.
+  int argc;
+  RETURN_ON_FAIL(voyeur_read_int, fd, &argc);
+
+  char** argv = malloc(sizeof(char*) * (argc + 1));
+  for (int i = 0 ; i < argc ; ++i) {
+    char* arg;
+    RETURN_ON_FAIL(voyeur_read_string, fd, &arg, 0);
+    argv[i] = arg;
+  }
+  argv[argc] = NULL;
+
+  // Read the environment.
+  int envc;
+  char** envp = NULL;
+  if (context->exec_opts & OBSERVE_EXEC_ENV) {
+    RETURN_ON_FAIL(voyeur_read_int, fd, &envc);
+
+    envp = malloc(sizeof(char*) * (envc + 1));
+    for (int i = 0 ; i < envc ; ++i) {
+      char* envvar;
+      RETURN_ON_FAIL(voyeur_read_string, fd, &envvar, 0);
+      envp[i] = envvar;
+    }
+    envp[envc] = NULL;
+  }
+
+  // Read the value of PATH.
+  char* path = NULL;
+  if (context->exec_opts & OBSERVE_EXEC_PATH) {
+    RETURN_ON_FAIL(voyeur_read_string, fd, &path, 0);
+  }
+
+  // Read the current working directory.
+  char* cwd = NULL;
+  if (context->exec_opts & OBSERVE_EXEC_CWD) {
+    RETURN_ON_FAIL(voyeur_read_string, fd, &cwd, 0);
+  }
+
+  // Read the pid and ppid.
+  pid_t pid, ppid;
+  RETURN_ON_FAIL(voyeur_read_pid, fd, &pid);
+  RETURN_ON_FAIL(voyeur_read_pid, fd, &ppid);
+
+  if (context->exec_cb) {
+    ((voyeur_exec_callback)context->exec_cb)(file, argv, envp,
+                                             path, cwd,
+                                             pid, ppid,
+                                             context->exec_userdata);
+  }
+
+  // Free everything.
+  free(file);
+
+  for (int i = 0 ; i < argc ; ++i) {
+    free(argv[i]);
+  }
+  free(argv);
+
+  if (context->exec_opts & OBSERVE_EXEC_ENV) {
+    for (int i = 0 ; i < envc ; ++i) {
+      free(envp[i]);
+    }
+    free(envp);
+  }
+
+  if (context->exec_opts & OBSERVE_EXEC_PATH) {
+    free(path);
+  }
+
+  if (context->exec_opts & OBSERVE_EXEC_CWD) {
+    free(cwd);
+  }
+}
+
+static void handle_exit(voyeur_context* context, int fd)
+{
+  int status;
+  pid_t pid, ppid;
+
+  RETURN_ON_FAIL(voyeur_read_int, fd, &status);
+  RETURN_ON_FAIL(voyeur_read_pid, fd, &pid);
+  RETURN_ON_FAIL(voyeur_read_pid, fd, &ppid);
+
+  if (context->exit_cb) {
+    ((voyeur_exit_callback)context->exit_cb)(status, pid, ppid,
+                                             context->exit_userdata);
+  }
+}
+
+static void handle_open(voyeur_context* context, int fd)
+{
+  char* path;
+  int oflag, mode, retval;
+  char* cwd = NULL;
+  pid_t pid;
+
+  RETURN_ON_FAIL(voyeur_read_string, fd, &path, 0);
+  RETURN_ON_FAIL(voyeur_read_int, fd, &oflag);
+  RETURN_ON_FAIL(voyeur_read_int, fd, &mode);
+  RETURN_ON_FAIL(voyeur_read_int, fd, &retval);
+
+  if (context->open_opts & OBSERVE_OPEN_CWD) {
+    RETURN_ON_FAIL(voyeur_read_string, fd, &cwd, 0);
+  }
+
+  RETURN_ON_FAIL(voyeur_read_pid, fd, &pid);
+
+  if (context->open_cb) {
+    ((voyeur_open_callback)context->open_cb)(path, oflag,
+                                             (mode_t) mode,
+                                             cwd, retval, pid,
+                                             context->open_userdata);
+  }
+
+  free(path);
+}
+
+static void handle_close(voyeur_context* context, int fd)
+{
+  int fildes, retval;
+  pid_t pid;
+
+  RETURN_ON_FAIL(voyeur_read_int, fd, &fildes);
+  RETURN_ON_FAIL(voyeur_read_int, fd, &retval);
+  RETURN_ON_FAIL(voyeur_read_pid, fd, &pid);
+
+  if (context->close_cb) {
+    ((voyeur_close_callback)context->close_cb)(fildes, retval, pid,
+                                               context->close_userdata);
+  }
+}
+
+#define ON_EVENT(E, e)                          \
+  case VOYEUR_EVENT_##E:                        \
+    handle_##e(context, fd);                    \
+    break;
+
+void voyeur_handle_event(voyeur_context* context, voyeur_event_type type, int fd)
+{
+  switch (type) {
+    MAP_EVENTS
+    default:
+      SHOULD_NOT_REACH("libvoyeur: got unknown event type %u\n",
+                       (unsigned) type);
+      return;
+  }
+}
+
+#undef ON_EVENT
+
+#ifdef __APPLE__
+#define LIB_SUFFIX ".dylib"
+#else
+#define LIB_SUFFIX ".so"
+#endif
+
+#define ON_EVENT(_, e)           \
+  "libvoyeur-" #e LIB_SUFFIX ":" \
+
+static size_t compute_libs_size(size_t libdir_size)
+{
+  static char all_libs[] =
+    MAP_EVENTS
+    ;
+
+  return sizeof(all_libs) + VOYEUR_EVENT_MAX * libdir_size;
+}
+
+#undef ON_EVENT
+
+static char* get_default_resource_path(voyeur_context* context, bool* did_allocate)
+{
+  // We want absolute paths to the libraries, relative to the location of
+  // libvoyeur. We use dladdr() to determine that location.
+  *did_allocate = false;
+  char* libdir = "./";
+  Dl_info dlinfo;
+
+  if (dladdr(voyeur_requested_libs, &dlinfo) && dlinfo.dli_fname) {
+    // Strip the filename. It'd be great if 'dirname' could be used
+    // for this, but it's not thread safe. Note that if no slash is
+    // found, we just stick with "./".
+    char* last_slash = strrchr(dlinfo.dli_fname, '/');
+    if (last_slash) {
+      *did_allocate = true;
+      int len_with_slash_and_null = (last_slash - dlinfo.dli_fname) + 2;
+      libdir = calloc(1, len_with_slash_and_null);
+      strlcpy(libdir, dlinfo.dli_fname, len_with_slash_and_null);
+    }
+  }
+
+  return libdir;
+}
+
+#define ON_EVENT(_, e)                                    \
+  if (context->e##_cb) {                                  \
+    if (prev) strlcat(libs, ":", libs_size);              \
+    strlcat(libs, libdir, libs_size);                     \
+    strlcat(libs, "libvoyeur-" #e LIB_SUFFIX, libs_size); \
+    prev = 1;                                             \
+  }                                                       \
+
+char* voyeur_requested_libs(voyeur_context* context)
+{
+  bool did_allocate = false;
+  char* libdir = context->resource_path
+               ? context->resource_path
+               : get_default_resource_path(context, &did_allocate);
+
+  size_t libs_size = compute_libs_size(strnlen(libdir, 4096));
+  char* libs = calloc(1, libs_size);
+  char prev = 0;
+  
+  // If the user isn't observing exec events, we still want to apply libvoyeur
+  // recursively, so we link in voyeur-exec in any case. (We set
+  // OBSERVE_EXEC_SILENT so they won't get any events because of this.)
+  char did_force_exec = 0;
+  if (!context->exec_cb) {
+    did_force_exec = 1;
+    context->exec_cb = &did_force_exec;  // Just a dummy value.
+  }
+
+  MAP_EVENTS
+
+  if (did_force_exec) {
+    context->exec_cb = NULL;
+  }
+
+  if (did_allocate) {
+    free(libdir);
+  }
+
+  return libs;
+}
+
+#undef ON_EVENT
+
+#define ON_EVENT(E, e) opts[VOYEUR_EVENT_##E] = voyeur_encode_options(context->e##_opts);
+
+char* voyeur_requested_opts(voyeur_context* context)
+{
+  // VOYEUR_EVENT_MAX is one greater than the highest event number and
+  // thus leaves room for a terminating null.
+  char* opts = calloc(1, VOYEUR_EVENT_MAX);
+
+  // If the user isn't observing exec events, we still want to apply libvoyeur
+  // recursively, so we link in voyeur-exec in any case. (We set
+  // OBSERVE_EXEC_SILENT so they won't get any events because of this.)
+  if (!context->exec_cb) {
+    context->exec_opts = OBSERVE_EXEC_SILENT;
+  }
+
+  MAP_EVENTS
+
+  return opts;
+}
+
+#undef ON_EVENT
+
+#define ON_EVENT(_, e)                                  \
+void voyeur_observe_##e(voyeur_context_t ctx,           \
+                        uint8_t opts,                   \
+                        voyeur_##e##_callback callback, \
+                        void* userdata)                 \
+{                                                       \
+  voyeur_context* context = (voyeur_context*) ctx;      \
+  context->e##_opts = opts;                             \
+  context->e##_cb = (void*) callback;                   \
+  context->e##_userdata = userdata;                     \
+}
+
+MAP_EVENTS
+
+#undef ON_EVENT
diff --git a/libvoyeur/src/event.h b/libvoyeur/src/event.h
new file mode 100644
--- /dev/null
+++ b/libvoyeur/src/event.h
@@ -0,0 +1,58 @@
+#ifndef VOYEUR_EVENTS_H
+#define VOYEUR_EVENTS_H
+
+#include <stdint.h>
+
+// How to define a new event:
+// 1. Add the new event name to MAP_EVENTS.
+// 2. Define the public API in voyeur.h. (To keep things readable, avoid using
+//    MAP_EVENTS there.)
+// 3. Implement voyeur-xxx.c based on one of the existing events, and
+//    add it to LIBNAMES in the Makefile.
+// 4. Implement a handle_xxx function in event.c.
+// 5. Add a test!
+
+#define MAP_EVENTS                              \
+  ON_EVENT(EXEC, exec)                          \
+  ON_EVENT(EXIT, exit)                          \
+  ON_EVENT(OPEN, open)                          \
+  ON_EVENT(CLOSE, close)                        \
+
+// Define an enumeration for event types.
+#define ON_EVENT(E, _) VOYEUR_EVENT_##E,
+typedef enum {
+  MAP_EVENTS
+  VOYEUR_EVENT_MAX
+} voyeur_event_type;
+#undef ON_EVENT
+
+// Define the voyeur_context type, which is primarily a container for
+// event options and callbacks.
+#define ON_EVENT(_, e)                          \
+  uint8_t e##_opts;                             \
+  void* e##_cb;                                 \
+  void* e##_userdata;
+
+typedef struct {
+  MAP_EVENTS
+
+  char* resource_path;
+  void* server_state;
+} voyeur_context;
+
+#undef ON_EVENT
+
+// Dispatch to the correct handler for the given event type.
+void voyeur_handle_event(voyeur_context* context, voyeur_event_type type, int fd);
+
+// Create the VOYEUR_LIBS and VOYEUR_OPTS strings based on the
+// context. The caller is responsible for freeing them.
+char* voyeur_requested_libs(voyeur_context* context);
+char* voyeur_requested_opts(voyeur_context* context);
+
+// A special hidden option for the exec() handler.
+typedef enum {
+  OBSERVE_EXEC_SILENT = 1 << 4
+} voyeur_extra_exec_options;
+
+#endif
diff --git a/libvoyeur/src/net.c b/libvoyeur/src/net.c
new file mode 100644
--- /dev/null
+++ b/libvoyeur/src/net.c
@@ -0,0 +1,253 @@
+#ifdef __linux__
+#define _GNU_SOURCE
+#endif
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#ifdef __linux__
+#include <bsd/bsd.h>
+#endif
+
+#include "net.h"
+#include "util.h"
+
+int voyeur_create_server_socket(struct sockaddr_un* sockinfo)
+{
+  // Configure a unix domain socket at a temporary path.
+  char sockdir[] = "/tmp/libvoyeur-XXXXXXXXX";
+  mkdtemp(sockdir);
+  
+  memset(sockinfo, 0, sizeof(struct sockaddr_un));
+  sockinfo->sun_family = AF_UNIX;
+  strlcpy(sockinfo->sun_path, sockdir, sizeof(sockinfo->sun_path));
+  strlcat(sockinfo->sun_path, "/socket", sizeof(sockinfo->sun_path));
+  unlink(sockinfo->sun_path);
+
+  // Start the server.
+  int server_sock = socket(AF_UNIX, SOCK_STREAM, 0);
+  RETURN_ERROR_ON_FAIL(bind, server_sock, (struct sockaddr*) sockinfo,
+                                          sizeof(struct sockaddr_un));
+  RETURN_ERROR_ON_FAIL(listen, server_sock, 200);
+
+  //chmod(sockinfo->sun_path, 0777);
+  //fchmod(server_sock, 0777);
+
+  fcntl(server_sock, F_SETFD, FD_CLOEXEC);
+  return server_sock;
+}
+
+#define CONNECT_RETRIES 20
+#define CONNECT_WAIT_MS 50
+#define CONNECT_DEVIATION_MS 20
+
+int voyeur_create_client_socket(const char* sockpath)
+{
+  struct sockaddr_un sockinfo;
+
+  memset(&sockinfo, 0, sizeof(struct sockaddr_un));
+  sockinfo.sun_family = AF_UNIX;
+  strlcpy(sockinfo.sun_path, sockpath, sizeof(sockinfo.sun_path));
+
+  // Connect to the server.
+  int client_sock = socket(AF_UNIX, SOCK_STREAM, 0);
+  int set = 1;
+
+#ifdef __APPLE__
+  setsockopt(client_sock, SOL_SOCKET, SO_NOSIGPIPE, (void *)&set, sizeof(int));
+#endif
+
+  unsigned try = 0;
+  int connect_status = connect(client_sock,
+                               (struct sockaddr*) &sockinfo,
+                               sizeof(struct sockaddr_un));
+  
+  while (connect_status < 0 && ++try < CONNECT_RETRIES) {
+    usleep(((CONNECT_WAIT_MS - CONNECT_DEVIATION_MS / 2) +
+            arc4random_uniform(CONNECT_DEVIATION_MS)) * 1000);
+    connect_status = connect(client_sock,
+                             (struct sockaddr*) &sockinfo,
+                             sizeof(struct sockaddr_un));
+  }
+
+  fcntl(client_sock, F_SETFD, FD_CLOEXEC);
+  return client_sock;
+}
+
+void voyeur_close_socket(int fd)
+{
+  while (close(fd) < 0) {
+    if (errno != EINTR) {
+      // We really only want to keep spinning for EINTR.
+      break;
+    }
+  }
+}
+
+#ifdef __linux__
+#define SEND_OPTS MSG_NOSIGNAL
+#else
+#define SEND_OPTS 0
+#endif
+
+static int do_write(int fd, void* buf, size_t buf_size)
+{
+  ssize_t total_out = 0;
+  while (total_out < (ssize_t) buf_size) {
+    ssize_t out = send(fd,
+                       buf + total_out,
+                       buf_size - total_out,
+                       SEND_OPTS);
+
+    // Handle errors.
+    if (out < 0) {
+      if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
+        continue;
+      } else {
+        return -1;
+      }
+    }
+
+    total_out += out;
+  }
+
+  return 0;
+}
+
+static int do_read(int fd, void* buf, size_t buf_size)
+{
+  ssize_t total_in = 0;
+  while (total_in < (ssize_t) buf_size) {
+    ssize_t in = read(fd,
+                      buf + total_in,
+                      buf_size - total_in);
+
+    // Handle errors.
+    if (in < 0) {
+      if (errno == EAGAIN || errno == EINTR) {
+        continue;
+      } else {
+        return -1;
+      }
+    }
+
+    // Handle EOF.
+    if (in == 0) {
+      return -1;
+    }
+
+    total_in += in;
+  }
+
+  return 0;
+}
+  
+int voyeur_write_msg_type(int fd, voyeur_msg_type val)
+{
+  return do_write(fd, (void*) &val, sizeof(voyeur_msg_type));
+}
+
+int voyeur_read_msg_type(int fd, voyeur_msg_type* val)
+{
+  return do_read(fd, (void*) val, sizeof(voyeur_msg_type));
+}
+
+int voyeur_write_event_type(int fd, voyeur_event_type val)
+{
+  return do_write(fd, (void*) &val, sizeof(voyeur_event_type));
+}
+
+int voyeur_read_event_type(int fd, voyeur_event_type* val)
+{
+  return do_read(fd, (void*) val, sizeof(voyeur_event_type));
+}
+
+int voyeur_write_byte(int fd, char val)
+{
+  return do_write(fd, (void*) &val, sizeof(char));
+}
+
+int voyeur_read_byte(int fd, char* val)
+{
+  return do_read(fd, (void*) val, sizeof(char));
+}
+
+int voyeur_write_int(int fd, int val)
+{
+  return do_write(fd, (void*) &val, sizeof(int));
+}
+
+int voyeur_read_int(int fd, int* val)
+{
+  return do_read(fd, (void*) val, sizeof(int));
+}
+
+int voyeur_write_size(int fd, size_t val)
+{
+  return do_write(fd, (void*) &val, sizeof(size_t));
+}
+
+int voyeur_read_size(int fd, size_t* val)
+{
+  return do_read(fd, (void*) val, sizeof(size_t));
+}
+
+int voyeur_write_pid(int fd, pid_t val)
+{
+  return do_write(fd, (void*) &val, sizeof(pid_t));
+}
+
+int voyeur_read_pid(int fd, pid_t* val)
+{
+  return do_read(fd, (void*) val, sizeof(pid_t));
+}
+
+int voyeur_write_string(int fd, const char* val, size_t len)
+{
+  if (val == NULL) {
+    len = 0;
+  } else if (len == 0) {
+    len = strnlen(val, VOYEUR_MAX_STRLEN);
+  }
+
+  if (voyeur_write_size(fd, len) < 0) {
+    return -1;
+  }
+  
+  return do_write(fd, (void*) val, len);
+}
+
+int voyeur_read_string(int fd, char** val, size_t maxlen)
+{
+  size_t len;
+  if (voyeur_read_size(fd, &len) < 0) {
+    return -1;
+  }
+
+  if (maxlen == 0) {
+    *val = malloc(len + 1);
+  } else if (maxlen <= len) {
+    SHOULD_NOT_REACH("libvoyeur: string buffer of size %zu "
+                     "is too small for string of length %zu\n",
+                     maxlen,
+                     len);
+    return -1;
+  }
+  
+  if (do_read(fd, (void*) *val, len) < 0) {
+    if (maxlen == 0) {
+      free(*val);
+    }
+    return -1;
+  }
+
+  (*val)[len] = '\0';
+  return 0;
+}
diff --git a/libvoyeur/src/net.h b/libvoyeur/src/net.h
new file mode 100644
--- /dev/null
+++ b/libvoyeur/src/net.h
@@ -0,0 +1,101 @@
+#ifndef VOYEUR_NET_H
+#define VOYEUR_NET_H
+
+#include <stddef.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#include "event.h"
+
+//////////////////////////////////////////////////
+// Sock creation and connection.
+//////////////////////////////////////////////////
+
+// Creates a socket and starts listening on it. The caller must
+// provided an uninitialized sockaddr_un struct. After
+// create_server_socket returns, the socket path will be available
+// in sockinfo->sun_path.
+int voyeur_create_server_socket(struct sockaddr_un* sockinfo);
+
+// Creates a socket and connects to the provided socket path on it.
+int voyeur_create_client_socket(const char* sockpath);
+
+// Closes the provided socket (or pipe) safely.
+void voyeur_close_socket(int fd);
+
+
+//////////////////////////////////////////////////
+// Message serialization.
+//////////////////////////////////////////////////
+
+// Every libvoyeur network message starts with a message type.
+
+typedef enum {
+  VOYEUR_MSG_EVENT,
+  VOYEUR_MSG_DONE
+} voyeur_msg_type;
+
+// Reader and writer for message types.
+int voyeur_write_msg_type(int fd, voyeur_msg_type val);
+int voyeur_read_msg_type(int fd, voyeur_msg_type* val);
+
+//////////////////////////////////////////////////
+// Event serialization.
+//////////////////////////////////////////////////
+
+// All libvoyeur events consist of an event type followed by a
+// sequence of bytes, integers, and strings particular to the event.
+//
+// A typical sequence of calls for a writer:
+//   voyeur_write_msg_type(fd, VOYEUR_MSG_EVENT);
+//   voyeur_write_event_type(fd, VOYEUR_EVENT_XXX);
+//   voyeur_write_string(fd, file, strlen(file));
+//   voyeur_write_int(fd, flags);
+//
+// A matching sequence of calls for a reader:
+//   voyeur_read_msg_type(fd, &msgtype);
+//   /* dispatch to handler for VOYEUR_MSG_EVENT */
+//   voyeur_read_event_type(fd, &type);
+//   /* dispatch to handler for VOYEUR_EVENT_XXX */
+//   voyeur_read_string(fd, &file, &len);
+//   voyeur_read_int(fd, &flags);
+//
+// Every read/write function returns 0 on success and -1 on error.
+
+// Reader and writer for event types.
+int voyeur_write_event_type(int fd, voyeur_event_type val);
+int voyeur_read_event_type(int fd, voyeur_event_type* val);
+
+// Reader and writer for bytes.
+int voyeur_write_byte(int fd, char val);
+int voyeur_read_byte(int fd, char* val);
+
+// Reader and writer for integers.
+int voyeur_write_int(int fd, int val);
+int voyeur_read_int(int fd, int* val);
+
+// Reader and writer for size_t.
+int voyeur_write_size(int fd, size_t val);
+int voyeur_read_size(int fd, size_t* val);
+
+// Reader and writer for pid_t.
+int voyeur_write_pid(int fd, pid_t val);
+int voyeur_read_pid(int fd, pid_t* val);
+
+#define VOYEUR_MAX_STRLEN 4096
+
+// Write a string. If 'len' is 0, the length is determined by calling
+// strnlen(val, VOYEUR_MAX_STRLEN).
+int voyeur_write_string(int fd, const char* val, size_t len);
+
+// Read a string. If 'maxlen' is 0, voyeur_read_string will allocate a
+// buffer large enough to hold the string, which the caller is
+// responsible for freeing. Otherwise, voyeur_read_string will use the
+// buffer provided by the caller.
+//
+// Note that if you use a maximum length when reading, you must make
+// sure to use the same limit when writing. If there isn't enough
+// space in the buffer, voyeur_read_string will report an error.
+int voyeur_read_string(int fd, char** val, size_t maxlen);
+
+#endif
diff --git a/libvoyeur/src/util.c b/libvoyeur/src/util.c
new file mode 100644
--- /dev/null
+++ b/libvoyeur/src/util.c
@@ -0,0 +1,15 @@
+#include <unistd.h>
+
+#include "util.h"
+
+extern void voyeur_log(const char* str);
+
+void voyeur_request_debug(const char* reason)
+{
+  perror(reason);
+  char keep_looping = 1;
+  while (keep_looping) {
+    printf("ATTACH DEBUGGER TO %u\n", (unsigned) getpid());
+    sleep(10);
+  }
+}
diff --git a/libvoyeur/src/util.h b/libvoyeur/src/util.h
new file mode 100644
--- /dev/null
+++ b/libvoyeur/src/util.h
@@ -0,0 +1,80 @@
+#ifndef LIBVOYEUR_UTIL_H
+#define LIBVOYEUR_UTIL_H
+
+#include <stdio.h>
+
+#define RETURN_ON_FAIL(_f, ...)                 \
+  do {                                          \
+    if (_f(__VA_ARGS__) < 0) {                  \
+      perror(#_f);                              \
+      return;                                   \
+    }                                           \
+  } while (0)
+
+#define RETURN_ERROR_ON_FAIL(_f, ...)           \
+  do {                                          \
+    if (_f(__VA_ARGS__) < 0) {                  \
+      perror(#_f);                              \
+      return -1;                                \
+    }                                           \
+  } while (0)
+
+#define ABORT_ON_FAIL(_f, ...)                  \
+  do {                                          \
+    if (_f(__VA_ARGS__) < 0) {                  \
+      perror(#_f);                              \
+      exit(EXIT_FAILURE);                       \
+    }                                           \
+  } while (0)
+
+#ifdef DEBUG
+
+#  define WARN_ON_FAIL(_f, ...)                 \
+  do {                                          \
+    if (_f(__VA_ARGS__) < 0) {                  \
+      perror(#_f);                              \
+    }                                           \
+  } while (0)
+
+#  define WARN_ON_FAIL_VALUE(_var, _err)        \
+  do {                                          \
+    if (_var < 0) {                             \
+      perror(_err);                             \
+      exit(EXIT_FAILURE);                       \
+    }                                           \
+  } while (0)
+
+#  define ASSERT(_check, ...)                   \
+  do {                                          \
+    if (!(_check)) {                             \
+      fprintf(stderr, __VA_ARGS__);             \
+      exit(EXIT_FAILURE);                       \
+    }                                           \
+  } while (0)
+
+#else
+
+#  define WARN_ON_FAIL(...)
+#  define WARN_ON_FAIL_VALUE(...)
+#  define ASSERT(...)
+
+#endif
+
+#define SHOULD_NOT_REACH(...) ASSERT(-1, __VA_ARGS__)
+
+inline void voyeur_log(const char* str)
+{
+#ifdef DEBUG
+  fprintf(stderr, "%s", str);
+  fflush(stderr);
+#else
+  // Do nothing.
+#endif
+}
+
+// Enters an infinite loop and requests the user to attach a
+// debugger. For debugging only.
+void voyeur_request_debug(const char* reason);
+
+
+#endif
diff --git a/libvoyeur/src/voyeur-close.c b/libvoyeur/src/voyeur-close.c
new file mode 100644
--- /dev/null
+++ b/libvoyeur/src/voyeur-close.c
@@ -0,0 +1,80 @@
+#ifndef __APPLE__
+#define _GNU_SOURCE
+#endif
+
+#include <fcntl.h>
+#include <pthread.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "dyld.h"
+#include "env.h"
+#include "net.h"
+
+typedef int (*close_fptr_t)(int);
+VOYEUR_STATIC_DECLARE_NEXT(close_fptr_t, close)
+
+static pthread_mutex_t voyeur_close_mutex = PTHREAD_MUTEX_INITIALIZER;
+static char voyeur_close_initialized = 0;
+static char voyeur_close_finalized = 0;
+static uint8_t voyeur_close_opts = 0;
+static int voyeur_close_sock = 0;
+
+__attribute__((destructor)) void voyeur_cleanup_close()
+{
+  pthread_mutex_lock(&voyeur_close_mutex);
+
+  voyeur_close_finalized = 1;
+
+  if (voyeur_close_initialized) {
+    if (voyeur_close_sock >= 0) {
+      voyeur_write_msg_type(voyeur_close_sock, VOYEUR_MSG_DONE);
+      voyeur_close_socket(voyeur_close_sock);
+      voyeur_close_sock = -1;
+    }
+    
+    voyeur_close_initialized = 0;
+  }
+
+  pthread_mutex_unlock(&voyeur_close_mutex);
+}
+
+int VOYEUR_FUNC(close)(int fildes)
+{
+  if (voyeur_close_finalized) {
+    // We're in the process of shutting down, so just forward to the real close.
+    return VOYEUR_CALL_NEXT(close, fildes);
+  }
+
+  pthread_mutex_lock(&voyeur_close_mutex);
+
+  if (!voyeur_close_initialized) {
+    const char* voyeur_close_sockpath = getenv("LIBVOYEUR_SOCKET");
+    voyeur_close_opts = voyeur_decode_options(getenv("LIBVOYEUR_OPTS"),
+                                              VOYEUR_EVENT_CLOSE);
+    voyeur_close_sock = voyeur_create_client_socket(voyeur_close_sockpath);
+    VOYEUR_LOOKUP_NEXT(close_fptr_t, close);
+    voyeur_close_initialized = 1;
+  }
+
+  // Pass through the call to the real close.
+  int retval = VOYEUR_CALL_NEXT(close, fildes);
+
+  // Write the event to the socket.
+  if (voyeur_close_sock >= 0) {
+    voyeur_write_msg_type(voyeur_close_sock, VOYEUR_MSG_EVENT);
+    voyeur_write_event_type(voyeur_close_sock, VOYEUR_EVENT_CLOSE);
+    voyeur_write_int(voyeur_close_sock, fildes);
+    voyeur_write_int(voyeur_close_sock, retval);
+    voyeur_write_pid(voyeur_close_sock, getpid());
+  }
+
+  pthread_mutex_unlock(&voyeur_close_mutex);
+
+  return retval;
+}
+
+VOYEUR_INTERPOSE(close)
diff --git a/libvoyeur/src/voyeur-exec.c b/libvoyeur/src/voyeur-exec.c
new file mode 100644
--- /dev/null
+++ b/libvoyeur/src/voyeur-exec.c
@@ -0,0 +1,456 @@
+#ifndef __APPLE__
+#define _GNU_SOURCE
+#endif
+
+#include <pthread.h>
+#include <spawn.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "dyld.h"
+#include "env.h"
+#include "net.h"
+#include "util.h"
+
+
+//////////////////////////////////////////////////
+// Shared code for all exec*() functions.
+//////////////////////////////////////////////////
+
+static void write_exec_event(int sock, uint8_t options, const char* path,
+                             char* const argv[], char* const envp[],
+                             pid_t pid, pid_t ppid)
+{
+  if (options & OBSERVE_EXEC_SILENT) {
+    // We're just here to propagate libvoyeur instrumentation.
+    return;
+  }
+
+  if (!(options & OBSERVE_EXEC_NOACCESS)) {
+    // Make sure this exec() call could succeed before reporting the event.
+    if (access(path, X_OK) < 0) {
+      return;
+    }
+  }
+
+  voyeur_write_msg_type(sock, VOYEUR_MSG_EVENT);
+  voyeur_write_event_type(sock, VOYEUR_EVENT_EXEC);
+  voyeur_write_string(sock, path, 0);
+
+  int argc = 0;
+  while (argv[argc]) {
+    ++argc;
+  }
+  voyeur_write_int(sock, argc);
+  for (int i = 0 ; i < argc ; ++i) {
+    voyeur_write_string(sock, argv[i], 0);
+  }
+
+  if (options & OBSERVE_EXEC_ENV) {
+    int envc = 0;
+    while (envp[envc]) {
+      ++envc;
+    }
+    voyeur_write_int(sock, envc);
+    for (int i = 0 ; i < envc ; ++i) {
+      voyeur_write_string(sock, envp[i], 0);
+    }
+  }
+
+  if (options & OBSERVE_EXEC_PATH) {
+    voyeur_write_string(sock, getenv("PATH"), 0);
+  }
+
+  if (options & OBSERVE_EXEC_CWD) {
+    voyeur_write_string(sock, getcwd(NULL, 0), 0);
+  }
+
+  voyeur_write_pid(sock, pid);
+  voyeur_write_pid(sock, ppid);
+}
+
+
+//////////////////////////////////////////////////
+// Replace vfork() with fork()
+//////////////////////////////////////////////////
+
+typedef pid_t (*vfork_fptr_t)();
+
+pid_t VOYEUR_FUNC(vfork)(void)
+{
+    return fork();
+}
+
+VOYEUR_INTERPOSE(vfork)
+
+
+//////////////////////////////////////////////////
+// execve
+//////////////////////////////////////////////////
+
+typedef int (*execve_fptr_t)(const char*, char* const[], char* const []);
+
+int VOYEUR_FUNC(execve)(const char* path, char* const argv[], char* const envp[])
+{
+  // In the case of exec we don't bother caching anything, since exec
+  // will wipe out this whole process image anyway.
+  const char* libs = getenv("LIBVOYEUR_LIBS");
+  const char* opts = getenv("LIBVOYEUR_OPTS");
+  uint8_t options = voyeur_decode_options(opts, VOYEUR_EVENT_EXEC);
+  const char* sockpath = getenv("LIBVOYEUR_SOCKET");
+
+  // Write the event to the socket.
+  int sock = voyeur_create_client_socket(sockpath);
+  if (sock >= 0) {
+    write_exec_event(sock, options, path, argv, envp, getpid(), getppid());
+
+    // We might as well close the socket since there's no chance we'll
+    // ever be called a second time by the same process. (Even if the
+    // exec fails, generally the fork'd process will just bail.)
+    voyeur_write_msg_type(sock, VOYEUR_MSG_DONE);
+    voyeur_close_socket(sock);
+  }
+
+  // Add libvoyeur-specific environment variables. (We don't bother
+  // freeing 'buf' since we need it until the execve call and we have
+  // no way of freeing it after that.)
+  void* buf;
+  char** voyeur_envp =
+    voyeur_augment_environment(envp, libs, opts, sockpath, &buf);
+
+  // Pass through the call to the real execve.
+  VOYEUR_DECLARE_NEXT(execve_fptr_t, execve);
+  VOYEUR_LOOKUP_NEXT(execve_fptr_t, execve);
+  return VOYEUR_CALL_NEXT(execve, path, argv, voyeur_envp);
+}
+
+VOYEUR_INTERPOSE(execve)
+
+
+//////////////////////////////////////////////////
+// posix_spawn
+//////////////////////////////////////////////////
+
+typedef int (*posix_spawn_fptr_t)(pid_t* restrict,
+                                  const char* restrict,
+                                  const posix_spawn_file_actions_t*,
+                                  const posix_spawnattr_t* restrict,
+                                  char* const[restrict],
+                                  char* const[restrict]);
+
+static pthread_mutex_t voyeur_posix_spawn_mutex = PTHREAD_MUTEX_INITIALIZER;
+static char voyeur_posix_spawn_initialized = 0;
+static char* voyeur_posix_spawn_libs = NULL;
+static char* voyeur_posix_spawn_opts = NULL;
+static uint8_t voyeur_posix_spawn_options = 0;
+static char* voyeur_posix_spawn_sockpath = NULL;
+static int voyeur_posix_spawn_sock = 0;
+VOYEUR_STATIC_DECLARE_NEXT(posix_spawn_fptr_t, posix_spawn);
+VOYEUR_STATIC_DECLARE_NEXT(posix_spawn_fptr_t, posix_spawnp);
+
+__attribute__((destructor)) void voyeur_cleanup_posix_spawn()
+{
+  pthread_mutex_lock(&voyeur_posix_spawn_mutex);
+
+  if (voyeur_posix_spawn_initialized) {
+    if (voyeur_posix_spawn_sock >= 0) {
+      voyeur_write_msg_type(voyeur_posix_spawn_sock, VOYEUR_MSG_DONE);
+      voyeur_close_socket(voyeur_posix_spawn_sock);
+      voyeur_posix_spawn_sock = -1;
+    }
+
+    voyeur_posix_spawn_initialized = 0;
+  }
+
+  pthread_mutex_unlock(&voyeur_posix_spawn_mutex);
+}
+
+static void voyeur_init_posix_spawn()
+{
+  if (!voyeur_posix_spawn_initialized) {
+    voyeur_posix_spawn_libs = getenv("LIBVOYEUR_LIBS");
+    voyeur_posix_spawn_opts = getenv("LIBVOYEUR_OPTS");
+    voyeur_posix_spawn_options =
+      voyeur_decode_options(voyeur_posix_spawn_opts, VOYEUR_EVENT_EXEC);
+    voyeur_posix_spawn_sockpath = getenv("LIBVOYEUR_SOCKET");
+    voyeur_posix_spawn_sock =
+      voyeur_create_client_socket(voyeur_posix_spawn_sockpath);
+    VOYEUR_LOOKUP_NEXT(posix_spawn_fptr_t, posix_spawn);
+    VOYEUR_LOOKUP_NEXT(posix_spawn_fptr_t, posix_spawnp);
+    voyeur_posix_spawn_initialized = 1;
+  }
+}
+
+int VOYEUR_FUNC(posix_spawn)(pid_t* pid,
+                             const char* restrict path,
+                             const posix_spawn_file_actions_t* file_actions,
+                             const posix_spawnattr_t* restrict attrp,
+                             char* const argv[restrict],
+                             char* const envp[restrict])
+{
+  pthread_mutex_lock(&voyeur_posix_spawn_mutex);
+
+  voyeur_init_posix_spawn();
+
+  // Add libvoyeur-specific environment variables.
+  void* buf;
+  char** voyeur_envp =
+    voyeur_augment_environment(envp,
+                               voyeur_posix_spawn_libs,
+                               voyeur_posix_spawn_opts,
+                               voyeur_posix_spawn_sockpath,
+                               &buf);
+
+  // Pass through the call to the real posix_spawn.
+  pid_t child_pid;
+  int retval = VOYEUR_CALL_NEXT(posix_spawn, &child_pid, path,
+                                file_actions, attrp,
+                                argv, voyeur_envp);
+
+  // Write the event to the socket.
+  if (voyeur_posix_spawn_sock >= 0) {
+    write_exec_event(voyeur_posix_spawn_sock,
+                     voyeur_posix_spawn_options,
+                     path, argv, envp,
+                     child_pid, getpid());
+  }
+
+  pthread_mutex_unlock(&voyeur_posix_spawn_mutex);
+
+  // Free the resources we allocated.
+  free(voyeur_envp);
+  free(buf);
+
+  // It's legal to pass NULL for the pid argument, so double-check we
+  // have somewhere to write the pid to before doing it.
+  if (pid) {
+    *pid = child_pid;
+  }
+
+  return retval;
+}
+
+VOYEUR_INTERPOSE(posix_spawn)
+
+#ifdef __linux__
+
+// On Linux we need to interpose on every exec variant separately because they
+// don't just forward the work to execve and posix_spawn like OS X's versions do.
+
+//////////////////////////////////////////////////
+// execl, execle, execv
+//////////////////////////////////////////////////
+
+#define VARARGS_TO_ARGV(_start, _path, _argv, _envp)     \
+  do {                                                   \
+    va_list args;                                        \
+                                                         \
+    /* Get total number of entries. */                   \
+    unsigned length = 0;                                 \
+    va_start(args, (_start));                            \
+    while (va_arg(args, char*)) {                        \
+      ++length;                                          \
+    }                                                    \
+    va_end(args);                                        \
+                                                         \
+    /* Increase size to fit argv[0] and final NULL. */   \
+    length += 2;                                         \
+                                                         \
+    /* Create an appropriately sized _argv. */           \
+    _argv = malloc(sizeof(const char*) * length);        \
+    _argv[0] = (char *) _path;                           \
+                                                         \
+    /* Copy. */                                          \
+    unsigned index = 1;                                  \
+    va_start(args, (_start));                            \
+    while (index < length) {                             \
+      _argv[index] = va_arg(args, char*);                \
+      ++index;                                           \
+    }                                                    \
+                                                         \
+    /* Pull out envp from one past the end of argv. */   \
+    _envp = va_arg(args, char**);                        \
+                                                         \
+    va_end(args);                                        \
+  } while(0)
+
+
+int VOYEUR_FUNC(execl)(const char* path, const char* start, ...)
+{
+  char** argv;
+  char** dummy_envp;
+  VARARGS_TO_ARGV(start, path, argv, dummy_envp);
+
+  return execve(path, argv, environ);
+}
+
+VOYEUR_INTERPOSE(execl)
+
+
+int VOYEUR_FUNC(execle)(const char* path, const char* start, ...)
+{
+  char** argv;
+  char** envp;
+  VARARGS_TO_ARGV(start, path, argv, envp);
+
+  return execve(path, argv, envp);
+}
+
+VOYEUR_INTERPOSE(execle)
+
+
+int VOYEUR_FUNC(execv)(const char* path, char* const argv[])
+{
+  return execve(path, argv, environ);
+}
+
+VOYEUR_INTERPOSE(execv)
+
+
+//////////////////////////////////////////////////
+// execlp, execvp, execvpe
+//////////////////////////////////////////////////
+
+typedef int (*execvpe_fptr_t)(const char*, char* const [], char* const []);
+
+int VOYEUR_FUNC(execlp)(const char* path, const char* start, ...)
+{
+  const char* libs = getenv("LIBVOYEUR_LIBS");
+  const char* opts = getenv("LIBVOYEUR_OPTS");
+  uint8_t options = voyeur_decode_options(opts, VOYEUR_EVENT_EXEC);
+  const char* sockpath = getenv("LIBVOYEUR_SOCKET");
+
+  char** argv;
+  char** dummy_envp;
+  VARARGS_TO_ARGV(start, path, argv, dummy_envp);
+  char** envp = environ;
+
+  int sock = voyeur_create_client_socket(sockpath);
+  if (sock >= 0) {
+    write_exec_event(sock, options, path, argv, envp, getpid(), getppid());
+    voyeur_write_msg_type(sock, VOYEUR_MSG_DONE);
+    voyeur_close_socket(sock);
+  }
+
+  void* buf;
+  char** voyeur_envp =
+    voyeur_augment_environment(envp, libs, opts, sockpath, &buf);
+
+  // Need to pass through to execvpe since we need to provide an environment.
+  VOYEUR_DECLARE_NEXT(execvpe_fptr_t, execvpe);
+  VOYEUR_LOOKUP_NEXT(execvpe_fptr_t, execvpe);
+  return VOYEUR_CALL_NEXT(execvpe, path, argv, voyeur_envp);
+}
+
+VOYEUR_INTERPOSE(execlp)
+
+
+int VOYEUR_FUNC(execvp)(const char* path, char* const argv[])
+{
+  const char* libs = getenv("LIBVOYEUR_LIBS");
+  const char* opts = getenv("LIBVOYEUR_OPTS");
+  uint8_t options = voyeur_decode_options(opts, VOYEUR_EVENT_EXEC);
+  const char* sockpath = getenv("LIBVOYEUR_SOCKET");
+
+  char** envp = environ;
+
+  int sock = voyeur_create_client_socket(sockpath);
+  if (sock >= 0) {
+    write_exec_event(sock, options, path, argv, envp, getpid(), getppid());
+    voyeur_write_msg_type(sock, VOYEUR_MSG_DONE);
+    voyeur_close_socket(sock);
+  }
+
+  void* buf;
+  char** voyeur_envp =
+    voyeur_augment_environment(envp, libs, opts, sockpath, &buf);
+
+  // Need to pass through to execvpe since we need to provide an environment.
+  VOYEUR_DECLARE_NEXT(execvpe_fptr_t, execvpe);
+  VOYEUR_LOOKUP_NEXT(execvpe_fptr_t, execvpe);
+  return VOYEUR_CALL_NEXT(execvpe, path, argv, voyeur_envp);
+}
+
+VOYEUR_INTERPOSE(execvp)
+
+
+int VOYEUR_FUNC(execvpe)(const char* path, char* const argv[], char* const envp[])
+{
+  const char* libs = getenv("LIBVOYEUR_LIBS");
+  const char* opts = getenv("LIBVOYEUR_OPTS");
+  uint8_t options = voyeur_decode_options(opts, VOYEUR_EVENT_EXEC);
+  const char* sockpath = getenv("LIBVOYEUR_SOCKET");
+
+  int sock = voyeur_create_client_socket(sockpath);
+  if (sock >= 0) {
+    write_exec_event(sock, options, path, argv, envp, getpid(), getppid());
+    voyeur_write_msg_type(sock, VOYEUR_MSG_DONE);
+    voyeur_close_socket(sock);
+  }
+
+  void* buf;
+  char** voyeur_envp =
+    voyeur_augment_environment(envp, libs, opts, sockpath, &buf);
+
+  // Pass through the call to the real execvpe.
+  VOYEUR_DECLARE_NEXT(execvpe_fptr_t, execvpe);
+  VOYEUR_LOOKUP_NEXT(execvpe_fptr_t, execvpe);
+  return VOYEUR_CALL_NEXT(execvpe, path, argv, voyeur_envp);
+}
+
+VOYEUR_INTERPOSE(execvpe)
+
+
+//////////////////////////////////////////////////
+// posix_spawnp
+//////////////////////////////////////////////////
+
+int VOYEUR_FUNC(posix_spawnp)(pid_t* pid,
+                              const char* restrict path,
+                              const posix_spawn_file_actions_t* file_actions,
+                              const posix_spawnattr_t* restrict attrp,
+                              char* const argv[restrict],
+                              char* const envp[restrict])
+{
+  pthread_mutex_lock(&voyeur_posix_spawn_mutex);
+
+  voyeur_init_posix_spawn();
+
+  void* buf;
+  char** voyeur_envp =
+    voyeur_augment_environment(envp,
+                               voyeur_posix_spawn_libs,
+                               voyeur_posix_spawn_opts,
+                               voyeur_posix_spawn_sockpath,
+                               &buf);
+
+  // Pass through the call to the real posix_spawnp.
+  pid_t child_pid;
+  int retval = VOYEUR_CALL_NEXT(posix_spawnp, &child_pid, path,
+                                file_actions, attrp,
+                                argv, voyeur_envp);
+
+  if (voyeur_posix_spawn_sock >= 0) {
+    write_exec_event(voyeur_posix_spawn_sock,
+                     voyeur_posix_spawn_options,
+                     path, argv, envp,
+                     child_pid, getpid());
+  }
+
+  pthread_mutex_unlock(&voyeur_posix_spawn_mutex);
+
+  free(voyeur_envp);
+  free(buf);
+
+  if (pid) {
+    *pid = child_pid;
+  }
+
+  return retval;
+}
+
+VOYEUR_INTERPOSE(posix_spawnp)
+
+#endif
diff --git a/libvoyeur/src/voyeur-exit.c b/libvoyeur/src/voyeur-exit.c
new file mode 100644
--- /dev/null
+++ b/libvoyeur/src/voyeur-exit.c
@@ -0,0 +1,113 @@
+#ifndef __APPLE__
+#define _GNU_SOURCE
+#endif
+
+#include <stdio.h>
+#include <unistd.h>
+
+#include "dyld.h"
+#include "env.h"
+#include "net.h"
+
+static char did_exit_already = 0;
+
+//////////////////////////////////////////////////
+// Shared code for all exit*() functions.
+//////////////////////////////////////////////////
+
+static void write_exit_event(int status)
+{
+  if (!did_exit_already) {
+    did_exit_already = 1;
+
+    // In the case of exit we don't bother caching anything; we are about to exit,
+    // after all!
+    const char* sockpath = getenv("LIBVOYEUR_SOCKET");
+    int sock = voyeur_create_client_socket(sockpath);
+    if (sock >= 0) {
+      voyeur_write_msg_type(sock, VOYEUR_MSG_EVENT);
+      voyeur_write_event_type(sock, VOYEUR_EVENT_EXIT);
+      voyeur_write_int(sock, status);
+      voyeur_write_pid(sock, getpid());
+      voyeur_write_pid(sock, getppid());
+
+      // We might as well close the socket since there's no chance we'll
+      // ever be called a second time by the same process.
+      voyeur_write_msg_type(sock, VOYEUR_MSG_DONE);
+      voyeur_close_socket(sock);
+    }
+  }
+}
+
+
+//////////////////////////////////////////////////
+// exit*() variants.
+//////////////////////////////////////////////////
+
+// There are a lot of variants here because none of them seem to be perfectly
+// reliable, especially on Linux. We are still likely to miss the case where a
+// process gets signaled, unfortunately.
+
+typedef void (*exit_fptr_t)(int);
+
+void VOYEUR_FUNC(exit)(int status)
+{
+  write_exit_event(status);
+
+  // Pass through the call to the real exit.
+  VOYEUR_DECLARE_NEXT(exit_fptr_t, exit);
+  VOYEUR_LOOKUP_NEXT(exit_fptr_t, exit);
+  return VOYEUR_CALL_NEXT(exit, status);
+}
+
+VOYEUR_INTERPOSE(exit)
+
+void VOYEUR_FUNC(_exit)(int status)
+{
+  write_exit_event(status);
+
+  // Pass through the call to the real _exit.
+  VOYEUR_DECLARE_NEXT(exit_fptr_t, _exit);
+  VOYEUR_LOOKUP_NEXT(exit_fptr_t, _exit);
+  return VOYEUR_CALL_NEXT(_exit, status);
+}
+
+VOYEUR_INTERPOSE(_exit)
+
+void VOYEUR_FUNC(_Exit)(int status)
+{
+  write_exit_event(status);
+
+  // Pass through the call to the real _Exit.
+  VOYEUR_DECLARE_NEXT(exit_fptr_t, _Exit);
+  VOYEUR_LOOKUP_NEXT(exit_fptr_t, _Exit);
+  return VOYEUR_CALL_NEXT(_Exit, status);
+}
+
+VOYEUR_INTERPOSE(_Exit)
+
+#ifdef __linux__
+
+void VOYEUR_FUNC(exit_group)(int status)
+{
+  write_exit_event(status);
+
+  // Pass through the call to the real exit_group.
+  VOYEUR_DECLARE_NEXT(exit_fptr_t, exit_group);
+  VOYEUR_LOOKUP_NEXT(exit_fptr_t, exit_group);
+  return VOYEUR_CALL_NEXT(exit_group, status);
+}
+
+VOYEUR_INTERPOSE(exit_group)
+
+static void voyeur_exit_handler(int status, void* unused)
+{
+  write_exit_event(status);
+}
+
+__attribute__((constructor)) void voyeur_init_exit_handler()
+{
+  on_exit(voyeur_exit_handler, 0);
+}
+
+#endif
diff --git a/libvoyeur/src/voyeur-open.c b/libvoyeur/src/voyeur-open.c
new file mode 100644
--- /dev/null
+++ b/libvoyeur/src/voyeur-open.c
@@ -0,0 +1,103 @@
+#ifndef __APPLE__
+#define _GNU_SOURCE
+#endif
+
+#include <fcntl.h>
+#include <pthread.h>
+#include <stdarg.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "dyld.h"
+#include "env.h"
+#include "net.h"
+
+typedef int (*open_fptr_t)(const char*, int, ...);
+VOYEUR_STATIC_DECLARE_NEXT(open_fptr_t, open)
+
+static pthread_mutex_t voyeur_open_mutex = PTHREAD_MUTEX_INITIALIZER;
+static char voyeur_open_initialized = 0;
+static uint8_t voyeur_open_opts = 0;
+static int voyeur_open_sock = 0;
+
+__attribute__((destructor)) void voyeur_cleanup_open()
+{
+  pthread_mutex_lock(&voyeur_open_mutex);
+
+  if (voyeur_open_initialized) {
+    if (voyeur_open_sock >= 0) {
+      voyeur_write_msg_type(voyeur_open_sock, VOYEUR_MSG_DONE);
+      voyeur_close_socket(voyeur_open_sock);
+      voyeur_open_sock = -1;
+    }
+
+    voyeur_open_initialized = 0;
+  }
+
+  pthread_mutex_unlock(&voyeur_open_mutex);
+}
+
+int VOYEUR_FUNC(open)(const char* path, int oflag, ...)
+{
+  pthread_mutex_lock(&voyeur_open_mutex);
+  
+  if (!voyeur_open_initialized) {
+    const char* voyeur_open_sockpath = getenv("LIBVOYEUR_SOCKET");
+    voyeur_open_opts = voyeur_decode_options(getenv("LIBVOYEUR_OPTS"),
+                                             VOYEUR_EVENT_OPEN);
+    voyeur_open_sock = voyeur_create_client_socket(voyeur_open_sockpath);
+    VOYEUR_LOOKUP_NEXT(open_fptr_t, open);
+    voyeur_open_initialized = 1;
+  }
+
+  // Extract the mode argument if necessary.
+  mode_t mode;
+  if (oflag & O_CREAT) {
+    va_list args;
+    va_start(args, oflag);
+    // Note that 'int' is used instead of 'mode_t' due to warnings
+    // that mode_t, being in reality a short on OS X, is of promotable
+    // type, which is verboten for va_arg.
+    mode = va_arg(args, int);
+    va_end(args);
+  }
+  
+  // Pass through the call to the real open.
+  int retval;
+  if (oflag & O_CREAT) {
+    retval = VOYEUR_CALL_NEXT(open, path, oflag, mode);
+  } else {
+    retval = VOYEUR_CALL_NEXT(open, path, oflag);
+  }
+
+  // Write the event to the socket.
+  if (voyeur_open_sock >= 0) {
+    voyeur_write_msg_type(voyeur_open_sock, VOYEUR_MSG_EVENT);
+    voyeur_write_event_type(voyeur_open_sock, VOYEUR_EVENT_OPEN);
+    voyeur_write_string(voyeur_open_sock, path, 0);
+    voyeur_write_int(voyeur_open_sock, oflag);
+
+    if (oflag & O_CREAT) {
+      voyeur_write_int(voyeur_open_sock, (int) mode);
+    } else {
+      voyeur_write_int(voyeur_open_sock, 0);
+    }
+
+    voyeur_write_int(voyeur_open_sock, retval);
+
+    if (voyeur_open_opts & OBSERVE_OPEN_CWD) {
+      char* cwd = getcwd(NULL, 0);
+      voyeur_write_string(voyeur_open_sock, cwd, 0);
+      free(cwd);
+    }
+
+    voyeur_write_pid(voyeur_open_sock, getpid());
+  }
+
+  pthread_mutex_unlock(&voyeur_open_mutex);
+
+  return retval;
+}
+
+VOYEUR_INTERPOSE(open)
diff --git a/libvoyeur/src/voyeur.c b/libvoyeur/src/voyeur.c
new file mode 100644
--- /dev/null
+++ b/libvoyeur/src/voyeur.c
@@ -0,0 +1,270 @@
+#ifdef __linux__
+#define _GNU_SOURCE
+#endif
+
+#include <errno.h>
+#include <pthread.h>
+#include <spawn.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <sys/un.h>
+#include <unistd.h>
+
+#ifdef __linux__
+#include <bsd/bsd.h>
+#endif
+
+#include <voyeur.h>
+#include "env.h"
+#include "event.h"
+#include "net.h"
+#include "util.h"
+
+typedef struct {
+  struct sockaddr_un sockinfo;
+  int server_sock;
+  void* env_buf;
+} server_state;
+
+voyeur_context_t voyeur_context_create()
+{
+  voyeur_context* ctx = calloc(1, sizeof(voyeur_context));
+  return (voyeur_context_t) ctx;
+}
+
+void voyeur_context_destroy(voyeur_context_t ctx)
+{
+  voyeur_context* context = (voyeur_context*) ctx;
+
+  if (context->resource_path) {
+    free(context->resource_path);
+  }
+
+  if (context->server_state) {
+    server_state* state = (server_state*) context->server_state;
+    free(state->env_buf);
+    free(state);
+  }
+  
+  free(context);
+}
+
+void voyeur_set_resource_path(voyeur_context_t ctx,
+                              const char* path)
+{
+  voyeur_context* context = (voyeur_context*) ctx;
+  context->resource_path = calloc(1, strnlen(path, 4096));
+  strlcat(context->resource_path, path, 4096);
+}
+
+typedef struct {
+  pid_t child_pid;
+  int child_pipe_input;
+} waitpid_thread_arg;
+
+static void* waitpid_thread(void* arg_ptr)
+{
+  waitpid_thread_arg* arg = (waitpid_thread_arg*) arg_ptr;
+  
+  int status;
+  waitpid(arg->child_pid, &status, 0);
+
+  voyeur_write_int(arg->child_pipe_input, status);
+
+  voyeur_close_socket(arg->child_pipe_input);
+  free(arg);
+
+  return NULL;
+}
+
+static int start_waitpid_thread(pid_t child_pid)
+{
+  // Create a pipe that will be used to announce the termination of
+  // the child process.
+  int waitpid_pipe[2];
+  ABORT_ON_FAIL(pipe, waitpid_pipe);
+
+  waitpid_thread_arg* arg = malloc(sizeof(waitpid_thread_arg));
+  arg->child_pid = child_pid;
+  arg->child_pipe_input = waitpid_pipe[1];
+  
+  pthread_t thread;
+  pthread_create(&thread, NULL, waitpid_thread, (void*) arg);
+  pthread_detach(thread);
+
+  return waitpid_pipe[0];
+}
+
+static int accept_connection(int server_sock)
+{
+  struct sockaddr_un client_info;
+  socklen_t client_info_len = sizeof(struct sockaddr_un);
+
+  int client_sock =
+    accept(server_sock, (struct sockaddr *) &client_info, &client_info_len);
+  WARN_ON_FAIL_VALUE(client_sock, "accept");
+  
+  return client_sock;
+}
+
+static int handle_message(voyeur_context* context, int sock)
+{
+  voyeur_msg_type msgtype;
+  if (voyeur_read_msg_type(sock, &msgtype) < 0) {
+    return -1;
+  }
+
+  if (msgtype == VOYEUR_MSG_DONE) {
+    // The client is done sending messages on this socket.
+    return -1;
+  } else if (msgtype == VOYEUR_MSG_EVENT) {
+    voyeur_event_type type;
+    if (voyeur_read_event_type(sock, &type) < 0) {
+      return -1;
+    }
+
+    // Got a voyeur event; dispatch to the appropriate handler.
+    voyeur_handle_event(context, type, sock);
+    return 0;
+  } else {
+    // Got an unknown message type.
+    voyeur_log("Unknown message type\n");
+    return -1;
+  }
+}
+
+static int run_server(voyeur_context* context,
+                      int server_sock,
+                      int child_pipe_output)
+{
+  fd_set active_fd_set, read_fd_set, error_fd_set;
+  FD_ZERO(&active_fd_set);
+  FD_SET(server_sock, &active_fd_set);
+  FD_SET(child_pipe_output, &active_fd_set);
+  
+  int child_exited = 0;
+  int child_status = 0;
+  
+  while (!child_exited) {
+    // Block until input arrives.
+    read_fd_set = active_fd_set;
+    error_fd_set = active_fd_set;
+    if (select(FD_SETSIZE, &read_fd_set, NULL, &error_fd_set, NULL) < 0) {
+      if (errno == EAGAIN || errno == EINTR) {
+        continue;  // This is a temporary error.
+      } else {
+        perror("select");
+        break;     // This is unrecoverable.
+      }
+    }
+
+    for (int fd = 0 ; fd < FD_SETSIZE ; ++fd) {
+      if (FD_ISSET(fd, &error_fd_set)) {
+        voyeur_log("Closed file descriptor due to error\n");
+        voyeur_close_socket(fd);
+        FD_CLR(fd, &active_fd_set);
+      } else if (FD_ISSET(fd, &read_fd_set)) {
+        if (fd == server_sock) {
+          int client_sock = accept_connection(server_sock);
+          if (client_sock >= 0) {
+            FD_SET(client_sock, &active_fd_set);
+          }
+        } else if (fd == child_pipe_output) {
+          child_exited = 1;
+          voyeur_read_int(fd, &child_status);
+          voyeur_close_socket(fd);
+          FD_CLR(fd, &active_fd_set);
+        } else if (handle_message(context, fd) < 0) {
+          voyeur_close_socket(fd);
+          FD_CLR(fd, &active_fd_set);
+        }
+      }
+    }
+  }
+
+  voyeur_close_socket(server_sock);
+
+  // Clean up any stragglers.
+  for (int fd = 0 ; fd < FD_SETSIZE ; ++fd) {
+    if (FD_ISSET(fd, &active_fd_set)) {
+      voyeur_close_socket(fd);
+    }
+  }
+  
+  if (WIFEXITED(child_status)) {
+    return WEXITSTATUS(child_status);
+  } else {
+    fprintf(stderr, "libvoyeur: child process did not terminate normally\n");
+    return -1;
+  }
+}
+
+char** voyeur_prepare(voyeur_context_t ctx, char* const envp[])
+{
+  voyeur_context* context = (voyeur_context*) ctx;
+  server_state* state = calloc(1, sizeof(server_state));
+  context->server_state = (void*) state;
+
+  // Prepare the server. We need to do this in advance both to avoid
+  // racing and so that we can include the socket path in the
+  // environment variables.
+  state->server_sock = voyeur_create_server_socket(&state->sockinfo);
+  if (state->server_sock < 0) {
+    return NULL;
+  }
+  
+  // Add libvoyeur-specific environment variables.
+  char* libs = voyeur_requested_libs(context);
+  char* opts = voyeur_requested_opts(context);
+  char** voyeur_envp = voyeur_augment_environment(envp, libs, opts,
+                                                  state->sockinfo.sun_path,
+                                                  &state->env_buf);
+
+  return voyeur_envp;
+}
+
+int voyeur_start(voyeur_context_t ctx, pid_t child_pid)
+{
+  voyeur_context* context = (voyeur_context*) ctx;
+  server_state* state = (server_state*) context->server_state;
+
+  // Run the server.
+  int child_pipe_output = start_waitpid_thread(child_pid);
+  int res = run_server(context,
+                       state->server_sock,
+                       child_pipe_output);
+
+  // Clean up the socket file.
+  WARN_ON_FAIL(unlink, state->sockinfo.sun_path);
+  char* last_slash = strrchr(state->sockinfo.sun_path, '/');
+  if (last_slash) {
+    *last_slash = '\0';
+  }
+  WARN_ON_FAIL(rmdir, state->sockinfo.sun_path);
+
+  return res;
+}
+
+int voyeur_exec(voyeur_context_t ctx,
+                const char* path,
+                char* const argv[],
+                char* const envp[])
+{
+  char** voyeur_envp = voyeur_prepare(ctx, envp);
+  if (!voyeur_envp) {
+    return -1;
+  }
+  
+  pid_t child_pid;
+  if (posix_spawnp(&child_pid, path, NULL, NULL, argv, voyeur_envp) != 0) {
+    free(voyeur_envp);
+    return -1;
+  }
+  
+  int retval = voyeur_start(ctx, child_pid);
+  free(voyeur_envp);
+  return retval;
+}
diff --git a/src/System/Process/Voyeur.hs b/src/System/Process/Voyeur.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Process/Voyeur.hs
@@ -0,0 +1,141 @@
+{-# LANGUAGE TypeSynonymInstances #-}
+
+-- | This package provides bindings to libvoyeur, a library for
+-- observing the private activity of processes. Libvoyeur observes
+-- a child process and all of its descendants, so it works even
+-- when the child process calls out to other processes to do the
+-- actual work.
+--
+-- To observe a process, use 'withVoyeur' to create an
+-- 'FFI.VoyeurContext', then register handlers for the events you want
+-- to observe using functions like 'FFI.observeExec'. When you've
+-- set up all your handlers, use 'FFI.prepareEnvironment' to create a
+-- special environment that will inject libvoyeur code into the
+-- child process, and pass that environment to a function like
+-- 'System.Process.runProcess'. Finally, pass the resulting 'ProcessHandle'
+-- or 'ProcessID' to 'startObserving', and your handlers will be
+-- called as events happen.
+--
+-- A simple function that prints a message every time a child
+-- process opened a file might look like this:
+--
+-- > withVoyeur $ \ctx -> do
+-- >   -- Set up a handler.
+-- >   observeOpen ctx defaultOpenFlags $
+-- >     \path _ _ _ _ pid -> putStrLn $ show pid ++ " opened " ++ show path
+-- >
+-- >   -- Set up the environment.
+-- >   curEnv <- getEnvironment
+-- >   newEnv <- prepareEnvironment ctx
+-- >   
+-- >   when (isJust newEnv) $ do
+-- >     -- Start the child process.
+-- >     handle <- runProcess program args Nothing newEnv Nothing Nothing Nothing
+-- >
+-- >     -- Observe it! startObserving only returns when the child process
+-- >     -- exits, so we don't need to wait.
+-- >     void $ startObserving ctx handle
+--
+-- A larger example program is included with the source code to this package.
+module System.Process.Voyeur
+(
+-- * Observing a process
+  withVoyeur
+, FFI.prepareEnvironment
+, startObserving
+
+-- * Observing 'exec*' calls
+, FFI.ObserveExecFlags(..)
+, FFI.defaultExecFlags
+, FFI.ObserveExecHandler
+, FFI.observeExec
+
+-- * Observing \'exit\' calls
+, FFI.ObserveExitHandler
+, FFI.observeExit
+
+-- * Observing \'open\' calls
+, FFI.ObserveOpenFlags(..)
+, FFI.defaultOpenFlags
+, FFI.ObserveOpenHandler
+, FFI.observeOpen
+
+-- * Observing \'close\' calls
+, FFI.ObserveCloseHandler
+, FFI.observeClose
+
+-- * Types
+, FFI.VoyeurContext
+, HasPid
+) where
+
+import Control.Concurrent.MVar (readMVar)
+import Control.Exception (bracket)
+import System.Exit (ExitCode(..))
+import System.Posix.Types (ProcessID)
+import System.Process.Internals (ProcessHandle(..), ProcessHandle__(..))
+
+import Paths_voyeur (getDataFileName)
+import qualified System.Process.Voyeur.FFI as FFI
+
+-- | Creates a 'FFI.VoyeurContext' and runs an IO action that observes
+-- a process using it.
+withVoyeur :: (FFI.VoyeurContext -> IO a) -> IO a
+withVoyeur = bracket initContext FFI.destroyContext
+ where
+   initContext = do
+     c <- FFI.createContext
+     rPath <- getDataFileName $ "/"
+     FFI.setResourcePath c rPath
+     return c
+
+-- | The class of values that contain a 'ProcessID'. This is used to
+-- abstract over the different representations of a process used by
+-- the various process libraries.
+class HasPid a where
+  toPid :: a -> IO (Maybe ProcessID)
+
+instance HasPid ProcessID where
+  toPid = return . Just
+
+-- Unfortunately, we have to reach into the internals of
+-- System.Process for this one.
+
+#if MIN_VERSION_process(1,2,0)
+
+instance HasPid ProcessHandle where
+  toPid (ProcessHandle m _) = do
+    p <- readMVar m
+    case p of
+      (OpenHandle pid) -> return $ Just pid
+      _                -> return Nothing
+    
+#else
+
+instance HasPid ProcessHandle where
+  toPid (ProcessHandle m) = do
+    p <- readMVar m
+    case p of
+      (OpenHandle pid) -> return $ Just pid
+      _                -> return Nothing
+
+#endif
+
+-- | Start observing a child process. Your handlers will be called
+-- while the process runs. Note that no handlers will be called if
+-- you didn't start the process with an environment produced by
+-- 'FFI.prepareEnvironment'.
+--
+-- When the child process exits, 'startObserving' will terminate the
+-- server component of libvoyeur and return. This means that
+-- 'startObserving' implicitly waits for the child process, so you
+-- don't need to do this on your own.
+startObserving :: HasPid a
+               => FFI.VoyeurContext  -- ^ The context.
+               -> a                  -- ^ The child process to observe.
+               -> IO ExitCode        -- ^ The exit status of the child process.
+startObserving c p = do
+  mayPid <- toPid p
+  case mayPid of
+    Just pid -> FFI.startObserving c pid
+    Nothing  -> return $ ExitFailure (-1)
diff --git a/src/System/Process/Voyeur/FFI.hs b/src/System/Process/Voyeur/FFI.hs
new file mode 100644
--- /dev/null
+++ b/src/System/Process/Voyeur/FFI.hs
@@ -0,0 +1,308 @@
+{-# LANGUAGE ForeignFunctionInterface #-}
+{-# LANGUAGE RecordWildCards #-}
+
+module System.Process.Voyeur.FFI
+( VoyeurContext
+, createContext
+, destroyContext
+, ObserveExecFlags(..)
+, defaultExecFlags
+, ObserveExecHandler
+, observeExec
+, ObserveExitHandler
+, observeExit
+, ObserveOpenFlags(..)
+, defaultOpenFlags
+, ObserveOpenHandler
+, observeOpen
+, ObserveCloseHandler
+, observeClose
+, setResourcePath
+, prepareEnvironment
+, startObserving
+) where
+
+import Control.Applicative
+import Control.Exception
+import Data.Bits
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Char8 as BC
+import Data.Word
+import Foreign
+import Foreign.C.String
+import Foreign.C.Types
+import System.Exit
+import System.Posix.Types
+
+--------------------------------------------------
+-- Creating and destroying libvoyeur contexts.
+--------------------------------------------------
+
+-- | The context libvoyeur uses to store its state.
+newtype VoyeurContext = VoyeurContext { unVoyeurContext :: Ptr () }
+
+foreign import ccall unsafe "voyeur.h voyeur_context_create"
+  voyeur_context_create :: IO (Ptr ())
+
+createContext :: IO VoyeurContext
+createContext = VoyeurContext <$> voyeur_context_create
+
+foreign import ccall unsafe "voyeur.h voyeur_context_destroy"
+  voyeur_context_destroy :: Ptr () -> IO ()
+
+destroyContext :: VoyeurContext -> IO ()
+destroyContext = voyeur_context_destroy . unVoyeurContext
+
+
+--------------------------------------------------
+-- Registering callbacks for particular events.
+--------------------------------------------------
+
+-- | Flags for observing 'exec*' calls.
+data ObserveExecFlags = ObserveExecFlags
+  { observeExecCWD      :: !Bool  -- ^ True if you want the current working directory.
+  , observeExecEnv      :: !Bool  -- ^ True if you want the environment. (Potentially slow.)
+  , observeExecPath     :: !Bool  -- ^ True if you want the value of 'PATH'.
+  , observeExecNoAccess :: !Bool  -- ^ True if you want to see calls that
+                                  --   fail due to access restrictions.
+  } deriving (Eq, Show)
+
+-- | Default flags which observe the minimum amount of information.
+defaultExecFlags :: ObserveExecFlags
+defaultExecFlags = ObserveExecFlags False False False False
+
+type ExecCallback = CString -> Ptr CString -> Ptr CString -> CString
+                 -> CString -> CPid -> CPid -> Ptr () -> IO ()
+
+foreign import ccall "wrapper"
+  wrapExecCallback :: ExecCallback -> IO (FunPtr ExecCallback)
+
+foreign import ccall unsafe "voyeur.h voyeur_observe_exec"
+  voyeur_observe_exec :: Ptr () -> Word8 -> FunPtr ExecCallback -> Ptr () -> IO ()
+                         
+-- | A handler for 'exec*' calls.
+type ObserveExecHandler = BS.ByteString                     -- ^ The file being executed.
+                       -> [BS.ByteString]                   -- ^ The arguments.
+                       -> [(BS.ByteString, BS.ByteString)]  -- ^ The environment (if requested).
+                       -> BS.ByteString                     -- ^ The value of 'PATH' (if requested).
+                       -> BS.ByteString                     -- ^ The working directory
+                                                            --   (if requested).
+                       -> ProcessID                         -- ^ The new process ID.
+                       -> ProcessID                         -- ^ The parent process ID.
+                       -> IO ()
+
+-- | Observe calls to the 'exec*' and 'posix_spawn*' families of functions.
+observeExec :: VoyeurContext        -- ^ The context.
+            -> ObserveExecFlags     -- ^ Flags controlling what will be observed.
+            -> ObserveExecHandler   -- ^ A handler for observed 'exec*' events.
+            -> IO ()
+observeExec c (ObserveExecFlags {..}) h = do
+  let flags =  asBit 0 observeExecCWD
+           .|. asBit 1 observeExecEnv
+           .|. asBit 2 observeExecPath
+           .|. asBit 3 observeExecNoAccess
+
+  h' <- wrapExecCallback $ \file argv envp path cwd pid ppid _ -> do
+    file' <- safePackCString file
+    argv' <- packCStringArray argv
+    envp' <- packCStringArray envp
+    path' <- safePackCString path
+    cwd'  <- safePackCString cwd
+    h file' argv' (map bsEnvSplit envp') path' cwd' pid ppid
+
+  voyeur_observe_exec (unVoyeurContext c) flags h' nullPtr
+    
+-- Observing exit() calls.
+type ExitCallback = CInt -> CPid -> CPid -> Ptr () -> IO ()
+
+foreign import ccall "wrapper"
+  wrapExitCallback :: ExitCallback -> IO (FunPtr ExitCallback)
+
+foreign import ccall unsafe "voyeur.h voyeur_observe_exit"
+  voyeur_observe_exit :: Ptr () -> Word8 -> FunPtr ExitCallback -> Ptr () -> IO ()
+                         
+-- | A handler for \'exit\' calls.
+type ObserveExitHandler = ExitCode   -- ^ The exit status.
+                       -> ProcessID  -- ^ The process ID of the exiting process.
+                       -> ProcessID  -- ^ The parent process ID of the exiting process.
+                       -> IO ()
+
+-- | Observe calls to \'exit\'.
+observeExit :: VoyeurContext       -- ^ The context.
+            -> ObserveExitHandler  -- ^ A handler for observed \'exit\' events.
+            -> IO ()
+observeExit c h = do
+  h' <- wrapExitCallback $ \status pid ppid _ ->
+    h (asExitCode status) pid ppid
+
+  voyeur_observe_exit (unVoyeurContext c) 0 h' nullPtr
+
+-- | Flags for observing \'open\' calls.
+data ObserveOpenFlags = ObserveOpenFlags
+  { observeOpenCWD :: !Bool  -- ^ True if you want the current working directory.
+  } deriving (Eq, Show)
+
+-- | Default flags which observe the minimum amount of information.
+defaultOpenFlags :: ObserveOpenFlags
+defaultOpenFlags = ObserveOpenFlags False
+
+type OpenCallback = CString -> CInt -> CMode -> CString -> CInt -> CPid -> Ptr () -> IO ()
+
+foreign import ccall "wrapper"
+  wrapOpenCallback :: OpenCallback -> IO (FunPtr OpenCallback)
+
+foreign import ccall unsafe "voyeur.h voyeur_observe_open"
+  voyeur_observe_open :: Ptr () -> Word8 -> FunPtr OpenCallback -> Ptr () -> IO ()
+                         
+-- | A handler for \'open\' calls.
+type ObserveOpenHandler = BS.ByteString  -- ^ The file being opened.
+                       -> Int            -- ^ The flags used to open the file.
+                       -> FileMode       -- ^ The mode. Only meaningful if O_CREAT
+                                         --   was specified.
+                       -> BS.ByteString  -- ^ The working directory (if requested).
+                       -> Int            -- ^ The return value of \'open\'. May be a
+                                         --   file descriptor or an error value.
+                       -> ProcessID      -- ^ The process ID of the observed process.
+                       -> IO ()
+
+-- | Observe calls to \'open\'.
+observeOpen :: VoyeurContext       -- ^ The context.
+            -> ObserveOpenFlags    -- ^ Flags controlling what will be observed.
+            -> ObserveOpenHandler  -- ^ A handler for observed \'open\' events.
+            -> IO ()
+observeOpen c (ObserveOpenFlags {..}) h = do
+  let flags = (asBit 0 observeOpenCWD)
+
+  h' <- wrapOpenCallback $ \path oflag mode cwd retval pid _ -> do
+    path' <- safePackCString path
+    cwd' <- safePackCString cwd
+    h path' (fromIntegral oflag) mode cwd' (fromIntegral retval) pid
+
+  voyeur_observe_open (unVoyeurContext c) flags h' nullPtr
+
+-- Observing close() calls.
+type CloseCallback = CInt -> CInt -> CPid -> Ptr () -> IO ()
+
+foreign import ccall "wrapper"
+  wrapCloseCallback :: CloseCallback -> IO (FunPtr CloseCallback)
+
+foreign import ccall unsafe "voyeur.h voyeur_observe_close"
+  voyeur_observe_close :: Ptr () -> Word8 -> FunPtr CloseCallback -> Ptr () -> IO ()
+                         
+-- | A handler for \'close\' calls.
+type ObserveCloseHandler = Int        -- ^ The file descriptor being closed.
+                        -> Int        -- ^ The return value of \'close\'.
+                        -> ProcessID  -- ^ The process ID of the observed process.
+                        -> IO ()
+
+-- | Observe calls to \'close\'.
+observeClose :: VoyeurContext        -- ^ The context.
+             -> ObserveCloseHandler  -- ^ A handler for observed \'close\' events.
+             -> IO ()
+observeClose c h = do
+  h' <- wrapCloseCallback $ \fd retval pid _ ->
+    h (fromIntegral fd) (fromIntegral retval) pid
+
+  voyeur_observe_close (unVoyeurContext c) 0 h' nullPtr
+
+
+--------------------------------------------------
+-- Other context configuration options.
+--------------------------------------------------
+
+foreign import ccall unsafe "voyeur.h voyeur_set_resource_path"
+  voyeur_set_resource_path :: Ptr () -> CString -> IO ()
+
+setResourcePath :: VoyeurContext -> FilePath -> IO ()
+setResourcePath c path = withCString path $ \cPath ->
+                           voyeur_set_resource_path (unVoyeurContext c) cPath
+
+--------------------------------------------------
+-- Observing processes.
+--------------------------------------------------
+
+foreign import ccall unsafe "voyeur.h voyeur_prepare"
+  voyeur_prepare :: Ptr () -> Ptr CString -> IO (Ptr CString)
+
+-- | Prepares an environment for a child process you want to observe.
+-- 'prepareEnvironment' starts the server component of libvoyeur and
+-- adds or modifies environment variables as necessary to inject code
+-- into the child process you're about to create and make sure it can
+-- connect to the server.
+--
+-- Generally after calling 'prepareEnvironment', you'll want to start
+-- the child process using the returned environment, and then call
+-- 'System.Process.Voyeur.startObserving' to begin receiving events.
+--
+-- If something goes wrong, 'prepareEnvironment' will return 'Nothing'.
+prepareEnvironment :: VoyeurContext                  -- ^ The context.
+                   -> [(String, String)]             -- ^ The environment you want to use.
+                   -> IO (Maybe [(String, String)])  -- ^ A modified version of that
+                                                     --   environment, or 'Nothing' if something
+                                                     --   went wrong.
+prepareEnvironment c envp = bracket newCEnvp freeCEnvp $ \envp' -> do
+    cEnvp <- withArray0 nullPtr envp' (voyeur_prepare (unVoyeurContext c))
+    if cEnvp == nullPtr
+       then return Nothing
+       else do envp'' <- peekCStringArray cEnvp
+               free cEnvp
+               return . Just $ map envSplit envp''
+  where
+    newCEnvp = mapM (newCString . envJoin) envp
+    freeCEnvp = mapM free
+
+foreign import ccall safe "voyeur.h voyeur_start"
+  voyeur_start :: Ptr () -> CPid -> IO CInt
+
+startObserving :: VoyeurContext -> ProcessID -> IO ExitCode
+startObserving c pid = asExitCode <$> voyeur_start (unVoyeurContext c) pid
+
+
+--------------------------------------------------
+-- Utility functions.
+--------------------------------------------------
+
+asBit :: Int -> Bool -> Word8
+asBit n True  = shiftL 1 n
+asBit _ False = 0
+
+asExitCode :: CInt -> ExitCode
+asExitCode 0 = ExitSuccess
+asExitCode s = ExitFailure (fromIntegral s)
+
+safePackCString :: CString -> IO BS.ByteString
+safePackCString s | s == nullPtr = return BS.empty
+                  | otherwise    = BS.packCString s
+
+safePeekCString :: CString -> IO String
+safePeekCString s | s == nullPtr = return ""
+                  | otherwise    = peekCString s
+
+packCStringArray :: Ptr CString -> IO [BS.ByteString]
+packCStringArray array
+  | array == nullPtr = return []
+  | otherwise        = do array' <- peekArray0 nullPtr array
+                          mapM safePackCString array'
+
+peekCStringArray :: Ptr CString -> IO [String]
+peekCStringArray array
+  | array == nullPtr = return []
+  | otherwise        = do array' <- peekArray0 nullPtr array
+                          mapM safePeekCString array'
+
+envJoin :: (String, String) -> String
+envJoin (k, v) = concat [k, "=", v]
+
+envSplit :: String -> (String, String)
+envSplit = safeTailSnd . break (== '=')
+
+bsEnvSplit :: BS.ByteString -> (BS.ByteString, BS.ByteString)
+bsEnvSplit = bsSafeTailSnd . BC.break (== '=')
+  
+safeTailSnd :: (String, String) -> (String, String)
+safeTailSnd (a, b) | null b    = (a, b)
+                   | otherwise = (a, tail b)
+
+bsSafeTailSnd :: (BS.ByteString, BS.ByteString) -> (BS.ByteString, BS.ByteString)
+bsSafeTailSnd (a, b) | BS.null b = (a, b)
+                     | otherwise = (a, BS.tail b)
diff --git a/voyeur.cabal b/voyeur.cabal
new file mode 100644
--- /dev/null
+++ b/voyeur.cabal
@@ -0,0 +1,54 @@
+name:                voyeur
+version:             0.1.0.0
+synopsis:            Haskell bindings for libvoyeur
+description:         Haskell bindings for libvoyeur, a library for observing the private
+                     activities of child processes.
+homepage:            https://github.com/sethfowler/hslibvoyeur
+license:             BSD3
+license-file:        LICENSE
+author:              Seth Fowler <mark.seth.fowler@gmail.com>
+maintainer:          Seth Fowler <mark.seth.fowler@gmail.com>
+copyright:           Copyright (C) 2014 Seth Fowler
+category:            System
+build-type:          Custom
+cabal-version:       >=1.18
+extra-source-files: README.md,
+                    changelog,
+
+                    -- Bake in libvoyeur in its entirety.
+                    libvoyeur/include/voyeur.h,
+                    libvoyeur/LICENSE,
+                    libvoyeur/Makefile,
+                    libvoyeur/README.md,
+                    libvoyeur/src/dyld.h,
+                    libvoyeur/src/env.c,
+                    libvoyeur/src/env.h,
+                    libvoyeur/src/event.c,
+                    libvoyeur/src/event.h,
+                    libvoyeur/src/net.c,
+                    libvoyeur/src/net.h,
+                    libvoyeur/src/util.c,
+                    libvoyeur/src/util.h,
+                    libvoyeur/src/voyeur-close.c,
+                    libvoyeur/src/voyeur-exec.c,
+                    libvoyeur/src/voyeur-exit.c,
+                    libvoyeur/src/voyeur-open.c,
+                    libvoyeur/src/voyeur.c
+
+source-repository head
+  type: git
+  location: https://github.com/sethfowler/hslibvoyeur.git
+  tag: 0.1
+
+library
+  ghc-options:         -Wall
+  exposed-modules:     System.Process.Voyeur
+  other-modules:       System.Process.Voyeur.FFI,
+                       Paths_voyeur
+  build-depends:       base >=4.6 && <4.8,
+                       bytestring >=0.10 && <0.11,
+                       process >=1.1 && <1.3,
+                       utf8-string >=0.3 && <0.4
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+  default-extensions:  CPP
