You should treat a crash dump as a floor plan of your systems, not as a harmless error message. Remote coding models only need the failing assertion, the relevant source, and enough context to propose a patch. Everything else in a typical CI log is inventory: paths, hostnames, tokens, container ids, and fixtures that resemble real customers. If you paste the whole log, you have already given a stranger a sketch of how the building is wired.
This walkthrough is about that sketch, not about prompt cleverness or another gitignore lecture. You will inspect a realistic failure artifact, run a preflight redactor, and keep the topology on your side of the network. The same habit matters whether the model runs in a browser tab or on a borrowed machine across the planet. A consultant can help you fix a jammed door without taking home the master keyring.
What a simple CI failure actually contains
Imagine you asked for help on a flaky checkout test and pasted the job log because extraction felt slow. The log looks like an error, yet it reads like a tour of the office if you slow down. Runner labels, image digests, compose project names, and the fixture email in the assertion share one stream. A model does not need to be malicious to remember those details inside a transcript you cannot later unsend.
Here is a compressed sample that resembles logs many teams paste without a second look. Treat it as fiction, but notice how ordinary each line feels until you ask who should see it. Copy it into a file you control, then read it the way an outsider would read a stolen notebook. That extra pause is the whole security control you still have before the paste.
##[debug] Starting: checkout-integration
Runner name: gha-prod-euc1-17
Runner network: 10.8.4.0/24
IMAGE: ghcr.io/acme-internal/checkout-ci@sha256:9f3c000000000000000000000000000000000000000000000000000000000001
DATABASE_URL=postgres://checkout_ci:s8Coffee-Ridge@orders-ci.internal:5432/orders
STRIPE_TEST_KEY=sk_test_51NexampleNotARealKey9901
Using fixture customer: Ana Gomez <ana.gomez+prodlike@acme-customers.test>
AssertionError: expected total 4200, got 0
at /home/runner/work/orders/orders/src/checkout/tax.py:88
at /home/runner/work/orders/orders/src/checkout/tax.py:141
DEBUG jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwiaWF0IjoxNTE2MjM5MDIyfQ.signature
##[debug] Finishing: checkout-integration
You came for the assertion on tax.py, and that is the only sentence that describes the bug. The runner name hints at a region and a pool size, and the slash-twenty-four sketches a private network. That connection string is still a door key, because teams reuse CI passwords more often than they admit. The fixture is not production data, yet it is production-shaped, which is how quiet recon often begins.
A stack frame works like a room number on a door you did not mean to photograph. A container digest labels a locked cabinet, and a debug JWT acts like a visitor badge. You would not pin those photos on a public whiteboard and then call the board private. A remote transcript sits closer to that whiteboard than it sits to a sealed envelope.
Threat model in one pass
Your real asset is not one secret string; it is the graph those strings imply when they sit together. An attacker who never touches your cluster can still learn where CI lives and which registries you trust. The actor may be a curious operator, a compromised log store, or a later leak of assistant transcripts. You do not need a movie villain for the floor plan to become useful to somebody else.
The trust boundary is the moment bytes leave the machine that already holds the log. Local redaction, local summarization, and local extraction of the failing test all happen before that boundary. After the boundary, you should assume the transcript is copied, indexed, and kept longer than the chat UI suggests. You should treat remote inference as disclosure, then send the smallest true statement of the failure.
Tool-calling agents make the boundary easier to miss, because the model can request the log as a tool result. You thought you pasted a question; the agent thought it was helpful to attach the whole ci.log file. If you allow an assistant to read the working tree, the tool channel is the same disclosure as paste. The floor plan travels in both corridors, and neither corridor will send those bytes back to you.
A preflight you can run before any paste
Regex will not save a determined leak, but it will catch inventory you stopped seeing because it looks like plumbing. Save the script below as crash_preflight.py and point it at a file you were about to ship to a model. It prints findings with line numbers and refuses a clean exit when high-risk shapes appear in the text. You still make the final call; the script only slows the reflex to paste the entire dump.
#!/usr/bin/env python3
"""Preflight scan for crash dumps and CI logs before they leave the machine.
This is a heuristic gate, not a security boundary. Encoded, split, or
vendor-specific secrets will slip through. Review every hit by hand.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
RULES = [
("private_ipv4", re.compile(
r"\b10\.\d{1,3}\.\d{1,3}\.\d{1,3}\b"
r"|\b192\.168\.\d{1,3}\.\d{1,3}\b"
r"|\b172\.(1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}\b"
)),
("connection_url", re.compile(
r"\b(?:postgres|mysql|mongodb|redis|amqp)://[^\s]+", re.I
)),
("pem_block", re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")),
("jwt", re.compile(
r"\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b"
)),
("aws_access_key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
("stripe_like", re.compile(
r"\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}\b"
)),
("github_pat", re.compile(
r"\bghp_[A-Za-z0-9]{20,}\b|\bgithub_pat_[A-Za-z0-9_]{20,}\b"
)),
("email", re.compile(
r"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", re.I
)),
("bearer", re.compile(
r"\bBearer\s+[A-Za-z0-9._\-+=/]{12,}\b", re.I
)),
("home_path", re.compile(
r"/home/[^/\s]+|/Users/[^/\s]+|\\Users\\[^\\\s]+"
)),
]
SOFT_RULES = [
("runner_or_cluster", re.compile(
r"\b(gha-prod|eks-|arn:aws:eks|cluster\.local)\b", re.I
)),
("internal_host", re.compile(r"\b[a-z0-9.-]+\.internal\b", re.I)),
("image_digest", re.compile(r"\bsha256:[a-f0-9]{64}\b")),
]
def scan(text: str) -> list[tuple[int, str, str]]:
hits: list[tuple[int, str, str]] = []
for lineno, line in enumerate(text.splitlines(), start=1):
for name, rx in RULES + SOFT_RULES:
if rx.search(line):
excerpt = line.strip()
if len(excerpt) > 160:
excerpt = excerpt[:157] + "..."
hits.append((lineno, name, excerpt))
return hits
def main(argv: list[str]) -> int:
if len(argv) != 2:
print("usage: crash_preflight.py <log-or-dump>", file=sys.stderr)
return 2
path = Path(argv[1])
text = path.read_text(encoding="utf-8", errors="replace")
hits = scan(text)
if not hits:
print(f"no heuristic hits in {path}")
return 0
print(f"{len(hits)} heuristic hit(s) in {path}")
for lineno, name, excerpt in hits:
print(f" L{lineno:4d} {name:18s} {excerpt}")
hard = {h[1] for h in hits} & {r[0] for r in RULES}
return 1 if hard else 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))
Run it against the sample after you save that text as sample.ci.log on your machine. A non-zero exit means you still owe the log a human pass, not that zero equals safety. The command below should fail closed on the sample, which is the result you want before any remote paste. Read the labels, then decide which rooms you can delete without hiding the actual assertion.
python3 crash_preflight.py sample.ci.log
echo $?
You should see hits on the private network, the database URL, the Stripe-shaped key, and the fixture email. The JWT, the runner name, the internal hostname, and the home-style path should light up as well. That list is the floor plan, and asking a model to ignore secrets does not erase the rooms they name. Stop sending those rooms; write a replacement artifact by hand so you do not launder the same file.
A model-sized failure report looks like the block below, and that smaller block is almost always enough. Keep it short enough that you could read it aloud without exposing a host, a person, or a key. If you cannot read it aloud, it is still a floor plan and it should not leave the machine.
Test: checkout tax total for a domestic cart
Expected: 4200
Actual: 0
Failing unit: src/checkout/tax.py, assertion near line 88
Nearby logic: tax.py lines 70-95 (rate table lookup) and 130-145 (rounding)
I cannot share CI env, runner names, image digests, or fixtures.
Question: which missing rate or rounding branch yields a zero total?
Notice what disappeared: accounts, regions, registries, people-shaped names, and every URL that named a host. Notice what remained: the behavior, the file, and a bounded question the model can actually answer. You can attach those seventy-five lines of tax.py if the function is self-contained and free of fixtures. You should not attach docker-compose.ci.yml, because compose files are floor plans with ports drawn in ink.
How to extract without dragging the building along
Commands help you slice the log the way you already slice a git bisect when a test goes red. Prefer extracting the assertion and the few frames that mention your package, then stop before the runner noise. The snippets below stay local and boring, which is the entire point of the extraction habit.
# Keep the assertion and nearby frames, then stop.
rg -n -C 3 "AssertionError|Error:|FAIL:" sample.ci.log | head -n 40
# Frames inside your code, not the runner image.
rg -n "work/orders/orders/src" sample.ci.log
# Replace production-shaped fixtures before anything leaves the machine.
sed -E \
-e 's/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/user@example.test/Ig' \
-e 's#postgres://[^[:space:]]+#postgres://redacted#g' \
sample.ci.log > sample.ci.redacted.log
python3 crash_preflight.py sample.ci.redacted.log
Sed is not a cryptographic eraser, and you should not pretend a substitution is the same as deletion. It is a speed bump that makes the floor plan uglier, which is often enough to stop an accidental paste. If a line still looks like a badge after substitution, delete the line instead of decorating it. Models cannot patch code they never needed, and they cannot keep data you refused to send across the wire.
When an agent offers to read the log and summarize, summarize first on your own machine and paste only that. That inversion feels slower until you count how long a leaked CI credential takes to rotate everywhere it reached. Speed belongs to the extraction step, not to the upload step that follows a red failure.
Where a remote free assistant still fits
There are honest reasons to send a tightly bounded failure to a remote model, including a thin laptop. MonkeyCode is one such assistant, with free model access and a free server option for remote inference. Disclosure: This article was prepared as part of MonkeyCode's product outreach. You can still use that path for the redacted assertion and a local source slice while keeping raw runner logs on your machine.
A free remote hop is not a reason to lower the boundary you just drew with the preflight script. Convenience is how floor plans travel, so you should run the same gate every time bytes want to leave. If the assistant cannot propose a patch from the assertion plus seventy-five lines of tax.py, you probably need a test. Add a failing unit test that contains no customer-shaped names, and send that test instead of the inventory.
Limitations, and who should skip this
Heuristic preflight is a flashlight, not a lock, and determined leaks will walk around these regular expressions. Secrets split across lines, base64 blobs inside JSON, custom header names, and screenshots of logs will not match. Unicode lookalikes in URLs, truncated JWTs, and vendor tokens outside the shapes above will also pass quietly. If your threat model includes a motivated insider at the inference provider, do not send repository code there.
You should not use this workflow when the bug only reproduces with a production dump or a customer export. Packet captures and warehouse extracts belong in an incident channel with retention rules, not in a coding assistant. Skip remote models when license, contract, or regulation treats source as restricted even after a redaction pass. A green preflight exit is not legal advice, and it is not evidence that the file is empty of meaning.
Teams that already stream CI logs to a shared SaaS still have a different processor than a model transcript. Do not confuse the fact that a CI vendor saw the log with permission for any model to see it. Different processors keep different retention, and they have different incentives to train or debug from your paste. If you cannot name the processor, you are not ready to send the log off the machine.
The conclusion does not change with the brand of assistant you happen to have open today. Paste the assertion, the failing function, and a synthetic test, and keep the floor plan off the wire. Run the preflight until the extra minute bores you, because boring gates are the ones you will actually use. The crash dump will keep drawing your network in the margin, and your job is to mail the symptom only.
Top comments (0)