Hermes Now Hands You the Files Its Agent Creates Inside Remote Sandboxes


Picture a Friday evening: you’ve put Hermes’ hands inside a remote sandbox — a Modal cloud environment, or a company SSH dev box — because you want clean isolation that never touches your laptop’s files. The agent finishes the job, produces a chart, a report, a web-page screenshot inside that sandbox, and cheerfully attaches the file path to its reply. Your chat window shows nothing. The only trace is a log line: “Skipping unsafe MEDIA directive path.” The file exists — it just can’t reach you.

Why files used to disappear

To understand this, you first need to know how Hermes hands you files at all. The agent writes a MEDIA:<path> tag in its reply; the gateway sees it, pulls the file out, and sends it to your chat as an image or attachment. That path validation has always been host-only by design — media delivery must never become a channel for reading arbitrary system files, so ~/.ssh, system directories, and Hermes’ own credential stores sit on a denylist and are always refused.

The problem is precisely that “host-only” part. When your terminal backend is ssh, Modal, Daytona, Singularity, or a Vercel sandbox, the agent’s terminal commands run on another machine — another filesystem. The file behind MEDIA: simply isn’t on the gateway host, validation fails, and the attachment is dropped as an “unsafe path.” The file was generated fine; the gateway just couldn’t reach it.

The fix: reach into the sandbox and pull the file back

PR #103600, merged on September 5, 2026 (the outbound half of issue #466), changes that. The idea is straightforward: when host validation fails but the current session is genuinely running inside a remote sandbox, the gateway now pulls the file back over the exec channel and runs it through the normal delivery flow.

It works in three layers:

  • Transport (tools/environments/base.py): every environment backend gained a uniform fetch_file / fetch_realpath — the file is base64-encoded and shipped back over the exec channel, size-capped inside the sandbox (head -c max+1, the same trick tools.image_source uses so an infinite stream like /dev/zero can’t flood host memory), with payload markers fencing off login-shell noise.
  • Trigger (new gateway/media_fetch.py, ~120 lines): fires only when a remote backend is active and host validation has already failed — a local backend never takes this path.
  • Hook (gateway/platforms/base.py::_validated_delivery_path): a three-line addition, the single choke point shared by every MEDIA filtering site — one fix, effective everywhere.

Security boundaries: this is not a backdoor around the denylist

Pulling files from a remote sandbox is exactly the kind of feature that could become a denylist bypass, so the path is screened twice: first through the same host denylist (system prefixes, ~/.ssh-class directories, Hermes credential stores) before any bytes move, then again after readlink -f resolves the real path — a symlink pointing at a credential file is refused outright, not a single byte transferred. Files that pass are copied into cache/documents/ (itself an allowlisted delivery root) and then validated and delivered exactly like any host file. The cap is 50MB, matching each platform’s upload limit.

Want the old behavior back? Under HERMES_MEDIA_DELIVERY_STRICT strict mode, remote files are still never fetched — a fetched copy would land in an allowlisted root and could skip the recency gate strict mode exists for, so strict mode keeps things as they were.

Which setups benefit

Any remote terminal backend — ssh, Modal, Daytona, Singularity, Vercel sandbox — now delivers files the agent creates there and references with a MEDIA: tag: screenshots from a scraper running in the sandbox, charts from a data-analysis script, reports written after a long task. Anything you asked the agent to “save and send me.” Local backends behave exactly as before — they never needed fetching.

One honest boundary: backends that can report their remote home directory (_remote_home, e.g. ssh, Daytona, Vercel) get full support; Modal, Singularity, and Docker without host mounts cannot confirm the remote home, so they keep the conservative any-component denylist — files under system prefixes like /root are still not fetched by default there, unless the environment explicitly sets _remote_home to opt in.

What’s still missing

This lands only the “outbound” half of #466. The PR deliberately excludes: a standalone send_file core tool (the existing MEDIA: tag is the interface — footprint-ladder rung 1), per-backend native transports, and inbound attachment→sandbox injection (still open on #466). Those follow the footprint ladder and ship incrementally rather than all at once.

How to get it

No new command, no new config — this is a gateway behavior fix; it activates on upgrade. The change is merged to main (2026-09-05) and is not yet in any release tag; run a dev/main install to try it now, or just hermes update when the next release lands. A quick way to verify afterward: inside a remote sandbox, have the agent generate an image and “send it to you” — it should arrive; then drop a symlink pointing at ~/.ssh and confirm it’s still refused. Both screenings doing their job.

For more on sandboxes and backends, see our earlier guides on shared Docker containers and pluggable terminal backends; release cadence lives on the releases page.