Dev.to Security 🔐 Cybersecurity 👁 0 📖 7 min read

A Free Allowance Is Not a License to Paste: Budget the Bytes Before They Reach the Model

Illustrative scenario (constructed for this article, not an incident report). A platform team plans a single request to a free coding assistant: "why does the parser return null on empty input?" The plan attaches src/par

Illustrative scenario (constructed for this article, not an incident report). A platform team plans a single request to a free coding assistant: "why does the parser return null on empty input?" The plan attaches src/parser/**, the full logs/ directory from a staging run, and config/. Someone measures it before sending. The question is 14 words; the attachment set is 1,900 files. Nothing in the request is malicious. Nothing in it is minimal either.

That gap is the topic. Cheap or free token allowances change developer behavior before they change any security control. When context feels free, the natural workflow becomes "attach the repo," and a narrow debugging question quietly turns into a full source export across a boundary you do not own.

This post is about the control that gap needs: a declared byte ceiling, checked before the run, that fails closed — not another ignore file.

Where the boundary actually sits

A free-tier agent run is not one hop. It is a chain, and each hop has a different owner and a different answer to "what happens to this after the run?"

Hop What it can see What you control Typical failure
Your editor / local agent Everything you attach Attachment selection Over-selection by convenience
Free server option The working copy on that machine What gets cloned or uploaded Secrets committed to the repo get carried along
Model endpoint The assembled prompt per turn Prompt size and content Retries and tool loops multiply sends
Logs on any of the above Request metadata, sometimes bodies Verbosity settings, if exposed Debug mode writes payloads to disk

Two consequences follow. First, "I only sent one file" can be false even when you attached one file — the agent reads more. Second, bytes, not files, is the unit that matters, because a tool-using run re-sends context on every turn.

Step 1 — Declare a ceiling before you look at the prompt

A budget set after you see the prompt is a rationalization. Set it from the task class instead: a bug triage question should not cost the same bytes as a migration review.

# context-budget.yaml — illustrative policy, adjust ceilings to your repo
task: triage-null-parser
ceiling:
  bytes_out: 200000
  files: 12
  classes:
    source: 150000
    logs: 20000
    config: 5000
deny:
  - "**/.env*"
  - "**/*.pem"
  - "**/secrets/**"
  - "**/fixtures/customer/**"
  - "**/vendor/**"
require_human_signoff: true

The ceilings are the only numbers here you must justify. Pick them by taking your last three honest debugging sessions, recording bytes sent, and setting the ceiling near the median — not the maximum. A ceiling that never fires is decoration.

Step 2 — Measure the plan, not the transcript

You cannot budget what you assemble on the fly, so budget a plan file: one path per line, committed next to the task. The checker below is illustrative and has not been executed against a real workspace; treat it as a starting point to adapt and test.

#!/usr/bin/env python3
# ILLUSTRATIVE / UNEXECUTED. Adapt paths and markers before trusting it.
import fnmatch, json, pathlib, sys

MARKERS = (b"-----BEGIN", b"AKIA", b"ghp_", b"xoxb-")

def classify(rel: str) -> str:
    if rel.startswith("logs/"):
        return "logs"
    if rel.startswith("config/"):
        return "config"
    return "source"

def main(plan_path: str, policy_path: str) -> int:
    policy = json.loads(pathlib.Path(policy_path).read_text())
    plan = [p.strip() for p in pathlib.Path(plan_path).read_text().splitlines() if p.strip()]
    ceiling = policy["ceiling"]
    out = {"files": 0, "bytes_out": 0, "classes": {}, "denied": [], "markers": []}

    for rel in plan:
        if any(fnmatch.fnmatch(rel, g) for g in policy["deny"]):
            out["denied"].append(rel)
            continue
        p = pathlib.Path(rel)
        if not p.is_file():
            out["denied"].append(rel + " (missing)")
            continue
        blob = p.read_bytes()
        out["files"] += 1
        out["bytes_out"] += len(blob)
        cls = classify(rel)
        out["classes"][cls] = out["classes"].get(cls, 0) + len(blob)
        if any(m in blob for m in MARKERS):
            out["markers"].append(rel)

    print(json.dumps(out, indent=2))
    if out["denied"] or out["markers"]:
        return 3
    if out["bytes_out"] > ceiling["bytes_out"] or out["files"] > ceiling["files"]:
        return 2
    for cls, limit in ceiling["classes"].items():
        if out["classes"].get(cls, 0) > limit:
            return 2
    return 0

