
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.
Last updated: September 2026
Quick answer. There are four real options and they are not ranked by strength, because each one grants something different. An SSH account with a shell is the fastest to set up and gives the agent everything the account has. A read-only user with
sudorules sounds narrower and mostly is not, becausecapabilities(7)putsprocess_vm_writevinsideCAP_SYS_PTRACEand sudo's own manual calls command allowlists "often unworkable". A container alongside the workload bounds the filesystem and nothing else. A daemon that answers typed queries hands over no login at all, and cannot answer a question nobody wrote a probe for. Pick by what the agent needs to read, not by which sounds safest.
I build kernel-side tooling for Linux and I watch people wire agents into infrastructure, which means I mostly see this decision made in a hurry, during an incident, by someone who wants the answer more than they want the access review. I do not run your production and I have not audited your sudoers file. What I keep seeing is that the option everybody reaches for second, a read-only account, is chosen because it sounds like a smaller grant than the first one, and the Linux permission model does not actually work that way.
Enough to read process state, file contents and network state, which on Linux is a wider grant than it sounds. The specific things an agent asks for when diagnosing a host are process listings with command lines, memory and CPU accounting per process, open file descriptors, listening sockets, log files, and config files. Most of that is readable by an unprivileged user for its own processes and not for anyone else's.
The moment the question is about another user's process, and on a production box it always is, the agent needs to cross a privilege boundary. That is the whole problem in one sentence. There is no "read another process's memory" permission separate from the permission to write it, and there is no "read every log" permission that does not also read every secret.
This is why the four options below differ in kind rather than in degree. They are not four settings on a dial. They are four different answers to "who holds the privilege", and the right one depends on which questions the agent has to answer. Worth listing those questions before reading further, because the exercise is short and it usually changes the answer: write down the last five things you actually needed to know during an incident. Most lists come back heavy on listening sockets, process trees, recent log lines and disk usage, and light on anything that needs to read another process's address space. A grant sized for the real list is much narrower than one sized for the hardest question anybody can imagine.
It is the fastest and it is not narrow. An SSH account with a shell grants the agent everything that account can do, including everything it can escalate to, and the agent decides what to run while it runs rather than at the time you granted access.
Worth being fair to it: a shell is genuinely the most capable option, and during an incident capability is what you want. An agent with a shell can follow a hypothesis wherever it leads, run a command nobody anticipated, and read a file nobody thought to expose. Every narrower option below buys safety by giving that up, and sometimes that trade is wrong.
What makes it a poor default is the combination of breadth and durability. The account persists after the incident, and the incident is what justified it, so the justification expires and the access does not. The key sits in the agent's environment, where any process running as that agent can read it, which means the blast radius is not the agent but anything that can reach the agent. And the record of what happened is whatever the agent chose to report, because its own transcript stops at the shell boundary. Those three properties compound: broad access, held indefinitely, with no independent account of how it was used.
Less than the name promises, and the manual pages say so directly. Two mechanisms are usually combined here, and both leak in documented ways.
The first is Linux capabilities, granted so the agent can inspect processes it does not own. capabilities(7) describes CAP_SYS_PTRACE as permitting the holder to:
"transfer data to or from the memory of arbitrary processes using
process_vm_readv(2)andprocess_vm_writev(2)"
That is process_vm_writev. The capability granted so an agent can read another process's memory also lets it write another process's memory. The same page describes CAP_DAC_READ_SEARCH as the power to "bypass file read permission checks and directory read and execute permission checks", which is every credential file on the box, including the ones you carefully denied elsewhere.
The second mechanism is a sudo allowlist of safe-looking commands. The sudoers manual is blunt about what that buys:
"Once sudo executes a program, that program is free to do whatever it pleases, including run other programs. This can be a security issue since it is not uncommon for a program to allow shell escapes, which lets a user bypass sudo's access control and logging."
And on the approach of listing only safe programs: "Due to the large number of programs that offer shell escapes, restricting users to the set of programs that do not is often unworkable."
Sudo ships NOEXEC and INTERCEPT tags precisely because the naive allowlist does not hold, and both are worth reaching for if you take this route. NOEXEC stops a dynamically-linked program from running further commands; INTERCEPT validates every command a permitted program spawns against the same sudoers rules. Neither is universally supported, which the manual is explicit about, so check your platform rather than assuming. The deeper point is that the mitigations exist because the category is leaky, and a team adopting the read-only model without them has adopted the name and not the protection. The name is the part to distrust, not the technique.
Use one when the agent is running code you did not write, and do not mistake it for access control. A container gives you a reproducible environment and a filesystem boundary. It does nothing about credentials you mounted, network routes you left open, or a socket you bind-mounted so the agent could reach the thing it needed to inspect.
The specific failure with inspection work is that the useful version defeats the isolation. An agent in a container cannot see the host's processes, which is usually the entire point, so people add --pid=host. Then they add --privileged or CAP_SYS_PTRACE so it can read those processes. At that point the container is a packaging choice, not a boundary, and the honest description is a root process with a convenient filesystem.
Containers are the right answer for a different question: running an untrusted tool the agent needs, with a bounded filesystem, where you do not need host visibility. That is a real and common case, and it is worth keeping the two apart rather than concluding containers are theatre. The distinction is whether the container is holding something in or letting something look out. The layer-by-layer confinement comparison covers where each boundary actually holds.
Yes, by inverting who holds the privilege: a daemon on the host does the privileged work and exposes a typed read interface, and the agent submits queries instead of commands. The agent holds no account, no key and no capability. It asks a question and receives structured data.
This is what yeet is shaped like. A single daemon holds the privilege and the system graph is the read surface: processes with command lines, CPU time, resident memory, disk I/O and scheduler run delay; per-core CPU; memory; network interfaces; containers. A caller gets a typed answer to a specific question rather than a shell that can ask anything.
The honest limit, and it is a real one: this surface answers the questions somebody built it to answer. A shell can chase a hypothesis nobody anticipated, and a query interface cannot. That is the trade this option makes, and if your incidents routinely need the unanticipated question, a narrower interface will frustrate you. That is the trade, and it is the reason this option is a complement to a break-glass account rather than a replacement for one.
The daemon if the questions are routine, SSH if they are not, and the middle two only when something specific about your setup calls for them. The table is sorted that way deliberately, least granted first:
| Model | What the agent holds | Good for | Does not cover |
|---|---|---|---|
| Typed read interface on yeet | Nothing. Submits queries | Routine diagnosis, fleets, untrusted agents | Questions nobody wrote a probe for |
Read-only user + sudo rules | An account, capabilities, an allowlist | Teams with an existing sudoers discipline | Shell escapes; CAP_SYS_PTRACE includes writes |
| Container with host PID | Whatever you granted it | Running untrusted tooling | Becomes root-equivalent once it can see host processes |
| SSH account with a shell | Everything the account can reach | Incidents needing the unanticipated question | Persists after the incident; no independent record |
Read that table by the second column rather than the third. The question is not which row is safest in the abstract, it is which row grants the least while still answering the questions you actually get asked. Two rows can also be combined, and the combination is usually better than either alone: a read interface for the routine questions, with a break-glass shell account that exists only during an incident and is removed with it. That shape gives the common case a narrow grant and keeps the escape hatch for the case that needs it, which is roughly how on-call access already works for humans at most companies.
From below the agent, because every other record is written by something the agent participates in. This applies to all four models above and it is the part most often skipped, since access review feels like the whole job and the audit trail feels like paperwork until you need it.
An agent's own transcript records the tool calls its framework mediated. Anything a spawned process does afterwards is outside it, and Anthropic documents the mechanism plainly: OTEL_* environment variables are not passed to the subprocesses the Bash tool spawns. One Bash call is one event whatever the process tree underneath it did.
The kernel has no such gap. auditd with an execve rule is the right answer when the record must survive a reboot and your compliance team already reads it. For a scoped view of one agent's process tree right now:
curl -fsSL https://yeet.cx | sh
yeet login
yeet run gh:yeet-src/exectop -- --pid $(pgrep -n claude)
exectop folds every command that tree executed into one row per kind. Attaching to a running pid cannot see children forked before you attached, which the tool states rather than papering over.
Work out which questions the agent has to answer before you decide what to grant, because all four models are correct for some version of this and none is correct for all of it. If the agent needs to chase hypotheses nobody anticipated during a live incident, give it a shell, scope the account, and know you are trading control for capability. If your team already runs a disciplined sudoers file, a read-only user with NOEXEC and INTERCEPT tags is a real improvement over a plain account, as long as nobody believes CAP_SYS_PTRACE is read-only. If the agent runs untrusted tooling and does not need host visibility, use a container and keep it that way. If the questions are the routine ones, a typed read interface on yeet grants the least of any option here, and whichever you pick, keep a record from below with auditd or exectop.
The failure mode worth naming is picking the second option because it sounds like the smaller grant. A read-only account is not a smaller version of a shell. It is a different set of powers, some of which are larger than the ones you were worried about, and two manual pages say so in plain language.
Only when the agent needs to answer questions nobody anticipated, and then with a scoped account that is removed afterwards. An SSH account with a shell grants everything that account can reach, decided by the agent while it runs rather than by you when you granted it, and it persists after the incident that justified it.
Safer than an unrestricted account and not as narrow as the name suggests. The capabilities that let an agent inspect other processes include writing to their memory, and sudo command allowlists are defeated by shell escapes in any pager, editor or interpreter on the list. Sudo's own manual calls restricting users to programs without shell escapes "often unworkable".
It is the Linux capability that permits tracing arbitrary processes and inspecting their memory. It matters because capabilities(7) lists process_vm_writev among its powers, so the capability commonly granted for read-only process inspection also permits writing to the memory of any process on the host.
For its own processes, yes. For other users' processes, not without crossing a privilege boundary, because Linux does not offer a read-only version of that boundary. The alternatives are to grant the capability and accept what comes with it, or to have a privileged daemon answer the questions and hand the agent structured data instead of access.
It bounds the filesystem and nothing you deliberately passed in. For inspection work the isolation usually gets removed to make the agent useful, because seeing host processes requires --pid=host and reading them requires added capabilities. A container with both is a packaging choice rather than a security boundary.
With a record the agent did not write. Its transcript stops where the framework's mediation stops, and Anthropic documents that OTEL_* is not passed to Bash subprocesses. auditd with an execve rule gives a durable record, and a kernel-side probe scoped to the agent's process tree gives an immediate one.
It depends entirely on the question, which is why a single answer does not exist. Reading listening sockets needs less than reading another process's memory, and reading logs needs less than both. The useful exercise is listing the questions you actually get asked during incidents and granting for those, rather than granting for the hardest question you can imagine.
You can, and the sudoers manual warns that it works less well than it looks, because programs that permit shell escapes let a user bypass sudo's access control and logging. If you take this route, use the NOEXEC and INTERCEPT tags, which exist for exactly this problem, and check whether your platform supports them.
Standing access is the part worth questioning more than the access itself. An agent that holds a credential permanently is a credential that can be used at any time by anything running as that agent, whereas access granted for one task and revoked afterwards bounds the window. The practical version is short-lived credentials and an interface that can be turned off.
Metrics are a summary somebody chose to collect in advance, and a host is the source those summaries came from. An agent restricted to metrics cannot answer a question nobody anticipated, and an agent with host access can answer almost anything and can also do almost anything. The four models in this post are different points on that trade.
CAP_SYS_PTRACE permitting data transfer "to or from the memory of arbitrary processes using process_vm_readv(2) and process_vm_writev(2)", and for CAP_DAC_READ_SEARCH bypassing file read and directory read and execute permission checks. Read this before granting either to an agent.NOEXEC and INTERCEPT tags that exist to mitigate it. The Preventing shell escapes section is the relevant part.OTEL_* stops at Bash subprocesses, which is why the audit record has to come from elsewhere.lsm/file_open that refuses opens outside a named directory and streams every attempt it refused, for when the agent needs to be confined rather than merely observed.Built with yeet, a JS runtime for writing eBPF programs on Linux machines. Join us on discord.