diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ee55750..2e48fc0 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -379,7 +379,7 @@ jobs:
           architecture: x86-64
           version: '14.3'
           run: |
-            sudo pkg install -y meson pkgconf ninja lmdb libarchive yyjson libsodium
+            sudo pkg install -y meson pkgconf ninja lmdb libarchive yyjson libsodium gpgme
             meson setup build --buildtype=release
             meson compile -C build
             meson test -C build
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9ab8928..6eb0380 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,12 +19,24 @@ All notable changes to this project will be documented in this file.
   `cross-riscv32.txt`, `cross-mips.txt`, `cross-powerpc.txt`) and CI coverage:
   an `arch-asm` job that assembles and PIC-links every 32-bit backend, plus a
   full `armhf` cross-compile job
-- FreeBSD support: the library builds, links, and passes its test suite on
-  FreeBSD with the libsodium signing backend; no source changes are required
-  because the seccomp dependency is optional and the install-script runner
-  already has a non-Linux path
+- FreeBSD install-script sandbox: `run_script()` now isolates scripts on
+  FreeBSD using Capsicum capability mode (`cap_enter()`); the script binary is
+  opened before entering capability mode and executed with `fexecve()`, and
+  sandbox failure is fail-closed, matching the Linux `unshare()` guarantee
 - CI job that builds and tests the library in a native FreeBSD x86_64 virtual
-  machine
+  machine, now with the `gpgme` signing backend installed so the preferred
+  backend gets the same test coverage it has on Linux
+
+### Fixed
+
+- `-D_GNU_SOURCE` is now only added on Linux; it was previously applied
+  unconditionally, which is a glibc-specific feature macro with no meaning on
+  BSD libc
+- Default `keyring_dir` and `keys_dir` now resolve under `/usr/local/etc/apg/`
+  on FreeBSD instead of `/etc/apg/`, following FreeBSD's `hier(7)` convention
+  for locally-installed configuration
+- README documents FreeBSD `pkg install` dependencies and the platform's
+  sandbox and configuration-path differences
 
 ## [1.9.0] - 2026-06-22
 
