---
title: "A sandbox inside the sandbox: moving my open-source agent to Hermes on OpenShell"
description: I put Hermes Agent inside NVIDIA OpenShell on an old Dell and started moving Forge, my open-source contribution agent, there. Forge runs strangers' code, and inside one OpenShell sandbox a process the agent starts gets the agent's network rules, so its workers needed a cage of their own. What installed cleanly, where the keys go, the two rules I didn't expect, and what's still left.
date: 2026-10-24
tags: AI agents, agent security, OpenShell, Hermes Agent, Landlock, sandboxing
---

Forge, my open-source agent, clones strangers' repositories and runs their install scripts and test suites all day. When I started moving it to Hermes Agent inside NVIDIA OpenShell, I found the sandbox around the agent wasn't enough: inside one OpenShell sandbox, a process the agent starts gets the agent's network rules. So Forge's workers get a second fence inside the first, built from Landlock, a seccomp filter and a small relay that is their only way out. The move isn't finished. The cage works, and OpenShell taught me two rules I didn't expect.

On October 23 I wrote that NVIDIA's advice for agents [starts with the sandbox](/notes/hope-isnt-security-enterprise-agents.html), and that my own sandbox row was empty. The next day Hermes was running inside OpenShell on an old Dell laptop. This note is what happened after the install: where the keys live, what a child process inherits, and what a sandbox inside a sandbox has to block.

## Forge runs strangers' code for a living

Forge is my open-source contribution agent. It looks for issues on popular projects in my areas, picks the ones worth building, and hands each one to a detached worker. Claude Code clones the repo, writes the fix and runs the project's tests. A gate re-runs the checks itself, a second model reviews the diff, and the fix goes to my fork. Anything said on a pull request after that waits for my ok.

It runs on a budget of $5 a day, with at most four pull requests open at once.

The risky step is the build. "Run the tests" means running whatever a repo's install scripts and test suites do, written by people I've never met. On the Mac mini, where Forge lives today as an OpenClaw agent, the workers run inside srt, a sandbox runtime built on macOS Seatbelt. Moving Forge to Linux meant building that fence again.

<figure class="fd-fig sx sx-forge" aria-labelledby="sx1-cap" data-desc="One Forge job in five steps, lit in turn. One, scout: Forge finds issues on popular projects in my areas and picks the ones worth building. Two, the worker: Claude Code clones the repository, writes the fix and runs the install and the tests, which is code written by strangers; this step is marked in red as the one that needs a fence. Three, the gate: it re-runs the worker's checks itself and sends the worker back once if they fail. Four, the reviewer: a second model reads the diff, and a blocking finding gets one more fix. Five, ship: the fix is pushed to my fork and the pull request opens, and anything said on it later waits for my ok. Today Forge runs as an OpenClaw agent on the Mac mini, with its workers in srt on macOS Seatbelt, on five dollars a day and at most four open pull requests.">
<p class="ea-now ea-say5" aria-hidden="true"><span>Pick an issue</span><span class="ea-no">Run a stranger's code</span><span>Re-run the checks</span><span>A second model reviews</span><span>Push to my fork</span></p>
<ol class="ea-rows ea-cyc5" aria-label="One Forge job">
<li><b>1</b><span class="ea-who">Scout</span><span>Finds issues on popular projects in my areas and picks the ones worth building.</span></li>
<li class="ea-row-no"><b>2</b><span class="ea-who">Worker</span><span>Claude Code clones the repo, writes the fix, runs the install and the tests: code written by strangers.</span></li>
<li><b>3</b><span class="ea-who">Gate</span><span>Re-runs the worker's checks itself, and sends it back once if they fail.</span></li>
<li><b>4</b><span class="ea-who">Reviewer</span><span>A second model reads the diff. A blocking finding gets one more fix.</span></li>
<li><b>5</b><span class="ea-who">Ship</span><span>Pushes to my fork and opens the pull request. Anything said on it later waits for my ok.</span></li>
</ol>
<p class="mp-strip"><b>Today</b><span>OpenClaw on the Mac mini, workers in srt. $5 a day, at most four open PRs.</span></p>
<figcaption id="sx1-cap">One job, start to finish. Step 2 is the one that needs a fence, so it lights red.</figcaption>
</figure>

## Hermes moved in first

