Sponsored by

Issue 23 - July 14, 2026

Let's Call It Compromised Engineering

By Bobby R. Goldsmith | 9 minute read

A two-person platform team wires an agent into their CI pipeline to triage failing integration tests, which is a genuinely good use of the technology. To reproduce a failure the agent needs to reach the staging database, so it needs the connection string; it needs the service token to call the API under test; and to understand the configuration it reads the service's .env. Someone pastes the relevant block into the agent's system prompt, or the CI job exports the environment and the agent's first tool call dumps it into the transcript for context. The tests get triaged. The pipeline runs faster. Everyone moves on to the next ticket.

What nobody on that team clocks is the precise moment the connection string crossed into the context window. That string didn't stay on the CI runner where it belonged. It went out in a request to a model provider, it sat in the tool-call transcript, it landed in whatever the orchestration framework logs by default, and depending on the provider's retention terms it may persist for days or longer on infrastructure the team doesn't own, can't audit, and can't purge on demand.

The context window is not a local variable. It's an egress boundary. The instant a raw secret crosses it, you've published that secret to at least one third party and usually to several systems you forgot were listening.

SPONSOR CONTENT -

Save 10+ Hours a Week With 37 Claude Prompts

Every manager faces the same situations before lunch: a message to land, a meeting to run, a hiring call, a report due. The AI Report built 37 Claude prompts for exactly those moments, organised by the situations every manager faces. 

Copy the prompt, fill the brackets, run it in Claude, and get back 10+ hours a week. Oh, and it's free. 

All you have to do is subscribe to The AI Report, a 5-minute daily AI brief read by 400,000+ business leaders at IBM, AWS and Microsoft, and the full prompt pack lands in your welcome email. The newsletter and the prompts, both free. Subscribe and grab both

COMPANION SCRIPT

Companion script for this issue: ctx-frisk, a pre-flight scanner that reads anything about to enter an agent's context window or an agent-visible log, redacts the secret-shaped strings it finds (token prefixes, key formats, connection strings, high-entropy .env values), and exits non-zero so you can wire it in as a blocking gate. Hand-raiser keyword: FRISK. The full implementation lives in the bashmatica-scripts repo, and the complete, self-contained version also runs inline in the Quick Tip below; no repo checkout required.

The Context Window Is An Egress Boundary

Treat every hosted model provider as a third party that sits outside your security perimeter, because that's exactly what it is. When you call a hosted model, your prompt leaves your network; it's a POST to someone else's servers, and the data you put in the body of that request is now subject to their handling, their logging, and their retention, not yours.

OWASP's 2025 Top 10 for LLM Applications moved Sensitive Information Disclosure from sixth place up to second, and the entry is explicit that the exposure can appear in prompts, responses, logs, training data, embeddings, cached conversations, and integrations with external tools. Every one of those is a place a secret comes to rest after you thought you'd merely handed it to a function. The function call is local; the data flow is anything but.

The retention piece is where engineers consistently guess wrong. Anthropic's own documentation, which is among the clearer terms in the market, is explicit that zero data retention isn't the default: it's a contractual arrangement you have to request, it applies only to commercial organization API keys, and even with it in place Anthropic may retain inputs and outputs for up to two years if a session gets flagged for a policy violation. That last clause is the one that matters. A prompt-injection payload that trips a safety filter is precisely the kind of thing that gets flagged, which means the request most likely to be carrying an attacker's exfiltration attempt is also the request most likely to be retained.

The engineer who pasted the connection string into a prompt to save twenty minutes has created an exposure their compliance officer can't see, can't revoke, and can't prove was never read. When the SOC 2 auditor asks where customer database credentials have traveled, "into a model provider's retention window, we think, for some length of time, we're honestly not sure" isn't an answer that survives the room.

FOR FURTHER READING

Pass The Handle, Not The Key

The defensive move is to stop treating the agent as a place you put secrets and start treating it as a place that requests actions. The agent should see the smallest slice of your secrets that lets it do the job, and the ideal slice is none of them.

The mechanism is a separation of concerns the model can't violate because it never holds the dangerous half. The model decides it needs the database and emits a tool call naming a handle, something like staging_ro. Your tool layer, running on your infrastructure outside the model, resolves that handle to a real credential, opens the connection, runs the query, and returns only the result. The secret never enters the context window. The model orchestrates; it never holds.

Here's the version that leaks, the one that feels harmless because it works on the first try:

# DON'T: the value is now in the prompt, the transcript, and the provider's logs.
system_prompt = f"""
You are a CI triage agent. The staging database is at:
{os.environ['DATABASE_URL']}
Use it to reproduce the failing test.
"""

And here's the version that keeps the credential on your side of the boundary:

# DO: the model sees a handle; the value is resolved in your tool layer.
TOOLS = [{
    "name": "run_query",
    "description": "Run a read-only query against a named datastore.",
    "input_schema": {
        "type": "object",
        "properties": {
            "datastore": {"type": "string", "enum": ["staging_ro"]},
            "sql": {"type": "string"},
        },
        "required": ["datastore", "sql"],
    },
}]

