EngineeringAGA-5616 min read

We audited OpenShell's source. The design holds and the defaults do not.

By Luis Hasanaj · AI Gen AppsOctober 2026

Abstract

A read of NVIDIA OpenShell's source at a fixed commit, against each claim the announcement makes. Enforcement outside the agent's process is real and fails closed. Request rules default to logging rather than blocking, credentials are guarded on the way out but not on the way back, the formal prover is not what gates approvals, and a security bulletin our first note missed listed two vulnerabilities rated 9.9.

This is an audit of NVIDIA's OpenShell source on GitHub, read at commit bc4c8f0 on 23 October 2026, together with NVIDIA's August 2026 security bulletin for the project. We read the code, the documentation and the design RFCs. We did not build or run it, so every finding below is either something we read in a code path or something the project's own documentation states, and the text says which. The runtime is NVIDIA's work. The reading, and the judgement about what matters, is ours.

Three weeks ago we wrote that one sentence in NVIDIA's OpenShell announcement was doing more work than the rest combined: enforcement happens outside the agent's process, where it cannot be prompted away. That note, OpenShell puts the enforcement outside the agent, was a reading of the product page. It said plainly that we had not looked further.

So we looked further. The source is public under Apache 2.0: about 610,000 lines of Rust across 38 crates, moving at roughly eight commits a day. We split it into the four places the announcement makes promises (the sandbox, the network policy engine, the credential path, and the policy prover) and read each one against the claim made for it.

The short version is that the sentence we praised holds up in code. The architecture is real, it is careful, and in most places it fails closed. What does not hold up is everything a team inherits by not setting a field. The design is strong and several defaults are not.

We also owe a correction. NVIDIA published a security bulletin for OpenShell on 25 August, five weeks before our first note, listing six vulnerabilities including two rated 9.9. Our first note did not mention it. It should have, and section 7 covers it.

One request, and the default at the end of it
yesnoauditAgent opens a socketagentPinned to loopbackengineBinary andhost allowed??Request breaks an L7 rulechannelRefused at connectendpointEnforcement field unsetstorageLogged, then forwardedendpoint

1. What we read, and how

Each of the four areas was read against two questions: what does the announcement or the documentation claim, and what does the code path actually do. A finding counts as verified only where we read the code that implements it. Where only the documentation says something, we say so.

AreaMain cratesClaim tested
Sandboxopenshell-sandbox, compute driversKernel controls confine files, syscalls and network
Networkopenshell-supervisor-network, openshell-binary-identityEvery request is checked by binary, host, method and path
Credentialsopenshell-core, openshell-providers, credential driversAgents never see real credentials
Proveropenshell-prover, gateway policy serviceFormal verification flags risky policy changes

The project is young and moving fast. Version 0.1.0 was tagged on 25 September, 0.1.5 on 20 October, and the history since mid-June holds close to a thousand commits. Anything below can be out of date within weeks, in either direction. That is a reason to read it as a snapshot and to check the commit you deploy.

2. The core claim holds

Start with what is right, because most of it is.

On Linux the workload process runs under three controls, and the sandbox refuses to start without all of them. A startup qualification step checks for a non-root identity, an empty capability set, no_new_privs, a working Landlock filesystem sandbox at ABI 3 or newer, a working seccomp notification channel, and socket confinement that the workload cannot undo. If any check fails, the process exits. We found no flag, environment variable or development mode that switches the sandbox off.

Network access is not left to the agent's good manners. A seccomp filter allows only Unix, route-netlink and internet sockets, so raw sockets, packet sockets and ICMP are refused. Every internet socket the workload opens is intercepted by a broker in the supervisor and bound to the loopback device before the workload can use it. That binding survives fork and exec, and clearing it needs a capability the sandbox does not hold. DNS goes to a relay that answers only for names the policy allows.

That is the sentence from the announcement, implemented. The agent can be talked into anything, and what it is talked into still arrives as a request at a supervisor it cannot reach.