The host is an old Dell Inspiron laptop running Ubuntu Server 26.04 with the lid closed. NemoClaw, NVIDIA's installer for agents in OpenShell, set up the CLI and the gateway in 78 seconds, without sudo. I pinned version 0.0.137, which brings OpenShell 0.0.116, and picked Hermes as the agent. It came up as Hermes Agent v0.21.3, on Claude Sonnet 5.5 through OpenRouter.

The first wall was Docker. The Dell runs Docker rootless, and OpenShell's gateway can't live there: the sandboxes sit in rootless Docker's own network namespace while the gateway listens on the host, so a sandbox can't call it back. The rootless path that works on Linux is Podman, version 5 or later. It took one root step, a small system unit that adds an address on loopback. I ran it by hand, because my agents can't sudo on that box.

Out of the box the sandbox denies by default. With only the PyPI preset on, a request to example.com gets a 403. Hermes's API and dashboard listen on loopback, and NemoClaw owns Hermes's config file and checks its hash, so you don't edit it by hand. Forge will run as its own Hermes profile instead, with its own SOUL.md and a hook that sees every tool call before it runs. In a test, that hook blocked an `rm`.

<figure class="fd-fig sx sx-stack" aria-labelledby="sx2-cap" data-desc="The stack on the Dell, as nested boxes. The outer box is a Dell Inspiron laptop running Ubuntu Server 26.04. Inside it, rootless Podman 5.7. Inside that, OpenShell 0.0.116: its gateway, its proxy and the keys. Inside that, the sandbox, deny by default, and in it Hermes Agent v0.21.3, with Forge planned as its own Hermes profile. Under the stack, three numbers: NemoClaw installed in 78 seconds without sudo; one root step, an address on loopback for Podman; and a 403 for example.com, because the sandbox denies by default. Rootless Docker did not work: the sandboxes sit in its network namespace and the gateway on the host, so they can't call it back.">
<div class="sx-nest">
<div class="sx-l sx-l-host"><p class="sx-lk">Dell Inspiron · Ubuntu Server 26.04</p>
<div class="sx-l"><p class="sx-lk">Rootless Podman 5.7</p>
<div class="sx-l sx-l-os"><p class="sx-lk">OpenShell 0.0.116 · gateway, proxy, keys</p>
<div class="sx-l sx-l-sb"><p class="sx-lk">Sandbox · deny by default</p>
<p class="sx-agent">Hermes Agent v0.21.3<span>Forge: its own profile, next</span></p>
</div>
</div>
</div>
</div>
</div>
<div class="mp-stats">
<p><b>78 s</b><span>NemoClaw install, no sudo</span></p>
<p><b>1</b><span>root step: an address on loopback for Podman</span></p>
<p><b>403</b><span>example.com, denied by default</span></p>
</div>
<p class="mp-strip"><b>Rootless Docker</b><span>No: sandboxes in its namespace, the gateway on the host, no way back.</span></p>
<figcaption id="sx2-cap">Each box holds the next. The keys sit in the OpenShell layer, outside the sandbox.</figcaption>
</figure>

## The keys arrive as placeholders

My last note explained OpenShell's credential trick from the docs. Here's how it looks from inside. The sandbox's environment holds no API key, only a placeholder that starts with `openshell:resolve:env:`. When Hermes calls OpenRouter with that placeholder in its auth header, OpenShell's proxy names the program, checks the policy, and swaps in the real key on the way out. Hermes never sees it.

Then I put placeholders where they don't belong. One in a request body: the proxy dropped the connection. Another provider's placeholder in a header: 403.

Telegram goes one step further. NemoClaw binds the bot token to one host, one port and one path, the bot API, and anywhere else the placeholder stays a placeholder.

