How Do I Control What an AI Agent Can Do?

Necco Ceresani
Necco Ceresani
·11 min read

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: October 2026

Quick answer. You cannot write policy for actions you cannot see, so the order is observe, then decide, then enforce. Most teams skip the first step and end up with permission rules written from guesswork, which either block legitimate work or permit the thing they were meant to stop. Start by recording what the agent actually does on a representative session: yeet run gh:yeet-src/exectop for every process it launches and yeet run gh:yeet-src/dialout for every host it dials. Then write rules against that record. The enforcement layer you reach for depends on what you are bounding, and the three boundaries (filesystem, process, network) are enforced by three different mechanisms that do not substitute for each other.

I build eBPF tooling at yeet, which means the agent-control conversations that reach me are usually a measurement problem wearing a policy problem's clothes. Someone wants to restrict an agent and cannot say what it currently does, so every proposed rule is a guess with a blast radius.

Why can't I just write a permission list for my AI agent?

Because a permission list is a prediction about behaviour, and an agent's behaviour is decided at runtime by a model rather than by a dependency graph you can read ahead of time. Everything awkward about agent policy follows from that one difference.

A conventional program has an enumerable set of things it touches. You can read the source, list the files it opens and the hosts it dials, and write a policy that matches. Claude Code's permission rules are expressive enough for that shape: Bash(npm run test *) in an allow list, Read(./.env) in a deny list. An agent decides what to run based on its judgment about a prompt, so the set of commands it might execute is not knowable in advance, and the set of files it might read is bounded only by what your user can read.

The consequence shows up in two failure modes, both common. A rule written too tightly blocks legitimate work, the agent fails in a way that looks like a bug, and someone widens the rule under time pressure until it stops mattering. A rule written too loosely passes every test you thought to run and does not cover the case you were worried about.

Both come from the same root: the policy was written before anyone measured what the agent does. The configuration layer has its own traps on top of that, such as allow and deny lists merging across scopes rather than overriding, so a rule you added locally does not replace the one your team committed. The two threat models worth separating make the point from the other direction, which is that what you are defending against determines which boundary matters.

How do I record what an AI coding agent actually does?

Capture a representative session at the kernel, where the record does not depend on the agent reporting accurately on itself. Two captures answer most of what a policy needs, and neither requires configuring anything on the agent's side or trusting what it writes down.

Two captures cover most of what a policy needs to know. Process launches tell you what it executed, including the subprocesses a build or test command spawned that nobody enumerated:

curl -fsSL https://yeet.cx | sh         # install yeet, once
yeet run gh:yeet-src/exectop            # every program the agent starts

Outbound connections tell you where it reached, with the process attached, which separates the agent's own traffic from an MCP server's:

yeet run gh:yeet-src/dialout            # every outbound connection, by process

The reason to read these at the kernel rather than from the agent's transcript is that a transcript records decisions rather than actions. It lists the tool calls the agent chose to make; it does not list what those tools did underneath. The gap between the two is exactly where the interesting entries live, because the things nobody chose deliberately are the things no policy anticipated.

Run this across work that resembles your real workload rather than a toy task, because the baseline is only as good as the session that produced it. A capture taken while the agent edits a few files produces a record that makes a file-only policy look entirely sufficient, right up until the first time it runs a build.

What should a Claude Code or Codex policy actually restrict?

Three boundaries, enforced by three entirely different mechanisms, and treating them as a single control is the mistake that reliably produces a policy with a hole in it. They are worth naming separately because a rule written for one says nothing about the other two.

BoundaryWhat it boundsEnforced by
FilesystemWhich paths the agent can read and writeThe agent's sandbox, a container, Landlock, or a BPF LSM hook
ProcessWhat it can execute, and as whomThe sandbox's subprocess scope, plus the user it runs as
NetworkWhich hosts it can reachHost egress rules or a namespace, not the agent's sandbox config