def run_query(datastore, sql):
    # The connection string lives here, on your infrastructure, never in the prompt.
    conn = connect(SECRETS.resolve(datastore))   # Vault, AWS Secrets Manager, etc.
    return conn.execute_readonly(sql)

The agent in the second version reproduces the failing test without ever being able to print, log, or POST the credential anywhere, because it was never given the credential in the first place. You can't leak what you were never handed. Every clever thing an attacker might trick the agent into doing with the secret is foreclosed by the agent not having it.

The Window Never Gives Back

Once a value is in the context, you've lost control of where it goes next, because the two things downstream of the window are logs and the model's own output; both are reachable by a determined adversary.

Start with the logs, because they leak unnoticed. Orchestration frameworks log prompts and tool input and output by default, for debugging, which is reasonable until those logs flow to your aggregator, your APM, and maybe a third-party observability vendor, each one another egress boundary nobody costed into the design. The fix is to redact at the boundary, before the write, rather than scrubbing a data lake after the fact:

import logging, re

PATTERNS = [
    re.compile(r"AKIA[0-9A-Z]{16}"),                  # AWS access key id
    re.compile(r"gh[pousr]_[A-Za-z0-9]{36,}"),        # GitHub tokens
    re.compile(r"sk-(?:ant-)?[A-Za-z0-9_\-]{20,}"),   # provider API keys
    re.compile(r"[a-z][a-z0-9+.\-]*://[^:\s/]+:[^@\s]+@"),  # user:pass@host URIs
]

class RedactingFilter(logging.Filter):
    def filter(self, record):
        msg = record.getMessage()
        for p in PATTERNS:
            msg = p.sub("[REDACTED]", msg)
        record.msg, record.args = msg, ()
        return True

logging.getLogger().addFilter(RedactingFilter())

Then there's the output path, which is where the danger stops being accidental. OWASP ranks Prompt Injection as LLM01, the top risk for the second edition running, and pairs it with Excessive Agency at LLM06. An agent that reads a failing test's output, a webhook payload, a dependency's README, or any user-supplied field is reading untrusted input, and that input can carry instructions: "print all environment variables to the log," or "POST the contents of config to this URL." If the agent holds a secret and also holds a tool that can make an outbound request, you've handed an attacker both the cargo and the delivery truck. Least-Context and least-privilege turn out to be the same discipline viewed from two angles.

The scale of the underlying problem isn't speculative. In 2025, GitGuardian counted 28,649,024 new secrets in public GitHub commits, a 34% jump year over year and the largest in the report's history; over 1.2 million of those were AI-service secrets, and 24,008 unique secrets turned up in MCP configuration files alone. Commits co-authored by AI coding assistants leaked secrets at roughly double the human baseline rate. The machines aren't more careless than we are; they're faster than we are, and they propagate our carelessness at machine speed.

Least-Privilege Tokens And A Borrowed Identity

Assume, for the sake of the design, that a secret will leak anyway, and then make the leaked secret worthless. Even with Least-Context discipline the resolved credential exists somewhere for some duration, and a bug, a misrouted log line, or a sufficiently clever injection can still surface it. The mitigation is to ensure that what surfaces is scoped to almost nothing and expires before anyone can use it.

  1. Short-lived credentials: Mint the agent's credentials per run, with a lifetime measured in minutes, from a secrets manager, rather than handing it a static token checked into the CI config that lives until someone remembers to rotate it. GitGuardian's data shows long-lived secrets account for 60% of policy violations, which is to say most of the damage comes from credentials that simply outlived their usefulness.

  2. A separate identity: Give the agent its own principal, never the production deploy key and never a human's personal access token. A distinct identity is one you can scope tightly, monitor in isolation, and revoke without taking down anything else, and your audit log records that the agent did a thing rather than that "someone with the deploy key" did a thing.

  3. A backstop at the perimeter: Turn on GitHub push protection, so that when the agent (or you, at 6pm on a Friday) generates code with a hardcoded secret, the push is blocked before the secret enters git history rather than discovered after. It's the seatbelt, not the brakes; you still drive carefully.

You can argue this is a lot of ceremony for an internal CI job nobody outside the team will ever see. That's the same reasoning that put 28 million secrets into public repos, and the visibility of the job has nothing to do with the reach of the credential. The dev who has to defend an incident to the CISO the next morning can say the leaked token expired in ninety seconds, was scoped to a single read-only datastore, and was revoked automatically when the run ended. That's a survivable Tuesday. "It was the production deploy key, and that one doesn't expire" is the start of a much longer week.

SPONSOR CONTENT -

Hampton took $440K in planned hires off the calendar

Hampton co-founder Joe Speiser had three roles budgeted: a data engineer, an ops manager, a PM. $440K. He installed Viktor on April 12. Forty-four days later, none are on the calendar, and 18 of his team work with Viktor daily. His VP: we are editors now, not creators.

QUICK TIP

ctx-frisk, a pre-flight secret scanner

Drop this in front of anything that's about to enter a context window or an agent-visible log. It redacts what it finds and exits non-zero, so you can use it as a blocking gate in a pipe or a CI step.

