Keeping Secrets Out of Your Antigravity Agent's Output: Layered Defenses for Logs, Diffs, and PR Bodies
The three paths through which background agents leak secrets, and how to defend commit diffs, execution logs, and PR bodies with layered protection, drawn from running six apps and measured false-positive rates.
One night, while a background agent was handling update work across six of my apps, I noticed that the generated pull request description contained what looked like a Firebase server key. Trying to write a thorough crash-reproduction guide, the agent had pasted the raw contents of an environment variable for debugging. It was a private repository and nothing was published, but the thought of what would have happened with auto-merge enabled sent a chill down my spine.
I'm Masaki Hirokawa. I've been building apps on my own since 2014, mostly wallpaper, relaxation, and manifestation apps, and they have accumulated somewhere around 50 million downloads. Lately I've been handing more of my iOS and Android update work to Antigravity's agents, and I keep noticing that agents will casually cross secret-handling lines that a human would instinctively avoid, depending on the context. Here I want to break down the paths by which secrets escape through an agent's output into three, and share how I close each one, along with numbers I measured in actual operation.
The Three Exits Where Agents Leak Secrets
When people think about secrets management, attention tends to go to where secrets are stored. But in the age of agents, the truly dangerous part is not the storage location, it's the exit. Even if you've removed .env from Git and moved it into a Secret Manager, none of that matters if the agent reads those values at runtime and writes them out somewhere else.
The leak exits I observed sort into three:
Leaking into commit diffs. The agent includes a debug scratch file or a config snapshot directly in a commit.
Leaking into execution logs and audit trails. The agent dumps environment variables, or pipes an entire API response into a log. The more carefully you design your system to retain audit logs, the higher the risk that a plaintext key gets persisted there.
Leaking into PR bodies and chat responses. This is the incident I opened with. The agent quotes a value "for the sake of explanation," and it's the hardest path to notice.
These three differ in nature, so no single countermeasure closes all of them. You stop the diff before the commit, redact the log while letting it flow, and run a final check on PRs and chat right before they go public. You need to think in layers.
Exit 1: Commit Diffs, Stopped at pre-commit
The first line of defense is the commit. Before the agent runs git commit, scan the staged diff and reject anything that looks like a key. I use a hook based on gitleaks, with patterns specific to my app business added on top.
Let's start with the approach that doesn't work. A hook like the following, which only looks at the commit message, lets the actual diff contents sail right through.
# Anti-pattern: inspects only the message, never the diff body#!/usr/bin/env bashmsg=$(cat "$1")if echo "$msg" | grep -qi "secret"; then echo "Commit message contains the word secret" exit 1fiexit 0
What you actually want to protect is the staged diff. The following passes the contents of git diff --cached to gitleaks.
#!/usr/bin/env bash# .git/hooks/pre-commit — inspect only the staged diffset -euo pipefail# Run gitleaks in diff mode if availableif command -v gitleaks >/dev/null 2>&1; then if ! gitleaks protect --staged --redact --config .gitleaks.toml; then echo "Secret-like string detected in the staged diff" echo "Run git restore --staged <file>, then re-commit" exit 1 fifi# Secondary check via local patterns (fallback when gitleaks is absent)diff=$(git diff --cached -U0 | grep '^+' || true)patterns=( 'AKIA[0-9A-Z]{16}' # AWS access key id 'AIza[0-9A-Za-z_-]{35}' # Google API key 'gh[pousr]_[0-9A-Za-z]{36}' # GitHub token 'sk_live_[0-9A-Za-z]{24,}' # Stripe live secret)for p in "${patterns[@]}"; do if echo "$diff" | grep -Eq "$p"; then echo "Key pattern /$p/ matched in the staged diff" exit 1 fidoneexit 0
The crucial part is --staged (in gitleaks, protect --staged). Scanning the entire repository history on every commit takes tens of seconds even on a small indie-developer machine, but scanning only the staged diff finishes in under a second on any of my six app repos. When you embed this in an agent's work loop, that speed is the practical dividing line.
In .gitleaks.toml, I declare the kinds of keys my app business actually uses, both as rules and as allowlist entries. Unless you exclude values that "look like keys but are safe to publish," such as an AdMob app ID, you'll be plagued by the false positives I describe later.
✦
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 layered design that defends three distinct leak paths separately: commit diffs, execution logs, and PR bodies
✦Working implementation code for a pre-commit hook and CI guard (gitleaks integration, regex, entropy handling)
✦Measured false-positive rates from running six apps, and the allowlist tuning that cut them from 12% to 0.4%
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.
Exit 2: Logs and Audit Trails, Redacted While Streaming
Even if you protect commits, an agent's execution log is a separate exit. I keep audit logs in R2 so I can reconstruct a background agent's decisions later, but ironically there was a dilemma: the more carefully I retained logs, the more carefully the keys were retained too.
What you need here is not to block the log, but a filter that masks only the keys while letting everything flow. Pipe stdout and stderr through it, and mask both known patterns and the live values of environment variables.
#!/usr/bin/env python3# redact_stream.py — receive agent output line by line, mask keys, pass throughimport osimport reimport sys# Known key formats (prefix + body)PATTERNS = [ re.compile(r"AKIA[0-9A-Z]{16}"), re.compile(r"AIza[0-9A-Za-z_-]{35}"), re.compile(r"gh[pousr]_[0-9A-Za-z]{36}"), re.compile(r"sk_live_[0-9A-Za-z]{24,}"),]# Mask the literal values of environment variables (most reliable)SECRET_ENV_KEYS = ("FIREBASE_SERVER_KEY", "ADMOB_API_SECRET", "STRIPE_SECRET_KEY")SECRET_VALUES = [v for k in SECRET_ENV_KEYS if (v := os.environ.get(k)) and len(v) >= 8]def redact(line: str) -> str: for value in SECRET_VALUES: line = line.replace(value, "«REDACTED:env»") for pat in PATTERNS: line = pat.sub("«REDACTED:pattern»", line) return linefor raw in sys.stdin: sys.stdout.write(redact(raw)) sys.stdout.flush()
The point is to make this two-tiered. Pattern matching alone misses keys in an in-house format, or keys that happen not to match the expected length. So I give top priority to replacing "the literal value stored in the environment variable." This rarely misses and reliably redacts regardless of format. When launching the agent, wiring it up as agent run ... 2>&1 | python3 redact_stream.py | tee -a audit.log protects both the screen and the audit log at once.
One thing to watch: forgetting flush() delays the log and breaks real-time monitoring. When you're watching a long-running agent from another terminal and the output arrives dozens of lines late, you notice a runaway too late. I had one close call with exactly this, so I always flush line by line.
Exit 3: PR Bodies and Chat Responses, with a Final Gate in CI
This is the exit where my opening incident happened. Even if you protect diffs and logs, when an agent copies a key into the body text "for explanation," that body passes neither the diff check nor the log filter. PR bodies and comments are often generated outside Git's management.
Put a final gate on the CI side here. With GitHub Actions, inspect both the body and the diff when a PR is opened or updated, and block the merge on detection.
name: secret-guardon: pull_request: types: [opened, edited, synchronize]jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Inspect code diff uses: gitleaks/gitleaks-action@v2 env: GITLEAKS_CONFIG: .gitleaks.toml - name: Inspect PR body env: BODY: ${{ github.event.pull_request.body }} run: | printf '%s' "$BODY" > pr_body.txt if grep -Eq 'AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}|gh[pousr]_[0-9A-Za-z]{36}|sk_live_[0-9A-Za-z]{24,}' pr_body.txt; then echo "::error::PR body contains a key-like string" exit 1 fi
The advantage of making CI the final gate is that even if something slips past the local hook (including the unexpected case of an agent committing with --no-verify), it always stops here. I have the local hook and CI share a single pattern-definition file, treating .gitleaks.toml as the single source of truth so I don't have to maintain two copies.
Reducing False Positives: The Balance of Allowlist and Entropy
Right after introducing layered defense, my repositories were flooded with false positives. In the first week, of 53 alerts across all six apps, zero were real leaks; every one was a false positive. Breaking it down, AdMob app IDs, dummy test keys, long base64 icon data, and commit hashes were all getting flagged as "high-entropy strings."
In terms of false-positive rate, about 12% of all detections were initially "things a human reviewed and judged fine," and my work stalled at every step. Here is how I brought it down:
Declare safe-to-publish values in the allowlist. AdMob app IDs and test keys go into the [allowlist] section of .gitleaks.toml as regular expressions.
Exclude by file. Files that are structurally bound to be high-entropy, like base64 icons or snapshot JSON, are excluded by path.
Don't raise the entropy threshold too far. Lowering sensitivity misses real keys, so I permit individually via the allowlist rather than via the threshold.
After running this loop, the false-positive rate fell to 0.4% in about two weeks. What mattered even more than the numbers was the habit of leaving a comment explaining why each allowlist entry was added. If the reason a value was permitted doesn't reach my future self (or the agent), the allowlist swells unchecked and the defense becomes hollow. I always attach a line like # AdMob app id is public, safe to commit to each allowlist entry.
When a Leak Does Happen: Recovery Is the Real Work
No matter how many layers you stack, leaks never reach zero. If anything, "we detected it" almost always means "it was already written." More than improving detection accuracy, I believe the real work is how fast you can neutralize a leak after it happens.
The procedure I've committed to is fixing the priority order.
First, rotate the key. Invalidating the leaked key and issuing a new value is the top priority. Before deleting the commit, make the key itself unusable.
Next, check the blast radius. Enumerate what services the key had permission for, and check for signs of misuse (anomalous activity in the AdMob or Stripe dashboards).
Last, scrub the history. Removing it from history with git filter-repo and the like comes at this stage. Treat any key already pushed to a remote as "already public," and don't let scrubbing stand in for neutralization.
Get the order wrong and try to "delete the history first," and the key keeps getting used in the meantime. Even when delegating auto-remediation to an agent, this priority order is the part I feel a human should keep their hands on.
How to Combine the Three Layers (Notes From Operating It)
The three layers differ in where they stop and how fast. pre-commit is fastest and doesn't hurt the developer experience, but it can be bypassed with --no-verify. The log filter redacts without stopping the flow, but can't prevent the commit itself. CI is reliable, but stops at the PR stage, so the rework is somewhat larger.
My conclusion was to put all three layers in thinly. Lean on any single one, and the moment something gets past it you're defenseless. What matters is that the three layers share the same pattern definitions (.gitleaks.toml) and the same environment-variable list, so maintenance cost doesn't grow. As someone running several Dolice Labs sites and six apps solo, I prioritize "defenses that don't collapse even when thin" over "thick defenses." The more I delegate work to agents, the more what a human should hold onto becomes not the individual checks, but the design of the layers themselves.
I hope this helps as a first step for anyone else running multiple projects alongside agents.
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.