The network row is the one that catches people. An agent sandbox typically governs the commands the agent runs, while the agent's own model requests, MCP connections and connector calls originate in the parent process outside that boundary. Codex's sandbox is a worked example: network_access = false is real and kernel-enforced, and it covers subprocesses rather than the agent.

So a policy expressed entirely in the agent's own configuration bounds its children and leaves the agent itself untouched. If the concern is an injected instruction acting through the agent's tools, that threat needs an outer boundary such as a namespace with default-deny egress, rather than a mode setting.

How do I find unapproved MCP servers on my machine?

Look for the processes, because an MCP server started by an agent is an ordinary process on your host rather than a hidden component of the agent. That makes this a far more tractable question than it first appears.

A server launched by the agent appears in a process-launch capture under its own command name, which is usually enough to recognise it. From there the useful questions are what it connects to and whether anyone approved it:

yeet run gh:yeet-src/exectop            # catches the server being started
yeet run gh:yeet-src/dialout            # catches where it reaches afterwards

One case is worth knowing because it looks like absence of evidence. A server communicating with the agent over a Unix socket produces no TCP for that leg, so a connection capture shows only its outbound traffic and not its conversation with the agent. And a server implemented as a library inside the agent process has no separate PID at all, so its connections attribute to the agent. Auditing a local MCP server covers the shapes in more detail.

The practical control here is inventory rather than enforcement. Knowing which servers are configured, and noticing when a new one appears, is most of the value, because an MCP server is a piece of software you installed and the question is whether you meant to.

Can I enforce AI agent policy on Linux without blocking the agent?

Yes, by separating detection from prevention and accepting that most of the practical value sits in the first of the two. This ordering is unintuitive, because prevention sounds like the serious control and detection sounds like a consolation prize.

Prevention is expensive to get right, because every rule risks breaking legitimate work and the failure is immediate and visible. Detection is cheap, because a record of an unexpected action costs nothing when nothing unexpected happens. A reasonable progression:

  1. Record first, enforce nothing. Build the baseline. This is safe and it is the input every later step needs.
  2. Deny the small set that is never legitimate. Link-local and cloud metadata, private ranges the agent has no business reaching, credential paths. Short lists, low breakage risk.
  3. Alert on departures from the baseline. A new destination or an unfamiliar binary is a question rather than an incident, and it is the signal a tight policy would have given you without the breakage.
  4. Tighten selectively, where the cost of being wrong is high. A production host earns a stricter policy than a development laptop.

What makes this work is that step one is not a delay before the real work. The record is what tells you which rules in steps two through four are worth writing at all.

Does an AI agent audit log prove what the agent did?

It depends entirely on who wrote it, and that distinction matters more for agents than it does for ordinary software. A log written by a deterministic program is a reasonable proxy for what the program did; a log written by a model is a record of what it believed it was doing.

An agent's own transcript is written by the thing being audited. For most purposes that is fine and it is the most readable account of intent you will get. For the question "what did this process do", it is the wrong instrument, because it records decisions rather than syscalls and it cannot record what it did not know about.

A kernel-side record has the opposite properties. It is unreadable as narrative, it has no idea why anything happened, and it does not depend on the agent's cooperation or its awareness. Auditing what an agent ran goes through the mechanics; the framing worth keeping is that the two records answer different questions and a control story needs both.

Measure the agent on Linux first, then write the rule

Agent control fails most often at the first step rather than the last. A policy written from assumptions about what an agent needs will be too tight or too loose, and you will not know which until it breaks something or fails to stop something. Record a representative session at the kernel, write rules against what you find, and keep the capture attached so a departure from the baseline is a question you get asked rather than one you never hear. The three boundaries stay separate throughout, because the mechanism that bounds a file read has nothing to do with the one that bounds a connection.

Frequently asked questions

How do I restrict what an AI agent can do on a server?

