Skip to content

Fix: Terminal subprocess env leaks AUXILIARY/GATEWAY_RELAY secrets - #53715

Closed
necoweb3 wants to merge 4 commits into
NousResearch:mainfrom
necoweb3:fix/terminal-auxiliary-credential-leak
Closed

Fix: Terminal subprocess env leaks AUXILIARY/GATEWAY_RELAY secrets#53715
necoweb3 wants to merge 4 commits into
NousResearch:mainfrom
necoweb3:fix/terminal-auxiliary-credential-leak

Conversation

@necoweb3

Copy link
Copy Markdown
Contributor

The terminal tool's env blocklist (_HERMES_PROVIDER_ENV_BLOCKLIST) uses name-based matching that catches exact provider key names (e.g. ANTHROPIC_API_KEY) but misses dynamic secrets injected by the gateway:

  • AUXILIARY_*_API_KEY (set by gateway/run.py:1564-1570 and cli.py for side-LLM work — curator, vision, embedding, title generation)
  • GATEWAY_RELAY_SECRET (set by gateway/relay/__init__.py:553-555 for relay auth)

These secrets are injected into os.environ at runtime. When the terminal tool spawns subprocesses (bash, docker, SSH), the env blocklist doesn't catch them because it matches by exact name, not prefix/pattern. The code_execution sandbox (code_execution_tool.py:90-91) correctly uses substring matching via _SECRET_SUBSTRINGS, but the terminal tool does not.

Any terminal command (ls, git status, cat, etc.) in a gateway session leaks these secrets to child processes, logs, and shared environments.

Fix

  1. Added _is_hermes_internal_secret(name) helper in local.py that checks both the name-based blocklist AND dynamic prefix/suffix patterns (AUXILIARY_*_API_KEY, GATEWAY_RELAY_SECRET)
  2. Updated _make_run_env and _sanitize_subprocess_env to use the new helper
  3. Updated the Docker backend's passthrough filter (docker.py) which imports the same blocklist

Repro

# In a gateway session:
import os
os.environ["AUXILIARY_VISION_API_KEY"] = "sk-secret-vision"
# Then run any terminal command:
# The secret is visible in /proc/PID/environ and subprocess env

Test

68-line regression test class TestAuxiliarySecretStripping with 4 test methods covering: blocklist passthrough, prefix matching, suffix matching, and combined env filtering.


@alt-glitch alt-glitch added type/security Security vulnerability or hardening P1 High — major feature broken, no workaround comp/tools Tool registry, model_tools, toolsets tool/terminal Terminal execution and process management backend/docker Docker container execution area/auth Authentication, OAuth, credential pools sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data labels Jun 27, 2026

@egilewski egilewski 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.

requesting changes

The terminal local-environment path still lets dynamically named AUXILIARY_* secrets through when they are registered in the sandbox env passthrough registry. That registry is reachable from skill declarations and tools.env_passthrough, and it currently refuses static Hermes provider credentials only when the name is in _HERMES_PROVIDER_ENV_BLOCKLIST. Because this PR handles dynamic AUXILIARY_<TASK>_API_KEY and AUXILIARY_<TASK>_BASE_URL only in tools.environments.local, a skill or config passthrough entry for one of those dynamic names still bypasses the new filter.

Blocking evidence: on the PR head, after registering AUXILIARY_VISION_API_KEY and AUXILIARY_VISION_BASE_URL through register_env_passthrough, both _make_run_env({}) and _sanitize_subprocess_env(...) still include the auxiliary key, and _make_run_env({}) still includes the auxiliary base URL. The same probe shows GATEWAY_RELAY_SECRET is refused only because this PR added that exact static name to the provider blocklist, which leaves the dynamic auxiliary pattern unprotected at the passthrough boundary.

Security impact: untrusted or model-influenced terminal commands and local subprocesses can still receive Hermes-managed provider credentials if a skill declaration or tools.env_passthrough registers the dynamic auxiliary names. The invariant needs to hold regardless of passthrough registration: AUXILIARY_<TASK>_API_KEY and AUXILIARY_<TASK>_BASE_URL must not reach _make_run_env or _sanitize_subprocess_env outputs. Please deny the dynamic auxiliary patterns either in the passthrough registry itself or unconditionally in the local subprocess environment filters, and add a regression test for the passthrough-registered case.

Signed: GPT-5.5-xhigh in Codex

@necoweb3

Copy link
Copy Markdown
Contributor Author

requesting changes

