Three in the morning is when the logging happens.
Your service went down, the stack trace is long, and the fix has to land before standup. You copy the trace, you paste it into the model, and the model starts explaining what broke. Somewhere inside that block of text sits a connection string. Password included.
That password just became part of a session history you do not own. You can delete the chat, export the transcript, or rotate the credential later. None of that un-pastes the string from wherever the platform stored it.
I have been testing free-model access and free-server options in AI development platforms, because free access changes the real attack surface. It stops being "who is allowed to send data out" and becomes "every engineer hits paste".
MonkeyCode is an open-source AI development platform with free model access, an advertised 10M-token allowance, and a free server option. The entry cost is near zero. When I put a platform like that in front of a team, I do not ask "how good is the model". I ask "what is allowed to leave the repository when an engineer uses it?"
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Three boundaries most teams never draw
Boundary one is the repository. Everything committed, reviewed, and merged lives there. You audit it, roughly, and you know what is in it.
Boundary two is the session. Every paste, every attached file, every referenced path gets copied into the model context. This boundary is invisible, grows in one direction, and has no git revert.
Boundary three is the artifact. Generated code, summaries, exports, screenshots that someone drops into a ticket. This is where the leaked secret comes back wearing a different name.
| Boundary | What crosses it | Who notices |
|---|---|---|
| Repo | explicit diffs | code review |
| Session | every paste and attachment | almost no one |
| Artifacts | summaries, exports, screenshots | no one, until the ticket |
The failure we keep seeing lives at boundary two. The reason is always the same: the paste was urgent, not malicious.
The fixture: a paste that should be blocked
Here is the minimal reproduction of the leak class. I use this as a canary before pointing any AI-assisted workflow at a repository.
mkdir -p /tmp/boundary-test && cd /tmp/boundary-test
git init -q
mkdir -p src scripts
Create a clean debug log file that should be safe to send:
cat > src/errors.log <<'EOF'
Traceback (most recent call last):
File "app.py", line 12, in <module>
runner.run()
RuntimeError: request timeout to worker-4
EOF
Now create the dangerous version. Same trace, same shapes, except the error line contains a Redis URL with credentials:
cat > src/bad-errors.log <<'EOF'
Traceback (most recent call last):
File "app.py", line 12, in <module>
runner.run()
RuntimeError: redis.exceptions.ConnectionError: Error connecting to
redis://user:SuperSecret1337@cache.internal:6379/0
EOF
No human reliably spots that difference at 3 a.m. So you do not rely on a human.
The gate: five patterns, one exit code
Here is the gate I run on any file before it is allowed to enter a session context. It is deliberately small: just five regex patterns and an exit code.
#!/usr/bin/env python3
"""secrets_gate.py - reject secret-shaped content before it enters an AI session."""
import hashlib
import json
import re
import sys
from pathlib import Path
SNIFFERS = {
"aws-access-key": re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
"github-token": re.compile(r"\bgh[pousr]_[A-Za-z0-9]{36,255}\b"),
"private-key": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
"url-credential": re.compile(r"[a-zA-Z][a-zA-Z0-9+.-]*://[^\s/:@]+:[^\s/@]+@"),
"secret-assignment": re.compile(
r"(?i)\b(password|passwd|secret|api[_-]?key|token)\b\s*[:=]\s*\S+"
),
}
def sniff(text: str) -> list[dict]:
findings = []
for lineno, line in enumerate(text.splitlines(), 1):
for kind, pattern in SNIFFERS.items():
if pattern.search(line):
findings.append({
"kind": kind,
"line": lineno,
"fingerprint": hashlib.sha256(
line.strip().encode()
).hexdigest()[:12],
})
return findings
def main() -> int:
targets = [Path(p) for p in sys.argv[1:]]
if not targets:
report = {"<stdin>": sniff(sys.stdin.read())}
else:
report = {}
for p in targets:
if p.exists():
report[str(p)] = sniff(p.read_text(errors="replace"))
findings = [item for items in report.values() for item in items]
print(json.dumps(report, indent=2))
if findings:
print(f"BLOCKED: {len(findings)} secret-shaped item(s). "
"Do not paste this into an AI session.")
return 1
print("CLEARED: no secret-shaped content detected.")
return 0
if __name__ == "__main__":
sys.exit(main())
Why five patterns? Because you are not building a security scanner. You are building a shape detector that catches the boring, predictable strings that still leak in real debugging traces. A URL with credentials at 3 a.m. is not a sophisticated attack. It is a paste.
Copy the script into the fixture repo and run the positive fixture. It must fail:
python3 scripts/secrets_gate.py src/bad-errors.log
# expect: BLOCKED with one url-credential finding
Then run the negative fixture. It must pass:
python3 scripts/secrets_gate.py src/errors.log
# expect: CLEARED
If those two outputs ever swap, your gate is broken. Fix the regex, not the test.
Make the gate stick
A one-time audit catches last week's mistake. It will not catch tomorrow's paste. So wire the gate into your workflow.
Pre-commit hook:
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: secrets-gate
name: block secret-shaped content before AI context
entry: python3 scripts/secrets_gate.py
language: system
pass_filenames: true
pip install pre-commit
pre-commit install
Pin pre-commit in your usual lockfile; the plugin runs from your repo, so it inherits your Python environment.
Pre-commit can be bypassed with --no-verify, so also run the gate in CI. This YAML is a template, not something I have executed in production:
# .github/workflows/secrets-gate.yml - template, adjust to your repo
name: secrets-gate
on: pull_request
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
python3 scripts/secrets_gate.py \
$(git diff --name-only origin/main...HEAD)
The workflow that shrinks the paste risk
- Write the context you want to send into a temp file: logs, snippets, config stubs, all in one place.
- Run
python3 scripts/secrets_gate.py that-file. - If
BLOCKED, replace the flagged line with[REDACTED]or a placeholder, then rerun. - Paste only when the gate returns
CLEARED. In a MonkeyCode session, or any other platform, this is the moment your context leaves the repo. - Delete the temp file when the session ends. The session history remains; the file does not.
- Re-run the canary fixture weekly. If the regexes produce false positives, tune them. If they produce a false negative, you will discover it at the worst possible moment.
| Phase | Action | Owner |
|---|---|---|
| Prevent | pre-commit rejects secret-shaped files before stage | developer |
| Detect | CI scans every PR diff | pipeline |
| Recover | rotate the credential, notify platform owner | security |
What you may send, what you must not
| Content | Send to AI session? | Notes |
|---|---|---|
| Stack trace without secrets | Yes | Redact paths and hostnames first |
package.json / requirements.txt
|
Yes | No credentials, but dependency info is intel |
.env with placeholder values |
Yes | Replace every value with VALUE_REDACTED
|
.env with real values |
Never | A regex may not catch it; policy must |
| Internal network diagrams | Only if you trust the provider | Platform logs become governance surface |
| Private keys and certificates | Never | End of discussion |
Who should not use this approach
If your team treats this gate as a replacement for secret management: stop. Shape detection is not rotation. A leaked key still has to be rotated, even if the gate catches it later.
If you have no CI baseline and expect one script to fix governance: it will not. Developers will rely on pre-commit alone, and pre-commit is one flag away from being skipped.
If you operate under formal compliance requirements, needing audit trails and DLP: this gate is a complement, not a substitute. Use Vault, short-lived credentials, and a real DLP layer. The gate is the seatbelt, not the crash structure.
Limitations and evidence
The fixture demonstrates the expected BLOCKED and CLEARED outcomes; I did not re-run this article's flow against a live MonkeyCode instance, so treat the outputs above as expectations to verify in your own CI before relying on them. I also did not benchmark the free model's throughput, the 10M-token allowance, or the free server's capacity. Those numbers come from MonkeyCode's public materials and will change; verify them at the source. The regex gate matches shapes, not semantics. Base64-wrapped secrets and random strings will pass. Use it as a boring-paste filter, not as a guarantee.
MonkeyCode is open source. You can read its source and inspect what it sends and where it stores session data. That inspection is worth more than anything I can promise here.
The boundary question
Which invariant belongs in CI, and which layer should enforce it? My default: secret shapes go in pre-commit, fast and local; filename policy, such as ".env, *.pem, and untracked key files never enter staged context", goes in CI, slow and unbypassable; platform-side retention you will only learn by reading the source.
Tomorrow, when you paste, paste from the temp file, not from terminal history. Make that habit as natural as 2>&1 | tee. That is the real invariant.
Top comments (0)