GUIDE · HARDENING 10 min ·

Hardening OpenCode for productionThe OSS agent stack, made boring

OpenCode's flexibility is exactly why teams pick it. That same flexibility, unmanaged, hands an agent your secrets, your shell, and your network. Here is what to lock down before you ship.

TL;DR· the answer, in twenty seconds

What: OpenCode inherits your full shell environment, executes arbitrary shell commands, and loads MCP servers with no built-in allowlist or credential redactor. Any secret exported in your shell is one agent session away from landing in model context or a state file.

Fix: Scope the workspace to a dedicated directory, launch OpenCode under a separate Unix user, strip secrets from the inherited env and inject them at exec time, add an MCP server allowlist, and filter outbound traffic. Config snippets below.

Lesson: An OSS agent that runs on your hardware and your model still follows the same failure modes as any other coding agent. The attack surface is the process, not the vendor.

OpenCode landed in mid-2026 with a compelling pitch: bring your own model. Point it at a local Ollama server running Qwen 3.7, or wire it to a DeepSeek API endpoint. Pay nothing to Anthropic. Keep inference on your network. The GitHub star count tells you how many teams took that pitch seriously.

What the README does not linger on: OpenCode reads your shell environment at launch, executes shell commands on your host, and connects to any MCP server you name in config without verifying a signature or a hash. The model is on your hardware. The secrets that leak through are not.

Knostic's February 2026 research focused on Claude Code, but the failure mode it described (agent reads ambient env, persists it to disk, disk ends up somewhere it should not) applies to every tool in this category. OX Security's early 2026 MCP ecosystem report counted roughly 7,000 MCP servers in the wild, around 150 million downloads total, with no signature requirement across the ecosystem. OpenCode is not the exception here. It is the default.

The good news: all of this is fixable without abandoning the tool. The tradeoffs are real, but they are manageable.

What OpenCode does with secrets out of the box

At launch, OpenCode forks from your shell. It inherits every exported variable in that shell, which typically includes AWS_ACCESS_KEY_ID, DATABASE_URL, OPENAI_API_KEY, and whatever else you set in .zshrc or .bashrc over the years. There is no deny list. There is no filter.

Those variables sit in the agent's process environment for the entire session. When the model generates a shell command and OpenCode executes it, the child process inherits the same environment. A command that reads $DATABASE_URL to test a migration has access to the same production DSN as a command that should only be touching test fixtures.

OpenCode also writes state to disk. The location varies by config, but the pattern mirrors what Knostic found in Claude Code: session data, accepted permissions, and tool outputs land in a predictable path inside or near your project directory. If that directory is under version control, the state file can travel.

MCP servers extend the surface further. OpenCode accepts a servers block in its config file that lists which MCP servers to start. Those servers run as subprocesses and can expose arbitrary tools to the model. A server advertising a read_file tool can read files. A server advertising a run_query tool can run queries. The MCP specification does not require the host application to sandbox these servers or verify their provenance.

This is not a backdoor. It is a design that assumes you trust everything in your config. In development, that assumption is usually fine. In production, on shared infrastructure, or in any environment that holds real credentials, it is not.

What to know in 60 seconds

  • OpenCode inherits your full shell env at launch, including every secret you have ever exported.
  • Shell exec runs under your current user, with your current file permissions, on your current network.
  • MCP servers in your config start automatically and can expose tools to the model with no additional approval step.
  • OpenCode ships no built-in secret redactor. Anything that reaches model context can reach model output.
  • State files land near your project directory by default. They follow your project wherever it goes.

Scope the workspace before anything else

The first change costs nothing and stops the broadest category of drift. Run OpenCode from a dedicated working directory that contains only the files it needs to touch. Do not launch it from your home directory or from a monorepo root that spans unrelated projects.

mkdir -p ~/work/myproject-agent
cd ~/work/myproject-agent
# Symlink or mount only the subdirectory the agent needs
ln -s ~/work/myproject/src ./src
ln -s ~/work/myproject/tests ./tests
opencode

This does not prevent the agent from reading environment variables or executing shell commands outside the directory, but it narrows what the agent touches by default and reduces what state files capture. Combine it with the process isolation below and the blast radius shrinks considerably.

Set the OpenCode workspace path explicitly in config so it does not drift:

{
  "workspace": "/home/agentuser/work/myproject-agent",
  "stateDir": "/home/agentuser/.local/share/opencode/myproject"
}

Moving stateDir outside the project tree means state files do not follow the project into git or into a published package.

Run OpenCode under a separate Unix user

This is the highest-leverage hardening step and the one teams skip because it takes ten minutes. Create a dedicated system account for agent sessions. That account owns nothing it should not touch. It has no access to production databases, no AWS credentials, no home directory full of cached tokens.