The terminal local-environment path still lets dynamically named AUXILIARY_* secrets through when they are registered in the sandbox env passthrough registry. That registry is reachable from skill declarations and tools.env_passthrough, and it currently refuses static Hermes provider credentials only when the name is in _HERMES_PROVIDER_ENV_BLOCKLIST. Because this PR handles dynamic AUXILIARY_<TASK>_API_KEY and AUXILIARY_<TASK>_BASE_URL only in tools.environments.local, a skill or config passthrough entry for one of those dynamic names still bypasses the new filter.

Blocking evidence: on the PR head, after registering AUXILIARY_VISION_API_KEY and AUXILIARY_VISION_BASE_URL through register_env_passthrough, both _make_run_env({}) and _sanitize_subprocess_env(...) still include the auxiliary key, and _make_run_env({}) still includes the auxiliary base URL. The same probe shows GATEWAY_RELAY_SECRET is refused only because this PR added that exact static name to the provider blocklist, which leaves the dynamic auxiliary pattern unprotected at the passthrough boundary.

Security impact: untrusted or model-influenced terminal commands and local subprocesses can still receive Hermes-managed provider credentials if a skill declaration or tools.env_passthrough registers the dynamic auxiliary names. The invariant needs to hold regardless of passthrough registration: AUXILIARY_<TASK>_API_KEY and AUXILIARY_<TASK>_BASE_URL must not reach _make_run_env or _sanitize_subprocess_env outputs. Please deny the dynamic auxiliary patterns either in the passthrough registry itself or unconditionally in the local subprocess environment filters, and add a regression test for the passthrough-registered case.

Signed: GPT-5.5-xhigh in Codex

Fixed, _is_hermes_internal_secret checks now block unconditionally regardless of passthrough registration. Added regression tests for the passthrough-registered case.

@egilewski egilewski 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.

looks mergeable

Security evidence:

  • trust boundary: gateway/CLI-managed secrets in the Hermes process environment must not flow into model-controlled terminal, background-process, or Docker subprocess environments.
  • source/sink/invariant: AUXILIARY_<TASK>_API_KEY, AUXILIARY_<TASK>_BASE_URL, and GATEWAY_RELAY_* values in os.environ or implicit passthrough registries must be stripped before _make_run_env, _sanitize_subprocess_env, or Docker init env forwarding expose child process environments.
  • current-main reproduction: on current main (d470ed0c4c4cb59e83ceb025609b6c4f2d0a614f), direct probes showed _make_run_env({}) and _sanitize_subprocess_env(os.environ.copy()) both retained AUXILIARY_VISION_API_KEY, AUXILIARY_VISION_BASE_URL, GATEWAY_RELAY_SECRET, and GATEWAY_RELAY_DELIVERY_KEY.
  • PR-head validation: on head 70c76bcd7b805793153e01ff5797f82247985751, the same probes stripped those secrets while preserving non-secret AUXILIARY_VISION_MODEL; a passthrough-stub probe showed auxiliary key/base URL are still stripped even when passthrough would otherwise allow them; Docker implicit passthrough stripped auxiliary key/base URL while preserving non-secret PUBLIC_CONFIG.
  • positive/negative cases: tests/tools/test_local_env_blocklist.py::TestAuxiliarySecretStripping passed (6 passed), covering direct auxiliary stripping, relay static blocklist stripping, helper matching, and passthrough-registered auxiliary cases.
  • residual bypass search: background local process spawning uses _sanitize_subprocess_env, and Docker implicit passthrough now filters dynamic auxiliary patterns before env injection; explicit docker_forward_env remains an existing operator opt-in and was not treated as a model-controlled bypass.
  • reviewer-tool status: CodeRabbit ran after rate-limit waits and returned two minor test-only suggestions; both were checked and treated as non-blocking because _sanitize_subprocess_env imports tools.env_passthrough.is_env_passthrough inside the function, and GATEWAY_RELAY_* is covered by the static blocklist rather than _is_hermes_internal_secret.

Validation:

  • git merge-tree --write-tree refs/remotes/origin/main refs/remotes/origin/pr/53715 passed with tree fece8e9cd0ce1970bcec7701734231ca8c7538d1.
  • git diff --check refs/remotes/origin/main...refs/remotes/origin/pr/53715 passed.
  • python -B -m pytest tests/tools/test_local_env_blocklist.py::TestAuxiliarySecretStripping -q -p no:cacheprovider passed with 6 passed.
  • py_compile passed for tools/environments/local.py, tools/environments/docker.py, and tests/tools/test_local_env_blocklist.py.

