fix: redact known Hermes secret env vars regardless of code_file mode - #61352
fix: redact known Hermes secret env vars regardless of code_file mode#61352ShaoRou459 wants to merge 3 commits into
Conversation
Terminal output from non-env-dump commands (cat, type) uses code_file=True which skips the generic KEY=VALUE redaction pass. This causes opaque API keys without recognized vendor prefixes (Gemini AQ.*, Mistral, Tavily dev keys, BrowserUse bu_*, Spotify client IDs) to leak when cat'ing .env files. Add a curated set of known Hermes secret env var names plus an AST-based auto-scanner for tool requires_env lists. A new redaction pass runs before the code_file gate, matching only these known names to avoid false positives on source code like MAX_TOKENS=100. All values are redacted to ***, consistent with the prefix-matcher output for recognized key formats.
|
suggesting changes The new known-env redaction still misses a common Security evidence:
Signed: GPT-5.5-xhigh in Codex |
|
suggesting changes The latest head fixes the inline-comment redaction gap from my earlier comment, but it also rewrites Security evidence:
Signed: GPT-5.5-xhigh in Codex |
The _HERMES_KNOWN_ENV_RE required value to reach end-of-line ((?=\s*$)), so KEY=VALUE # comment forms leaked the value. Change the lookahead to (?=\s*(?:#.*)?$) so optional inline # comments after the value are allowed — value still redacted, comment preserved. Adds 8 tests covering: plain, export, inline comment, quoted with comment, extra spaces before comment, non-secret passthrough, and multiline blocks with mixed comments.
3905781 to
8348396
Compare
|
Apologies about the whitespace issue, will watch out going forward. |
|
Thanks for addressing the Problems
Suggested changes
Automated hermes-sweeper review. |
…ar list Following review feedback from @teknium1, replaced the manual list + AST scanner approach with a simpler and more robust solution. Instead of maintaining a list of ~95 known secret env var names + scanning tools/*.py for requires_env, the fix now detects when a terminal command reads a .env file (cat .env, head .env.local, etc.) and sets code_file=False so the existing _ENV_ASSIGN_RE regex handles redaction. This regex already matches any KEY=value where the key contains API_KEY, TOKEN, SECRET, PASSWORD, CREDENTIAL, or AUTH — covering all current and future env vars with zero list maintenance. This also resolves the gap @teknium1 identified: NOUS_API_KEY and any other provider credential declared outside tools/*.py is now covered automatically, since we're no longer relying on enumerating declaration sites. Per AGENTS.md, .env is for secrets only, so running the generic ENV redactor on .env content is the correct behavior to prevent secret leaks. Template files (.env.example, .env.sample, etc.) are explicitly excluded so the agent can still read those freely. Trade-off for this approach is agent autonomy: Because .env values are now redacted when read through the terminal (the read_file tool already blocks .env entirely), the agent cannot inspect or compare raw secret values in .env files without human intervention. An agent debugging a misconfigured API key would see OPENAI_API_KEY=*** instead of the actual key, and would need to ask the user to verify the value. File writes (write_file, patch) are unaffected — the agent can still create and modify .env files.
|
Following review feedback from @teknium1, replaced the manual list + AST scanner approach with a simpler and more robust solution. Instead of maintaining a list of ~95 known secret env var names + scanning tools/*.py for requires_env, the fix now detects when a terminal command reads a .env file (cat .env, head .env.local, etc.) and sets code_file=False so the existing _ENV_ASSIGN_RE regex handles redaction. This regex already matches any KEY=value where the key contains API_KEY, TOKEN, SECRET, PASSWORD, CREDENTIAL, or AUTH — covering all current and future env vars with zero list maintenance. This also resolves the gap @teknium1 identified: NOUS_API_KEY and any other provider credential declared outside tools/*.py is now covered automatically, since we're no longer relying on enumerating declaration sites. Per AGENTS.md, .env is for secrets only, so running the generic ENV redactor on .env content is the correct behavior to prevent secret leaks. Template files (.env.example, .env.sample, etc.) are explicitly excluded so the agent can still read those freely. Trade-off for this approach is agent autonomy: Because .env values are now redacted when read through the terminal (the read_file tool already blocks .env entirely), the agent cannot inspect or compare raw secret values in .env files without human intervention. An agent debugging a misconfigured API key would see OPENAI_API_KEY=*** instead of the actual key, and would need to ask the user to verify the value. File writes (write_file, patch) are unaffected — the agent can still create and modify .env files.
|
- Import file_safety._BLOCKED_PROJECT_ENV_BASENAMES instead of copying it (comment-enforced parallel lists drift); lookup is now case-insensitive to match file_safety's .lower() semantics (cat .ENV on macOS/Windows case-insensitive filesystems reads the same secrets). - Strip shell quotes plain split() leaves attached (cat ".env"). - Drop the dead _ENV_FILE_EXCLUDE_SUFFIXES logic (exact-basename membership already excludes templates) and the stray blank-line noise. - Document the defense-in-depth limits (sudo/full-path/substitution readers) mirroring is_env_dump_command's precedent, and correct the docstring overclaim about name-independence. - Annotate command as str | None (tests pass None).
…ar list Terminal output from file-read commands (cat, head, tail, ...) uses code_file=True, which skips the generic ENV-assignment redaction pass. Reading a .env file through the terminal therefore leaked any key whose value has no recognized vendor prefix (Mistral, Gemini AQ.*, tvly-dev-, bu_, Spotify client secrets). Detect file-read commands targeting .env-style basenames (mirroring agent/file_safety's blocked list) and route them to code_file=False so the existing ENV pass masks opaque values. Templates (.env.example, .env.sample, ...) are excluded. Salvaged from NousResearch#61352 (145 commits of drift; conflict with the test-prune wave resolved by NOT resurrecting pruned tests). Authored by @ShaoRou459. Closes NousResearch#61352
|
Merged via #80964 — your commit was cherry-picked onto current main with your authorship preserved ( The branch had drifted ~6000 commits; the only conflict was with the test-prune wave, resolved by not resurrecting the pruned tests (two of the carried-over fixtures had also been corrupted by the display-redaction layer into sentinel strings, so dropping them fixed that too). A small follow-up commit on top hardened the detection per review: the basename list is now imported from Thanks for the fix — and for iterating through the three design rounds to the routing approach. |
…ar list Terminal output from file-read commands (cat, head, tail, ...) uses code_file=True, which skips the generic ENV-assignment redaction pass. Reading a .env file through the terminal therefore leaked any key whose value has no recognized vendor prefix (Mistral, Gemini AQ.*, tvly-dev-, bu_, Spotify client secrets). Detect file-read commands targeting .env-style basenames (mirroring agent/file_safety's blocked list) and route them to code_file=False so the existing ENV pass masks opaque values. Templates (.env.example, .env.sample, ...) are excluded. Salvaged from NousResearch#61352 (145 commits of drift; conflict with the test-prune wave resolved by NOT resurrecting pruned tests). Authored by @ShaoRou459. Closes NousResearch#61352
…61352 - Import file_safety._BLOCKED_PROJECT_ENV_BASENAMES instead of copying it (comment-enforced parallel lists drift); lookup is now case-insensitive to match file_safety's .lower() semantics (cat .ENV on macOS/Windows case-insensitive filesystems reads the same secrets). - Strip shell quotes plain split() leaves attached (cat ".env"). - Drop the dead _ENV_FILE_EXCLUDE_SUFFIXES logic (exact-basename membership already excludes templates) and the stray blank-line noise. - Document the defense-in-depth limits (sudo/full-path/substitution readers) mirroring is_env_dump_command's precedent, and correct the docstring overclaim about name-independence. - Annotate command as str | None (tests pass None).
…ar list Terminal output from file-read commands (cat, head, tail, ...) uses code_file=True, which skips the generic ENV-assignment redaction pass. Reading a .env file through the terminal therefore leaked any key whose value has no recognized vendor prefix (Mistral, Gemini AQ.*, tvly-dev-, bu_, Spotify client secrets). Detect file-read commands targeting .env-style basenames (mirroring agent/file_safety's blocked list) and route them to code_file=False so the existing ENV pass masks opaque values. Templates (.env.example, .env.sample, ...) are excluded. Salvaged from NousResearch#61352 (145 commits of drift; conflict with the test-prune wave resolved by NOT resurrecting pruned tests). Authored by @ShaoRou459. Closes NousResearch#61352
…61352 - Import file_safety._BLOCKED_PROJECT_ENV_BASENAMES instead of copying it (comment-enforced parallel lists drift); lookup is now case-insensitive to match file_safety's .lower() semantics (cat .ENV on macOS/Windows case-insensitive filesystems reads the same secrets). - Strip shell quotes plain split() leaves attached (cat ".env"). - Drop the dead _ENV_FILE_EXCLUDE_SUFFIXES logic (exact-basename membership already excludes templates) and the stray blank-line noise. - Document the defense-in-depth limits (sudo/full-path/substitution readers) mirroring is_env_dump_command's precedent, and correct the docstring overclaim about name-independence. - Annotate command as str | None (tests pass None).
…ar list Terminal output from file-read commands (cat, head, tail, ...) uses code_file=True, which skips the generic ENV-assignment redaction pass. Reading a .env file through the terminal therefore leaked any key whose value has no recognized vendor prefix (Mistral, Gemini AQ.*, tvly-dev-, bu_, Spotify client secrets). Detect file-read commands targeting .env-style basenames (mirroring agent/file_safety's blocked list) and route them to code_file=False so the existing ENV pass masks opaque values. Templates (.env.example, .env.sample, ...) are excluded. Salvaged from NousResearch#61352 (145 commits of drift; conflict with the test-prune wave resolved by NOT resurrecting pruned tests). Authored by @ShaoRou459. Closes NousResearch#61352 (cherry picked from commit cf755f5)
…61352 - Import file_safety._BLOCKED_PROJECT_ENV_BASENAMES instead of copying it (comment-enforced parallel lists drift); lookup is now case-insensitive to match file_safety's .lower() semantics (cat .ENV on macOS/Windows case-insensitive filesystems reads the same secrets). - Strip shell quotes plain split() leaves attached (cat ".env"). - Drop the dead _ENV_FILE_EXCLUDE_SUFFIXES logic (exact-basename membership already excludes templates) and the stray blank-line noise. - Document the defense-in-depth limits (sudo/full-path/substitution readers) mirroring is_env_dump_command's precedent, and correct the docstring overclaim about name-independence. - Annotate command as str | None (tests pass None). (cherry picked from commit 15d7103)

Problem
Terminal output from non-env-dump commands (
cat,type) usescode_file=Truewhich skips the genericKEY=VALUEredaction pass. This causes opaque API keys without recognized vendor prefixes to leak when reading.envfiles through the terminal.Leaked keys: Gemini (
AQ.*), Mistral (no prefix), Tavily dev keys (tvly-dev-), BrowserUse (bu_), Spotify client IDs (hex).Root cause
redact_terminal_outputsetscode_file = not is_env_dump_command(command). Forcat, this isTrue, skipping all_ENV_ASSIGN_RE/_JSON_FIELD_RE/_YAML_ASSIGN_REpasses. Only prefix patterns run — but most of these keys have no recognized prefix.Fix
tools/*.pyforrequires_envlists inregistry.register()calls — so new tools are automatically coveredredact_sensitive_textthat runs before thecode_filegate, matching only these known names to avoid false positives on source code likeMAX_TOKENS=100All values are redacted to
***, consistent with the prefix-matcher output.Verification
cat .envnow redacts all 6 previously-leaked keys