<figure class="fd-fig sx sx-key" aria-labelledby="sx3-cap" data-desc="How a key reaches OpenRouter without entering the sandbox. Inside the sandbox, the key's variable holds only a placeholder starting with openshell:resolve:env. One: Hermes sends its request with the placeholder in the auth header. Two: OpenShell's proxy names the program behind the connection. Three: it checks the policy for that program and that host. Four: it swaps the real key in on the way out, so the key never enters the sandbox. Five: a placeholder in the wrong place is refused; in a request body the connection is dropped, and another provider's placeholder in a header gets a 403. Telegram goes further: the bot token is bound to one host, one port and one path, and anywhere else it stays a placeholder.">
<p class="ea-now ea-say5" aria-hidden="true"><span>Hermes sends the placeholder</span><span>The proxy names the program</span><span>The policy allows this host</span><span>The real key goes out</span><span class="ea-no">Wrong place: refused</span></p>
<p class="sx-env"><span>In the sandbox</span><code>API_KEY=openshell:resolve:env:…</code></p>
<div class="ea-lane" aria-hidden="true">
<div class="ea-box ea-box-sb"><p class="ea-box-k">Sandbox</p><p class="ea-box-n">Hermes</p></div>
<div class="ea-box ea-box-sv"><p class="ea-box-k">OpenShell</p><p class="ea-box-n">proxy, keys</p></div>
<div class="ea-box"><p class="ea-box-k">Endpoint</p><p class="ea-box-n">Open&shy;Router</p></div>
<i class="ea-req"></i>
</div>
<ol class="ea-rows ea-cyc5" aria-label="The request path">
<li><b>1</b><span class="ea-who">Hermes</span><span>Sends its request with the placeholder in the auth header.</span></li>
<li><b>2</b><span class="ea-who">Proxy</span><span>Names the program behind the connection.</span></li>
<li><b>3</b><span class="ea-who">Proxy</span><span>Checks the policy: this program, this host.</span></li>
<li><b>4</b><span class="ea-who">Proxy</span><span>Swaps the real key in on the way out. It never enters the sandbox.</span></li>
<li class="ea-row-no"><b>5</b><span class="ea-who">Proxy</span><span>In a request body: connection dropped. Another provider's, in a header: 403.</span></li>
</ol>
<p class="mp-strip"><b>Telegram</b><span>The bot token is bound to one host, port and path. Anywhere else it stays a placeholder.</span></p>
<figcaption id="sx3-cap">One request that gets its key, then one that doesn't. The square turns cobalt when the key joins, red when it's refused. Variable name shortened.</figcaption>
</figure>

## Credentials follow the endpoint, not the program

OpenShell's network rules name both sides of a call: which program may connect, and to which host. I assumed credentials worked the same way, so a key meant for `gh` would only be swapped into requests from `gh`.

They don't. A provider's key is resolved for any process that some policy lets through to that provider's endpoint. I found out when my relay, which a test preset had let through to GitHub's API, got the GitHub token swapped into its request. It was my own probe, so nothing leaked. But it rewrote my design rule: a program a worker can drive must never have a policy on a host that has a credential.

<figure class="fd-fig sx sx-ep" aria-labelledby="sx4-cap" data-desc="Two cards. On the left, what I assumed: keys follow the program. gh calling api.github.com gets the key, and the relay calling api.github.com gets only the placeholder. On the right, what OpenShell does: keys follow the endpoint. gh calling api.github.com gets the key, and the relay, which a test preset allowed through to the same host, gets the key too. Under them, the rule I work by now: a program a worker can drive gets no policy on a host that has a credential.">
<div class="mp-two-row">
<div class="mp-two-card">
<p class="mp-two-who">What I assumed</p>
<p class="mp-two-name">Keys follow the program</p>
<p class="sx-call"><span class="sx-prog">gh</span><span class="sx-host">→ api.github.com</span><b class="sx-res">key</b></p>
<p class="sx-call"><span class="sx-prog">relay</span><span class="sx-host">→ api.github.com</span><b class="sx-res sx-res-ph">placeholder</b></p>
</div>
<p class="mp-two-vs">vs</p>
<div class="mp-two-card mp-two-peter">
<p class="mp-two-who">What OpenShell does</p>
<p class="mp-two-name">Keys follow the endpoint</p>
<p class="sx-call"><span class="sx-prog">gh</span><span class="sx-host">→ api.github.com</span><b class="sx-res">key</b></p>
<p class="sx-call"><span class="sx-prog">relay</span><span class="sx-host">→ api.github.com</span><b class="sx-res sx-res-no">key</b></p>
<p class="mp-two-line">The relay was allowed there by a test preset. That was enough.</p>
</div>
</div>
<p class="mp-strip"><b>The rule now</b><span>A program a worker can drive gets no policy on a host that has a credential.</span></p>
<figcaption id="sx4-cap">The same two calls under two models. The red key is the one I didn't expect.</figcaption>
</figure>

## A child gets its parent's network

The second rule: policies follow the live process tree. A process gets the network rules of its own program plus those of every ancestor, checked when it makes the request.

That matters because Forge's scripts are Python, and the sandbox's Python is the one Hermes runs on. Every script Forge starts matches Hermes's own rules, OpenRouter and Telegram, and so does everything those scripts start. A worker launched the obvious way inherits a path to the agent's model provider and its chat bot.

