# NousResearch/hermes-agent · filtering secrets from terminal snapshot files

## Open now

- 1 issue, 0 PRs open on GitHub.
- #62336: \[Security\] Terminal environment snapshots capture credential-bearing env vars to disk

Multiple issues and fixes address terminal session snapshots capturing credential-bearing environment variables to disk. Proposed mitigations include filtering sensitive env vars from snapshots and restricting snapshot file permissions to owner-only.

## TL;DR

- 6 member(s): 1 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 3 PR(s) closed without merging.
- A fix was merged to main in PR #60199. Verify it addresses your case.
- **Recommended:** read PR #60199 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-60199`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text.
- Members outside that structural core may belong to a different underlying problem.
- Pairwise link density `0.29`: the share of member pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. `1.00` means every pair is linked.

## Open related issues

- #62336: \[Security\] Terminal environment snapshots capture credential-bearing env vars to disk

## Closed / duplicate issues

- #48441: \[Security\] Terminal session snapshots leak .env secrets to disk in plaintext (cache/terminal/hermes-snap-*.sh)

## Prior & competing PRs

- **#48443: fix(environments): filter secret env vars from terminal snapshots**. closed without merge · fix tier `suggested`
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#48441`, `#38249`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#60199: fix(security): create terminal env snapshots owner-only (0600)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `scripts/release.py`, `tests/tools/test_base_environment.py`, `tools/environments/base.py`
  - fix hunks:
  - `scripts/release.py`

    ```diff
    @@ -1811,6 +1811,7 @@
         "[redacted:email]": "vanthinh6886",  # PR #28018 salvage (yaml/flock/atomic write guards)
         "[redacted:email]": "erikengervall",  # PR #28774 (firecrawl integration tag)
         "[redacted:email]": "egilewski",  # PR #30432 (MEDIA path traversal fix, GHSA-jmf9-9729-7pp8)
    +    "[redacted:email]": "andrewhomeyer",  # PR #20056 (snapshot owner-only perms, co-author on #57386 salvage)
         "[redacted:email]": "McClean-Edison",  # PR #29817 (register_auxiliary_task plugin API)
         "OYLFLMH@users.noreply.github.com": "OYLFLMH",  # PR #48312 salvage (cli_refresh_interval config, #48309)
         "[redacted:email]": "SamuelZ12",  # PR #7480 (show recap after in-session resume)
    ```
  - `tests/tools/test_base_environment.py`

    ```diff
    @@ -174,6 +174,32 @@ def fake_run_bash(cmd_string, *, login=False, timeout=120, stdin_data=None):
             assert "$BASHPID" in boot
             assert ".tmp.$$" not in boot
     
    +    def test_snapshot_writes_use_private_umask_after_user_command(self):
    +        env = _TestableEnv()
    +        env._snapshot_ready = True
    +        wrapped = env._wrap_command("echo hi", "/tmp")
    +
    +        assert "umask 077" in wrapped
    +        assert wrapped.index("eval 'echo hi'") < wrapped.index("umask 077")
    +        assert wrapped.index("umask 077") < wrapped.index("export -p >")
    +
    +    def test_init_session_bootstrap_uses_private_umask(self):
    +        env = _TestableEnv()
    +        captured = {}
    +
    +        def fake_run_bash(cmd_string, *, login=False, timeout=120, stdin_data=None):
    +            captured["cmd"] = cmd_string
    +            raise RuntimeError("stop after capture")
    +
    +        env._run_bash = fake_run_bash  # type: ignore[assignment]
    +        try:
    +            env.init_session()
    +        except Exception:
    +            pass
    +        boot = captured.get("cmd", "")
    +        assert "umask 077" in boot
    +        assert boot.index("umask 077") < boot.index("export -p >")
    +
     
     class TestAtomicSnapshotConcurrencyBehavioral:
         """Behavioral regression for #38249 — actually EXECUTES the generated
    ```

    ```diff
    @@ -250,6 +276,57 @@ def test_failed_export_does_not_destroy_good_snapshot(self, tmp_path):
             assert "export GOOD=1" in out.stdout, "good snapshot was destroyed by a failed export"
     
     
    +class TestSnapshotFileModes:
    +    """Snapshot metadata files are private without changing user command umask."""
    +
    +    def test_snapshot_and_cwd_files_are_0600(self, tmp_path):
    +        import os
    +        from pathlib import Path
    +        import shutil
    +        import stat
    +        import subprocess
    +        if not shutil.which("bash"):
    +            import pytest
    +            pytest.skip("bash required")
    +
    +        class ExecutableEnv(BaseEnvironment):
    +            def __init__(self, temp_dir):
    +                self._temp_dir = str(temp_dir)
    +                super().__init__(cwd=str(temp_dir), timeout=10)
    +
    +            def get_temp_dir(self):
    +                return self._temp_dir
    +
    +            def _run_bash(self, cmd_string, *, login=False, timeout=120, stdin_data=None):
    +                proc = subprocess.Popen(
    +                    ["/bin/bash", "-lc", cmd_string],
    +                    stdout=subprocess.PIPE,
    +                    stderr=subprocess.STDOUT,
    +                    stdin=subprocess.DEVNULL,
    +                    text=True,
    +                    cwd=self.cwd,
    +                )
    +                proc.communicate(timeout=timeout)
    +                return proc
    +
    +            def cleanup(self):
    +                pass
    +
    +        old_umask = os.umask(0o022)
    +        try:
    +            env = ExecutableEnv(tmp_path)
    +            env.init_session()
    +
    +            user_file = tmp_path / "user-created.txt"
    +            env.execute(f"touch {user_file}")
    +
    +            assert stat.S_IMODE(user_file.stat().st_mode) == 0o644
    +            assert stat.S_IMODE(Path(env._snapshot_path).stat().st_mode) == 0o600
    +            assert stat.S_IMODE(Path(env._cwd_file).stat().st_mode) == 0o600
    +        finally:
    +            os.umask(old_umask)
    +
    +
     class TestExtractCwdFromOutput:
         def test_happy_path(self):
             env = _TestableEnv()
    ```
  - `tools/environments/base.py`

    ```diff
    @@ -395,6 +395,7 @@ def init_session(self):
             # with ``$BASHPID`` left outside the quotes so it still expands.
             _snap_tmp = shlex.quote(self._snapshot_path + ".tmp.") + "$BASHPID"
             bootstrap = (
    +            f"umask 077\n"
                 f"export -p > {_snap_tmp}\n"
                 # Dump function definitions, filtering out private (``_``-prefixed)
                 # helpers — mainly bash-completion internals (``_git``, ``_make``…)
    ```

    ```diff
    @@ -502,6 +503,9 @@ def _wrap_command(self, command: str, cwd: str) -> str:
             # Run the actual command
             parts.append(f"eval '{escaped}'")
             parts.append("__hermes_ec=$?")
    +        # Restrict Hermes metadata files without changing the user's command
    +        # umask. Snapshot files may contain env-carried secrets.
    +        parts.append("umask 077")
     
             # Re-dump env vars to snapshot (atomic replacement to avoid races).
             # Chain mv on the export succeeding so a failed/partial dump never
    ```
- **#56952: \[codex\] fix terminal snapshot secret filtering**. closed without merge
  - maintainer direction: category `wontfix_bydesign`; implication `wontfix`; refs `#48441`, `#41278`
- **#62346: fix(tools): filter credential-bearing env vars from terminal snapshots**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `a1e6ea7d7`

## Fix status

- Merged to main in PR #60199 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-07-07`.
- Subsystem: `environments`
