Show HN: Bulwark – a kernel read gate so coding agents can't read .env or .ssh Bulwark, an OS-level read gate for AI coding agents, blocks file reads at the kernel level by inode before bytes reach the agent, preventing access to protected files like .env or .ssh. It uses fanotify on Linux and Endpoint Security on macOS to enforce deny, allow, or consent policies, ensuring security boundaries are structural rather than prompt-dependent. Gate an agent's file reads at the OS, by inode, before the bytes reach it. Bulwark is an OS-level read gate for running AI coding agents on a developer machine. Launch an agent under it; when any process in its tree tries to open a protected file, Bulwark applies a policy before the bytes reach the agent: deny, allow, or ask for consent. On Linux it uses fanotify permission events; on macOS, Endpoint Security. Hardened mode adds a Landlock floor so protected paths stay denied even if the userspace gate dies. Run an agent, but deny it any read of your SSH keys. brew install obstalabs/tap/bulwark sudo bulwark run --protect ~/.ssh -- claude the agent works normally; a read of a protected file - Operation not permitted macOS note: sudo resets PATH, so on Homebrew/Apple Silicon use the full form, which also keeps the agent's own runtime e.g. node on PATH: sudo env "PATH=$PATH" "$ which bulwark " run --protect ~/.ssh -- claude It supervises a process tree, installs a fanotify FAN OPEN PERM mark, and decides each open by the file's inode , not its path string. A protected inode opened by the supervised tree is denied; the reader gets EPERM . Every decision is logged with the process ancestry that caused it. The point is that the boundary is an OS-mediated checkpoint, not a rule in a prompt the agent can be talked past. Not redaction. It does not scrub or obfuscate secrets out of content. That is NeuroRouter https://neurorouter.dev 's job. It stops the open from happening at all. Not an authority/approval system. It does not decide who may act. That is Verdict's job. Not a network or routing gate. It does not see or stop exfiltration over the wire. That is NeuroRouter https://neurorouter.dev 's job. Not protection for secrets already inside the allowed workspace. Bulwark bounds what the tree can reach , not what it already holds. Not protection against an unwrapped process. Only the tree launched under bulwark run is gated — including work the agent delegates to a separate daemon. An agent that calls docker run hands the read to dockerd a different tree , so it is not gated; the standard rule applies — don't give a confined agent a root-equivalent socket. The full tested boundary is in docs/containment-boundaries.md /obstalabs/bulwark/blob/main/docs/containment-boundaries.md . One mechanism — gate the open — with a few policies layered over it deny-list, allow-list, consent, a crash-safe hardened floor . Principiis obsta — resist the beginning. A security boundary must not depend on a prompt or a guess. Bulwark prevents the dangerous condition structurally rather than detecting it after the fact: the kernel is the evidence source, not the agent's self-report; the decision is deterministic, not probabilistic; the identity is the inode, not a string a symlink can forge. Linux fanotify and macOS Endpoint Security . The gate needs root — CAP SYS ADMIN for fanotify on Linux, root + Full Disk Access on macOS why, and how to set it up . Prebuilt binaries: brew install obstalabs/tap/bulwark or the. Releases page cargo build --release Deny the supervised command any read of files under ~/.ssh, by inode. sudo ./target/release/bulwark run \ --protect ~/.ssh \ --receipts /tmp/bulwark-receipts.jsonl \ -- bash -c 'cat ~/.ssh/id ed25519' - cat: Permission denied A benign-named symlink to a protected file is still denied — the decision is by inode, so the name cannot lie. bulwark run --protect