How I build: Discourse Credential Scanner


Someone is stuck on an API call. They want help, so they paste in everything: the request, the response, and the bearer token in the header. It’s an easy mistake and a very human one. Then it sits on a public page until someone notices.

I spent five years at Stack Overflow, where I saw this a lot. Here is how it works there:

The human way

When a key shows up in a question, a user or curator spots it, edits it out, and raises a moderator flag. A moderator then redacts it from every earlier revision, and a second moderator approves the redaction. The Meta Stack Overflow guidance on this says to treat the key as compromised regardless, because posts get archived and copied elsewhere.

It’s a careful system, and I respect it. It also depends on someone noticing. Surprisingly, we never built any automated detection at Stack, despite how common it was.. If a mod or user had a script, it was their own and work they were doing quietly.

Redaction requests were common at Stack, they comm up weekly. That’s why I wanted to add some protection to our community. The crowd here is technical, which makes an accidental share more likely, not less, and that includes Tines employees. I wanted to spare anyone, member or colleague, a rough day. Discourse makes this easy enough, since it’s something of a spiritual successor to Stack Overflow and shares a founder in Jeff Atwood. I didn’t want to take the human out. I wanted to take the finding out of the human’s job and leave the decision with a person, mostly me.

The build

A Discourse webhook fires on every post created or edited. From there it’s a funnel:

  1. Fetch post. Pulls the live post and any screenshot URLs, then hashes the text plus image URLs. If the last scan of this post had the same hash, the run stops. A typo fix costs nothing.

  2. Pre-filter scan. Regex plus Shannon entropy, in Python. Entropy measures randomness: real keys look like noise, so they score high. A clean post with no images never reaches an AI model.

  3. AI review. Claude Haiku looks first and can see up to three screenshots, resized to 1568px. Sonnet is only called if Haiku says “leak”, isn’t confident, or returns something unreadable.

  4. Flag and hide. Confirmed leaks are hidden with staff take-action, which puts an item in Discourse’s review queue.

  5. Slack notify. Alerts go to an internal Slack channel, #community-moderation.

  6. Log run. Writes to a SQLite audit database.

Why it looks this way

The verdicts are not_confirmed, confirmed and needs_review. Medium and High confidence get hidden. Low confidence does not. It goes to Slack as “Not hidden, please check.” Hiding a real person’s post by mistake is worse than a short delay, so the scanner only acts when it’s fairly sure.

The scanner can’t edit or delete anything. It hides the post and hands it to a person. That’s the Stack Overflow flow with the detection step automated. Our audit log won’t ever store a secret or any credential. It stores detector type, author, URLs, confidence and state. It doesn’t store the credential or the raw post text. A tool that hunts for secrets shouldn’t also be the place they all end up, neatly collected in one database. I made failure quite loud. If the AI review fails, the scan is logged as scan_failed, posted to Slack, and retried on the next edit. That way someone on the community team can check on it and get the workflow fixed.

Closing the loop

There’s an audit dashboard covering the last 90 days, filterable by verdict and author. It also feeds my community management dashboard through a status API and an action API. I can approve or deny each finding there, and every decision records who made it and when.

If you run a Discourse or any other community platform, how are you catching leaked credentials today? Tell me in the replies. I’m also curious about the patterns the scanner might be missing.

Note: Content sent to the modes is covered by our model processing plan’s privacy protections and isn’t used for training.

4 Likes