Signed: GPT-5.5-xhigh in Codex

@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 taking on a real security boundary issue. I verified the premise on current main: gateway/run.py:1523-1524 injects AUXILIARY_BASE_URL/API_KEY, gateway/relay/init.py:553-555 injects GATEWAY_RELAY*, and current tools/environments/local.py:232 plus :239 only filter exact blocklist names.

Problems

  • Current main has a newer sibling subprocess env helper, tools/environments/local.py:286 hermes_subprocess_env(), that copies os.environ at :318 and only strips exact blocklist keys when inherit_credentials is false at :328-331. PR head 70c76bcd fixes _make_run_env/sanitize_subprocess_env, but a salvage onto current main should also apply the same AUXILIARY* secret rule there.
  • PR head adds GATEWAY_RELAY_* to the provider blocklist at tools/environments/local.py:189-191, but current-main hermes_subprocess_env(inherit_credentials=True) preserves blocklisted provider credentials. If relay secrets are Tier-1 gateway secrets, they need always-strip behavior there too.

Suggested changes

  • During salvage, apply the new internal-secret predicate to hermes_subprocess_env() as well as the terminal/docker paths.
  • Add tests in tests/tools/test_hermes_subprocess_env.py for AUXILIARY__API_KEY/BASE_URL and GATEWAY_RELAY under both inherit_credentials=False and True.

Automated hermes-sweeper review.

})
return frozenset(blocked)


_HERMES_PROVIDER_ENV_BLOCKLIST = _build_provider_env_blocklist()


def _is_hermes_internal_secret(key: str) -> bool:

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.

When this helper is salvaged onto current main, please apply it to hermes_subprocess_env() too; that newer helper also copies os.environ for child processes and is outside the _make_run_env/_sanitize_subprocess_env paths changed here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the branch to apply the AUXILIARY/GATEWAY_RELAY internal-secret predicate to hermes_subprocess_env() as well.

The new coverage verifies these are stripped under both inherit_credentials=False and inherit_credentials=True, while non-secret auxiliary model/provider config remains visible.

Tests:

python -m pytest tests\tools\test_hermes_subprocess_env.py -q --timeout-method=thread
22 passed

python -m pytest tests\tools\test_local_env_blocklist.py -k "AuxiliarySecretStripping" -q --timeout-method=thread
6 passed, 40 deselected

python -m pytest tests\tools\test_hermes_subprocess_env.py -k "HermesInternalDynamicSecrets or TierInvariants or InheritCredentials" -q --timeout-method=thread
16 passed, 6 deselected

git diff --check
clean

necoweb3 added 4 commits June 29, 2026 02:08
…ess env

The terminal tool's subprocess env filter (_HERMES_PROVIDER_ENV_BLOCKLIST)
is name-based and does not include AUXILIARY_*_API_KEY or
GATEWAY_RELAY_SECRET — dynamic secrets injected by gateway/run.py and
cli.py from config.yaml[auxiliary]. The code-execution sandbox
(code_execution_tool.py) correctly catches these via substring matching
on KEY/SECRET/TOKEN, but the terminal backend was never brought along.

This asymmetry means model-authored terminal commands (curl, printenv,
echo ) can exfiltrate auxiliary API keys (vision, title-gen,
web-extract) and the gateway relay secret to external servers. The
approval system does not intercept plain curl, and redaction cannot
help when the secret traverses the network without touching stdout.

Fix:
- Add _is_hermes_internal_secret() to catch AUXILIARY_*_API_KEY and
  AUXILIARY_*_BASE_URL patterns not covered by the static blocklist.
- Add GATEWAY_RELAY_ID/SECRET/DELIVERY_KEY to the blocklist.
- Apply the check in _make_run_env, _sanitize_subprocess_env, and the
  Docker passthrough-key filter — all subprocess-spawning paths that
  previously relied on the name-only blocklist.

Impact: credential exfiltration via terminal tool on any gateway
deployment (Telegram/Discord/Slack) where auxiliary task keys are
configured. Bypasses both approval (plain curl not in DANGEROUS_PATTERNS)
and redaction (network exfil never touches stdout).
…tripping

Covers _make_run_env, _sanitize_subprocess_env, and _is_hermes_internal_secret
to prevent credential exfiltration via terminal tool subprocess env.
…rough registry

The passthrough registry (register_env_passthrough / tools.env_passthrough)
could override the _is_hermes_internal_secret blocklist, allowing
AUXILIARY_<TASK>_API_KEY and AUXILIARY_<TASK>_BASE_URL to reach subprocess
environments when registered via skill declarations or config.

