{"slug": "i-built-a-privacy-layer-for-ai-coding-tools-codemask-ui-and-codemask-proxy", "title": "I Built a Privacy Layer for AI Coding Tools — CodeMask UI and CodeMask Proxy", "summary": "A developer built CodeMask UI and CodeMask Proxy, two open-source tools that sanitize sensitive data before it is sent to AI coding assistants. CodeMask UI is a browser-based tool that replaces secrets with placeholders and restores them after the AI responds, while CodeMask Proxy intercepts API calls from AI coding agents to redact sensitive information in real time. The tools address the risk of developers inadvertently exposing credentials and internal data when using AI chat tools and coding agents.", "body_md": "I was debugging a config file of my personal project and pasted it straight into Claude to get help. A few seconds later I looked at what I had actually sent: internal IP addresses, a database password, and an API key. All of it. To a cloud API. Without thinking.\n\nI don't think I am the only one who has done this. Most developers use AI assistants daily, and most of the time the code going in contains things it shouldn't. This post is about the two tools I built to fix that — and what I learned building them.\n\nBefore building anything, I realized the problem has two distinct shapes depending on how you use AI.\n\n**Workflow 1 — Manual AI chat (ChatGPT, Claude.ai, Gemini)**\n\nYou copy a file, paste it into the chat, ask your question, copy the response. The sensitive data goes in with the code because you are doing the pasting manually and there is no gate between your clipboard and the AI.\n\n**Workflow 2 — AI coding agents (Cline, Claude Code, Cursor)**\n\nThese tools read your files automatically. When you ask Cline to explain a function, it reads the file, builds a prompt, and calls the LLM API directly. There is no paste step at all. Your secrets go out in the background with every request without you ever touching them.\n\nSame root problem, completely different mechanism. Each needed a different solution.\n\n**What it is:** A browser-based tool that sanitizes code before you share it with any AI chat, and restores real values after the AI responds.\n\n**How it works:** Two-way flow with a session registry.\n\nWhen you paste code in and click Sanitize, the tool scans for secrets using regex patterns and replaces each one with a numbered placeholder:\n\n```\nDB_HOST = \"192.168.50.100\"     →    DB_HOST = \"{{IP_1}}\"\nDB_PASSWORD = \"SuperSecret123\" →    DB_PASSWORD = \"{{PASSWORD_1}}\"\nAPI_KEY = \"sk-test-abc123...\"  →    API_KEY = \"{{OPENAI_KEY_1}}\"\n```\n\nA registry is built in the background mapping every placeholder to its real value. You copy the sanitized code, paste it into Claude or ChatGPT, and ask your question. When the AI responds with suggestions that reference `{{IP_1}}`\n\n, you paste that response back into the Restore tab. The tool looks up the registry and swaps every placeholder back to the real value before you see it.\n\nThe registry persists in `localStorage`\n\nso it survives a page refresh. If you sanitize three different code files in one session, the same IP always gets the same placeholder, and all three can be restored from the same registry.\n\n**What it detects automatically:**\n\n| Category | Examples |\n|---|---|\n| IPv4 / CIDR |\n`10.10.10.10` , `192.168.1.0/24`\n|\n| IPv6 | Full and compressed formats |\n| AWS Keys |\n`AKIA...` , `AWS_SECRET_ACCESS_KEY`\n|\n| OpenAI Keys |\n`sk-...` , `sk-proj-...` , `sk_test_...`\n|\n| Anthropic Keys | `sk-ant-...` |\n| GitHub Tokens |\n`ghp_...` , `gho_...`\n|\n| GitLab Tokens | `glpat-...` |\n| Slack Tokens |\n`xoxb-...` , `xoxp-...`\n|\n| Bearer Tokens | `Authorization: Bearer ...` |\n| Generic API Keys |\n`api_key = \"...\"` , `token: \"...\"`\n|\n| JWT / Signing Keys | `jwt_signing_key = \"...\"` |\n| Passwords (quoted) | `password = \"secret\"` |\n| Passwords (unquoted) |\n`PASSWORD=Hello123` , `db_password=secret`\n|\n| DB Connection Strings | `postgres://user:pass@host` |\n| PEM Private Keys |\n`-----BEGIN PRIVATE KEY-----` blocks |\n\n**Architecture — nothing leaves the browser:**\n\nThis is built with React and Vite. There is no backend, no server, no API calls. The entire sanitize and restore logic runs client-side in the browser. Your secrets never travel anywhere — not even to a server I control. The `patterns.js`\n\nfile holds all the detection regexes and `sanitizer.js`\n\nhandles the replace and restore logic. Both are plain JavaScript, readable, and extendable.\n\n```\ncodemask/\n├── src/\n│   ├── engine/\n│   │   ├── patterns.js       ← detection regexes (add your own here)\n│   │   └── sanitizer.js      ← sanitize + restore logic\n│   ├── hooks/\n│   │   ├── useRegistry.js    ← session state + localStorage persistence\n│   │   └── useCopy.js        ← clipboard hook\n│   ├── components/\n│   │   ├── CodePanel.jsx     ← input/output panels\n│   │   ├── RegistryTable.jsx ← secrets table with blur/reveal\n│   │   └── ManualAdd.jsx     ← manual secret registration\n│   └── App.jsx\n```\n\n**Running it locally:**\n\n```\ngit clone https://github.com/shubham-singhS2/CodeMask.git\ncd CodeMask\nnpm install\nnpm run dev\n# Open http://localhost:5173\n```\n\nOr with Docker:\n\n```\ndocker run -d -p 8080:80 shubhamsinghs2/codemask:latest\n# Open http://localhost:8080\n```\n\n**One thing worth mentioning:** CIDR notation is handled properly. `192.168.1.0/24`\n\nbecomes `{{IP_1}}/24`\n\nnot `{{IP_1}}`\n\n— the prefix is preserved so the AI still understands it is a network range, not just a host address.\n\n**The problem with the UI tool for agent workflows:**\n\nWhen you use Cline or Claude Code, the agent reads your project files and sends them to the LLM API automatically. There is no paste step, so there is no place to intercept manually. By the time you see any output, your secrets have already been sent.\n\nThe fix is a proxy server.\n\n**What it is:** A local Node.js/Express server that runs on `localhost:8080`\n\nand acts as a drop-in replacement for any OpenAI-compatible LLM API endpoint.\n\nYou change one setting in your AI agent — point the base URL to `localhost:8080`\n\ninstead of `api.openai.com`\n\nor `api.mistral.ai`\n\n. The agent never knows the difference. Every request passes through the proxy first.\n\n**The complete flow:**\n\n```\nAI Agent (Cline)\n      │\n      │  POST /v1/chat/completions\n      │  { messages: [{ content: \"DB=192.168.1.10 password=secret\" }] }\n      ▼\nCodeMask Proxy (localhost:8080)\n      │  scans all message content for secrets\n      │  builds session registry\n      │  replaces real values with placeholders\n      │\n      │  POST /v1/chat/completions  ← forwarded to real API\n      │  { messages: [{ content: \"DB={{IP_1}} password={{PASSWORD_1}}\" }] }\n      ▼\nReal LLM API (Mistral / OpenAI / your org LLM)\n      │\n      │  response: \"The config connects to {{IP_1}} using {{PASSWORD_1}}\"\n      ▼\nCodeMask Proxy\n      │  scans response for placeholders\n      │  restores real values from session registry\n      │\n      │  response: \"The config connects to 192.168.1.10 using secret\"\n      ▼\nAI Agent — receives real values, works normally\n```\n\n**Proof from a real test:**\n\nI asked Cline to explain a `config.py`\n\nfile containing real IPs and passwords while the proxy was running. This is what the proxy logged as going to Mistral:\n\n```\n[VERIFY] ── What proxy sent to LLM ──────────────────\n[read_file for 'config.py'] Result:\n1 | DB_HOST = \"{{IP_1}}\"\n2 | DB_PASSWORD = \"{{PASSWORD_1}}\"\n3 | API_KEY = \"{{OPENAI_KEY_1}}\"\n[VERIFY] ─────────────────────────────────────────────\n```\n\nMistral never saw a single real value. Cline received the response with real values fully restored and worked normally.\n\n**Session management — in-memory with TTL:**\n\nThe registry that maps `{{IP_1}}`\n\nback to `192.168.1.10`\n\nlives in server memory, not a database or file. Each agent gets a session (derived from its API key), and sessions expire automatically after 60 minutes of inactivity. A cleanup timer runs every 10 minutes. If the proxy restarts, sessions clear — which is intentional. There is no sensitive data persisted anywhere on disk.\n\n```\n// Each session holds:\n{\n  id: \"auto-abc123\",\n  registry: [\n    { placeholder: \"{{IP_1}}\", realValue: \"192.168.1.10\", type: \"ip\" },\n    { placeholder: \"{{PASSWORD_1}}\", realValue: \"secret\", type: \"pass\" }\n  ],\n  counters: { ip: 1, key: 0, pass: 1 },\n  lastUsed: Date.now(),\n  expiresInSec: 3450\n}\n```\n\n**Streaming — the tricky part:**\n\nLLM APIs stream responses as Server-Sent Events (SSE). Each chunk is a small JSON object with a few tokens of content. The challenge: a placeholder like `{{IP_1}}`\n\ncan arrive split across two chunks:\n\n```\nChunk 1: \"connect to {{IP\"\nChunk 2: \"_1}} and use port 5432\"\n```\n\nPer-chunk restoration fails because neither chunk contains the complete placeholder. The fix: buffer the entire stream from the LLM, restore placeholders on the complete assembled text, then re-emit the restored content as fresh SSE chunks back to the agent. The agent gets a streaming response. The placeholders are restored correctly. Both requirements satisfied.\n\n**Running it:**\n\n```\ngit clone https://github.com/shubham-singhS2/codemask-proxy.git\ncd codemask-proxy\nnpm install\ncp .env.example .env\nnpm start\n# Proxy running at http://localhost:8080\n```\n\nOr with Docker (recommended for daily use — runs in background, restarts on reboot):\n\n```\ndocker run -d \\\n  --name codemask-proxy \\\n  --restart always \\\n  -p 8080:8080 \\\n  -e OPENAI_TARGET_URL=https://api.mistral.ai/v1 \\\n  shubhamsinghs2/codemask-proxy:latest\n```\n\n**Configuring your agent (Cline example):**\n\n```\nAPI Provider: OpenAI Compatible\nBase URL:     http://localhost:8080\nAPI Key:      your-real-api-key  ← forwarded transparently\nModel:        mistral-small-latest\n```\n\nThat is the entire setup. One config change and every request is protected.\n\n**Monitoring — endpoints and dashboard:**\n\nThe proxy exposes management endpoints so you can inspect what is happening:\n\n```\n# Health + global stats\ncurl http://localhost:8080/status | jq\n\n# Active sessions\ncurl http://localhost:8080/sessions | jq\n\n# Full registry for one session (values partially masked in output)\ncurl http://localhost:8080/session/SESSION_ID | jq\n\n# Last 50 requests\ncurl http://localhost:8080/logs | jq\n\n# Clear a session\ncurl -X DELETE http://localhost:8080/session/SESSION_ID\n```\n\nThere is also a browser dashboard at `http://localhost:8080/dashboard`\n\nshowing stat cards, session table with expandable registry, and a request log — useful if you prefer a visual view over curl.\n\n**Provider compatibility:**\n\n| Provider | Works | Notes |\n|---|---|---|\n| OpenAI | ✅ | Standard format |\n| Mistral | ✅ | Verified with Cline |\n| Anthropic | ✅ |\n`/v1/messages` route |\n| Groq | ✅ | Set `OPENAI_TARGET_URL=https://api.groq.com/openai`\n|\n| Ollama | ✅ | Set `OPENAI_TARGET_URL=http://localhost:11434`\n|\n| Org / internal LLM | ✅ | Set `DISABLE_TLS_VERIFY=true` for self-signed certs |\n| AWS Bedrock | ❌ | SigV4 auth not supported yet |\n| Google Gemini | ❌ | Different format not supported yet |\n\n**The self-signed certificate case:**\n\nIf your organisation runs an internal LLM with a self-signed certificate, Node.js will refuse to connect by default. Rather than setting `NODE_TLS_REJECT_UNAUTHORIZED=0`\n\nin your terminal every time, you can set it once in `.env`\n\n:\n\n```\nDISABLE_TLS_VERIFY=true\n```\n\nThe proxy reads this at startup and sets the flag internally. You never have to think about it again.\n\nBoth tools can be tested completely without spending any API credits using a mock LLM server included in the proxy repo.\n\n```\n# Terminal 1 — start mock server (pretends to be Mistral/OpenAI)\nnode mock-llm.js\n\n# Terminal 2 — start proxy pointing at mock\n# Set OPENAI_TARGET_URL=http://localhost:9999 in .env\nnpm start\n\n# Terminal 3 — send a test request\ncurl -s -X POST http://localhost:8080/v1/chat/completions \\\n  -H \"Content-Type: application/json\" \\\n  -H \"x-api-key: fake-key\" \\\n  -d '{\n    \"model\": \"gpt-4\",\n    \"messages\": [{\n      \"role\": \"user\",\n      \"content\": \"DB=192.168.1.10 password=secret123 fix this\"\n    }]\n  }' | jq\n```\n\nThe mock server prints a security check for every request showing whether it received placeholders or real values:\n\n```\n🔍 Security Check:\n   Placeholders found : ✅ YES\n   Real IPs present   : ✅ NO\n   Real passwords     : ✅ NO\n```\n\nThey solve the same problem at different layers and are designed to be used together.\n\n| CodeMask UI | CodeMask Proxy | |\n|---|---|---|\nFor |\nManual AI chat (ChatGPT, Claude.ai) | AI coding agents (Cline, Claude Code) |\nTriggered by |\nYou, manually | Agent requests, automatically |\nStorage |\nBrowser localStorage | Server memory, TTL-based |\nRuns as |\nStatic site (Nginx/Vercel) | Node.js process or Docker container |\nSetup |\nOpen a URL | One docker run or npm start |\n\nBoth use the same detection engine — `patterns.js`\n\nand `sanitizer.js`\n\n— which is the core logic shared between them.\n\nBoth projects are deployed and working. The UI version is running on a K3s cluster deployed via ArgoCD using a GitOps pipeline — GitHub Actions builds a Docker image, pushes to Docker Hub, updates the Kubernetes manifests repo, and ArgoCD syncs the cluster automatically. That deployment story is worth a separate post.\n\nBoth are open source:\n\nIf you use AI coding tools and have not thought about what goes into those prompts, it is worth spending ten minutes on. The proxy setup takes less time than that.", "url": "https://wpnews.pro/news/i-built-a-privacy-layer-for-ai-coding-tools-codemask-ui-and-codemask-proxy", "canonical_source": "https://dev.to/shubhamdevops/i-built-a-privacy-layer-for-ai-coding-tools-codemask-ui-and-codemask-proxy-3ne6", "published_at": "2026-08-19 15:19:59+00:00", "updated_at": "2026-08-19 15:43:16.137612+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-safety", "ai-products"], "entities": ["CodeMask UI", "CodeMask Proxy", "Claude", "ChatGPT", "Cline", "Cursor", "OpenAI", "Anthropic"], "alternates": {"html": "https://wpnews.pro/news/i-built-a-privacy-layer-for-ai-coding-tools-codemask-ui-and-codemask-proxy", "markdown": "https://wpnews.pro/news/i-built-a-privacy-layer-for-ai-coding-tools-codemask-ui-and-codemask-proxy.md", "text": "https://wpnews.pro/news/i-built-a-privacy-layer-for-ai-coding-tools-codemask-ui-and-codemask-proxy.txt", "jsonld": "https://wpnews.pro/news/i-built-a-privacy-layer-for-ai-coding-tools-codemask-ui-and-codemask-proxy.jsonld"}}