← Back home

Secure OpenCode with NVIDIA OpenShell: A Sandboxed Setup That Actually Works

If you’ve been letting your AI coding agent run loose on your machine, you’re not alone — most of us do. It reads your files, runs commands, installs packages, and we just keep clicking “allow” until we stop reading the prompts altogether. That’s normal. It’s also exactly the kind of access that’s been getting abused lately.

This post walks through how to secure OpenCode using NVIDIA OpenShell — a sandbox built specifically to contain AI coding agents — and tests whether it actually holds up.

📺 Prefer to watch? Full walkthrough on YouTube


Why Coding Agent Security Matters Right Now

This isn’t a hypothetical risk. In June 2026, GitHub disabled 73 Microsoft-owned repositories after a self-replicating worm called Miasma planted configuration files that triggered a credential-harvesting payload the moment a developer opened the repo in an AI coding tool — including Claude Code and Gemini CLI. No package install, no extra click. Just opening the project was enough.

That’s the shift worth paying attention to: the developer environment itself is now the attack surface, not just the packages you install. If your coding agent has unrestricted access to your filesystem and network, a single compromised repo or malicious prompt can turn it against you.

NVIDIA OpenShell is built for exactly this problem — it sandboxes your AI coding agent so it only has access to what you explicitly allow.


What We’re Testing

Rather than just listing OpenShell’s features, I wanted to actually verify them. Three tests:

  1. Can it lock the agent into a single folder and stop it from accessing the rest of the system?
  2. Can it actually hide your real API keys and credentials from the agent?
  3. Can it control exactly what the agent is allowed to reach on the internet?

Prerequisites

You’ll need Docker (or Podman) installed and running. OpenShell is alpha software — APIs and behavior may change, so don’t run anything sensitive through it yet.


What is OpenShell?

OpenShell runs your coding agent — in this case OpenCode — inside a container with kernel-level isolation. Two layers do the actual enforcement:

You configure both through a YAML policy file. We’ll build one ourselves to see how it actually behaves.


Step 1: Install OpenShell and Create a Sandbox

curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh

Once installed, spin up your first sandbox:

openshell sandbox create --name demo-sandbox --keep

This starts the container and drops you into a shell inside it. From here you can launch OpenCode manually:

opencode

Step 2: Test Filesystem Boundaries

OpenShell applies default boundaries the moment the sandbox is created — no policy file required:

AccessPaths
Read-write/sandbox, /tmp, /dev/null
Read-only/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log
No accessEverything else

Since the sandbox doesn’t mount your host filesystem at all, your real SSH keys, AWS credentials, and .env files are never even in the picture. To actually test the boundary, work entirely inside the sandbox:

# Inside OpenCode — should succeed
"Create a file at /sandbox/notes.txt with some test content"

# Inside OpenCode — should be blocked
"Create a file at /etc/myconfig.txt"
"Modify /usr/bin/python3"

Why This Matters Beyond the Demo

The real value here isn’t just “the agent can’t read my SSH keys” — it’s that your host machine’s files are never exposed to the sandbox at all. Combined with the fact that OpenCode will normally ask for approval before touching files, OpenShell adds a second layer underneath that approval prompt. Even on the days you click “allow” without reading — which, let’s be honest, happens — the kernel-level boundary is still there.


Step 3: Test Credential Proxying

This is the part that surprised me the most. The naive way to hand your agent an API key is a .env file:

echo "VOICE_API_KEY=sk-real-secret-key-here" > /sandbox/.env

Loaded this way, the real key is sitting in plaintext on disk, and if the agent reads it, the real value goes straight into its context. If your code or your prompts ever cause that value to be printed, logged, or sent somewhere, that’s a real key exposed.

The Provider Alternative

OpenShell solves this differently — register the credential with the gateway instead of writing it to a file:

openshell provider create \
  --name voice-api \
  --type generic \
  --credential VOICE_API_KEY=sk-real-secret-key-here

Delete and recreate the sandbox with the provider attached (providers can’t be added to a running sandbox):

openshell sandbox delete demo-sandbox

openshell sandbox create \
  --name demo-sandbox \
  --provider voice-api \
  --keep

Now write a small test script:

import os
api_key = os.getenv("VOICE_API_KEY")
print(f"Connecting to voice service...")