Separate the three boundaries first. Filesystem access is bounded by a sandbox, container or LSM hook; process execution by the sandbox scope and the user it runs as; network reach by host egress rules rather than the agent's own configuration. Measure a representative session before writing any of them, because a rule written from assumptions is as likely to block real work as to stop the thing you were worried about.

How do I audit what an AI coding agent does on Linux?

Capture at the kernel rather than reading the agent's transcript. A process-launch monitor records every program it starts, including subprocesses a build spawned, and a connection monitor records every host it dialled with the process attached. Neither depends on the agent reporting accurately, and both see things the transcript never knew happened.

Does an agent sandbox control network access?

For the commands it runs, usually yes. For the agent itself, no. Model API requests, MCP server connections and connector calls originate in the parent process outside the command sandbox, so a policy written only in the sandbox configuration leaves exactly the traffic an injected instruction would use.

How do I find unapproved MCP servers running on my machine?

Watch process launches, since a server the agent starts is an ordinary process with its own command name. The exceptions are a server talking to the agent over a Unix socket, which produces no TCP for that leg, and a library-style server inside the agent process, which has no separate PID and attributes to the agent.

Can I set per-tool permissions for an AI agent?

The agent's own permission system can, within the limits of what it chooses to ask about. That is application-level logic rather than kernel enforcement, so it holds while the application keeps asking. An organization can harden it with managed settings, which sit above every user and project file, and the docs are explicit that "nothing in your own settings files or --settings overrides a managed key." A kernel-enforced boundary holds regardless, which is why the two layers are complements rather than alternatives.

What is the difference between auditing and enforcing agent behaviour?

Auditing records what happened and costs nothing when nothing unexpected does. Enforcing prevents actions and risks breaking legitimate work every time a rule is wrong. Most teams get more value from a good record than from a tight policy, because the record is what tells you which rules are worth the risk.

Should an AI agent run as its own user?

It is a cheap and effective step. A separate user bounds filesystem access by the ordinary Unix mechanism, makes cgroup or user-scoped egress rules straightforward, and keeps the agent out of your own credential paths without any agent-specific tooling.

Can I tell which process an agent spawned made a connection?

Yes, when it runs as its own process, because the connection is attributed to the PID that opened it. That is what separates the agent's own traffic from an MCP server's or a package manager's, and it is the attribution a network-level view cannot reconstruct afterwards.

Does blocking an agent's network access stop data exfiltration?

It bounds where data can go, which is a real reduction, and it does not prevent the agent being convinced to send it. Exfiltration through the agent's own tools uses the parent process, so the control has to sit around the agent rather than inside its sandbox.

How much overhead does recording agent activity add?

A kernel probe fires on events the kernel already generates, such as a process launch or a connection, so the cost scales with how often those happen rather than with how much work the agent does. Nothing is added to the execution path itself.

Sources

  • Claude Code: configure permissions. Source for the allow, ask and deny rule syntax quoted here, and for what the permission system does and does not enforce.
  • Claude Code: settings precedence. Source for managed settings sitting above every user and project file, and for allow and deny lists merging across scopes rather than overriding.
  • Claude Code: managed settings. The administrator-deployed policy layer, and the statement that nothing in a user's own files overrides a managed key.
  • codex-rs/linux-sandbox README. Source for what a command sandbox's network isolation covers, and the basis for the claim that it does not bound the agent's own traffic.
  • Landlock and BPF LSM. The two kernel mechanisms in the boundary table that enforce a path rule regardless of whether an application asks.
  • AWS instance metadata service. Source for why link-local belongs on the never-legitimate deny list on any cloud host.
  • Model Context Protocol: transports. Source for MCP servers running as separate processes, which is what makes per-process attribution work and why a stdio transport never appears as TCP.
  • exectop. The process-launch monitor used here, which folds the programs one application starts and flags the unusual ones.
  • dialout. Outbound TCP connections captured in-kernel and grouped by the process that dialled them.
  • yeet documentation. The runtime the probes here are written in.

Related resources