Three qualifications belong next to that.

The outer fence is the runtime's, not OpenShell's. The sandbox process does not create its own network namespace. Its source says direct egress is rejected "by Docker network-none, Kubernetes NetworkPolicy, or a NIC-less VM". On Docker and Podman that is set by the driver and is solid. On Kubernetes the documentation is explicit: the cluster's network plugin must enforce NetworkPolicy, and "without it, sandbox pods can bypass OpenShell network policy". OpenShell does not check that your plugin does. The security guide also describes a network namespace with a veth pair, which is not what the Docker and Podman path we read does, so treat that page as describing one deployment rather than all of them.

The syscall filter is a denylist. Its own module comment calls it "a default-allow policy with targeted blocks". The list is well chosen (ptrace, bpf, io_uring, mount and the new mount API, userfaultfd, user namespace creation and more), and a denylist is a normal choice for running arbitrary developer tools. It still means safety depends on the list being complete.

Filesystem policy degrades by default. The mandatory baseline is a hard requirement and protects OpenShell's private directory. The user's own filesystem policy defaults to compatibility: best_effort, and if applying it fails the code logs "Running WITHOUT filesystem restrictions" and continues on the baseline alone, which grants access to every top-level path except OpenShell's own. A high-severity event is emitted when this happens. Setting hard_requirement turns it into a startup failure.

3. The default that matters most

Here is the finding we would put first on any team's list.

The network policy can inspect individual HTTP requests and allow a read while refusing a write on the same API. That was the granularity we singled out three weeks ago. In code, those rules have an enforcement mode, and the mode is chosen like this:

