Skip to content

fix(security): deny writes to read-blocked HERMES_HOME credential stores - #38490

Open
Dusk1e wants to merge 1 commit into
NousResearch:mainfrom
Dusk1e:fix/credential-store-write-parity
Open

fix(security): deny writes to read-blocked HERMES_HOME credential stores#38490
Dusk1e wants to merge 1 commit into
NousResearch:mainfrom
Dusk1e:fix/credential-store-write-parity

Conversation

@Dusk1e

@Dusk1e Dusk1e commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

What & why

get_read_block_error hides several HERMES_HOME credential stores from the
agent, but is_write_denied did not block writing three of them — so a
prompt-injected write_file could still overwrite real secrets:

File Read-blocked Write-blocked (before)
auth/google_oauth.json (Gemini OAuth tokens)
cache/bws_cache.json (Bitwarden plaintext secrets)
auth.lock (auth.json advisory lock)

This closes the read/write asymmetry by enforcing the invariant "every
credential file hidden from reads is also protected from overwrites."
The read
and write guards now share a single CREDENTIAL_FILE_NAMES list, so any future
credential added to the read block is automatically write-protected too — the
lists can't drift apart again. config.yaml stays write-denied but
intentionally readable. Internal writers (auth.py, google_oauth.py,
bitwarden.py) write directly and don't pass through this guard, so no runtime
flow changes.

Changes

  • agent/file_safety.py — lift CREDENTIAL_FILE_NAMES to a shared module
    constant; is_write_denied now denies ("config.yaml",) + CREDENTIAL_FILE_NAMES.
  • tests/tools/test_write_deny.pyTestCredentialStoreWriteParity: per-file
    regression tests, root-widening-under-profile test, and a contract test
    asserting CREDENTIAL_FILE_NAMES ⊆ write-denied.

Testing

scripts/run_tests.sh tests/tools/test_write_deny.py tests/agent/test_file_safety_credentials.py -q
→ 44 passed, 1 skipped (Windows symlink test). Broader file-safety + write-path
suites: no regressions. scripts/check-windows-footguns.py: clean.

@alt-glitch alt-glitch added type/security Security vulnerability or hardening area/auth Authentication, OAuth, credential pools comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists labels Jun 3, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Closely overlaps #37336, which pairs write-deny for the same read-protected credential stores (google_oauth.json, bws_cache.json, auth.lock). Maintainers may want to consolidate these two PRs.

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Approved

Changes

Merges the credential-file list into a single CREDENTIAL_FILE_NAMES constant shared between read block (get_read_block_error) and write deny (is_write_denied), and adds config.yaml to the write-deny list (already readable, but not writable) while introducing auth.lock to the shared credential list.

Security Observations

  • Single source of truth — eliminates the duplication that caused the prior gap where auth.lock was blocklisted in get_read_block_error but missing from is_write_denied.
  • Correctly distinguishes read-blocked files (credentials that must not be read OR written) from config, which is readable but must not be written (to prevent profile overrides).
  • config.yaml write-block rationale is well documented in the code comment.

Testing

Comprehensive coverage in TestCredentialStoreWriteParity:

  • Per-profile and root-path write denies for auth.lock, google_oauth.json, bws_cache.json.
  • Invariant test that every CREDENTIAL_FILE_NAMES entry is write-denied.

Reviewed by Hermes Agent

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for identifying a real remaining gap: current agent/file_safety.py:241-251 read-blocks auth.lock, Google OAuth tokens, and the Bitwarden cache, while is_write_denied at agent/file_safety.py:109-144 does not cover them.

Problems

  • The new universal parity at agent/file_safety.py:139 also re-denies auth.json, webhook_subscriptions.json, and config.yaml. That conflicts with merged maintainer policy 81e42335 (#45947), which intentionally made Hermes control files writable; current tests/tools/test_file_operations.py:84-91 enforces that contract.
  • tests/tools/test_write_deny.py:146-159 freezes that broader parity, so it would prevent the intentional read/write distinction from being retained.

Suggested changes

  • Re-scope the write-deny entries and regression tests to the three remaining targets, preserving the current writable-control-file behavior.
  • Cover active-profile and root paths for each retained write-denied target.

Automated hermes-sweeper review.

Comment thread agent/file_safety.py
# be silently overwritten. ``config.yaml`` is the one extra: write-denied to
# protect the profile, but intentionally still readable (see
# get_read_block_error / test_config_yaml_not_blocked).
denied_file_names = ("config.yaml",) + CREDENTIAL_FILE_NAMES

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This couples all read-blocked files to write denial, but current main deliberately keeps auth.json, config.yaml, and webhook_subscriptions.json writable (81e42335, with coverage in tests/tools/test_file_operations.py:84-91). Please use a narrower write-denied set for the three intended stores instead of universal read/write parity.

@teknium1 teknium1 added sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

The PR identifies a real security gap: on current main, auth.lock, the Google OAuth store, and the Bitwarden plaintext cache are read-blocked but still writable through the generic file tools, including both the active profile and shared Hermes root.

Please rebase/replay this change onto current main and narrow the write-side additions to those three stores. The current file-safety policy intentionally preserves write access for auth.json, webhook_subscriptions.json, and config.yaml; the new universal parity test would regress that contract. Preserve the existing encrypted Bitwarden-cache and safe-root behavior while rebasing, then add active-profile and shared-root regression coverage for all three target stores.

Security evidence:

  • trust boundary: Prompt-injected file-tool arguments can target Hermes credential stores through generic writes.
  • source/sink/invariant: Read-blocked auth.lock, Google OAuth, and Bitwarden plaintext stores must also be write-denied at the active profile and shared root.
  • current-main reproduction: Current main reproduced the read/write asymmetry for all three stores at both locations.
  • PR-head or patch-replay validation: The proposed head denies all three stores, but its broad replay also denies intentionally writable control files and drops newer encrypted-cache behavior.
  • positive/negative cases: Profile/root denials and write-path protections passed; control-file writability, encrypted-cache protection, and safe-root behavior must remain intact.
  • residual bypass search: Generic write, patch, move, delete, and ACP write paths apply the guard; terminal access remains a documented same-user defense-in-depth bypass.
  • reviewer validation: Focused security tests passed (45 tests); the current-main replay needs the scope correction before merge.

Not checked:

  • Full test suite
  • CodeRabbit review

Signed: GPT-5.6-sol-xhigh in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants