<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shubham Singh</title>
    <description>The latest articles on DEV Community by Shubham Singh (@shubhamdevops).</description>
    <link>https://dev.to/shubhamdevops</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4058328%2F8cd75a48-d080-45a1-814d-a41965db5568.png</url>
      <title>DEV Community: Shubham Singh</title>
      <link>https://dev.to/shubhamdevops</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shubhamdevops"/>
    <language>en</language>
    <item>
      <title>I Built a Privacy Layer for AI Coding Tools — CodeMask UI and CodeMask Proxy</title>
      <dc:creator>Shubham Singh</dc:creator>
      <pubDate>Wed, 19 Aug 2026 15:19:59 +0000</pubDate>
      <link>https://dev.to/shubhamdevops/i-built-a-privacy-layer-for-ai-coding-tools-codemask-ui-and-codemask-proxy-3ne6</link>
      <guid>https://dev.to/shubhamdevops/i-built-a-privacy-layer-for-ai-coding-tools-codemask-ui-and-codemask-proxy-3ne6</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I 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.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Problem — Two Different Workflows
&lt;/h3&gt;

&lt;p&gt;Before building anything, I realized the problem has two distinct shapes depending on how you use AI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow 1 — Manual AI chat (ChatGPT, Claude.ai, Gemini)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow 2 — AI coding agents (Cline, Claude Code, Cursor)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These 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.&lt;/p&gt;

&lt;p&gt;Same root problem, completely different mechanism. Each needed a different solution.&lt;/p&gt;




&lt;h3&gt;
  
  
  Solution 1 — CodeMask UI
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; A browser-based tool that sanitizes code before you share it with any AI chat, and restores real values after the AI responds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; Two-way flow with a session registry.&lt;/p&gt;

&lt;p&gt;When you paste code in and click Sanitize, the tool scans for secrets using regex patterns and replaces each one with a numbered placeholder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_HOST = "192.168.50.100"     →    DB_HOST = "{{IP_1}}"
DB_PASSWORD = "SuperSecret123" →    DB_PASSWORD = "{{PASSWORD_1}}"
API_KEY = "sk-test-abc123..."  →    API_KEY = "{{OPENAI_KEY_1}}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 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 &lt;code&gt;{{IP_1}}&lt;/code&gt;, 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.&lt;/p&gt;

&lt;p&gt;The registry persists in &lt;code&gt;localStorage&lt;/code&gt; so 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it detects automatically:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IPv4 / CIDR&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;10.10.10.10&lt;/code&gt;, &lt;code&gt;192.168.1.0/24&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IPv6&lt;/td&gt;
&lt;td&gt;Full and compressed formats&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Keys&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;AKIA...&lt;/code&gt;, &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Keys&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sk-...&lt;/code&gt;, &lt;code&gt;sk-proj-...&lt;/code&gt;, &lt;code&gt;sk_test_...&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic Keys&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sk-ant-...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Tokens&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ghp_...&lt;/code&gt;, &lt;code&gt;gho_...&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitLab Tokens&lt;/td&gt;
&lt;td&gt;&lt;code&gt;glpat-...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slack Tokens&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;xoxb-...&lt;/code&gt;, &lt;code&gt;xoxp-...&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bearer Tokens&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Authorization: Bearer ...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generic API Keys&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;api_key = "..."&lt;/code&gt;, &lt;code&gt;token: "..."&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JWT / Signing Keys&lt;/td&gt;
&lt;td&gt;&lt;code&gt;jwt_signing_key = "..."&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passwords (quoted)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;password = "secret"&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Passwords (unquoted)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PASSWORD=Hello123&lt;/code&gt;, &lt;code&gt;db_password=secret&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DB Connection Strings&lt;/td&gt;
&lt;td&gt;&lt;code&gt;postgres://user:pass@host&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PEM Private Keys&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;-----BEGIN PRIVATE KEY-----&lt;/code&gt; blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Architecture — nothing leaves the browser:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This 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 &lt;code&gt;patterns.js&lt;/code&gt; file holds all the detection regexes and &lt;code&gt;sanitizer.js&lt;/code&gt; handles the replace and restore logic. Both are plain JavaScript, readable, and extendable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;codemask/
├── src/
│   ├── engine/
│   │   ├── patterns.js       ← detection regexes (add your own here)
│   │   └── sanitizer.js      ← sanitize + restore logic
│   ├── hooks/
│   │   ├── useRegistry.js    ← session state + localStorage persistence
│   │   └── useCopy.js        ← clipboard hook
│   ├── components/
│   │   ├── CodePanel.jsx     ← input/output panels
│   │   ├── RegistryTable.jsx ← secrets table with blur/reveal
│   │   └── ManualAdd.jsx     ← manual secret registration
│   └── App.jsx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Running it locally:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/shubham-singhS2/CodeMask.git
&lt;span class="nb"&gt;cd &lt;/span&gt;CodeMask
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;span class="c"&gt;# Open http://localhost:5173&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or with Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:80 shubhamsinghs2/codemask:latest
&lt;span class="c"&gt;# Open http://localhost:8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;One thing worth mentioning:&lt;/strong&gt; CIDR notation is handled properly. &lt;code&gt;192.168.1.0/24&lt;/code&gt; becomes &lt;code&gt;{{IP_1}}/24&lt;/code&gt; not &lt;code&gt;{{IP_1}}&lt;/code&gt; — the prefix is preserved so the AI still understands it is a network range, not just a host address.&lt;/p&gt;




