<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Jahangir on Medium]]></title>
        <description><![CDATA[Stories by Jahangir on Medium]]></description>
        <link>https://medium.com/@jahangir80842?source=rss-1e1a07bda9c1------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/0*ixe9FH013Vrftjwl</url>
            <title>Stories by Jahangir on Medium</title>
            <link>https://medium.com/@jahangir80842?source=rss-1e1a07bda9c1------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Wed, 04 Nov 2026 18:39:53 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@jahangir80842/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com" rel="hub"/>
        <item>
            <title><![CDATA[The Anatomy of an AI Agent]]></title>
            <link>https://medium.com/@jahangir80842/the-anatomy-of-an-ai-agent-ce8add8d98ae?source=rss-1e1a07bda9c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/ce8add8d98ae</guid>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[linux]]></category>
            <dc:creator><![CDATA[Jahangir]]></dc:creator>
            <pubDate>Mon, 05 Oct 2026 05:36:01 GMT</pubDate>
            <atom:updated>2026-10-05T05:36:01.550Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/939/1*oHMSPpbVYR1mg-eztnwRxA.png" /></figure><h3>Your AI Agent Is Not Just an LLM: Think of It Like a Human Body 🧠⚡</h3><p>Most people hear <strong>“AI Agent”</strong> and immediately think:</p><blockquote><em>“It’s just an LLM with a prompt.</em></blockquote><p>That mental model is becoming outdated.</p><p>An LLM can understand language, reason, write code, and generate answers. But by itself, it is surprisingly limited.</p><p>It is like having a <strong>very intelligent brain sitting in a room with no books, no access to the outside world, no learned procedures, and no hands.</strong></p><p>The interesting part begins when we give that brain the rest of the body.</p><p>The image above provides a surprisingly useful way to understand the modern AI stack:</p><p><strong>LLM → RAG → MCP → Skills → Agent</strong></p><p>Think of them as:</p><p>🧠 <strong>Brain</strong><br>📚 <strong>Library</strong><br>🧬 <strong>Nervous system</strong><br>💪 <strong>Muscle memory</strong><br>🤖 <strong>Brain + Hands</strong></p><p>Let’s break it down.</p><h3>🧠 1. LLM = The Brain</h3><p>At the center is the <strong>Large Language Model</strong>.</p><p>Think of models such as GPT, Claude, Gemini, Llama, Qwen, etc.</p><p>The LLM provides the intelligence needed to:</p><ul><li>Understand language</li><li>Reason about problems</li><li>Generate text</li><li>Write code</li><li>Analyze information</li><li>Decide what to do next</li></ul><p>But there is an important limitation:</p><h3>An LLM only knows what is available to it.</h3><p>Suppose you ask:</p><blockquote><em>“What is the status of our production Kubernetes cluster?”</em></blockquote><p>Your LLM doesn’t magically know.</p><p>It needs access to:</p><ul><li>Kubernetes</li><li>Monitoring</li><li>Logs</li><li>Databases</li><li>APIs</li><li>Internal documentation</li></ul><p>Without those connections, it can only make an educated response based on its existing knowledge.</p><p>That’s where the next layer comes in.</p><h3>📚 2. RAG = The Brain + Library</h3><p><strong>RAG = Retrieval-Augmented Generation</strong></p><p>Imagine giving the brain access to a massive library.</p><p>Instead of asking:</p><blockquote><em>“What do you remember?”</em></blockquote><p>you ask:</p><blockquote><em>“Go find the relevant information first, then answer.”</em></blockquote><p>For example, your company has:</p><pre>company-docs/<br>├── HR/<br>├── DevOps/<br>├── Security/<br>├── SOPs/<br>├── Architecture/<br>└── Policies/</pre><p>A user asks:</p><blockquote><em>“What is our production deployment procedure?”</em></blockquote><p>A RAG system can:</p><pre>Question<br>   ↓<br>Search knowledge base<br>   ↓<br>Retrieve relevant documents<br>   ↓<br>Give context to LLM<br>   ↓<br>Generate answer</pre><p>This changes the LLM from:</p><p><strong>“I think this is the answer.”</strong></p><p>to:</p><p><strong>“Here is the answer based on your organization’s information.”</strong></p><p>That’s a huge difference.</p><h3>RAG gives AI access to knowledge.</h3><p>But knowledge isn’t action.</p><p>And that’s where things get really interesting.</p><h3>🧬 3. MCP = The Nervous System</h3><p>This is one of the most interesting parts of the diagram.</p><p><strong>MCP = Model Context Protocol</strong></p><p>A useful mental model is:</p><blockquote><strong><em>MCP provides a standardized way for AI models to interact with external tools and data.</em></strong></blockquote><p>Think about the human nervous system.</p><p>Your brain doesn’t directly become a database, camera, keyboard, or hand.</p><p>Instead, the nervous system provides pathways between the brain and the rest of the body.</p><p>Similarly, an AI system may need access to:</p><pre>         AI Agent<br>            │<br>           MCP<br>      ┌─────┼─────┐<br>      ↓     ↓     ↓<br>   GitHub  DB   Files<br>      ↓     ↓     ↓<br>   APIs   Cloud  Tools</pre><p>The important idea is <strong>standardization</strong>.</p><p>Instead of building a completely different integration mechanism for every AI application, MCP provides a common protocol for exposing tools and context.</p><p>For example, an agent might have tools such as:</p><pre>get_server_status()<br>query_database()<br>search_documents()<br>create_github_issue()<br>restart_service()<br>read_logs()</pre><p>Now the AI isn’t just talking.</p><p>It has pathways into the environment around it.</p><h3>💪 4. Skills = Muscle Memory</h3><p>Now imagine you’ve taught the AI how to perform a specific task repeatedly.</p><p>For example:</p><blockquote><em>“Deploy a Docker application safely.”</em></blockquote><p>Instead of explaining the entire procedure every time, you can package the procedure as a reusable <strong>skill/workflow</strong>.</p><p>Something like:</p><pre>Deployment Skill<br>1. Validate configuration<br>2. Pull latest image<br>3. Run health checks<br>4. Deploy container<br>5. Verify application<br>6. Check logs<br>7. Roll back if unhealthy</pre><p>The agent doesn’t need to reinvent the procedure every time.</p><p>This is similar to <strong>muscle memory</strong>.</p><p>A skilled DevOps engineer doesn’t consciously rethink:</p><blockquote><em>“What is SSH?”</em></blockquote><p>every time they connect to a server.</p><p>They simply execute the familiar workflow.</p><p>Skills give AI that kind of reusable procedural knowledge.</p><h3>RAG answers:</h3><blockquote><em>“What do I know?”</em></blockquote><h3>Skills answer:</h3><blockquote><em>“How do I usually do this?”</em></blockquote><p>That’s an important distinction.</p><h3>🤖 5. AI Agent = Brain + Hands</h3><p>Now we arrive at the final layer.</p><p>An <strong>AI Agent</strong> combines reasoning with tools, knowledge and procedures to accomplish a goal.</p><p>Instead of:</p><pre>User → Question → LLM → Answer</pre><p>we can have:</p><pre>User<br>  ↓<br>AI Agent<br>  ↓<br>Reason<br>  ↓<br>Retrieve information<br>  ↓<br>Choose a tool<br>  ↓<br>Execute action<br>  ↓<br>Observe result<br>  ↓<br>Reason again<br>  ↓<br>Complete task</pre><p>For example, imagine telling an AI DevOps agent:</p><blockquote><strong><em>“Investigate why the production API is slow.”</em></strong></blockquote><p>A basic chatbot might give you a list of possible causes.</p><p>An agent could potentially:</p><pre>1. Check monitoring<br>        ↓<br>2. Inspect CPU / memory<br>        ↓<br>3. Query application logs<br>        ↓<br>4. Check database performance<br>        ↓<br>5. Inspect recent deployments<br>        ↓<br>6. Identify suspicious change<br>        ↓<br>7. Recommend or execute remediation<br>        ↓<br>8. Verify recovery</pre><p>That’s a completely different paradigm.</p><p>The AI isn’t merely <strong>answering a question</strong>.</p><p>It’s <strong>working toward an outcome</strong>.</p><h3>🔥 The Real Magic Happens When They Work Together</h3><p>The individual components are useful.</p><p>But the combination is where modern AI systems become powerful.</p><p>Think about this architecture:</p><pre>                    ┌──────────────┐<br>                    │     User     │<br>                    └──────┬───────┘<br>                           ↓<br>                    ┌──────────────┐<br>                    │  AI Agent    │<br>                    │              │<br>                    │ Reason + Plan│<br>                    └──────┬───────┘<br>                           │<br>          ┌────────────────┼────────────────┐<br>          ↓                ↓                ↓<br>       🧠 LLM            📚 RAG            💪 Skills<br>          │                │                │<br>     Reasoning         Knowledge        Procedures<br>          │                │                │<br>          └────────────────┼────────────────┘<br>                           ↓<br>                      🧬 MCP<br>                           │<br>          ┌────────────────┼─────────────────┐<br>          ↓                ↓                 ↓<br>       GitHub           Database           Cloud<br>          ↓                ↓                 ↓<br>       Servers           APIs              Files</pre><p>Now the architecture starts looking less like a chatbot…</p><p>and more like a <strong>digital worker</strong>.</p><h3>🤯 Here’s the Important Distinction</h3><p>A lot of AI discussions mix these concepts together.</p><p>But they’re solving different problems.</p><p>ComponentHuman analogyMain purpose<strong>LLM</strong>🧠 BrainReason and generate<strong>RAG</strong>📚 LibraryRetrieve knowledge<strong>MCP</strong>🧬 Nervous systemConnect AI to tools/data<strong>Skills</strong>💪 Muscle memoryReuse procedures<strong>Agent</strong>🤖 Brain + handsPlan and execute</p><p>And this gives us a simple formula:</p><blockquote><strong><em>Intelligence + Knowledge + Connectivity + Procedures + Action = Agentic AI</em></strong></blockquote><h3>🚀 Why This Matters for Developers</h3><p>This also changes how we should think about building AI applications.</p><p>The old approach was often:</p><pre>Build chatbot<br>      ↓<br>Add bigger model<br>      ↓<br>Add better prompt</pre><p>The newer approach is increasingly:</p><pre>Choose model<br>      ↓<br>Give it relevant knowledge<br>      ↓<br>Connect it to real systems<br>      ↓<br>Give it reusable skills<br>      ↓<br>Let it plan<br>      ↓<br>Let it act<br>      ↓<br>Observe and verify</pre><p>And suddenly your application isn’t just an <strong>AI chatbot</strong>.</p><p>It’s an <strong>AI system</strong>.</p><h3>⚠️ But There’s a Catch</h3><p>Giving an AI hands also means giving it <strong>responsibility</strong>.</p><p>If an agent can:</p><pre>read files<br>↓<br>access databases<br>↓<br>modify infrastructure<br>↓<br>deploy applications<br>↓<br>send emails<br>↓<br>delete resources</pre><p>then mistakes can have real consequences.</p><p>So production agents need things like:</p><ul><li>Least-privilege access</li><li>Authentication</li><li>Authorization</li><li>Tool restrictions</li><li>Approval gates</li><li>Audit logs</li><li>Sandboxing</li><li>Observability</li><li>Rate limits</li><li>Human oversight</li><li>Rollback mechanisms</li></ul><p>A brain with no hands can’t break your server.</p><p>A brain with <strong>root access</strong> potentially can.</p><p>That’s why <strong>agent engineering is also systems engineering and security engineering.</strong></p><h3>🧠 The Easiest Way to Remember It</h3><p>Use this sequence:</p><h3>Brain → Library → Nerves → Muscles → Hands</h3><p><strong>LLM → RAG → MCP → Skills → Agent</strong></p><p>Or remember:</p><blockquote><strong><em>Think → Know → Connect → Practice → Act</em></strong></blockquote><p>That’s the architecture in five words.</p><h3>🌐 The Bigger Picture</h3><p>We’re moving from:</p><p><strong>AI that answers</strong></p><p>to:</p><p><strong>AI that retrieves</strong></p><p>to:</p><p><strong>AI that uses tools</strong></p><p>to:</p><p><strong>AI that follows procedures</strong></p><p>to:</p><p><strong>AI that completes tasks.</strong></p><p>And that final transition is the one that could fundamentally change how we build software.</p><p>The future isn’t necessarily:</p><blockquote><em>“Everyone gets a chatbot.”</em></blockquote><p>It may be:</p><blockquote><strong><em>“Every organization gets a layer of AI workers connected to its knowledge, tools, systems and workflows.”</em></strong></blockquote><p>And once you see the AI stack as a <strong>human body</strong>, the architecture suddenly becomes much easier to understand.</p><p><strong>The LLM is the brain.</strong></p><p><strong>RAG gives it a library.</strong></p><p><strong>MCP gives it nerves.</strong></p><p><strong>Skills give it muscle memory.</strong></p><p><strong>And the agent gives it hands.</strong></p><p>The interesting question is no longer:</p><blockquote><strong><em>“How smart is the model?”</em></strong></blockquote><p>It’s:</p><blockquote><strong><em>“What can we connect that intelligence to?”</em></strong></blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ce8add8d98ae" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[package.json vs package-lock.json: The Dependency Battle Behind Every Node.js App]]></title>
            <link>https://medium.com/@jahangir80842/package-json-vs-package-lock-json-the-dependency-battle-behind-every-node-js-app-4a38784225af?source=rss-1e1a07bda9c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/4a38784225af</guid>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[nodejs]]></category>
            <category><![CDATA[software-engineering]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[docker]]></category>
            <dc:creator><![CDATA[Jahangir]]></dc:creator>
            <pubDate>Sat, 03 Oct 2026 09:03:14 GMT</pubDate>
            <atom:updated>2026-10-03T09:03:14.879Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/808/1*8iwdzVGjFhZ82NNRl_BtWw.png" /></figure><p>There is a small moment almost every Node.js developer experiences.</p><p>You clone a project:</p><pre>git clone https://github.com/example/my-app.git<br>cd my-app<br>npm install<br>npm start</pre><p>And everything works.</p><p>You come back three months later, run:</p><pre>npm install</pre><p>And suddenly…</p><p>💥 Something breaks.</p><p>A dependency changed.<br>A transitive dependency changed.<br>A bug appeared.<br>A security patch changed behavior.<br>Or, even worse, it works on your laptop but fails in CI/CD.</p><p>Then you look at the repository and see:</p><pre>package.json<br>package-lock.json</pre><p>Both seem to contain dependencies.</p><p>So the obvious question is:</p><blockquote><strong><em>Why does Node.js need two files to describe dependencies?</em></strong></blockquote><p>The answer is much more interesting than it first appears.</p><h3>📦 package.json is what you WANT</h3><p>Think of package.json as the <strong>contract</strong> for your application.</p><p>It tells npm:</p><blockquote><em>“These are the packages my application needs, and these are the kinds of versions I’m willing to accept.”</em></blockquote><p>For example:</p><pre>{<br>  &quot;dependencies&quot;: {<br>    &quot;express&quot;: &quot;^5.1.0&quot;,<br>    &quot;axios&quot;: &quot;^1.12.0&quot;<br>  }<br>}</pre><p>The important part is:</p><pre>^5.1.0</pre><p>This does <strong>not necessarily mean</strong>:</p><blockquote><em>Install exactly Express 5.1.0.</em></blockquote><p>It means roughly:</p><blockquote><em>Give me a compatible version within the allowed semver range.</em></blockquote><p>So if a newer compatible version exists, npm may install it.</p><p>That’s useful for developers because you don’t want to manually update every dependency every time a compatible patch release appears.</p><h3>🔒 package-lock.json is what you ACTUALLY GOT</h3><p>Now comes the interesting part.</p><p>Suppose your package.json says:</p><pre>&quot;express&quot;: &quot;^5.1.0&quot;</pre><p>At the time you ran:</p><pre>npm install</pre><p>npm might resolve that to:</p><pre>express 5.1.0</pre><p>But Express itself depends on other packages.</p><p>Those packages have dependencies.</p><p>Those dependencies have dependencies.</p><p>You end up with a dependency tree:</p><pre>your-app<br>│<br>├── express<br>│   ├── dependency-A<br>│   │   ├── dependency-X<br>│   │   └── dependency-Y<br>│   └── dependency-B<br>│       └── dependency-Z<br>│<br>└── axios<br>    └── dependency-C</pre><p>package-lock.json records this resolved dependency tree.</p><p>It contains information such as:</p><ul><li>exact package versions</li><li>resolved package locations</li><li>integrity hashes</li><li>dependency relationships</li><li>nested dependencies</li></ul><p>So you can think of it as:</p><blockquote><strong><em>package.json = dependency requirements</em></strong></blockquote><blockquote><strong><em>package-lock.json = exact dependency resolution</em></strong></blockquote><h3>🎯 Here’s the easiest analogy</h3><p>Imagine you’re ordering food.</p><h3>package.json</h3><p>You say:</p><blockquote><em>“I want pizza.”</em></blockquote><p>That’s your requirement.</p><h3>package-lock.json</h3><p>The restaurant records:</p><blockquote><em>“Large chicken pizza, thin crust, extra cheese, delivered to this address.”</em></blockquote><p>That’s the exact result.</p><p>Next time you order using the record, you get essentially the same thing.</p><p>Without the lock file, npm has more freedom to resolve the request again.</p><h3>😈 The hidden problem: dependencies have dependencies</h3><p>This is where things become really important for DevOps.</p><p>Imagine your package.json contains:</p><pre>{<br>  &quot;dependencies&quot;: {<br>    &quot;my-framework&quot;: &quot;^2.0.0&quot;<br>  }<br>}</pre><p>You install it today.</p><p>You get:</p><pre>my-framework 2.0.3</pre><p>But internally:</p><pre>my-framework 2.0.3<br>└── helper-library 4.1.2</pre><p>Three months later, my-framework releases:</p><pre>2.4.0</pre><p>and its dependency changes:</p><pre>helper-library 4.5.0</pre><p>Now someone else clones your project.</p><p>If npm resolves dependencies from scratch, they could potentially get:</p><pre>my-framework 2.4.0<br>└── helper-library 4.5.0</pre><p>while your original environment had:</p><pre>my-framework 2.0.3<br>└── helper-library 4.1.2</pre><p>Same:</p><pre>package.json</pre><p>Different:</p><pre>node_modules/</pre><p>And that’s a nightmare for reproducible deployments.</p><h3>🔐 This is why package-lock.json matters</h3><p>With a lock file committed to Git:</p><pre>Git repository<br>│<br>├── package.json<br>└── package-lock.json</pre><p>your CI/CD pipeline can reproduce the dependency tree much more reliably.</p><p>For example:</p><pre>npm ci</pre><p>is specifically designed for clean, reproducible installation from the lock file.</p><p>A typical CI pipeline might therefore use:</p><pre>npm ci<br>npm test<br>npm run build</pre><p>rather than:</p><pre>npm install<br>npm test<br>npm run build</pre><h3>⚔️ npm install vs npm ci</h3><p>This distinction is extremely important.</p><h3>npm install</h3><p>Generally used when you’re <strong>developing</strong>.</p><p>It can:</p><ul><li>install dependencies</li><li>update package-lock.json</li><li>resolve dependencies</li><li>modify the lock file when necessary</li></ul><p>Example:</p><pre>npm install axios</pre><p>This can modify both:</p><pre>package.json<br>package-lock.json</pre><h3>npm ci</h3><p>ci means <strong>Clean Install</strong>.</p><p>It’s designed for CI/CD environments.</p><p>Example:</p><pre>npm ci</pre><p>It uses the lock file as the source of truth and installs the dependency tree described there.</p><p>It also expects the lock file and package.json to be in sync.</p><p>If they aren’t, npm can fail instead of silently resolving a new dependency tree.</p><p>That’s actually a good thing in CI.</p><p>You want the pipeline to say:</p><blockquote><em>“The dependency definition changed but the lock file wasn’t updated.”</em></blockquote><p>rather than:</p><blockquote><em>“No problem, I’ll figure something out.”</em></blockquote><h3>🧠 So why not just put exact versions in package.json?</h3><p>You might ask:</p><blockquote><em>“Why not simply do this?”</em></blockquote><pre>{<br>  &quot;dependencies&quot;: {<br>    &quot;express&quot;: &quot;5.1.0&quot;,<br>    &quot;axios&quot;: &quot;1.12.0&quot;<br>  }<br>}</pre><p>That still doesn’t completely solve the problem.</p><p>Because Express itself has dependencies.</p><p>For example:</p><pre>your application<br>└── express 5.1.0<br>    ├── package-A<br>    ├── package-B<br>    └── package-C</pre><p>You aren’t necessarily specifying the exact versions of those transitive dependencies in package.json.</p><p>That’s one of the major jobs of the lock file.</p><h3>🌳 package.json vs package-lock.json</h3><p>A useful mental model:</p><pre>package.json<br>                      │<br>                      │<br>                &quot;What I need&quot;<br>                      │<br>                      ▼<br>               npm dependency<br>                  resolver<br>                      │<br>                      ▼<br>              package-lock.json<br>                      │<br>                      │<br>               &quot;What to install&quot;<br>                      │<br>                      ▼<br>                  node_modules</pre><p>Or even simpler:</p><pre>package.json<br>     ↓<br>Requirements<br><br>package-lock.json<br>     ↓<br>Resolution<br><br>node_modules<br>     ↓<br>Reality</pre><h3>🚨 What happens if you delete package-lock.json?</h3><p>Suppose your project contains:</p><pre>package.json<br>package-lock.json</pre><p>You delete:</p><pre>rm package-lock.json</pre><p>Then run:</p><pre>npm install</pre><p>npm has to resolve the dependency tree again.</p><p>It may produce a new:</p><pre>package-lock.json</pre><p>Potentially with different versions of dependencies.</p><p>That doesn’t necessarily mean the application will break.</p><p>But it means you’ve thrown away an important piece of reproducibility information.</p><p>For production projects, that’s usually something you don’t want to do casually.</p><h3>🛡️ What about security?</h3><p>This is another reason the lock file matters.</p><p>Imagine your application has:</p><pre>my-app<br>└── framework<br>    └── vulnerable-library</pre><p>You can use npm’s auditing capabilities to identify vulnerabilities in your dependency tree.</p><p>For example:</p><pre>npm audit</pre><p>The lock file helps npm understand the concrete dependency tree being used.</p><p>You can also inspect the dependency tree with:</p><pre>npm ls</pre><p>For example:</p><pre>npm ls express</pre><p>or:</p><pre>npm ls</pre><p>This is extremely useful when troubleshooting:</p><blockquote><em>“Why is this vulnerable package installed when I never added it?”</em></blockquote><p>The answer is often:</p><blockquote><em>It’s a </em><strong><em>transitive dependency</em></strong><em>.</em></blockquote><h3>🤯 The package you never installed</h3><p>Imagine:</p><pre>&quot;dependencies&quot;: {<br>  &quot;some-framework&quot;: &quot;^3.0.0&quot;<br>}</pre><p>You never installed:</p><pre>left-pad</pre><p>But:</p><pre>some-framework<br>    ↓<br>library-A<br>    ↓<br>library-B<br>    ↓<br>left-pad</pre><p>So:</p><pre>npm ls left-pad</pre><p>could reveal:</p><pre>my-app<br>└─┬ some-framework<br>  └─┬ library-A<br>    └─┬ library-B<br>      └── left-pad</pre><p>This is one reason dependency management becomes complicated surprisingly quickly.</p><h3>🐳 And this becomes even more important with Docker</h3><p>Consider this Dockerfile:</p><pre>FROM node:24<br>WORKDIR /app<br>COPY package*.json ./<br>RUN npm ci<br>COPY . .<br>CMD [&quot;npm&quot;, &quot;start&quot;]</pre><p>Notice this:</p><pre>COPY package*.json ./</pre><p>That copies:</p><pre>package.json<br>package-lock.json</pre><p>Then:</p><pre>RUN npm ci</pre><p>installs the locked dependency tree.</p><p>This also creates a useful Docker layer:</p><pre>Dockerfile<br>   │<br>   ├── COPY package*.json<br>   │<br>   ├── RUN npm ci<br>   │<br>   └── COPY application source</pre><p>If you change only your application code:</p><pre>src/app.js</pre><p>but don’t change:</p><pre>package.json<br>package-lock.json</pre><p>Docker can often reuse the dependency-install layer.</p><p>That makes builds faster.</p><h3>🚀 CI/CD: This is where DevOps people should care</h3><p>A production pipeline might look like:</p><pre>steps:<br>  - checkout<br>- install:<br>      npm ci<br>  - test:<br>      npm test<br>  - build:<br>      npm run build<br>  - deploy:<br>      deploy-to-production</pre><p>The important part is:</p><pre>npm ci</pre><p>not:</p><pre>npm install</pre><p>Because the goal of CI/CD is generally:</p><pre>Developer machine<br>        │<br>        ▼<br>       Git<br>        │<br>        ▼<br>       CI<br>        │<br>        ▼<br>   Same dependency tree<br>        │<br>        ▼<br>   Production</pre><p>The fewer variables you have, the easier the system is to reproduce.</p><h3>🧩 Should package-lock.json be committed to Git?</h3><p>For an npm application, <strong>generally yes</strong>.</p><p>Your repository should typically contain:</p><pre>my-node-app/<br>├── package.json<br>├── package-lock.json<br>├── src/<br>├── test/<br>├── Dockerfile<br>└── .gitignore</pre><p>You normally should <strong>not</strong> commit:</p><pre>node_modules/</pre><p>So your .gitignore would commonly contain:</p><pre>node_modules/</pre><p>But:</p><pre>package-lock.json</pre><p>should normally stay tracked.</p><h3>🔄 What if I change package.json manually?</h3><p>Suppose you change:</p><pre>&quot;express&quot;: &quot;^5.1.0&quot;</pre><p>to:</p><pre>&quot;express&quot;: &quot;^5.2.0&quot;</pre><p>Now your dependency declaration and lock file may no longer represent the same desired state.</p><p>Run:</p><pre>npm install</pre><p>npm updates the lock file accordingly.</p><p>Then commit both:</p><pre>git add package.json package-lock.json<br>git commit -m &quot;Update express dependency&quot;</pre><p>Think:</p><pre>package.json changed<br>       ↓<br>dependency intent changed<br>       ↓<br>npm install<br>       ↓<br>lock file updated<br>       ↓<br>commit both</pre><h3>🔥 The most important rule</h3><p>Here’s the rule I would remember:</p><blockquote><strong><em>Don’t manually edit package-lock.json unless you really know why you’re doing it.</em></strong></blockquote><p>Let npm manage it.</p><p>For example:</p><pre>npm install express@5.2.0</pre><p>rather than manually opening:</p><pre>package-lock.json</pre><p>and changing versions yourself.</p><h3>🧠 A DevOps mental model</h3><p>If you’re coming from infrastructure as code, there’s actually a very nice analogy.</p><p>Think of:</p><pre>package.json</pre><p>like your <strong>desired configuration</strong>.</p><p>And:</p><pre>package-lock.json</pre><p>like the <strong>resolved plan/state of exactly what dependencies were selected</strong>.</p><p>Then:</p><pre>node_modules/</pre><p>is the actual environment.</p><p>So:</p><pre>package.json<br>      │<br>      │ desired dependencies<br>      ▼<br>package-lock.json<br>      │<br>      │ resolved dependencies<br>      ▼<br>node_modules<br>      │<br>      │ runtime<br>      ▼<br>Application</pre><p>This isn’t a perfect Terraform analogy, but it’s a useful mental model for understanding the roles.</p><h3>🎯 One example that makes everything click</h3><p>Imagine Alice creates a project today.</p><p>Her:</p><pre>package.json</pre><p>says:</p><pre>{<br>  &quot;dependencies&quot;: {<br>    &quot;express&quot;: &quot;^5.1.0&quot;<br>  }<br>}</pre><p>She runs:</p><pre>npm install</pre><p>and gets:</p><pre>express 5.1.0</pre><p>Her repository becomes:</p><pre>package.json<br>package-lock.json</pre><p>Bob clones it tomorrow.</p><p>If Bob runs:</p><pre>npm ci</pre><p>he gets the dependency tree recorded by the lock file.</p><p>Now imagine six months later Express has released:</p><pre>5.1.1<br>5.1.2<br>5.1.3</pre><p>The ^5.1.0 declaration may allow a newer compatible release.</p><p>But Bob’s existing lock file still tells npm which concrete version was resolved for the project.</p><p>That’s the fundamental reason lock files exist.</p><h3>🧠 The 10-second memory trick</h3><p>Remember:</p><blockquote><strong><em>JSON = WHAT</em></strong><em><br></em><strong><em>LOCK = EXACTLY WHAT</em></strong></blockquote><p>Or:</p><pre>package.json<br>      ↓<br>&quot;I need Express&quot;<br><br>package-lock.json<br>      ↓<br>&quot;Here is exactly which Express dependency tree we resolved&quot;</pre><p>And for DevOps:</p><blockquote><strong><em>npm install for development, </em></strong><strong><em>npm ci for reproducible CI/CD.</em></strong></blockquote><h3>Final picture</h3><pre>package.json<br>           ┌───────────────┐<br>           │ Direct deps   │<br>           │ Version ranges│<br>           │ Scripts       │<br>           │ Project info  │<br>           └───────┬───────┘<br>                   │<br>                   ▼<br>                 npm<br>                   │<br>                   ▼<br>          package-lock.json<br>        ┌─────────────────────┐<br>        │ Exact resolutions   │<br>        │ Dependency tree     │<br>        │ Integrity hashes    │<br>        │ Transitive deps     │<br>        └──────────┬──────────┘<br>                   │<br>                   ▼<br>              node_modules<br>                   │<br>                   ▼<br>              Your App</pre><p>So the next time you see both files in a repository, don’t think:</p><blockquote><em>“Why are there two dependency files?”</em></blockquote><p>Think:</p><blockquote><strong><em>package.json describes the dependency contract. </em></strong><strong><em>package-lock.json makes that contract reproducible.</em></strong></blockquote><p>And that tiny distinction is one of the foundations of reliable Node.js builds and CI/CD.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4a38784225af" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Claude + Jev: Are We Building an AI Nervous System?]]></title>
            <link>https://medium.com/@jahangir80842/claude-jev-are-we-building-an-ai-nervous-system-30ebb2f7b9ef?source=rss-1e1a07bda9c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/30ebb2f7b9ef</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[jevs]]></category>
            <dc:creator><![CDATA[Jahangir]]></dc:creator>
            <pubDate>Thu, 24 Sep 2026 18:27:52 GMT</pubDate>
            <atom:updated>2026-09-24T18:35:46.242Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/948/1*wDtmmoDbxD0x3HeQkF9unw.png" /></figure><p><em>I thought the next leap in AI would come from a model that could think longer. Jev made me wonder if the real leap is knowing when to stop thinking and make a </em><strong><em>decision</em></strong><em>.</em></p><p>Lets understand the application of Jev and why we should integrate it with Claude or Codex.</p><p>A production alert fires at 2:13 a.m.</p><p>Your AI agent reads the logs, checks the last deployment, compares metrics, and recognizes a familiar pattern. It has enough information to choose a next step. But instead of acting, it starts another conversation with itself. More tokens. More explanation. Another paragraph about what <em>might</em> be happening.</p><p>Meanwhile, customers are still waiting.</p><p>That is the strange thing about today’s AI agents: we have built systems that can reason through enormous problems, then ask them to spend the same kind of effort deciding whether an alert belongs in a ticket or on someone’s phone.</p><p><strong>What if the agent had two gears?</strong></p><p>One gear investigates and explains. The other makes quick, narrowly defined judgments that software can measure and use. That is why I am interested in connecting Claude with Jev.</p><h3>Think of Claude as the investigator and Jev as the reflex</h3><p>Claude can inspect a repository, trace a failure, write a fix, and explain the result to a human. Jev is built for a different shape of question. You give it the relevant state and ask it to choose among defined options, score a specific attribute, or evaluate a statement. It returns structured values instead of a written explanation. That is how TypeSafe AI describes its model and three question types.</p><p>Here is the distinction that clicked for me:</p><blockquote><strong><em>Claude:</em></strong><em> “Here is what the evidence suggests, and here is how I would fix it.”<br></em><strong><em>Jev:</em></strong><em> “Given this evidence, which of these four routes fits best?”<br></em><strong><em>Your code:</em></strong><em> “This route is allowed only if our rules and checks pass.”</em></blockquote><p>A reflex is valuable because it handles a familiar choice quickly. It does not take control of the whole body. I would treat Jev the same way: a component for bounded decisions, with ordinary code holding the authority to act.</p><h3>Picture an AI operations agent that actually closes the loop</h3><p>Suppose your monitoring system reports that your WordPress site is responding slowly. Claude gathers the recent response times, origin logs, CPU usage, PHP worker status, and deployment history. It notices that the homepage works at the origin but intermittently times out through the edge.</p><p>Now the workflow needs to choose among four next steps:</p><ol><li>Investigate the origin further.</li><li>Investigate the edge or CDN path.</li><li>Open a routine performance ticket.</li><li>Escalate to a human immediately.</li></ol><p>This is a place where Jev <em>could</em> evaluate a carefully framed choice. The application would then apply its own rules: an ongoing outage might always escalate; a low-confidence answer might go to review; an automatic restart might require separate evidence and explicit permission. Claude could explain the resulting recommendation in clear language.</p><p>The crucial word is <em>could</em>. I have not benchmarked that workflow, and a plausible architecture is not a production result. But it is a much more interesting experiment than telling two models to “collaborate” and hoping something magical happens.</p><h3>How I would try it with Claude Code</h3><p>TypeSafe offers an official agent skill that teaches Claude Code its API and patterns. Add its marketplace and install the skill:</p><pre>claude plugin marketplace add typesafe-ai/skills<br>claude plugin install typesafe@typesafe-ai</pre><p>Open Claude Code in a project and invoke /typesafe:typesafe-ai, or ask Claude to use the TypeSafe skill. When you are ready to make actual Jev API calls, create a TypeSafe API key and make it available as TYPESAFE_API_KEY. These are the steps in TypeSafe’s agent-skill documentation.</p><p>I would start with this prompt:</p><blockquote><em>Use the TypeSafe skill. Inspect our incident triage code and identify one repeated decision with a fixed set of outcomes. Design a Jev experiment for that decision using historical, labeled incidents. Keep the questions and thresholds in one file. Compare its decisions with our current process, report missed urgent incidents separately, and do not connect it to live remediation.</em></blockquote><p>There is an important boundary: installing the skill helps Claude <em>build</em> a Jev integration. It does not silently replace Claude’s reasoning or make every Claude Code tool call pass through Jev. The application has to make the Jev call and decide how to use its answer. </p><h3>The part that could change agent design</h3><p>Today, “more capable AI” often means a bigger model, a longer context window, or more time spent reasoning. But operational systems also need thousands of small judgments: <strong>Is this alert urgent? Does this request have enough information? Which specialist should handle it? Is this result uncertain enough to ask a person?</strong></p><p>Give every one of those questions to a general-purpose chatbot and the agent may spend a lot of effort producing words that your software must translate back into decisions. Give the narrow questions to a decision model, and you can begin to inspect the choices themselves: Where was it right? Where was it wrong? Which mistakes matter most?</p><p>That does not mean Jev should judge everything. Fixed rules belong in code. Unfamiliar, complex problems still need deeper investigation. The opportunity lies in the middle: recurring decisions that are hard to express as simple rules but specific enough to test against real examples.</p><p>My first test would run Jev alongside the current process without giving it control. I would compare decisions on past incidents, then record its live recommendations in shadow mode. I would measure false negatives before considering any automatic action. TypeSafe also advises keeping questions and thresholds easy to review and checking an agent’s assumptions.</p><h3>Maybe the future AI agent is less like one genius and more like a nervous system</h3><p>The impressive part of Claude is that it can follow a complicated situation. The intriguing part of Jev is that it can turn a well-framed judgment into a value that code can use. Together, they suggest an agent that investigates deeply when necessary and handles familiar decisions through a path we can test.</p><p>That is the experiment I want to see: <strong>Claude for the thinking. Jev for the bounded judgment. Code for the authority.</strong></p><p>The next leap may arrive when we stop asking one model to perform every kind of intelligence in exactly the same way.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=30ebb2f7b9ef" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Jev vs. LLMs: The Real Battle Is Happening Behind the Scenes]]></title>
            <link>https://medium.com/@jahangir80842/jev-vs-llms-the-real-battle-is-happening-behind-the-scenes-ef9120178005?source=rss-1e1a07bda9c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/ef9120178005</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[kubernetes]]></category>
            <dc:creator><![CDATA[Jahangir]]></dc:creator>
            <pubDate>Tue, 22 Sep 2026 05:41:01 GMT</pubDate>
            <atom:updated>2026-09-22T05:41:01.399Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/991/1*Tfi-MkZmvy_TiPyCqEKd2Q.png" /></figure><p>Will Jev Replace LLMs? The Answer Is More Interesting Than the Hype</p><p><em>Everyone is talking about an AI model that doesn’t want to chat. As someone who operates software systems, that got my attention.</em></p><p>When I first saw people talking about Jev, I had the same reaction I have to most new AI launches: <em>Do we really need another model?</em></p><p>Then I looked at what it was built to do. Jev isn’t trying to write a better email, produce a longer answer, or become your new favorite chatbot. It is designed to make structured decisions that software can act on.</p><p>That immediately raises the headline question: <strong>If AI can make decisions directly, will Jev replace LLMs?</strong></p><p>My answer is: probably not across the board. But it could replace an expensive, awkward use of LLMs that many of us have quietly accepted as normal.</p><p>I haven’t deployed Jev or benchmarked it myself. Here is why its approach interests me, where I would test it, and what would have to be true before I trusted it in production.</p><h3>We’ve been asking chatbots to do a lot of unglamorous work</h3><p>Picture a production alert arriving at 2 a.m. It includes a failed health check, a few lines of logs, and a customer impact flag. Your system needs to choose one action: page an engineer, create a ticket, retry the check, or send the alert for review.</p><p>An LLM can read the alert and produce an excellent paragraph about it. But the automation needs an actionable choice. So we ask the model to output JSON, parse it, validate it, and write more code to decide whether its answer is safe to use.</p><p>That pattern works in some systems. It also feels like asking a skilled writer to operate a traffic light.</p><p>According to <a href="https://docs.typesafe.ai/introduction">TypeSafe AI’s documentation</a>, Jev takes a state and typed questions, then returns structured answers your code can use. Its question types include choosing an option, scoring on a rubric, and evaluating whether a statement is true. Choice and Score return probabilities and confidence information.</p><p>In other words, the application can ask a bounded question such as, “Which of these four routes fits this alert?” and receive a result designed for code to consume.</p><h3>Why the “LLM killer” headline misses the real opportunity</h3><p>I still want an LLM when I need an incident summary, a draft customer update, an investigation plan, or help understanding unfamiliar logs. Those jobs need language and explanation.</p><p>I might want Jev for the repeated decisions surrounding those jobs: Which queue should receive the incident? Is there enough information to proceed? Does this case need a human? Which tool should an agent call next?</p><p>My working mental model is simple: <strong>an LLM can explain the situation; a decision model can help route the next step.</strong> That is a design possibility, not a claim that Jev has already proved itself better in my environment.</p><p>The difference could matter at scale. One impressive answer is easy to notice. Thousands of small routing decisions, made every day inside applications, are much less visible. Their errors still create real costs: missed incidents, noisy pages, delayed support, and brittle automations.</p><h3>The first test I would actually run</h3><p>I would start with alert triage, because I already understand the failure modes. I would take a set of past alerts with known outcomes and give Jev a fixed menu: <strong>page now, create a ticket, retry the check, or request human review</strong>.</p><p>Then I would compare its decisions against our current rules or model. I would measure false negatives separately: missing a real outage is far worse than creating an extra ticket. I would also check whether confidence is useful in practice. When the model says it is highly confident, is it actually right often enough on <em>our</em> alerts?</p><p>Only after that would I consider letting a high-confidence result trigger an action. The automation would still enforce permissions, thresholds, rate limits, and logging. A model’s answer should never become a blank check to restart a service or close an incident.</p><p>TypeSafe AI <a href="https://typesafe.ai/">describes using confidence thresholds</a> to decide when software acts and when a person reviews the result. I like that architecture. I would set those thresholds from my own test data rather than from a launch demo.</p><h3>The part the hype can’t answer for me</h3><p>A typed answer can still be wrong. A confidence value can still be poorly calibrated for the data I care about. And a model that handles clean examples may struggle when an alert contains missing fields, misleading logs, or two problems at once.</p><p>The <a href="https://docs.typesafe.ai/introduction">Jev documentation</a> recommends asking small, specific questions and combining the results in code. That fits how I would build this: separately assess customer impact, persistence, and service criticality, then apply an explicit escalation policy. I could inspect that policy, change it, and roll it back.</p><p>So, will Jev replace LLMs? I think the more useful question is: <strong>How often are we paying a language model to make a tiny decision when what we really need is a tested, measurable route through our software?</strong></p><p>Jev has earned my attention because it makes that question hard to ignore. It hasn’t earned a place in my production stack yet. That comes after a real evaluation: representative cases, a baseline, measured mistakes, and safeguards around every action.</p><p>If the results hold up, I can imagine a useful pairing: the LLM helps us understand and communicate; Jev helps the system choose from well-defined options. And if they don’t hold up, I still come away with a better way to examine the decisions hidden inside my automations.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ef9120178005" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ShellCheck and Bats: A Safety Net for Linux Shell Scripts]]></title>
            <link>https://medium.com/@jahangir80842/shellcheck-and-bats-a-safety-net-for-linux-shell-scripts-f06df0916bae?source=rss-1e1a07bda9c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/f06df0916bae</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[linux]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Jahangir]]></dc:creator>
            <pubDate>Mon, 21 Sep 2026 05:36:01 GMT</pubDate>
            <atom:updated>2026-09-21T05:36:01.051Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*9f8XNLtHdyZQNT43xvQLSg.png" /></figure><p>Shell scripts often begin as a few convenient commands. Over time, they grow into deployment tools, backup jobs, health checks, installers, and production automation. At that point, a quoting mistake or an incorrect exit code can cause far more damage than expected.</p><p>Two tools solve different parts of this problem:</p><ul><li><strong>ShellCheck</strong> examines shell code without running it and identifies suspicious syntax, unsafe patterns, portability problems, and common mistakes.</li><li><strong>Bats</strong>, short for Bash Automated Testing System, runs shell commands and verifies that their real behavior matches your expectations.</li></ul><p>They are most effective when used together. ShellCheck asks, “Does this code look unsafe or incorrect?” Bats asks, “Does this code actually behave correctly?”</p><h3>Why shell scripts need automated checks</h3><p>Bash is flexible, but that flexibility creates traps. Consider this cleanup command:</p><pre>rm -rf $backup_dir/*</pre><p>If backup_dir is empty or contains spaces, the command may behave very differently from what the author intended. Quoting the variable is safer:</p><pre>rm -rf -- &quot;${backup_dir:?}&quot;/*</pre><p>The quotes prevent word splitting, -- ends option processing, and ${backup_dir:?} stops the command if the variable is empty or unset. A reviewer may catch the original problem, but an automated tool can catch it on every commit.</p><p>Testing is also necessary because syntactically valid code can still produce the wrong result. A backup script might run without errors but omit hidden files. A service check might print “healthy” while returning a failure exit code. Static analysis alone cannot validate these behaviors.</p><h3>ShellCheck: static analysis for shell code</h3><p><a href="https://www.shellcheck.net/">ShellCheck</a> analyzes shell scripts and reports diagnostics identified by codes such as SC2086. It supports common shells including sh, Bash, Dash, and KornShell.</p><p>On Ubuntu or Debian, install it with:</p><pre>sudo apt update<br>sudo apt install shellcheck</pre><p>Then check a script:</p><pre>shellcheck backup.sh</pre><p>Consider this example:</p><pre>#!/usr/bin/env bash<br>name=$1<br>for file in $(find /var/log -name &quot;*.log&quot;); do<br>    echo Processing $file for $name<br>done</pre><p>ShellCheck will warn about issues such as unquoted variables and iterating over the output of find. A safer version is:</p><pre>#!/usr/bin/env bash<br>name=${1:?Usage: backup.sh NAME}<br>while IFS= read -r -d &#39;&#39; file; do<br>    printf &#39;Processing %s for %s\n&#39; &quot;$file&quot; &quot;$name&quot;<br>done &lt; &lt;(find /var/log -type f -name &#39;*.log&#39; -print0)</pre><p>This version correctly handles filenames containing spaces, tabs, and newlines.</p><h3>What ShellCheck can catch</h3><p>ShellCheck is especially useful for detecting:</p><ul><li>unquoted variable expansion;</li><li>accidental word splitting and glob expansion;</li><li>ineffective assignments in subshells;</li><li>incorrect tests and conditionals;</li><li>suspicious use of cd, find, grep, and pipelines;</li><li>Bash features used in scripts declared as POSIX sh;</li><li>variables that appear unused or incorrectly referenced.</li></ul><p>You can check every shell script in a repository with:</p><pre>find . -type f -name &#39;*.sh&#39; -print0 | xargs -0 shellcheck</pre><p>If executable scripts do not use the .sh extension, target them explicitly or maintain a list in your build configuration.</p><h3>Suppressing a warning carefully</h3><p>Occasionally, a flagged construct is intentional. A specific warning can be disabled near the relevant line:</p><pre># shellcheck disable=SC2086<br>command $intentionally_split_arguments</pre><p>A suppression should be narrow and accompanied by a short explanation. Disabling checks globally can hide unrelated defects.</p><h3>Bats: behavioral testing for Bash and Unix commands</h3><p><a href="https://github.com/bats-core/bats-core">Bats-core</a> is a TAP-compliant testing framework for Bash. A .bats file contains named test cases built from normal shell commands. Bats can test Bash scripts as well as other Unix command-line programs.</p><p>On Ubuntu, a simple installation option is:</p><pre>sudo apt update<br>sudo apt install bats</pre><p>Package versions vary by Linux distribution. The <a href="https://bats-core.readthedocs.io/en/stable/installation.html">official Bats installation guide</a> also documents npm, Homebrew, source, and other installation methods.</p><p>Verify the installation:</p><pre>bats --version</pre><h3>Your first Bats test</h3><p>Suppose we have a script named greet.sh:</p><pre>#!/usr/bin/env bash<br>set -euo pipefail<br>if (( $# != 1 )); then<br>    printf &#39;Usage: %s NAME\n&#39; &quot;$0&quot; &gt;&amp;2<br>    exit 64<br>fi<br>printf &#39;Hello, %s!\n&#39; &quot;$1&quot;</pre><p>Make it executable:</p><pre>chmod +x greet.sh</pre><p>Create test/greet.bats:</p><pre>#!/usr/bin/env bats<br>setup() {<br>    PROJECT_ROOT=&quot;$(cd &quot;$BATS_TEST_DIRNAME/..&quot; &amp;&amp; pwd)&quot;<br>    GREET=&quot;$PROJECT_ROOT/greet.sh&quot;<br>}<br>@test &quot;prints a greeting for a supplied name&quot; {<br>    run &quot;$GREET&quot; &quot;Jagz&quot;<br>    [ &quot;$status&quot; -eq 0 ]<br>    [ &quot;$output&quot; = &quot;Hello, Jagz!&quot; ]<br>}<br>@test &quot;fails with usage information when the name is missing&quot; {<br>    run &quot;$GREET&quot;<br>    [ &quot;$status&quot; -eq 64 ]<br>    [[ &quot;$output&quot; == *&quot;Usage:&quot;* ]]<br>}</pre><p>Run the tests:</p><pre>bats test/greet.bats</pre><p>In a Bats test, run executes a command and captures its result. The most commonly used values are:</p><ul><li>$status: the command&#39;s exit status;</li><li>$output: combined standard output and standard error;</li><li>${lines[0]}, ${lines[1]}, and so on: individual output lines.</li></ul><p>The official Bats documentation notes that run executes the command in a subshell. Therefore, environment changes made inside that command do not remain after run finishes.</p><h3>Setup, teardown, and isolated temporary files</h3><p>Tests should not depend on files left behind by earlier runs. Bats provides lifecycle functions and a temporary directory for isolation:</p><pre>setup() {<br>    config_file=&quot;$BATS_TEST_TMPDIR/app.conf&quot;<br>    printf &#39;PORT=8080\n&#39; &gt; &quot;$config_file&quot;<br>}<br>teardown() {<br>    # Optional cleanup for resources outside BATS_TEST_TMPDIR.<br>    true<br>}<br>@test &quot;configuration contains the expected port&quot; {<br>    run grep &#39;^PORT=8080$&#39; &quot;$config_file&quot;<br>    [ &quot;$status&quot; -eq 0 ]<br>    [ &quot;$output&quot; = &quot;PORT=8080&quot; ]<br>}</pre><p>Use setup() before each test and teardown() after each test. BATS_TEST_TMPDIR gives each test a suitable place for disposable files.</p><p>For suite-wide preparation, Bats also supports setup_file() and teardown_file() in a test file. Per-test setup is usually safer because it reduces hidden dependencies between tests.</p><h3>Testing failure paths, not only success paths</h3><p>Operational scripts must fail safely. Test invalid arguments, unavailable dependencies, permission errors, malformed configuration, and repeated execution.</p><p>For example:</p><pre>@test &quot;rejects an unsupported environment&quot; {<br>    run -2 ./deploy.sh productionn<br>[[ &quot;$output&quot; == *&quot;Unsupported environment&quot;* ]]<br>}</pre><p>Here, run -2 tells Bats that exit status 2 is expected. You can also use run ! command when any nonzero status is acceptable. These forms make the intended failure explicit.</p><p>A useful test suite should answer questions such as:</p><ul><li>Does the command return zero when it succeeds?</li><li>Does it return nonzero when it fails?</li><li>Is the error message understandable?</li><li>Are existing files preserved when validation fails?</li><li>Can the script run twice without corrupting state?</li><li>Does it handle paths containing spaces?</li><li>What happens when a required command is missing?</li></ul><h3>Be careful when testing pipelines</h3><p>This test does not behave as many people expect:</p><pre>run generate_report | grep &#39;healthy&#39;</pre><p>The shell constructs the pipeline outside run. Current Bats documentation recommends bats_pipe when a pipeline must execute inside the captured command:</p><pre>run bats_pipe generate_report \| grep &#39;healthy&#39;<br>[ &quot;$status&quot; -eq 0 ]</pre><p>For simpler cases, invoking Bash explicitly can also make the pipeline a single command:</p><pre>run bash -o pipefail -c &#39;./generate_report | grep &quot;healthy&quot;&#39;<br>[ &quot;$status&quot; -eq 0 ]</pre><p>Avoid inserting untrusted data into a bash -c string.</p><h3>ShellCheck and Bats in one workflow</h3><p>A small project might use this structure:</p><pre>project/<br>├── bin/<br>│   └── greet.sh<br>└── test/<br>    └── greet.bats</pre><p>Run both checks locally:</p><pre>shellcheck bin/*.sh<br>bats test</pre><p>You can wrap them in a Makefile:</p><pre>.PHONY: lint test check<br>lint:<br> shellcheck bin/*.sh<br>test:<br> bats test<br>check: lint test</pre><p>Now one command performs the complete validation:</p><pre>make check</pre><p>The same command can run in CI, ensuring that developers and the pipeline use identical checks.</p><h3>Example GitHub Actions workflow</h3><p>Create .github/workflows/shell-tests.yml:</p><pre>name: Shell tests<br>on:<br>  push:<br>  pull_request:<br>jobs:<br>  test:<br>    runs-on: ubuntu-latest<br>    steps:<br>      - name: Check out repository<br>        uses: actions/checkout@v4<br>      - name: Install test tools<br>        run: |<br>          sudo apt-get update<br>          sudo apt-get install --yes shellcheck bats<br>      - name: Run ShellCheck<br>        run: shellcheck bin/*.sh<br>      - name: Run Bats tests<br>        run: bats test</pre><p>For reproducible CI, pin the runner and tool versions according to your organization’s dependency policy. Distribution packages are convenient, while a container or explicitly pinned release provides tighter version control.</p><h3>What each tool can and cannot prove</h3><p>QuestionShellCheckBatsIs there a likely quoting or expansion mistake?YesOnly if a test exposes itDoes the script return the correct exit status?NoYesIs the script using syntax incompatible with its declared shell?OftenPossiblyDoes the program create the correct file?NoYesDoes an error path preserve existing data?NoYesCan it identify suspicious code without executing it?YesNo</p><p>Neither tool proves that a script is completely safe. ShellCheck uses rules and cannot understand every operational intention. Bats validates only the scenarios you write. Together, however, they eliminate a large class of preventable failures.</p><h3>Practical best practices</h3><ol><li>Start scripts with an accurate shebang, such as #!/usr/bin/env bash for Bash-specific code.</li><li>Quote variable expansions unless splitting or globbing is deliberate.</li><li>Treat exit codes as part of the command’s interface.</li><li>Test both successful and unsuccessful paths.</li><li>Use temporary directories rather than real production paths.</li><li>Mock destructive or external commands such as rm, curl, ssh, and cloud CLIs when unit testing.</li><li>Keep each test independent.</li><li>Run ShellCheck and Bats locally and in CI.</li><li>Add a regression test whenever a bug is fixed.</li><li>Keep scripts small enough that their behavior is easy to test.</li></ol><p>Never run destructive integration tests against a production host. Use disposable containers, virtual machines, temporary directories, or dedicated test accounts.</p><h3>A simple memory trick</h3><p>Remember: <strong>ShellCheck checks the script; Bats checks the behavior.</strong></p><p>Or use the shorter phrase: <strong>Lint before launch, test before trust.</strong></p><p>ShellCheck is the code reviewer looking for dangerous patterns. Bats is the operator executing controlled scenarios and confirming the results.</p><h3>Conclusion</h3><p>Shell scripting deserves the same engineering discipline as application code. A script may be only 30 lines long, but if it manages backups, deployments, users, or servers, its operational impact can be enormous.</p><p>ShellCheck gives immediate feedback about risky shell constructs. Bats turns expected command behavior into executable documentation. Adding both to a repository creates a fast safety net: lint the code, test its behavior, and reject regressions before they reach a live Linux system.</p><h3>Further reading</h3><ul><li><a href="https://www.shellcheck.net/">ShellCheck project</a></li><li><a href="https://github.com/bats-core/bats-core">Bats-core project</a></li><li><a href="https://bats-core.readthedocs.io/en/stable/writing-tests.html">Writing Bats tests</a></li><li><a href="https://bats-core.readthedocs.io/en/stable/installation.html">Installing Bats</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=f06df0916bae" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The Scariest Thing About AI Is Not Intelligence. It Is Autonomy]]></title>
            <link>https://medium.com/@jahangir80842/the-scariest-thing-about-ai-is-not-intelligence-it-is-autonomy-1a855bf0edf4?source=rss-1e1a07bda9c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/1a855bf0edf4</guid>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[google]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Jahangir]]></dc:creator>
            <pubDate>Thu, 17 Sep 2026 05:36:01 GMT</pubDate>
            <atom:updated>2026-09-17T05:36:01.276Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/954/1*5oqz_nJhk3989PKdCv7-SQ.png" /></figure><p>A few years ago, the biggest argument in artificial intelligence was whether models should be allowed to write essays, generate images, or help programmers write code.</p><p>That conversation already feels outdated.</p><p>The much more important question today is this:</p><p><strong>What happens when AI stops being just a chatbot and becomes an operator?</strong></p><p>Not something that simply tells you how to perform a task.</p><p>Something that can analyze a target, write code, test it, notice that it failed, rewrite it, try again, and continue working while the human goes to sleep.</p><p>That is where the AI story is starting to become much more serious.</p><p>And recent reports about AI misuse give us a glimpse of what that future might look like.</p><h3>The Most Important AI Shift Is Not Intelligence</h3><p>Everyone talks about smarter models.</p><p>More parameters.</p><p>Better reasoning.</p><p>Larger context windows.</p><p>Higher benchmark scores.</p><p>But there is another change happening quietly that may matter even more.</p><p>AI systems are becoming <strong>persistent</strong>.</p><p>Give an AI agent access to tools, code execution, APIs, browsers, and infrastructure, and suddenly it is no longer answering a question.</p><p>It is running a workflow.</p><p>Consider the difference.</p><p>A traditional chatbot might say:</p><blockquote><em>“Here is how you could analyze this software for vulnerabilities.”</em></blockquote><p>An AI agent could potentially:</p><ol><li>Decompile the software.</li><li>Analyze the code.</li><li>Generate hypotheses about weaknesses.</li><li>Write proof-of-concept code.</li><li>Test those ideas.</li><li>Analyze the results.</li><li>Repeat the process.</li></ol><p>The scary capability is not any single step.</p><p>The scary capability is the <strong>loop</strong>.</p><p>Humans get tired.</p><p>Agents do not.</p><h3>Cybersecurity May Be the First Battlefield</h3><p>One of the clearest areas where AI is changing the balance of power is cybersecurity.</p><p>Attackers traditionally need considerable expertise and time to perform tasks such as reverse engineering software, modifying malware, finding leaked credentials, or searching millions of files for vulnerabilities.</p><p>AI can dramatically compress that workflow.</p><p>Imagine malware being detected by an antivirus product.</p><p>Traditionally, a developer might need to investigate why it was detected, modify the malware, rebuild it, and test it again.</p><p>Now imagine an automated system monitoring detection results and repeatedly changing the software until it avoids detection.</p><p>That does not necessarily create a completely new kind of cyberattack.</p><p>It does something potentially more important.</p><p>It makes existing attacks <strong>cheaper, faster, and more scalable</strong>.</p><p>And scale changes everything.</p><h3>The Million-App Problem</h3><p>There is another example that should make every developer uncomfortable.</p><p>Mobile applications often contain secrets they should never contain.</p><p>API keys.</p><p>Cloud credentials.</p><p>Tokens.</p><p>Private endpoints.</p><p>Database credentials.</p><p>Sometimes those secrets are accidentally embedded directly into application packages.</p><p>Finding them manually across thousands of applications would be painfully slow.</p><p>AI changes the economics of the attack.</p><p>A system could potentially download huge numbers of application packages, decompile them, inspect the source, identify suspicious credentials, classify them, and automatically prioritize the most valuable findings.</p><p>The uncomfortable part is that many of those vulnerable applications may themselves have been written quickly using AI coding tools.</p><p>Which creates a strange loop.</p><p>AI helps developers produce software faster.</p><p>Developers accidentally ship insecure software.</p><p>AI helps attackers inspect that software faster.</p><p>Attackers discover credentials for accessing more AI services.</p><p>Welcome to the modern software supply chain.</p><h3>AI Does Not Need To Invent New Attacks</h3><p>This is one of the biggest misunderstandings about AI risk.</p><p>People imagine that dangerous AI means some superhuman system inventing a cyberweapon humanity has never seen before.</p><p>But that may not be necessary.</p><p>The immediate risk is much simpler.</p><p>AI can take techniques that already exist and make them available to far more people.</p><p>Think about the progression of computing.</p><p>Cloud computing did not invent servers.</p><p>It made servers available on demand.</p><p>GitHub did not invent source code.</p><p>It made sharing software nearly effortless.</p><p>Docker did not invent virtualization.</p><p>It made packaging and deploying software dramatically easier.</p><p>AI may do something similar to expertise.</p><p>It can make fragments of expert-level knowledge available on demand.</p><p>That is incredibly useful when the person asking the questions is a developer.</p><p>It becomes much more complicated when the person asking the questions is an attacker.</p><h3>The Risk Extends Beyond Cybersecurity</h3><p>Cybersecurity is only one part of the discussion.</p><p>Advanced AI systems increasingly interact with scientific knowledge.</p><p>Chemistry.</p><p>Biology.</p><p>Engineering.</p><p>Electronics.</p><p>Materials science.</p><p>These capabilities have enormous positive potential.</p><p>AI could help researchers discover medicines, design proteins, understand diseases, and accelerate scientific research.</p><p>But the same principle that applies to cybersecurity applies here too.</p><p>Powerful knowledge is usually dual-use.</p><p>The same system that helps one researcher understand biological mechanisms might help another person investigate how those mechanisms could be misused.</p><p>That does not mean an AI model can magically create a biological weapon.</p><p>Real-world science requires laboratories, materials, expertise, equipment, and experimentation.</p><p>But AI can potentially reduce the amount of expertise required to navigate complicated technical information.</p><p>That reduction in friction is what security researchers worry about.</p><h3>Then There Is AI Helping Build Weapons</h3><p>AI-assisted engineering creates another uncomfortable category.</p><p>A model that can help design a robot can potentially help design a drone.</p><p>A model that can write control software can potentially help write targeting software.</p><p>A model that understands electronics can potentially help integrate sensors, navigation systems, and control hardware.</p><p>Again, the technology itself is not automatically dangerous.</p><p>The problem comes from combining:</p><p><strong>capability + automation + access to the physical world.</strong></p><p>A chatbot sitting in a browser has limited power.</p><p>An autonomous agent connected to APIs has more.</p><p>An autonomous agent connected to machines has much more.</p><p>This is why discussions about AI safety increasingly focus not only on what models know, but also on <strong>what models are allowed to do</strong>.</p><h3>One of the Strangest AI Battles Is Model Distillation</h3><p>Not every major AI security problem involves malware or weapons.</p><p>Some involve the AI companies attacking each other economically.</p><p>One increasingly important issue is <strong>model distillation</strong>.</p><p>The basic idea is simple.</p><p>Suppose Company A builds an extremely powerful AI model after spending billions of dollars on research, infrastructure, and training.</p><p>Company B wants similar capabilities.</p><p>Instead of reproducing the entire training process, Company B sends huge numbers of prompts to Company A’s model and collects the answers.</p><p>Those answers can then potentially be used as training data for another model.</p><p>In other words:</p><p>Model A teaches Model B.</p><p>Sometimes intentionally.</p><p>Sometimes not.</p><p>At small scale, learning from model outputs is common.</p><p>At industrial scale, however, AI providers increasingly treat systematic extraction of model behavior as an attack.</p><p>The result is a bizarre new security problem.</p><p>AI companies now have to protect their models not only from hackers stealing model weights, but also from competitors extracting their <strong>behavior through the API</strong>.</p><p>Imagine trying to protect intellectual property when your product’s entire purpose is answering questions.</p><p>That is the problem frontier AI labs are dealing with.</p><h3>The Real AI Security Problem Is Economics</h3><p>This is the part of the AI debate that deserves more attention.</p><p>AI does not need to make attackers dramatically smarter.</p><p>It only needs to make attacks dramatically cheaper.</p><p>Suppose a sophisticated vulnerability assessment previously required an experienced engineer working for eight hours.</p><p>If an AI agent can perform 70% of that work automatically, the economics change.</p><p>One attacker can investigate more targets.</p><p>A small team can behave like a larger team.</p><p>Thousands of applications can be inspected instead of dozens.</p><p>Tasks can run overnight.</p><p>Agents can repeat failed attempts.</p><p>Automation turns expertise into throughput.</p><p>And throughput is powerful.</p><p>The biggest AI security threat may not be an evil superintelligence.</p><p>It may simply be <strong>millions of automated workers doing questionable things extremely cheaply</strong>.</p><h3>But What About Superintelligence?</h3><p>This is where the conversation becomes much more speculative.</p><p>Some AI researchers believe today’s systems are early versions of something vastly more powerful.</p><p>Their concern is not mainly about scams, malware, or leaked API keys.</p><p>Their concern is about a future AI system capable of improving its own capabilities.</p><p>Imagine a system that can:</p><ul><li>write better AI software,</li><li>design better training algorithms,</li><li>optimize its own infrastructure,</li><li>conduct AI research,</li><li>run experiments,</li><li>and improve the next generation of itself.</li></ul><p>That creates the possibility of a feedback loop.</p><p>Better AI helps build better AI.</p><p>Better AI builds even better AI.</p><p>And so on.</p><p>This idea is often called <strong>recursive self-improvement</strong>.</p><p>Whether this leads to rapid superintelligence is heavily debated.</p><p>Some researchers believe it represents one of humanity’s most serious long-term risks.</p><p>Others argue that intelligence improvements will continue to face physical, economic, data, energy, and engineering constraints.</p><p>Nobody currently knows which side will be correct.</p><p>But the question is important enough that it should not be dismissed.</p><h3>The Alignment Problem</h3><p>Suppose we eventually build a system much more intelligent than its creators.</p><p>What would it want?</p><p>That question may sound philosophical, but it becomes an engineering problem once AI systems are capable of taking autonomous actions.</p><p>AI does not need to hate humans to become dangerous.</p><p>A badly specified objective could be enough.</p><p>Imagine telling an extremely capable system:</p><p>“Maximize production.”</p><p>It might discover methods that technically satisfy the instruction while creating consequences the designers never intended.</p><p>This is the core of the AI alignment problem.</p><p>How do you guarantee that increasingly capable systems continue doing what humans actually intended rather than merely optimizing the literal instructions they were given?</p><p>Software engineers already know this problem in miniature.</p><p>Computers are extremely good at doing exactly what you told them to do.</p><p>Unfortunately, that is not always what you meant.</p><h3>The Most Dangerous Word in AI Might Be “Autonomous”</h3><p>Large language models themselves are powerful.</p><p>But the real transformation happens when we connect them to tools.</p><p>A model becomes more capable when it can access:</p><ul><li>terminals,</li><li>browsers,</li><li>databases,</li><li>APIs,</li><li>cloud infrastructure,</li><li>source repositories,</li><li>CI/CD systems,</li><li>robots,</li><li>laboratory equipment.</li></ul><p>Each new tool increases what the system can accomplish.</p><p>This is why the future of AI security may resemble identity and access management more than traditional chatbot moderation.</p><p>The question will no longer be only:</p><p><strong>“What is the model allowed to say?”</strong></p><p>It will increasingly become:</p><p><strong>“What is the model allowed to execute?”</strong></p><p>That is a much harder problem.</p><h3>Developers Are Quietly Building This Future</h3><p>Here is the irony.</p><p>The same engineers worried about autonomous AI are simultaneously building the infrastructure that makes autonomous AI possible.</p><p>AI coding agents can already:</p><p>read repositories,</p><p>create branches,</p><p>modify files,</p><p>run tests,</p><p>review pull requests,</p><p>deploy infrastructure,</p><p>and interact with cloud services.</p><p>None of this means those systems are dangerous by default.</p><p>In fact, they can dramatically improve developer productivity.</p><p>But every improvement in AI’s ability to operate software also expands the number of actions an autonomous system could potentially perform.</p><p>The line between <strong>assistant</strong> and <strong>operator</strong> is disappearing.</p><h3>So Should We Panic?</h3><p>Probably not.</p><p>Panic rarely produces good engineering.</p><p>But ignoring the problem would be equally irresponsible.</p><p>The correct response is the same response engineers use for every powerful technology.</p><p>Defense in depth.</p><p>Least privilege.</p><p>Isolation.</p><p>Monitoring.</p><p>Auditing.</p><p>Rate limits.</p><p>Human approval for high-impact actions.</p><p>Strong authentication.</p><p>Secret management.</p><p>Sandboxing.</p><p>Anomaly detection.</p><p>Incident response.</p><p>In other words, AI safety is increasingly becoming a DevSecOps problem.</p><p>If an agent is going to execute commands against production infrastructure, treat it like a privileged service account.</p><p>If an AI system can access sensitive information, apply proper access controls.</p><p>If an autonomous workflow can make expensive or destructive changes, require approval boundaries.</p><p>Do not rely on the assumption that the model will always behave correctly.</p><p>Design the surrounding system so that one bad decision cannot become a catastrophic one.</p><h3>The Future Might Not Look Like Terminator</h3><p>When people imagine dangerous AI, they usually imagine robots marching through cities.</p><p>Reality may be less cinematic.</p><p>The first major AI disasters could look like ordinary software incidents.</p><p>A compromised agent.</p><p>A leaked credential.</p><p>An automated attack running at enormous scale.</p><p>An AI-generated vulnerability spreading across thousands of systems.</p><p>A scientific tool being used in an unexpected way.</p><p>A badly configured autonomous system making thousands of decisions before a human notices.</p><p>That might be the most important lesson from today’s AI misuse reports.</p><p>The AI apocalypse, if it ever comes, may not begin with a robot announcing that humanity is obsolete.</p><p>It might start with a process running quietly in the background.</p><pre>while True:<br>    analyze()<br>    generate()<br>    execute()<br>    evaluate()<br>    improve()</pre><p>And nobody notices the loop until it has already run a million times.</p><h3>Final Thought</h3><p>We spent the last few years asking:</p><p><strong>“How intelligent can AI become?”</strong></p><p>The next few years may force us to ask a different question:</p><p><strong>“How much authority should we give it?”</strong></p><p>Because intelligence by itself is interesting.</p><p>Intelligence connected to tools is useful.</p><p>Intelligence connected to tools, infrastructure, money, machines, and the ability to operate autonomously is something else entirely.</p><p>That does not mean humanity is doomed.</p><p>It means the architecture around AI may become just as important as the AI itself.</p><p>And for developers, security engineers, cloud engineers, and DevOps teams, that creates an entirely new responsibility.</p><p>We are not only deploying software anymore.</p><p>We are beginning to deploy <strong>software that can act</strong>.</p><p>And we need to design accordingly.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=1a855bf0edf4" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[The SSH Fingerprint Mistake That Can Put You on the Wrong Server]]></title>
            <link>https://medium.com/@jahangir80842/the-ssh-fingerprint-mistake-that-can-put-you-on-the-wrong-server-0d3f70dca832?source=rss-1e1a07bda9c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/0d3f70dca832</guid>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[linux]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <dc:creator><![CDATA[Jahangir]]></dc:creator>
            <pubDate>Wed, 16 Sep 2026 17:39:28 GMT</pubDate>
            <atom:updated>2026-09-16T17:39:28.190Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/935/1*KzZqmSFyxoaQ_ivPylsiBQ.png" /></figure><p>The Small Security Detail That Can Save You From Connecting to the Wrong Server</p><p>The first time you connect to a new Linux server using SSH, you usually see something like this:</p><pre>ssh ubuntu@192.168.1.50</pre><p>And SSH responds:</p><pre>The authenticity of host &#39;192.168.1.50 (192.168.1.50)&#39; can&#39;t be established.ED25519 key fingerprint is:<br>SHA256:Qf3j9Hh2pYx7L7n8G5bP0mC2E3kT9A1xV4ZsN6wJ8Ks<br>Are you sure you want to continue connecting (yes/no/[fingerprint])?<br></pre><p>Most people type:</p><pre>yes</pre><p>and continue.</p><p>But that fingerprint is not just some random technical information.</p><p>It is one of the most important security checks in SSH.</p><h3>What Is an SSH Fingerprint?</h3><p>An SSH fingerprint is a short representation of a public key.</p><p>Instead of comparing a long public key like this:</p><pre>ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBX...</pre><p>SSH calculates a hash of the key and displays something easier to compare:</p><pre>SHA256:Qf3j9Hh2pYx7L7n8G5bP0mC2E3kT9A1xV4ZsN6wJ8Ks</pre><p>Think of it like a human-readable identity card for a cryptographic key.</p><p>If two systems show the same fingerprint, they are referring to the same public key.</p><h3>Why Does SSH Show a Fingerprint?</h3><p>Because encryption alone is not enough.</p><p>Imagine you want to connect to:</p><pre>server.example.com</pre><p>You run:</p><pre>ssh admin@server.example.com</pre><p>Your computer can encrypt the SSH connection.</p><p>But there is another question:</p><p><strong>Are you actually talking to your server?</strong></p><p>An attacker sitting between you and the server could potentially pretend to be the destination.</p><p>This is known as a Man-in-the-Middle attack.</p><p>SSH fingerprints help protect against this.</p><p>Before trusting a new server, you can compare the fingerprint SSH shows you with the fingerprint stored on the actual server.</p><p>If both match, you have strong evidence that you are connecting to the correct machine.</p><h3>There Are Two Types of SSH Fingerprints You Should Understand</h3><p>People often say “SSH fingerprint” without explaining that two different things may be involved.</p><h3>Host key fingerprint</h3><p>This identifies the SSH server.</p><p>When you connect to a server for the first time, this is usually the fingerprint you see.</p><p>For example:</p><pre>ED25519 key fingerprint is SHA256:abc123...</pre><p>It tells your SSH client:</p><blockquote><em>This is the identity of the remote SSH server.</em></blockquote><p>The server’s public host keys are normally stored in:</p><pre>/etc/ssh/</pre><p>You may see files like:</p><pre>ssh_host_ed25519_key<br>ssh_host_ed25519_key.pub<br>ssh_host_rsa_key<br>ssh_host_rsa_key.pub</pre><p>The private file stays on the server.</p><p>The .pub file contains the public key.</p><h3>User key fingerprint</h3><p>This identifies your SSH public key.</p><p>For example, you might create a key with:</p><pre>ssh-keygen -t ed25519</pre><p>You then get:</p><pre>~/.ssh/id_ed25519<br>~/.ssh/id_ed25519.pub</pre><p>You can calculate its fingerprint with:</p><pre>ssh-keygen -lf ~/.ssh/id_ed25519.pub</pre><p>Example output:</p><pre>256 SHA256:xP4vHtM6Y9N2kJ7fA3sQ8L5mC1bW0rE2tU7iO9pK4Lg user@laptop (ED25519)</pre><p>This fingerprint identifies your SSH key.</p><h3>How to Check a Server’s SSH Fingerprint</h3><p>Suppose you have console access to the server.</p><p>You can check the ED25519 host fingerprint directly:</p><pre>sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub</pre><p>You might get:</p><pre>256 SHA256:Qf3j9Hh2pYx7L7n8G5bP0mC2E3kT9A1xV4ZsN6wJ8Ks root@server (ED25519)</pre><p>Now connect from your laptop:</p><pre>ssh admin@server.example.com</pre><p>If SSH displays:</p><pre>SHA256:Qf3j9Hh2pYx7L7n8G5bP0mC2E3kT9A1xV4ZsN6wJ8Ks</pre><p>the fingerprints match.</p><p>That is what you want.</p><h3>What Happens After You Type “yes”?</h3><p>SSH saves the server’s public key locally.</p><p>Usually in:</p><pre>~/.ssh/known_hosts</pre><p>From that point onward, SSH remembers the server.</p><p>Next time you connect, it silently compares the server’s key with the key stored in known_hosts.</p><p>If they match, SSH continues.</p><p>If they do not match, you get one of the most frightening-looking SSH messages:</p><pre>@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@<br>@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @<br>@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@</pre><p>This warning should not be ignored automatically.</p><h3>Why Would a Host Fingerprint Change?</h3><p>A fingerprint change does not always mean someone is attacking you.</p><p>There are many legitimate reasons.</p><p>For example:</p><pre>Server reinstalled<br>SSH host keys regenerated<br>VM replaced<br>Cloud instance recreated<br>IP address reused<br>Operating system rebuilt<br>Server migrated</pre><p>But there is also a more dangerous possibility:</p><pre>You are connecting to the wrong machine</pre><p>or:</p><pre>Someone is attempting a Man-in-the-Middle attack</pre><p>That is why the correct response is not immediately:</p><pre>ssh-keygen -R server.example.com</pre><p>The correct response is:</p><p><strong>Verify first. Remove the old key second.</strong></p><h3>How to Remove an Old Known Host Entry</h3><p>Once you confirm that the server was legitimately rebuilt, you can remove the old entry.</p><p>For a hostname:</p><pre>ssh-keygen -R server.example.com</pre><p>For an IP address:</p><pre>ssh-keygen -R 192.168.1.50</pre><p>Then reconnect:</p><pre>ssh admin@server.example.com</pre><p>SSH will ask you to trust the new fingerprint.</p><p>Verify it before accepting.</p><h3>See All Server Host Key Fingerprints</h3><p>You can check multiple SSH host keys with:</p><pre>for key in /etc/ssh/ssh_host_*_key.pub; do<br>    ssh-keygen -lf &quot;$key&quot;<br>done</pre><p>Example:</p><pre>256 SHA256:abc123... root@server (ED25519)<br>3072 SHA256:def456... root@server (RSA)<br>256 SHA256:ghi789... root@server (ECDSA)</pre><p>Modern systems commonly prefer ED25519.</p><h3>SHA256 vs MD5 Fingerprints</h3><p>Older SSH documentation sometimes shows fingerprints like:</p><pre>MD5:12:34:56:78:90:ab:cd:ef</pre><p>Modern OpenSSH normally displays SHA256:</p><pre>SHA256:Qf3j9Hh2pYx7L7n8G5bP0mC2E3kT9A1xV4ZsN6wJ8Ks</pre><p>You can explicitly display SHA256 with:</p><pre>ssh-keygen -lf ~/.ssh/id_ed25519.pub -E sha256</pre><p>Or MD5:</p><pre>ssh-keygen -lf ~/.ssh/id_ed25519.pub -E md5</pre><p>For modern environments, SHA256 should generally be preferred.</p><h3>A Real DevOps Example</h3><p>Imagine you create a new production server in AWS.</p><p>You receive:</p><pre>54.201.10.25</pre><p>You immediately run:</p><pre>ssh ubuntu@54.201.10.25</pre><p>SSH shows a fingerprint.</p><p>Instead of immediately typing yes, you open the AWS console or connect through a trusted management method.</p><p>On the server, you run:</p><pre>sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub</pre><p>You compare the result with what your laptop shows.</p><p>If they match:</p><pre>Laptop:<br>SHA256:4kLnJ2...<br>Server:<br>SHA256:4kLnJ2...</pre><p>you accept the connection.</p><p>Now you know you trusted the right machine from the beginning.</p><p>The same idea applies whether the server is running in:</p><pre>AWS EC2<br>Azure Virtual Machines<br>Huawei Cloud ECS<br>Hetzner<br>DigitalOcean<br>Proxmox<br>VMware<br>Bare metal<br>Your home lab</pre><p>SSH itself works the same way.</p><h3>Fingerprints Are Also Useful for Managing User Keys</h3><p>Suppose a server contains dozens of keys in:</p><pre>~/.ssh/authorized_keys</pre><p>Reading the entire public key is not convenient.</p><p>You can inspect fingerprints instead.</p><p>For example:</p><pre>ssh-keygen -lf ~/.ssh/authorized_keys</pre><p>This helps identify which keys belong to which administrators.</p><p>You might maintain an inventory like:</p><pre>Jagz Laptop<br>SHA256:AAAA...<br>CI/CD Runner<br>SHA256:BBBB...<br>Backup Server<br>SHA256:CCCC...</pre><p>That is much easier to audit than comparing massive public key strings.</p><h3>Fingerprints Become Even More Important in Automation</h3><p>In small environments, administrators often manually accept host keys.</p><p>In larger DevOps environments, that approach becomes dangerous.</p><p>You may have:</p><pre>Ansible<br>GitHub Actions<br>GitLab CI<br>Jenkins<br>Terraform provisioners<br>deployment scripts<br>backup servers<br>monitoring systems</pre><p>connecting to servers automatically.</p><p>Blindly disabling host verification using:</p><pre>-o StrictHostKeyChecking=no</pre><p>may make automation easier, but it removes an important security check.</p><p>A better approach is to manage trusted host keys intentionally.</p><p>For example:</p><pre>ssh-keyscan server.example.com &gt;&gt; ~/.ssh/known_hosts</pre><p>But even ssh-keyscan should not automatically be considered proof of identity.</p><p>If an attacker is intercepting the network at the moment you run it, you may simply record the attacker’s key.</p><p>The safest approach is to obtain the expected fingerprint through an independent trusted channel.</p><h3>The Security Idea Behind the Fingerprint</h3><p>The important idea is simple:</p><pre>Encryption protects the conversation.<br>Authentication proves who you are talking to.</pre><p>SSH provides both.</p><p>Your connection may be strongly encrypted, but if you encrypt a conversation with an attacker instead of your server, encryption has not solved the identity problem.</p><p>The host key and its fingerprint solve that part.</p><h3>A Simple Mental Model</h3><p>Think about SSH like meeting someone at an airport.</p><p>They tell you:</p><blockquote><em>I am the person you came to meet.</em></blockquote><p>You could simply believe them.</p><p>Or you could ask for identification.</p><p>The SSH host key is the identification.</p><p>The fingerprint is the easy-to-check summary printed on the ID.</p><p>Once you verify the person once, you remember what they look like.</p><p>That is essentially what:</p><pre>~/.ssh/known_hosts</pre><p>does.</p><h3>Commands Worth Remembering</h3><p>Check your public key fingerprint:</p><pre>ssh-keygen -lf ~/.ssh/id_ed25519.pub</pre><p>Check the server’s ED25519 fingerprint:</p><pre>sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub</pre><p>Remove an outdated host entry:</p><pre>ssh-keygen -R server.example.com</pre><p>Inspect your stored known hosts:</p><pre>cat ~/.ssh/known_hosts</pre><p>Scan a server’s published SSH host key:</p><pre>ssh-keyscan server.example.com</pre><h3>Easy Way to Remember It</h3><p>Remember:</p><p><strong>Fingerprint = identity, known_hosts = memory.</strong></p><p>SSH asks:</p><pre>Who is this server?</pre><p>The fingerprint answers:</p><pre>This is its identity.</pre><p>Then known_hosts says:</p><pre>I have seen this identity before.</pre><p>If the identity suddenly changes, SSH warns you.</p><p>That simple model explains most of SSH host key verification.</p><h3>Final Thought</h3><p>SSH is considered secure partly because it does not silently trust every server you connect to.</p><p>That strange-looking SHA256 string during your first connection is SSH giving you an opportunity to verify the machine’s identity.</p><p>Most administrators encounter SSH fingerprints every day.</p><p>Far fewer actually verify them.</p><p>And that is the important difference.</p><p>The next time SSH asks:</p><pre>Are you sure you want to continue connecting?</pre><p>do not think of it as an annoying prompt blocking your login.</p><p>Think of it as SSH asking one very important question:</p><p><strong>“Are you sure this is really your server?”</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0d3f70dca832" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Webhook vs API: The Difference That Finally Clicked for Me]]></title>
            <link>https://medium.com/@jahangir80842/webhook-vs-api-the-difference-that-finally-clicked-for-me-6dddbb2ab3e6?source=rss-1e1a07bda9c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/6dddbb2ab3e6</guid>
            <category><![CDATA[api]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[linux]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Jahangir]]></dc:creator>
            <pubDate>Thu, 10 Sep 2026 17:19:17 GMT</pubDate>
            <atom:updated>2026-09-10T17:19:17.301Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6-3x45laqeuN0vbxZiI_cA.png" /></figure><p>It&#39;s like a PULL and PUSH button on the door, but how, lets findout.</p><p>For a long time, I understood APIs.</p><p>At least, I thought I did.</p><p>One application needs data from another application.</p><p>It sends a request.</p><p>The other application sends a response.</p><p>Simple.</p><p>Then I started working with systems where one service needed to react immediately when something happened somewhere else.</p><p>A payment completed.</p><p>A GitHub push happened.</p><p>A new order arrived.</p><p>A trading signal appeared.</p><p>A deployment finished.</p><p>My first instinct was always the same:</p><p><strong>Call the API.</strong></p><p>And that is exactly where the problem starts.</p><p>Because sometimes the biggest difference between a good integration and a terrible one comes down to one question:</p><p><strong>Who should start the conversation?</strong></p><p>That question explains almost everything about APIs and webhooks.</p><h3>Imagine You Are Waiting for a Package</h3><p>Suppose you ordered a new laptop.</p><p>There are two ways to find out whether it has arrived.</p><p>The first method is to call the delivery company every five minutes.</p><blockquote><em>“Has my package arrived?”</em></blockquote><p>They say no.</p><p>Five minutes later:</p><blockquote><em>“Has my package arrived?”</em></blockquote><p>No.</p><p>Again.</p><p>And again.</p><p>Eventually:</p><blockquote><em>“Yes, it arrived.”</em></blockquote><p>Technically, this works.</p><p>But it is obviously inefficient.</p><p>This is basically what happens when you repeatedly call an API to check whether something has changed.</p><p>It is called <strong>polling</strong>.</p><p>Now imagine another approach.</p><p>You give the delivery company your phone number and say:</p><blockquote><em>“Call me when the package arrives.”</em></blockquote><p>You stop checking.</p><p>You continue doing your work.</p><p>When the package arrives, they contact you automatically.</p><p>That is basically a <strong>webhook</strong>.</p><p>And once I understood it this way, the difference became much easier to remember.</p><h3>API Means: “I Need Something From You”</h3><p>With an API, your application usually starts the communication.</p><p>Your application sends a request.</p><p>The remote system sends back a response.</p><p>For example:</p><pre>My App<br>   |<br>   | GET /users/42<br>   v<br>API Server<br>   |<br>   | User data<br>   v<br>My App</pre><p>You ask:</p><pre>GET /api/orders/123</pre><p>The API responds:</p><pre>{<br>  &quot;id&quot;: 123,<br>  &quot;status&quot;: &quot;paid&quot;<br>}</pre><p>The important part is this:</p><p><strong>Your application asked first.</strong></p><p>That makes APIs perfect when you need information or want to perform an action.</p><p>For example:</p><pre>POST /api/users</pre><p>Create a user.</p><pre>GET /api/products</pre><p>Get products.</p><pre>DELETE /api/server/42</pre><p>Delete a resource.</p><p>The client controls when the communication happens.</p><h3>Webhook Means: “Tell Me When Something Happens”</h3><p>A webhook reverses that relationship.</p><p>Instead of your application constantly asking:</p><pre>Did something happen?</pre><pre>Did something happen?</pre><pre>Did something happen?</pre><p>You provide another application with an endpoint.</p><p>For example:</p><pre>https://myapp.com/webhooks/payment</pre><p>Then you say:</p><blockquote><em>“Whenever a payment succeeds, send the event here.”</em></blockquote><p>Later, when a payment actually succeeds:</p><pre>Payment Provider<br>       |<br>       | POST payment event<br>       v<br>https://myapp.com/webhooks/payment</pre><p>The payload might look like this:</p><pre>{<br>  &quot;event&quot;: &quot;payment.completed&quot;,<br>  &quot;payment_id&quot;: &quot;pay_98372&quot;,<br>  &quot;amount&quot;: 100,<br>  &quot;currency&quot;: &quot;USD&quot;<br>}</pre><p>Your application did not ask for this information at that moment.</p><p>The external service sent it because an event occurred.</p><p>That is the core idea.</p><p><strong>APIs are usually request-driven.</strong></p><p><strong>Webhooks are event-driven.</strong></p><h3>The Mistake Many Developers Make</h3><p>Imagine your application needs to know when someone pays an invoice.</p><p>One solution is:</p><pre>while True:<br>    status = check_payment_status()</pre><pre>    if status == &quot;paid&quot;:<br>        process_payment()</pre><pre>    time.sleep(10)</pre><p>This means your application calls the payment API every 10 seconds.</p><p>Let us say the customer takes 20 minutes to pay.</p><p>That means:</p><pre>20 minutes<br>= 1200 seconds</pre><pre>1200 / 10<br>= 120 API requests</pre><p>119 of those requests might simply return:</p><pre>not paid</pre><p>Then the 120th request finally returns:</p><pre>paid</pre><p>Now imagine thousands of customers.</p><p>Suddenly your infrastructure is generating enormous amounts of traffic just asking:</p><blockquote><em>“Anything changed?”</em></blockquote><p>This is why webhooks are so powerful.</p><p>Instead of checking 120 times, the payment provider sends exactly one request when the payment happens.</p><h3>API Polling</h3><p>The flow looks like this:</p><pre>Your App ---&gt; API: Anything new?<br>Your App ---&gt; API: Anything new?<br>Your App ---&gt; API: Anything new?<br>Your App ---&gt; API: Anything new?<br>Your App ---&gt; API: Anything new?<br>API ---&gt; Your App: Yes, payment completed.</pre><p>Your application is constantly pulling information.</p><h3>Webhook</h3><p>With a webhook:</p><pre>Your App ---&gt; Payment Service:<br>Notify me at /webhook/payment<br>time passes...<br>Payment Service ---&gt; Your App:<br>Payment completed.</pre><p>Much cleaner.</p><p>Much faster.</p><p>Much less unnecessary traffic.</p><h3>But Here Is Something Important</h3><p>People often compare:</p><p><strong>Webhook vs API</strong></p><p>as if you must choose one.</p><p>That is slightly misleading.</p><p>In many real systems, you use <strong>both</strong>.</p><p>For example, imagine Stripe tells your server:</p><pre>payment.completed</pre><p>through a webhook.</p><p>Your application might then call Stripe’s API:</p><pre>GET /payments/pay_98372</pre><p>to retrieve additional information.</p><p>So the architecture becomes:</p><pre>Stripe<br>   |<br>   | Webhook<br>   v<br>Your Backend<br>   |<br>   | API request<br>   v<br>Stripe API<br>   |<br>   | Full payment details<br>   v<br>Your Backend</pre><p>The webhook tells you:</p><blockquote><em>“Something happened.”</em></blockquote><p>The API lets you ask:</p><blockquote><em>“Give me more information about what happened.”</em></blockquote><p>That combination is extremely common.</p><h3>A Real DevOps Example: GitHub</h3><p>Imagine you have a CI/CD system.</p><p>You want a deployment to start every time someone pushes code.</p><p>You could continuously call the GitHub API:</p><pre>Any new commits?<br>Any new commits?<br>Any new commits?<br>Any new commits?</pre><p>That would be terrible.</p><p>Instead, GitHub sends a webhook when someone pushes:</p><pre>Developer<br>    |<br>    | git push<br>    v<br>GitHub<br>    |<br>    | Webhook<br>    v<br>CI/CD Server<br>    |<br>    | Build<br>    | Test<br>    | Deploy<br>    v<br>Production</pre><p>The entire automation becomes event-driven.</p><p>This idea powers a huge part of modern DevOps.</p><p>GitHub Actions.</p><p>Jenkins.</p><p>Argo CD.</p><p>CI/CD pipelines.</p><p>Slack integrations.</p><p>Monitoring alerts.</p><p>Payment systems.</p><p>Automation platforms.</p><p>They all rely heavily on events.</p><h3>Another Example: Trading Systems</h3><p>Suppose a trading application needs to react when a signal appears.</p><p>One architecture is polling:</p><pre>Trading Bot<br>    |<br>    | API request<br>    v<br>Signal Server<br><br>&quot;Any signal?&quot;</pre><p>Five seconds later:</p><pre>&quot;Any signal?&quot;</pre><p>Again:</p><pre>&quot;Any signal?&quot;</pre><p>Instead, the signal provider can send:</p><pre>POST /webhook/signal</pre><p>with:</p><pre>{<br>  &quot;symbol&quot;: &quot;BTCUSDT&quot;,<br>  &quot;action&quot;: &quot;BUY&quot;,<br>  &quot;price&quot;: 112500<br>}</pre><p>Now the flow becomes:</p><pre>Signal Generator<br>       |<br>       | Webhook<br>       v<br>Trading Backend<br>       |<br>       | API request<br>       v<br>Exchange<br>       |<br>       v<br>Order Created</pre><p>This is where webhooks become very powerful.</p><p>They connect event-driven systems.</p><h3>So Which One Is Faster?</h3><p>Usually, webhook-based event notification is faster.</p><p>With polling every 60 seconds, an event might occur right after your last poll.</p><p>That means your application might wait almost 60 seconds before discovering it.</p><p>With webhooks, the provider can notify you almost immediately.</p><p>So instead of:</p><pre>Event happens<br>wait...<br>        wait...<br>        wait...<br>Next API poll<br>Event discovered</pre><p>you get:</p><pre>Event happens<br>      |<br>      v<br>Webhook sent<br>      |<br>      v<br>Application reacts</pre><p>This is why webhooks are common in real-time automation.</p><h3>But Webhooks Are Not Automatically Better</h3><p>Webhooks introduce their own problems.</p><p>Your webhook endpoint must be reachable.</p><p>For example:</p><pre>https://api.example.com/webhooks/github</pre><p>The external service needs to access it.</p><p>Your endpoint also needs to handle failures.</p><p>Imagine:</p><pre>Stripe ---&gt; Your Server<br>500 Internal Server Error</pre><p>What happens now?</p><p>A good webhook provider will retry.</p><p>Maybe after:</p><pre>10 seconds<br>1 minute<br>5 minutes<br>30 minutes</pre><p>This means your application must also be prepared to receive the same webhook more than once.</p><p>Which leads to one of the most important webhook concepts.</p><h3>Webhooks Must Be Idempotent</h3><p>Imagine a payment webhook arrives twice:</p><pre>payment.completed<br>payment.completed</pre><p>If your code does this:</p><pre>create_order()<br>send_product()</pre><p>twice, you might accidentally create two orders.</p><p>Instead, you usually store the event ID.</p><p>For example:</p><pre>{<br>  &quot;event_id&quot;: &quot;evt_12345&quot;<br>}</pre><p>Before processing it:</p><pre>if event_already_processed(event_id):<br>    return<br>process_event()<br>mark_as_processed(event_id)</pre><p>Now duplicate deliveries are safe.</p><p>This concept is called <strong>idempotency</strong>.</p><p>It is incredibly important when building webhook systems.</p><h3>Then Comes Security</h3><p>Here is another mistake beginners make.</p><p>They create:</p><pre>POST /webhook/payment</pre><p>and trust anything sent to it.</p><p>That means anyone on the internet might send:</p><pre>{<br>  &quot;event&quot;: &quot;payment.completed&quot;,<br>  &quot;amount&quot;: 100000<br>}</pre><p>Your application must verify that the webhook actually came from the real provider.</p><p>Most platforms solve this using cryptographic signatures.</p><p>For example:</p><pre>Webhook Payload<br>+<br>Secret Key<br>+<br>Signature</pre><p>Your server calculates the expected signature and compares it with the incoming one.</p><p>If they match:</p><pre>Accept webhook</pre><p>If not:</p><pre>Reject request</pre><p>This is why you often see headers such as:</p><pre>X-Hub-Signature<br>Stripe-Signature<br>X-Webhook-Signature</pre><p>Never blindly trust webhook payloads.</p><h3>API Authentication and Webhook Authentication Are Different</h3><p>With APIs, your application normally proves its identity to the server.</p><p>For example:</p><pre>Authorization: Bearer API_KEY</pre><p>The conversation looks like:</p><pre>Your App ---&gt; API<br>            &quot;Here is my token.&quot;</pre><p>With webhooks, the opposite side often needs to prove the event came from them.</p><pre>Webhook Provider ---&gt; Your App<br>                     &quot;Here is my signature.&quot;</pre><p>That difference is worth remembering.</p><h3>REST API vs Webhook</h3><p>Another common confusion is thinking webhook means some completely different protocol.</p><p>Usually, it does not.</p><p>A webhook often uses normal HTTP.</p><p>For example:</p><pre>POST /webhooks/order<br>Content-Type: application/json</pre><p>So technically, a webhook endpoint may itself be an HTTP API endpoint.</p><p>The difference is mainly <strong>how and why it gets called</strong>.</p><p>With a normal API:</p><pre>You decide when to send the request.</pre><p>With a webhook:</p><pre>An external event decides when the request is sent.</pre><h3>A Simple Comparison</h3><p>FeatureAPIWebhookWho starts communication?ClientEvent providerTypical modelRequest/responseEvent notificationReal-timeDepends on pollingUsually near real-timeTrafficCan be high with pollingUsually lowerBest forQuerying and actionsNotificationsDirectionYou call themThey call youPublic endpoint neededNot necessarilyUsually yesRetry handlingUsually client controlledProvider controlledDuplicate handlingLess commonVery important</p><h3>When Should You Use an API?</h3><p>Use an API when your application needs to request something.</p><p>Examples:</p><pre>Get user details<br>Create a VM<br>Start a deployment<br>Check inventory<br>Create an order<br>Update a database record<br>Get monitoring metrics</pre><p>Your application knows exactly when it needs the information.</p><h3>When Should You Use a Webhook?</h3><p>Use a webhook when your application needs to react to something.</p><p>Examples:</p><pre>Payment completed<br>GitHub code pushed<br>Customer registered<br>Order shipped<br>Server alert triggered<br>Build finished<br>Message received<br>Trading signal generated</pre><p>The external system knows when the event happens.</p><p>So let it tell you.</p><h3>The Architecture Pattern I Now Look For</h3><p>Whenever I design an integration, I ask:</p><p><strong>Do I need information right now?</strong></p><p>Use an API.</p><p>Or:</p><p><strong>Do I need to know when something happens?</strong></p><p>Use a webhook.</p><p>And sometimes the answer is:</p><p><strong>Both.</strong></p><p>For example:</p><pre>Customer pays<br>      |<br>      v<br>Payment Provider<br>      |<br>      | webhook<br>      v<br>Your Application<br>      |<br>      | API request<br>      v<br>Payment Provider<br>      |<br>      | details<br>      v<br>Your Application<br>      |<br>      v<br>Database<br>      |<br>      v<br>Email / Notification / Shipping</pre><p>This is one of the most common patterns in modern distributed systems.</p><h3>The Bigger Lesson Is Not About Webhooks</h3><p>The bigger lesson is about architecture.</p><p>Traditional systems often think like this:</p><pre>Check.<br>Check.<br>Check.<br>Check.<br>Check.</pre><p>Modern event-driven systems think like this:</p><pre>Something happened.</pre><pre>React.</pre><p>That small shift changes everything.</p><p>It reduces unnecessary requests.</p><p>It improves response time.</p><p>It makes automation easier.</p><p>It allows systems to become more loosely coupled.</p><p>And once you start recognizing this pattern, you see it everywhere.</p><p>GitHub push events.</p><p>Stripe payment events.</p><p>Slack messages.</p><p>Cloud events.</p><p>CI/CD pipelines.</p><p>Server monitoring.</p><p>Trading signals.</p><p>IoT devices.</p><p>Kafka streams.</p><p>Message queues.</p><p>Serverless functions.</p><p>Modern infrastructure is increasingly built around events.</p><h3>The Easiest Way to Remember It</h3><p>I use this mental shortcut:</p><p><strong>API = Ask</strong></p><p><strong>Webhook = Tell</strong></p><p>API:</p><blockquote><em>“Give me the latest order.”</em></blockquote><p>Webhook:</p><blockquote><em>“Tell me when a new order arrives.”</em></blockquote><p>Or even shorter:</p><pre>API     → I call you.<br>Webhook → You call me.</pre><p>That one sentence explains most of the difference.</p><p>And once you understand that, webhooks stop looking like another complicated backend concept.</p><p>They become something much simpler:</p><p><strong>A way for one system to say to another system:</strong></p><blockquote><em>“Don’t keep asking me. I’ll tell you when something happens.”</em></blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=6dddbb2ab3e6" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Docker Tags vs Digests: The Difference Every DevOps Engineer Should Know]]></title>
            <link>https://medium.com/@jahangir80842/docker-tags-vs-digests-the-difference-every-devops-engineer-should-know-ad500f7c7385?source=rss-1e1a07bda9c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/ad500f7c7385</guid>
            <category><![CDATA[kubernetes]]></category>
            <category><![CDATA[cloud-computing]]></category>
            <category><![CDATA[aws]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[ai]]></category>
            <dc:creator><![CDATA[Jahangir]]></dc:creator>
            <pubDate>Wed, 09 Sep 2026 06:06:00 GMT</pubDate>
            <atom:updated>2026-09-09T06:06:00.806Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/904/1*qwd44GgY45vR9ze91HEadg.png" /></figure><h3>Your Docker Image Tag Is Lying to You</h3><p>For years, this looked completely normal to me:</p><pre>services:<br>  api:<br>    image: mycompany/api:v1.4.2</pre><p>Version v1.4.2.</p><p>Clear. Predictable. Safe.</p><p>Or at least that is what it looks like.</p><p>Then you spend enough time working with Docker, CI/CD pipelines, Kubernetes, registries, and production systems, and eventually you discover something slightly uncomfortable:</p><p><strong>A Docker tag does not actually identify an image permanently.</strong></p><p>It is just a pointer.</p><p>And pointers can move.</p><p>That is where Docker digests enter the story.</p><h3>Imagine This Production Deployment</h3><p>Suppose our CI pipeline builds an application:</p><pre>docker build -t registry.example.com/payment-api:v2.3.0 .<br>docker push registry.example.com/payment-api:v2.3.0</pre><p>We test it.</p><p>Everything passes.</p><p>We deploy:</p><pre>image: registry.example.com/payment-api:v2.3.0</pre><p>Perfect.</p><p>A week later somebody rebuilds the image and pushes it using the same tag:</p><pre>docker push registry.example.com/payment-api:v2.3.0</pre><p>Now the registry contains a different image behind the same tag.</p><p>The deployment configuration still says:</p><pre>v2.3.0</pre><p>Nothing appears to have changed.</p><p>But the actual container image may have changed completely.</p><p>That is the important distinction.</p><h3>Docker Tags Are Names</h3><p>Most of us interact with Docker images through tags:</p><pre>nginx:latest<br>nginx:1.27<br>postgres:16<br>myapp:production<br>myapp:v2.4.1</pre><p>The basic structure is:</p><pre>repository:tag</pre><p>For example:</p><pre>nginx:1.27</pre><p>Here:</p><pre>nginx</pre><p>is the repository, while:</p><pre>1.27</pre><p>is the tag.</p><p>Tags are extremely useful because humans can understand them.</p><p>Compare:</p><pre>payment-api:v2.3.0</pre><p>with:</p><pre>payment-api@sha256:7d83f4ab32c917f...</pre><p>I know which one I would rather type.</p><p>But there is a trade-off.</p><p><strong>Tags are mutable.</strong></p><p>A registry can allow another image to be pushed using the same tag.</p><p>Think of a Docker tag like a DNS name.</p><p>You visit:</p><pre>example.com</pre><p>but the IP address behind it can change.</p><p>Similarly:</p><pre>myapp:v1</pre><p>is a convenient name pointing toward some image.</p><p>The image behind it can potentially change.</p><h3>Docker Digests Are Identities</h3><p>Docker also identifies images using content digests.</p><p>They normally look like this:</p><pre>sha256:2f7c5d8a4e6b...</pre><p>You can reference an image like this:</p><pre>nginx@sha256:abcdef123456...</pre><p>Instead of:</p><pre>nginx:1.27</pre><p>The major difference is that the digest is based on the image content.</p><p>If the image changes, the digest changes.</p><p>So:</p><pre>myapp@sha256:AAA</pre><p>and:</p><pre>myapp@sha256:BBB</pre><p>cannot silently become the same artifact.</p><p>That makes a digest extremely useful for reproducible deployments.</p><h3>Tag vs Digest</h3><p>The easiest mental model I use is this:</p><pre>Tag     = Name<br>Digest  = Identity</pre><p>Or:</p><pre>Tag     -&gt; What humans call the image<br>Digest  -&gt; What Docker proves the image is</pre><p>Consider Git.</p><p>A Docker tag is somewhat similar to a Git branch:</p><pre>main</pre><p>Today main might point to:</p><pre>abc123</pre><p>Tomorrow it might point to:</p><pre>def456</pre><p>The name stayed the same.</p><p>The code changed.</p><p>A Docker digest is more like a Git commit hash.</p><pre>abc123</pre><p>That commit identifies a specific state.</p><p>This analogy made Docker digests much easier for me to remember.</p><h3>The Most Dangerous Tag</h3><p>You can probably guess it.</p><pre>latest</pre><p>I still see production configurations such as:</p><pre>services:<br>  backend:<br>    image: company/backend:latest</pre><p>It feels convenient.</p><p>But what exactly is latest?</p><p>Version 2?</p><p>Version 5?</p><p>Yesterday’s build?</p><p>Today’s build?</p><p>The last successful build?</p><p>The build pushed by someone’s laptop ten minutes ago?</p><p>The answer is simply:</p><p><strong>Whatever image the registry currently associates with that tag.</strong></p><p>latest does not magically mean stable.</p><p>It is just a tag named latest.</p><h3>Even Version Tags Can Move</h3><p>A common response is:</p><blockquote><em>I don’t use </em><em>latest. I use semantic versions.</em></blockquote><p>For example:</p><pre>myapp:v1.7.3</pre><p>Much better.</p><p>But there is still an important detail.</p><p>Unless the registry prevents it, somebody can push another image using:</p><pre>v1.7.3</pre><p>again.</p><p>Now:</p><pre>v1.7.3</pre><p>points somewhere else.</p><p>This is why mature container registries often support <strong>immutable tags</strong>.</p><p>Once:</p><pre>v1.7.3</pre><p>is published, the registry refuses to let someone overwrite it.</p><p>That gives you much stronger guarantees.</p><p>But digest pinning provides an additional level of certainty.</p><h3>Finding a Docker Image Digest</h3><p>Pull an image:</p><pre>docker pull nginx:1.27</pre><p>Then inspect it:</p><pre>docker image inspect nginx:1.27</pre><p>Or:</p><pre>docker image inspect nginx:1.27 \<br>  --format=&#39;{{json .RepoDigests}}&#39;</pre><p>You may get something similar to:</p><pre>[<br>  &quot;nginx@sha256:abc123...&quot;<br>]</pre><p>Now instead of deploying:</p><pre>nginx:1.27</pre><p>you could deploy:</p><pre>nginx@sha256:abc123...</pre><p>That deployment now points to a specific artifact.</p><h3>Docker Compose With a Digest</h3><p>Instead of:</p><pre>services:<br>  web:<br>    image: nginx:1.27</pre><p>you can write:</p><pre>services:<br>  web:<br>    image: nginx@sha256:abc123...</pre><p>Now another push to:</p><pre>nginx:1.27</pre><p>does not change what your deployment uses.</p><p>Your Compose configuration references the exact image.</p><h3>Kubernetes Makes This Even More Important</h3><p>Suppose your Kubernetes Deployment contains:</p><pre>containers:<br>  - name: api<br>    image: registry.example.com/api:production</pre><p>The tag:</p><pre>production</pre><p>might move every time your CI pipeline runs.</p><p>That can make debugging painful.</p><p>Imagine three nodes pulling the image at different times.</p><p>Operationally, you want to be able to answer:</p><blockquote><em>Exactly which artifact is running in production?</em></blockquote><p>Not:</p><blockquote><em>Probably whatever </em><em>production pointed to when this pod started.</em></blockquote><p>A stronger deployment specification looks like:</p><pre>containers:<br>  - name: api<br>    image: registry.example.com/api@sha256:abc123...</pre><p>Now there is no ambiguity.</p><h3>What I Prefer in CI/CD</h3><p>I do not think tags are bad.</p><p>Actually, I want both.</p><p>A good CI/CD pipeline can produce:</p><pre>myapp:2.8.4<br>myapp:git-a8f61d2<br>myapp@sha256:7ae9...</pre><p>Each serves a purpose.</p><p>The semantic version is understandable:</p><pre>2.8.4</pre><p>The Git SHA gives traceability:</p><pre>git-a8f61d2</pre><p>And the digest provides artifact identity:</p><pre>sha256:7ae9...</pre><p>A pipeline might build:</p><pre>docker build \<br>  -t registry.example.com/myapp:${VERSION} \<br>  -t registry.example.com/myapp:${GIT_SHA} \<br>  .</pre><p>Then push both:</p><pre>docker push registry.example.com/myapp:${VERSION}<br>docker push registry.example.com/myapp:${GIT_SHA}</pre><p>After the push, obtain the digest.</p><p>Your deployment system can then promote the exact digest that passed testing.</p><p>This changes an important part of the release process.</p><p>Instead of:</p><pre>Build production image</pre><p>you aim for:</p><pre>Build once<br>        ↓<br>Test artifact<br>        ↓<br>Promote same artifact<br>        ↓<br>Deploy exact artifact</pre><p>That is a much stronger model.</p><h3>Build Once, Deploy Many</h3><p>This is one of the principles I care about most in DevOps.</p><p>Imagine this pipeline:</p><pre>DEV<br> ↓<br>build image</pre><pre>STAGING<br> ↓<br>rebuild image</pre><pre>PRODUCTION<br> ↓<br>rebuild image</pre><p>Those are technically three different builds.</p><p>Even if they use the same source code.</p><p>Dependencies could change.</p><p>Base images could change.</p><p>Package repositories could change.</p><p>Build scripts could behave differently.</p><p>Instead I prefer:</p><pre>Build<br>  ↓<br>Image Digest ABC<br>  ↓<br>Dev<br>  ↓<br>Staging<br>  ↓<br>Production</pre><p>The exact artifact moves through environments.</p><p>That means the container running in production is literally the artifact that survived your previous validation stages.</p><p>This is where digest-based deployment becomes extremely powerful.</p><h3>Rollbacks Become More Predictable</h3><p>Suppose release A has:</p><pre>sha256:111</pre><p>and release B has:</p><pre>sha256:222</pre><p>You deploy B.</p><p>Something goes wrong.</p><p>Rollback becomes:</p><pre>sha256:222<br>        ↓<br>sha256:111</pre><p>There is no question about what version the old tag currently points toward.</p><p>You already know the exact artifact.</p><p>This is particularly useful during incidents where the last thing you want is ambiguity.</p><h3>Digests Also Matter for Supply Chain Security</h3><p>Container security is no longer just:</p><pre>Is this image vulnerable?</pre><p>Another important question is:</p><pre>Is this actually the artifact we approved?</pre><p>Suppose your security pipeline scans:</p><pre>myapp:v4</pre><p>Everything passes.</p><p>Later someone replaces the image behind:</p><pre>myapp:v4</pre><p>If production only references that tag, the artifact you deploy may no longer be the artifact you scanned.</p><p>With digest-based promotion you can establish a stronger chain:</p><pre>Build<br>   ↓<br>sha256:ABC<br>   ↓<br>Scan ABC<br>   ↓<br>Sign ABC<br>   ↓<br>Approve ABC<br>   ↓<br>Deploy ABC</pre><p>Now security checks follow the artifact itself rather than merely its friendly name.</p><p>This becomes even more useful with tools such as:</p><pre>Cosign<br>Sigstore<br>SBOM generation<br>Trivy<br>Grype<br>Admission controllers</pre><p>The digest becomes the anchor connecting those controls.</p><h3>So Should We Stop Using Tags?</h3><p>No.</p><p>That would make working with Docker unnecessarily painful.</p><p>Tags are excellent for humans.</p><p>I still want:</p><pre>v2.5.0</pre><p>instead of having conversations like:</p><pre>Can you deploy sha256:74072e995c3cf...</pre><p>The better approach is:</p><pre>Humans discuss tags.<br>Machines deploy digests.</pre><p>For example, your release dashboard may show:</p><pre>Release: v2.5.0<br>Commit:  8fa8c21<br>Image:   sha256:92af...</pre><p>The developer sees:</p><pre>v2.5.0</pre><p>The DevOps pipeline sees:</p><pre>sha256:92af...</pre><p>Everybody wins.</p><h3>A Production-Friendly Strategy</h3><p>For systems I manage, a strong container release model looks something like this:</p><pre>Developer pushes code<br>        ↓<br>CI builds Docker image<br>        ↓<br>Tag with semantic version<br>        ↓<br>Tag with Git SHA<br>        ↓<br>Push to registry<br>        ↓<br>Capture image digest<br>        ↓<br>Run vulnerability scan<br>        ↓<br>Run integration tests<br>        ↓<br>Sign/approve artifact<br>        ↓<br>Deploy exact digest</pre><p>For example:</p><pre>payment-api:3.4.1<br>payment-api:8a721fe<br>payment-api@sha256:2839...</pre><p>The first is useful for people.</p><p>The second connects the container to Git.</p><p>The third tells infrastructure exactly what should run.</p><h3>One Small Docker Detail, Huge Operational Difference</h3><p>Docker tags feel like versions.</p><p>But technically they are references.</p><p>Docker digests are what identify the actual artifact.</p><p>That distinction becomes increasingly important as your infrastructure grows from:</p><pre>docker run</pre><p>to:</p><pre>Docker Compose<br>Kubernetes<br>GitOps<br>Argo CD<br>Flux<br>CI/CD pipelines<br>multiple environments<br>multiple clusters<br>software supply-chain controls</pre><p>At small scale, deploying:</p><pre>myapp:latest</pre><p>might feel perfectly fine.</p><p>At production scale, you eventually want to know:</p><pre>What exact code is running?<br>What exact image was tested?<br>What exact artifact was scanned?<br>What exact artifact should we roll back to?</pre><p>Docker digests give you much stronger answers to those questions.</p><p>So the mental model I now keep is extremely simple:</p><pre>Tags tell me what we call the release.</pre><p>Digests tell infrastructure what the release actually is.</p><p>And when production is involved, that difference matters.</p><p><strong>Tags are for humans. Digests are for certainty.</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ad500f7c7385" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I Thought CLAUDE.md Was Enough. Then I discovered intent.md, spec.md and plan.md]]></title>
            <link>https://blog.devgenius.io/i-thought-claude-md-was-enough-then-i-discovered-intent-md-spec-md-and-plan-md-a9e94252dc7c?source=rss-1e1a07bda9c1------2</link>
            <guid isPermaLink="false">https://medium.com/p/a9e94252dc7c</guid>
            <category><![CDATA[linux]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[ai]]></category>
            <category><![CDATA[claude]]></category>
            <category><![CDATA[aws]]></category>
            <dc:creator><![CDATA[Jahangir]]></dc:creator>
            <pubDate>Tue, 08 Sep 2026 06:11:00 GMT</pubDate>
            <atom:updated>2026-09-08T12:51:51.123Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/904/1*EqdctoWMydgBCEqWz5pneg.png" /></figure><p>AI Coding Is Moving From Prompt Engineering to Context Engineering. intent.md Might Be More Important Than Your Next 1,000-Line Prompt</p><p>A few days ago, I thought I had finally figured out how to work properly with AI coding agents.</p><p>I had learned about CLAUDE.md.</p><p>And it felt like a superpower.</p><p>Instead of repeatedly telling Claude:</p><p>“Use this architecture.”</p><p>“Do not touch this folder.”</p><p>“Run these tests.”</p><p>“Follow this coding style.”</p><p>“Use Docker for local development.”</p><p>I could put all of that information inside one file.</p><p>Claude could read it.</p><p>The project suddenly had memory.</p><p>I thought:</p><p><strong>This is it.</strong></p><p>This must be how serious developers use AI coding agents.</p><p>Then I discovered something that completely changed how I think about AI-assisted development.</p><p>CLAUDE.md is only one layer.</p><p>There can also be:</p><p>intent.md</p><p>spec.md</p><p>plan.md</p><p>architecture.md</p><p>tasks.md</p><p>decisions.md</p><p>And suddenly I realized something important.</p><p>We may be moving toward a world where developers do not only write code.</p><p>We also write <strong>context for machines</strong>.</p><h3>The Problem With AI Coding Is Usually Not the AI</h3><p>Imagine giving a very talented engineer access to your repository.</p><p>But you tell them almost nothing.</p><p>You say:</p><blockquote><em>“Build the authentication system.”</em></blockquote><p>That engineer now has dozens of questions.</p><p>What authentication method?</p><p>JWT?</p><p>Sessions?</p><p>OAuth?</p><p>Who are the users?</p><p>What database?</p><p>What security requirements?</p><p>What happens when the token expires?</p><p>Should refresh tokens exist?</p><p>Should there be MFA?</p><p>What should the API response look like?</p><p>An AI coding agent has exactly the same problem.</p><p>And yet this is how many of us use AI.</p><p>We give the agent one sentence.</p><p>Then we become surprised when it makes assumptions.</p><p>The agent writes technically correct code.</p><p>But it solves the wrong problem.</p><p>This is where structured context files become incredibly powerful.</p><h3>CLAUDE.md Tells the Agent How to Work</h3><p>Think of CLAUDE.md as the project&#39;s operating manual.</p><p>It can contain information such as:</p><pre>Use Python 3.12.</pre><pre>Use FastAPI for APIs.</pre><pre>Use PostgreSQL.</pre><pre>Use pytest for tests.</pre><pre>Never modify database migrations manually.</pre><pre>Run tests before completing a task.</pre><pre>Use Docker Compose for local development.</pre><p>This is extremely useful.</p><p>But notice something.</p><p>These instructions describe <strong>how the agent should behave</strong>.</p><p>They do not necessarily explain <strong>why the project exists</strong>.</p><p>That is where things become more interesting.</p><h3>intent.md Explains Why We Are Building Something</h3><p>Imagine a project contains this:</p><pre># Intent<br>We are building a simple deployment platform for small development teams.<br>The goal is to allow a developer to connect a Git repository and deploy an application without manually managing servers.<br>The product should feel simpler than Kubernetes.<br>Users should not need infrastructure knowledge.</pre><p>This tiny file changes everything.</p><p>Now the AI understands the direction of the product.</p><p>If the agent needs to choose between:</p><p>A complex Kubernetes architecture</p><p>or</p><p>A simple Docker-based deployment system</p><p>the intent helps guide the decision.</p><p>Without intent, the AI might ask:</p><p>“What is technically possible?”</p><p>With intent, it can ask:</p><p>“What best matches the product we are trying to build?”</p><p>That is a huge difference.</p><h3>spec.md Explains What Must Actually Exist</h3><p>Now imagine another file.</p><pre># Authentication Specification<br><br>Users must be able to:<br>- register with email and password<br>- verify their email<br>- log in<br>- request password reset<br>- refresh access tokens<br>Access tokens expire after 15 minutes.<br>Refresh tokens expire after 30 days.<br>Passwords must be hashed using Argon2.<br>The API must return JSON responses.</pre><p>Now the AI does not have to invent requirements.</p><p>The requirements already exist.</p><p>This reduces one of the biggest problems with AI coding:</p><p><strong>guessing.</strong></p><p>A good specification turns an AI coding agent from:</p><p>“something that generates code”</p><p>into:</p><p>“something that implements decisions.”</p><h3>plan.md Can Tell the Agent How to Get There</h3><p>Then you can go even further.</p><pre># Implementation Plan<br><br>1. Create user database model<br>2. Add password hashing<br>3. Add registration endpoint<br>4. Add login endpoint<br>5. Add JWT generation<br>6. Add refresh token support<br>7. Add password reset<br>8. Add tests<br>9. Update API documentation</pre><p>Now something interesting happens.</p><p>The AI no longer needs to solve the whole problem in one giant prompt.</p><p>It can work through a sequence.</p><p>This matters because large AI coding tasks often fail for the same reason large human projects fail.</p><p>Too much ambiguity.</p><p>Too many decisions.</p><p>Too much happening at once.</p><p>A plan reduces the search space.</p><h3>This Starts Looking Less Like Prompt Engineering</h3><p>And more like software engineering.</p><p>That was the part that surprised me.</p><p>For a long time, AI coding conversations looked like this:</p><pre>Me:<br>Build feature X.<br><br>AI:<br>Here is some code.</pre><p>Then prompt engineering became popular.</p><p>People started writing longer prompts.</p><p>Then system prompts.</p><p>Then repository instructions.</p><p>But structured files like:</p><pre>CLAUDE.md<br>intent.md<br>spec.md<br>plan.md<br>architecture.md<br>tasks.md</pre><p>feel like the next step.</p><p>We are no longer writing one giant prompt.</p><p>We are creating a <strong>machine-readable development environment</strong>.</p><h3>A Simple Mental Model</h3><p>Here is how I now think about these files.</p><h3>CLAUDE.md</h3><p><strong>How should you work?</strong></p><p>Coding rules, commands, conventions, constraints.</p><h3>intent.md</h3><p><strong>Why are we building this?</strong></p><p>Business goals, user goals, philosophy, product direction.</p><h3>spec.md</h3><p><strong>What exactly should be built?</strong></p><p>Requirements and expected behavior.</p><h3>plan.md</h3><p><strong>How will we build it?</strong></p><p>Implementation sequence.</p><h3>architecture.md</h3><p><strong>How is the system designed?</strong></p><p>Services, databases, queues, APIs, boundaries.</p><h3>tasks.md</h3><p><strong>What should be done next?</strong></p><p>Concrete pieces of work.</p><h3>decisions.md</h3><p><strong>Why did we choose this approach?</strong></p><p>Important architectural decisions and trade-offs.</p><h3>Imagine an AI Agent Entering This Repository</h3><p>It opens the project.</p><p>It reads:</p><pre>CLAUDE.md</pre><p>Now it knows how to behave.</p><p>Then:</p><pre>intent.md</pre><p>Now it understands the purpose.</p><p>Then:</p><pre>spec.md</pre><p>Now it understands the requirements.</p><p>Then:</p><pre>architecture.md</pre><p>Now it understands the system.</p><p>Then:</p><pre>plan.md</pre><p>Now it understands the execution strategy.</p><p>At that point, the agent is no longer entering the project blind.</p><p>It has something surprisingly close to what a new engineer receives during good onboarding.</p><p>And that may be the real breakthrough.</p><h3>AI Agents Need Onboarding Too</h3><p>Companies spend days onboarding engineers.</p><p>Here is the architecture.</p><p>Here are our standards.</p><p>Here is why we built the system this way.</p><p>Here is what the product is trying to achieve.</p><p>Here are the current priorities.</p><p>Here are the things you must never break.</p><p>But when we open an AI coding agent, we often give it none of that.</p><p>Then we expect senior-engineer-level decisions.</p><p>That is probably unrealistic.</p><p>Maybe the future of AI development is not about finding the perfect prompt.</p><p>Maybe it is about building the perfect <strong>context environment</strong>.</p><h3>Context Is Becoming Part of the Codebase</h3><p>This is the part I find fascinating.</p><p>Repositories traditionally contain:</p><pre>source code<br>tests<br>configuration<br>documentation<br>CI/CD<br>infrastructure</pre><p>Now they may increasingly contain another layer:</p><pre>AI context</pre><p>Files written specifically to help intelligent agents understand the project.</p><p>That means documentation is no longer only for humans.</p><p>It becomes part of the execution environment.</p><p>A missing specification might soon be almost as important as a missing configuration file.</p><h3>The Developers Who Win With AI May Not Be the Best Prompters</h3><p>They may be the people who are best at defining:</p><ul><li>intent</li><li>constraints</li><li>architecture</li><li>specifications</li><li>plans</li><li>acceptance criteria</li></ul><p>Because once those things are clear, AI can generate a surprising amount of the implementation.</p><p>This changes the developer’s role.</p><p>Instead of spending all day telling the computer:</p><p><strong>how to write every line</strong></p><p>we may spend more time telling it:</p><p><strong>what the system should become.</strong></p><h3>The Repository of the Future Might Look Like This</h3><pre>project/<br>│<br>├── CLAUDE.md<br>├── intent.md<br>├── spec.md<br>├── architecture.md<br>├── plan.md<br>├── tasks.md<br>│<br>├── src/<br>├── tests/<br>├── infrastructure/<br>└── docker-compose.yml</pre><p>The code is still there.</p><p>But the instructions surrounding the code become much richer.</p><p>The AI can understand the project before touching it.</p><h3>And This Is Where Things Get Really Interesting</h3><p>Imagine multiple AI agents.</p><p>One reads the specification.</p><p>One designs the architecture.</p><p>One implements the backend.</p><p>One writes tests.</p><p>One reviews security.</p><p>One updates documentation.</p><p>All of them reading the same shared context.</p><p>At that point, these Markdown files are not just documentation anymore.</p><p>They are almost like an <strong>API between humans and AI agents</strong>.</p><p>Humans define intent.</p><p>Agents transform intent into software.</p><h3>My Biggest Realization</h3><p>When I first learned about CLAUDE.md, I thought:</p><blockquote><em>“Great. Now Claude understands my coding rules.”</em></blockquote><p>After learning about intent.md, spec.md, and similar files, my thinking changed.</p><p>Now I think the bigger idea is:</p><blockquote><strong><em>AI coding agents need structured context just like human engineers need good documentation.</em></strong></blockquote><p>The better the context, the less the agent guesses.</p><p>The less it guesses, the better the implementation.</p><p>And that means the next generation of great developers may become extremely good at something we previously underestimated.</p><p><strong>Writing clear instructions about what software should be.</strong></p><p>Not just writing the software itself.</p><p>That is a very different skill.</p><p>And I have a feeling it is going to become one of the most important engineering skills of the AI era.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a9e94252dc7c" width="1" height="1" alt=""><hr><p><a href="https://blog.devgenius.io/i-thought-claude-md-was-enough-then-i-discovered-intent-md-spec-md-and-plan-md-a9e94252dc7c">I Thought CLAUDE.md Was Enough. Then I discovered intent.md, spec.md and plan.md</a> was originally published in <a href="https://blog.devgenius.io">Dev Genius</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>