What this covers
- Why the cost of an LLM agent grows with roughly the square of its step count, shown with a formula and calculated values.
- Why budget alerts don't stop anything, and a minimal guard that prices a call before it runs.
- A pre-commit / pre-push gate, written with the Python standard library only, that blocks personal traces (home paths with a login name, secret keys, e-mail addresses, EXIF, OS residue) from reaching a public repo or a distributable ZIP, and how it was verified.
Every dollar figure below is calculated from the formula, not taken from an invoice. The prices are example values; substitute your provider's current official prices.
A browser-only demo (no network requests) is here: https://zero-trace-ship-kit.zeus-lab-global.workers.dev/
1. The shape of the bill
On every step an agent resends the conversation so far, including every tool result. Let:
-
B= base context (system prompt, tool definitions) -
A= tokens added per step (tool result plus the previous reply) -
O= output tokens per step -
S= number of steps
Input on step k is B + (k-1)·A. Summed over S steps:
total input = S·B + A·S·(S-1)/2 The second term is quadratic. Double the steps and it roughly quadruples.
With the example values B=6,000, A=2,500, O=500, input at $3 per million tokens, output at $15, cached input (prompt-cache reads) at $0.30:
| Steps | Total input tokens | Total cost | If every prefix is a cache hit | Avg step ÷ first step |
|---|---|---|---|---|
| 10 | 172,500 | $0.59 | $0.20 | 2.3× |
| 20 | 595,000 | $1.94 | $0.47 | 3.8× |
| 40 | 2,190,000 | $6.87 | $1.24 | 6.7× |
| 80 | 8,380,000 | $25.74 | $3.66 | 12.6× |
| 160 | 32,760,000 | $99.48 | $12.12 | 24.4× |
Sixteen times the steps costs about 169 times as much. That is why a job that cost cents yesterday changes order of magnitude on the day it runs long.
Three things follow:
- Steps and per-step growth matter more than unit price. Halve the price, run 1.5× the steps, and you are back where you started.
- Caching helps a lot but does not remove the curve. The cached column still accelerates. Rewriting the start of the context drops the cache and returns that step to full price, and price lists with a cache-write surcharge land above this column.
- Shrinking tool results is the strongest lever.
Ais the coefficient of the quadratic term. A tool that returns the lines you need instead of a whole file changes the total directly.
2. Enforce before the call, don't alert after it
A budget alert arrives after the money is spent. To stop spending, price the worst case before the request and refuse to send it.
class BudgetExceeded(RuntimeError): pass class Guard: def __init__(self, cap_usd, in_per_mtok, out_per_mtok): self.cap, self.pin, self.pout, self.spent = cap_usd, in_per_mtok, out_per_mtok, 0.0 def authorize(self, input_tokens, max_output_tokens): worst = (input_tokens * self.pin + max_output_tokens * self.pout) / 1_000_000 if self.spent + worst > self.cap: raise BudgetExceeded(f"spent ${self.spent:.4f}, next call up to ${worst:.4f}") def record(self, input_tokens, output_tokens): self.spent += (input_tokens * self.pin + output_tokens * self.pout) / 1_000_000 -
authorizeassumes the full output allowance is used. Judging by the worst case means no call that could cross the cap is ever sent. -
recordtakes the usage the provider actually reported. - Keep a cap per month as well as per run. Point several processes at one ledger file and the cap covers their sum.
In the example above, a $2 cap stops the run at step 21. The gap to the $6.87 it would have cost at 40 steps is what one run saves.
Repeating calls (reading the same file again, running the same search again) are the other classic. Compare a string built from the tool name and its arguments with the previous one, and stop after a few identical requests in a row.
3. Traces that end up in what you publish
When an agent works on your machine, logs, configs, error output and test results pick up details of that machine. Typical leftovers:
| Kind | Example |
|---|---|
| Absolute home paths | /Users/<login>/..., /home/<login>/..., C:\Users\<login>\... |
| Keys | service API keys, anything starting BEGIN ... PRIVATE KEY |
| E-mail addresses | a contact line in a README, the commit identity |
| Image metadata | JPEG EXIF (can include GPS), PNG text chunks |
| Document authors | PDF Author, Office creator fields |
| OS residue | .DS_Store, __MACOSX/, Thumbs.db, .env |
| Git identity | commits made under a personal name and address |
ZIPs are where this gets missed: people review the working folder, not the archive they built from it, and certainly not a ZIP inside that ZIP.
4. The gate
The policy is one line: if anything is found, the commit or push fails.
4.1 Known shapes: regular expressions
HOME_PATH = re.compile( r"(?<![\w.])(?:/Users/|/home/|[A-Za-z]:\\{1,2}Users\\{1,2})([A-Za-z0-9][A-Za-z0-9._-]*)" ) The leading (?<![\w.]) keeps URLs such as https://example.com/home/dashboard from matching. Placeholder and CI names such as you or runner are allowed.
4.2 Your own name: read at run time, never stored
A scanner with your real name written in it is itself a leak. The gate reads the login name, the full name registered with the OS, and the global git identity from the machine on each run, matches in memory only, and masks them in its output.
raw.add(getpass.getuser()) raw.add(Path.home().name) full = run(["id", "-F"]) # macOS: registered full name Terms are searched as UTF-8 and as UTF-16, because Office files and some binaries store names that way.
4.3 Archives: open them all the way down
def scan_archive(self, where, data, depth): with zipfile.ZipFile(io.BytesIO(data)) as zf: for info in zf.infolist(): self.scan_bytes(f"{where}!/{info.filename}", zf.read(info), depth + 1) scan_bytes calls scan_archive again when a member is itself a ZIP. .docx and .xlsx are ZIPs too, so creator fields are read through the same path.
4.4 Images: walking segments is enough
Walk the JPEG segments; an APP1 segment that starts with Exif means EXIF is present, and tag 0x8825 inside it means GPS data. No imaging library needed.
4.5 Hooks
# .git/hooks/pre-commit exec python3 gatekeeper/zero_trace_check.py --staged --identity # .git/hooks/pre-push exec python3 gatekeeper/zero_trace_check.py --all --identity --history --remote --path dist Pre-commit scans exactly what is staged. Pre-push scans the whole tree, commit authors and messages, the remote URL, and the git-ignored folder where distributable ZIPs are built. The same check runs as the first job in GitHub Actions; publishing jobs depend on it.
5. Verification
A checker grading itself proves nothing, so a separate program builds a throw-away corpus of planted leaks and clean look-alikes and runs the gate over it. Planted values are assembled at run time, so the probe itself passes the gate.
| Item | Result |
|---|---|
| Files with a planted leak | 21 of 21 detected |
| Clean look-alikes | 0 false positives out of 12 |
| Time | 6.9 ms (Python 3.14, Apple Silicon Mac, single run) |
The planted set: absolute paths for three operating systems, four kinds of key and private key, an e-mail address, a hard-coded password, a JPEG with EXIF and GPS, a PNG with a text chunk, .DS_Store, .env, a PDF with an author, a ZIP containing a path, a ZIP inside a ZIP containing a key, a ZIP with __MACOSX, a truncated ZIP, a .docx with a creator, a binary holding a private term in UTF-16, and a file name containing a private term.
The clean set includes placeholder paths like /Users/you/, actions/checkout@v4, logo@2x.png, example.com addresses, a URL containing /home/, and code that only reads credentials from the environment.
The raw result is published as is: https://zero-trace-ship-kit.zeus-lab-global.workers.dev/evidence/selftest.json
6. Limits
- Free-form information is not detected. A real name or address in prose is only caught if it is a term read from the machine or one you added to the local deny-list.
- Key detection covers common formats only.
- Text visible inside an image is not read.
- "Zero findings" means none of the shapes this gate knows are present. It is not a proof of safety.
Summary
- Agent cost follows
S·B + A·S·(S-1)/2. Steps and per-step growth are what matter. - To stop spending, price the worst case before the call and refuse. Alerts do not stop anything.
- Stop personal traces at commit and push, not with attention. Open archives all the way down. Read your own name at run time instead of storing it.
The browser scanner and a lite command-line report for text files are free: https://zero-trace-ship-kit.zeus-lab-global.workers.dev/
The full kit adds the complete gate that blocks commit and push, a packager that builds clean ZIPs, the cost guard, the publishing pipeline and a commercial license: https://zeuslab.gumroad.com/l/zero-trace-ship-kit