Fix: remove the 'and not _is_passthrough(k)' guard from all three
_is_hermes_internal_secret checks in _make_run_env and
_sanitize_subprocess_env. Internal secrets are now blocked unconditionally
regardless of passthrough registration. The passthrough escape hatch
still applies to the static _HERMES_PROVIDER_ENV_BLOCKLIST.

Add regression tests verifying passthrough-registered auxiliary secrets
are blocked in both _make_run_env and _sanitize_subprocess_env.
@necoweb3
necoweb3 force-pushed the fix/terminal-auxiliary-credential-leak branch from 70c76bc to 9f3f405 Compare June 29, 2026 01:16
@necoweb3
necoweb3 requested a review from teknium1 June 29, 2026 01:20
@egilewski

Copy link
Copy Markdown
Contributor

looks mergeable

This PR fixes the credential-environment leak by stripping dynamic auxiliary API key/base URL values and relay-auth secrets before Hermes-managed terminal, helper, subprocess, and Docker implicit passthrough environments are built, while preserving non-secret auxiliary model configuration and the documented explicit terminal.docker_forward_env operator opt-in; the same probes that reproduced the leak on current main passed on PR head, focused affected tests passed with 68 passed, py_compile passed for the changed source/tests, git merge-tree --write-tree and git diff --check passed, and CodeRabbit's Docker explicit-forward concern was adjudicated as non-blocking against the repo's documented opt-in semantics.

Signed: GPT-5.5-xhigh in Codex

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jun 29, 2026
kshitijk4poor added a commit that referenced this pull request Jul 1, 2026
… env

Subprocesses spawned by the terminal tool, execute_code, Docker backend, and
the codex app-server could inherit Hermes-internal secrets that the name-based
`_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into
`os.environ` at runtime under dynamic names:

- `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM
  credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py
  (vision, web_extract, approval, compression, plugin-registered tasks). Often
  separate, higher-spend keys plus base URLs pointing at private endpoints.
- `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned
  by gateway/relay.

Additionally, agent/transports/codex_app_server.py built its spawn env from a
raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()`
helper entirely — handing every codex subprocess the full Tier-1 secret set
(GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session
token) unfiltered. This is the #29157 sibling spawn-site gap; copilot_acp_client
already routes through the helper.

Fix — single chokepoint:
- Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the
  single source of truth for the dynamic secret patterns. Matches
  AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves
  non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible.
- Wire the predicate into every spawn path unconditionally (ignores skill
  env_passthrough opt-in AND inherit_credentials — a model-driving CLI never
  needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env`
  (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter.
- Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the
  exact-match path catches them independently of the predicate.
- Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS`
  (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including
  the codex/copilot `inherit_credentials=True` path that skips the Tier-2
  blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has
  no secret suffix, so enumerating it here is what closes its leak on the
  inherit path (self-review W1).
- Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now
  consults the same predicate, so a skill can't register these names as
  passthrough and tunnel them into an execute_code / terminal child.
- Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)`
  — strips Tier-1 + dynamic-internal secrets while provider creds (which codex
  needs to authenticate) still flow.

Consolidates PRs #53715 (necoweb3 — the _is_hermes_internal_secret backbone +
Docker filter), #53503 (srojk34 — env_passthrough guard), and #55709 (srojk34 —
codex routing). Retires #52348 (claudlos): its copilot half is already on main,
and its codex half used the full-strip `_sanitize_subprocess_env` which would
break codex provider auth — the correct tier is `inherit_credentials=True`.

Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough
override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers),
TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough
defense-in-depth cases.

Co-authored-by: necoweb3 <sswdarius@gmail.com>
Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com>
Co-authored-by: claudlos <claudlos@agentmail.to>
@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Merged via #56202 (commit a658f3b) — your commit's authorship is preserved via Co-authored-by. This PR was the strongest of the cluster: your _is_hermes_internal_secret predicate became the single source of truth wired unconditionally into every subprocess spawn path (_sanitize_subprocess_env, _make_run_env, hermes_subprocess_env, the Docker forward filter, and env_passthrough), plus your Docker passthrough-filter fix and the static GATEWAY_RELAY_* blocklist additions landed as-is.

The consolidated PR also folded in the env_passthrough defense-in-depth guard (#53503) and codex app-server routing (#55709), and added the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to _ALWAYS_STRIP_KEYS so _ID (no secret suffix) is stripped on the model-driving-CLI inherit path too. Thanks!

waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
… env

Subprocesses spawned by the terminal tool, execute_code, Docker backend, and
the codex app-server could inherit Hermes-internal secrets that the name-based
`_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into
`os.environ` at runtime under dynamic names:

- `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM
  credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py
  (vision, web_extract, approval, compression, plugin-registered tasks). Often
  separate, higher-spend keys plus base URLs pointing at private endpoints.
- `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned
  by gateway/relay.

Additionally, agent/transports/codex_app_server.py built its spawn env from a
raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()`
helper entirely — handing every codex subprocess the full Tier-1 secret set
(GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session
token) unfiltered. This is the NousResearch#29157 sibling spawn-site gap; copilot_acp_client
already routes through the helper.

Fix — single chokepoint:
- Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the
  single source of truth for the dynamic secret patterns. Matches
  AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves
  non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible.
- Wire the predicate into every spawn path unconditionally (ignores skill
  env_passthrough opt-in AND inherit_credentials — a model-driving CLI never
  needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env`
  (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter.
- Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the
  exact-match path catches them independently of the predicate.
- Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS`
  (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including
  the codex/copilot `inherit_credentials=True` path that skips the Tier-2
  blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has
  no secret suffix, so enumerating it here is what closes its leak on the
  inherit path (self-review W1).
- Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now
  consults the same predicate, so a skill can't register these names as
  passthrough and tunnel them into an execute_code / terminal child.
- Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)`
  — strips Tier-1 + dynamic-internal secrets while provider creds (which codex
  needs to authenticate) still flow.

Consolidates PRs NousResearch#53715 (necoweb3 — the _is_hermes_internal_secret backbone +
Docker filter), NousResearch#53503 (srojk34 — env_passthrough guard), and NousResearch#55709 (srojk34 —
codex routing). Retires NousResearch#52348 (claudlos): its copilot half is already on main,
and its codex half used the full-strip `_sanitize_subprocess_env` which would
break codex provider auth — the correct tier is `inherit_credentials=True`.

Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough
override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers),
TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough
defense-in-depth cases.