# Linux
sudo useradd -m -s /bin/bash agentuser
sudo -u agentuser opencode

On macOS, use dscl or create the account in System Settings, then:

sudo -u agentuser /usr/local/bin/opencode

The new user needs read access to the source tree and write access to its own home directory. Nothing else. SSH keys, cloud credentials, and database passwords that live in your home directory are invisible to it.

This breaks anything that relies on your personal credentials being present, which is exactly the point. Wire the agent's model endpoint access through environment variables you set explicitly in the sudo -u agentuser env call, not through files the account inherits from your profile.

Strip the inherited env, inject what you need

Launching OpenCode with a clean environment and adding back only the variables it requires takes the ambient-secret problem off the table entirely.

env -i \
  HOME=/home/agentuser \
  PATH=/usr/local/bin:/usr/bin:/bin \
  OPENCODE_API_BASE=http://localhost:11434/v1 \
  OPENCODE_MODEL=qwen3:7b \
  opencode

The -i flag to env clears the inherited environment completely. You add back what you need. OPENCODE_API_BASE points at your local Ollama endpoint. OPENCODE_MODEL selects the model. No database URLs, no cloud tokens, no stray API keys from projects you worked on last month.

If the agent needs database access for a specific task, inject that credential for that task only:

env -i \
  HOME=/home/agentuser \
  PATH=/usr/local/bin:/usr/bin:/bin \
  OPENCODE_API_BASE=http://localhost:11434/v1 \
  OPENCODE_MODEL=qwen3:7b \
  DATABASE_URL="$(get-secret db/myproject/test)" \
  opencode

The get-secret call above is a placeholder for whatever secret retrieval mechanism you use. The point is that the value appears for this process and is gone when the process exits.

A broker like hasp automates this pattern across every agent session. Instead of writing a wrapper script per credential, you bind secrets once in the hasp vault, then launch OpenCode with hasp run -- opencode. The broker handles the env -i setup, injects the right value at exec time, and logs every grant to an append-only audit trail. The manual env -i pattern is the right shape; hasp makes it the default rather than a discipline you have to enforce.

One tradeoff worth naming: any OpenCode feature that auto-detects cloud credentials (AWS, GCP, Azure) by reading standard environment variable names or credential files will stop working. That is the correct outcome. Agent processes should not auto-detect production cloud credentials. Wire the specific access you want, explicitly, for the specific session.

Enable shell command logging

OpenCode can execute shell commands. You want a log of what it ran, when, and what the output was. Without that log, reconstructing what happened in a session that touched production data requires reading model context from memory.

Most Linux distributions ship auditd. A minimal rule set to capture exec calls from the agent user:

# Add to /etc/audit/rules.d/opencode.rules
-a always,exit -F arch=b64 -S execve -F uid=agentuser -k opencode-exec

Then reload:

sudo auditctl -R /etc/audit/rules.d/opencode.rules

Query the log after a session:

sudo ausearch -k opencode-exec --start today

On macOS, use script to wrap the session:

script -a ~/logs/opencode-$(date +%Y%m%dT%H%M%S).log \
  env -i HOME=/home/agentuser PATH=/usr/local/bin:/usr/bin:/bin \
  opencode

The script approach captures everything the terminal sees, including tool calls and outputs. It is crude but effective for audit purposes. Store these logs somewhere the agent user cannot write.

Restrict the MCP server allowlist

OpenCode loads MCP servers listed in its config. The risk is not that OpenCode ships malicious servers. The risk is that the config can grow to include servers installed from the internet, servers shared across projects with different trust levels, or servers that expose more capability than the agent needs for a given task.

Start from an empty servers block and add only what you have reviewed:

{
  "servers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/home/agentuser/work/myproject-agent"],
      "env": {}
    }
  }
}

This config loads one MCP server. That server exposes filesystem access scoped to the agent's working directory. No database tools. No shell execution tools. No servers pulled from a third-party registry whose code you have not read.

GitGuardian's research blog has documented MCP server supply chain risk in 2026 alongside the broader secrets-in-code problem. The mechanism is the same as any npm package: a popular server gets compromised, the malicious version ships to whoever updates next. Pin versions. Review changelogs.

For servers you write yourself, keep the source in the same repo as the project, review it in the same code review process, and do not share a single server instance across projects with different sensitivity levels.

Filter outbound network traffic

With a local model, OpenCode does not need internet access to run inference. The only outbound traffic the agent process should generate is to the model endpoint and to whatever APIs you explicitly authorize.

On Linux with nftables, a simple egress policy for the agent user:

