Sponsored Content

DEV Community

jaryn
jaryn

Posted on

The Paste That Leaks: Build a Gate Between Your Repo and the AI Context

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then run the negative fixture. It must pass:

python3 scripts/secrets_gate.py src/errors.log
# expect: CLEARED
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
pip install pre-commit
pre-commit install
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

The workflow that shrinks the paste risk

  1. Write the context you want to send into a temp file: logs, snippets, config stubs, all in one place.
  2. Run python3 scripts/secrets_gate.py that-file.
  3. If BLOCKED, replace the flagged line with [REDACTED] or a placeholder, then rerun.
  4. Paste only when the gate returns CLEARED. In a MonkeyCode session, or any other platform, this is the moment your context leaves the repo.
  5. Delete the temp file when the session ends. The session history remains; the file does not.
  6. 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)