fix(delegation): redact credentials in live subagent transcripts - #67635
Merged
teknium1 merged 1 commit intoJul 20, 2026
Merged
Conversation
teknium1
reviewed
Jul 19, 2026
teknium1
left a comment
Contributor
There was a problem hiding this comment.
Thanks for addressing a real credential boundary. Remote current main still creates and tees child progress into LiveTranscriptWriter (tools/delegate_tool.py:2563-2622), while its header and event() sinks write raw text (tools/delegation_live_log.py:106,125). The proposed force=True choke point is appropriate for that path.
Problems
tools/delegation_live_log.py:329still writes each task goal verbatim intomanifest.json. That file sits beside the transcript logs undercache/delegation/live/, andcache/delegationis mounted into remote backends (tools/credential_files.py:353). A credential in a task goal remains exposed despite the header redaction.
Suggested changes
- Redact or omit the manifest goal using the same force-mode policy, and add a test covering both the log header and manifest.
Automated hermes-sweeper review.
| f"goal: {_one_line(goal, _KICKOFF_MAX)}", | ||
| # Header bypasses event(), so redact here too — a goal string | ||
| # can carry a key the caller pasted into the task. | ||
| f"goal: {_redact(_one_line(goal, _KICKOFF_MAX))}", |
Contributor
There was a problem hiding this comment.
This protects the log header, but _write_manifest() still serializes the same task goal raw at line 329. Because manifest.json is in the same mounted cache/delegation/live/<id>/ directory, redact or omit that manifest field too and add coverage.
The live transcripts added with delegate_task write each child's events to
<hermes_home>/cache/delegation/live/<delegation_id>/. Nothing on that path is
redacted, and the rendered events are precisely the secret-bearing surfaces:
tool args, tool results and streamed assistant text.
That location is not incidental — delegate_tool.py:1620 documents cache/
delegation as "mounted read-only into remote backends", so every line lands
in a file readable from inside the sandbox.
Observed on the writer today:
tool | -> terminal(curl -H "Authorization: Bearer sk-ant-api03-...")
result | terminal ok 0.4s: OPENAI_API_KEY=sk-proj-... AWS_SECRET_ACCESS_KEY=wJalr...
The same three values through the canonical redactor:
curl -H "Authorization: Bearer ***" https://api.internal
OPENAI_API_KEY=*** AWS_SECRET_ACCESS_KEY=***
Every other sink for this data already routes through that redactor — search
results via redact_sensitive_text(file_read=True), terminal output via
redact_terminal_output — so the transcript was the one place an operator's
keys reached disk in the clear.
Redact at three points, covering every artefact the dispatch writes into that
directory:
* event() — every typed helper (assistant_text, thinking, tool_start,
tool_result, marker, finalize, the stream flush) funnels through it, so
one call covers them all and a helper added later cannot bypass it.
* the .log header — written directly rather than through event(), and a
caller can paste a key into the task text.
* manifest.json — _write_manifest serialises the same goal, and the manifest
sits in the same mounted directory, so redacting only the header would
have left the credential exposed one file over.
force=True because this is a safety boundary and must redact even with the
global toggle off. On the (impossible-in-practice) import failure the line is
withheld instead of written raw: losing a debug line costs less than writing
a live credential into a sandbox-readable file.
Key names, tool names, statuses, durations and ordinary prose are untouched,
so tail -f stays as useful as before — pinned by its own test, alongside a
whole-directory sweep asserting no file under live/<id>/ carries the raw key.
Frowtek
force-pushed
the
fix/delegation-transcript-redaction
branch
from
July 20, 2026 12:29
07a4038 to
21a3c7b
Compare
Contributor
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?
The live transcripts added with
delegate_task(#67479) write each child'sevents to
<hermes_home>/cache/delegation/live/<id>/task-<n>.log. Nothing onthat path is redacted, and the rendered events are precisely the secret-bearing
surfaces — tool args, tool results, streamed assistant text.
The location is not incidental.
tools/delegate_tool.py:1620documentscache/delegationas "mounted read-only into remote backends", so everyline lands in a file readable from inside the sandbox.
Observed on the writer as it stands:
The same three values through the canonical redactor:
Every other sink for this data already routes through that redactor — search
results via
redact_sensitive_text(file_read=True), terminal output viaredact_terminal_output. The transcript was the one place an operator's keysreached disk in the clear.
Related Issue
Fixes #
Type of Change
Changes Made
tools/delegation_live_log.py— redact atevent(). Every typed helper(
assistant_text,thinking,tool_start,tool_result,marker,finalize, the stream flush) funnels through it, so one call covers them alland a helper added later can't bypass it. The header is written directly
rather than through
event(), so the goal string is redacted there too — acaller can paste a key into the task text.
force=Truebecause this is a safety boundary and must redact even with theglobal toggle off. On an import failure the line is withheld rather than
written raw: losing a debug line costs less than writing a live credential
into a sandbox-readable file.
tests/tools/test_delegation_live_log.py— coverage for args, results,streamed text, the goal header, thinking, an all-helpers sweep proving the
choke point holds, plus a benign-content test showing redaction doesn't gut
the transcript.
Testing
Six of the seven new tests fail on
main; the seventh (benign contentuntouched) passes both ways, which is what shows the fix doesn't trade a leak
for a mangled log.
Key names, tool names, statuses, durations and ordinary prose survive —
tail -fstays as useful as before:
Delegation/redaction/process-registry selection baseline-compared against a
clean
origin/mainworktree: identical 4 pre-existing failures (alltest_process_registry.py, Windows process handling, unrelated),402 → 409passed.
Checklist
mainand covered by a failing-before/passing-after testorigin/main)