DEV Community

Dakota Huang
Dakota Huang

Posted on

Build a Local Redacting Proxy for Free Model APIs in Under 60 Lines

A single paste into a chat window can expose more than you intend. API keys, internal hostnames, and customer emails travel in the prompt. Free model endpoints may store or log that text. You cannot inspect their retention after the request leaves.

The safer pattern is to redact before you send. This tutorial builds a local HTTP proxy. It strips common secrets from outgoing JSON. It logs only a one-way hash of removed values.

MonkeyCode's free model access is one way to run this experiment without a paid account. Disclosure: This article was prepared as part of MonkeyCode's product outreach. The code works with any JSON chat-completion endpoint.

The problem

Chat windows make it easy to paste whole files. Those files often contain secrets. A free model endpoint is not your private vault. Its logs may outlive your session.

You need a boundary that you control. The boundary should act before the request leaves your machine.

What you build

You build two small Python files.

  1. proxy.py sits between your client and the model endpoint.
  2. mock_upstream.py pretends to be the endpoint for offline tests.

The proxy follows four steps.

  1. Read the JSON body.
  2. Redact matching text.
  3. Forward only the cleaned body.
  4. Return the upstream response unchanged.

Step 1: Define redaction rules

Start with rules for common tokens. Keep the rules explicit. Do not try to catch every possible secret.

Rule Pattern Replacement
API key sk-[A-Za-z0-9_-]{16,} [REDACTED_API_KEY]
JWT eyJ[A-Za-z0-9_-]{10,}[.][A-Za-z0-9_-]{10,}[.][A-Za-z0-9_-]{10,} [REDACTED_JWT]
Email [A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+[.][A-Za-z]{2,} [REDACTED_EMAIL]
AWS key AKIA[0-9A-Z]{16} [REDACTED_AWS_KEY]
Private key block -----BEGIN [A-Z ]*PRIVATE KEY----- [REDACTED_PRIVATE_KEY]

Add one rule for your own secret format. A narrow rule beats a broad one.

Step 2: Write the proxy

Create proxy.py.

import hashlib
import json
import os
import re
import urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer

UPSTREAM = os.environ.get('UPSTREAM_URL', 'http://127.0.0.1:9000/echo')
KEY = os.environ.get('UPSTREAM_API_KEY', '')
RULES = [
    ('sk-[A-Za-z0-9_-]{16,}', '[REDACTED_API_KEY]'),
    ('eyJ[A-Za-z0-9_-]{10,}[.][A-Za-z0-9_-]{10,}[.][A-Za-z0-9_-]{10,}', '[REDACTED_JWT]'),
    ('[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+[.][A-Za-z]{2,}', '[REDACTED_EMAIL]'),
    ('AKIA[0-9A-Z]{16}', '[REDACTED_AWS_KEY]'),
    ('-----BEGIN [A-Z ]*PRIVATE KEY-----', '[REDACTED_PRIVATE_KEY]'),
]

def redact(value):
    if isinstance(value, dict):
        return {k: redact(v) for k, v in value.items()}
    if isinstance(value, list):
        return [redact(item) for item in value]
    if isinstance(value, str):
        for pattern, replacement in RULES:
            match = re.search(pattern, value)
            if match:
                token = match.group(0)
                digest = hashlib.sha256(token.encode()).hexdigest()[:12]
                print('redacted ' + digest, flush=True)
                value = re.sub(pattern, replacement, value)
    return value

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get('content-length', '0'))
        raw = self.rfile.read(length)
        data = json.loads(raw.decode('utf-8'))
        cleaned = redact(data)
        req = urllib.request.Request(
            UPSTREAM,
            data=json.dumps(cleaned).encode('utf-8'),
            method='POST',
            headers={'content-type': 'application/json'},
        )
        if KEY:
            req.add_header('authorization', 'Bearer ' + KEY)
        with urllib.request.urlopen(req, timeout=30) as resp:
            body = resp.read()
        self.send_response(resp.status)
        self.send_header('content-type', 'application/json')
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, fmt, *args):
        pass

HTTPServer(('127.0.0.1', 8000), Handler).serve_forever()
Enter fullscreen mode Exit fullscreen mode

The proxy does not print the secret. It prints only a short hash. That hash is enough to confirm a rule fired.

Step 3: Add a mock upstream

Create mock_upstream.py.

import json
from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get('content-length', '0'))
        raw = self.rfile.read(length)
        print('UPSTREAM_GOT ' + raw.decode('utf-8'), flush=True)
        self.send_response(200)
        self.send_header('content-type', 'application/json')
        self.end_headers()
        payload = json.dumps({'choices': [{'message': {'content': 'ok'}}]}).encode('utf-8')
        self.wfile.write(payload)

HTTPServer(('127.0.0.1', 9000), Handler).serve_forever()
Enter fullscreen mode Exit fullscreen mode

This file prints exactly what the proxy would forward. Use it to verify redaction before you call a real provider.

Step 4: Run the verification

Start the mock upstream in one terminal.

python mock_upstream.py
Enter fullscreen mode Exit fullscreen mode

Start the proxy in a second terminal.

python proxy.py
Enter fullscreen mode Exit fullscreen mode

Send a fixture from a third terminal.

import json
import urllib.request

payload = {
    'model': 'example',
    'messages': [
        {'role': 'user', 'content': 'Fix sk-12345678901234567890 for dev@example.com'}
    ]
}
req = urllib.request.Request(
    'http://127.0.0.1:8000/',
    data=json.dumps(payload).encode('utf-8'),
    method='POST',
    headers={'content-type': 'application/json'},
)
print(urllib.request.urlopen(req).read().decode())
Enter fullscreen mode Exit fullscreen mode

Check the mock terminal. It should show [REDACTED_API_KEY] and [REDACTED_EMAIL]. It should not show sk-12345678901234567890 or dev@example.com.

The proxy terminal should show two short hashes. Those hashes are not reversible for high-entropy secrets.

Step 5: Point it at a real endpoint

Set the upstream variables and restart the proxy.

export UPSTREAM_URL='https://your-provider.example/v1/chat/completions'
export UPSTREAM_API_KEY='your-key-here'
python proxy.py
Enter fullscreen mode Exit fullscreen mode

MonkeyCode's free model access can be used as the upstream if the provider gives a standard chat-completion URL. The free server option may also host this script if it permits a long-running Python process. Verify current limits first.

Do not hardcode the key in source control. Use environment variables or a secret manager.

Limitations

Regex redaction is a best-effort filter. It will miss secrets in screenshots, PDFs, or base64 blobs. It will also miss unknown token formats.

The local proxy adds one network hop. It is not a replacement for access controls or data classification. Do not use this for regulated data such as health records or payment details.

Do not use this if your team requires approved DLP tooling. This tutorial is a small technical control, not a compliance program.

This version handles non-streaming JSON only. Streaming responses need a different transport path.

Who should not use this

Skip this if you upload files rather than text prompts. Skip it if the extra process is unacceptable. Skip it if you cannot maintain the regex rules over time.

A stale rule gives false confidence. The proxy is only as good as its rule list.

Summary

Redact at the boundary, not inside the prompt. A 60-line proxy stops obvious leaks before they reach an external API. Test with a mock upstream so you can see exactly what leaves your machine.

Run the mock test once. Then replace the upstream with an endpoint you already use. Add one rule that matches your own secret format.

Top comments (0)