#12296: security: persistence-only secret redaction for session transcripts
docs
gateway
agents
size: L
## Summary
🤖 **AI-assisted** — Built by an OpenClaw agent (Claude Opus 4.6), fully tested, fully reviewed. The author understands the code and architecture decisions.
Redact sensitive secrets (API keys, tokens, passwords) from session transcript JSONL files **at the persistence layer only**, keeping in-memory entries unredacted so the LLM can reason about full tool output (e.g., `curl` with Bearer tokens, `cat` of config files).
See [design document on #12182](https://github.com/openclaw/openclaw/issues/12182#issuecomment-3869025731) for full architecture rationale, tradeoffs, and coverage analysis.
## Root Cause
In `session-tool-result-guard.ts`, tool results are persisted via `_appendEntry()` → `_persist()` which calls `JSON.stringify(entry)` + `appendFileSync` — no redaction applied. The existing `redactSensitiveText()` was only used on the read path.
Additionally, `appendAssistantMessageToSessionTranscript()` in `transcript.ts` creates a raw `SessionManager.open()` without the guard, bypassing any redaction.
## Fix
### Architecture: Persistence-only via `_persist` monkey-patch
- **`SessionManager._persist()`** — Replaced entirely to apply `redactEntryForPersistence()` during `JSON.stringify` serialization (both bulk-flush and single-entry paths)
- **`SessionManager._rewriteFile()`** — Also replaced for the migration/recovery write path
- **`transcript.ts`** — Wrapped with `guardSessionManager()` to close the bypass
- **In-memory `fileEntries[]`** — Untouched. LLM sees full unredacted content via `buildSessionContext()`
### Entry types covered (all text-carrying types)
| Entry Type | Field(s) Redacted |
|---|---|
| `message` (toolResult, assistant, user) | `message.content[].text` |
| `compaction` / `branch_summary` | `summary` string |
| `custom_message` | `content` (string or TextContent[]) |
### Why monkey-patching
`SessionManager` lives in the upstream `@mariozechner/pi-coding-agent` package — we cannot modify `_persist` directly. The existing guard already monkey-patches `appendMessage`; our `_persist` patch follows the same established pattern. We pin to upstream v0.52.8 with SHA-256 hash tests that fail on any structural change.
### Other improvements
- Cache compiled regex patterns (content-based `JSON.stringify` comparison)
- Resolve redact options once at guard install time (no per-call `loadConfig()`)
- Extract shared `redactTextBlocks()` helper (zero code duplication)
## Test plan
**38 tests total**, all passing (Node + Bun):
- **Dual-path verification**: every redaction test checks both in-memory (unredacted) and on-disk (redacted) using disk-persisted `SessionManager`
- **All message roles**: toolResult, assistant, user
- **All entry types**: compaction, branch_summary, custom_message (string + array)
- **Non-message passthrough**: session header unchanged
- **Transcript mirror**: delivery-mirror redaction verified
- **Upstream compatibility**: SHA-256 hash of `_persist.toString()` and `_rewriteFile.toString()`
- **Clean passthrough**: entries without secrets return same reference
## Related
- Refs: #12182
- Related: #12260 (similar goal, different architecture — redacts before `appendMessage` which also affects LLM context)
## Files changed (5)
- `src/agents/session-tool-result-guard.ts` — `redactEntryForPersistence()`, `_persist`/`_rewriteFile` patches, `redactTextBlocks()` helper
- `src/agents/session-tool-result-guard.test.ts` — 26 guard tests with dual-path verification
- `src/config/sessions/transcript.ts` — `guardSessionManager()` wrapper
- `src/config/sessions/transcript.test.ts` — delivery-mirror redaction test
- `src/logging/redact.ts` — pattern caching
<!-- greptile_comment -->
<h2>Greptile Overview</h2>
<h3>Greptile Summary</h3>
This PR changes session transcript persistence so secrets are redacted only when writing JSONL to disk, keeping in-memory session entries unredacted for LLM context. It does this by extending the existing `guardSessionManager()` approach to wrap `SessionManager`’s persistence methods (`_persist` and `_rewriteFile`) to serialize redacted copies of entries, and by ensuring `transcript.ts` uses a guarded session manager so it can’t bypass redaction. It also updates the redaction utility to cache compiled regex/pattern work and adds tests that verify the “dual path” behavior (unredacted in-memory, redacted on disk) across message roles and other transcript entry types.
<h3>Confidence Score: 5/5</h3>
- This PR is safe to merge with minimal risk.
- Reviewed the changed persistence wrapper and transcript guard integration along with the updated redaction caching and tests; changes are focused, preserve upstream persistence semantics, and include strong dual-path test coverage to prevent regressions.
- No files require special attention
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#12260: fix: redact secrets in tool results before persisting to session tr...
by Yida-Dev · 2026-02-09
89.3%
#22231: fix(security): redact sensitive data in session transcripts
by novalis133 · 2026-02-20
83.5%
#15050: fix: transcript corruption resilience — strip aborted tool_use bloc...
by yashchitneni · 2026-02-12
81.4%
#19024: fix: Fix normalise toolid
by chetaniitbhilai · 2026-02-17
80.5%
#16779: feat: add `openclaw sessions scrub` command and doctor check for se...
by akoscz · 2026-02-15
80.4%
#9011: fix(session): auto-recovery for corrupted tool responses [AI-assisted]
by cheenu1092-oss · 2026-02-04
80.2%
#10915: fix: prevent session bloat from oversized tool results and improve ...
by DukeDeSouth · 2026-02-07
80.1%
#16928: fix(security): OC-07 redact session history credentials and enforce...
by aether-ai-agent · 2026-02-15
78.8%
#3647: fix: sanitize tool arguments in session history
by nhangen · 2026-01-29
78.7%
#19094: Fix empty tool_call_id and function names in provider transcript pa...
by yxshee · 2026-02-17
78.7%