Govern coding-agent extensions
Coding agents such as Claude Code can load extensions (Claude Code calls them mods) that read files, run processes and make network requests on the agent's behalf. This guide shows how to see which extensions loaded in an agentvisor exec session, what they did, and how to have policy decide whether each one may load or be used.
AgentVisor™ treats extension governance as an addition to the controls you already have, not a replacement for them: the sandbox and host-side egress policy remain the boundary. Read Honest limits before relying on any of it.
Extensions run inside the same sandbox as the coding agent that loads them. Whatever isolation the agent has from the host, so do its extensions, and that doesn't depend on how the coding agent treats them itself. Claude Code, for example, doesn't sandbox its own mods, but under AgentVisor they still can't reach anything the sandbox and egress policy don't allow. Governance tells you what each extension did and lets policy narrow it further.
Prerequisites
agentvisor execrunning a coding agent in adockerorgvisorsandbox- A policy domain that covers extension decisions (the example base policy does; see Policy)
- A harness that supports governance. Today that is
claude-code.
Quick start: observe
agentvisor exec . --harness claude-code --extensions observe
--harness names the coding agent. It is always explicit: AgentVisor never infers it from --command. When you don't pass --command, the harness supplies its own default command. That default applies to agentvisor exec only: in other modes the agent framework's own command is launched, and the harness contributes only its environment, provisioned files and extension governance.
In observe mode every extension load and call is decided by policy, audited and counted, but never blocked. With a policy that allows everything, the session behaves exactly as it did without the flag. When the agent exits, the extension inventory is printed on stderr:
agentvisor: mods observed (harness claude-code, mode observe)
NAME TIER DECLARED CAPABILITIES OBSERVED CALLS DECISIONS
alpha user fs.read,net.fetch fs.read=3,net.fetch=1 allowed=4 denied=0
The same settings can live in configuration:
guest:
harness:
type: claude-code
extensions:
mode: observe
See the exec reference and the configuration reference for every option and environment variable.
Modes
| Mode | Behavior |
|---|---|
off | Default. Nothing is installed or reported. |
observe | Decisions are made, audited and counted. A decision that would deny is recorded as "would deny" and the extension still loads and runs. |
enforce | A denial blocks the extension load or call. |
Start with observe, read the inventory and audit records, write policy for what you saw, then move to enforce.
Two further settings tighten what may load (set them in configuration or the environment, not on the command line):
| Setting | Effect |
|---|---|
guest.harness.extensions.allow_user | false permits only operator-supplied extensions. Default true. |
guest.harness.extensions.disable_sideload | true turns off the harness's sideloading of extensions from the command line or working directory, where supported. Default false. |
guest.harness.extensions.sensor_grace | How long a running session may go without a sensor heartbeat before it is reported unobserved (no_sensor). Between 1s and 10m. Default 30s. |
Policy
Each extension decision is an ordinary policy request:
| Field | Value |
|---|---|
| Operation | agentvisor:extension:load when an extension loads, agentvisor:extension:call for each capability it uses |
| Resource | mrn:agentvisor:extension:<harness>:<extension>, with /<capability> appended for calls |
| Context | extension.name, extension.tier (managed, user or builtin), extension.capabilities (declared), extension.capability, extension.native_capability, extension.harness and a bounded extension.args_summary |
Capabilities are normalized across harnesses where a normal form exists (fs.read, fs.write, net.fetch, process.run); other calls arrive under the harness's own name. extension.capability and the /<capability> suffix of the resource MRN carry the normalized name (for example net.fetch for the harness's http.fetch); extension.native_capability always carries the harness's own name. A capability with no normal form is decided under its own name and is omitted from extension.capabilities.
The example base policy shipped with AgentVisor allows every extension decision, so observe changes nothing. It also carries commented examples. To deny by capability, uncomment a rule and add it to the grant rule:
deny_capability if {
input.operation == "agentvisor:extension:call"
input.context.extension.capability == "process.run"
}
allow if {
input.principal.sub != ""
not deny_capability
}
Because extension decisions are policy decisions, the existing audit trail records them, including denials.
Attributing network requests to an extension
When an extension makes an outbound HTTP request, the harness sensor labels it, and the label reaches your agentvisor:http:request and agentvisor:http:upgrade policies as input.context.http.extension. Use it to constrain, for example "extension example-ext may only reach api.example.com":
extension_allowed_hosts := {
"example-ext": {"api.example.com"},
}
extension_violation if {
allowed := extension_allowed_hosts[input.context.http.extension]
not input.context.http.host in allowed
}
then require not extension_violation in the rule that grants the request. Use the label only to deny or constrain, never to grant. See Policy Enforcement for why.
Reading the inventory
The post-exit summary lists each extension with its tier, declared capabilities, observed calls and decisions. The same inventory is available in the audit trail, in OpenTelemetry telemetry, and to programs that embed AgentVisor through its observer API; Kura is another consumer of the same data.
Counts are decisions, not calls
To stay inside the harness's hook time budget, the sensor remembers each policy verdict for 30 seconds, keyed by extension, capability and a coarse form of the argument (the directory of a path, the origin of a URL, the program name of a command). A repeat of such a call inside that window reuses the verdict and is not reported to AgentVisor.
So the numbers in the summary, the audit trail and the observer API (Calls, TotalCalls, Allowed, Denied, WouldDeny) count policy decisions. Ten reads in the same directory within 30 seconds appear as one. Treat them as a lower bound on activity, not an exact tally, and don't show them as "number of calls".
The window is fixed and not configurable. In enforce mode it has two consequences:
- A policy change takes effect for an already-decided call only after its cached verdict expires, at most 30 seconds later.
- Two calls that differ only in the part of the argument the sensor ignores (for example two different files in one directory) share a verdict, so policy that distinguishes them by the full argument is not applied to the second.
Write enforce-mode policy at the capability, directory, origin or program level, not on the full argument.
A session that should have a sensor but does not is reported as unobserved, with a machine-readable reason:
| Reason | Meaning |
|---|---|
no_sensor | The sensor never reported, for example because the harness started in a mode that loads no extensions. |
below_version_floor | The harness is older than the lowest supported version. |
managed_settings_not_honored | A sensor reported, but not with the settings AgentVisor provisioned. |
For an unobserved session the listed extensions are incomplete: extensions may have run that AgentVisor could not see. The agentvisor_extension_unobserved_sessions_total metric counts these sessions by reason. The summary never changes the exit code, which is always the command's own.
An application embedding AgentVisor learns this while the run is live, not only at the end: its extension observer receives an unobserved event the first time a session is flagged. A harness below the version floor is flagged at its first sensor report; a sensor that never reports is flagged once guest.harness.extensions.sensor_grace has elapsed.
Where governance fails open
enforce mode refuses what the sensor is present to refuse. It is not fail-closed about the sensor itself. In two cases the harness keeps running and extensions the user installed are not mediated:
| Case | What happens | What you see |
|---|---|---|
| Managed settings missing, unreadable or malformed | The harness starts without the sensor. User extensions load and run unchecked. | No extension events arrive. The session is flagged no_sensor, but only in the post-exit summary, the audit record and the metric. There is no live inventory entry to flag while the session runs. |
| The sensor stops on its own (for example it wedges the harness's hook worker and the harness drops it) | The harness keeps running with the user's extensions and without the sensor. | The sensor announces itself once, when the session starts. A sensor that stops after that point is not flagged. The session still reports as observed, with an inventory that simply stops growing. |
A sensor that is slow or unreachable is handled differently: in enforce mode a check that fails or times out refuses user-installed extensions rather than allowing them.
Two related paths behave as the harness defines them, and AgentVisor does not independently verify them:
- Starting the harness in a mode that loads no extensions (for example
--safe-modeor--bare) removes the user's extensions along with the sensor. The session is flaggedno_sensor. - The harness unloads its extension worker after repeated crashes. This is documented harness behavior and is believed to remove every installed extension, the sensor included, but AgentVisor has no test that exercises it.
AgentVisor does not currently terminate or refuse a session because its sensor is absent or silent. If your deployment must not run unmediated extensions, don't rely on enforce alone:
- Set
guest.harness.extensions.allow_user: falseso that only operator-supplied extensions can load, anddisable_sideload: true. - Treat an
unobservedflag as a failure of the run and discard or re-run it. - Rely on host-side egress mediation for anything that must hold regardless of the sensor.
Honest limits
- Reports are agent-attested. The sensor runs inside the agent's process, in the same trust domain as the extensions it watches. A malicious extension, or anything else running as the agent, can fail to report, report falsely, or forge an attribution label. Treat the inventory as evidence about a cooperating agent, not proof about a hostile one.
- Host-side egress mediation is the enforcement boundary. What an extension can do to the outside world is decided by your HTTP policy and credential brokering on the host, which the agent cannot alter. Extension decisions and attribution can only narrow access on top of that. A report cannot cause an allow that policy would not otherwise give.
- Child processes are attributed to the agent. When an extension starts a process (for example through
process.run), that child and anything it does are not attributed to the extension. Its network traffic is attributed to the agent, with no extension label. --sandbox=noneis unconfined. Without a sandbox the agent can reach the host directly and read or alter what the sensor reports.agentvisor execrefusesobserveandenforcewith--sandbox=nonerather than give a false sense of coverage.
Extension governance adds visibility and policy hooks. It does not by itself establish that an agent is confined; see Confinement for what that requires.
Troubleshooting
| Symptom | Cause |
|---|---|
exec refuses to start with --extensions observe | The sandbox is none, or --harness is missing, or the harness doesn't support governance. |
| Summary says the session is unobserved | See the reason codes above and Where governance fails open. |
An extension still loads in enforce mode | Check the audit trail: the policy decision may have been allow. A default-allow policy allows everything. |