The way out of the inheritance is to stop being a descendant. A process that double-forks ends up a child of PID 1 and keeps only the rules of its own program. That's how Forge's relay starts: detached, on its own copy of Python so it doesn't match Hermes's either, watching the worker step that launched it and exiting when that step ends.

<figure class="fd-fig sx sx-proc" aria-labelledby="sx5-cap" data-desc="The process tree inside the sandbox, lit one process at a time. Hermes runs on the sandbox's Python and has two network rules, OpenRouter and Telegram. Forge's script, a child of Hermes on the same Python, gets the same two. The worker step, Claude Code started by the script, inherits them too, though its cage keeps it from reaching OpenShell's proxy at all. The relay is detached: it double-forks, its parent becomes PID 1, it runs on its own copy of Python, and it keeps only its own rules, the package registries and Anthropic.">
<p class="ea-now ea-say4" aria-hidden="true"><span>Hermes: OpenRouter, Telegram</span><span>Forge's script: the same</span><span>The worker: the same</span><span>Detached relay: only its own</span></p>
<ol class="sx-tree ea-cyc4" aria-label="The process tree">
<li class="sx-n"><p class="sx-pn">Hermes<small>the sandbox's Python</small></p><p class="sx-rules"><i>OpenRouter</i><i>Telegram</i></p></li>
<li class="sx-n sx-d1"><p class="sx-pn">Forge's script<small>child of Hermes, same Python</small></p><p class="sx-rules"><i>OpenRouter</i><i>Telegram</i></p></li>
<li class="sx-n sx-d2"><p class="sx-pn">Worker step<small>Claude Code, started by the script</small></p><p class="sx-rules"><i class="sx-inh">OpenRouter</i><i class="sx-inh">Telegram</i><span>inherited; its cage can't reach the proxy</span></p></li>
<li class="sx-n sx-orphan"><p class="sx-pn">Relay, detached<small>parent: PID 1, its own Python</small></p><p class="sx-rules"><i class="sx-own">Registries</i><i class="sx-own">Anthropic</i></p></li>
</ol>
<figcaption id="sx5-cap">Indent is parentage. The relay sits at the root on purpose: double-forked, it inherits nothing.</figcaption>
</figure>

## The second fence

Inside OpenShell, each worker step runs in a cage of its own. It has three parts, two from the Linux kernel and a relay of about two hundred lines of Python.

- **Landlock**, the kernel's unprivileged sandbox, at ABI 8: lists of what the worker may read, write and execute; TCP connections, except to OpenShell's proxy, its gateway and Hermes's ports; no signals or abstract unix sockets to anything outside the cage.
- **A seccomp filter** that refuses unix sockets and io_uring. Landlock doesn't check a connection to a socket file, and Hermes's gateway listens on one: under Landlock alone, my test reached it. io_uring goes too, because it could open that socket without the system call the filter watches.
- **The relay**: a CONNECT-only proxy with a host allowlist, the package registries and Anthropic's API. The sandbox has no direct route out, so the relay is the only door left. It forwards to OpenShell's proxy, which sees the relay as the caller.

So the worker can install packages and call Claude. Registries pass as plain TLS, with no inspection and no key to add. On the relay's calls to Anthropic, OpenShell adds Forge's key, for the Messages and token-counting endpoints only. The worker spends tokens without ever holding the key. The reviewer runs as its own program with its own rule for OpenRouter, on GLM 5.3.

I tested it end to end. `claude -p` inside the cage answered through the relay for $0.026 on Haiku, and the reviewer answered through OpenRouter.

