Safe-root vs credential-path write denial messaging
NousResearch/hermes-agent · cluster 55607 · agent
Data as of
Nothing open on GitHub. Fix merged in #64347. Check it covers your case.
The short answer
- Known?
- Yes. 5 related items grouped here, 2 issues already closed.
- Fix event
- A fix was merged to main in PR #64347. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
unresolved. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These items concern a bug where write denials caused by paths outside the safe-root boundary are incorrectly reported as protected credential-path blocks. The work focuses on distinguishing and clarifying these two separate error conditions.
Open issues0
Open PRs0
Fixes on record
-
#64347 fix(file-safety): distinguish safe-root write denials from credential blocks
files:
agent/copilot_acp_client.py,agent/file_safety.py,tests/agent/test_copilot_acp_client.py,tests/tools/test_file_write_safety.py,tools/file_operations.py,website/docs/reference/environment-variables.md,website/docs/user-guide/configuration.md,website/docs/user-guide/features/cron.md,website/docs/user-guide/security.md,website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/environment-variables.mdShow the diff
-
agent/copilot_acp_client.py@@ -26,7 +26,7 @@ Function, ) -from agent.file_safety import get_read_block_error, is_write_denied +from agent.file_safety import get_read_block_error, get_write_denied_error from agent.redact import redact_sensitive_text from tools.environments.local import hermes_subprocess_env @@ -727,10 +727,9 @@ def _handle_server_message( elif method == "fs/write_text_file": try: path = _ensure_path_within_cwd(str(params.get("path") or ""), cwd) - if is_write_denied(str(path)): - raise PermissionError( - f"Write denied: '{path}' is a protected system/credential file." - ) + denied = get_write_denied_error(str(path)) + if denied: + raise PermissionError(denied) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(str(params.get("content") or "")) response = { -
agent/file_safety.py@@ -95,16 +95,16 @@ def get_safe_write_roots() -> set[str]: return roots -def is_write_denied(path: str) -> bool: - """Return True if path is blocked by the write denylist or safe root.""" +def _classify_write_denial(path: str) -> Optional[str]: + """Return ``'credential'``, ``'safe_root'``, or ``None`` if writes are allowed.""" home = os.path.realpath(os.path.expanduser("~")) resolved = os.path.realpath(os.path.expanduser(str(path))) if resolved in build_write_denied_paths(home): - return True + return "credential" for prefix in build_write_denied_prefixes(home): if resolved.startswith(prefix): - return True + return "credential" mcp_tokens_dir_name = "[redacted:secret-assignment]" @@ -121,13 +121,13 @@ def is_write_denied(path: str) -> bool: try: mcp_real = os.path.realpath(os.path.join(base_real, mcp_tokens_dir_name)) if resolved == mcp_real or resolved.startswith(mcp_real + os.sep): - return True + return "credential" except Exception: pass try: pairing_real = os.path.realpath(os.path.join(base_real, "pairing")) if resolved == pairing_real or resolved.startswith(pairing_real + os.sep): - return True + return "credential" except Exception: pass @@ -139,9 +139,28 @@ def is_write_denied(path: str) -> bool: allowed = True break if not allowed: - return True + return "safe_root" + + return None + + +def is_write_denied(path: str) -> bool: + """Return True if path is blocked by the write denylist or safe root.""" + return _classify_write_denial(path) is not None + - return False +def get_write_denied_error(path: str, *, verb: str = "Write") -> Optional[str]: + """Return a user/model-facing error when writes to ``path`` are blocked.""" + denial = _classify_write_denial(path) + if denial is None: + return None + if denial == "safe_root": + roots_display = os.pathsep.join(sorted(get_safe_write_roots())) + return ( + f"{verb} denied: '{path}' is outside HERMES_WRITE_SAFE_ROOT " + f"({roots_display}). Unset the variable or add this path's directory prefix." + ) + return f"{verb} denied: '{path}' is a protected system/credential file." # Common secret-bearing project-local environment file basenames. -
tests/agent/test_copilot_acp_client.py@@ -209,7 +209,11 @@ def test_write_text_file_reuses_write_denylist(self) -> None: target = home / ".ssh" / "id_rsa" target.parent.mkdir(parents=True, exist_ok=True) - with patch("agent.copilot_acp_client.is_write_denied", return_value=True, create=True): + with patch( + "agent.copilot_acp_client.get_write_denied_error", + return_value="Write denied: protected", + create=True, + ): response = self._dispatch( { "jsonrpc": "2.0", @@ -248,6 +252,7 @@ def test_write_text_file_respects_safe_root(self) -> None: ) self.assertIn("error", response) + self.assertIn("HERMES_WRITE_SAFE_ROOT", str(response["error"])) self.assertFalse(outside.exists()) -
tests/tools/test_file_write_safety.py@@ -168,6 +168,107 @@ def test_duplicate_roots_deduplicated(self, tmp_path: Path, monkeypatch): assert _is_write_denied(str(inside)) is False +class TestGetWriteDeniedError: + """get_write_denied_error() should distinguish credential vs safe-root blocks.""" + + def test_credential_path_message(self): + from agent.file_safety import get_write_denied_error + + err = get_write_denied_error(os.path.expanduser("~/.ssh/id_rsa")) + assert err is not None + assert "protected system/credential file" in err + assert "HERMES_WRITE_SAFE_ROOT" not in err + + def test_safe_root_message(self, tmp_path: Path, monkeypatch): + from agent.file_safety import get_write_denied_error + + safe_root = tmp_path / "workspace" + outside = tmp_path / "outside.txt" + os.makedirs(safe_root, exist_ok=True) + + monkeypatch.setenv("HERMES_WRITE_SAFE_ROOT", str(safe_root)) + err = get_write_denied_error(str(outside)) + assert err is not None + assert "outside HERMES_WRITE_SAFE_ROOT" in err + assert str(safe_root) in err + assert "protected system/credential file" not in err + + def test_allowed_path_returns_none(self, tmp_path: Path): + from agent.file_safety import get_write_denied_error + + target = tmp_path / "ok.txt" + assert get_write_denied_error(str(target)) is None + + +class TestSafeRootDenialMessageIntegration: + """Regression tests verifying that file-tools surface the correct denial + message when HERMES_WRITE_SAFE_ROOT blocks a path. + + Prior to this fix, ALL write denials returned the same "protected + system/credential file" message regardless of root cause. These tests + exercise the actual write_file / patch_replace code path, not just + the get_write_denied_error() helper in isolation. + """ + + @pytest.fixture + def ops(self, tmp_path: Path): + from tools.environments.local import LocalEnvironment + from tools.file_operations import ShellFileOperations + env = LocalEnvironment(cwd=str(tmp_path)) + return ShellFileOperations(env, cwd=str(tmp_path)) + + def test_write_file_safe_root_outside_shows_safe_root_message( + self, ops, tmp_path: Path, monkeypatch + ): + safe_root = tmp_path / "workspace" + safe_root.mkdir() + outside = tmp_path / "other" / "file.txt" + outside.parent.mkdir() + monkeypatch.setenv("HERMES_WRITE_SAFE_ROOT", str(safe_root)) + + res = ops.write_file(str(outside), "content") + assert res.error is not None + assert "outside HERMES_WRITE_SAFE_ROOT" in res.error + assert str(safe_root) in res.error + assert "credential" not in res.error + assert not outside.exists() + + def test_patch_replace_safe_root_outside_shows_safe_root_message( + self, ops, tmp_path: Path, monkeypatch + ): + safe_root = tmp_path / "workspace" + safe_root.mkdir() + outside = tmp_path / "other" / "file.txt" + outside.parent.mkdir() + outside.write_text("old content") + monkeypatch.setenv("HERMES_WRITE_SAFE_ROOT", str(safe_root)) + + res = ops.patch_replace(str(outside), "old", "new") + assert res.error is not None + assert "outside HERMES_WRITE_SAFE_ROOT" in res.error + assert "credential" not in res.error + + def test_write_file_credential_path_shows_credential_message( + self, ops, tmp_path: Path + ): + res = ops.write_file("/etc/shadow", "content") + assert res.error is not None + assert "protected system/credential file" in res.error + assert "outside" not in res.error + + def test_write_file_allowed_path_returns_no_error( + self, ops, tmp_path: Path, monkeypatch + ): + safe_root = tmp_path / "workspace" + safe_root.mkdir() + inside = safe_root / "file.txt" + monkeypatch.setenv("HERMES_WRITE_SAFE_ROOT", str(safe_root)) + + res = ops.write_file(str(inside), "content") + assert res.error is None + assert inside.read_text() == "content" + + class TestCheckSensitivePathMacOSBypass: """Verify _check_sensitive_path blocks /private/etc paths (issue #8734).""" -
tools/file_operations.py@@ -37,6 +37,7 @@ from agent.file_safety import ( build_write_denied_paths, build_write_denied_prefixes, + get_write_denied_error, is_write_denied as _shared_is_write_denied, ) @@ -1289,8 +1290,9 @@ def delete_path(self, path: str, recursive: bool = False) -> WriteResult: def _python_delete(self, path: str, recursive: bool) -> WriteResult: path = self._expand_path(path) - if _is_write_denied(path): - return WriteResult(error=f"Delete denied: {path} is a protected path") + denied = get_write_denied_error(path, verb="Delete") + if denied: + return WriteResult(error=denied) # We can't shell out to ``rm`` here — it doesn't exist on Windows # ``cmd.exe`` or PowerShell, so this code path is what's left when @@ -1335,8 +1337,9 @@ def move_file(self, src: str, dst: str) -> WriteResult: src = self._expand_path(src) dst = self._expand_path(dst) for p in (src, dst): - if _is_write_denied(p): - return WriteResult(error=f"Move denied: {p} is a protected path") + denied = get_write_denied_error(p, verb="Move") + if denied: + return WriteResult(error=denied) result = self._exec( f"mv {self._escape_shell_arg(src)} {self._escape_shell_arg(dst)}" ) @@ -1383,8 +1386,9 @@ def write_file(self, path: str, content: str) -> WriteResult: path = self._expand_path(path) # Block writes to sensitive paths - if _is_write_denied(path): - return WriteResult(error=f"Write denied: '{path}' is a protected system/credential file.") + denied = get_write_denied_error(path) + if denied: + return WriteResult(error=denied) # ── Fail-closed pre-write syntax gate ─────────────────────────── # Validate the CANDIDATE content BEFORE any bytes touch disk — @@ -1566,8 +1570,9 @@ def patch_replace(self, path: str, old_string: str, new_string: str, path = self._expand_path(path) # Block writes to sensitive paths - if _is_write_denied(path): - return PatchResult(error=f"Write denied: '{path}' is a protected system/credential file.") + denied = get_write_denied_error(path) + if denied: + return PatchResult(error=denied) # Read current content read_cmd = f"cat {self._escape_shell_arg(path)} 2>/dev/null" -
website/docs/reference/environment-variables.md@@ -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 with an `outside HERMES_WRITE_SAFE_ROOT` error. + +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 | -
website/docs/user-guide/configuration.md@@ -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 outside HERMES_WRITE_SAFE_ROOT (/path/to/project) + • ~/.hermes/scripts/monitor.py — [write_file] Write denied: '…' is outside HERMES_WRITE_SAFE_ROOT (/path/to/project) +``` + +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. -
website/docs/user-guide/features/cron.md@@ -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. -
website/docs/user-guide/security.md@@ -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`. + +Safe-root violations return `Write denied: '…' is outside HERMES_WRITE_SAFE_ROOT (…)`. Credential-path blocks use `Write denied: '…' is a protected system/credential file.` + +### 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. -
website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/environment-variables.md@@ -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` 都会失败并返回 `outside 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))。 + ## 界面 | 变量 | 描述 |
-
2 PRs closed without merging
From the close comments: merged via another change 2
Closed issues · 2
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.54: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- agent
- Merged PRs
- #64347
- Closed issues
- 2
- Attempts
- 2 PRs closed without merging
- Fix link checked
- 14 Jul 2026