filtering secrets from terminal snapshot files
Cluster 48441 · environments. Data as of
1 open item. Fix merged in #60199 on 7 Jul 2026. Check it covers your case.
The short answer
- Known?
- Yes. 6 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #60199. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #60199. That is separate from the fix event. - Still open
- 1 issue and 0 PRs.
Next step: read #60199, then check whether your report is the one still open.
Read #60199Pick up a Lab taskMultiple 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.
Open issues1
- Issue#62336[Security] Terminal environment snapshots capture credential-bearing env vars to disk
Open PRs0
Fixes on record
-
#60199 fix(security): create terminal env snapshots owner-only (0600)
files:
scripts/release.py,tests/tools/test_base_environment.py,tools/environments/base.pyShow the diff
-
scripts/release.py@@ -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) "[email protected]": "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@@ -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 @@ -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@@ -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``…) @@ -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
-
3 PRs closed without merging
From the close comments: superseded by another PR 1 · won't fix / by design 1 · other 1
-
#48443 fix(environments): filter secret env vars from terminal snapshots · other →
#48441,#38249 -
#56952 [codex] fix terminal snapshot secret filtering · won't fix / by design →
#48441,#41278 -
#62346 fix(tools): filter credential-bearing env vars from terminal snapshots · superseded by another PR →
a1e6ea7
Closed issues · 1
- Closed#48441[Security] Terminal session snapshots leak .env secrets to disk in plaintext (cache/terminal/hermes-snap-*.sh)
How this cluster was grouped
-
Link evidence (
partly structural links): 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 item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- environments
- Merged PRs
- #60199
- Closed issues
- 1
- Attempts
- 3 PRs closed without merging
- Fix link checked
- 7 Jul 2026