diff --git a/README.md b/README.md
index 996bb5e..7960d44 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@ Package management library for NurOS.
 | [lmdb](https://www.symas.com/lmdb) | Embedded key-value database |
 | [yyjson](https://github.com/ibireme/yyjson) | JSON library |
 | [gpgme](https://www.gnupg.org/related_software/gpgme/) **or** [libsodium](https://libsodium.org/) | Package signing |
-| [libseccomp](https://github.com/seccomp/libseccomp) *(optional)* | Syscall filtering for install script sandbox |
+| [libseccomp](https://github.com/seccomp/libseccomp) *(optional, Linux only)* | Syscall filtering for install script sandbox |
 
 ## Signing backends
 
@@ -22,6 +22,16 @@ libapg supports two signing backends. The build system picks the first one avail
 - **gpgme** (preferred) — PGP signing via GnuPG. Only ECC keys are accepted by default (Ed25519, ECDSA). RSA can be enabled explicitly by passing `allow_rsa = true` to `sign_verify`.
 - **libsodium** (fallback) — Ed25519 signing. Always ECC, keys are read from `/etc/apg/keys/`.
 
+## Install-script sandbox
+
+`run_script()` isolates pre/post-install scripts using the strongest primitive available on the host:
+
+- **Linux** — `unshare()` with isolated network, mount, UTS, and IPC namespaces, plus optional `libseccomp` syscall filtering.
+- **FreeBSD** — [Capsicum](https://man.freebsd.org/cgi/man.cgi?query=capsicum) capability mode (`cap_enter()`). The script binary is opened before entering capability mode and executed with `fexecve()`; once sandboxed, the process loses access to global namespaces (path-based lookups, most syscalls).
+- **Other POSIX platforms** — no sandbox primitive is available; scripts run without isolation.
+
+On Linux and FreeBSD, if the sandbox cannot be established the script is not executed (fail closed).
+
 ## Building
 
 ### With Nix
@@ -74,6 +84,14 @@ sudo emerge dev-build/meson dev-build/ninja dev-util/pkgconf app-arch/libarchive
 sudo xbps-install meson ninja pkgconf libarchive-devel lmdb-devel gpgme-devel
 ```
 
+#### FreeBSD
+
+```bash
+sudo pkg install meson pkgconf ninja lmdb libarchive yyjson gpgme
+```
+
+`libseccomp` is not available on FreeBSD; the install-script sandbox uses Capsicum instead (see [Install-script sandbox](#install-script-sandbox)). Default configuration paths follow FreeBSD's `hier(7)` and resolve under `/usr/local/etc/apg/` instead of `/etc/apg/`.
+
 #### Build
 
 ```bash
diff --git a/include/apg/scripts.h b/include/apg/scripts.h
index e5b5535..04ad94a 100644
--- a/include/apg/scripts.h
+++ b/include/apg/scripts.h
@@ -14,9 +14,14 @@
  * @brief Run a lifecycle script from a package's @c scripts/ directory.
  *
  * Executes @c pkg_dir/scripts/name if the file exists and is executable.
- * The script runs in a sandboxed child process with isolated network, mount,
- * UTS, and IPC namespaces. If the sandbox cannot be established the script
- * is not executed and the call returns false (fail closed).
+ * On Linux, the script runs in a child process with isolated network, mount,
+ * UTS, and IPC namespaces (@c unshare()). On FreeBSD, the script runs under
+ * Capsicum capability mode (@c cap_enter()), which drops access to global
+ * namespaces (path-based lookups, most syscalls) after the executable is
+ * opened. On both platforms, if the sandbox cannot be established the script
+ * is not executed and the call returns false (fail closed). On other POSIX
+ * platforms, no sandbox primitive is available and the script runs without
+ * isolation.
  * Common script names are @c pre-install, @c post-install, @c pre-remove,
  * and @c post-remove.
  *
diff --git a/meson.build b/meson.build
index 9234eda..2976888 100644
--- a/meson.build
+++ b/meson.build
@@ -35,11 +35,24 @@ libapg_sources = files(
   'src/transaction/commit.c',
 )
 
-add_project_arguments('-D_GNU_SOURCE', language: 'c')
+if host_machine.system() == 'linux'
+  add_project_arguments('-D_GNU_SOURCE', language: 'c')
+endif
+
+keyring_dir = get_option('keyring_dir')
+keys_dir = get_option('keys_dir')
+if host_machine.system() == 'freebsd'
+  if keyring_dir == '/etc/apg/trusted.d'
+    keyring_dir = '/usr/local/etc/apg/trusted.d'
+  endif
+  if keys_dir == '/etc/apg/keys'
+    keys_dir = '/usr/local/etc/apg/keys'
+  endif
+endif
 
 add_project_arguments(
-  '-DAPG_KEYRING_DIR="' + get_option('keyring_dir') + '"',
-  '-DAPG_KEYS_DIR="' + get_option('keys_dir') + '"',
+  '-DAPG_KEYRING_DIR="' + keyring_dir + '"',
+  '-DAPG_KEYS_DIR="' + keys_dir + '"',
   '-DAPG_TMP_DIR="' + get_option('tmp_dir') + '"',
   '-DAPG_DB_MAPSIZE=' + get_option('db_mapsize').to_string(),
   language: 'c',
diff --git a/meson_options.txt b/meson_options.txt
index aab2db9..89daf23 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,13 +1,13 @@
 option('keyring_dir',
   type: 'string',
   value: '/etc/apg/trusted.d',
-  description: 'Default directory for trusted signing keys',
+  description: 'Default directory for trusted signing keys (auto-adjusted to /usr/local/etc/apg/trusted.d on FreeBSD unless overridden)',
 )
 
 option('keys_dir',
   type: 'string',
   value: '/etc/apg/keys',
-  description: 'Default directory for libsodium key files',
+  description: 'Default directory for libsodium key files (auto-adjusted to /usr/local/etc/apg/keys on FreeBSD unless overridden)',
 )
 
 option('tmp_dir',
diff --git a/src/install/scripts.c b/src/install/scripts.c
index f2c8753..26fcdda 100644
--- a/src/install/scripts.c
+++ b/src/install/scripts.c
@@ -11,6 +11,9 @@
 
 #ifdef __linux__
 #include <sched.h>
+#elif defined(__FreeBSD__)
+#include <fcntl.h>
+#include <sys/capsicum.h>
 #endif
 
 #include "../../include/apg/scripts.h"
@@ -64,8 +67,62 @@ exec_script(const char *path)
         _exit(1);
     }
 
-    // Parent: close write end and check whether child signalled sandbox
-    // failure.
+    close(pipefd[1]);
+    uint8_t err = 0;
+    ssize_t n = read(pipefd[0], &err, 1);
+    close(pipefd[0]);
+
+    if (n > 0)
+    {
+        waitpid(pid, NULL, 0);
+        return false;
+    }
+
+    int status;
+    if (waitpid(pid, &status, 0) < 0)
+        return false;
+    return WIFEXITED(status) && WEXITSTATUS(status) == 0;
+#elif defined(__FreeBSD__)
+    int fd = open(path, O_EXEC);
+    if (fd < 0)
+        return false;
+
+    int pipefd[2];
+    if (pipe(pipefd) < 0)
+    {
+        close(fd);
+        return false;
+    }
+
+    pid_t pid = fork();
+    if (pid < 0)
+    {
+        close(fd);
+        close(pipefd[0]);
+        close(pipefd[1]);
+        return false;
+    }
+
+    if (pid == 0)
+    {
+        close(pipefd[0]);
+
+        if (cap_enter() < 0)
+        {
+            uint8_t err = 1;
+            (void)write(pipefd[1], &err, 1);
+            close(pipefd[1]);
+            _exit(1);
+        }
+
+        close(pipefd[1]);
+        char *const argv[] = {(char *)path, NULL};
+        char *const envp[] = {NULL};
+        fexecve(fd, argv, envp);
+        _exit(1);
+    }
+
+    close(fd);
     close(pipefd[1]);
     uint8_t err = 0;
     ssize_t n = read(pipefd[0], &err, 1);
