| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Run Hermes Agent's terminal, file, and code-execution tools inside an E2B cloud sandbox instead of on the host machine.
Hermes itself keeps running wherever you started it. Every shell command the agent issues, every file it reads or writes through the file tools, and every execute_code call is dispatched into an E2B sandbox.
The plugin touches no Hermes core files. It registers through the standard plugin entry point — register(ctx) calling ctx.register_terminal_environment_provider — and everything else follows from the provider's declared metadata: dispatch, the hermes setup backend picker, hermes doctor / hermes status, the dashboard picker, remote path handling, and secret stripping.
It is opt-in and never auto-selected.
See the concise behavior contract for the lifecycle, state, concurrency, and security guarantees the backend provides.
Install the E2B SDK into the same interpreter that runs Hermes. Hermes never installs plugin dependencies for you.
pip install 'e2b>=2.46,<3'Get an API key at https://e2b.dev/dashboard and put it in your Hermes profile's env file:
echo 'E2B_API_KEY=e2b_...' >> ~/.hermes/.envInstall and enable the plugin:
hermes plugins install e2b-dev/hermes-plugin-e2b
hermes plugins enable e2b
hermes config set terminal.backend e2bCheck it:
hermes doctorTo pin a version, install with an explicit commit: hermes plugins install e2b-dev/hermes-plugin-e2b --ref <40-char sha>.
hermes config set terminal.backend local
hermes plugins disable e2b
hermes plugins remove e2bDisabling the plugin removes the backend from the registry. Any persistent sandboxes stay in your E2B account until their lease expires and they pause; delete them from the E2B dashboard if you want them gone immediately.
| Hermes Agent | any build with pluggable terminal backends (PR #94400 or later). Built against main @ 1bbb6e5b and re-verified against main @ e60983a6. |
| E2B Python SDK | >=2.46,<3. Verified against 2.46.0 by reading the installed source. |
| Python | 3.11+ (Hermes' own floor) |
Hermes has no plugin API version handshake for terminal backends — native plugins are protected by behaviour, not a version number. (A manifest may declare an api_version; the loader parses and stores it, but nothing gates a terminal backend on it.) This plugin uses only documented PluginContext and provider-ABC surface, plus tools.environments.BaseEnvironment and FileSyncManager, which the terminal backend guide names explicitly.
That surface has been stable across the whole verification range: the provider ABC (agent/terminal_env_provider.py), the backend registry (agent/terminal_env_registry.py), FileSyncManager (tools/environments/file_sync.py), and the terminal-backend guide itself are byte-identical at 1bbb6e5b and e60983a6, despite roughly 3,500 commits between them. BaseEnvironment changed additively only. What did move is the consumer code around that surface, which is why the contract gaps are re-checked against current main rather than merely re-hashed — all of them still stand, and none has been fixed upstream.
The E2B floor is not conservative padding: it is the version whose Sandbox.create / Sandbox.list / lifecycle behaviour was read from source and relied on. Older releases may work; they have not been checked.
Backend selection uses the standard Hermes keys:
terminal:
backend: e2b
container_persistent: true # default
lifetime_seconds: 300 # default; also the sandbox lease
timeout: 180 # default per-command timeoutPlugin-specific settings live in the plugin's own config namespace:
plugins:
enabled:
- e2b
entries:
e2b:
settings:
template: base # E2B template name or id
cwd: /home/user # starting directory in the sandbox
lease_seconds: 300 # overrides terminal.lifetime_seconds
command_grace_seconds: 30 # lease headroom beyond a command's timeout
allow_internet_access: true # false puts the sandbox behind deny-all egress
secure: true # E2B envd access-token protection
metadata: # extra metadata stamped on every sandbox
team: infra| Setting | Default | Meaning |
|---|---|---|
| template | base | E2B template name or id. Part of sandbox identity: a sandbox built from a different template is never adopted. |
| cwd | /home/user | Starting directory. Overridden by the sandbox's real $HOME when it differs. |
| lease_seconds | terminal.lifetime_seconds, else 300 | Baseline sandbox lease. Extended per command; never shortened. |
| command_grace_seconds | 30 | Extra lease beyond a command's timeout, so E2B's timeout action cannot fire mid-command. |
| allow_internet_access | true | false denies all egress from the sandbox. |
| secure | true | Keep E2B's envd access-token protection on. |
| metadata | {} | Merged into sandbox metadata. Identity keys always win. |
terminal.image and the container_cpu / container_memory / container_disk resource keys are ignored: E2B sizes sandboxes by template, and Hermes does not forward a per-backend image value to plugin backends (see contract gaps).
container_persistent: true (default). The sandbox is created with E2B's lifecycle set to {"on_timeout": {"action": "pause", "keep_memory": false}}. When its lease runs out, E2B pauses it and preserves the filesystem. The next session finds it again and resumes it, so installed packages and working files survive across Hermes restarts.
This plugin never pauses or kills a persistent sandbox. That is deliberate: two Hermes processes attached to the same sandbox can never destroy it under each other. If E2B has already paused the sandbox when Hermes begins teardown, the writer reconnects immediately before every download attempt with a transfer-sized lease, pulls state, and leaves E2B to pause it again. This preserves Hermes' own retry policy without introducing a destructive cleanup decision in either Hermes process.
Concurrent sessions share one sandbox with a single state synchroniser. Two live sessions on the same scope (a second CLI session of the same profile, or a cron run beside one) both attach, but only one at a time — the holder of a per-scope writer lease — runs this plugin's state protocol: resume recovery, the host push, the per-command sync, and the teardown pull. The other sessions run commands normally as readers and take the role over (with a full recover-then-push hand-off) once it is released, at which point host-side changes made during their wait propagate. Without this, a second session's bring-up would push its host snapshot over the sandbox's live state.
Holding the lease is not exclusive write access to the sandbox's ~/.hermes. Readers execute commands, and a sandbox may still be running processes an earlier session started; any of them can write there. What is serialised is the plugin's own synchronisation.
The guarantee is at command admission, not for a command's duration. Two gates decide whether a reader's command is admitted. A session that has not yet attached must find the sandbox stamped as prepared by the writer (it re-stamps after every completed push, and each new writer generation invalidates the previous stamp before touching state). Every reader, on every command, must find the scope not marked dirty on the host — the scope is marked while a writer transition is in flight and after any failed incremental sync, and cleared once a push or sync has established the host's state. Either gate failing means the command fails closed with a retry hint instead of running against a sandbox with no, half-written, or knowingly stale Hermes state. The marker lives in ~/.hermes/cache/e2b/, which Hermes does not mirror into sandboxes, so it still answers when the sandbox transport is the thing that broke — and if that file cannot be written or read at all, the plugin says so once at WARNING level, because the per-command gate is then not working.
Only the writer ever creates a persistent sandbox, and a writer whose bring-up or attach fails releases the role, so a session that cannot do the work never freezes the scope.
Caveats:
container_persistent: false. Every session gets its own fresh sandbox, never adopted from anywhere, destroyed on teardown, and created with on_timeout: kill so an abandoned one dies on its own. Its lease is never shorter than terminal.lifetime_seconds plus one 60-second Hermes reaper interval plus E2B 2.46's 60-second control-plane request timeout; normal idle cleanup therefore has time to renew before pulling state and killing explicitly ahead of the abandoned-resource backstop. The lifecycle margin is independent of command_grace_seconds, so setting command grace to zero does not recreate the reaper/kill race. The plugin does not guess an account-specific maximum lease: if E2B rejects the requested lifetime, Hermes reports the lifecycle operation and requested duration as a configuration/infrastructure error.
A sandbox belongs to the scope Hermes already resolved for its own environment cache. Concretely:
Identity is carried as E2B sandbox metadata, not a file on your machine. The scope value is a SHA-256 digest of (profile home, task id, template, persistent/ephemeral, internet access, secure) — see the security section for why the last three are part of identity. Your session ids and home path never leave the host. There is no local pointer store to corrupt, and a sandbox whose creation response was lost is still found on the next run instead of being billed forever with nobody holding its id.
Hermes owns the command deadline. The plugin extends the sandbox lease to max(lease_seconds, command timeout + command_grace_seconds) before every command, so E2B's timeout action can never fire while a command is running. Normal ephemeral environments also apply the idle-reaper floor described above; prompt probes retain their dedicated short kill lease because Hermes never registers them with the reaper.
Leases are extended with the class-level Sandbox.connect(id, timeout=…), which the SDK documents as extend-only for a running sandbox. The returned SDK object is retained because a resumed sandbox may have new envd connection metadata; the instance method discards that response in E2B 2.46.0. set_timeout() is deliberately unused: it can shorten a lease, which on a shared sandbox would let a short command cut a long one short.
Command reconnects resolve the API key from the current Hermes profile at the moment of renewal, so key rotation takes effect. Teardown reconnects instead inherit the already attached sandbox's SDK connection parameters: cleanup may run after the profile secret scope has disappeared, and it must not consult a different profile's key. Neither key is logged or persisted by the plugin.
Timeouts and Ctrl-C kill the command, by pid. The sandbox survives, and so does anything else running in it — including background processes Hermes started through its process registry. (The built-in Daytona and Modal backends stop the whole sandbox to cancel one command.)
Output is forwarded chunk by chunk as E2B delivers it, into the pipe Hermes drains, so its bounded head/tail collector can start evicting immediately and a verbose command cannot grow host memory without bound. The E2B SDK's own command handle accumulates every chunk in an uncapped list; the plugin drops that copy as it goes. If a future SDK removes those internals the plugin logs a one-time warning and falls back to the SDK's buffering rather than breaking.
The host owns ~/.hermes. The sandbox owns its workspace.
On bring-up and before each command, Hermes' own FileSyncManager uploads the files it syncs to every remote backend — registered credential files, skills, and the cache directories (uploaded documents, images, screenshots) — to $HOME/.hermes inside the sandbox. On teardown it pulls remote changes back. This plugin only supplies the transport; the change detection, the transactional rollback, the credential upload-only rule, the 2 GiB archive cap, and the cross-process lock are all Hermes'.
Everything else the agent creates lives in the sandbox and stays there, which in persistent mode means it is still there next session.
When a sync fails. Hermes' manager reports neither success nor failure — it logs and returns — so this plugin recovers the signal itself:
Recovery on resume. Because a pull can fail — and because a hard crash skips it entirely — resuming a persistent sandbox pulls agent-authored state before pushing the host snapshot over it. The guarantee covers the skills/ and memories/ trees as recovery's snapshot of the sandbox found them: every file there is preserved in place, parked where a human can find it, or named in a warning. Other synced paths are not recovered — a sandbox-side change under cache/documents/ and the other mounted cache trees is overwritten by the host copy, which is the intended direction for them (the host owns that content; the sandbox gets a copy to read).
This closes the data-loss window the in-tree remote backends still have, without a durable sync baseline (which is core surface a plugin cannot add).
One window it does not close. A sandbox process — a daemon the agent started, or a background command from an earlier session — can write to ~/.hermes inside the sandbox after recovery's snapshot is taken and before the force-push lands. Nothing can drain those processes, so:
Both directions are pinned by a test, so the boundary is a stated contract rather than an assumption.
| Trigger | Persistent | Ephemeral |
|---|---|---|
| Session close / idle reaping / atexit | reconnect if needed, pull state back, detach; E2B pauses the sandbox at lease expiry | reconnect for the pull window, pull state back, kill the sandbox |
| Cleanup arriving during a command | deferred until the command finishes | deferred until the command finishes |
| Python finalising a dropped reference | releases locally, never pulls state | kills the sandbox, never pulls state |
| Repeated cleanup | no-op | no-op |
| Sandbox already gone | no-op | no-op |
Ordinary reconnect, sync-back, scratch-removal, and kill failures are logged without skipping mandatory release/reset/destruction. Python process-control exceptions are different: KeyboardInterrupt, SystemExit, and GeneratorExit are deferred until that mandatory cleanup has completed and then re-raised. This preserves Hermes' own sync-back SIGINT contract instead of silently turning Ctrl-C into a successful cleanup.
hermes doctor says the backend is not configured. Check both rows: the SDK must be importable by the interpreter running Hermes, and E2B_API_KEY must be set for the active profile (~/.hermes/.env, or the profile's own .env under a multiplexed gateway).
Commands fail with an infrastructure error. "E2B … failed" messages mean the backend, not your command: the E2B API is unreachable, the key is rejected, the sandbox vanished, or state could not be synced. Hermes retries the command a few times against the same environment first (transient failures self-heal because each retry re-runs bring-up and the state sync). Only an error raised while the environment is being created is rendered as a structured status: "degraded" result with a retry hint and evicts the cached environment — this plugin attaches lazily, so most of its failures surface on execute and are reported as a plain error instead (a reported contract gap).
"Sandbox … is not ready: this scope's Hermes state is being rebuilt or is known to be stale." Another session sharing this persistent scope is preparing the sandbox, or its last state sync failed. Retry — the command proceeds as soon as that session completes a sync, and this session takes the role over if it is released. If it persists, look in the other session's log for an E2B sync error: something is stopping state from reaching the sandbox, and running against it anyway is what this refuses to do.
The plugin does not appear in hermes plugins list. It must be enabled: hermes plugins enable e2b. Check the log for Skipping 'e2b' (not in plugins.enabled).
A new chat gets a fresh sandbox. Expected under the current core key resolution — see the identity section above.
Duplicate sandboxes for one scope. The plugin logs a warning naming the count and adopts the newest. Two Hermes processes cold-starting at the exact same moment can produce this despite the cross-process creation lock (for example on a filesystem without flock). The extras pause on their own leases; delete them from the E2B dashboard.
Something ran on the host instead of the sandbox. Confirm terminal.backend is e2b (hermes status). Processes that skip Hermes' launchers read the backend from config.yaml via core's own bridge, so a stale TERMINAL_ENV export in your shell can win — unset it.
python3 -m venv .venv && .venv/bin/pip install -e '.[test]'
.venv/bin/pip install 'e2b>=2.46,<3'
.venv/bin/pip install -e /path/to/hermes-agent # for the integration tests
.venv/bin/python -m pytestThe suite runs against a behavioural double of the E2B SDK and a real Hermes installation. It does not touch the network.
Live tests that create real, billable sandboxes are marked live and deselected by default:
E2B_API_KEY=... .venv/bin/python -m pytest -m liveValidate the plugin the way Hermes does:
hermes plugins doctor . --ciThe E2B terminal backend for Hermes was first proposed and implemented by Berkant Ay (@berkantay) in hermes-agent#18348. That work is the origin of this plugin. See NOTICE for the full attribution and licensing detail.
MIT — see LICENSE.
| Back | FazBrowse Home | New Git URL |