macFUSE-based protective filesystem overlay for OpenClaw on macOS.
Goal: mount a protective overlay at ~/.openclaw, migrate your existing data to ~/.openclaw.real on first run, and start the OpenClaw gateway via the supervisor.
-
Install and enable macFUSE (required on macOS).
-
Clone + install:
gh repo clone Martin-Tech-Labs/openclaw-protectfs
cd openclaw-protectfs
npm installOptional: one-command quickstart smoke test (Refs #88):
bash scripts/quickstart.sh- Build the Swift FUSE daemon (preferred).
The Swift FUSE daemon executable (ocprotectfs-fuse) is an opt-in SwiftPM product because it depends on macFUSE headers.
# Build Swift components (macOS only)
OCPROTECTFS_BUILD_FUSEFS_SWIFT=1 make swift-build
# Swift is the default on macOS. Optionally point the Node launcher at a specific Swift daemon binary:
export OCPROTECTFS_FUSE_SWIFT_BIN="$(pwd)/fusefs-swift/.build/debug/ocprotectfs-fuse"
# To force the legacy Node implementation (deprecated):
# export OCPROTECTFS_FUSE_IMPL=node
# Note: on macOS, the legacy Node (fuse-native) path is not supported on Node >= 25 by default
# due to known native-addon instability; use Node 22/24 LTS or prefer Swift.- Start the supervisor.
The supervisor supervises two long-running processes:
- the FUSE daemon (this repo): Swift (preferred) via
fusefs-swift(launched throughfusefs/ocprotectfs-fuse.js) - your OpenClaw gateway process (must stay running; supervisor will shut down the mount if it exits)
If you just want to validate the mount + encryption behavior without starting OpenClaw yet, you can use a dummy gateway (/bin/sleep) for a smoke test:
node wrapper/ocprotectfs.js \
--require-fuse-ready \
--fuse-bin node \
--fuse-arg fusefs/ocprotectfs-fuse.js \
--gateway-bin /bin/sleep \
--gateway-arg 1000000Notes:
- The KEK (Key Encryption Key) is retrieved/created in macOS Keychain (
service=ocprotectfs,account=kek). - The supervisor passes the KEK to the FUSE daemon in-memory via an anonymous pipe (no env secret).
- For a real deployment, replace the dummy gateway with the command that runs your OpenClaw gateway in the foreground.
Optional (advanced): reset the KEK in Keychain.
By default, the supervisor will create and manage the KEK Keychain item automatically on first run. On macOS, it uses native Keychain APIs (via a small Swift helper) and configures the item to require interactive user presence (Touch ID / password) when accessed.
If you need to rotate/reset the KEK (this will make existing encrypted data unreadable unless you also rotate/re-encrypt DEKs), you can delete the item:
# Danger: rotating the KEK breaks decryption of previously-encrypted data.
security delete-generic-password -s ocprotectfs -a kek- Quick verify (plaintext vs ciphertext):
# Workspace is passthrough plaintext
echo "hello" > ~/.openclaw/workspace/_ocpfs_smoketest.txt
# Non-workspace paths are encrypted-at-rest in ~/.openclaw.real
echo "secret" > ~/.openclaw/_ocpfs_smoketest_secret.txt
# The mounted view shows plaintext...
cat ~/.openclaw/_ocpfs_smoketest_secret.txt
# ...but the backstore should not.
# (If this finds the word 'secret' under ~/.openclaw.real, something is wrong.)
grep -R "secret" ~/.openclaw.real || echo "OK: not found in backstore"Optional: run the scripted confidence pass (Issue #161 helper):
# Run from an interactive terminal (Keychain user-presence prompt).
bash scripts/real-mount-verify.sh- Rollback (if anything looks wrong): stop the supervisor (Ctrl-C) and see Safety / rollback below.
npm install
npm test
# real macFUSE mount tests run automatically on macOS when prerequisites exist.
# On very new Node majors, they are skipped unless explicitly forced.
# In CI they are skipped unless explicitly enabled:
# CI=1 OCPROTECTFS_RUN_REAL_MOUNT_TESTS=1 npm test- Initial: COMPLETE (see
tasks/STATUS.mdfor the canonical tracker) - Recommendation: disable the protectfs repo heartbeat cron unless you want post-initial verification/backlog work.
- Last updated: 2026-03-25 (final bookkeeping)
Final bookkeeping: Initial is complete; this repo loop can be disabled.
OpenClaw stores sensitive data under ~/.openclaw (sessions, profiles, internal state). Tools and other same-user processes can often read those files.
This project provides a path-compatible mount over ~/.openclaw that:
- keeps workspace data usable and plaintext
- stores everything else encrypted at rest
- enforces a fail-closed access policy for sensitive paths
-
Supervisor (
ocprotectfs-supervisor)- obtains the Key Encryption Key (KEK) from macOS Keychain (user presence)
- mounts the FUSE filesystem at
~/.openclaw - starts OpenClaw gateway as a child process
- maintains a liveness socket so the FUSE layer can fail-closed if supervisor/gateway die
-
FUSE daemon (
ocprotectfs-fuse)- implemented in Swift (
fusefs-swift) and launched via the Node entrypointfusefs/ocprotectfs-fuse.js- Swift is the default on macOS; the Node implementation is legacy and requires
--impl node/OCPROTECTFS_FUSE_IMPL=node
- Swift is the default on macOS; the Node implementation is legacy and requires
- implements filesystem operations (getattr/readdir/open/read/write/rename/unlink/…)
- classifies paths (workspace passthrough vs encrypted)
- encrypts/decrypts non-workspace file contents
- hides sidecar metadata files from the mounted view
- implemented in Swift (
-
Backstore (
~/.openclaw.real)- real on-disk storage
- workspace subtree is stored as plaintext
- sensitive subtree is stored as ciphertext + sidecars
-
OpenClaw gateway
- performs normal OpenClaw operations and reads/writes via the mounted
~/.openclaw
- performs normal OpenClaw operations and reads/writes via the mounted
- FUSE (Filesystem in Userspace) lets you implement a filesystem in a normal user-space process.
- macFUSE is the macOS kernel extension + tooling that enables FUSE filesystems on macOS.
In this project, macFUSE routes file operations on ~/.openclaw into our FUSE daemon, which then enforces policy and reads/writes the backstore.
-
Plaintext passthrough (configurable): selected top-level prefixes under the mount.
- Default passthrough prefixes:
~/.openclaw/workspace/**
- Configure passthrough prefixes (examples):
- FUSE flags (repeatable):
ocprotectfs-fuse --plaintext-prefix workspace --plaintext-prefix workspace-joao - Or env var (comma-separated):
OCPROTECTFS_PLAINTEXT_PREFIXES=workspace,workspace-joao
- FUSE flags (repeatable):
- Default passthrough prefixes:
-
Encrypted-at-rest (everything else under
~/.openclaw/**)- stored encrypted in
~/.openclaw.real - each encrypted file has a wrapped per-file DEK sidecar
*.ocpfs.dek(hidden from mount)
- stored encrypted in
-
Fail-closed rules for encrypted paths
- deny access unless supervisor/gateway checks pass (initial currently includes bring-up gating; see Security notes)
flowchart TB
subgraph UserSpace[User space: agent]
W[supervisor — ocprotectfs-supervisor]
F[FUSE daemon — ocprotectfs-fuse]
G[OpenClaw gateway]
end
subgraph FS[Filesystem]
M[mountpoint: ~/.openclaw]
B[backstore: ~/.openclaw.real]
K[Keychain item: KEK]
end
W -->|starts| F
W -->|spawns| G
W -->|gets KEK user presence| K
W -->|passes KEK in-memory| F
G -->|file ops| M
M -->|FUSE ops| F
F -->|plaintext passthrough| B
F -->|encrypt/decrypt non-workspace| B
flowchart LR
P[Plaintext file bytes] -->|encrypt with per-file DEK| C[Ciphertext file]
KEK[KEK Keychain-derived] -->|wrap/unwrap| DEK[DEK per file]
DEK -->|AEAD XChaCha20-Poly1305| C
C -->|decrypt| P
DEK --> S[Sidecar: *.ocpfs.dek — wrapped DEK + metadata]
- macOS
- macFUSE installed and enabled
- Node.js (use the same node runtime you use for OpenClaw)
gh repo clone Martin-Tech-Labs/openclaw-protectfs
cd openclaw-protectfs
npm install
npm testReal macFUSE mount tests run automatically on macOS when prerequisites exist.
In CI they are skipped unless explicitly enabled:
CI=1 OCPROTECTFS_RUN_REAL_MOUNT_TESTS=1 npm test(See docs/local-macfuse.md for prerequisites/troubleshooting.)
ProtectFS mounts a path-compatible view at ~/.openclaw, but the real bytes on disk live in the backstore:
~/.openclaw— the mounted view (what OpenClaw and tools read/write)~/.openclaw.real— the backstore (the underlying storage)
Think of ~/.openclaw.real as “the real data directory”, and ~/.openclaw as a policy-enforced overlay on top.
Notes:
- Workspace passthrough prefixes are stored plaintext in the backstore (so developer tooling stays usable).
- Everything else is stored encrypted-at-rest in the backstore (ciphertext +
*.ocpfs.deksidecars).
When ProtectFS first mounts at ~/.openclaw, any pre-existing content that was already in ~/.openclaw would otherwise become hidden under the mount.
To avoid that, the supervisor performs a one-time migration step before mounting:
- Moves existing entries out of
~/.openclawinto:~/.openclaw.real/.legacy-openclaw/<timestamp>/...
- Writes a marker file:
~/.openclaw.real/.ocpfs.migrated.json
- Uses an in-progress file for fail-closed behavior (if present, startup stops and you inspect manually):
~/.openclaw.real/.ocpfs.migrating.json
If you ever need to inspect what got moved, look in the .legacy-openclaw/ directory in the backstore.
Run the supervisor (supervisor entrypoint) which mounts FUSE and starts the gateway.
(Exact command names/flags are in-repo; this README is the single operator entrypoint.)
- KEK (Key Encryption Key): stored in macOS Keychain as a generic password item
- service:
ocprotectfs - account:
kek - value: base64-encoded 32-byte random key (so arbitrary bytes round-trip)
- created automatically by the supervisor if missing (or you can pre-provision it; see TL;DR above)
- service:
- DEKs: per-file, wrapped by KEK and stored in
*.ocpfs.deksidecars in the backstore - Ciphertext: stored in
~/.openclaw.realfor all non-workspace paths
Some bring-up flows use explicit env gates for testing (e.g. allowing gateway access checks). Those are not intended as the final trust boundary; the intended boundary is supervisor/gateway liveness + identity checks enforced at the FUSE layer.
OWASP-oriented hardening notes (PLAN 23):
- All FUSE ops must enforce access checks consistently (no “authz then still do the syscall” footguns).
- Fail-closed by default for encrypted paths.
Known limitations / non-goals:
- The encrypted file implementation currently buffers whole-file plaintext in memory for some operations (large-file DoS potential).
- Metadata is not fully hidden (e.g. directory structure + filenames exist in the backstore).
- Logging favors operator debuggability and may include absolute paths.
- Work is tracked under
tasks/. - Toby authors PRs; Joao reviews (max 2 rounds).
- The default CI workflow runs on ubuntu-latest and macos-latest.
- On GitHub-hosted macOS runners, you generally cannot install/enable the macFUSE kernel extension reliably.
- So CI runs unit-style tests only (real-mount acceptance tests remain opt-in).
- To run real-mount acceptance tests in CI, use a self-hosted macOS runner with macFUSE installed:
- workflow:
.github/workflows/macos-real-mount.yml(manualworkflow_dispatch)
- workflow:
-
wrapper/src/**— supervisor implementation -
wrapper/test/**— unit-style tests for supervisor -
wrapper/acceptance/**— opt-in real-mount acceptance tests (macOS + macFUSE) -
fusefs/src/**— FUSE implementation -
fusefs/test/**— unit/wiring tests for the FUSE layer -
fusefs/acceptance/**— best-effort real-mount acceptance tests (macOS + macFUSE)
npm test
# or:
make testCoverage (Node test runner):
npm run coverage
npm run coverage:checkReal macFUSE mount acceptance tests run automatically on macOS when prerequisites exist.
In CI they are skipped unless explicitly enabled:
CI=1 OCPROTECTFS_RUN_REAL_MOUNT_TESTS=1 npm testWrapper-provided:
OCPROTECTFS_LIVENESS_SOCK: unix socket path created by the supervisor and passed to child processes. Encrypted-path operations fail closed unless this socket is present.
KEK handling (recommended):
- Default (v1): supervisor retrieves/creates KEK from macOS Keychain (
service=ocprotectfs,account=kek). - Opt-in (v2): set
OCPROTECTFS_KEK_V2=1to store a wrapped KEK in Keychain (service=ocprotectfs,account=kek.v2.wrapped) and unwrap it via a Keychain-held non-exportable RSA private key (tag:ocprotectfs.kekwrap.v2).- This avoids persisting the raw KEK as an exportable Keychain secret.
- Migration/rollback: v2 does not read the v1
kekitem; to reset, delete thekek.v2.wrappeditem (and theocprotectfs.kekwrap.v2keypair if desired).
- Wrapper passes KEK to the FUSE daemon in-memory via an anonymous pipe FD (
--kek-fd).
Legacy/testing-only:
OCPROTECTFS_KEK_B64: base64-encoded 32-byte KEK (do not use for production runs)
Wrapper entrypoint:
node wrapper/ocprotectfs.js --helpSmoke-test invocation (mount + encrypt/decrypt behavior, with a dummy gateway that just keeps the supervisor alive).
Preferred: run the Swift FUSE daemon via the Node launcher (default on macOS):
export OCPROTECTFS_FUSE_SWIFT_BIN="$(pwd)/fusefs-swift/.build/debug/ocprotectfs-fuse"
node wrapper/ocprotectfs.js \
--require-fuse-ready \
--fuse-bin "$(command -v node)" \
--fuse-arg "$(pwd)/fusefs/ocprotectfs-fuse.js" \
--gateway-bin /bin/sleep \
--gateway-arg 1000000Real deployment:
- Use the same
--fuse-*flags as above. - Replace
--gateway-bin/--gateway-argwith the command that runs your OpenClaw gateway in the foreground (the supervisor supervises it; if it exits, the supervisor will unmount and fail closed).
To stop and roll back:
- Stop the supervisor process (SIGINT / SIGTERM).
- Roll back using the helper script (recommended):
bash scripts/rollback.sh --restore-layoutOptional: if you want a plaintext export of the protected tree before unmounting (i.e., a full “decrypt” of the view that the FUSE overlay exposes):
bash scripts/rollback.sh --decrypt-to "$HOME/openclaw-plaintext-export" --restore-layoutNotes / safety:
- The rollback script is intentionally conservative: it refuses unsafe paths, refuses to restore layout while the mount is active, and prompts before any destructive moves.
- If anything looks wrong, stop and unmount manually (
umount ~/.openclaw) before attempting directory moves.
- Canonical plan:
tasks/PLAN.md - Current status:
tasks/STATUS.md