Co-authored-by: necoweb3 <sswdarius@gmail.com>
Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com>
Co-authored-by: claudlos <claudlos@agentmail.to>
caozuohua pushed a commit to caozuohua/hermes-agent that referenced this pull request Jul 2, 2026
… env

Subprocesses spawned by the terminal tool, execute_code, Docker backend, and
the codex app-server could inherit Hermes-internal secrets that the name-based
`_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into
`os.environ` at runtime under dynamic names:

- `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM
  credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py
  (vision, web_extract, approval, compression, plugin-registered tasks). Often
  separate, higher-spend keys plus base URLs pointing at private endpoints.
- `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned
  by gateway/relay.

Additionally, agent/transports/codex_app_server.py built its spawn env from a
raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()`
helper entirely — handing every codex subprocess the full Tier-1 secret set
(GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session
token) unfiltered. This is the NousResearch#29157 sibling spawn-site gap; copilot_acp_client
already routes through the helper.

Fix — single chokepoint:
- Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the
  single source of truth for the dynamic secret patterns. Matches
  AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves
  non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible.
- Wire the predicate into every spawn path unconditionally (ignores skill
  env_passthrough opt-in AND inherit_credentials — a model-driving CLI never
  needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env`
  (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter.
- Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the
  exact-match path catches them independently of the predicate.
- Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS`
  (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including
  the codex/copilot `inherit_credentials=True` path that skips the Tier-2
  blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has
  no secret suffix, so enumerating it here is what closes its leak on the
  inherit path (self-review W1).
- Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now
  consults the same predicate, so a skill can't register these names as
  passthrough and tunnel them into an execute_code / terminal child.
- Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)`
  — strips Tier-1 + dynamic-internal secrets while provider creds (which codex
  needs to authenticate) still flow.

Consolidates PRs NousResearch#53715 (necoweb3 — the _is_hermes_internal_secret backbone +
Docker filter), NousResearch#53503 (srojk34 — env_passthrough guard), and NousResearch#55709 (srojk34 —
codex routing). Retires NousResearch#52348 (claudlos): its copilot half is already on main,
and its codex half used the full-strip `_sanitize_subprocess_env` which would
break codex provider auth — the correct tier is `inherit_credentials=True`.

Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough
override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers),
TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough
defense-in-depth cases.

Co-authored-by: necoweb3 <sswdarius@gmail.com>
Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com>
Co-authored-by: claudlos <claudlos@agentmail.to>
Jasper6439 pushed a commit to Jasper6439/hermes-agent that referenced this pull request Jul 5, 2026
… env

Subprocesses spawned by the terminal tool, execute_code, Docker backend, and
the codex app-server could inherit Hermes-internal secrets that the name-based
`_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into
`os.environ` at runtime under dynamic names:

- `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM
  credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py
  (vision, web_extract, approval, compression, plugin-registered tasks). Often
  separate, higher-spend keys plus base URLs pointing at private endpoints.
- `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned
  by gateway/relay.

Additionally, agent/transports/codex_app_server.py built its spawn env from a
raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()`
helper entirely — handing every codex subprocess the full Tier-1 secret set
(GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session
token) unfiltered. This is the NousResearch#29157 sibling spawn-site gap; copilot_acp_client
already routes through the helper.

Fix — single chokepoint:
- Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the
  single source of truth for the dynamic secret patterns. Matches
  AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves
  non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible.
- Wire the predicate into every spawn path unconditionally (ignores skill
  env_passthrough opt-in AND inherit_credentials — a model-driving CLI never
  needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env`
  (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter.
- Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the
  exact-match path catches them independently of the predicate.
- Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS`
  (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including
  the codex/copilot `inherit_credentials=True` path that skips the Tier-2
  blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has
  no secret suffix, so enumerating it here is what closes its leak on the
  inherit path (self-review W1).
- Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now
  consults the same predicate, so a skill can't register these names as
  passthrough and tunnel them into an execute_code / terminal child.
- Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)`
  — strips Tier-1 + dynamic-internal secrets while provider creds (which codex
  needs to authenticate) still flow.

Consolidates PRs NousResearch#53715 (necoweb3 — the _is_hermes_internal_secret backbone +
Docker filter), NousResearch#53503 (srojk34 — env_passthrough guard), and NousResearch#55709 (srojk34 —
codex routing). Retires NousResearch#52348 (claudlos): its copilot half is already on main,
and its codex half used the full-strip `_sanitize_subprocess_env` which would
break codex provider auth — the correct tier is `inherit_credentials=True`.

Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough
override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers),
TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough
defense-in-depth cases.

