docs: clarify write safety, HERMES_WRITE_SAFE_ROOT, and file-mutation verifier

Document that safe-root violations are hard-blocked (not approval-gated),
add a security guide section for write_file/patch guards, and link cron
and verifier docs so users trust the footer over agent summaries.
This commit is contained in:
HexLab98 2026-07-12 20:27:14 +07:00 committed by kshitij
parent 2a0dd95ccf
commit 1b5ceec2a0
5 changed files with 103 additions and 8 deletions

View file

@ -745,7 +745,7 @@ Advanced per-platform knobs for throttling the outbound message batcher. Most us
| `HERMES_PREFILL_MESSAGES_FILE` | Path to a JSON file of ephemeral prefill messages injected at API-call time. |
| `HERMES_ALLOW_PRIVATE_URLS` | `true`/`false` — allow tools to fetch localhost/private-network URLs. Off by default in gateway mode. |
| `HERMES_REDACT_SECRETS` | `true`/`false` — control secret redaction in tool output, logs, and chat responses (default: `true`). |
| `HERMES_WRITE_SAFE_ROOT` | Optional directory prefix that restricts `write_file`/`patch` writes; paths outside require approval. Supports multiple directories separated by `os.pathsep` (`:` on Unix, `;` on Windows). |
| `HERMES_WRITE_SAFE_ROOT` | Optional directory prefix that **hard-blocks** `write_file`/`patch` writes outside the listed roots (no approval prompt). Supports multiple directories separated by `os.pathsep` (`:` on Unix, `;` on Windows). See [HERMES_WRITE_SAFE_ROOT](#hermes_write_safe_root) below. |
| `HERMES_DISABLE_LAZY_INSTALLS` | Internal bridge var set automatically in the official Docker image to prevent runtime dependency installs into the immutable `/opt/hermes` tree. The user-facing equivalent is `security.allow_lazy_installs: false` in `config.yaml`; do not set this in `.env`. |
| `HERMES_DISABLE_FILE_STATE_GUARD` | Set to `1` to turn off the "file changed since you read it" guard on `patch`/`write_file`. |
| `HERMES_CORE_TOOLS` | Comma-separated override for the canonical core tool list (advanced; rarely needed). |
@ -760,6 +760,22 @@ Advanced per-platform knobs for throttling the outbound message batcher. Most us
| `HERMES_AGENT_LOGO` | Override the ASCII banner logo at CLI startup. |
| `DELEGATION_MAX_CONCURRENT_CHILDREN` | Max parallel subagents per `delegate_task` batch (default: `3`, floor of 1, no ceiling). Also configurable via `delegation.max_concurrent_children` in `config.yaml` — the config value takes priority. |
### HERMES_WRITE_SAFE_ROOT {#hermes_write_safe_root}
When this variable is set, `write_file` and `patch` may only target paths inside the listed directory prefix(es). Any path outside those roots is **rejected immediately** — the write does not go through the dangerous-command approval system and there is no prompt to override it.
The official Docker image sets `HERMES_WRITE_SAFE_ROOT=/opt/data` alongside `HERMES_HOME=/opt/data` so the agent cannot escape the mounted data volume.
**Do not add this to `~/.hermes/.env` unless you intend to sandbox writes.** A common mistake is pointing it at a project directory while expecting the agent to edit `~/.hermes/cron/jobs.json`, `~/.hermes/skills/`, or scripts under a profile — those paths are outside the sandbox and every `write_file`/`patch` to them fails. The tool error currently reads `Write denied: '…' is a protected system/credential file.` for all blocked writes (including safe-root violations); check `echo $HERMES_WRITE_SAFE_ROOT` when you see that message.
To allow both a workspace and Hermes state, list both prefixes (order does not matter):
```bash
export HERMES_WRITE_SAFE_ROOT=/path/to/project:/home/you/.hermes
```
Unset the variable or remove it from `.env` to restore normal writes (still subject to the credential-path denylist — see [File write safety](../user-guide/security.md#file-write-safety)).
## Interface
| Variable | Description |

View file

@ -1447,6 +1447,22 @@ Example footer:
Set `file_mutation_verifier: false` (or `HERMES_FILE_MUTATION_VERIFIER=0`) to suppress the footer. The verifier only fires when real failures are outstanding at turn end — a model that retries a failed patch and succeeds within the same turn will not trigger it for that file.
**Trust the verifier over the model's summary.** The footer means the listed files were **not** modified on disk, even if the assistant's closing message says the task is done. Common causes:
- **Write denied** — path is on the credential denylist or outside `HERMES_WRITE_SAFE_ROOT` (see [File write safety](./security.md#file-write-safety))
- **Patch mismatch**`old_string` did not match the file on disk
- **Syntax gate** — candidate content failed JSON/YAML/TOML validation before write
Example footer when writes are blocked:
```
⚠️ File-mutation verifier: 2 file(s) were NOT modified this turn despite any wording above that may suggest otherwise. Run `git status` or `read_file` to confirm.
• ~/.hermes/cron/jobs.json — [patch] Write denied: '…' is a protected system/credential file.
• ~/.hermes/scripts/monitor.py — [write_file] Write denied: '…' is a protected system/credential file.
```
If writes to Hermes state (cron jobs, skills, scripts under `~/.hermes/`) are failing, check whether `HERMES_WRITE_SAFE_ROOT` is set in your environment. For cron changes, use the `cronjob` tool or `hermes cron edit` instead of patching `jobs.json` directly.
### UI language for static messages
The `display.language` setting translates a small set of static user-facing messages — the CLI approval prompt, a handful of gateway slash-command replies (e.g. restart-drain notices, "approval expired", "goal cleared"). It does **not** translate agent responses, log lines, tool output, error tracebacks, or slash-command descriptions — those stay in English. If you want the agent itself to reply in another language, just tell it in your prompt or system message.

View file

@ -732,6 +732,10 @@ The referenced jobs' most recent completed outputs are injected above the prompt
Jobs are stored in `~/.hermes/cron/jobs.json`. Output from job runs is saved to `~/.hermes/cron/output/{job_id}/{timestamp}.md`.
:::tip
Ask the agent to manage jobs through the `cronjob` tool, `hermes cron edit`, or `/cron` — not by patching `jobs.json` directly. Direct edits can fail silently when [file write safety](../security.md#file-write-safety) blocks the path (for example when `HERMES_WRITE_SAFE_ROOT` is set), and the [file-mutation verifier](../configuration.md#file-mutation-verifier) footer is the authoritative signal that nothing was saved.
:::
Jobs may store `model` and `provider` as `null`. When those fields are omitted, Hermes resolves them at execution time from the global configuration. They only appear in the job record when a per-job override is set.
The storage uses atomic file writes so interrupted writes do not leave a partially written job file behind.

View file

@ -10,15 +10,16 @@ Hermes Agent is designed with a defense-in-depth security model. This page cover
## Overview
The security model has seven layers:
The security model has eight layers:
1. **User authorization** — who can talk to the agent (allowlists, DM pairing)
2. **Dangerous command approval** — human-in-the-loop for destructive operations
3. **Container isolation** — Docker/Singularity/Modal sandboxing with hardened settings
4. **MCP credential filtering** — environment variable isolation for MCP subprocesses
5. **Context file scanning** — prompt injection detection in project files
6. **Cross-session isolation** — sessions cannot access each other's data or state; cron job storage paths are hardened against path traversal attacks
7. **Input sanitization** — working directory parameters in terminal tool backends are validated against an allowlist to prevent shell injection
3. **File write safety** — denylist and optional write sandbox for `write_file`/`patch`
4. **Container isolation** — Docker/Singularity/Modal sandboxing with hardened settings
5. **MCP credential filtering** — environment variable isolation for MCP subprocesses
6. **Context file scanning** — prompt injection detection in project files
7. **Cross-session isolation** — sessions cannot access each other's data or state; cron job storage paths are hardened against path traversal attacks
8. **Input sanitization** — working directory parameters in terminal tool backends are validated against an allowlist to prevent shell injection
## Dangerous Command Approval
@ -232,6 +233,48 @@ These patterns are loaded at startup and silently approved in all future session
Use `hermes config edit` to review or remove patterns from your permanent allowlist.
:::
## File Write Safety {#file-write-safety}
Before `write_file` or `patch` touches disk, Hermes checks the target path against a denylist and an optional sandbox. Blocked writes return an error to the agent immediately — **there is no approval prompt** and no way to override from the chat UI. The model may still claim the edit succeeded; when `display.file_mutation_verifier` is on (default), trust the [file-mutation verifier footer](./configuration.md#file-mutation-verifier) over the assistant's closing summary.
### Protected paths (always blocked)
These categories are always denied, even when `HERMES_WRITE_SAFE_ROOT` is unset:
| Category | Examples |
|----------|----------|
| OS credential stores | `~/.ssh/`, `~/.aws/`, `~/.kube/`, `/etc/sudoers`, `~/.netrc` |
| Hermes credential stores | `auth.json`, `.env`, `.anthropic_oauth.json`, `mcp-tokens/`, `pairing/` under HERMES_HOME (active profile and global root) |
| Project secret files | `.env`, `.env.local`, `.env.production`, `.envrc` anywhere on disk |
Sensitive paths inside the safe root are still blocked — pointing `HERMES_WRITE_SAFE_ROOT` at `$HOME` does not allow writing `~/.ssh/id_rsa`.
The tool error message is always `Write denied: '…' is a protected system/credential file.` even when the actual reason is the safe-root sandbox below. If the path does not look like a credential file, check `echo $HERMES_WRITE_SAFE_ROOT`.
### HERMES_WRITE_SAFE_ROOT (optional sandbox)
When set, `write_file` and `patch` may only target paths inside the listed directory prefix(es). Anything outside is **hard-blocked** — not routed through dangerous-command approval.
- Set automatically in the [official Docker image](https://github.com/NousResearch/hermes-agent) (`HERMES_WRITE_SAFE_ROOT=/opt/data`)
- Supports multiple roots separated by `:` on Unix or `;` on Windows
- **Do not add to `~/.hermes/.env` casually.** If you set it to a project directory, the agent cannot write to `~/.hermes/cron/jobs.json`, profile skills, or other Hermes state outside that prefix
To allow both a workspace and Hermes home:
```bash
export HERMES_WRITE_SAFE_ROOT=/path/to/project:/home/you/.hermes
```
Unset the variable to restore unrestricted writes (subject to the protected-path denylist). Full reference: [HERMES_WRITE_SAFE_ROOT](../reference/environment-variables.md#hermes_write_safe_root).
### Cron and other Hermes state
Do not ask the agent to `patch` `~/.hermes/cron/jobs.json` directly. Use the `cronjob` tool, [`hermes cron`](./features/cron.md), or `/cron` — they update the job store through the supported API. The same applies to other Hermes control files when write safety blocks direct edits.
:::note Defense-in-depth, not a hard boundary
Write guards apply to `write_file` and `patch` only. The `terminal` tool runs as the same OS user and can still `cat` or overwrite denied paths via shell commands. The denylist reduces accidental damage and gives models a clear stop signal; it does not sandbox a hostile or compromised agent.
:::
## User Authorization (Gateway)
When running the messaging gateway, Hermes controls who can interact with the bot through a layered authorization system.

View file

@ -559,7 +559,7 @@ Graph 事件Teams 会议、日历、聊天等)的入站变更通知监听
| `HERMES_PREFILL_MESSAGES_FILE` | 包含在 API 调用时注入的临时预填消息的 JSON 文件路径。 |
| `HERMES_ALLOW_PRIVATE_URLS` | `true`/`false`——允许工具获取 localhost/私有网络 URL。gateway 模式下默认关闭。 |
| `HERMES_REDACT_SECRETS` | `true`/`false`——控制工具输出、日志和聊天响应中的密钥脱敏(默认:`true`)。 |
| `HERMES_WRITE_SAFE_ROOT` | 可选目录前缀,限制 `write_file`/`patch` 写入;超出范围的路径需要审批。支持多个目录,使用 `os.pathsep` 分隔Unix 为 `:`Windows 为 `;`)。 |
| `HERMES_WRITE_SAFE_ROOT` | 可选目录前缀,**硬阻止** `write_file`/`patch` 写入列出的根目录之外的路径(无审批提示)。支持多个目录,使用 `os.pathsep` 分隔Unix 为 `:`Windows 为 `;`)。详见下方 [HERMES_WRITE_SAFE_ROOT](#hermes_write_safe_root)。 |
| `HERMES_DISABLE_LAZY_INSTALLS` | 官方 Docker 镜像中自动设置的内部桥接变量,用于阻止运行时将依赖安装到不可变的 `/opt/hermes` 树。面向用户的等价配置是 `config.yaml` 中的 `security.allow_lazy_installs: false`;不要在 `.env` 中手动设置此变量。 |
| `HERMES_DISABLE_FILE_STATE_GUARD` | 设为 `1` 可关闭 `patch`/`write_file` 上的"文件自上次读取后已更改"保护。 |
| `HERMES_CORE_TOOLS` | 规范核心工具列表的逗号分隔覆盖(高级;极少需要)。 |
@ -574,6 +574,22 @@ Graph 事件Teams 会议、日历、聊天等)的入站变更通知监听
| `HERMES_AGENT_LOGO` | 覆盖 CLI 启动时的 ASCII 横幅 logo。 |
| `DELEGATION_MAX_CONCURRENT_CHILDREN` | 每个 `delegate_task` 批次的最大并行子 agent 数(默认:`3`,下限为 1无上限。也可通过 `config.yaml` 中的 `delegation.max_concurrent_children` 配置——config 值优先。 |
### HERMES_WRITE_SAFE_ROOT {#hermes_write_safe_root}
设置此变量后,`write_file``patch` 只能写入列出的目录前缀内的路径。超出这些根目录的路径会被**立即拒绝**——不会进入危险命令审批流程,也没有聊天界面可以覆盖。
官方 Docker 镜像会设置 `HERMES_WRITE_SAFE_ROOT=/opt/data``HERMES_HOME=/opt/data`,防止 agent 逃出挂载的数据卷。
**除非有意沙箱化写入,否则不要将此变量加入 `~/.hermes/.env`。** 常见错误是将其指向项目目录,却期望 agent 编辑 `~/.hermes/cron/jobs.json``~/.hermes/skills/` 或 profile 下的脚本——这些路径在沙箱外,每次 `write_file`/`patch` 都会失败。当前所有被阻止的写入(包括 safe-root 违规)都显示 `Write denied: '…' is a protected system/credential file.`;看到此消息时请检查 `echo $HERMES_WRITE_SAFE_ROOT`
若需同时允许工作区和 Hermes 状态目录,列出两个前缀(顺序无关):
```bash
export HERMES_WRITE_SAFE_ROOT=/path/to/project:/home/you/.hermes
```
取消设置或从 `.env` 中移除此变量可恢复常规写入(仍受凭证路径拒绝列表约束——见[文件写入安全](../user-guide/security.md#file-write-safety))。
## 界面
| 变量 | 描述 |