fix(file-safety): defense-in-depth read-deny on Hermes credential stores (#17659)#30721
Merged
Conversation
…17656) `get_read_block_error` previously only denied reads inside `${HERMES_HOME}/skills/.hub`, which left `auth.json` (provider OAuth state + plaintext API keys) and `.anthropic_oauth.json` (Anthropic PKCE tokens) directly readable by the agent. A prompt-injection reaching `read_file` could exfiltrate active provider credentials in plaintext. Mode-0600 file permissions only protect against *other Unix users* — the agent runs as the file's owner, so `read_file` is unaffected. Extend the existing deny list with the three credential paths identified in #17656 (`auth.json`, `auth.lock`, `.anthropic_oauth.json`). The check uses the same `Path.resolve()` pattern as `skills/.hub`, so symlink/path-traversal indirection is caught too. The agent doesn't need to read these directly — `auxiliary_client` and `credential_pool` consume them through process env / OAuth flows that bypass `read_file`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
read_file_tool resolves relative paths against TERMINAL_CWD (or the task's live terminal cwd), but the prior call passed the original unresolved string to get_read_block_error. That function's own resolve() is anchored at the Python process cwd, so when a task's TERMINAL_CWD pointed at HERMES_HOME and the agent issued read_file on the relative path "auth.json", the credential-store denylist was never reached and the file was read normally. Pass the already-resolved absolute path string at the file_tools call site, document the contract on get_read_block_error, and add a read_file_tool-level regression test that pins the relative-path case under TERMINAL_CWD == HERMES_HOME. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ts, root Extends @briandevans's PR #17659 from {auth.json, auth.lock, .anthropic_oauth.json} to also cover: - HERMES_HOME/.env (provider API keys) - HERMES_HOME/webhook_subscriptions.json (per-route HMAC secrets) - HERMES_HOME/mcp-tokens/ (OAuth token directory; dir + everything inside) …AND iterates over both _hermes_home_path() AND _hermes_root_path() so profile-mode runs (HERMES_HOME = <root>/profiles/<name>) also block <root>/{auth.json, .env, mcp-tokens/, ...}. Same widening shape as the write-deny side already does (#15981, #14157). Explicitly NOT a security boundary. Per the personal-assistant trust model, the terminal tool runs as the same OS user and can `cat auth.json` directly. This read-deny exists as defense-in-depth: - Models that respect tool denials empirically tend to stop rather than reach for the shell. - The denial surfaces an audit trail when something tries to read credentials — easier to spot in logs than a generic `cat`. Docstring + error message both flag this as defense-in-depth so future contributors don't mistake it for a real security boundary and don't re-decline reports that propose the same fix shape. Absorbs the .env and mcp-tokens/ coverage from @tomqiaozc's parallel PR #8055 (closed-as-duplicate, credited). Co-authored-by: Tom Qiao <zqiao@microsoft.com>
Contributor
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-import |
1 |
First entries
tests/agent/test_file_safety_credentials.py:19: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
✅ Fixed issues: none
Unchanged: 4771 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
This was referenced May 23, 2026
This was referenced May 23, 2026
13 tasks
13 tasks
briandevans
added a commit
to briandevans/hermes-agent
that referenced
this pull request
Jun 5, 2026
…Claude Code / Copilot / MiniMax) NousResearch#17656 / NousResearch#30721 / NousResearch#30972 added read_file denial for credential stores under HERMES_HOME / the Hermes root (auth.json, .anthropic_oauth.json, .env, webhook_subscriptions.json, auth/google_oauth.json, cache/bws_cache.json, mcp-tokens/). Their resolve-loop only walks HERMES_HOME and hermes_root, so the EXTERNAL provider-CLI credential stores Hermes imports OAuth tokens from — which live in each provider tool's own home, outside HERMES_HOME — are never reached. These hold plaintext OAuth / API-key material and are live, reachable paths: the anthropic adapter reads ~/.claude/.credentials.json directly, and the model layer enumerates ~/.config/github-copilot/{hosts,apps}.json and ~/.minimax/credentials.json. read_file could therefore exfiltrate them under the same prompt-injection vector NousResearch#17656 hardened against. Extend get_read_block_error with the same exact-path read-deny for ~/.claude/.credentials.json, ~/.claude.json, ~/.config/github-copilot/{hosts,apps}.json (XDG_CONFIG_HOME honored), and ~/.minimax/credentials.json. ~/.codex/auth.json is intentionally excluded — NousResearch#12360 made Hermes stop touching it by design (single-use refresh-token race). Defense-in-depth, not a security boundary: the terminal tool can still bypass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Salvages @briandevans's PR #17659 and absorbs the broader coverage from @tomqiaozc's parallel PR #8055.
read_filenow denies reads against credential / secret stores under bothHERMES_HOMEand the global Hermes root:auth.json,auth.lock,.anthropic_oauth.json,.env,webhook_subscriptions.json, and anything undermcp-tokens/.What this is — and what it is NOT
This is defense-in-depth, not a security boundary. The terminal tool runs as the same OS user with shell access; the agent can still
cat ~/.hermes/auth.jsonand exfiltrate the file. The read-deny is documented as bypassable in both the docstring and the error message text. We're shipping it anyway because:cat.read_file(a common shape in indirect prompt injections through tool outputs) genuinely cannot read the file.Our published security policy still applies — we will continue to decline bogus reports that claim this is a vulnerability bypass, because it isn't a boundary. We will continue to accept contributor PRs that add to this gate, because the engineering value is real.
Changes
agent/file_safety.py::get_read_block_error():auth.json,auth.lock,.anthropic_oauth.json.env,webhook_subscriptions.json,mcp-tokens/directory prefix_hermes_home_path()AND_hermes_root_path()so profile-mode (HERMES_HOME =<root>/profiles/<name>) doesn't leave the global root credential stores readable. Same shape as the write-deny widening in write_file tool bypasses credential protection for global ~/.hermes/.env #15981 / fix(security): protect Hermes control-plane files from prompt injection #14072 #14157.tools/file_tools.py::read_file_tool()(@briandevans): pass the already-resolved absolute path toget_read_block_error(). Without this, a relative-path read like"auth.json"resolved againstTERMINAL_CWDwould miss the denylist whenTERMINAL_CWDdiffers from the Python process cwd. Subtle bypass; good catch.tests/agent/test_file_safety_credentials.py:.env,webhook_subscriptions.json,mcp-tokens/files and nested,mcp-tokens/directory itself, identically-named files OUTSIDE HERMES_HOME stay readable,config.yamlNOT blocked (it's a control file but not a secret), profile-mode blocks<root>/credential storesValidation
read_file(~/.hermes/auth.json)read_file(~/.hermes/.env)read_file(~/.hermes/mcp-tokens/gh.json)read_file(~/.hermes/webhook_subscriptions.json)read_file("auth.json")with TERMINAL_CWD=HERMES_HOMEread_file(<root>/auth.json)read_file(~/.hermes/config.yaml)read_file(~/myproject/.env)terminal("cat ~/.hermes/auth.json")tests/agent/test_file_safety_credentials.py— 17/17 pass (9 contributor + 8 follow-up).Full file-safety surface (4 files, 135 tests) — green, no regressions.
Attribution
git log.Co-authored-by:trailer on the follow-up commit (the.env+mcp-tokens/coverage from his PR fix(security): block file_tools from reading auth.json, mcp-tokens, .env #8055). His PR will be closed as duplicate with credit pointing here.Closes #17656 + #17659 + #8055 on merge.
Infographic