hermes - ✅(Solved) Fix read_file has no sensitive-path deny list — SSH keys, .env, shell history readable without restriction [2 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#16809Fetched 2026-04-29 06:38:57
View on GitHub
Comments
0
Participants
1
Timeline
6
Reactions
0
Author
Participants
Timeline (top)
labeled ×3cross-referenced ×2closed ×1

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.


Root Cause

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.


Fix Action

Fix / Workaround

DefenseStatus 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

PR fix notes

PR #16851: fix(file-safety): refuse read_file on credential paths (SSH keys, .env, …) (#16809)

Description (problem / solution / changelog)

Summary

  • Add a read-side counterpart to build_write_denied_paths() in agent/file_safety.py and have read_file_tool consult it before performing the read.
  • Blocks SSH private keys, the active HERMES_HOME .env, shell history, and credential directories (.ssh, .aws, .gnupg, .kube, .docker, .azure, .config/gh) from being read by the model.

The bug

read_file had no sensitive-path deny list. The reproduction in #16809 — and on a fresh checkout of main — succeeds today:

read_file("~/.ssh/id_ed25519")  # → full SSH private key
read_file("~/.hermes/.env")     # → full API keys
read_file("~/.zsh_history")     # → shell history

The only defense was pattern-based output redaction via redact_sensitive_text(), and that path was made off by default in 73753417 ("feat(security): make secret redaction off by default", #16794) and uses a module-level _REDACT_ENABLED snapshotted at import time — so config-driven opt-in is fragile, and well-known credential prefixes are the only thing recognised even when redaction is on.

Write-side protection exists (build_write_denied_paths + _check_sensitive_path) but has no read-side counterpart.

The fix

Add an access-control floor for high-value credential files where leakage is unrecoverable.

agent/file_safety.py gains three new helpers:

  • build_read_denied_paths(home) — exact paths: SSH keys (id_rsa/id_ed25519/id_ecdsa/id_dsa/authorized_keys/config), .netrc, .pgpass, .npmrc, .pypirc, the active HERMES_HOME / .env (resolved through get_hermes_home() so profile-aware), and shell history (.bash_history, .zsh_history, .psql_history).
  • build_read_denied_prefixes(home) — directory prefixes: ~/.ssh, ~/.aws, ~/.gnupg, ~/.kube, ~/.docker, ~/.azure, ~/.config/gh.
  • is_read_denied(path) — symmetric helper. Resolves through os.path.realpath before checking, so symlink shims are caught.

tools/file_tools.pyread_file_tool calls is_read_denied(path) after the existing device / binary / Hermes-internal guards (so denied paths can't return cached content from the dedup tracker either) and returns a structured error message pointing the model at the terminal tool when a credential read is genuinely intentional.

Intentional scope decisions

  • /etc/passwd is not blocked — world-readable user metadata, not a secret. /etc/shadow is already unreadable to non-root callers, so blocking it would be cosmetic.
  • Shell config (.bashrc, .zshrc, .profile, …) is write-denied but read-allowed: debugging PATH issues / alias setup is a legitimate case, and inline export FOO=key strings remain covered by redact_sensitive_text when security.redact_secrets is on.
  • The escape hatch for genuinely intentional reads (e.g. an agent that wants to inspect its own SSH config) is the existing terminal tool, which gates on user approval — exactly the layer this guard is designed to defer to.

Test plan

  • Focusedtests/tools/test_read_deny.py (new): 26 tests covering exact paths, prefixes, allowed paths, and end-to-end read_file_tool denial. All pass.
  • Adjacenttests/tools/test_write_deny.py, tests/tools/test_file_read_guards.py, tests/tools/test_file_operations.py, tests/tools/test_read_loop_detection.py, tests/tools/test_file_write_safety.py, tests/agent/test_copilot_acp_client.py. The 6 test_file_read_guards.py::TestFileDedup/TestWriteInvalidatesDedup failures and the 1 test_copilot_acp_client.py::test_read_text_file_redacts_sensitive_content failure reproduce on clean origin/main (8081425a1) — they're pre-existing macOS /private/var write-deny collision and the redaction-off-by-default regression respectively, unrelated to this change.
  • Regression guard — verified directly:
    Without guard, contents readable: True
    With guard, error returned: True
    With guard, secret leaked: False

Related

  • Fixes #16809.
  • Companion to the read-side concerns surfaced in #7826 (v0.8.0 audit) and adjacent to the redaction work in #16794 / #16700 / #16843 / #16849 — those address output-side redaction; this is the access-control floor underneath.

Changed files

  • agent/file_safety.py (modified, +81/-0)
  • tests/tools/test_read_deny.py (added, +206/-0)
  • tools/file_tools.py (modified, +24/-1)

PR #17018: fix: resolve 7 identified issues [automated]

Description (problem / solution / changelog)

Resumo

Este PR corrige 7 issues identificados no repositório Hermes Agent, abrangendo bugs, endurecimento de segurança e compatibilidade cross-platform.

Issues Corrigidos

1. #16844 - Persisted assistant messages store reasoning incorrectly for DeepSeek/Kimi replay

  • Arquivo:
  • Problema: Mensagens assistentes persistidas armazenavam reasoning no campo interno 'reasoning' em vez de 'reasoning_content', envenenando sessões futuras com DeepSeek/Kimi thinking-mode replay.
  • Correção: Adicionado campo ao persistir mensagens de assistant.

2. #16809 - read_file has no sensitive-path deny list (Security - P1)

  • Arquivo:
  • Problema: A ferramenta read_file não tinha lista de negação para caminhos sensíveis. SSH keys, .env, e shell history eram livremente legíveis.
  • Correção: Adicionadas , , e integrada à para bloquear acesso a arquivos sensíveis.

3. #16730 / #16723 - Ollama provider API key e terminal timeout

  • Arquivo:
  • Problema: Provider ollama exigia API key desnecessariamente; timeout padrão do terminal era 60s quando o terminal_tool usa 180s.
  • Correção: Allow ollama provider sem API key; corrige fallback do terminal_timeout para 180s.

4. #16725 / #16713 - Discord known-thread bypass e rate-limit timeout

  • Arquivo:
  • Problema: Mensagens em threads conhecidas pelo bot ignoravam mention gating incondicionalmente; _safe_sync_slash_commands podia estourar timeout de 30s após mass prune.
  • Correção: Adicionado opt-in; separado sync de comandos críticos (fast_only=True) de cleanup de orphans via background task com rate-limit pacing.

5. #16720 - MEDIA: tags extraídas de qualquer tool output

  • Arquivo:
  • Problema: Regex MEDIA: era aplicado a todos os tool outputs, fazendo attachments spuriously de documentation/search/skills tools.
  • Correção: Allowlist de tool names () para extração de MEDIA:; mapeamento tool_call_id→tool_name.

6. #16703 - execute_code Docker DooD permission denied

  • Arquivo:
  • Problema: Erro genérico quando hermes user sem acesso ao docker.sock tentava usar Docker backend dentro de container (DoD).
  • Correção: Handler específico para PermissionError com mensagem explicativa sobre --group-add/docker GID.

Arquivos Modificados

ArquivoCommits
#16844
#16809
#16730, #16723
#16725, #16713
#16720
#16703

Como Testar

  1. Verificar que retorna erro de acesso negado
  2. Verificar que em config.yaml respeita mention gating em threads conhecidas
  3. Verificar que Docker backend no DooD mostra mensagem de erro informativa sobre --group-add
  4. Verificar que mensagens assistentes com reasoning são corretamente persistidas com reasoning_content

Gerado automaticamente em 2026-04-28T13:29:15Z

Changed files

  • agent/display.py (modified, +3/-1)
  • agent/file_safety.py (modified, +83/-1)
  • cli.py (modified, +6/-2)
  • gateway/platforms/api_server.py (modified, +10/-1)
  • gateway/platforms/dingtalk.py (modified, +22/-0)
  • gateway/platforms/discord.py (modified, +165/-6)
  • gateway/platforms/qqbot/adapter.py (modified, +12/-7)
  • gateway/run.py (modified, +22/-2)
  • hermes_cli/tools_config.py (modified, +1/-1)
  • run_agent.py (modified, +2/-1)
  • setup-hermes.sh (modified, +3/-2)
  • tools/environments/docker.py (modified, +76/-4)
  • tools/image_generation_tool.py (modified, +151/-0)
  • tools/mcp_tool.py (modified, +39/-4)
  • toolsets.py (modified, +2/-2)

Code Example

# 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)

---

# 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().

---

_REDACT_ENABLED = os.getenv("HERMES_REDACT_SECRETS", "").lower() not in ("0", "false", "no", "off")
RAW_BUFFERClick to expand / collapse

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

DefenseStatus 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

  • #363 (Closed, Mar 2026) — Added output redaction to read_file via redact_sensitive_text(). This was masking, not access control. The fix is now effectively undone by 73753417 (redaction off by default).
  • #7826 (Open, v0.8.0 audit) — C2 mentions "Full filesystem read access with no deny list" among 22 findings. No follow-up PR or focused issue.
  • #9389 (Open, P3) — Feature request for declarative allowed_paths config scoping. A structural solution but lower priority.
  • #15981 (Open, P0) — write_file bypasses credential protection for global .env. Different vector (write vs. read).

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.

extent analysis

TL;DR

Implement a read deny list in agent/file_safety.py to restrict access to sensitive files and paths.

Guidance

  • Add a build_read_denied_paths function to agent/file_safety.py to define sensitive paths that should not be readable.
  • Modify read_file_tool() in tools/file_tools.py to check against the read deny list before performing the read operation.
  • Consider fixing the _REDACT_ENABLED module-level caching issue by changing it to a runtime function call to ensure accurate reflection of the current configuration.
  • Review and consolidate the existing write deny list and sensitive path checking mechanisms to ensure consistency and completeness.

Example

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
    }

Notes

The provided suggestions focus on addressing the immediate issue of lacking a read deny list. However, a more comprehensive solution would involve reviewing and potentially unifying the existing mechanisms for handling sensitive paths and denied lists for both read and write operations.

Recommendation

Apply the short-term fix by adding a read deny list alongside the write deny list, as this directly addresses the identified vulnerability and provides an immediate layer of protection against unauthorized access to sensitive files.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix read_file has no sensitive-path deny list — SSH keys, .env, shell history readable without restriction [2 pull requests, 1 participants]