
Fifteen years building engineering platforms, currently focused on advanced AI infrastructure at yeet. I love turning the deeply complex topics into something everyone can understand. I relate deeply with the core yeet philosophy that you can just build things.
Quick answer. An AI coding agent runs as you, with your filesystem access, and picks what to read on its own. The real options on Linux are the agent's own sandbox (bubblewrap on Linux, Seatbelt on macOS), a container or VM, Landlock self-confinement, and a BPF LSM hook that refuses each open in the kernel. The important question is not which is strongest but what each one actually covers, because the built-in sandboxes govern shell commands rather than the agent's own file reads, and their default read policy is the whole computer. agent-lock takes the last route: one eBPF program on
lsm/file_openreturns-EPERMfor any path outside your project directory, and the same hook streams every attempt to a dashboard so you can watch what the agent reached for.
I build eBPF tooling at yeet, where thousands of engineers run these scripts against their own machines, and I have spent the last stretch of it writing the kernel side of an agent jail. What I am not is the person who decides your company's policy on running agents against production checkouts. What I have seen, over and over, is the gap between what engineers believe a coding agent can reach and what it can actually reach, and the moment that gap closes is usually the moment someone puts a real audit log in front of them. That is the gap this post is about, and the reason it starts with vendor documentation rather than with a kernel hook.
Yes, by default, and it does not require the agent to be malicious or compromised. A coding agent is a process running under your user account, which means it inherits your user's full filesystem access, and unlike a compiler or a linter it decides what to open based on a model's judgment rather than a fixed dependency graph. The specific evidence is worth quoting because it comes from a vendor's own security documentation rather than from a critic: Claude Code's sandbox docs state that the default read behavior is "read access to the entire computer, except certain denied directories" and add, in the same breath, that "this default still allows reading credential files such as ~/.aws/credentials and ~/.ssh/." That is not a bug report but a deliberate, documented default, and the reason is not hard to infer even though the docs do not state it: a read-restrictive default breaks too many legitimate workflows to ship out of the box.
That is the situation for an agent that ships a sandbox. The floor is lower for the ones that do not, and they are a large and growing share of what people actually run: Aider, Cline, OpenHands, Goose, oh-my-pi, a DeepSeek-based or Qwen-based agent behind a local endpoint, or any of the open-source agents assembled from a model plus a tool loop. Those have your user's full filesystem access with no sandbox layer of any kind, no permission prompts, and no vendor documentation describing what they will and will not touch. When the agent is a few hundred lines of Python around an inference endpoint, the boundary is whatever the operating system gives it, which is everything you can read.
The practical consequence is that "the agent has a sandbox" and "the agent cannot read my keys" are two different claims, and most engineers hold the first while believing the second. The same documentation notes there is "no built-in credential deny list, so only the files and variables you list are restricted," which means credential protection is opt-in configuration you have to write, per path, per machine. Nothing about that is dishonest on the vendor's part, but it does mean the default posture of a freshly installed agent is closer to "trusted user process" than to "confined workload."
Partly, and the part it does not cover is the one that matters here. Its boundary is shell commands and their children, which is narrower than most people picture when they read the word sandbox. Claude Code's documentation is explicit in a section titled Scope: the sandbox "isolates Bash subprocesses," and "Built-in file tools: Read, Edit, and Write use the permission system directly rather than running through the sandbox." Those are two different enforcement mechanisms with two different failure modes. A shell command runs inside an OS-level jail built on bubblewrap on Linux and Seatbelt on macOS, where the kernel refuses what the policy denies. The agent's own file reads run through the permission system, which is application-level logic deciding whether to prompt you, and it is enforcement only insofar as the application keeps asking.
This distinction is the single most useful thing to understand before choosing a confinement strategy, because it tells you where to look for gaps rather than which vendor to trust:
enableWeakerNestedSandbox mode that the docs say "considerably weakens security and should only be used when additional isolation is otherwise enforced."None of this makes the built-in sandbox a bad tool. It makes it a tool with a documented scope, and the honest reading is that it was built to let an agent run shell commands autonomously without prompting you fifty times, which it does well. It was not built to be the thing standing between a model's judgment and your private keys.
Yes, and five ways to do it without building an image or booting a guest: the agent's own sandbox, Landlock, agent-jail, seccomp for the narrow syscall case, and a BPF LSM hook. Six routes once you include containers and VMs, differing less in strength than in setup cost and coverage. The table leads with decision criteria rather than feature lists, because the choice is usually driven by whether you can tolerate an image build or a kernel prerequisite rather than by a security ranking.
| Route | Enforced by | Covers the agent's own reads | Needs a build or boot | Shows you attempts |
|---|---|---|---|---|
| agent-lock (BPF LSM) | Kernel, lsm/file_open, by path | Yes, every open by the process tree | No | Yes, same hook |
| agent-jail (Landlock) | Kernel, by resolved inode | Yes, the process tree | No | No live view |
| Agent's built-in sandbox | Kernel, via bubblewrap or Seatbelt | No, Bash subprocesses only | No | Permission prompts |
| Container or VM | Namespaces and a separate FS view, or a hypervisor | Yes, within the container or guest | Image build or guest boot | No |
| Landlock directly | Kernel, stackable LSM | Yes, if the agent self-restricts | No | No |
| seccomp | Kernel, by syscall number and registers | Cannot express a path rule | No | No |
agent-lock attaches one eBPF program to the lsm/file_open LSM hook, which runs on every file open with the kernel's struct file before the descriptor is handed back. For a jailed process it resolves the real path with bpf_d_path, classifies it, and returns -EPERM to refuse anything outside the jailed directory. Two scheduler tracepoints keep the jailed set correct: sched_process_fork copies membership to each new child so the whole process tree is covered, and sched_process_exit drops a process when it exits. Because the resolved path is what gets checked, the traversal tricks that defeat a naive string filter all collapse to the same forbidden file: ../../etc/passwd, a symlink pointing out of the directory, and the /proc/self/root re-entry trick are one target after the kernel walks it, and all three are refused.
What you give up is breadth. It governs file opens rather than sockets, it reads paths and verdicts rather than contents, and there is no retention or cross-machine view, since the jail is a directory argument to a single run. It is validated on Linux 6.12 on arm64 and needs CONFIG_BPF_LSM=y with bpf in the active LSM list. It is also, plainly, experimental rather than a hardened security product, and the right way to read it is as a kernel-enforced boundary you can watch rather than as a compliance control.
One current limitation deserves more than a clause, because it fails in the direction that should worry you. Confinement is matched by process name, and the shipped wrapper passes a literal --comm omp rather than deriving the name from the binary you handed it. An agent launched under its own name is therefore never enrolled, and an unenrolled agent runs unconfined. What makes that dangerous rather than merely annoying is that the failure looks exactly like success: an empty escape list is what a working jail produces, and it is also what a jail enforcing on nothing produces. So launch your agent under the expected name, and then confirm the match rather than assuming it. --audit showing nothing is the tell, since audit mode reports every decision for an enrolled process and should never be silent. Matching by name also cuts the other way, in that an unrelated process sharing the name gets confined too.
If you are running Claude Code, there is a sandbox already in the binary, it needs no image, and /sandbox will tell you what is missing on your platform. On macOS it uses the built-in Seatbelt framework and needs nothing installed; on Linux and WSL2 it uses bubblewrap plus socat, with an optional seccomp filter that adds Unix domain socket blocking. It does things a filesystem hook does not: it isolates the network by allowed domain, it scrubs credential environment variables from subprocesses, and it protects a carefully chosen list of paths that a command could otherwise use to grant itself permissions, including .claude settings files, .mcp.json, and shell startup files. That last category is genuinely thoughtful security engineering and has no equivalent in a directory-scoped path check.
Use it, and read its Scope section before you rely on it. The boundary is Bash subprocesses; Read, Edit, and Write go through the permission system instead. Reads default to the entire computer including ~/.ssh unless you configure otherwise. Those are the two facts that decide whether it is sufficient on its own for the machine you are on.
A container isolates with namespaces and gives the agent a separate filesystem view, a strong and well-understood boundary. It also solves problems a path check does not touch: dependency isolation, a reproducible environment, resource limits, and a clean teardown where deleting the container takes everything the agent did with it. A VM goes further, putting a hypervisor between the agent and your host, which is the only option here that also contains a kernel exploit. Running an agent against genuinely untrusted code, or on a machine holding credentials you cannot rotate cheaply, justifies that weight.
The cost is the wiring, and it is why people skip it for interactive work. You build or pull an image, mount the project as a volume, then reconcile everything the agent needs from your real environment: git config, registry credentials, language toolchain and caches, editor integration. Each is a decision about what to pass through, and each thing you pass through is a hole in the boundary you just built. That friction is enough that the realistic outcome is often no sandbox at all, which is the failure mode this category exists to prevent. Agent-lock's trade is the inverse: no image and no mounts, because the agent stays in your filesystem and the kernel refuses the paths outside one directory.
Landlock is a stackable LSM built for exactly this shape of problem. Its own description is that it lets you restrict "ambient rights (e.g. global filesystem access) for a set of processes," and its defining property is that it is unprivileged: any process, including an unprivileged one, can securely restrict itself. That is a real advantage over a BPF LSM program, which is privileged to load. Where a BPF LSM hook needs CONFIG_BPF_LSM=y and bpf in the boot-time LSM list, Landlock asks nothing of you but a recent kernel, which makes it the better fit for a tool that has to run on machines it does not control.
The catch for jailing an agent is the word "themselves." Landlock is designed for a process that opts into its own confinement, so using it to jail an agent means either the agent adopts it or you wrap the agent in a launcher that applies a ruleset before exec. It also has no reporting surface: you get a refusal, not a stream of what was refused, so the "watch what it reached for" half of the problem needs separate tooling. If you want the enforcement without the kernel prerequisite, Landlock is the answer; if you want enforcement and the live view from one place, that is the case for the LSM-plus-ring-buffer design.
agent-jail is the Landlock-based predecessor to agent-lock, and it is the more interesting comparison of the two because the difference is mechanism rather than polish. Same directory boundary, same idea of confining an agent's process tree, but it enforces on the resolved inode instead of the path. That distinction is not academic: it means agent-jail does not have the hardlink gap described below, where a link inside the jail pointing at a file outside it resolves to an in-bounds path and is allowed. If an agent deliberately hardlinking its way out is in your threat model, this is the tool that answers it, and being unprivileged Landlock it also runs where BPF LSM is unavailable.
What you trade away is the live view. The whole reason agent-lock puts enforcement in a BPF program is that lsm/file_open is the one place where both "which file is this, really" and "may this process have it" are answerable, so the refusal and the record can come from the same code rather than from a policy engine and a separate auditor that drift apart. Landlock gives you the refusal without the stream. Pick by which half you need more: inode-exact enforcement, or watching the attempts as they happen.
seccomp comes up constantly in agent-sandboxing discussions, and the reason it does not fit is structural rather than a missing feature. seccomp filters syscalls by number and register values, so it can refuse open outright but cannot follow a path: the argument it sees is a pointer to a string in userspace, and the string is not the file. Both ../../etc/passwd and a symlink pass any test seccomp can express. Use it to restrict which syscalls exist at all, which is why container runtimes ship seccomp profiles, and an LSM hook when the question is which paths a syscall may touch. AppArmor and SELinux fit when you need a persistent system-wide policy rather than a per-session jail.
Pick the layer where the answer is a refusal rather than a decision, then make the rule about the directory the agent is allowed into rather than the directories it is not. Deny-listing sensitive paths is intuitive and weaker, because a deny list is only as complete as your imagination: you block ~/.ssh and ~/.aws, and the agent reads ~/.kube/config, ~/.netrc, ~/.gnupg, the shell history holding a production connection string, or the .env in a sibling repo. Every one is a separate line you had to think of in advance.
Allow-listing inverts the problem in your favor. If enforcement says "this process tree may open paths under ~/project, plus the loader and library paths programs need to start, and nothing else," then whatever you failed to anticipate is refused by default rather than permitted by default. This is what agent-lock implements: the BPF program classifies every open by a jailed process as in-bounds, a permitted system path, or an escape, and returns -EPERM for the escape. You never enumerate ~/.ssh at all, because it is outside ~/project and that is the whole rule. Sensitive targets still get a flame marker in the dashboard, but that is presentation on top of a refusal that already happened.
You need something that reports every open rather than only the ones a policy stopped, and the two questions turn out to be answerable by the same code. In agent-lock the lsm/file_open hook does both jobs in one pass: it decides the open, returning -EPERM for an escape and 0 otherwise, and it emits the decision, meaning the path, the process name, and whether it was blocked, into a ring buffer the dashboard reads. Enforcement and observation are the same kernel code rather than two mechanisms kept in sync, which matters more than it sounds like it should. When the counter says 37 escape attempts blocked, that number came from the code that did the blocking, so there is no drift between what the log claims and what the kernel actually did.
The dashboard puts the in-bounds and blocked counts in the same bar for the same reason. Watching only the blocked stream tells you the jail fires; watching the split tells you the agent is doing real work inside the directory while escapes are refused, which is what you want to know before trusting it on a real repo. --audit runs the agent unconfined and reports what it reaches for, so you learn what a policy would have broken before turning it on rather than debugging a mysterious agent failure at 2am.
When you need an isolated environment rather than a path boundary, which comes down to five situations worth checking before you install anything. Reach for a container, a VM, or a different tool when:
lsm/file_open governs opens, not sockets. The agent's calls to its model provider keep working, which is what makes it usable, and by the same token nothing here prevents exfiltration. Pair it with a network namespace or a firewall, or use a sandbox with domain allow-listing.CONFIG_BPF_LSM=y and bpf in /sys/kernel/security/lsm, which is set at boot via the lsm= parameter. Without it the program does not attach, and there is deliberately no unconfined fallback, so it fails to load rather than quietly running your agent unprotected. On such a machine, Landlock or a container is the route.c copying a session summary is the whole export story. Cross-machine questions and reboot-surviving policy belong to AppArmor, SELinux, or your existing tooling.One more caveat belongs here rather than in a footnote. Enrollment fires on a process's first open, so a handful of the agent's earliest opens precede it. For an interactive agent the reaches that matter happen during a session, not in the first millisecond, but if your threat model is a binary going for your keys immediately on exec, that gap is real.
Yes, provided the sandbox follows the process tree, and this is what separates a working agent jail from a leaky one. Agents do most of their real work through child processes: a cat to read a file, a git to check history, a grep to search a tree, a language server, a test runner. If confinement keys only on the agent's own process, every child is a way out, and the leak looks ordinary in a process listing. Agent-lock handles it by propagating jail membership at sched_process_fork, copying the tgid into the jailed map when a new child appears, so a cat the agent shells out to is covered even though its process name is not the agent's. Membership is inherited rather than re-derived, which is why a whole tree stays covered without anyone enumerating what the agent might spawn.
The visible consequence is that a child reaching outside gets refused like anything else. A git that wants ~/.gitconfig is an escape, because ~/.gitconfig is not under your project directory, and the dashboard attributes the blocked open to git rather than to the agent. That attribution is what makes the escape list readable at all: you see which process in the tree went for the file, not just that something did. It also means a system allow-list is not optional but load-bearing, since the loader, library, and locale reads every program does at startup have to keep working or nothing the agent spawns can start. Benign /usr, /lib, and /tmp reads are permitted and kept off the escape list, so a real reach at your credentials is not buried under a thousand library lookups.
A symlink, no. A hardlink, yes, and that asymmetry is the most important thing to understand about path-based enforcement. The reason is that a path rule is only as good as the string it compares, and the kernel is the only participant that knows what a path really points at. Consider the ways a process inside ~/project can name /etc/passwd without ever typing it: ../../etc/passwd walks out with relative components, a symlink placed inside the project points wherever its target says, and /proc/self/root re-enters the filesystem from the process's own root and gives you a second spelling for everything. A filter comparing the string it was handed against a prefix passes all three, and the failure is silent, which is the worst property a security check can have.
Agent-lock reads the file's resolved path with bpf_d_path, which is the real target after the kernel has walked symlinks and .. and reached a struct file. Every one of those three spellings arrives at the same resolved path, so all three are refused by the same comparison and none of them needs a special case. This is also why the enforcement point matters as much as the policy: at lsm/file_open the kernel has already done the resolution work, so the program is checking a fact rather than parsing an intention.
There is one case path resolution does not close, and it is the honest limit of this whole approach. A hardlink inside the jail pointing at a file outside it is allowed. A hardlink is a genuine directory entry under the jailed directory, so bpf_d_path resolves the open to a path that really is in-bounds, and a prefix comparison against that path has no way to know the inode is shared with something outside. Closing it requires checking the inode rather than the path. The project's own breakout self-test, which you run with make adversary, exercises this case explicitly and reports it as [known] rather than passing it off as covered, which is the right way to hold a limitation like this one. If your threat model includes an agent deliberately hardlinking its way out, the sibling tool agent-jail takes the Landlock route and enforces on the resolved inode, so it does not have this gap, at the cost of the live in-kernel view.
Hand the job to the agent you already have open. The setup is a clone, a kernel check, a build and a self-test, which is the kind of errand a coding agent does well. Paste this into Claude Code, Codex, Aider, Cline, oh-my-pi, or anything else with shell access:
Clone https://github.com/yeet-src/agent-lock and work in it.
Read AGENTS.md first, then:
1. Install yeet if it isn't present: curl -fsSL https://yeet.cx | sh
2. Check this kernel can enforce at all, before building anything:
cat /sys/kernel/security/lsm # must contain "bpf"
If "bpf" is absent, STOP and report it: the program cannot attach, and
no amount of building will change that. It is set at boot via the lsm=
kernel parameter, not at runtime.
3. Run `make` and confirm bin/probe.bpf.o was produced.
4. Prove the jail holds, which is not the same as "it built":
sudo make adversary
Expect 0 leaks and exit 0. One line reports a [known] hardlink
limitation; that is expected and is not counted as a leak.
5. Run the demo so there is something on screen without a real agent:
sudo ./scripts/demo.sh
Confirm the escape leaderboard fills with blocked reaches at ~/.ssh
and ~/.aws, then press q.
"It compiled" is not the same as "it works", and for a containment tool
that gap is the whole point: report step 4's leak count, not just that
make exited 0.
The last instruction is the one to keep if you rewrite the rest. An agent left to its own judgment reports success when make exits 0, and for a containment tool that is the wrong bar, since a program that compiles and attaches and enforces nothing looks identical from outside. Asking for the leak count forces the check that matters, and the [known] hardlink line coming back is how you confirm the self-test ran honestly rather than telling you what you wanted to hear. Step 2 is first for the same reason: cat /sys/kernel/security/lsm has to include bpf, and a kernel compiled with CONFIG_BPF_LSM=y can still fail to run this because the active list is set at boot by the lsm= parameter, so adding it costs a reboot rather than a config edit.
By hand it is four commands, ending in a jailed agent with a dashboard above it:
curl -fsSL https://yeet.cx | sh # install the yeet daemon, once
git clone https://github.com/yeet-src/agent-lock && cd agent-lock
make # bin/probe.bpf.o + the JS bundle
sudo ./scripts/agent-lock ~/project # jail your agent, watch it live
Two details will otherwise cost you an afternoon. The launched binary needs to be named omp, because the wrapper passes --comm omp, and sudo must be sudo -E or OMP is dropped from the environment and you silently get the default. Driving the script directly avoids the name constraint entirely, since the process name is just an argument and the daemon handles the privileged load: yeet run . -- --dir ~/project --comm claude --mode jail, with --dir absolute. Start with --audit on a repo you have not confined before, and use --headless for unattended runs, which writes one JSON record per escape to stdout, de-duplicated per path with a rollup every five seconds.
This is a yeet script, not a binary, so the policy is readable JavaScript over a small BPF object rather than something you configure through flags. The edits people actually want are local: lib/classify.js holds the sensitive-target list if you want your own credential shapes flagged, lib/report.js shapes the headless records your log pipeline receives, and the system allow-list lives in the BPF file. One rule to respect: userspace does not re-decide anything, because a second classifier in JS could disagree with the kernel, and a dashboard that disagrees with the enforcer is worse than no dashboard. The runtime underneath is yeet, so if you need a different hook or a different boundary, point your agent at the yeet docs and describe the tool you want.
Every route on this list is a real answer to a real version of the question, and the mistake is not picking the wrong one but assuming the one you have covers more than it does. If you run Claude Code, its built-in sandbox on bubblewrap and Seatbelt is already there and worth turning on, and you should read its Scope section and know that it governs Bash subprocesses while reads default to the entire computer including ~/.ssh. If you are running an agent against untrusted code or need a reproducible environment, use a container or a VM and pay the setup cost deliberately. If you need enforcement on a machine where you cannot load BPF, Landlock is unprivileged and made for this, and agent-jail is the ready-made tool that takes that route and closes the hardlink gap by enforcing on the inode. If what you want is the kernel refusing every open outside one directory for the agent and everything it spawns, while you watch the attempts as they happen, that is agent-lock: one eBPF program on lsm/file_open, -EPERM for anything outside the jail, bpf_d_path so the traversal tricks resolve to the same refusal, and one ring buffer feeding a dashboard. Confirm your agent's process name enrolled before you trust it, because an unenrolled agent runs unconfined and looks the same as a quiet one. It is built on yeet, so if the ready-made shape is wrong for you, the runtime underneath is the one these programs are written in.
The part worth carrying away is smaller than the tooling. An agent decides what to read, which makes "what did it try to open" a question you should be able to answer on any machine where you run one, and most engineers currently cannot answer it at all.
By default yes, on every agent that does not ship a sandbox and on some that do. An agent runs as your user with your filesystem access and chooses what to open. At least one major vendor documents that its sandbox default grants read access to the entire computer, explicitly including credential files such as ~/.aws/credentials and ~/.ssh/.
It depends on the mechanism. Landlock is designed for unprivileged use, so any process can restrict itself with no root and no BPF at all. Loading a BPF LSM program is privileged by contrast, so a tool built that way needs either root or a daemon holding the privilege on its behalf.
A kernel LSM hook, because the decision happens in kernel context on each open with no extra process and nothing to boot. The work per open is a bounded path comparison, and a well-built program drops permitted reads in the kernel before emitting anything so the cost scales with decisions rather than with total filesystem activity.
Yes. Landlock, a BPF LSM program, seccomp, and an agent's own built-in sandbox all confine a process in place with no image to build and no guest to boot. Containers and VMs are the right answer when you need an isolated environment rather than a path boundary, since they also give you dependency isolation and clean teardown.
An eBPF program attached to a Linux Security Module hook, so the kernel calls your code at a security decision point such as a file open. The program returns 0 to allow the operation or a negative errno such as -EPERM to deny it. It needs CONFIG_BPF_LSM=y and bpf present in the active LSM list at /sys/kernel/security/lsm.
No, and the reason is structural rather than a missing feature. seccomp filters syscalls by number and register values, and the path argument it sees is a pointer to a string in userspace rather than the file itself. Both ../../etc/passwd and a symlink pass any test it can express, so path rules need an LSM hook instead.
No. A hook on file opens governs the filesystem and says nothing about sockets, which is why a confined agent keeps talking to its model provider normally and could exfiltrate over that same socket. Pair a filesystem boundary with a network namespace or a firewall if egress matters.
A symlink, no, when enforcement checks the resolved path: the kernel walks the link first, so the target is what gets compared. A hardlink is different. It is a real directory entry inside the jail pointing at an inode outside it, so a path check sees an in-bounds path and allows the open. Closing that gap needs enforcement on the inode rather than the path.
Only if membership propagates at fork, and this is where confinement most often leaks. A jail that keys on one process name lets every child escape, so enrollment has to be copied at the fork tracepoint to cover a git, cat, node or grep the program spawns. Most of an agent's file opens come from children rather than the parent binary.
They are all Linux Security Modules, so they hook the same decision points, but the authoring model differs. The older modules load policy files into a fixed module, are usually managed distribution-wide, and persist across reboots. A BPF LSM program is code instead, so it can consult maps, resolve paths and emit events, which suits a per-session jail you also want to watch.
It narrows the blast radius rather than preventing the injection. If injected instructions tell an agent to read ~/.ssh/id_rsa, a kernel hook refuses the open regardless of why the agent asked. If they tell it to send your project source to an endpoint, a filesystem boundary has nothing to say about that.
Run it in an observe-only mode first and read the list. agent-lock's --audit flag runs the agent unconfined while the same hook classifies and reports every decision, so you learn what a policy would have blocked before you turn it on. That is how you find the legitimate outside reads a toolchain makes.
~/.aws/credentials and ~/.ssh/," the Scope section stating the sandbox "isolates Bash subprocesses" while Read, Edit, and Write "use the permission system directly," the bubblewrap and Seatbelt mechanisms, the Ubuntu 24.04 AppArmor prerequisite, and the enableWeakerNestedSandbox warning.-EPERM denies an operation and 0 allows it, and the framing of BPF LSM as a way to implement "system-wide MAC and Audit policies using eBPF."bpf in its active LSM list, so Landlock is the route on a machine you do not control. It also enforces on the resolved inode, which is why the hardlink case this post describes does not affect it.lsm/file_open enforcer and emitter, bpf_d_path path resolution, the sched_process_fork and sched_process_exit tracepoints that maintain the jailed set, the jailed hash map keyed on tgid, the --audit, --headless and --comm flags, the make adversary breakout self-test, the seccomp-cannot-follow-a-path argument, and the stated requirements and limits.[known] by the self-test, the wrapper's hardcoded --comm omp and the unconfined-agent failure mode that looks like success, the pre-enrollment opens, and the one-host-no-history scope.file_open is one. It is the framework SELinux, AppArmor and Landlock all implement, which is why comparing them is a question about authoring model and scope rather than about which one is strongest at blocking a file open.--comm flag, the sudo -E requirement, and how to confirm the name actually enrolled.Built with yeet, a JS runtime for writing eBPF programs on Linux machines. Join us on discord.