fix(credentials): never mount master credential stores into skill sandboxes - #67640
Merged
teknium1 merged 1 commit intoJul 20, 2026
Merged
Conversation
…dboxes
register_credential_file() takes a skill-declared relative path from
required_credential_files frontmatter and bind-mounts it read-only into the
remote sandbox the skill's own code runs in. It validates that the resolved
path stays inside HERMES_HOME — the docstring names the threat directly:
so that a malicious skill cannot declare
required_credential_files: ['../../.ssh/id_rsa'] and exfiltrate
sensitive host files into a container sandbox
Containment is the wrong boundary on its own, because HERMES_HOME is exactly
where the master credential stores live. Traversal is blocked; asking for the
keys by name is not:
skill declares mounted? agent may read it?
.env YES DENIED
auth.json YES DENIED
.anthropic_oauth.json YES DENIED
cache/bws_cache.json YES DENIED
mcp-tokens/srv.json YES DENIED
google_token.json YES allowed
../../.ssh/id_rsa no n/a
Every row marked DENIED is refused by the canonical read guard
(agent.file_safety.get_read_block_error) — the agent cannot read_file them —
yet one line of hub-installed skill frontmatter gets them bind-mounted where
that skill can cat them. .env alone is every provider API key.
Reuse the canonical deny-list as the mount bar: what the agent is forbidden
to read is not mountable either, so the mount surface cannot hand a skill
what the read surface denies it. Fails CLOSED — if the guard can't be
consulted the mount is refused rather than risked.
The module keeps doing its job: a skill still mounts its own service token
(google_token.json, skills/*), and a refused entry is reported back through
register_credential_files' missing list instead of failing the batch.
The three prior PRs here (NousResearch#3946, NousResearch#3951, NousResearch#4316) all hardened traversal; this
closes the half that traversal validation never covered.
Contributor
|
Thanks for the focused security fix. The premise is confirmed on current main: The proposed canonical-guard approach matches the existing fail-closed security pattern in Automated hermes-sweeper review. |
Contributor
This was referenced Jul 20, 2026
teknium1
added a commit
that referenced
this pull request
Jul 20, 2026
Follow-up to #67640: move the agent.file_safety import to module top (stdlib-only, no circular-import concern), replace the over-broad except Exception + logger.warning with an import sentinel plus logger.exception so a guard failure is debuggable instead of silently swallowed. Adds fail-closed tests asserting the diagnostic is emitted.
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…search#67665) Follow-up to NousResearch#67640: move the agent.file_safety import to module top (stdlib-only, no circular-import concern), replace the over-broad except Exception + logger.warning with an import sentinel plus logger.exception so a guard failure is debuggable instead of silently swallowed. Adds fail-closed tests asserting the diagnostic is emitted.
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.

What does this PR do?
register_credential_file()takes a skill-declared relative path fromrequired_credential_filesfrontmatter and bind-mounts it read-only into theremote sandbox the skill's own code runs in. It validates that the resolved
path stays inside
HERMES_HOME, and the docstring names the threat directly:Containment is the wrong boundary on its own, because
HERMES_HOMEis exactlywhere the master credential stores live. Traversal is blocked; asking for the
keys by name is not:
.envauth.json.anthropic_oauth.jsoncache/bws_cache.jsonmcp-tokens/srv.jsongoogle_token.json../../.ssh/id_rsa/etc/passwdEvery DENIED row is refused by the canonical read guard
(
agent.file_safety.get_read_block_error) — the agent cannotread_filethem —yet one line of hub-installed skill frontmatter gets them bind-mounted where that
skill can
catthem..envalone is every provider API key;auth.jsonis everyprovider token and OAuth grant.
Related Issue
Fixes #
Type of Change
Changes Made
tools/credential_files.py— reuse the canonical deny-list as the mount bar:what the agent is forbidden to read is not mountable either, so the mount
surface can't hand a skill what the read surface denies it. Fails closed —
if the guard can't be consulted the mount is refused rather than risked.
tests/tools/test_credential_files.py— parametrised refusal coverage for allsix master stores, plus the legitimate path, batch behaviour, and the
pre-existing traversal guard.
The module keeps doing its job: a skill still mounts its own service token
(
google_token.json,skills/*), and a refused entry is reported back throughregister_credential_files'missinglist instead of failing the batch.Testing
Seven of the nine new tests fail on
main(every master store plus the batchcase). The other two — legitimate token still mounts, traversal still refused —
pass both ways, which is what shows the fix doesn't trade a leak for a
regression.
Credential/skill/sandbox/docker/modal selection baseline-compared against a clean
origin/mainworktree: identical 28 pre-existing failures (all Windows-env —symlink creation, docker discovery, path mapping),
1335 → 1344passed.Checklist
mainand covered by a failing-before/passing-after testorigin/main)