&lt;h3&gt;
  
  
  Solution 2 — CodeMask Proxy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The problem with the UI tool for agent workflows:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When 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.&lt;/p&gt;

&lt;p&gt;The fix is a proxy server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; A local Node.js/Express server that runs on &lt;code&gt;localhost:8080&lt;/code&gt; and acts as a drop-in replacement for any OpenAI-compatible LLM API endpoint.&lt;/p&gt;

&lt;p&gt;You change one setting in your AI agent — point the base URL to &lt;code&gt;localhost:8080&lt;/code&gt; instead of &lt;code&gt;api.openai.com&lt;/code&gt; or &lt;code&gt;api.mistral.ai&lt;/code&gt;. The agent never knows the difference. Every request passes through the proxy first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The complete flow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI Agent (Cline)
      │
      │  POST /v1/chat/completions
      │  { messages: [{ content: "DB=192.168.1.10 password=secret" }] }
      ▼
CodeMask Proxy (localhost:8080)
      │  scans all message content for secrets
      │  builds session registry
      │  replaces real values with placeholders
      │
      │  POST /v1/chat/completions  ← forwarded to real API
      │  { messages: [{ content: "DB={{IP_1}} password={{PASSWORD_1}}" }] }
      ▼
Real LLM API (Mistral / OpenAI / your org LLM)
      │
      │  response: "The config connects to {{IP_1}} using {{PASSWORD_1}}"
      ▼
CodeMask Proxy
      │  scans response for placeholders
      │  restores real values from session registry
      │
      │  response: "The config connects to 192.168.1.10 using secret"
      ▼