let enforcement = match enforcement_value.as_str() {
    "enforce" => EnforcementMode::Enforce,
    "" | "audit" => EnforcementMode::Audit,

An empty field means audit, and in audit mode a request that breaks a rule is logged and then forwarded. So a policy that says access: read-only and leaves enforcement out does not prevent writes. It records them. The documentation states this, and recommends starting in audit and switching later. The shipped provider profiles and the quickstart do set enforce, which is to their credit. A team writing its own rule from the schema reference gets audit.

Two more defaults compound it. An endpoint with no protocol field gets no request rules at all: host, port and binary are checked, and then any method and any path pass. And for WebSocket upgrades on such an endpoint, the code's own log line is "raw bidirectional relay (L7 enforcement no longer active)".

The prover agrees with us. Its formal containment check refuses to vouch for a REST endpoint whose enforcement is anything other than enforce, and reports it as unsupported. The one part of the system built to reason about what a policy permits does not accept audit mode as a permission boundary. Neither should anyone deploying it.

"Nothing is permitted by default" is true at the connection. It is not true of the method and path rules that make the announcement interesting, unless you write one extra word.

4. A binary is a tool, not an intent

Every network rule names the binaries allowed to use it, and the identity check is careful. The executable is read from /proc/<pid>/exe, so symlinks and argv[0] cannot fake it, and its SHA-256 is pinned the first time it is seen. A later mismatch is denied.

Two properties of that design matter in practice.

Ancestry grants access. A rule matches if the connecting executable, or any process above it, is listed. List /usr/bin/claude and every shell, script and tool that the agent spawns passes the binary check. List curl and anything that shells out to curl passes. The documentation is candid about interpreters: a rule that lists the Python interpreter so that pip can install packages means "any Python program in the sandbox can use it". Binary scoping tells you which tool made a request. It cannot tell you why.

The pin trusts whatever arrives first. Policies name paths, never hashes, and the hash is whatever is observed on first use. That is sound for system paths the sandbox cannot write. The shipped PyPI profile also lists interpreter paths under /sandbox, which is the writable working directory. We have not tested it, but it follows from the code that an agent which writes its own executable to one of those paths before first use gets its own binary pinned as trusted. Keep authorised binaries out of writable paths.

5. The credential is guarded on the way out, not on the way back

The key is guarded leaving, not returning
yesnoupstream answersPlaceholder in the envstorageAgent places it anywhereagentEndpointbound??Real key substitutedengineRefused, fail closedendpointResponse relayed unchangedchannelAgent reads any echoendpoint

The outbound half is as good as the announcement says. The agent's environment holds a placeholder of the form openshell:resolve:env:<key>, never the value. The supervisor substitutes the real value only on endpoints the provider is bound to, after the policy check, and a placeholder that cannot be resolved fails the request rather than leaking it. Static credentials have been bound to their provider's endpoints since a change merged on 10 August.

Three findings narrow what that guarantees.

The agent chooses where the key goes. Substitution applies to the request line, every query parameter and every header value, with no restriction on header name. On a bound endpoint the agent can put the real key into any header or parameter it likes, and where request body rewriting is enabled, into any field of the body.

Nothing scrubs the response. The response relay is never given the resolver and does no matching. The provider documentation says it directly: "The proxy does not rewrite cookies, response content, unsupported request bodies, or WebSocket binary frames." An approved API that echoes a header, reflects a key in an error message, or stores a field and serves it back will hand the real value to the agent. The shipped Codex profile places OAuth refresh tokens in the sandbox as placeholders, and a token refresh response carries new tokens back the same way. We could not determine from the repository how that client behaves in practice.

The real values are inside the sandbox runtime. They are held by the supervisor process, separated from the workload by the ptrace and memory-read blocks and a non-dumpable process. That is a reasonable boundary. It is a process boundary, and section 7 is a reminder that process boundaries get escaped.

So the sentence we wrote last time, that a fully compromised agent cannot exfiltrate a secret it never held, needs a clause. It cannot send the key anywhere the provider is not bound. It can still see the key if a bound endpoint shows it, and it can still send any data at all to any endpoint it is allowed to reach. The security guide says that last part itself: each allowed endpoint "is a potential data exfiltration path". Request bodies are not inspected by default, and the one built-in body filter describes itself as "not a secret scanner".

6. Two provers, and only one of them is formal

The announcement describes a prover that uses formal verification to check whether a policy stays inside a declared boundary. In the code that is two separate engines, and they answer different questions.

The containment checker is the real thing. It encodes network policy into the Z3 solver using string, regular expression and integer theories, and asks whether any action the candidate policy allows falls outside the boundary policy. Where the answer is yes, it produces a concrete action and replays it to confirm. Anything it cannot model (middleware, GraphQL, MCP, credential settings, audit mode, unusual globs) is reported as unsupported rather than ignored, and a solver timeout is reported as inconclusive rather than as a pass. Eleven tests check it against the runtime's actual policy rules. That is good engineering. Two limits: filesystem and process rules are compared by exact match rather than reasoned about, and it is a command line tool. The project's own README says gateway callers "continue to use the proposal-risk API". The containment checker is not in the approval path.

The engine that gates approvals is a fixed checklist. When a sandbox is blocked and a rule is proposed, the gateway runs a proposal-risk check. It loads facts into Z3 as constants asserted true, and the real decisions happen in ordinary code. It looks for four things: link-local reach, a credentialed host reachable by a binary that bypasses inspection, new credential reach, and new capability. It does not model paths. And by design, in the code's own words: "Un-credentialed reach is not a tracked risk."

What automatic approval lets through
yesnoConnection blockedchannelRule drafted from itagentFour riskchecks clear??Public host, no credentialstorageHeld for a personhumanApproval mode set to autoengineMerged, nobody lookedendpoint

That matters because approval can be automatic. The default is manual, which is right. An operator can set the approval mode to auto, and then a proposal that clears the four checks is merged with nobody looking, including rules OpenShell drafts itself from blocked connections. The advisor documentation warns about exactly this: turn it on only if you accept that binaries in the sandbox can gain access to public hosts without your review.

The design RFC for agent-driven policy said automatic application should happen only when the prover, validation and an organisation-wide ceiling all succeed. What shipped has no ceiling and no containment step. Three weeks ago we called the prover the first time we had seen formal methods shipped as part of an agent product. That was half right. They shipped, as a tool you run. They are not yet what decides.

7. The bulletin we should have cited

NVIDIA's security bulletin 5872, released 25 August, covers NemoClaw and OpenShell. The six OpenShell entries all affect versions up to 0.0.33, are fixed in 0.0.34, and were found externally.

CVEScoreWhat NVIDIA describe
CVE-2026-650939.9Sandbox escape on Linux, uncontrolled search path element
CVE-2026-650839.9Sandbox provisioning API, incomplete list of disallowed inputs
CVE-2026-650918.8Command injection by a malicious gateway, all platforms
CVE-2026-650928.5Path traversal that bypasses the L7 REST network policy
CVE-2026-650866.8Command injection in the sandbox exec handler
CVE-2026-650855.2Improper output encoding in the inference proxy

None of this argues against the architecture. Fast disclosure and a fix in the next release is what a responsible project does, and the history around it shows the same classes being hardened: path dot-segment canonicalisation on 17 August, trailing-dot host normalisation on 24 August, a fix to stop rules inheriting authorisation they never declared on 10 August.

One entry is worth dwelling on. CVE-2026-65092 is a path traversal past the REST policy, and the containment checker's model of a request simply forbids such paths. Its domain asserts that a path never contains //, /./ or /../, which is correct only if the proxy normalises paths before matching. That assumption is reasonable, and it is exactly where a real bypass lived. We cannot tie that CVE to a specific commit. The point stands regardless: a proof is a statement about a model, and the model's assumptions about the enforcement layer are the part nobody proves.

8. The scorecard

ClaimWhat the code doesVerdict
Enforcement outside the agent's processSupervisor brokers every socket; startup fails closed on missing kernel controlsHolds
Nothing permitted by defaultConnections deny by default; L7 rules default to audit and forwardHolds at connection, not at request
Checked by binary, host, method, pathBinary by exe and hash, ancestors included; method and path only where protocol is setHolds, with wide edges
Agents never see real credentialsNever in the environment; not scrubbed from responsesHolds outbound only
Formal verification of policy changesReal SMT checker as a CLI; approvals use a four-item checklistOverstated
Runs everywhereLinux kernel controls; Kubernetes relies on the cluster's network pluginHolds, conditionally

9. If you run it

Most of what we found is fixed by configuration, which is the good news. In priority order:

  1. Set enforcement: enforce on every inspected endpoint, and give every API endpoint a protocol. Treat a missing field as a review failure.
  2. Run 0.0.34 or later. The 0.1 series is current.
  3. Leave approval mode on manual. If you use auto, accept in writing that public hosts can be added without review.
  4. Keep authorised binaries out of writable paths, and prefer the specific tool over the interpreter or the agent binary. Remember that ancestors authorise descendants.
  5. Set filesystem compatibility: hard_requirement.
  6. On Kubernetes, prove your network plugin enforces NetworkPolicy before relying on any of the above, and enable user namespaces where the cluster supports them. They are off by default.
  7. Prefer exact hosts to wildcards. The documentation notes a wildcard can open a DNS-label exfiltration channel.
  8. Write a boundary policy and run the containment checker against every change in CI. It is the strongest component in the project and nothing calls it unless you do.
  9. Decide on telemetry. It is anonymous, documented, and on by default.

10. What this changes about the argument

Our first note argued that the right place for an agent's permission decision is somewhere the agent's text cannot reach. The source confirms OpenShell puts it there, and does so with more care than most systems we have read, including the ones we argued from in the boundary is the product and the real attack surface.

What the audit adds is the other half. Moving enforcement out of the model changes who decides. It does not change what is decided, and what is decided is mostly set by defaults. A runtime that cannot be prompted away will still forward a write it was configured only to log, still hand back a key an approved API echoes, and still approve a host if someone switched approvals to automatic to stop the prompts. None of those failures involves the model at all.

That is the same lesson as code orchestrates and governed autonomy, seen from the infrastructure side. Deterministic enforcement is necessary. It enforces whatever it was given, and the review that matters is of what it was given.

11. Sources

  1. NVIDIA, OpenShell on GitHub, read at commit bc4c8f0, including the security best practices, policy schema, network rules, providers, advisor and prover documentation under docs/, and RFCs 0001, 0002, 0009 and 0012. Every path and quotation in this note refers to that commit.
  2. NVIDIA PSIRT, Security Bulletin: NVIDIA NemoClaw and OpenShell, August 2026, bulletin 5872 and its CVE records, for section 7.
  3. NVIDIA, the OpenShell product page, for the claims tested in section 8.
  4. Our own OpenShell puts the enforcement outside the agent, the reading this audit extends and in two places corrects.

We read the code and did not run it. Where a finding is marked as following from the code rather than tested, such as the binary pin in section 4 and the token refresh in section 5, it is an inference and should be tested before being relied on. The project moves at around eight commits a day, so check any finding against the version you deploy.

This note sits in our Engineering track, alongside the agentic, governance and system design threads. The full library is at Research.

Questions

Is NVIDIA OpenShell secure?
The architecture is sound and mostly fails closed: the sandbox refuses to start without Landlock, seccomp and socket confinement, and every connection is brokered by a supervisor the agent cannot reach. The weaknesses are in defaults. Request level rules default to audit mode, which logs a violation and forwards the request, so a policy has to set enforcement to enforce to block anything at the method and path level.
What does audit mode mean in an OpenShell network policy?
When the enforcement field is empty or set to audit, a request that breaks a REST, GraphQL or WebSocket rule is logged and then forwarded. Only enforcement set to enforce returns a 403. A rule written as read-only without that field records writes rather than preventing them.
Can an agent in OpenShell see a real API key?
It never receives one in its environment, only a placeholder that the supervisor resolves on endpoints the provider is bound to. But the supervisor does not scrub responses, so if an approved API echoes a header, reflects the key in an error, or returns a newly minted token, the real value reaches the agent. The agent also chooses which header or query parameter the key is substituted into.
Is the OpenShell policy prover formal verification?
Partly. The containment checker encodes network policy into the Z3 solver and returns a concrete counterexample when a policy exceeds a declared boundary, and it fails closed on anything it cannot model. It is a command line tool. The check the gateway actually runs before approving a policy change is a fixed list of four risk categories that does not model paths and does not treat new access to a public host without a credential as a risk.
What vulnerabilities have been disclosed in OpenShell?
NVIDIA's August 2026 security bulletin lists six for OpenShell, affecting versions up to 0.0.33 and fixed in 0.0.34: a sandbox escape and a provisioning API flaw both rated 9.9, command injection by a malicious gateway rated 8.8, a path traversal past the REST network policy rated 8.5, command injection in the exec handler rated 6.8, and an output encoding flaw in the inference proxy rated 5.2. All were found externally.
How should a team configure OpenShell before relying on it?
Set enforcement to enforce and declare a protocol on every API endpoint, run 0.0.34 or later, leave approval mode on manual, keep authorised binaries out of writable paths, set filesystem compatibility to hard_requirement, confirm the Kubernetes network plugin enforces NetworkPolicy, prefer exact hosts to wildcards, and run the containment checker against a written boundary in CI.

Related research

  1. October 2026 · Engineering
    DeepSeek-V4.1-Flash and the 890-byte KV cache
  2. October 2026 · Engineering
    ScaleSweep, and what AbsMax leaves on the table in NVFP4
  3. October 2026 · Engineering
    Lynx: the KV cache does not have to arrive whole
More in Engineering →