# Allow agent user to reach only localhost and a specific API host
nft add rule ip filter OUTPUT skuid agentuser ip daddr != 127.0.0.1 ip daddr != 192.0.2.10 drop

Replace 192.0.2.10 with the actual IP of your DeepSeek or Qwen API endpoint if you are using a remote model. If you are running Ollama locally, the rule becomes simpler: allow only 127.0.0.1.

On macOS, the built-in application firewall operates at the application level, not the user level. Use pf for user-level filtering or reach for a network-layer proxy. The MCP docs describe server transport options that can help here: running MCP servers on a Unix socket instead of a TCP port keeps their traffic off the network interface entirely.

Outbound filtering breaks anything that requires the agent to fetch packages, call external APIs, or clone repos. Decide which of those capabilities you want the agent to have, and open exactly those ports. Everything else stays closed.

What gets missed in the hardening conversation

Teams harden OpenCode's secrets exposure and stop there. The shell execution surface is the bigger problem.

A secret in the agent's environment can be read by the model. A shell command executed by the agent runs as the agent user, with the agent user's file permissions, against the agent user's network access. If that user can write to /etc/cron.d or has sudo access to restart services, the agent can do those things too, with or without any secret in the environment.

Principle of least privilege for the Unix user matters more than env var filtering. The Unix user sets the ceiling on what the agent can do. Env var filtering reduces what the agent knows, but the agent can learn things by running shell commands. A shell command that runs aws sts get-caller-identity tells the agent what AWS account it is in, regardless of whether AWS_PROFILE was in the environment at launch.

The second thing that gets missed: state files from past sessions. Aider's documentation mentions its own session history format. OpenCode has its own. These files accumulate. A state file from six months ago may hold tool outputs containing data that was sensitive then and is sensitive now. If you move the project, the file moves. If you publish the project directory, the file publishes.

Run a scan before you ship anything:

trufflehog filesystem ./myproject-agent --results=verified

TruffleHog and Gitleaks both pattern-match on common credential formats. Neither is exhaustive, but both catch the obvious cases.

A checklist you can paste into a PR

## OpenCode production hardening

- [ ] OpenCode runs from a scoped working directory (not home, not monorepo root)
- [ ] stateDir configured to a path outside the project git tree
- [ ] Agent runs under a dedicated Unix user (not your dev account)
- [ ] Agent user has no write access to production systems, cron, or service restart
- [ ] OpenCode launched with env -i, only required vars passed explicitly
- [ ] Shell command logging enabled (auditd or script wrapper)
- [ ] MCP server list starts empty; only reviewed, pinned servers added
- [ ] MCP server versions pinned in config; changelogs reviewed on update
- [ ] Outbound egress filtered to model endpoint and explicitly authorized hosts
- [ ] trufflehog or gitleaks scan run against working directory before each release
- [ ] Session log files stored outside agent-writable directories
- [ ] .gitignore covers opencode state dirs for this project

What this means for your stack

The hardening steps above buy you time and reduce surface area. They do not solve the underlying problem: the agent process still has access to secrets at runtime, the agent still executes shell commands, and state files still accumulate. Manual hygiene degrades under deadline pressure.

The durable fix changes the model. Secrets live in an encrypted local store. The agent requests access to a specific credential for a specific task. The broker injects the value into one child process at exec time and revokes it when that process exits. State files the agent writes hold references, not values. An append-only audit log records every grant, answering "did the agent touch the prod token between 2 and 4pm Tuesday?" in seconds.

hasp is one working implementation. curl -fsSL https://gethasp.com/install.sh | sh, hasp setup, connect a project, and the next session gets a reference instead of a key. hasp ships a dedicated hermes profile for generic agents and handles env injection, redaction, and audit logging without changes to OpenCode's config. Source-available (FCL-1.0), local-first, macOS and Linux, no account.

OSS models on local hardware reduce inference cost and keep data off third-party servers. The credential problem is orthogonal to where inference runs. An agent reading your shell environment on your machine can cause the same damage as one doing it on a cloud host.

Sources· cited above, in one place

NEXT STEP~90 seconds

Stop handing the agent your real keys.

hasp keeps secrets in one local encrypted vault, brokers them into the child process at exec, and never lets the agent read the value.

  • Local, encrypted vault — no account, no cloud, no telemetry by default.
  • Brokered run — agent gets a reference, the child process gets the value.
  • Pre-commit + pre-push hooks catch managed values before they ship.
  • Append-only HMAC audit log answers "did the agent touch the prod token?" in seconds.
→ okvault unlocked · binding ./api
→ okgrant once · pid 88421
→ okagent never read

macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.

Browse all clusters· eight threads, one index