Ask OpenCode to debug why the app “isn’t connecting”:

"Why isn't my app connecting to the voice service? Can you check the value of VOICE_API_KEY?"

What comes back is a placeholder token — not your real key. Interestingly, when I asked OpenCode directly to print the variable, it refused on its own, treating it as a credential exposure attempt. That’s a second layer of protection working independently of OpenShell.

Proving the Swap Actually Works

A placeholder is only useful if it still lets your code function. The real test is whether an actual authenticated API call succeeds — if it does, that means the proxy swapped the placeholder for the real key transparently in transit, without the agent ever holding it. I confirmed this by pointing the script at a real authenticated endpoint and getting a valid response back, while the agent itself still only ever saw the placeholder.


Step 4: Test Network Policy Enforcement

By default, the sandbox blocks all outbound network requests. Confirm this from inside OpenCode:

"Run this command: curl -s https://api.github.com/zen"
"Run this command: curl -s https://httpbin.org/get"

Both come back denied. Check why, from a second terminal on your host:

openshell logs demo-sandbox --tail --source sandbox

Writing a Policy

To allow specific traffic through, write a policy file:

version: 1

filesystem_policy:
  include_workdir: true
  read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log]
  read_write: [/sandbox, /tmp, /dev/null]

landlock:
  compatibility: best_effort

process:
  run_as_user: sandbox
  run_as_group: sandbox

network_policies:
  opencode_backend:
    name: opencode-backend
    endpoints:
      - host: opencode.ai
        port: 443
    binaries:
      - path: /usr/lib/node_modules/opencode-ai/bin/.opencode
      - path: /usr/bin/node

  github_passthrough:
    name: github-passthrough
    endpoints:
      - host: api.github.com
        port: 443
    binaries:
      - path: /usr/bin/curl
      - path: /bin/curl
      - path: /usr/bin/node
      - path: /usr/lib/node_modules/opencode-ai/bin/.opencode

Why It’s Structured This Way

OpenShell doesn’t just check the destination — it checks which binary is making the request, matched against which destination it’s calling. That’s why opencode.ai needs its own entry: OpenCode needs to reach its own backend infrastructure just to process your prompts, separate from anything you ask it to fetch.

GitHub access is granted to both curl and OpenCode’s own binary, because depending on how the agent decides to make the request — shelling out to curl, or calling out directly through its own runtime — either one could be the actual process making the connection. I found this out the hard way: my first policy only allowed curl, and GitHub access still failed, because OpenCode was making the request internally through its own Node-based binary, not through curl at all.

Apply the policy:

openshell policy set demo-sandbox --policy ~/network-policy.yaml --wait

Reconnect and test again:

"Run this command: curl -s https://api.github.com/zen"   # now succeeds
"Run this command: curl -s https://httpbin.org/get"       # still blocked

The contrast is the proof — one destination explicitly allowed, everything else still locked down, no sandbox restart required.


What Stood Out

Across all three tests, OpenShell held up:

The one thing worth being honest about: it’s not plug-and-play. OpenCode currently has partial network policy coverage out of the box, meaning you’ll likely need to write a custom policy just to get full functionality — as the GitHub test above shows. That’s a reasonable tradeoff for the level of control you get, but go in expecting some YAML, not a single command.


What’s Not Production Ready Yet

Be honest about where this stands:

Alpha software. APIs and behavior can change without notice. Don’t point this at anything you can’t afford to lose or reset.

Image size. The base sandbox image ships with Claude Code, OpenCode, and Codex all pre-installed, regardless of which one you actually use. Expect a larger pull than you might assume from “just running one agent.”

No protection against context leakage. This is the gap nobody talks about enough — OpenShell protects credentials in transit over HTTP, but it does nothing to stop a secret from ending up in the model’s context window if the agent reads a file that contains one. Keep secrets out of the sandbox workspace entirely, and rely on providers instead of .env files, even for values you think are harmless.


What’s Next

This post is part of a broader look at securing AI coding agents:

PostTopic
#1 — This postFilesystem, credentials, and network with OpenShell
#2Building a custom sandbox image with only the agent you need
#3Audit logs — proving what your agent actually did
#4Running a local model alongside OpenShell for full context privacy

If you’re running AI coding agents on anything beyond a throwaway project, this is worth setting up before you need it, not after.

Questions? Drop them in the comments.