<figure class="fd-fig sx sx-fence" aria-labelledby="sx6-cap" data-desc="The worker's cage inside the OpenShell sandbox. The sandbox holds Hermes with Forge, and next to it the worker cage, Landlock plus seccomp, with the Claude Code worker inside. The only way out of the cage is the relay, which allows the package registries and Anthropic and forwards to OpenShell's proxy; OpenShell adds Forge's Anthropic key there. Then one row per rule. Files: Landlock read, write and execute lists. TCP: Landlock blocks OpenShell's proxy and gateway and Hermes's ports. Unix sockets and io_uring: refused by seccomp. The way out: the relay, CONNECT only, with a host allowlist. Keys: added by OpenShell on the relay's Anthropic calls, never inside the cage. Tested: claude -p answered through the relay for 0.026 dollars on Haiku, and the reviewer answered on GLM 5.3.">
<div class="sx-cagefig" aria-hidden="true">
<div class="sx-sbx">
<p class="sx-lk">OpenShell sandbox</p>
<div class="sx-pair">
<div class="ea-box"><p class="ea-box-k">Agent</p><p class="ea-box-n">Hermes + Forge</p></div>
<div class="sx-cage"><p class="sx-lk">Cage · Landlock + seccomp</p><div class="ea-box sx-w"><p class="ea-box-k">Worker</p><p class="ea-box-n">Claude Code</p></div></div>
</div>
<p class="sx-door"><b>Relay</b><span>registries, Anthropic</span></p>
</div>
<p class="sx-out"><b>OpenShell proxy</b><span>adds Forge's key for Anthropic</span></p>
</div>
<ol class="ea-card" aria-label="The cage, rule by rule">
<li><span class="ea-card-k">Files</span><span class="ea-card-v">Landlock: read, write and execute lists</span><b class="ea-pill">Listed</b></li>
<li><span class="ea-card-k">TCP</span><span class="ea-card-v">Landlock: not OpenShell's proxy or gateway, not Hermes's ports</span><b class="ea-pill">Blocked</b></li>
<li><span class="ea-card-k">Sockets</span><span class="ea-card-v">seccomp: unix sockets and io_uring</span><b class="ea-pill ea-pill-no">Refused</b></li>
<li><span class="ea-card-k">Way out</span><span class="ea-card-v">The relay: CONNECT only, a host allowlist</span><b class="ea-pill ea-pill-mid">One door</b></li>
<li><span class="ea-card-k">Keys</span><span class="ea-card-v">Added by OpenShell on the relay's Anthropic calls</span><b class="ea-pill">Never inside</b></li>
</ol>
<p class="mp-strip"><b>Tested</b><span><code>claude -p</code> through the relay, $0.026 on Haiku. The reviewer on GLM 5.3.</span></p>
<figcaption id="sx6-cap">Two fences: OpenShell around everything, the cage around the worker. The relay is the one door between them.</figcaption>
</figure>

## Four traps worth knowing first

None of these is in a quickstart. Each one stopped me until I found it.

The error you don't see. Two policies that name the same host must give it the same shape, TLS mode, protocol and enforcement, wildcards included. OpenShell refuses a mismatch with "network endpoint ambiguity validation failed". NemoClaw only said "OpenShell did not confirm the policy submission". A wrapper script named `openshell`, first on PATH and logging every call, showed me the real error.

Programs need their loader. Under Landlock, a dynamically linked program also needs execute rights on the dynamic loader. So the execute list grants `/usr`, not single binaries.

The wrong Python. Inside the sandbox, `python3` on PATH is Hermes's virtual environment. The worker's PATH leaves it out, and its config file is unreadable under Landlock anyway.

Two spellings of a proxy. curl prefers the lowercase `https_proxy`, so the worker sets both cases.

<figure class="fd-fig sx sx-traps-fig" aria-labelledby="sx7-cap" data-desc="Four traps, one card each, with what you see and what fixed it. The error you don't see: NemoClaw says OpenShell did not confirm the policy submission, while the real cause is the same host in two policies with different shapes; a logging openshell wrapper first on PATH shows OpenShell's own error, network endpoint ambiguity validation failed. Programs need their loader: under Landlock a dynamic binary also needs execute rights on the dynamic loader, so allow /usr rather than single binaries. The wrong Python: python3 on PATH inside the sandbox is Hermes's virtual environment, so the worker's PATH leaves it out. Two spellings of a proxy: curl prefers the lowercase https_proxy, so set both cases.">
<ul class="sx-traps">
<li><p class="sx-t-k">The error you don't see</p><p class="sx-t-say">"OpenShell did not confirm the policy submission"</p><p class="sx-t-fix">Same host, two shapes. A logging <code>openshell</code> wrapper showed "network endpoint ambiguity validation failed".</p></li>
<li><p class="sx-t-k">Programs need their loader</p><p class="sx-t-say">A binary on the list still won't start</p><p class="sx-t-fix">It needs execute rights on the dynamic loader too. Allow <code>/usr</code>.</p></li>
<li><p class="sx-t-k">The wrong Python</p><p class="sx-t-say"><code>python3</code> is Hermes's virtual environment</p><p class="sx-t-fix">Leave it off the worker's PATH.</p></li>
<li><p class="sx-t-k">Two spellings of a proxy</p><p class="sx-t-say">curl ignores <code>HTTPS_PROXY</code> when <code>https_proxy</code> is set</p><p class="sx-t-fix">Set both cases.</p></li>
</ul>
<figcaption id="sx7-cap">What each trap looks like, then what fixed it.</figcaption>
</figure>

