The Problem Is Irreversibility
When an AI agent can clone a repository, mine a knowledge base, draft a post, and publish it to the open web without a human in the loop, the failure mode is not a typo. It is a leaked credential that is indexed by Google within minutes and scraped by bots within seconds. A leaked key is unrecoverable once public. Rotation helps, but the window between exposure and rotation is enough to drain a cloud account, exfiltrate a database, or compromise a CI pipeline.
We built an agentic content system at Savage Digital Solutions that does exactly this: it pulls from a git-cloned knowledge base, generates drafts in TypeScript, and publishes to Next.js-powered properties. Before we let it touch the publish button, we needed to answer one question honestly: what is the worst thing this agent could accidentally ship?
The answer was a list. API keys. Database URIs. Internal hostnames. Private IP ranges. JWT tokens. Once we had the list, we built two independent layers to stop them.
Layer One: Exclusion by Construction
The knowledge base the agent mines is built from a git clone. That single architectural decision does most of the work. Anything listed in .gitignore is excluded before the agent ever sees it.
Our secrets directory is in .gitignore. So are local environment files, certificate bundles, and any directory that holds credentials for third-party services like HeyGen or MongoDB Atlas. The agent cannot leak what it cannot read.
This is not a scrubber. It is a structural guarantee. The knowledge base is assembled from the cloned tree, and the cloned tree does not contain the secrets directory by construction. No regex, no scanning, no runtime check required at this layer.
The practical implication: if a developer accidentally commits a secret to a tracked file, that is a separate problem handled by pre-commit hooks and GitHub's secret scanning. The agent layer assumes the repository is clean and adds its own independent defense on top.
Layer Two: A Pure Pattern Scrubber on Every Draft
The second layer runs on every draft the agent produces, regardless of where the content came from. It is a pure function: it takes a string, scans it against a set of credential patterns, and returns either a pass or a block. It never modifies the content. It never cleans and ships.
The patterns it checks for include:
sk-prefix keys (OpenAI and similar services)AKIAprefix strings (AWS access key IDs)ghp_prefix strings (GitHub personal access tokens)AIzaprefix strings (Google API keys)eyJprefix strings (JWT tokens in their base64-encoded form)Bearerfollowed by a token (authorization headers that have leaked into prose)- Database URIs with embedded passwords (MongoDB connection strings, PostgreSQL DSNs, and similar formats where credentials appear inline)
- RFC 1918 private address ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x)
- Internal file paths (absolute paths that reveal server directory structure)
We describe these as patterns rather than publishing the live regular expressions here, because the scrubber itself would flag a document containing live credential-matching regex as a potential leak. That is the correct behavior.
The policy is block-on-doubt. Any match holds the post and fires an alert. A human reviews it before anything is published. This is not a soft warning. The agent cannot override it.
// Simplified structure of the scrubber interface
interface ScrubResult {
passed: boolean;
matches: ScrubMatch[];
}
interface ScrubMatch {
patternName: string;
excerpt: string; // surrounding context, not the secret itself
position: number;
}
function scrubDraft(draft: string): ScrubResult {
// Runs all patterns against draft
// Returns passed: false and halts pipeline on any match
// Never mutates draft content
}
The function is pure and stateless. It has no side effects beyond returning the result. The pipeline that calls it is responsible for blocking and alerting. This separation makes the scrubber easy to unit test against a fixture library of known-bad strings.
Why Two Layers Instead of One
Each layer fails in a different direction.
The .gitignore exclusion fails if a developer commits a secret to a tracked file. It does nothing about secrets that the agent might construct or infer from context, and it does nothing about secrets that arrive through a prompt injection in source content.
The scrubber fails if a credential pattern is novel enough that no existing pattern matches it. It also cannot catch secrets that are semantically present but syntactically disguised (a key split across two sentences, for example).
Neither layer is sufficient alone. Together, they cover the realistic failure modes: accidental inclusion of a secrets file in the knowledge base, and accidental generation of a credential-shaped string in a draft.
The combination also satisfies a compliance requirement we care about: we can demonstrate to a client that two independent controls exist, that neither depends on the other, and that the policy is block-on-doubt rather than clean-and-ship. That last point matters. A scrubber that removes the credential and publishes anyway is not a safety control. It is a false sense of security that obscures the fact that a credential was present in the pipeline at all.
What This Looks Like in Practice
The agent pipeline runs in this order:
.gitignore exclusions apply here).scrubDraft().passed: false, hold the post, log the ScrubMatch array, and send an alert to the review queue.passed: true, proceed to the Next.js publishing step.Step 5 has fired three times in our testing phase, twice on synthetic test fixtures and once on a draft that included a MongoDB Atlas URI that had been pasted into a markdown file in the knowledge base. That file was not in .gitignore. The scrubber caught it. We added the file to .gitignore and the URI pattern confirmed the scrubber was working as intended.
The alert in step 5 includes the patternName and the surrounding context (not the secret itself) so the reviewer knows what to look for without the alert itself becoming a credential leak.
Key Takeaways
git clone so .gitignore exclusions apply by construction. The agent cannot leak what it cannot read.sk- keys, AKIA strings, ghp_ tokens, AIza keys, eyJ JWTs, Bearer tokens, database URIs with embedded passwords, RFC 1918 addresses, and internal file paths.