Strip Secrets Out of the Agent Logs You Keep: Designing a Redaction Layer
Once you start keeping logs from unattended agents, a token or API key eventually lands in them in plaintext. Rotating the key doesn't unmake the leaked log. This designs a redaction layer that reliably drops secrets right before the write, going beyond regex to register known secrets and mask them for certain, with working Python and field notes.
I was looking back over the logs of an agent running unattended. On one line, an external API token sat there in the clear. During an error retry, the whole request had been written out for debugging — that single setting alone had carved a plaintext secret onto disk.
You can delete a log. But logs already forwarded elsewhere, or pulled into a backup, have spread beyond your reach. Rotating the token stops the damage, yet the fact that the log itself is contaminated does not go away. So the fix has to happen before the write, not after the leak.
What this designs is a redaction layer that every log write passes through. Antigravity 2.0 agents are useful precisely because recording tool calls and model I/O verbatim helps — but that very candor carries the secrets along too. Keep the usefulness of the record; build the one point that drops only the secrets.
Write logs assuming someone will read them someday
The first thing to set down is an operational premise: logs are not read only by the person who wrote them.
The forwarding monitor, shared storage, the error aggregator, and your future self. Logs travel further than you think. The assumption that "only I will see this" is especially brittle in unattended operation. So make "never put secrets in the log in the first place" a premise of the design.
Rather than scrubbing after the fact, narrow the entry path to one and drop the secrets reliably there. In spirit it's the same shape as gathering permission into one place with an allowlist.
Where the leaks come from
To decide what to drop, it helps to know first where secrets mix in.
Source
Secrets that mix in
Typical recording path
Requests to external APIs
Bearer tokens, API keys
Full request dump on retry
Dumping env vars
Secrets, connection strings
Startup config dump, exception locals
Model I/O
Pasted credentials
Prompt and tool-arg logging
Stack traces
Tokens embedded in URLs
Verbatim exception messages
Sources down the side, what gets recorded and how across the top. Even when you never meant to write them, the "dump everything" behavior of exception handling and retries carries secrets along — that's the awkward part. Not an attack; a kind design becoming the hole.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A map of where secrets sneak into agent run logs, and where to place a redaction layer that every write passes through exactly once, with the design reasoning
✦For what regex misses, a method that registers the real values of env vars and secrets at startup and masks them for certain, given as drop-in Python
✦For the dilemma that over-redaction blocks incident investigation, guidance on choosing between partial masking and stable hashing, drawn from years of unattended indie operation
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The handiest form for the redaction layer is a logging formatter. No matter where in the app you call logging, build the single point where it must pass this filter before reaching disk.
import logging, re# Drop well-shaped secrets by patternPATTERNS = [ (re.compile(r"Bearer\s+[A-Za-z0-9._\-]+"), "Bearer [REDACTED]"), (re.compile(r"(?i)(api[_-]?key\"?\s*[:=]\s*\"?)[A-Za-z0-9._\-]{12,}"), r"\1[REDACTED]"), (re.compile(r"://[^:/@\s]+:[^@\s]+@"), "://[REDACTED]@"), # creds in a URL]class RedactingFilter(logging.Filter): def filter(self, record: logging.LogRecord) -> bool: msg = record.getMessage() for pat, repl in PATTERNS: msg = pat.sub(repl, msg) record.msg = msg record.args = () # stop a later re-expansion from restoring raw values return Truelogger = logging.getLogger("agent")handler = logging.FileHandler("agent.jsonl")handler.addFilter(RedactingFilter()) # always passes through, right before the writelogger.addHandler(handler)
That one line, record.args = (), quietly matters. Rewrite only the pre-format message and, if the args survive, a later stage restores the raw value. After masking, empty the args and seal the way back.
Regex alone leaks: register known secrets and drop them
Regex is strong on well-shaped secrets but misses shapeless ones — a random token you minted yourself, a particular connection string. So run a second method alongside it: at startup, register the real values of the secrets you know, and drop them unconditionally whenever they appear in a log.
class SecretRegistry: def __init__(self): self._secrets: list[str] = [] def register(self, value: str): if value and len(value) >= 8: # too-short values misfire; skip them self._secrets.append(value) def scrub(self, text: str) -> str: for s in self._secrets: if s in text: text = text.replace(s, "[REDACTED:known]") return text# Register the real values from env vars / secret store at startupregistry = SecretRegistry()import osfor key in ("ADMOB_TOKEN", "STORE_API_KEY", "DB_PASSWORD"): registry.register(os.environ.get(key, ""))
Skipping short values avoids misfires. Drawing the line around eight characters prevents common strings like true or a date from being masked as collateral. Shape by pattern, real value by registry — those two stages cut the misses sharply.
Over-redaction blocks investigation: partial masks vs. hashes
Here a dilemma particular to production appears. Crush a secret entirely to [REDACTED] and you can no longer tell, during an incident, even which token failed.
What I settled on is choosing the masking style by purpose:
Things safe to crush entirely (passwords, card numbers) get wiped whole as [REDACTED].
Things you only need to identify (which token) get a partial mask keeping the last four characters.
Things you need to correlate (did the same value appear on multiple lines) get replaced by the first eight digits of a stable hash.
With a hash, the raw value stays unrecoverable while you can still tell that "the same secret appears on another line." What an investigation needs is usually not the value itself but exactly this sameness.
Field notes: never stop redaction; run detection alongside
Two impressions from running it.
A moment comes when you want to drop the redaction layer for performance. Running regex over a flood of logs is indeed not free. Even so, I strongly recommend not stopping here. Once a secret lands on disk, there is no taking it back. Treating the cost as an insurance premium is, in unattended operation, ultimately the cheapest.
Second, make redaction the last wall, not the only wall. That a mask fired is itself evidence a secret flowed into the log path at all. Count how often [REDACTED] appears, separately, and read a sudden rise as "somewhere we started passing a secret in the clear." Running the mechanism that drops alongside the mechanism that notices is what I find reassuring.
Running Dolice Labs' four sites unattended as an indie developer, plenty of agents touch secrets — from pulling AdMob reports to processing App Store assets. Deciding that all of their logs pass the same single point before the write is, on its own, what lets me watch the logs pile up through the night with a steady mind.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.