## Where it stands

Hermes runs in OpenShell, the credential and inheritance rules are mapped, and the cage works. A Forge profile with its own SOUL.md and a tool-call hook answers through Hermes's API.

What's left is the bulk of it. Forge's scripts still have to be ported to the Dell. Then a dry run, with no pushes and no messages, and only then the cutover: the @forge Telegram bot moves, the keys go into OpenShell providers, and the copies on the mini are deleted. Forge's dashboard stays on the mini and pulls its data from the Dell.

Until then, Forge runs where it always did: OpenClaw on the Mac mini, workers in srt.

<figure class="fd-fig sx sx-status" aria-labelledby="sx8-cap" data-desc="Where the move stands on October 24, one row per piece. Hermes in OpenShell, on Podman, deny by default, Sonnet 5.5: live. Keys and rules, placeholders, endpoints and inheritance: mapped. The worker cage, Landlock, seccomp and the relay, with claude -p and the reviewer answering: tested. Forge as a Hermes profile with its own SOUL.md and a hook that blocked rm: tested. Porting Forge's scripts to the Dell: not yet. The dry run, no pushes and no messages: not yet. The cutover, moving the Telegram bot and the keys and deleting the mini's copies: not yet. Until then Forge runs on OpenClaw on the Mac mini, with its workers in srt.">
<ol class="ea-card" aria-label="The move, piece by piece">
<li><span class="ea-card-k">Hermes</span><span class="ea-card-v">In OpenShell on Podman, deny by default, Sonnet 5.5</span><b class="ea-pill">Live</b></li>
<li><span class="ea-card-k">Rules</span><span class="ea-card-v">Placeholders, endpoints, inheritance</span><b class="ea-pill ea-pill-mid">Mapped</b></li>
<li><span class="ea-card-k">Cage</span><span class="ea-card-v">Landlock, seccomp, relay; <code>claude -p</code> and the reviewer answered</span><b class="ea-pill">Tested</b></li>
<li><span class="ea-card-k">Profile</span><span class="ea-card-v">Its own SOUL.md; a hook that blocked <code>rm</code></span><b class="ea-pill">Tested</b></li>
<li class="ea-row-no"><span class="ea-card-k">Port</span><span class="ea-card-v">Forge's scripts on the Dell</span><b class="ea-pill ea-pill-no">Not yet</b></li>
<li class="ea-row-no"><span class="ea-card-k">Dry run</span><span class="ea-card-v">No pushes, no messages</span><b class="ea-pill ea-pill-no">Not yet</b></li>
<li class="ea-row-no"><span class="ea-card-k">Cutover</span><span class="ea-card-v">The bot and the keys move; the mini's copies are deleted</span><b class="ea-pill ea-pill-no">Not yet</b></li>
</ol>
<p class="mp-strip"><b>Until then</b><span>Forge runs on OpenClaw on the Mac mini, workers in srt.</span></p>
<figcaption id="sx8-cap">The state on October 24. The red rows are the part that takes the longest.</figcaption>
</figure>

## Try it

NemoClaw is the short path: it installs OpenShell, sets up OpenClaw or Hermes inside a sandbox and starts you on a deny-by-default policy. Read the bootstrap script before you run it, pin a version, and on Linux without root, use Podman.

If you want more:

- [OpenShell on GitHub](https://github.com/NVIDIA/OpenShell), Apache 2.0, with the [policy docs](https://docs.nvidia.com/openshell/latest/how-it-works/policies/overview)
- [NemoClaw on GitHub](https://github.com/NVIDIA/NemoClaw), the installer I used
- [Hermes Agent](https://github.com/NousResearch/hermes-agent), from Nous Research
- [Landlock in the kernel docs](https://docs.kernel.org/userspace-api/landlock.html), the unprivileged sandbox under the cage
- [Hope isn't security: the five parts of an enterprise agent](/notes/hope-isnt-security-enterprise-agents.html), the note this one follows
- [OpenClaw and Hermes Agent ship the same features with different defaults](/notes/hermes-agent-vs-openclaw.html), where both run tools on the host by default

If your agent starts processes that run code you didn't write, look at what those children inherit before you trust the sandbox around the parent. And if you've fenced workers inside OpenShell a different way, tell me how. I'd like to compare cages.
