Want Hermes to use a specific cloud sandbox as its terminal? Now you write a plugin — no waiting for core changes

You’ve always wanted Hermes to run terminal commands in a particular cloud sandbox — safer code, cleaner environments, and no more local dependency conflicts. Then you open the docs and see only the built-in backends listed. The one you want means waiting for the maintainers to merge vendor code into the core repo, or maintaining your own fork forever. PR #94400, merged on August 25, ends that wait: terminal backends are now a pluggable subsystem. A third-party cloud sandbox can register itself as a terminal.backend value through a standalone plugin — no core changes, no fork.
Why this was the “last big one”
Across Hermes’ tool ecosystem, image/video generation, web scraping, browser, memory, TTS/STT, and model providers all supported plugin-style integration long ago — except terminal backends. Terminals touch too many sensitive spots: approvals, container paths, caches, secret stripping — the largest blast radius of any subsystem. PR #94400 closes that gap: sandbox vendors can now write a plugin and let users set terminal.backend: <your-backend-name> directly.
There’s a real first consumer behind this: the Sprites cloud-sandbox backend (originally #93523) was the first to migrate onto this interface — it moved to a standalone private plugin repo instead of living in core.
What a plugin looks like: an ABC + a registration function
The mechanism centers on two new files: agent/terminal_env_provider.py defines the TerminalEnvironmentProvider abstract base class, and agent/terminal_env_registry.py is a thread-safe registry. What a plugin author does (the minimal example from the official developer guide, developer-guide/terminal-environment-plugin.md):
# ~/.hermes/plugins/acmebox/__init__.py
from agent.terminal_env_provider import TerminalEnvironmentProvider
class AcmeBoxEnvironment:
"""Must satisfy the BaseEnvironment duck-typed contract."""
def __init__(self, cwd, timeout, task_id):
self.cwd, self.timeout, self.task_id = cwd, timeout, task_id
def execute(self, command, timeout=None, **kwargs):
... # run the command in the sandbox
return {"output": "...", "exit_code": 0}
def cleanup(self):
... # tear down / detach
class AcmeBoxProvider(TerminalEnvironmentProvider):
name = "acmebox"
display_name = "AcmeBox"
is_remote = True # commands don't run on the host
is_container = True # container-style path/cwd semantics
@property
def cache_path_base(self):
return "~/.hermes" # where synced cache files land, or None
@property
def strip_env_keys(self):
return frozenset({"ACMEBOX_TOKEN"}) # secrets stripped from subprocesses
def create_environment(self, *, cwd, timeout, task_id="default",
image=None, container_config=None, **kwargs):
return AcmeBoxEnvironment(cwd, timeout, task_id)
def register(ctx):
ctx.register_terminal_environment_provider(AcmeBoxProvider())
The plugin directory also carries a plugin.yaml (name, version, kind: backend). Then enable and select it:
hermes plugins enable acmebox
hermes config set terminal.backend acmebox
Built-in backend names (local, docker, singularity, modal, daytona, vercel_sandbox, ssh) are reserved — plugins extend the set, never shadow a built-in.
Six classification flags that kill the “new backend missed a site” bug class
In the past, adding a backend meant syncing judgment logic across seven or eight places in the code — which backends are remote, which are containers, which skip approvals, how cache paths translate — and missing one was a hard-to-find bug (issue #30112 needed a seven-site sweep). The new design declares all of it:
is_remote: commands run somewhere other than the host. Suppresses host OS/home/cwd hints, the host Python env probe, and remote-aware skill handling;is_container: behaves like a container/sandbox with its own filesystem — container resource config passes through, host-looking cwds are sanitized, file tools use container path resolution;skip_container_guards: the sandbox is isolated enough that dangerous-command approval prompts are skipped (defaults tois_container; backends that can mount host paths should override to False);cache_path_base: where auto-synced~/.hermes/cachefiles land inside the backend (e.g.~/.hermesor/root/.hermes), or None when nothing needs translating;strip_env_keys: credential env vars owned by this backend (vendor API tokens), stripped from every subprocess the agent spawns so model-written commands can never read them;session_isolated_when_nonpersistent: non-persistent mode gives each session its own sandbox identity instead of sharing one.
Where the plugin shows up once registered
A registered backend isn’t just a config-file value — every surface picks it up automatically:
- The
hermes setupbackend picker shows the new option with vendor-guided configuration; hermes status/hermes doctorlist the plugin backend and its health;- The dashboard’s terminal-backend picker supports plugin backends and recomputes per request — a plugin installed mid-session appears immediately.
The official developer guide walks through writing a backend plugin from scratch.
What this means for ordinary users
If you only use the built-in backends (local terminal, Docker, Modal, SSH), this change is behaviorally invisible — it’s an architectural door, not a behavior switch. The ecosystem impact is what matters: when a sandbox vendor says “supports Hermes”, it now means “install the plugin”, not “wait for a core merge”; plugin quality is the vendor’s responsibility, and hermes doctor tells you whether it’s healthy. For plugin basics, see the hermes plugins command reference; for the entry-point mechanism behind pip-installed providers, our pip model-provider plugin guide covers the lineage. Backend selection day-to-day is documented in the install guide.
Summary
Pluggable terminal backends turn “get Hermes to use my cloud sandbox” from “beg for a core merge” into “write a plugin, declare six flags, register, done”. It’s the last piece of Hermes’ tool ecosystem to go plugin-native — third-party backends are now decoupled from core, with security policy enforced uniformly through declarative flags.