Summary
read_file has no sensitive-path deny list. SSH private keys, .env credential files, and shell history are all freely readable. The only line of defense — output redaction via redact_sensitive_text() — was just made off by default (commit 73753417, Apr 27 2026) and uses a module-level _REDACT_ENABLED flag captured at import time, making config-driven opt-in fragile.
Write-side protection exists (build_write_denied_paths() + _check_sensitive_path()) but has no read-side counterpart.
How to Reproduce
# Terminal:
read_file("~/.ssh/id_ed25519") # → full SSH private key
read_file("~/.hermes/.env") # → full API keys
read_file("~/.zsh_history") # → shell history
read_file("/etc/passwd") # → user database (readable on macOS)
All return unredacted content by default as of commit 73753417.
Existing defenses that don't cover this
| Defense |
Status for read |
build_write_denied_paths() in agent/file_safety.py — blocks writes to SSH keys, .env, .bashrc, etc. |
❌ Write only — no read counterpart |
_check_sensitive_path() in tools/file_tools.py — blocks writes to /etc/, /boot/, etc. |
❌ Only called by write_file_tool() and patch_tool(), NOT by read_file_tool() |
redact_sensitive_text() — output masking |
⚠️ Off by default since 73753417; module-level flag captured at import time so config bridge timing is fragile |
get_read_block_error() in agent/file_safety.py |
❌ Only blocks Hermes internal cache files (skills/.hub/index-cache), not sensitive user files |
Related but distinct issues
Suggested Fixes
Short-term: Add read deny list alongside the write deny list
# In agent/file_safety.py, add:
def build_read_denied_paths(home: str) -> set[str]:
"""Return exact sensitive paths that must never be read."""
hermes_home = _hermes_home_path()
return {
os.path.join(home, ".ssh", "id_rsa"),
os.path.join(home, ".ssh", "id_ed25519"),
str(hermes_home / ".env"),
os.path.join(home, ".ssh", "authorized_keys"),
# ... same as write denied paths
}
# In tools/file_tools.py, apply it in read_file_tool() before performing the read,
# similar to how write_file_tool() calls _check_sensitive_path().
Medium-term: Fix _REDACT_ENABLED module-level caching
The module-level variable in agent/redact.py:60 is evaluated at import time:
_REDACT_ENABLED = os.getenv("HERMES_REDACT_SECRETS", "").lower() not in ("0", "false", "no", "off")
If the config bridge (in hermes_cli/main.py / gateway/run.py) sets the env var after the module is imported, this check is stale. Change to a runtime function call.
Long-term: Unify read/write deny lists
build_write_denied_paths() and _check_sensitive_path() are two parallel systems (one in agent/file_safety.py, one in tools/file_tools.py). Consolidate into a single source of truth covering both read and write operations.
Summary
read_filehas no sensitive-path deny list. SSH private keys,.envcredential files, and shell history are all freely readable. The only line of defense — output redaction viaredact_sensitive_text()— was just made off by default (commit73753417, Apr 27 2026) and uses a module-level_REDACT_ENABLEDflag captured at import time, making config-driven opt-in fragile.Write-side protection exists (
build_write_denied_paths()+_check_sensitive_path()) but has no read-side counterpart.How to Reproduce
All return unredacted content by default as of commit
73753417.Existing defenses that don't cover this
build_write_denied_paths()inagent/file_safety.py— blocks writes to SSH keys,.env,.bashrc, etc._check_sensitive_path()intools/file_tools.py— blocks writes to/etc/,/boot/, etc.write_file_tool()andpatch_tool(), NOT byread_file_tool()redact_sensitive_text()— output masking73753417; module-level flag captured at import time so config bridge timing is fragileget_read_block_error()inagent/file_safety.pyskills/.hub/index-cache), not sensitive user filesRelated but distinct issues
read_fileviaredact_sensitive_text(). This was masking, not access control. The fix is now effectively undone by73753417(redaction off by default).tools.*.allowed_pathsconfig-time scoping for filesystem tools #9389 (Open, P3) — Feature request for declarativeallowed_pathsconfig scoping. A structural solution but lower priority.write_filebypasses credential protection for global.env. Different vector (write vs. read).Suggested Fixes
Short-term: Add read deny list alongside the write deny list
Medium-term: Fix
_REDACT_ENABLEDmodule-level cachingThe module-level variable in
agent/redact.py:60is evaluated at import time:If the config bridge (in
hermes_cli/main.py/gateway/run.py) sets the env var after the module is imported, this check is stale. Change to a runtime function call.Long-term: Unify read/write deny lists
build_write_denied_paths()and_check_sensitive_path()are two parallel systems (one inagent/file_safety.py, one intools/file_tools.py). Consolidate into a single source of truth covering both read and write operations.