Co-authored-by: necoweb3 <sswdarius@gmail.com>
Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com>
Co-authored-by: claudlos <claudlos@agentmail.to>
habarmc1223-sudo pushed a commit to habarmc1223-sudo/hermes-agent-fluxmem that referenced this pull request Jul 8, 2026
… env

Subprocesses spawned by the terminal tool, execute_code, Docker backend, and
the codex app-server could inherit Hermes-internal secrets that the name-based
`_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into
`os.environ` at runtime under dynamic names:

- `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM
  credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py
  (vision, web_extract, approval, compression, plugin-registered tasks). Often
  separate, higher-spend keys plus base URLs pointing at private endpoints.
- `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned
  by gateway/relay.

Additionally, agent/transports/codex_app_server.py built its spawn env from a
raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()`
helper entirely — handing every codex subprocess the full Tier-1 secret set
(GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session
token) unfiltered. This is the NousResearch#29157 sibling spawn-site gap; copilot_acp_client
already routes through the helper.

Fix — single chokepoint:
- Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the
  single source of truth for the dynamic secret patterns. Matches
  AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves
  non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible.
- Wire the predicate into every spawn path unconditionally (ignores skill
  env_passthrough opt-in AND inherit_credentials — a model-driving CLI never
  needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env`
  (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter.
- Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the
  exact-match path catches them independently of the predicate.
- Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS`
  (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including
  the codex/copilot `inherit_credentials=True` path that skips the Tier-2
  blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has
  no secret suffix, so enumerating it here is what closes its leak on the
  inherit path (self-review W1).
- Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now
  consults the same predicate, so a skill can't register these names as
  passthrough and tunnel them into an execute_code / terminal child.
- Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)`
  — strips Tier-1 + dynamic-internal secrets while provider creds (which codex
  needs to authenticate) still flow.

Consolidates PRs NousResearch#53715 (necoweb3 — the _is_hermes_internal_secret backbone +
Docker filter), NousResearch#53503 (srojk34 — env_passthrough guard), and NousResearch#55709 (srojk34 —
codex routing). Retires NousResearch#52348 (claudlos): its copilot half is already on main,
and its codex half used the full-strip `_sanitize_subprocess_env` which would
break codex provider auth — the correct tier is `inherit_credentials=True`.

Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough
override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers),
TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough
defense-in-depth cases.

Co-authored-by: necoweb3 <sswdarius@gmail.com>
Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com>
Co-authored-by: claudlos <claudlos@agentmail.to>
santhreal pushed a commit to santhreal/hermes-agent that referenced this pull request Jul 13, 2026
… env

Subprocesses spawned by the terminal tool, execute_code, Docker backend, and
the codex app-server could inherit Hermes-internal secrets that the name-based
`_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into
`os.environ` at runtime under dynamic names:

- `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM
  credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py
  (vision, web_extract, approval, compression, plugin-registered tasks). Often
  separate, higher-spend keys plus base URLs pointing at private endpoints.
- `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned
  by gateway/relay.

Additionally, agent/transports/codex_app_server.py built its spawn env from a
raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()`
helper entirely — handing every codex subprocess the full Tier-1 secret set
(GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session
token) unfiltered. This is the NousResearch#29157 sibling spawn-site gap; copilot_acp_client
already routes through the helper.

Fix — single chokepoint:
- Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the
  single source of truth for the dynamic secret patterns. Matches
  AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves
  non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible.
- Wire the predicate into every spawn path unconditionally (ignores skill
  env_passthrough opt-in AND inherit_credentials — a model-driving CLI never
  needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env`
  (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter.
- Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the
  exact-match path catches them independently of the predicate.
- Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS`
  (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including
  the codex/copilot `inherit_credentials=True` path that skips the Tier-2
  blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has
  no secret suffix, so enumerating it here is what closes its leak on the
  inherit path (self-review W1).
- Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now
  consults the same predicate, so a skill can't register these names as
  passthrough and tunnel them into an execute_code / terminal child.
- Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)`
  — strips Tier-1 + dynamic-internal secrets while provider creds (which codex
  needs to authenticate) still flow.

Consolidates PRs NousResearch#53715 (necoweb3 — the _is_hermes_internal_secret backbone +
Docker filter), NousResearch#53503 (srojk34 — env_passthrough guard), and NousResearch#55709 (srojk34 —
codex routing). Retires NousResearch#52348 (claudlos): its copilot half is already on main,
and its codex half used the full-strip `_sanitize_subprocess_env` which would
break codex provider auth — the correct tier is `inherit_credentials=True`.

Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough
override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers),
TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough
defense-in-depth cases.

