Torn-paper collage on an ultramarine field. A ragged black halftone band runs diagonally from lower left to upper right, behind an amber prescription vial with a ribbed yellow cap, tilted slightly off vertical. The vial's white label reads REDACTED five times over, in heavy black condensed capitals. CHEAP is set large in white at the upper left, SHOT at the lower right. A small yellow box in the top right corner reads ALL CAPS RESEARCH & DESIGN LLC.

The Bottle on the Counter

A prescription label went up in a photo I uploaded for an unrelated reason. Post-hoc filtering leaves you trusting a retention policy. Pre-upload stripping leaves nothing to trust, because nothing moved. So I built the tool that way.

A wide countertop photo, uploaded to a chat assistant for an unrelated reason: identifying pantry contents for a kitchen-inventory idea.

Somewhere in the frame, a prescription bottle. Label legible.

Nobody intended to share it. It was in the shot because it was on the counter, which is where it lives.

The sequence, precisely

  1. Photo captured on a phone. EXIF written: GPS coordinates, timestamp, device identifier.
  2. Photo uploaded. Full frame, full metadata, unmodified.
  3. A server-side model reads the image — the beans, the detergent, the prescription.
  4. Only now does anything resembling privacy handling occur.

By step 3 the exposure has already happened. Filtering after transmission is redaction of an artifact that already moved. What is left is a trust relationship with someone else’s retention policy, which is a legal instrument, not a technical guarantee.

Deletion is real. But deletion is a remedy, not a protection.

Post-hoc filtering: the data moved, then got handled carefully. You are trusting a policy.

Pre-upload stripping: the data never moved. There is nothing to trust, because there is nothing there.

Why this is a design problem and not a user problem

The person who uploaded it reads scopes. Understands OAuth grants. Knows what server-side means.

He uploaded it anyway.

Not through ignorance — through ordinary attention economics. He was thinking about beans. The bottle was in the room the way a coffee maker is in the room.

Any design whose safety depends on the user noticing has already failed, and it fails hardest on informed users, because they are the ones moving fast and confident.

So the tool works the other way around

cheapshot does OCR through Apple’s Vision framework, which has shipped on every Mac since 2019. The text goes into the session. The image never leaves the machine.

That is not a feature I bolted on. It is the entire reason the thing exists. A screenshot pasted into an agent session is words rendered as pixels, sent somewhere to be turned back into words — and everything else in the frame goes with them.

Because the extracted text still reaches a model, it redacts before handing anything over: keys, card numbers, account numbers. On by default.

Then it did its own version of the same thing

I fed it a ten-minute screen recording and read the output against the screen it came from. One line was wrong:

…516a0d4d[BANK_ACCT]af6df3d8…

That is a SHA-256 with a bite taken out of the middle. Eight digits — 02126931 — replaced by a label claiming they were a bank account number. The full string was 96817c4d87e7c5bf7854371cbc2353516a0d4d02126931af6df3d875061ef4d6, and a hash with a hole in it is worth exactly nothing.

The rule that did it:

(?<![\d\-/])\d{8,}(?![\d\-/])

The lookarounds exclude digits. They do not exclude letters. A hex hash is digits and letters, so any run of eight or more digits fenced by two hex characters reads as a free-standing account number.

Worse, in the same output, a path had become [TOKEN]. The catch-all secret rule included / and . in its character class and fired on any twenty-plus-character mixed-case run — which describes every absolute path on macOS. It had been doing that the whole time.

I only caught it because one surviving path was truncated three characters under the threshold, and seeing one path intact beside one path missing is what made me look.

A redactor that fires too often does not fail loudly. It hands you a clean-looking document with the load-bearing parts quietly removed.

The fix was not “redact less”

Loosening the rules trades a false-positive problem for a false-negative one, and a false negative here is a leaked key.

What the catch-all needed was a way to know it was guessing. Every secret with a recognizable prefix — AKIA, ghp_, sk-, xoxb-, eyJ — already has a dedicated rule ahead of it, so the catch-all only ever handles secrets with no shape at all. It can afford a high bar.

So I measured entropy over real input:

bits/char
/Users/ryan/local-dev/dotfiles3.90
CleanShot-2026-09-02-at-13.50.19.mp44.14
— threshold: 4.4 —
wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY4.66
AbCd3fGh1jKlM9nOpQrS7uVwXyZ04.81

Paths and filenames top out at 4.14. Prefix-less secrets start at 4.66. The gap is real because the properties are: a path is words, and words are low-entropy.

Same lesson as the bottle, one level down. The question was never whether to automate. It was which part still needs a human, and how the machine signals that it is unsure. My redactor had no way to be unsure. It matched or it didn’t, and when it matched it destroyed.

What should ship, and not as a setting

Metadata first, because there is no excuse for that tier. EXIF carries GPS, timestamp, device ID. It is deterministic, free to remove, and worth nothing to a model identifying chickpeas. Strip it client-side, every image, no toggle. Keep the orientation tag, drop the rest.

Pixels are harder and still tractable. Both mobile platforms already ship fast on-device text detection. Detect regions locally, classify against high-risk layouts — pharmacy labels, name-and-address blocks, card numbers, ID documents — and show what was found before the upload fires.

The interaction is the actual contribution, because every privacy UI fails the same way: it blocks, so people learn to dismiss it.

  1. Never modal. A dialog between someone and their task gets tapped through unread inside a week.
  2. Show, don’t ask. Redaction boxes drawn on the thumbnail. Glanceable, ignorable.
  3. Default to protected. The safe state takes no action. The unsafe one takes a deliberate tap.
  4. Undo, not confirm. One tap to un-redact. Reversal is cheap; interrogation is expensive.
  5. Silent when clean. No “0 items found” toast. Most photos are fine, and noise on the common path destroys signal on the rare one.

Cost to the user in the common case: zero taps, zero dialogs, nothing to read.

Where this stops being theory

The kitchen-inventory idea has this as a core requirement, not an edge case. It asks people to photograph the rooms they live in. Counters, cabinets, fridges. Mail, medication, mounted screens, and other people are in those frames by default.

A privacy paragraph in a signup flow is not a response to that. The guarantee has to be architectural: non-food objects are dropped on-device, before upload, and the crop of the prescription bottle never leaves the phone.

The demo is showing the counter photo, and showing the app return nothing for that object. Not blurred. Not flagged. Not redacted after the fact. Absent, because it was never sent.

Then showing the version that already happened — the same photo, uploaded whole, read whole. Not a hypothetical failure. A real one, and the person it happened to knew better.

Be honest about the limits

False negatives are certain. Handwriting, unusual layouts, non-Latin scripts, partial occlusion. This raises the floor. It does not close the hole, and it should not be marketed as a guarantee.

Sometimes the label is the point. “What is this medication?” is a legitimate request. Redaction has to be reversible in one tap, and the model should be able to say something is covered rather than silently receiving a hole.

Detection runs on the unstripped image. On-device, never transmitted, discarded after — but the client does momentarily process the sensitive pixels. That is an enormous improvement over transmitting them, and the claim should be stated that precisely rather than overclaimed.

OCR mangles exactly what secrets are made of. In my own test, a planted credential came back with Cyrillic homoglyphs substituted — А, М, Х, р — plus l1 and O0. The substitutions break the character run a secret rule needs. Redaction over OCR text is best-effort and cannot be a guarantee. Saying so is more useful than shipping one that isn’t.


cheapshot is public and licensed. So is proton-drive-backup, which came out of the same instinct: its README documents what actually happens when you kill an upload mid-flight, verified by reading the vendor’s SDK source rather than its marketing.