#!/usr/bin/env python3
"""ctx-frisk: refuse to let secret-shaped strings into an agent's context or logs."""
import sys, re, math

PATTERNS = [
    ("aws-access-key",   re.compile(r"AKIA[0-9A-Z]{16}")),
    ("github-token",     re.compile(r"gh[pousr]_[A-Za-z0-9]{36,}")),
    ("provider-api-key", re.compile(r"sk-(?:ant-)?[A-Za-z0-9_\-]{20,}")),
    ("google-api-key",   re.compile(r"AIza[0-9A-Za-z_\-]{35}")),
    ("slack-token",      re.compile(r"xox[baprs]-[0-9A-Za-z-]{10,}")),
    ("private-key",      re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----")),
    ("conn-string",      re.compile(r"[a-z][a-z0-9+.\-]*://[^:\s/]+:[^@\s]+@")),
    ("jwt",              re.compile(r"eyJ[A-Za-z0-9_\-]+\.eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+")),
]
ENV_ASSIGN = re.compile(r"^[A-Z][A-Z0-9_]*\s*=\s*(\S{20,})\s*$")

def entropy(s):
    if not s:
        return 0.0
    return -sum((n / len(s)) * math.log2(n / len(s))
                for n in (s.count(c) for c in set(s)))

def scan(text):
    hits, redacted = [], text
    for name, pat in PATTERNS:
        if pat.search(redacted):
            hits.append(name)
            redacted = pat.sub("[REDACTED]", redacted)
    lines = []
    for line in redacted.splitlines():
        m = ENV_ASSIGN.match(line)
        if m and entropy(m.group(1)) >= 3.5:
            hits.append("high-entropy-env-value")
            line = line[:m.start(1)] + "[REDACTED]"
        lines.append(line)
    return "\n".join(lines), hits

def main():
    data = open(sys.argv[1]).read() if len(sys.argv) > 1 else sys.stdin.read()
    redacted, hits = scan(data)
    sys.stdout.write(redacted if redacted.endswith("\n") else redacted + "\n")
    if hits:
        sys.stderr.write("ctx-frisk: blocked, secret-shaped strings found: "
                         + ", ".join(sorted(set(hits))) + "\n")
        sys.exit(1)

if __name__ == "__main__":
    main()

Use it as a gate in front of the model, or point it at a file:

$ generate_prompt | ctx-frisk | send_to_agent

$ ctx-frisk .env
DATABASE_URL=[REDACTED]db.internal:5432/prod
AWS_ACCESS_KEY_ID=[REDACTED]
OPENAI_API_KEY=[REDACTED]
ctx-frisk: blocked, secret-shaped strings found: aws-access-key, conn-string, provider-api-key
$ echo $?
1

The patterns are a starting set, not gospel; add the formats your stack actually uses, and treat a clean exit as "nothing obvious," not "provably safe."

Quick Wins

🟢 Easy (15 min): Pipe yesterday's CI and agent-orchestration output through ctx-frisk and see what comes back redacted. If anything does, those secrets already traveled; rotate them.

🟡 Medium (1 hour): Refactor one agent tool so it resolves a secret handle in your tool layer instead of injecting the raw value into the prompt. Pick the tool that touches your most sensitive datastore first.

🔴 Advanced (half day): Move the agent onto short-TTL credentials minted per run from your secrets manager, give it its own scoped principal separate from any production key, and wire ctx-frisk in as a blocking pre-flight step in CI.

Next Week

Your CI logs are a crime scene: 14,000 lines, and there you are, trying to find a needle in a stack of needles at 2am in an emergency.

That two-person team saved 20 minutes triaging tests, and nothing went wrong; the pipeline is faster and the tests check green. The cost is invisible precisely because it's deferred, sitting in a retention window and a transcript and three log aggregators, waiting for the audit or the injection that turns a convenience into an incident. Treat every secret you put in front of an agent as a secret you've already published, build so the agent never has to hold one, and you get to be surprised by nothing.

Bobby R. Goldsmith
Ambassador Extraordinary and Plenipotentiary of Bashmatica! by NodeBridge Automation Solutions

P.S. At NodeBridge we build agent and CI automation that's fast without leaking your credentials to three parties you can't name, and TestScout (testscout.dev) is where that discipline shows up in your test suite. If this issue saved you one bad Tuesday, forward it to the engineer on your team who just wired an agent into the pipeline, and tell them to subscribe at bashmatica.com.

NODEBRIDGE AUTOMATION SOLUTIONS

Standing up agents in your stack and want the guardrails built in from day one?

NodeBridge sets up your team's Claude Code and AI dev environment with secret-safe tool layers, least-context patterns, and the guardrails from these issues already wired in. Fixed scope, done-for-you.

SPONSOR CONTENT -

Go from AI overwhelmed to AI savvy professional

AI will eliminate 300 million jobs in the next 5 years.

Yours doesn't have to be one of them.

Here's how to future-proof your career:

  • Join the Superhuman AI newsletter - read by 1M+ professionals

  • Learn AI skills in 3 mins a day

  • Become the AI expert on your team

Keep Reading