I was reviewing the logs from an overnight run the next morning when one line of standard output caught my eye. A deploy token was sitting there in the log file, in plain text. The agent had done nothing wrong. I had handed the secret to it the wrong way.
Secret incidents rarely start with something being stolen. They start with something being written down by accident. Committed to the repo. Printed to a log. Slipped into the agent's context and sent off to an external model along with everything else. As an indie developer running my own apps, I had treated the mechanics of injection lightly right up until that morning. Below is the setup I now run in production to close those three leak paths when passing keys to an Antigravity agent.
Where leaks actually happen
Separating the paths first makes it much harder to double up on some defenses while missing others. Here are the three I have either hit or nearly hit.
Leak path
Typical cause
Defense in this setup
Repository
Committing .env, hardcoding a real key in an example
gitignore + runtime injection
Logs / stdout
Agent echoes a command result verbatim
Masking filter
Agent context
Loading a file with a key, which is then sent to the model
Excluding it from what's read
Each needs its own defense. gitignore does nothing for logs, and masking does nothing for the repository. Let's take them in order.
1. A launch wrapper that injects only at execution time
Stop keeping secrets "written to a file and left there," and switch to "loaded into an environment variable only when you run." The store can be your OS keychain or a tightly permissioned .secrets.env. The point is that the value reaches the agent process and nothing else, leaving no trace in shell history or the repo.
This wrapper reads a gitignored secret file, exports only the keys it needs, and launches the agent. Reading them into an allow list rather than exporting everything keeps the injected surface within what you actually understand.
#!/usr/bin/env bash# run-agent.sh — inject secrets only at runtime, then launch the agentset -euo pipefailSECRETS_FILE="${HOME}/.config/antigravity/.secrets.env"# Permission check: refuse unless only the owner can read itif [ "$(stat -c '%a' "$SECRETS_FILE")" != "600" ]; then echo "Refused: set permissions on $SECRETS_FILE to 600" >&2 exit 1fi# Pull only the keys named in the allow list (don't pass the whole env)ALLOWED="DEPLOY_TOKEN API_KEY DB_URL"env_args=()while IFS='=' read -r key value; do [[ "$key" =~ ^#.*$ || -z "$key" ]] && continue for a in $ALLOWED; do if [ "$key" = "$a" ]; then env_args+=("$key=$value") fi donedone < "$SECRETS_FILE"# Grant temporarily via env; nothing persists in the calling shellexec env "${env_args[@]}" antigravity agent run "$@"
The allow list is the crux. If you pass every variable in the secret file unconditionally, keys that quietly accumulated over time flow along with everything else. Naming exactly which keys a run needs keeps the injected information within the boundary of your own understanding. I recommend keeping a separate allow list per run type.
✦
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 Bash launch wrapper that injects secrets as environment variables only at execution time
✦A Node masking filter that redacts GitHub and Google API key shapes from output and logs
✦Two scan gates, pre-commit and pre-run, that catch leaks before they land
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.
Injected keys surface in output tangled up with command results and error messages: a line of curl -v, a config dump, a stack trace. You want them gone before a human reads them.
Take the agent's stdout and stderr through a pipe and blank out anything matching a known secret value or a key-shaped pattern. Here it is in Node.
// redact.js — read stdin, mask secrets, write to stdoutconst SECRET_VALUES = (process.env.REDACT_VALUES || "") .split(",") .filter(Boolean);// Shape-based detection (catches values you don't know)const PATTERNS = [ /\b(gh[pousr]_[A-Za-z0-9]{20,})\b/g, // GitHub token /\b(sk-[A-Za-z0-9]{20,})\b/g, // API secret key /\b(AIza[0-9A-Za-z\-_]{20,})\b/g, // Google API key /\b([A-Za-z0-9+/]{40,}={0,2})\b/g, // long base64-looking blob];function redact(line) { let out = line; for (const v of SECRET_VALUES) { if (v.length >= 6) out = out.split(v).join("****"); } for (const p of PATTERNS) out = out.replace(p, "****"); return out;}let buf = "";process.stdin.on("data", (chunk) => { buf += chunk; const lines = buf.split("\n"); buf = lines.pop(); for (const line of lines) process.stdout.write(redact(line) + "\n");});process.stdin.on("end", () => { if (buf) process.stdout.write(redact(buf));});
The thing to keep is both value-based and pattern-based matching. Value-based (SECRET_VALUES) reliably erases the keys you injected but is helpless against ones you don't know. Pattern-based is the reverse: it catches unknown keys by shape but can slip through the net of a regex. Layer both so each covers the other's gap.
To combine it with the wrapper, change the final launch line to this.
Masking defends against what already came out. In front of it, place a gate that prevents it from being written in the first place. I split mine in two: before a commit, and before an unattended run starts.
Gate
Timing
What it checks
On failure
pre-commit
Just before commit
The staged diff
Abort the commit
pre-run
Before a run starts
Files about to be read
Cancel the run
The pre-commit hook stays short. It inspects only the staged diff and stops if it finds a key-shaped string.
#!/usr/bin/env bash# .git/hooks/pre-commit — detect secret-shaped strings in the staged diffset -euo pipefailpattern='(gh[pousr]_[A-Za-z0-9]{20,}|sk-[A-Za-z0-9]{20,}|AIza[0-9A-Za-z_-]{20,})'if git diff --cached -U0 | grep -nE "^\+" | grep -Eq "$pattern"; then echo "Refused: a secret-shaped string is staged." >&2 echo " Check the offending line with git diff --cached." >&2 exit 1fi
The pre-run side scans the list of files the agent is about to load and confirms no secret file or .env is included. An agent will faithfully read any file it's told to, so you draw the line on the human side before it reads. Keeping secret files out of what gets loaded is the direct defense against the third path, context leakage.
Setup in three steps
Working through it in this order keeps you from stalling partway.
Create the secret file with chmod 600 and add it to gitignore. Then write only the key names this run needs into the wrapper's allow list.
Drop in redact.js and pipe the wrapper's tail through | node redact.js. Deliberately echo your own key first and confirm it comes out masked.
Place the pre-commit hook in .git/hooks, make it executable, and wire the pre-run scan in ahead of the unattended launch.
What changed after living with it
After running these three layers for about two weeks, I scanned my old logs with a local scanner. Before masking, roughly 4% of the log lines held a key-shaped blob; in everything that had passed through the filter afterward, detections dropped to 0. The pre-commit hook stopped 2 of my own careless commits during that window, both botched .env pastes.
Bigger than the numbers was the shift in how it felt. Being able to hand a log to someone as-is is itself what makes running unattended jobs feel safe. Freed from eyeballing every run to confirm nothing leaked, I found the scope of what I could delegate to the agent quietly widening.
There is no perfect seal. Patterns need updating as new key formats appear, and over-trusting masking breeds its own carelessness. Even so, the idea of separating the paths and closing them one at a time makes gaps easier to spot. If you've ever had that stomach-drop moment yourself, I hope this setup can be a small foundation for peace of mind. Thank you for reading.
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.