Co-authored-by: necoweb3 <sswdarius@gmail.com>
Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com>
Co-authored-by: claudlos <claudlos@agentmail.to>
Gravezzz pushed a commit to Gravezzz/hermes-agent that referenced this pull request Jul 21, 2026
… env

Subprocesses spawned by the terminal tool, execute_code, Docker backend, and
the codex app-server could inherit Hermes-internal secrets that the name-based
`_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into
`os.environ` at runtime under dynamic names:

- `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM
  credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py
  (vision, web_extract, approval, compression, plugin-registered tasks). Often
  separate, higher-spend keys plus base URLs pointing at private endpoints.
- `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned
  by gateway/relay.

Additionally, agent/transports/codex_app_server.py built its spawn env from a
raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()`
helper entirely — handing every codex subprocess the full Tier-1 secret set
(GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session
token) unfiltered. This is the NousResearch#29157 sibling spawn-site gap; copilot_acp_client
already routes through the helper.

Fix — single chokepoint:
- Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the
  single source of truth for the dynamic secret patterns. Matches
  AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves
  non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible.
- Wire the predicate into every spawn path unconditionally (ignores skill
  env_passthrough opt-in AND inherit_credentials — a model-driving CLI never
  needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env`
  (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter.
- Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the
  exact-match path catches them independently of the predicate.
- Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS`
  (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including
  the codex/copilot `inherit_credentials=True` path that skips the Tier-2
  blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has
  no secret suffix, so enumerating it here is what closes its leak on the
  inherit path (self-review W1).
- Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now
  consults the same predicate, so a skill can't register these names as
  passthrough and tunnel them into an execute_code / terminal child.
- Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)`
  — strips Tier-1 + dynamic-internal secrets while provider creds (which codex
  needs to authenticate) still flow.

Consolidates PRs NousResearch#53715 (necoweb3 — the _is_hermes_internal_secret backbone +
Docker filter), NousResearch#53503 (srojk34 — env_passthrough guard), and NousResearch#55709 (srojk34 —
codex routing). Retires NousResearch#52348 (claudlos): its copilot half is already on main,
and its codex half used the full-strip `_sanitize_subprocess_env` which would
break codex provider auth — the correct tier is `inherit_credentials=True`.

Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough
override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers),
TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough
defense-in-depth cases.

Co-authored-by: necoweb3 <sswdarius@gmail.com>
Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com>
Co-authored-by: claudlos <claudlos@agentmail.to>
leewenjie pushed a commit to leewenjie/hermes-agent that referenced this pull request Aug 7, 2026
… env

Subprocesses spawned by the terminal tool, execute_code, Docker backend, and
the codex app-server could inherit Hermes-internal secrets that the name-based
`_HERMES_PROVIDER_ENV_BLOCKLIST` can't enumerate, because they're injected into
`os.environ` at runtime under dynamic names:

- `AUXILIARY_<TASK>_API_KEY` / `AUXILIARY_<TASK>_BASE_URL` — per-task side-LLM
  credentials bridged from `config.yaml[auxiliary]` by gateway/run.py and cli.py
  (vision, web_extract, approval, compression, plugin-registered tasks). Often
  separate, higher-spend keys plus base URLs pointing at private endpoints.
- `GATEWAY_RELAY_*_SECRET` / `_KEY` / `_TOKEN` — relay-auth material provisioned
  by gateway/relay.

Additionally, agent/transports/codex_app_server.py built its spawn env from a
raw `os.environ.copy()`, bypassing the centralized `hermes_subprocess_env()`
helper entirely — handing every codex subprocess the full Tier-1 secret set
(GH_TOKEN, gateway bot tokens, Modal/Daytona infra tokens, dashboard session
token) unfiltered. This is the NousResearch#29157 sibling spawn-site gap; copilot_acp_client
already routes through the helper.

Fix — single chokepoint:
- Add `_is_hermes_internal_secret(key)` in tools/environments/local.py as the
  single source of truth for the dynamic secret patterns. Matches
  AUXILIARY_*_API_KEY / _BASE_URL and GATEWAY_RELAY_*_SECRET/_KEY/_TOKEN; leaves
  non-secret AUXILIARY_*_PROVIDER/_MODEL and GATEWAY_RELAY routing hints visible.
- Wire the predicate into every spawn path unconditionally (ignores skill
  env_passthrough opt-in AND inherit_credentials — a model-driving CLI never
  needs these): `_sanitize_subprocess_env` (both loops), `_make_run_env`
  (foreground), `hermes_subprocess_env` (Tier-1), and the Docker forward filter.
- Add the static GATEWAY_RELAY_* names to `_HERMES_PROVIDER_ENV_BLOCKLIST` so the
  exact-match path catches them independently of the predicate.
- Add the GATEWAY_RELAY_ID/_SECRET/_DELIVERY_KEY triplet to `_ALWAYS_STRIP_KEYS`
  (Tier-1) so it is stripped unconditionally on EVERY spawn surface — including
  the codex/copilot `inherit_credentials=True` path that skips the Tier-2
  blocklist. `_SECRET`/`_DELIVERY_KEY` are already predicate-matched; `_ID` has
  no secret suffix, so enumerating it here is what closes its leak on the
  inherit path (self-review W1).
- Defense in depth: env_passthrough.py `_is_hermes_provider_credential()` now
  consults the same predicate, so a skill can't register these names as
  passthrough and tunnel them into an execute_code / terminal child.
- Route codex_app_server through `hermes_subprocess_env(inherit_credentials=True)`
  — strips Tier-1 + dynamic-internal secrets while provider creds (which codex
  needs to authenticate) still flow.

Consolidates PRs NousResearch#53715 (necoweb3 — the _is_hermes_internal_secret backbone +
Docker filter), NousResearch#53503 (srojk34 — env_passthrough guard), and NousResearch#55709 (srojk34 —
codex routing). Retires NousResearch#52348 (claudlos): its copilot half is already on main,
and its codex half used the full-strip `_sanitize_subprocess_env` which would
break codex provider auth — the correct tier is `inherit_credentials=True`.

Tests: TestHermesInternalDynamicSecrets (terminal + predicate + passthrough
override), TestInternalDynamicSecrets (hermes_subprocess_env both tiers),
TestSpawnEnvSecretStripping (codex spawn env), plus env_passthrough
defense-in-depth cases.

Co-authored-by: necoweb3 <sswdarius@gmail.com>
Co-authored-by: srojk34 <286497132+srojk34@users.noreply.github.com>
Co-authored-by: claudlos <claudlos@agentmail.to>
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 backend/docker Docker container execution comp/tools Tool registry, model_tools, toolsets P1 High — major feature broken, no workaround sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit 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 tool/terminal Terminal execution and process management type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants