diff --git a/README b/README
index 039f960..e4fb5ec 100644
--- a/README
+++ b/README
@@ -11,6 +11,9 @@ environment, allowing ordinary users to run commands which give the illusion
 of creating device nodes, changing file ownership, and otherwise doing
 things necessary for creating distribution packages or filesystems.
 
+pseudo is not a security boundary and must not be used in security
+sensitive applications.  See README.security for details.
+
 Building pseudo requires GCC 5 or later (or clang), which the configure
 script checks for.
 
diff --git a/README.security b/README.security
new file mode 100644
index 0000000..2cb124f
--- /dev/null
+++ b/README.security
@@ -0,0 +1,152 @@
+pseudo -- security considerations
+
+SUMMARY:
+
+pseudo is a build tool.  It exists so that ordinary users can produce
+packages and filesystem images containing files that appear to be owned
+by root, device nodes, special permissions and extended attributes.
+
+pseudo is NOT a security boundary and must not be used in security
+sensitive applications.  In particular, do not use pseudo:
+
+* to contain, confine or sandbox untrusted programs;
+* as a substitute for real privileges, a real chroot, a container or a
+  virtual machine;
+* to process filesystem trees that untrusted or concurrent processes can
+  modify while pseudo is running;
+* as the only guarantee that the ownership, permissions, SELinux labels
+  or file capabilities in a security critical image are correct.
+
+
+1. INTERCEPTION IS COOPERATIVE
+
+pseudo works by loading libpseudo with LD_PRELOAD (DYLD_INSERT_LIBRARIES
+on Darwin) and wrapping libc functions.  Anything that does not go
+through those wrappers reaches the real filesystem directly, with the
+real privileges of the user who started pseudo.  This includes:
+
+* statically linked programs;
+* programs that make system calls directly, including language runtimes
+  that do not use libc for file operations, and interfaces such as
+  io_uring;
+* setuid/setgid programs, for which the dynamic linker ignores
+  LD_PRELOAD;
+* any program that clears LD_PRELOAD, or that sets PSEUDO_UNLOAD or
+  PSEUDO_DISABLED (see pseudo_client.c), both of which are deliberately
+  supported ways to turn pseudo off.
+
+A program running under pseudo can therefore always step outside of it.
+pseudo cannot be used to restrict what a program does.
+
+The Darwin port is described as implemented, but it has not worked for
+a long time: System Integrity Protection and the hardened runtime block
+DYLD_INSERT_LIBRARIES, so it is unlikely to be useful.
+
+
+2. "ROOT" EXISTS ONLY IN THE DATABASE
+
+pseudo grants no privileges.  Files on disk remain owned by the real
+user, and the ownership and modes that programs see under pseudo are
+read back from pseudo's database.  The kernel does not enforce any of
+the emulated ownership or permissions.
+
+The real on-disk modes are also deliberately different from the
+emulated ones.  PSEUDO_FS_MODE (pseudo_client.h) always adds owner read
+and write permission (and owner execute for directories) so that the
+emulated root can access everything, and removes group/other write,
+setuid and setgid bits from the real file.  A file that is meant to be
+mode 0600 or 0000 and owned by root, for example /etc/shadow or a
+private key, is readable and writable on disk by the build user and by
+any process running as that user, whether or not it is under pseudo.
+
+Treat everything pseudo writes as accessible to the build user.
+
+
+3. FILE DESCRIPTORS BECOME PATHS (TOCTOU)
+
+Many security sensitive programs rely on the kernel resolving a file
+descriptor, or a path relative to a directory descriptor, to a specific
+inode.  This is how they avoid races where a path component is replaced
+with a symlink between checking a file and using it.  pseudo removes
+that protection in several ways:
+
+* Paths are canonicalized in user space.  pseudo_fix_path()
+  (pseudo_util.c) walks each path component with lstat() and readlink()
+  and builds an absolute path, and the real function is then called
+  with that absolute path (see doc/chroot).  A call such as
+  openat(dirfd, "file", ...) is no longer anchored to dirfd; it becomes
+  an operation on "<recorded path of dirfd>/file".  Kernel guarantees
+  such as O_NOFOLLOW on intermediate components or RESOLVE_BENEATH are
+  not reproduced by this resolution, except in the few wrappers marked
+  preserve_paths in wrapfuncs.in, which handle paths themselves.
+
+* The path pseudo recorded for a descriptor (fd_path() in
+  pseudo_client.c) is used to resolve paths relative to it.  That
+  record can be stale: the file or directory may have been renamed, or
+  the descriptor may have been inherited, received over a socket, or
+  opened by a call pseudo did not see.
+
+* Several wrappers check a path and then act on it by path in a second
+  step.  For example, fchmodat (ports/unix/guts/fchmodat.c) stats the
+  path and later calls the real fchmodat() on the same path, and mknodat
+  (ports/linux/guts/__xmknodat.c) removes the path again by name if the
+  database update fails.
+
+If anything else can change the tree while pseudo is working on it, an
+operation may be applied to a different file than the one that was
+checked, both in pseudo's database and on the real filesystem.  Only
+run pseudo on trees that are fully under the control of the build.
+
+
+4. THE CHROOT IS EMULATED
+
+pseudo's chroot() support only prepends a stored prefix to absolute
+paths and strips it from some results (see doc/chroot).  No real
+chroot() is performed, and the README notes the support is incomplete.
+Any of the bypasses in section 1, or any path pseudo resolves
+differently from the kernel, reaches the host filesystem.  pseudo's
+chroot must not be relied on to keep a program inside a directory.
+
+
+5. METADATA LIVES IN A DATABASE THAT CAN DRIFT
+
+Emulated ownership, modes, device nodes and extended attributes,
+including security.selinux and security.capability, exist only in
+pseudo's database until a packaging or image step copies them out.
+Entries are keyed by path and inode number.  When files are changed
+outside of pseudo, for example deleted or recreated without pseudo
+loaded, an inode number can be reused and the database can attach the
+old file's ownership, mode or attributes to a new file.  The server
+reports such cases as "inode mismatch" or "path mismatch" (pseudo.c),
+but cannot always know which side is correct.
+
+Accesses to paths listed in PSEUDO_IGNORE_PATHS are deliberately not
+tracked at all.
+
+Generated images should be verified independently if their ownership,
+permissions, labels or capabilities matter for security.  Do not
+ignore mismatch errors in pseudo's log.
+
+
+6. THE SERVER TRUSTS ITS CLIENTS
+
+The pseudo server listens on a Unix socket in its local state directory
+and does not authenticate clients beyond the filesystem permissions of
+that socket.  Any process running as the build user can connect and
+change the database, and the database files themselves are writable by
+that user.  The resulting metadata can only be trusted as much as every
+process running as the build user.
+
+
+RECOMMENDATIONS:
+
+* Run pseudo only on build output that is private to the build and is
+  not modified concurrently by anything outside of it.
+* Run untrusted build steps under a real isolation mechanism (a
+  container, a virtual machine, or a dedicated user), with pseudo inside
+  it if needed, never pseudo alone.
+* Do not store secrets in a pseudo-managed tree on a shared system;
+  they are readable by the build user regardless of their emulated
+  ownership and mode.
+* Verify the ownership, permissions, labels and capabilities of
+  security relevant files in the final image.