AI Agent — receives real values, works normally
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Proof from a real test:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I asked Cline to explain a &lt;code&gt;config.py&lt;/code&gt; file containing real IPs and passwords while the proxy was running. This is what the proxy logged as going to Mistral:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[VERIFY] ── What proxy sent to LLM ──────────────────
[read_file for 'config.py'] Result:
1 | DB_HOST = "{{IP_1}}"
2 | DB_PASSWORD = "{{PASSWORD_1}}"
3 | API_KEY = "{{OPENAI_KEY_1}}"
[VERIFY] ─────────────────────────────────────────────
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mistral never saw a single real value. Cline received the response with real values fully restored and worked normally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session management — in-memory with TTL:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The registry that maps &lt;code&gt;{{IP_1}}&lt;/code&gt; back to &lt;code&gt;192.168.1.10&lt;/code&gt; lives 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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Each session holds:&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;auto-abc123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{IP_1}}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;realValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;192.168.1.10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{PASSWORD_1}}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;realValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;secret&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pass&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;counters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;lastUsed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nx"&gt;expiresInSec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3450&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Streaming — the tricky part:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;LLM 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 &lt;code&gt;{{IP_1}}&lt;/code&gt; can arrive split across two chunks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Chunk 1: "connect to {{IP"
Chunk 2: "_1}} and use port 5432"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Per-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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running it:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/shubham-singhS2/codemask-proxy.git
&lt;span class="nb"&gt;cd &lt;/span&gt;codemask-proxy
npm &lt;span class="nb"&gt;install
cp&lt;/span&gt; .env.example .env
npm start
&lt;span class="c"&gt;# Proxy running at http://localhost:8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or with Docker (recommended for daily use — runs in background, restarts on reboot):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; codemask-proxy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--restart&lt;/span&gt; always &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:8080 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;OPENAI_TARGET_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://api.mistral.ai/v1 &lt;span class="se"&gt;\&lt;/span&gt;
  shubhamsinghs2/codemask-proxy:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configuring your agent (Cline example):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;API&lt;/span&gt; &lt;span class="py"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OpenAI Compatible&lt;/span&gt;
&lt;span class="err"&gt;Base&lt;/span&gt; &lt;span class="py"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;http://localhost:8080&lt;/span&gt;
&lt;span class="err"&gt;API&lt;/span&gt; &lt;span class="py"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="s"&gt;your-real-api-key  ← forwarded transparently&lt;/span&gt;
&lt;span class="py"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="s"&gt;mistral-small-latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire setup. One config change and every request is protected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring — endpoints and dashboard:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The proxy exposes management endpoints so you can inspect what is happening:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Health + global stats&lt;/span&gt;
curl http://localhost:8080/status | jq

&lt;span class="c"&gt;# Active sessions&lt;/span&gt;
curl http://localhost:8080/sessions | jq

&lt;span class="c"&gt;# Full registry for one session (values partially masked in output)&lt;/span&gt;
curl http://localhost:8080/session/SESSION_ID | jq

&lt;span class="c"&gt;# Last 50 requests&lt;/span&gt;
curl http://localhost:8080/logs | jq

&lt;span class="c"&gt;# Clear a session&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE http://localhost:8080/session/SESSION_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a browser dashboard at &lt;code&gt;http://localhost:8080/dashboard&lt;/code&gt; showing stat cards, session table with expandable registry, and a request log — useful if you prefer a visual view over curl.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Provider compatibility:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Works&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Standard format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mistral&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Verified with Cline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anthropic&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/v1/messages&lt;/code&gt; route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groq&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;OPENAI_TARGET_URL=https://api.groq.com/openai&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ollama&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;OPENAI_TARGET_URL=http://localhost:11434&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Org / internal LLM&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;DISABLE_TLS_VERIFY=true&lt;/code&gt; for self-signed certs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS Bedrock&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;SigV4 auth not supported yet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Gemini&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Different format not supported yet&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The self-signed certificate case:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your organisation runs an internal LLM with a self-signed certificate, Node.js will refuse to connect by default. Rather than setting &lt;code&gt;NODE_TLS_REJECT_UNAUTHORIZED=0&lt;/code&gt; in your terminal every time, you can set it once in &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DISABLE_TLS_VERIFY=true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proxy reads this at startup and sets the flag internally. You never have to think about it again.&lt;/p&gt;




&lt;h3&gt;
  
  
  Testing Without a Real API Key
&lt;/h3&gt;

&lt;p&gt;Both tools can be tested completely without spending any API credits using a mock LLM server included in the proxy repo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal 1 — start mock server (pretends to be Mistral/OpenAI)&lt;/span&gt;
node mock-llm.js

&lt;span class="c"&gt;# Terminal 2 — start proxy pointing at mock&lt;/span&gt;
&lt;span class="c"&gt;# Set OPENAI_TARGET_URL=http://localhost:9999 in .env&lt;/span&gt;
npm start

&lt;span class="c"&gt;# Terminal 3 — send a test request&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/v1/chat/completions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-api-key: fake-key"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "model": "gpt-4",
    "messages": [{
      "role": "user",
      "content": "DB=192.168.1.10 password=secret123 fix this"
    }]
  }'&lt;/span&gt; | jq
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mock server prints a security check for every request showing whether it received placeholders or real values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🔍 Security Check:
   Placeholders found : ✅ YES
   Real IPs present   : ✅ NO
   Real passwords     : ✅ NO
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  How the Two Tools Relate
&lt;/h3&gt;

&lt;p&gt;They solve the same problem at different layers and are designed to be used together.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;CodeMask UI&lt;/th&gt;
&lt;th&gt;CodeMask Proxy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;For&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual AI chat (ChatGPT, Claude.ai)&lt;/td&gt;
&lt;td&gt;AI coding agents (Cline, Claude Code)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Triggered by&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;You, manually&lt;/td&gt;
&lt;td&gt;Agent requests, automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Browser localStorage&lt;/td&gt;
&lt;td&gt;Server memory, TTL-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Runs as&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Static site (Nginx/Vercel)&lt;/td&gt;
&lt;td&gt;Node.js process or Docker container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Setup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open a URL&lt;/td&gt;
&lt;td&gt;One docker run or npm start&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both use the same detection engine — &lt;code&gt;patterns.js&lt;/code&gt; and &lt;code&gt;sanitizer.js&lt;/code&gt; — which is the core logic shared between them.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Is Next
&lt;/h3&gt;

&lt;p&gt;Both 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.&lt;/p&gt;

&lt;p&gt;Both are open source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CodeMask UI:&lt;/strong&gt; &lt;a href="https://github.com/shubham-singhS2/CodeMask" rel="noopener noreferrer"&gt;github.com/shubham-singhS2/CodeMask&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CodeMask Proxy:&lt;/strong&gt; &lt;a href="https://github.com/shubham-singhS2/codemask-proxy" rel="noopener noreferrer"&gt;github.com/shubham-singhS2/codemask-proxy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Hub (UI):&lt;/strong&gt; &lt;a href="https://hub.docker.com/r/shubhamsinghs2/codemask" rel="noopener noreferrer"&gt;hub.docker.com/r/shubhamsinghs2/codemask&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Hub (Proxy):&lt;/strong&gt; &lt;a href="https://hub.docker.com/r/shubhamsinghs2/codemask-proxy" rel="noopener noreferrer"&gt;hub.docker.com/r/shubhamsinghs2/codemask-proxy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If 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.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>claude</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Set Up a Free Custom Domain Email with Zoho Mail, Cloudflare, and Your Own Domain</title>
      <dc:creator>Shubham Singh</dc:creator>
      <pubDate>Sat, 01 Aug 2026 18:29:57 +0000</pubDate>
      <link>https://dev.to/shubhamdevops/how-to-set-up-a-free-custom-domain-email-with-zoho-mail-cloudflare-and-your-own-domain-5dpo</link>
      <guid>https://dev.to/shubhamdevops/how-to-set-up-a-free-custom-domain-email-with-zoho-mail-cloudflare-and-your-own-domain-5dpo</guid>
      <description>&lt;h3&gt;
  
  
  How to Set Up a Free Custom Domain Email with Zoho Mail, Cloudflare, and Your Own Domain
&lt;/h3&gt;

&lt;p&gt;A custom email address like &lt;code&gt;contact@yourdomain.com&lt;/code&gt; makes a huge difference when you are building a personal brand, portfolio, or freelance presence. It looks more professional than a free Gmail address, and it is surprisingly easy to set up using Zoho Mail’s free plan and your domain’s DNS.&lt;/p&gt;

&lt;p&gt;In this guide, I will walk through the exact flow I used to create a professional email address on a custom domain, without paying for a traditional business email suite.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; This post uses placeholder values instead of real DNS records, IPs, or credentials. Replace the examples with the values Zoho shows for your own account.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What you will need
&lt;/h2&gt;

&lt;p&gt;Before you begin, make sure you already have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A domain name (for example, &lt;code&gt;yourdomain.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;DNS access in Cloudflare or your domain provider&lt;/li&gt;
&lt;li&gt;A Zoho Mail account&lt;/li&gt;
&lt;li&gt;A few minutes to add DNS records and wait for propagation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this setup, I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website hosting:&lt;/strong&gt; Vercel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DNS:&lt;/strong&gt; Cloudflare&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email provider:&lt;/strong&gt; Zoho Mail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination works very well for a personal website or portfolio.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why use Zoho Mail?
&lt;/h2&gt;

&lt;p&gt;Zoho Mail is useful because it lets you create a professional email address using your own domain. On the free plan, Zoho supports a single domain with up to 5 users, 5 GB of storage per user, and web-only access. The free plan is available only in select data centers, so availability may vary by region.&lt;/p&gt;

&lt;p&gt;For a personal website, that is usually more than enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  The overall flow
&lt;/h2&gt;

&lt;p&gt;Here is the setup in simple terms:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Buy a domain.&lt;/li&gt;
&lt;li&gt;Sign up for Zoho Mail.&lt;/li&gt;
&lt;li&gt;Add your existing domain.&lt;/li&gt;
&lt;li&gt;Verify that you own the domain using a TXT record.&lt;/li&gt;
&lt;li&gt;Add MX records so mail is delivered to Zoho.&lt;/li&gt;
&lt;li&gt;Add SPF, DKIM, and DMARC for email authentication.&lt;/li&gt;
&lt;li&gt;Test sending and receiving mail.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 1: Sign up for Zoho Mail
&lt;/h2&gt;

&lt;p&gt;Go to Zoho Mail’s signup page and choose the free plan.&lt;/p&gt;

&lt;p&gt;During signup, Zoho will ask whether you want to add a new domain or an existing one. Since you already own the domain, choose &lt;strong&gt;Add an existing domain&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Enter your domain exactly as it should appear, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not type &lt;code&gt;www.yourdomain.com&lt;/code&gt; for email setup. Email should be configured on the root domain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Verify domain ownership
&lt;/h2&gt;

&lt;p&gt;Zoho will ask you to prove that you own the domain. The simplest method is to add a TXT record in Cloudflare DNS.&lt;/p&gt;

&lt;p&gt;Zoho will give you a TXT value similar to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;zoho-verification=zbXXXXXXXX.zmverify.zoho.in
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Cloudflare, add a new DNS record:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TXT&lt;/td&gt;
&lt;td&gt;@&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zoho-verification=zbXXXXXXXX.zmverify.zoho.in&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If Cloudflare does not accept &lt;code&gt;@&lt;/code&gt;, use the root domain name directly.&lt;/p&gt;

&lt;p&gt;After saving the record, go back to Zoho and click &lt;strong&gt;Verify TXT Record&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Once that works, Zoho knows the domain belongs to you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Create your mailbox
&lt;/h2&gt;

&lt;p&gt;After verification, Zoho will let you create the first user account for your domain.&lt;/p&gt;

&lt;p&gt;For a personal site, a good choice is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contact@yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also create:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hello@yourdomain.com
shubham@yourdomain.com
resume@yourdomain.com
jobs@yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only want one inbox, use aliases later instead of creating multiple mailboxes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Add MX records in Cloudflare
&lt;/h2&gt;

&lt;p&gt;MX records tell the internet where your email should be delivered.&lt;/p&gt;

&lt;p&gt;Without MX records, people can send mail to your domain name, but no one will know which server should receive it.&lt;/p&gt;

&lt;p&gt;Add the MX records Zoho provides for your data center. For Zoho’s India region, the records usually look like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Mail Server&lt;/th&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MX&lt;/td&gt;
&lt;td&gt;@&lt;/td&gt;
&lt;td&gt;mx.zoho.in&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MX&lt;/td&gt;
&lt;td&gt;@&lt;/td&gt;
&lt;td&gt;mx2.zoho.in&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MX&lt;/td&gt;
&lt;td&gt;@&lt;/td&gt;
&lt;td&gt;mx3.zoho.in&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In Cloudflare DNS:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Type: &lt;code&gt;MX&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;@&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Content: &lt;code&gt;mx.zoho.in&lt;/code&gt; / &lt;code&gt;mx2.zoho.in&lt;/code&gt; / &lt;code&gt;mx3.zoho.in&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Priority: &lt;code&gt;10&lt;/code&gt;, &lt;code&gt;20&lt;/code&gt;, &lt;code&gt;50&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not proxy MX records. Cloudflare handles them as DNS-only records.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Add SPF
&lt;/h2&gt;

&lt;p&gt;SPF tells the world which servers are allowed to send email for your domain.&lt;/p&gt;

&lt;p&gt;Add this TXT record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=spf1 include:zoho.in ~all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Cloudflare:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TXT&lt;/td&gt;
&lt;td&gt;@&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v=spf1 include:zoho.in ~all&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What SPF means
&lt;/h3&gt;

&lt;p&gt;This says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zoho is allowed to send mail for your domain.&lt;/li&gt;
&lt;li&gt;Other servers should not be trusted by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This helps reduce spoofing and improves inbox delivery.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Add DKIM
&lt;/h2&gt;

&lt;p&gt;DKIM adds a digital signature to outgoing email. It helps receivers confirm that the message really came from your domain and was not modified in transit.&lt;/p&gt;

&lt;p&gt;Zoho will generate a DKIM public key for you. The record usually looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TXT&lt;/td&gt;
&lt;td&gt;zmail._domainkey&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v=DKIM1; k=rsa; p=YOUR_LONG_PUBLIC_KEY_HERE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What to do
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Open Zoho Admin Console.&lt;/li&gt;
&lt;li&gt;Go to DKIM settings.&lt;/li&gt;
&lt;li&gt;Generate the DKIM record.&lt;/li&gt;
&lt;li&gt;Copy the full TXT value into Cloudflare.&lt;/li&gt;
&lt;li&gt;Save the record.&lt;/li&gt;
&lt;li&gt;Return to Zoho and verify it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;DKIM verification can take a little longer than SPF because DNS propagation and verification caches may not update instantly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Add DMARC
&lt;/h2&gt;

&lt;p&gt;DMARC is the policy layer that sits on top of SPF and DKIM.&lt;/p&gt;

&lt;p&gt;It tells receiving mail servers what to do when authentication fails.&lt;/p&gt;

&lt;p&gt;A good starting DMARC record for a personal domain is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v=DMARC1; p=quarantine; pct=100; adkim=r; aspf=r; rua=mailto:contact@yourdomain.com; ruf=mailto:contact@yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Cloudflare:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TXT&lt;/td&gt;
&lt;td&gt;_dmarc&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v=DMARC1; p=quarantine; pct=100; adkim=r; aspf=r; rua=mailto:contact@yourdomain.com; ruf=mailto:contact@yourdomain.com&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  What these parts mean
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;p=quarantine&lt;/code&gt; → suspicious messages should go to spam&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pct=100&lt;/code&gt; → apply this policy to all mail&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;adkim=r&lt;/code&gt; → relaxed DKIM alignment&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aspf=r&lt;/code&gt; → relaxed SPF alignment&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rua&lt;/code&gt; → aggregate reports&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ruf&lt;/code&gt; → forensic reports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most personal domains, relaxed alignment is the simplest and safest choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8: Wait for propagation
&lt;/h2&gt;

&lt;p&gt;After you add the records, the changes need time to spread across DNS resolvers.&lt;/p&gt;

&lt;p&gt;Usually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TXT records propagate quickly&lt;/li&gt;
&lt;li&gt;MX records may take a little longer&lt;/li&gt;
&lt;li&gt;Zoho’s dashboard may still show red or pending status even when everything is already working&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not panic if Zoho’s UI is slow to update.&lt;/p&gt;

&lt;p&gt;A good way to confirm is to test real mail flow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 9: Test sending and receiving
&lt;/h2&gt;

&lt;p&gt;Send a test email from your Zoho mailbox to Gmail.&lt;/p&gt;

&lt;p&gt;Then reply from Gmail back to your custom domain email.&lt;/p&gt;

&lt;p&gt;If both directions work, the setup is operational.&lt;/p&gt;

&lt;p&gt;You can also check the message headers in Gmail to confirm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SPF: pass&lt;/li&gt;
&lt;li&gt;DKIM: pass&lt;/li&gt;
&lt;li&gt;DMARC: pass&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the best real-world proof that your setup is correct.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common issues and fixes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Zoho still shows MX in red
&lt;/h3&gt;

&lt;p&gt;If mail is already working, the DNS is usually fine. Wait for propagation and check again later.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. SPF is green but DKIM is still pending
&lt;/h3&gt;

&lt;p&gt;That usually means the DKIM TXT record is correct but Zoho has not rechecked it yet, or the record was added recently.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Email is landing in spam
&lt;/h3&gt;

&lt;p&gt;Check all three:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SPF&lt;/li&gt;
&lt;li&gt;DKIM&lt;/li&gt;
&lt;li&gt;DMARC&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also make sure your domain is not sending from an unauthorized service.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Cloudflare says the record already exists
&lt;/h3&gt;

&lt;p&gt;Double-check for duplicates. You should have only one SPF TXT record and one DKIM TXT record for each selector.&lt;/p&gt;




&lt;h2&gt;
  
  
  Optional improvements
&lt;/h2&gt;

&lt;p&gt;Once the core setup works, you can make it even better:&lt;/p&gt;

&lt;h3&gt;
  
  
  Create aliases
&lt;/h3&gt;

&lt;p&gt;A single inbox can receive mail for multiple addresses such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contact@yourdomain.com
hello@yourdomain.com
resume@yourdomain.com
jobs@yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add a signature
&lt;/h3&gt;

&lt;p&gt;A clean signature makes every email look more professional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shubham Singh
DevOps / DevSecOps Engineer

Website: https://yourdomain.com
LinkedIn: linkedin.com/in/your-link
GitHub: github.com/your-handle
Email: contact@yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Use your custom email everywhere
&lt;/h3&gt;

&lt;p&gt;Replace your Gmail address on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portfolio website&lt;/li&gt;
&lt;li&gt;Resume&lt;/li&gt;
&lt;li&gt;LinkedIn contact info&lt;/li&gt;
&lt;li&gt;Job applications&lt;/li&gt;
&lt;li&gt;GitHub profile&lt;/li&gt;
&lt;li&gt;Freelance profiles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That keeps your brand consistent.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this setup gives you
&lt;/h2&gt;

&lt;p&gt;This setup is not end-to-end encrypted email, but it does give you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Professional custom email&lt;/li&gt;
&lt;li&gt;TLS encryption in transit&lt;/li&gt;
&lt;li&gt;SPF authentication&lt;/li&gt;
&lt;li&gt;DKIM signatures&lt;/li&gt;
&lt;li&gt;DMARC protection&lt;/li&gt;
&lt;li&gt;Better inbox deliverability&lt;/li&gt;
&lt;li&gt;A clean personal brand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a portfolio or professional website, this is exactly what you want.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final DNS record checklist
&lt;/h2&gt;

&lt;p&gt;Here is the full checklist in one place:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Record&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TXT&lt;/td&gt;
&lt;td&gt;Zoho domain verification&lt;/td&gt;
&lt;td&gt;&lt;code&gt;zoho-verification=zbXXXXXXXX.zmverify.zoho.in&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MX&lt;/td&gt;
&lt;td&gt;Incoming mail delivery&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mx.zoho.in&lt;/code&gt;, &lt;code&gt;mx2.zoho.in&lt;/code&gt;, &lt;code&gt;mx3.zoho.in&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TXT&lt;/td&gt;
&lt;td&gt;SPF&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v=spf1 include:zoho.in ~all&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TXT&lt;/td&gt;
&lt;td&gt;DKIM&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v=DKIM1; k=rsa; p=...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TXT&lt;/td&gt;
&lt;td&gt;DMARC&lt;/td&gt;
&lt;td&gt;&lt;code&gt;v=DMARC1; p=quarantine; ...&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;With a domain, Cloudflare DNS, and Zoho Mail’s free plan, you can build a fully professional email address without paying for a large business email suite.&lt;/p&gt;

&lt;p&gt;For a personal portfolio, that is one of the best low-cost upgrades you can make.&lt;/p&gt;

&lt;p&gt;It improves your credibility, looks cleaner on your website, and gives your project a more polished finish.&lt;/p&gt;

&lt;p&gt;``&lt;/p&gt;

</description>
      <category>customemail</category>
      <category>zoho</category>
    </item>
  </channel>
</rss>