if __name__ == "__main__":
    sys.exit(main(sys.argv[1], sys.argv[2]))

Three exit codes, three meanings: 0 allow, 2 over budget, 3 denied path or credential-shaped marker found. Fail closed in the wrapper so only 0 proceeds:

# illustrative CI step
python3 context_budget.py plan.txt policy.json || exit 1

Step 3 — Test the budget the way you test code

A budget checker that has never blocked anything is untested. Build fixtures and assert on exit codes, not on console text.

Fixture Setup Expected
Positive 6 small source files, 40 KB total exit 0, bytes_out ≈ 40960
Over-budget one 300 KB generated file exit 2, bytes_out > ceiling
Denied path plan line config/.env.local exit 3, listed under denied
Marker public test fixture containing -----BEGIN exit 3
Known gap PEM renamed to notes.txt passes the glob, is caught only if a marker is present

The last row is the honest one. Globs and markers are tripwires, not classification. A certificate in a format your marker list does not cover will pass. Test that case, then decide whether the residual risk is acceptable or needs a second, independent check.

Step 4 — Budget per run, not per prompt

Here the plan receipt stops being sufficient. Multi-turn tool use re-sends accumulated context, and one retry doubles a turn. A 200 KB plan can become well over a megabyte of bytes sent across a session. Instrument the wrapper that actually transmits, and total bytes for the whole run:

# ILLUSTRATIVE sketch — count at the transport boundary, not at the plan
run = {"turns": 0, "bytes_sent": 0}

def send(messages):
    payload = encode(messages)           # your client's real serializer
    run["turns"] += 1
    run["bytes_sent"] += len(payload)
    if run["bytes_sent"] > RUN_CEILING:  # separate, larger ceiling
        raise BudgetExceeded(run)
    return transport.post(payload)

Two ceilings, two jobs. The plan ceiling stops a bad request from being drafted. The run ceiling stops a correct request from being repeated forty times.

What not to send, regardless of budget

Category Why a ceiling will not save you
Credentials, keys, tokens Size is negligible; exposure is total
Customer fixtures and support exports Third-party data you may not be permitted to transfer
Production data dumps Even one row can be a regulated record
License-restricted vendor code The boundary is contractual, not technical
Unreviewed contracts or HR files They end up in a repo because someone used git add .

The free model access and free server option on an open-source platform like MonkeyCode (open-source per the project; the operator states a 10,000,000-token free allowance at the time of writing — confirm current terms yourself before planning around the number) make the first four rows easier to violate, because the cost signal that used to make you trim context is gone. Disabling a cost signal is a security change whether or not anyone calls it one.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Prevent, detect, recover

Layer Control Evidence it works
Prevent Ceiling declared per task class; deny globs; exit codes gate transmission Fixture table above asserts 0/2/3
Detect Per-run byte counter at the transport boundary; receipts compared against plan Turn count and bytes_sent recorded per run
Recover Rotate anything that crossed; re-scope the task; re-run with a lower ceiling Rotation ticket linked from the receipt

Recovery has one question you should ask rather than assume: what does the free tier retain, and for how long? If the answer is not documented where you can cite it, treat the data as retained and rotate accordingly. Do not put an assumption about retention into your threat model.

Limitations, and who should skip this

  • The checker measures a plan, not the agent's real reads. An agent that opens files on its own can exceed the plan without touching it.
  • Byte ceilings are a volume control. They do not classify sensitivity — a 2 KB key file passes every ceiling.
  • Solo prototypes, throwaway repos, and public OSS with no secrets get little from this. The cost is a policy file and a CI step you must maintain.
  • Regulated workloads need a documented data-flow review, not a shell script with exit codes.

The boundary question

A ceiling is only real if something refuses to proceed when it is crossed. So: should the byte budget live in CI as a hard gate on every agent-launched task, or at the wrapper that owns the transport, where it can also count retries? Pick one owner — if both enforce it, you will eventually debug which one lied. If you run this against the free server, the useful report is which exit code fired first and which file pushed you over.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.