S&OP Engineering V: The Autonomous Brain (Agentic AI)

1. The Execution Chasm

Companies invest millions in planning software and ERPs. They spend months integrating data, cleaning records, and tuning demand forecasts. Yet, at the end of the day, execution often relies on an analyst staring at a dashboard. A human reading charts, finally drafting an email to the plant director to request production adjustments.

That human delay is inefficient, error-prone, and, frankly, unnecessary in the modern era. Pure mathematics has no value if it isn’t executed.

The solution isn’t a rigid automation script, nor is it a chatbot you ask questions to. True modernization requires Agentic AI. We are not talking about ChatGPT. We are talking about Autonomous Agents: software entities with a specific role, a clear goal, and integrated with Tools that allow them to interact with real-world databases and APIs.

2. The Architecture: CrewAI + Supabase

For this Autonomous Brain, we have implemented a multi-agent architecture using CrewAI. Our digital team (“The Agency”) is composed of two key agents:

  • Senior Supply Chain Analyst: Its job is purely analytical. We don’t give it a PDF to summarize; we equipped it with a Python @tool that allows it to execute SQL queries against our Supabase database. It extracts the optimal production plan generated by PuLP in Chapter 4, identifies bottlenecks, and pinpoints the months where production stress is most critical.
  • Procurement & Plant Manager: Acts as the executor. Based strictly on the mathematical conclusions drawn by the Analyst, it drafts the definitive executive orders.

Crucial fact: They do not hallucinate. Their decisions are anchored in hard data mathematically retrieved from the central database.

3. The Code: Tools over Prompts

The true power of Agentic AI lies in providing LLMs with the ability to interact with external systems. The C-Suite must understand that we aren’t handing an Excel sheet to a language model; we are embedding our code with the ability to “think” and execute actions using the ReAct (Reasoning and Acting) pattern.

Here is a snippet of how we connect our Agent to Supabase:

from crewai.tools import tool
from supabase import create_client

@tool("fetch_latest_supply_plan")
def fetch_latest_supply_plan() -> str:
    """
    Connects to the Supabase database, finds the most recent execution of the 
    optimized production plan, and extracts the strategic plan for the 
    next 3 months across all SKUs.
    """
    supabase = create_client(SUPABASE_URL, SUPABASE_KEY)

    # 1. Find the most recent execution
    latest = supabase.table("supply_plans").select("execution_date").order("execution_date", desc=True).limit(1).execute()
    exec_date = latest.data[0]["execution_date"]

    # 2. Extract the optimal plan generated by PuLP
    response = supabase.table("supply_plans").select("*").eq("execution_date", exec_date).execute()

    # ... Format into structured Markdown to prevent hallucinations ...
    return report

# Instantiate the Analyst Agent by connecting it to the tool
analyst_agent = Agent(
    role='Senior Supply Chain Analyst',
    goal='Analyze the master production plan to detect bottlenecks and inventory risks.',
    backstory='You are a relentless analyst seeking capital efficiency. You hate excess inventory but fear stockouts...',
    verbose=True,
    allow_delegation=False,
    tools=[fetch_latest_supply_plan], # <-- We inject Database access here
    llm=gemini_llm
)

By compiling the agents into a Process.sequential pipeline, we orchestrate a workflow where the Analyst’s output directly fuels the actions of the Plant Manager.

4. The Money Shot

If we execute our “Crew”, this is exactly what the model returns in the terminal after querying the database and reasoning around the factory’s 12,000 monthly unit capacity limit:

[!NOTE]
SUBJECT: URGENT – July-September Production Plan | Bottleneck Alert

Hi Carlos,

After running the mathematical optimization model for the upcoming quarter, I confirm the Master Production Plan. We are facing a critical capacity bottleneck (12,000 units/month) during the summer that requires strict execution.

July (100% Capacity – Critical Risk):
The entire plant capacity must be exclusively dedicated to SKU-001 (12,000 units). Halt the SKU-002 and SKU-003 lines this month. The model has pulled forward this production to prevent stockouts in the following months.

August (100% Capacity – Production Mix):
The factory will remain at its limit. The distribution will be:

  • SKU-001: 5,000 units
  • SKU-002: 15 units
  • SKU-003: 6,985 units (High priority for the seasonal campaign)

September (83% Capacity – Stabilization):
The pressure will decrease. We will manufacture 8,000 units of SKU-001, 10 units of SKU-002, and 2,000 units of SKU-003, ensuring a buffer for line maintenance.

Please coordinate with purchasing to secure the raw materials for SKU-001 immediately for the first week of July. The algorithm leaves no margin for error regarding the 12,000 units for that month.

Regards,
Agentic S&OP Copilot (On behalf of Operations Management)

This email isn’t a generic template. It is an operational, justified, and unbiased decision, generated autonomously from a linear programming model and delivered in seconds.

5. The Sandbox

Want to see how they reason in real-time? I have prepared an interactive notebook so you can test the system yourself.

👉 Open the Agentic S&OP Google Colab

For security reasons, we have simulated the database return in the public environment. But if you insert your own Gemini API Key (it’s free), you will be able to watch the agents’ step-by-step reasoning process in the logs before they issue the final order.

6. Series Conclusion: The CTO Pitch

We have reached the end of our “S&OP Engineering” journey. Over the course of five chapters, we have evolved enterprise planning from its grimiest foundations to the technological bleeding edge:

  1. Foundations: We cleaned and normalized years of unstructured historical data.
  2. Vision: We implemented iterative Artificial Intelligence engines to forecast future demand with high precision (Prophet).
  3. Logic: We introduced Decision Sciences and Linear Programming to balance inventory constraints (PuLP).
  4. Scale: We elevated the mathematical model to the Enterprise level, dealing with shared capacity constraints and parallelized MLOps.
  5. Execution: We gave the system an Autonomous Brain to bridge the gap between the Dashboard and the real world.

The CTO Pitch: If your company is still planning demand with moving averages in Excel, you are bleeding money. Capital efficiency, stockout reduction, and intelligent workflow automation are no longer the future; they are the operational standard demanded by today’s margins.

It’s time to retire static models. Let’s talk about how to modernize your Supply Chain from its roots all the way to autonomous execution.

Supply Chain Attacks on Developers: Lessons from LiteLLM and Trivy

Supply chain attacks on developers have escalated dramatically in early 2026. Two major incidents, LiteLLM and Trivy, exposed thousands of projects to credential theft, backdoors, and potential data breaches.

These attacks represent a fundamental shift in how threat actors target software development. Instead of attacking finished applications, they compromise the tools developers use to build them.

The LiteLLM PyPI Compromise

In March 2026, malicious versions of LiteLLM appeared on PyPI, affecting thousands of Python projects.

What Happened:

Versions 1.82.7 and 1.82.8 of LiteLLM contained credential theft mechanisms and persistent backdoors. The attack was sophisticated, using obfuscated code and delayed execution to evade detection.

Attack Mechanism:

The malicious code activated 24 hours after installation, making it harder to correlate with the package update. It exfiltrated environment variables, including API keys and database credentials, to attacker-controlled servers.

Impact:

Over 15,000 projects downloaded the compromised versions before discovery. Major organizations using LiteLLM for LLM abstraction had to rotate credentials and audit access logs.

Discovery:

Security researchers at FutureSearch.ai identified the compromise through behavioral analysis. The malware attempted network connections to unusual domains, triggering automated alerts FutureSearch.ai, 2026.

The Trivy GitHub Actions Compromise

Trivy, a popular container vulnerability scanner, was compromised through its GitHub Actions integration.

Attack Vector:

Threat actors gained access to Trivy’s GitHub repository and pushed malicious tags. These tags were picked up by CI/CD pipelines worldwide, executing attacker-controlled code in build environments.

Scope:

The compromise affected any project using Trivy GitHub Actions with floating version tags. CI/CD secrets, including cloud provider credentials and deployment tokens, were exposed.

Exploitation:

The malicious code ran with the permissions of the CI/CD pipeline. In many cases, this included write access to container registries, production deployments, and infrastructure management.

Response:

Socket.dev and other security firms coordinated disclosure. GitHub revoked compromised tokens and worked with maintainers to secure the repository Socket.dev, 2026.

How Supply Chain Attacks Work

Understanding attack vectors is essential for defense.

Typosquatting:

Attackers publish packages with names similar to popular libraries. Developers mistype imports and install malware. Examples include reqeusts instead of requests, djano instead of django.

Dependency Confusion:

Public packages with the same name as internal packages take precedence in some package managers. Attackers upload public versions with higher version numbers, tricking build systems.

Compromised Maintainer Accounts:

Phishing attacks on package maintainers give attackers legitimate publishing credentials. These attacks are hard to detect since the packages come from trusted sources.

Build System Compromise:

Attackers target the infrastructure that builds packages. This was the vector for the Trivy compromise and the 2024 XZ Utils backdoor attempt.

Malicious Updates:

Legitimate packages are compromised and used to distribute malware. The LiteLLM attack followed this pattern, with attackers gaining access to the PyPI publishing pipeline.

Protecting Your Projects

Defense requires multiple layers of security.

Pin Dependencies:

Use exact version numbers in requirements files. Avoid floating versions like package>=1.0. Pin to specific hashes where possible.

# requirements.txt with hashes
litellm==1.82.6 
    --hash=sha256:abc123...

Private Registries:

Mirror public packages to private registries you control. Scan packages before internal distribution. This adds a review layer between public repositories and your builds.

Automated Scanning:

Integrate security scanning into CI/CD pipelines. Tools like Snyk, Socket.dev, and GitHub Advanced Security detect known malicious packages and vulnerable dependencies.

Least Privilege:

Run CI/CD pipelines with minimal permissions. Separate build, test, and deployment credentials. Use short-lived tokens with limited scope.

Dependency Review:

Audit new dependencies before adding them. Check maintainer reputation, update frequency, and community adoption. Avoid abandoned or single-maintainer projects for critical functionality.

Industry Response and Best Practices

The security community has responded with new tools and standards.

Software Bills of Materials (SBOM):

SBOMs inventory all dependencies and their versions. They enable rapid vulnerability assessment when new threats emerge. Executive Order 14028 mandates SBOMs for software sold to the US government.

Sigstore and Artifact Signing:

Sigstore provides free certificate authority and transparency logs for software artifacts. Signed packages can be verified against tampering. Adoption is growing across major package registries.

Package Manager Improvements:

PyPI, npm, and other registries have implemented stricter authentication, mandatory 2FA for maintainers, and malware scanning. These measures reduce but do not eliminate risk.

Security Frameworks:

NIST SSDF, OWASP SAMM, and SLSA provide frameworks for secure software development. Following these standards reduces supply chain attack surface.

The Economic Impact

Supply chain attacks have measurable business consequences.

Remediation Costs:

The average supply chain breach costs $4.5 million to remediate. This includes credential rotation, forensic analysis, legal fees, and reputation damage.

Development Velocity:

Security reviews slow development. Teams spend 15-20% more time on dependency management and security audits post-incident.

Insurance:

Cyber insurance premiums have increased 50% for software companies. Insurers now require SBOMs, dependency scanning, and incident response plans.

Regulatory:

The SEC now requires public companies to disclose material cybersecurity incidents within four days. Supply chain attacks affecting customer data trigger these disclosures.

FAQ

What is a supply chain attack?

A supply chain attack targets the tools, libraries, and services used to build software rather than the final application. By compromising upstream dependencies, attackers gain access to all downstream projects. These attacks are difficult to detect since the malicious code comes from trusted sources CISA, 2026.

How can I check if my project is affected?

Audit your dependency files (requirements.txt, package.json, Cargo.toml) for compromised versions. Use pip list or npm list to see installed packages. Security tools like Snyk and GitHub Dependabot alert on known malicious versions automatically.

What should I do if I used a compromised package?

Immediately rotate all credentials that may have been exposed. Check logs for unauthorized access. Update to clean versions of the package. Audit your codebase for signs of compromise. Notify security teams and affected users if sensitive data was accessed.

Are private package registries safer?

Private registries add a layer of control but are not inherently safer. They must be maintained, scanned, and updated like public registries. The benefit is the ability to review packages before internal distribution and control update timing.

How do I prevent dependency confusion attacks?

Use scoped packages (e.g., @company/package) for internal libraries. Configure package managers to prioritize private registries. Implement namespace reservation on public registries for your organization.

What is the future of supply chain security?

Expect mandatory code signing, SBOM requirements, and automated vulnerability scanning to become standard. Supply chain security will be integrated into DevSecOps pipelines by default. Regulatory requirements will drive adoption of secure development practices.

Conclusion

The LiteLLM and Trivy compromises are warnings, not anomalies. Supply chain attacks are increasing in frequency and sophistication. Development teams must treat dependencies as potential attack vectors.

The solution is not to avoid third-party code. Modern software development depends on open source. The solution is to manage dependencies with security in mind.

Pin versions. Scan for vulnerabilities. Use private registries. Implement least privilege. Build SBOMs. Sign artifacts. These practices add friction but prevent catastrophe.

The attackers are targeting your tools. Secure them before they do.

Pooya Golchian is an AI Engineer and Full Stack Developer specializing in secure software development. Follow him on Twitter @pooyagolchian for more insights on cybersecurity and development.

Top 7 AI Agent Frameworks in 2026: A Developer’s Comparison Guide

Top 7 AI Agent Frameworks in 2026: A Developer’s Comparison Guide
March 24, 2026 · 12 min read · By Paxrel

    There are now at least seven production-grade AI agent frameworks competing for your stack. Each takes a fundamentally different approach to the same problem: how do you build autonomous systems that actually work?

    We run an [AI agents newsletter](https://buttondown.com/paxrel) that's itself powered by an autonomous agent pipeline. We've read the docs, tried the APIs, and tracked the ecosystem since these tools launched. Here's our honest take on each framework as of March 2026.

    ## The Quick Comparison




                Framework
                Best For
                Model Lock-in
                Learning Curve



            **LangGraph**Complex stateful workflowsNoneSteep
            **CrewAI**Team-based agent systemsNoneEasy
            **AutoGen**Multi-agent conversationsNoneMedium
            **OpenAI Agents SDK**Fast prototyping with GPTOpenAI onlyEasy
            **Claude Agent SDK**Secure, sandboxed agentsClaude onlyMedium
            **Google ADK**Multimodal + A2AGemini-firstMedium
            **Smolagents**Lightweight code agentsNoneEasy



    ## 1. LangGraph
    Production-ready Model-agnostic Stateful

    LangGraph is the heavyweight. Built on top of LangChain, it models your agent's workflow as a directed graph where nodes are actions and edges are transitions. Every step gets checkpointed, so if your agent crashes mid-task, it picks up exactly where it left off.

    ### Why developers pick it

        - **Built-in persistence:** State checkpointing means long-running agents survive failures
        - **Explicit control flow:** You define exactly how your agent moves between steps
        - **Human-in-the-loop:** Native support for approval gates and manual overrides
        - **Model-agnostic:** Swap between OpenAI, Claude, Gemini, or local models


    ### The trade-offs

        - Steep learning curve &mdash; you need to think in graphs, not scripts
        - Verbose for simple tasks (50+ lines for a basic agent)
        - LangChain dependency brings complexity even if you only want the graph layer
# LangGraph: define a simple agent as a graph
from langgraph.graph import StateGraph, END

graph = StateGraph(AgentState)
graph.add_node("research", research_node)
graph.add_node("write", write_node)
graph.add_node("review", review_node)

graph.add_edge("research", "write")
graph.add_conditional_edges("review", should_revise,
    {"revise": "write", "approve": END})

agent = graph.compile(checkpointer=MemorySaver())
        **Pick LangGraph if:** You're building production agents that need state persistence, complex branching logic, or human approval steps. This is the framework most teams choose when "it has to work reliably at scale."



    ## 2. CrewAI
    Beginner-friendly Role-based YAML config

    CrewAI takes a completely different approach: instead of graphs, you define a *crew* of agents with roles, goals, and backstories. They collaborate on tasks like a team of specialists. It's the most intuitive framework if you're coming from a product or business background.

    ### Why developers pick it

        - **Lowest barrier to entry:** Define agents in YAML, not code
        - **Role-based metaphor:** "Researcher", "Writer", "Editor" &mdash; feels natural
        - **Active community:** Large Discord, frequent updates, growing ecosystem
        - **Built-in delegation:** Agents can hand off work to each other automatically


    ### The trade-offs

        - Less control over execution flow compared to LangGraph
        - Debugging multi-agent interactions can be opaque
        - Performance overhead from the role-playing abstraction layer
# CrewAI: define a research crew
from crewai import Agent, Task, Crew

researcher = Agent(
    role="AI Research Analyst",
    goal="Find the latest AI agent developments",
    backstory="Senior tech journalist with 10 years experience"
)

writer = Agent(
    role="Technical Writer",
    goal="Turn research into clear, actionable content",
    backstory="Dev advocate who writes for a 50k newsletter"
)

crew = Crew(agents=[researcher, writer], tasks=[...])
result = crew.kickoff()
        **Pick CrewAI if:** You want to prototype multi-agent workflows fast, your agents represent distinct roles, or your team includes non-engineers who need to understand the architecture. Best for content pipelines, research workflows, and business automation.



    ## 3. AutoGen (Microsoft)
    Multi-agent chat Conversation patterns Mature

    AutoGen pioneered the multi-agent conversation pattern. Agents talk to each other in structured dialogues &mdash; debates, round-robins, sequential chains. It's the most natural framework for tasks that require consensus or iterative refinement.

    ### Why developers pick it

        - **Conversation-first:** Agents interact through dialogue, not function calls
        - **Group chat:** Multiple agents can debate, vote, and reach consensus
        - **Code execution:** Built-in sandboxed Docker execution for generated code
        - **Flexible patterns:** Sequential, round-robin, or custom conversation flows


    ### The trade-offs

        - Microsoft shifted focus to the broader Agent Framework &mdash; AutoGen is in maintenance mode
        - Conversation-based routing can be unpredictable for deterministic workflows
        - Token costs add up fast when agents chat with each other



        **Note:** Microsoft announced the broader "Microsoft Agent Framework" in late 2025. AutoGen still works and has a large user base, but new investment is going into the platform-level framework. Worth watching, but probably not the best bet for greenfield projects in 2026.




        **Pick AutoGen if:** Your agents need to have multi-party conversations &mdash; group debates, consensus-building, or iterative code review. The conversation patterns are still the most diverse of any framework.



    ## 4. OpenAI Agents SDK
    Official OpenAI Batteries included Simple API

    The successor to the experimental Swarm project. OpenAI's official take on agent building: minimal boilerplate, built-in tools (code interpreter, web browsing, file handling), and native guardrails. If you're already in the OpenAI ecosystem, this is the path of least resistance.

    ### Why developers pick it

        - **Zero-config tools:** Code interpreter, web browsing, and file search out of the box
        - **Handoffs:** Agents can transfer control to specialized sub-agents cleanly
        - **Built-in guardrails:** Input/output validation without extra libraries
        - **Tracing:** Native observability for debugging agent behavior


    ### The trade-offs

        - Locked to OpenAI models &mdash; no Claude, no Gemini, no local models
        - Less customizable than LangGraph for complex state management
        - Relatively new &mdash; ecosystem and community still growing
# OpenAI Agents SDK: simple agent with tools
from agents import Agent, Runner

agent = Agent(
    name="Research Assistant",
    instructions="You help users research AI topics.",
    tools=[web_search, code_interpreter],
    guardrails=[input_guardrail, output_guardrail]
)

result = Runner.run_sync(agent, "What's new in AI agents?")
        **Pick OpenAI Agents SDK if:** You're already using GPT-4o/o3, want the fastest path from zero to working agent, and don't need multi-model support. The built-in tools (especially code interpreter) save hours of integration work.



    ## 5. Claude Agent SDK (Anthropic)
    Sandboxed execution MCP-native Security-first

    Anthropic's framework is built around safety and tool-use. The standout feature is deep integration with the Model Context Protocol (MCP) &mdash; which Anthropic designed &mdash; giving agents a standardized way to connect to external tools and data sources. The 1M-token context window means agents can reason over entire codebases.

    ### Why developers pick it

        - **Security-first:** Sandboxed code execution with Constitutional AI guardrails
        - **MCP integration:** Connect to any MCP-compatible tool server
        - **Massive context:** 1M tokens &mdash; feed it an entire repo and let it work
        - **Extended thinking:** Claude can show its reasoning chain transparently


    ### The trade-offs

        - Locked to Claude models
        - Smaller ecosystem than LangGraph or CrewAI
        - Fewer tutorials and community resources (still early)



        **Pick Claude Agent SDK if:** You're building agents that handle sensitive data, need sandboxed execution, or want MCP-native tool integration. Best-in-class for coding agents and security-sensitive automation.



    ## 6. Google Agent Development Kit (ADK)
    Multimodal Agent-to-Agent Google Cloud

    Google's entry focuses on two differentiators: multimodal agents (text + image + video + audio) and the Agent-to-Agent (A2A) protocol for inter-agent communication across organizational boundaries. If you're building agents that need to process images or video, or agents that talk to other companies' agents, ADK is the framework to watch.

    ### Why developers pick it

        - **Multimodal-native:** Agents that see, hear, and read &mdash; not just text
        - **A2A protocol:** Standardized agent-to-agent communication
        - **Google Cloud integration:** Vertex AI, BigQuery, Cloud Functions built in
        - **Model flexibility:** Gemini-first but supports other models


    ### The trade-offs

        - Heavily tied to Google Cloud ecosystem
        - A2A protocol adoption is still early
        - Less battle-tested than LangGraph for production workloads



        **Pick Google ADK if:** You need multimodal agents, agent-to-agent communication, or you're already deep in Google Cloud. The A2A protocol could be huge &mdash; but it's a bet on the future, not a sure thing today.



    ## 7. Smolagents (Hugging Face)
    Lightweight Code-first Open source

    The minimalist option. Smolagents takes a code-first approach: instead of generating JSON tool calls, agents write Python code directly. This makes them faster (fewer LLM calls) and more transparent (you can read exactly what the agent is doing). Perfect for developers who want maximum control with minimum abstraction.

    ### Why developers pick it

        - **Code-based actions:** Agent writes Python, not JSON &mdash; faster and more flexible
        - **Minimal abstraction:** ~1000 lines of core code, easy to understand and modify
        - **Model-agnostic:** Works with any model via Hugging Face's inference API
        - **Open source:** MIT license, full transparency


    ### The trade-offs

        - No built-in state persistence or checkpointing
        - Less suited for complex multi-agent orchestration
        - You'll build more infrastructure yourself



        **Pick Smolagents if:** You want a lightweight, transparent agent framework you can fully understand and customize. Great for single-agent tasks, research prototypes, and developers who prefer code over configuration.



    ## The Decision Matrix

    Here's how to pick your framework based on what you're building:




                If you need...
                Use this



            Production reliability + state management**LangGraph**
            Multi-agent team collaboration**CrewAI**
            Agent-to-agent conversations**AutoGen**
            Fastest path to a working agent (GPT)**OpenAI Agents SDK**
            Security + sandboxed execution**Claude Agent SDK**
            Multimodal or cross-org agents**Google ADK**
            Lightweight, transparent agents**Smolagents**
            You have no idea (start here)**CrewAI** or **OpenAI**




        **Our take:** There's no single "best" framework. LangGraph leads on production maturity. CrewAI leads on accessibility. The vendor SDKs (OpenAI, Claude, Google) are excellent if you're committed to their ecosystem. The real question isn't "which is best?" &mdash; it's "what am I building, and what trade-offs am I willing to make?"



    ## What We Use at Paxrel

    Our newsletter pipeline &mdash; [AI Agents Weekly](/blog-ai-agent-newsletter.html) &mdash; uses a custom Python pipeline with direct API calls to DeepSeek V3 and Claude. No framework. Why? Because our workflow is linear (scrape &rarr; score &rarr; write &rarr; publish), and adding a framework would be over-engineering.

    That said, if we were building something with branching logic, human approvals, or multi-agent collaboration, we'd reach for LangGraph first. It's the framework we'd trust with a production workload today.

    ## Stay in the Loop

    The AI agent ecosystem moves fast. We track every framework update, new tool, and production case study in our free newsletter.


        ### AI Agents Weekly
        The top AI agent news, curated 3x/week. Tools, frameworks, launches, and what actually works in production.

        [Subscribe Free](https://buttondown.com/paxrel)


    You can also grab our free resources:


        - [Top 10 AI Agent Tools 2026](/top-10-ai-agent-tools-2026.pdf) (14-page PDF guide)
        - [AI Agent Stack Cheat Sheet](/ai-agent-stack-cheatsheet-2026.pdf) (2-page reference card)


    ## Related Articles

        - [How to Run Autonomous AI Agents with Claude Code](/blog-claude-code-autonomous-agents.html)
        - [What Is MCP (Model Context Protocol)?](/blog-mcp-model-context-protocol.html)
        - [How to Build an AI Agent in 2026: Step-by-Step Guide](/blog-how-to-build-ai-agent.html)
        - [What Are AI Agents? The Complete Guide](/blog-what-are-ai-agents.html)

Want more AI agent content?

Originally published on paxrel.com

How to Deploy an AI Agent to Production: VPS, Docker & Serverless (2026)

How to Deploy an AI Agent to Production: VPS, Docker & Serverless (2026)

    Your agent works on your laptop. Great. Now how do you make it run 24/7 without you babysitting it? Deployment is where most AI agent projects die &mdash; not because the agent doesn't work, but because nobody figured out how to keep it running reliably.

    This guide covers three deployment approaches (VPS, Docker, serverless), with real configs, cost breakdowns, and the monitoring you need to sleep at night while your agent works.

    ## Choosing Your Deployment Model



            Approach
            Best For
            Monthly Cost
            Complexity
            Always-On?


            **VPS (bare metal)**
            24/7 autonomous agents
            $5-20
            Medium
            Yes


            **Docker + VPS**
            Reproducible, multi-agent
            $10-30
            Medium-High
            Yes


            **Serverless (Lambda/Cloud Run)**
            Event-triggered agents
            $1-50 (pay-per-use)
            Low-Medium
            No (triggered)


            **Managed platforms**
            No-ops teams
            $20-200
            Low
            Varies



    ## Option 1: VPS Deployment (What We Use)

    The simplest path to a 24/7 agent. Rent a virtual server, install your agent, set up a process manager, and let it run.

    ### Step 1: Choose a VPS Provider



            Provider
            Cheapest Plan
            Specs
            Best For


            **Hetzner**
            $4.50/mo
            2 vCPU, 4GB RAM, 40GB SSD
            Best value in EU


            **DigitalOcean**
            $6/mo
            1 vCPU, 1GB RAM, 25GB SSD
            Simple UI, good docs


            **Vultr**
            $6/mo
            1 vCPU, 1GB RAM, 25GB SSD
            Global locations


            **Contabo**
            $6.50/mo
            4 vCPU, 8GB RAM, 50GB SSD
            Most specs per dollar




        **What Paxrel uses:** A Hetzner CX22 ($5.50/mo) with 2 vCPU, 4GB RAM. Runs our full agent stack: newsletter pipeline, social media automation, web scraping, and Reddit karma builder &mdash; all on one server.


    ### Step 2: Initial Server Setup
# SSH into your new server
ssh root@your-server-ip

# Create a non-root user
adduser agent
usermod -aG sudo agent

# Install essentials
apt update && apt install -y python3 python3-pip python3-venv git curl

# Switch to agent user
su - agent

# Clone your agent code
git clone https://github.com/your-org/your-agent.git
cd your-agent

# Set up Python environment
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

# Create environment file for credentials
cat > .env > logs/pipeline.log 2>&1

# Social media posting: Every 6 hours
0 */6 * * * cd /home/agent/your-agent && .venv/bin/python3 post_tweet.py >> logs/twitter.log 2>&1

# Daily monitoring report
30 9 * * * cd /home/agent/your-agent && .venv/bin/python3 monitoring.py >> logs/monitoring.log 2>&1
    ## Option 2: Docker Deployment

    Docker adds reproducibility and isolation. Especially useful when running multiple agents or when your agent has complex dependencies.
# Dockerfile
FROM python:3.12-slim

WORKDIR /app

# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends 
    curl git && rm -rf /var/lib/apt/lists/*

# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy agent code
COPY . .

# Non-root user for security
RUN useradd -m agent
USER agent

CMD ["python3", "agent.py"]
# docker-compose.yml
version: '3.8'

services:
  agent:
    build: .
    restart: always
    env_file: .env
    volumes:
      - ./data:/app/data      # Persist agent memory/state
      - ./logs:/app/logs       # Persist logs
    deploy:
      resources:
        limits:
          memory: 2G
          cpus: '1.0'
    healthcheck:
      test: ["CMD", "python3", "-c", "import requests; requests.get('http://localhost:8080/health')"]
      interval: 60s
      timeout: 10s
      retries: 3

  # Optional: vector database for RAG
  chromadb:
    image: chromadb/chroma:latest
    restart: always
    volumes:
      - chroma_data:/chroma/chroma
    ports:
      - "8000:8000"

volumes:
  chroma_data:
# Deploy
docker compose up -d

# View logs
docker compose logs -f agent

# Update agent
git pull && docker compose build && docker compose up -d
    ## Option 3: Serverless Deployment

    For agents triggered by events (webhook, email, schedule) rather than running continuously. Pay only when the agent runs.

    ### AWS Lambda + EventBridge
# handler.py
import json
import boto3

def lambda_handler(event, context):
    """Triggered by EventBridge cron or API Gateway webhook"""

    # Your agent logic here
    from agent import run_agent
    result = run_agent(event)

    return {
        'statusCode': 200,
        'body': json.dumps(result)
    }
# serverless.yml (Serverless Framework)
service: ai-agent

provider:
  name: aws
  runtime: python3.12
  timeout: 300  # 5 minutes max
  memorySize: 512
  environment:
    OPENAI_API_KEY: ${ssm:/ai-agent/openai-key}

functions:
  newsletter:
    handler: handler.lambda_handler
    events:
      - schedule: cron(0 8 ? * MON,WED,FRI *)  # Mon/Wed/Fri 8am
  webhook:
    handler: handler.lambda_handler
    events:
      - httpApi:
          path: /webhook
          method: post
    ### Google Cloud Run
# For longer-running agents (up to 60 min)
gcloud run deploy ai-agent 
  --source . 
  --region us-central1 
  --memory 1Gi 
  --timeout 3600 
  --set-env-vars "OPENAI_API_KEY=sk-..." 
  --no-allow-unauthenticated
            Platform
            Max Runtime
            Cold Start
            Cost per Run


            AWS Lambda
            15 minutes
            1-5 seconds
            $0.0001-0.01


            Google Cloud Run
            60 minutes
            2-10 seconds
            $0.001-0.05


            Vercel Functions
            5 minutes (pro: 15)

            $0.0001-0.005


            Cloudflare Workers
            30 seconds (free)

            $0.00005



    ## Monitoring Your Deployed Agent

    A deployed agent without monitoring is a liability. Here's the minimum monitoring stack:

    ### Health Check Endpoint
from flask import Flask, jsonify
import psutil

app = Flask(__name__)

@app.route('/health')
def health():
    return jsonify({
        "status": "healthy",
        "uptime_hours": get_uptime(),
        "memory_mb": psutil.Process().memory_info().rss / 1024 / 1024,
        "last_run": get_last_run_timestamp(),
        "errors_24h": get_error_count(hours=24),
        "api_balance": check_api_balance()
    })
    ### Alert System
import requests

def send_alert(message, level="warning"):
    """Send alert via Telegram/Slack/email"""
    if level == "critical":
        # Telegram for immediate attention
        requests.post(
            f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
            data={"chat_id": OWNER_ID, "text": f"🚨 {message}"}
        )
    else:
        # Slack webhook for non-critical
        requests.post(SLACK_WEBHOOK, json={"text": f"⚠️ {message}"})

# Alerts to configure:
# - Agent crash / restart
# - API balance below threshold
# - Error rate spike (3+ errors in 10 min)
# - Agent stuck (no activity for 2+ hours)
# - Cost spike (daily spend > 2x average)
    ### Log Management
import logging
from logging.handlers import RotatingFileHandler

# Structured logging
handler = RotatingFileHandler(
    'logs/agent.log',
    maxBytes=10_000_000,  # 10MB per file
    backupCount=5          # Keep 5 rotated files
)
handler.setFormatter(logging.Formatter(
    '%(asctime)s [%(levelname)s] %(name)s: %(message)s'
))

logger = logging.getLogger('agent')
logger.addHandler(handler)

# Log every significant action
logger.info("Scraping 12 RSS feeds")
logger.info("Scored 97 articles, top score: 28")
logger.warning("API rate limited, retrying in 30s")
logger.error("Beehiiv publish failed: 401 Unauthorized")
    ## Production Hardening Checklist

    ### Security

        - API keys in environment variables or secrets manager, never in code
        - Non-root user for the agent process
        - Firewall: only allow SSH (22) and necessary ports
        - SSH key auth only, disable password login
        - Auto-update OS security patches (`unattended-upgrades`)


    ### Reliability

        - Process manager with auto-restart (systemd, Docker restart policy)
        - Graceful shutdown handling (catch SIGTERM, finish current task)
        - Exponential backoff on API errors (not infinite retry loops)
        - Circuit breaker for external services (stop calling after N failures)
        - Daily backup of agent state/memory to external storage


    ### Cost Control

        - Daily API spend limit with hard cutoff
        - Max steps per agent run (prevent infinite loops)
        - Token counting before API calls (reject oversized prompts)
        - Alert when daily spend exceeds 2x average
        - Weekly cost report to the team


    ## Deployment Patterns by Use Case



            Agent Type
            Best Deployment
            Why


            24/7 autonomous agent
            VPS + systemd
            Always-on, persistent state


            Scheduled pipeline
            VPS + cron or serverless
            Runs on schedule, sleeps between


            Webhook-triggered
            Serverless (Lambda/Cloud Run)
            Pay-per-use, auto-scales


            Multi-agent system
            Docker Compose on VPS
            Isolated containers, shared network


            Customer-facing chatbot
            Cloud Run or managed platform
            Auto-scale with traffic


            Development/testing
            Local Docker
            Reproducible environment



    ## Key Takeaways


        - **VPS + systemd is the simplest path** for always-on agents. $5-15/month, full control, works for 90% of use cases.
        - **Docker adds value** when you have complex dependencies, multiple agents, or need reproducibility across environments.
        - **Serverless is cheaper for sporadic workloads** but has runtime limits (15 min for Lambda) that don't suit long-running agents.
        - **Monitoring is not optional.** Health checks, alerts, and log rotation are the minimum. An unmonitored agent will fail silently.
        - **Security basics matter.** Non-root user, env vars for secrets, firewall, SSH keys. Takes 30 minutes, prevents disasters.
        - **Start simple, scale later.** A $5 VPS with cron jobs is a perfectly valid production deployment. Don't over-engineer until you need to.



        ### Deploy With Confidence
        Our AI Agent Playbook includes Dockerfiles, systemd configs, monitoring templates, and deployment checklists for production agents.

        [Get the Playbook &mdash; $29](https://paxrel.gumroad.com/l/ai-agent-playbook)



        ### Stay Updated on AI Agents
        Deployment patterns, infrastructure tips, and production war stories. 3x/week, no spam.

        [Subscribe to AI Agents Weekly](/newsletter.html)

Want more AI agent content?

Originally published on paxrel.com

Interview Follow-Up: Emails That Get Responses

Interview Follow-Up: Emails That Get Responses

Picture this: You just walked out of what felt like the perfect technical interview. The system design discussion was engaging, you solved the coding problem elegantly, and you had great chemistry with the team. But here’s the thing, 70% of candidates never send a follow-up email, and most of those who do send generic, forgettable messages that end up buried in hiring managers’ overflowing inboxes.

As a senior engineer who’s been on both sides of the interview table, I’ve seen how a well-crafted follow-up email can be the deciding factor between two equally qualified candidates. Think of your interview follow-up strategy as a system architecture problem: you need the right components, proper timing, and seamless integration to achieve your desired outcome.

Core Concepts: The Follow-Up Email System

The Essential Components

Your interview follow-up system consists of four critical components, each serving a specific purpose in your communication pipeline:

Gratitude Module: This handles the basic courtesy of thanking your interviewers for their time. While it seems obvious, this component sets the professional tone for all subsequent interactions.

Value-Add Processor: This component transforms your follow-up from a simple thank you note into something memorable. It processes additional insights, clarifications to your interview responses, or relevant resources that demonstrate continued engagement.

Timing Controller: This manages when and how often you reach out. Like a well-designed rate limiter, it ensures your communication frequency is optimal without overwhelming the recipients.

Response Handler: This component manages what happens when you receive responses, partial responses, or complete silence from your contacts.

Data Types in Your Follow-Up System

Your follow-up system processes several types of data inputs:

  • Interview Context: Who you met with, what you discussed, specific technical challenges covered
  • Personal Connection Points: Shared interests, mutual connections, or interesting topics that came up
  • Technical Insights: Additional thoughts on problems discussed, or solutions to questions you couldn’t fully answer
  • Company Research: New information about their tech stack, recent company news, or industry developments

How It Works: The Follow-Up Email Flow

Initial Processing (24-Hour Window)

Your follow-up system should trigger within 24 hours of your interview. This timing ensures you’re fresh in the interviewer’s memory while demonstrating promptness and professionalism.

The initial email flows through your gratitude module first, acknowledging each person you met. Then it routes through the value-add processor to include something specific from your conversation. This might be a clarification of a technical point, a link to an article you mentioned, or additional context for a question you answered.

Content Processing Pipeline

Your email content moves through several processing stages:

Subject Line Generator: Creates clear, specific subject lines like “Thank you for yesterday’s senior engineer interview” rather than generic “Thank you” messages.

Opening Processor: Crafts personalized openings that reference specific moments from your interview, not templated greetings that could apply to any company.

Body Content Engine: This is where the magic happens. Instead of just saying “I’m very interested in the role,” this component processes specific technical discussions, your thoughts on their architecture challenges, or insights about their product direction.

Closing Handler: Manages your call-to-action and next steps, whether that’s expressing continued interest, offering to provide additional information, or simply stating you look forward to hearing about next steps.

Multi-Channel Distribution

Your follow-up system should handle multiple communication channels:

  • Primary Channel: Email to your main interview contact or recruiter
  • Secondary Channels: LinkedIn connections to team members you met (when appropriate)
  • Backup Channels: Alternative contact methods if you don’t receive responses through primary channels

You can visualize this multi-channel follow-up architecture using InfraSketch to see how different communication paths connect and where potential bottlenecks might occur.

Design Considerations: Building an Effective Follow-Up Strategy

Timing Trade-offs

Immediate vs. Delayed: Sending your follow-up too quickly might seem automated or desperate, while waiting too long reduces your memorability. The 24-hour window strikes the optimal balance between thoughtfulness and timeliness.

Single vs. Multiple Touchpoints: Your system needs to handle the decision between one comprehensive follow-up versus multiple, smaller interactions. Generally, one substantive initial follow-up followed by strategic additional touchpoints works best.

Content Scaling Strategies

One-to-One vs. One-to-Many: Each interviewer should receive a personalized message, even if you’re sending to multiple people. Your system should maintain individual context for each recipient while reusing core content efficiently.

Technical Depth Adjustment: Your content processor should adjust technical depth based on the recipient. A follow-up to an engineering manager might focus on team dynamics and technical leadership, while one to a staff engineer might dive deeper into architectural decisions.

Handling System Failures (The Silent Response)

Your follow-up system must gracefully handle the most common scenario: complete silence. Design your expectations accordingly:

Week 1: Initial follow-up sent, no response expected yet
Week 2: Reasonable to send a brief, friendly check-in
Week 3+: If you haven’t heard anything, your system should implement an exponential backoff strategy, similar to how distributed systems handle failed requests

Persistence vs. Respect Boundaries

Like any well-designed system, your follow-up architecture needs proper rate limiting. Too many messages become spam, too few suggest disinterest. Design your system to be persistent but respectful:

  • Maximum of 2-3 follow-up touchpoints
  • Increasing intervals between messages (1 week, then 2 weeks, then monthly)
  • Clear value-add in each subsequent message

When to Use Different Follow-Up Architectures

Startup Interviews: Often require more informal, direct follow-ups that demonstrate cultural fit alongside technical competence.

Large Company Interviews: May need more structured follow-ups that work within formal hiring processes and multiple stakeholders.

Technical Deep-Dives: When your interview focused heavily on system design or architecture problems, your follow-up should continue the technical conversation with additional insights or resources.

Tools like InfraSketch can help you think through these different scenarios and plan your follow-up strategy systematically.

Key Takeaways: Architecting Your Follow-Up Success

Your interview follow-up system is more than just good manners, it’s a strategic communication architecture that can differentiate you from other candidates. Here are the most critical design principles:

Timing is Architecture: Like system latency, timing in follow-ups can make or break the user experience. Optimize for the 24-hour window while planning for longer-term touchpoints.

Personalization Scales: Build reusable components for gratitude and structure, but always customize the core content for each recipient and company.

Value-Add Processing: Every follow-up should include something beyond “thank you.” Additional insights, clarifications, or resources demonstrate continued engagement and technical thinking.

Graceful Degradation: Design your system to handle silence professionally. No response is still data, it tells you about company culture, process efficiency, and communication styles.

Failure Recovery: Don’t let one awkward follow-up crash your entire system. Learn from responses (or lack thereof) and adjust your architecture for future interviews.

Remember, your follow-up email is often your last direct communication before the hiring decision. Make it count by treating it as seriously as you would any other technical system you design.

Try It Yourself

Now that you understand the architecture of effective interview follow-ups, try designing your own follow-up system. Consider the specific companies you’re interviewing with, the types of roles you’re pursuing, and the communication styles that align with your personality.

Think through the components, data flows, and timing mechanisms that would work best for your situation. Map out how different scenarios (startup vs. enterprise, technical vs. behavioral interviews, positive vs. challenging interview experiences) would route through your system differently.

Head over to InfraSketch and describe your follow-up communication system in plain English. In seconds, you’ll have a professional architecture diagram that shows how all your follow-up components connect, complete with a design document. No drawing skills required, just clear thinking about how to systematically approach one of the most overlooked aspects of the interview process.

Learn Synchronous and Asynchronous JavaScript

Synchronous

In synchronous programming, tasks are done one after another in order. Each line of code waits for the previous one to finish before moving to the next. This means the program runs step by step in a clear and predictable way.

console.log("Start");
console.log("Task 1");
console.log("Task 2");
console.log("End");

In this example, “Start” is printed first, followed by “Task 1”, then “Task 2”, and finally “End”. Each line runs only after the previous one has finished. This shows that the code executes in a fixed order, step by step, without skipping any task.

Asynchronous

In asynchronous programming, tasks can run independently without waiting for previous tasks to complete. While one task is in progress, other tasks can continue running. This improves performance and makes applications faster and smoother.

console.log("Start");

setTimeout(function() {
  console.log("Task 1");
}, 2000);

console.log("End");

Output:
Start
End
Task 1

In this example, “Start” is printed first, followed by “End”. The “Task 1” message is printed after a delay. This happens because the setTimeout function runs asynchronously, so the code does not wait and continues executing the next line.

How Asynchronous JavaScript Works

Call Stack:

The call stack is where JavaScript runs functions.
It follows a simple rule: one function runs at a time, in order.
Each function is added to the stack, executed, and then removed before the next one runs.

Web APIs (Browser)

Some tasks like setTimeout, API calls, and event listeners are handled by the browser using Web APIs.

When you use something like setTimeout, JavaScript sends it to the Web API.
The Web API handles the waiting time, so the main code does not stop.

Callback Queue

After the Web API finishes its task (like waiting for time), it sends the function (callback) to the callback queue.

Event Loop

The event loop keeps checking the call stack.

  • If the call stack is empty
  • It takes functions from the callback queue
  • And moves them to the call stack for execution

That’s why delayed tasks run after the main code finishes.

Treating blog posts like production code

When I start a new project, I immediately set up a repository, linters, tests, git hooks, and CI. Not because I’m forced to, but because fast feedback loops make me a better developer.

When I decided to start this blog, I asked myself: why should writing be any different?

Repository setup

Go to your GitHub account (https://github.com/<YOUR_ACCOUNT>?tab=repositories) and create a new repository named blog (for example).

Then, set it up with the following commands:

mkdir blog

cd blog

echo "# blog" >> README.md

git init
git branch -M main
git add README.md
git commit -m "Initial commit"
git remote add origin https://github.com/davidjulien/blog.git
git push -u origin main

Linters

Linters ensure that articles are well-formatted and consistent.

Here are the linters I set up:

  • markdownlint to check Markdown files and flag style issues.
  • vale for writing style.
  • ltex-ls-plus to check for grammar and spelling mistakes.
  • lychee to check that the links in my articles are valid.

Checking Markdown files with markdownlint-cli2

Since I use Markdown for my articles, I want to ensure that they’re well-formatted and consistent. For that, I use markdownlint-cli2, which is a command-line interface for markdownlint.

You can also try markdownlint-cli.

I chose markdownlint-cli2 for the following reasons:

  • jsonc for configuration, which allows me to add comments and explanations in the configuration file.
  • Written by the markdownlint author himself
  • Has an official GitHub Action

Setting up markdownlint-cli2

Install the tool as a development dependency:

npm install markdownlint-cli2 --save-dev

And write package.json with the following content:

{
  "scripts": {
    "lint:md": "markdownlint-cli2",
    "lint:md:fix": "markdownlint-cli2 --fix"
  },
  "devDependencies": {
    "markdownlint-cli2": "^0.21.0"
  }
}

Then, add a basic configuration file .markdownlint-cli2.jsonc at the root of your repository.

{
  "$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/refs/heads/main/schema/markdownlint-cli2-config-schema.json",
  // Only lint articles
  "globs": [
    "articles/**/*.md"
  ],
  "config": {
  }
}

I prefer starting with everything enabled, then turn off with intention. Every rule you turn off should be a conscious decision, not a default.
That applies beyond linting. Code reviews, architecture choices, dependency upgrades: start strict, relax where it makes sense, and know why.
Don’t forget to add comments in the configuration file to explain why you choose to enable or turn off certain rules.

markdownlint documentation lists available rules and their default configuration.

In my case, I started with this configuration:

{
  "$schema": "https://raw.githubusercontent.com/DavidAnson/markdownlint-cli2/refs/heads/main/schema/markdownlint-cli2-config-schema.json",
  // Only lint articles
  "globs": [
    "articles/**/*.md"
  ],
  "config": {
    // MD003: Enforce ATX-style headings (e.g. "# Heading") for consistency
    "heading-style": {
      "style": "atx"
    },
    // MD004: Use dashes for unordered lists to keep a uniform style
    "ul-style": {
      "style": "dash"
    },
    // MD025: Tells markdownlint not to treat the frontmatter title as an h1
    "single-h1": {
      "front_matter_title": ""
    },
    // MD026: Allow trailing punctuation in headings (e.g. "What happened?")
    "no-trailing-punctuation": {
      "punctuation": ".,;:!"
    },
    // MD029: Ordered list items must use incrementing numbers (1, 2, 3) for clarity
    "ol-prefix": {
      "style": "ordered"
    },
    // MD033: Disallow inline HTML (authorized elements can be added later)
    "no-inline-html": {
      "allowed_elements": []
    },
    // MD035: Use "---" for horizontal rules for consistency
    "hr-style": {
      "style": "---"
    },
    // MD040: Fenced code blocks must specify a language for syntax highlighting
    "fenced-code-language": {
      "language_only": true
    },
    // MD046: Always use fenced style for code blocks (not indented)
    "code-block-style": {
      "style": "fenced"
    },
    // MD048: Use backticks (not tildes) for fenced code blocks
    "code-fence-style": {
      "style": "backtick"
    },
    // MD049: Use asterisks for emphasis (*italic*) for consistency
    "emphasis-style": {
      "style": "asterisk"
    },
    // MD050: Use asterisks for strong emphasis (**bold**) for consistency
    "strong-style": {
      "style": "asterisk"
    },
    // MD055: Use pipes on both sides of table rows for readability
    "table-pipe-style": {
      "style": "leading_and_trailing"
    }
  }
}

Running the linter

First run to test:

$ npm run lint:md


> lint:md
> markdownlint-cli2

markdownlint-cli2 v0.21.0 (markdownlint v0.40.0)
Finding: articles/**/*.md
Linting: 1 file(s)
Summary: 7 error(s)
articles/20260313-setup-a-repository-to-store-blog-articles.md:6 error MD025/single-title/single-h1 Multiple top-level headings in the same document [Context: "Treating blog posts like produ..."]
articles/20260313-setup-a-repository-to-store-blog-articles.md:8:81 error MD013/line-length Line length [Expected: 80; Actual: 95]
articles/20260313-setup-a-repository-to-store-blog-articles.md:26:4 error MD009/no-trailing-spaces Trailing spaces [Expected: 0 or 2; Actual: 1]
articles/20260313-setup-a-repository-to-store-blog-articles.md:33 error MD032/blanks-around-lists Lists should be surrounded by blank lines [Context: "- [markdownlint](https://githu..."]
...

It works!

Now I can analyze the errors:

  • The main error is MD013/line-length.
    This rule checks that lines don’t exceed a certain length (80 characters by default).
    I can either break the long line or increase the line length limit in the configuration file or disable the rule if I don’t care about it.
    I decided to disable this rule.
    Even if I prefer shorter lines and try to write one phrase per line, this rule is annoying when you have a long URL or you are just over the limit.
    I prefer to set a visual clue in my editor when I reach 120 characters.
  {
    "config": {
      // MD013: No limit (useful when you have urls in your articles)
      "line-length": false,
    }
  }
  • Another error is MD024/no-duplicate-heading.
    This rule checks that there are no duplicate headings in the same document.
    In my case, I had two ## Running the linter headings after I wrote the two first linter sections.
    I thought to disable this rule, but the documentation explained that “Some Markdown parsers generate anchors for headings based on the heading name; headings with the same content can cause problems with that.”
    I decided to keep this rule and change my headings to be more specific.

  • Another cryptic error I hit is MD025/single-title/single-h1.
    This rule checks that there is only one top-level heading in the document.
    In my case, I have an explicit one (a line starting with #) and a hidden one (the frontmatter title):

  ---
  title: "Treating blog posts like production code"
  tags: tooling, writing, vale, markdownlint
  ---

  # Treating blog posts like production code

I want to keep both:

  {
    "config": {
      // MD025: Tells markdownlint not to treat the frontmatter title as an h1 to keep the explicit one
      "single-h1": {
        "front_matter_title": ""
      }
    }
  }

After fixing errors and updating configuration, you can run the command again to check that everything is good:

$ npm run lint:md


> lint:md
> markdownlint-cli2

markdownlint-cli2 v0.21.0 (markdownlint v0.40.0)
Finding: articles/**/*.md
Linting: 1 file(s)
Summary: 0 error(s)

Vale for writing style

Now that my article is well-formatted, I want to check that it’s well-written.

Setting up vale

Install the tool with your package manager:

brew install vale

Then, create a configuration file .vale.ini at the root of your repository:

StylesPath = .vale/styles
Vocab = blog
MinAlertLevel = suggestion
Packages = Google, proselint, write-good, Readability, alex

[*.md]
BasedOnStyles = Vale, Google, proselint, write-good, Readability, alex

I chose these packages for the following reasons:

Package What it catches
Google Word choice, headings, contractions. Based on Google’s Developer Style Guide
proselint Clichés, redundancy, jargon. Advice aggregated from great editors
write-good Passive voice, weasel words, wordy phrases
Readability Flesch-Kincaid, SMOG. Are your sentences too dense?
alex Flags insensitive or inconsiderate language: gendered terms, ableist language, racial bias. alex catches things you might not think about as a non-native English speaker

Run this command to download the packages and to create the styles directory:

vale sync

Then, create the vocabularies for your blog and add custom words to allow or flag in your writing style.

mkdir -p .vale/styles/config/vocabularies/blog
touch .vale/styles/config/vocabularies/blog/accept.txt
touch .vale/styles/config/vocabularies/blog/reject.txt

Running vale

$ vale articles/20260213-setup-a-repository-to-store-blog-articles.md

 articles/20260213-setup-a-repository-to-store-blog-articles.md
[...]
✖ 13 errors, 29 warnings and 17 suggestions in 1 file.

Well… I knew that my writing style wasn’t perfect, but I didn’t expect that many warnings and suggestions!

Now I need to review each error and decide: fix or ignore?

I check the most common errors.
vale doesn’t have a built-in way to do that.
I use jq to parse the JSON output and count the occurrences of each rule:

Install jq with your package manager:

brew install jq
$ vale --output=JSON articles/ | jq -r '.[][].Check' | sort | uniq -c | sort -rn
  23 Google.FirstPerson
  13 write-good.E-Prime
   5 Google.EmDash
   4 Google.Parens
   3 alex.ProfanityUnlikely
   3 Vale.Spelling
   2 write-good.Weasel
   2 Google.WordList
   2 Google.Exclamation
   1 write-good.TooWordy
   1 Readability.SMOG
   1 Readability.FleschReadingEase
   1 Readability.ColemanLiau
   1 proselint.Very
   1 proselint.Typography
   1 proselint.But
   1 Google.Will
   1 Google.We
   1 Google.Spacing
   1 Google.Ellipses
   1 Google.Acronyms

Output analysis:

  • Google.FirstPerson I use a lot of first-person pronouns in my writing.
    Since I write a personal blog, I don’t care about this rule.
    I can disable it in the configuration file.
    I also disable Google.We for the same reason.
  # I'm writing a personal blog, not corporate docs
  Google.FirstPerson = NO
  Google.We = NO
  • write-good.E-Prime: this one is less clear to me. Fortunately, each rule has documentation:
  $ cat .vale/styles/write-good/E-Prime.yml
  extends: existence
  message: "Try to avoid using '%s'."
  ignorecase: true
  level: suggestion
  tokens:
    - am
    - are
    - aren't
    - be
    - been
    - being
    - he's
    - here's
    - here's
    - how's
    - i'm
    - is
    - isn't
    - it's
    - she's
    - that's
    - there's
    - they're
    - was
    - wasn't
    - we're
    - were
    - weren't
    - what's
    - where's
    - who's
    - you're

This rule flags all forms of “to be”.
I don’t find it useful for blog writing. I set it to NO.

  write-good.E-Prime = NO
  • Google.EmDash: The preceding cat command displays the rule content.
    I also want to know where the errors are in my article.
    For that, I can use the --output=JSON option to get the details of each error and use jq to filter the errors:
  $ vale --output=JSON articles/ | jq -r '.[][] | select(.Check == "Google.EmDash") | "(.Line):(.Span[0]) (.Message)"'
  176:47 Don't put a space before or after a dash.
  177:42 Don't put a space before or after a dash.
  179:37 Don't put a space before or after a dash.
  180:53 Don't put a space before or after a dash.
  183:87 Don't put a space before or after a dash.

To check my articles, I need these commands often. Instead of copy/pasting it, I create three scripts:

mkdir bin

cat > bin/vstats << 'EOF'
#!/bin/bash
# Show Vale warnings count per rule
# Usage: bin/vstats

vale --output=JSON articles/ | jq -r '.[][].Check' | sort | uniq -c | sort -rn
EOF

chmod +x bin/vstats
cat > bin/vdef << 'EOF'
#!/bin/bash
# Show a Vale rule definition
# Usage: bin/vdef write-good.E-Prime

pkg="${1%%.*}"
rule="${1##*.}"
cat ".vale/styles/${pkg}/${rule}.yml"
EOF

cat > bin/vgrep << 'EOF'
#!/bin/bash
# Show occurrences of a Vale rule
# Usage: bin/vgrep Google.EmDash

vale --output=JSON articles/ | jq -r ".[][] | select(.Check == "$1") | "(.Line):(.Span[0]) (.Message)""
EOF

chmod +x bin/vdef bin/vgrep

Examples:

$ bin/vdef Google.EmDash
extends: existence
message: "Don't put a space before or after a dash."
link: "https://developers.google.com/style/dashes"
nonword: true
level: error
action:
  name: edit
  params:
    - trim
    - " "
tokens:
  - 's[—–]s'

$ bin/vgrep Google.EmDash
183:87 Don't put a space before or after a dash.

Now it’s easier.
I continue to check my article and fix the errors one by one.
I only comment those where I can suggest a new kind of fix.

  • write-good.Weasel: Weasel words are vague qualifiers that sound meaningful but say nothing precise.
    For an engineering blog, I want to be precise and avoid vague language.
    I’ll keep this rule and fix these errors.
    After fixing, I agree that these sentences are better without these words.
  • Google.Parens: False positive — this triggers on Markdown link syntax. Not useful for articles. I set it to NO.
  • Google.WordList: This rule identified words that aren’t recommended by Google’s style guide.
    In my case, I used the word “disable” three times. It suggests replacing it with “turn off”.
    I’m not fully sure if it’s better since I target technical readers and “disable” feels right in this context.
    Using “turn off” is slightly awkward, it sounds like a light switch.
    Since I can’t define an exception for the word “disable”, it’s the occasion to learn how to write custom rules.

Start by checking the rule definition:

  cat .vale/styles/Google/WordList.yml

Then, create a custom rule by copying the existing one and modifying it:

  mkdir .vale/styles/MyBlog
  cp .vale/styles/Google/WordList.yml .vale/styles/MyBlog/WordList.yml

Now I can remove the word “disable” from my custom rule and keep the other words.

  • alex.ProfanityUnlikely: This rule flags words that readers might misinterpret as profane.
    In my case, it flagged the example I used to describe alex role. I don’t want to change this example, so I’m ignoring this rule
    for this specific case by adding an inline comment in the article:
  <!-- vale alex.ProfanityUnlikely = NO -->
  my text
  <!-- vale alex.ProfanityUnlikely = YES -->
  • Readability.FleschReadingEase: This rule calculates the Flesch Reading Ease score of the whole text.
    The value is for the whole text, not only the first line.
    A higher score indicates more readable text.
    My article was just below the expected score.
    I improved this score by breaking long sentences into shorter ones, using simpler words, and avoiding passive voice.

After discarding rules and fixing errors, I have the following stats:

✔ 0 errors, 0 warnings and 0 suggestions in 1 file.

I aim for zero warnings.
If you start with 10 errors, you might miss a new error lost between two existing ones.
If you start with 0 errors, any new error is immediately visible, and you can fix it right away.

Grammar and spelling mistakes

When you aren’t a native English speaker, it’s common to make grammar and spelling mistakes without noticing them.

I tried two different tools to catch these mistakes: ltex-ls-plus and harper. Even if harper is faster, I find
the result of ltex-ls-plus more accurate, so I decided to keep it.

Setting up ltex-ls-plus

brew install ltex-ls-plus

Running ltex-ls-plus

ltex-cli-plus articles/20260213-setup-a-repository-to-store-blog-articles.md

Checking links

To check that the links in my articles are valid, I use lychee, which is a fast and modern link checker.

Setting up lychee

brew install lychee

Running lychee

$ lychee articles/20260213-setup-a-repository-to-store
5/5 ━━━━━━━━━━━━━━━━━━━━ Finished extracting links                                                                                                                                Issues found in 1 input. Find details below.

[articles/20260213-setup-a-repository-to-store-blog-articles.md]:
   [ERROR] file:///Users/david/dev/davidjulien/blog/articles/markdownlint-cli | Cannot find file: File not found. Check if file exists and path is correct

🔍 5 Total (in 1s) ✅ 4 OK 🚫 1 Error

One link isn’t working. Without a line number, using grep is necessary to find the error location.
In my case, I permuted the link with the text:

$ grep -n "](markdownlint"
34:[https://github.com/markdownlint/markdownlint](markdownlint)

Automating local checks

I set up four tools locally to check my articles: markdownlint, vale, ltex-ls-plus, and lychee.
I want to run these tools before each commit to ensure that I don’t commit any errors.

Git hooks

To automate these checks, I use git hooks, which are scripts that run at specific points in the Git workflow.
The main problem is that the .git directory isn’t versioned, so I can’t track changes in the hooks and share them with my collaborators.
The solution is to store the hooks in a versioned directory (like .githooks) and tell git to use this directory.

$ mkdir .githooks
$ cat > .githooks/pre-commit << 'EOF'
#!/bin/bash
files=$(git diff --cached --name-only --diff-filter=ACM | grep 'articles/.*.md$')

if [ -z "$files" ]; then
  exit 0
fi

echo "--- Markdown syntax ---"
markdownlint-cli2 $files || exit 1

echo "--- Check links ---"
lychee $files || exit 1

echo "--- Style ---"
vale $files || exit 1

echo "--- Grammar ---"
JAVA_OPTS="--enable-native-access=ALL-UNNAMED" ltex-cli-plus $files || exit 1

echo "Check terminated with success."
EOF

$ chmod +x .githooks/pre-commit

$ git config core.hooksPath .githooks

Test it:

$ .githooks/pre-commit
--- Markdown syntax ---
markdownlint-cli2 v0.21.0 (markdownlint v0.40.0)
Finding: articles/20260213-setup-a-repository-to-store-blog-articles.md articles/**/*.md
Linting: 1 file(s)
Summary: 0 error(s)
--- Check links ---
6/6 ━━━━━━━━━━━━━━━━━━━━ Finished extracting links                                                                                                                                🔍 6 Total (in 1s) ✅ 6 OK 🚫 0 Errors

--- Style ---
✔ 0 errors, 0 warnings and 0 suggestions in 1 file.
--- Grammar ---
Check terminated with success.

An alternative is lefthook.
It has more features (run checks in parallel, better output formatting, etc.) but it requires a dependency.

Neovim integration

Linters output in the editor

Running all checks thanks to a script is good, but it’s even better to have these checks running in real-time while writing the article.
It allows you to fix errors right away and avoid having a long list of errors at the end.

This part depends on your editor.
Personally, I’m using Neovim with the following configuration:

In my lsp configuration:

vim.lsp.config("ltex_plus", {
  filetypes = { "markdown" },
  settings = {
    ltex = {
      language = "en-US",
    }
  }
})

vim.lsp.enable("ltex_plus")

In my linter configuration:

return {
  "mfussenegger/nvim-lint",
  config = function()
    local lint = require("lint")

    lint.linters_by_ft = {
      markdown = { "vale", "markdownlint-cli2" },
    }

    local lint_augroup = vim.api.nvim_create_augroup("lint", { clear = true })

    vim.api.nvim_create_autocmd({ "BufEnter", "BufWritePost", "InsertLeave" }, {
      group = lint_augroup,
      callback = function()
        lint.try_lint()
      end,
    })
  end,
}

Now I can see directly all errors in my editor while writing the article.
I can also navigate between errors with the following shortcuts:

vim.keymap.set('n', '<leader>q', vim.diagnostic.setloclist, { desc = 'Open diagnostics list' })
map("<leader>ca", vim.lsp.buf.code_action, "[C]ode [A]ction", { "n", "x" })

Preview in the browser

Writing Markdown is cool, but sometimes you need to check the rendering. For that, I install the markdown-preview.nvim plugin, which allows seeing a live preview of my article in the browser while writing it.

{
  "iamcco/markdown-preview.nvim",
  cmd = { "MarkdownPreviewToggle", "MarkdownPreview", "MarkdownPreviewStop" },
  ft = { "markdown" },
  build = "cd app && npm install",
}

I just need to run :MarkdownPreview to see the preview in the browser.

Continuous integration

Since I’m the only one writing articles in my repository, I don’t need to set up CI to enforce the checks. Git hooks ensure that I don’t commit any changes that don’t respect my quality standards. If you aren’t alone, it’s a good idea to set up CI to ensure that all articles go through the same checks before merging.

Conclusion

Writing code and writing prose aren’t that different. Both need fast feedback loops, consistent standards, and automation that catches what you miss. With this setup, every commit to my blog repository goes through the same rigor I’d apply to commit a change in my code.

The tools here — markdownlint, vale, ltex-ls-plus, lychee — took an afternoon to set up. Writing this article took more time!
The feedback they give me every time I write is worth that investment many times over.

My WhatsApp Bot Was Restarting Itself 7 Times a Day. Here’s What Stopped It.

My AI agent has a WhatsApp connection. For three days, it fell into a restart loop — up to 7 times in a single day, intervals shrinking as the day went on. Then on day four: nothing. Overnight stable. Health-monitor doing clean self-heals. The 499 loop gone.

I didn’t explicitly fix it. The health-monitor evolved to catch it first. Here’s the full story — failure modes, debugging methodology, and what actually stopped it.

The Symptom

Every few hours, I see this in the logs:

[whatsapp] status 499 — disconnected
[whatsapp] reconnecting...
[whatsapp] status 499 — disconnected
[whatsapp] reconnecting...
(repeat ~10 times over 60 seconds)

Status 499 in this context means: “No messages received in N minutes — restarting connection.” The WhatsApp library sees a prolonged silence on the socket and interprets it as a dead connection. It kicks off a reconnect. The reconnect succeeds briefly, then immediately gets flagged as silent again, triggering another restart. Loop.

The fix has been reliable: restart the gateway process. WhatsApp reconnects cleanly, and the loop stops. For 2–4 hours.

Four Days of Data

I started logging these episodes properly on day one:

Day 1 (Tuesday): First noticed flapping ~09:10. Multiple bouts throughout the morning and afternoon — roughly 5–6 episodes, each 10–15 minutes of disconnect/reconnect cycling. All auto-recovered without manual intervention. No pattern to timing.

Day 2 (Wednesday): Graduated from “interesting anomaly” to “recurring problem.” Four full flap episodes:

  • ~14:27 — lasted 70 minutes before I manually restarted the gateway
  • ~18:27 — caught earlier, fixed in 10 minutes
  • ~20:58 — third episode
  • ~21:48 — fourth episode, after which the failure mode changed to status 503 (server-side disconnects, shorter duration, auto-recovering cleanly)

Day 3 (Thursday): Seven episodes — the worst day. But something shifted: the health-monitor started catching some episodes earlier (as “stale socket” before they became full 499 loops), and gateway restarts held for ~4 hours each time — suggesting the loop stabilizes after a clean restart. Episodes: 08:04, 12:39, 17:06, 18:36, 21:01, 22:07, and a late-night one. Intervals shrinking through the day (4h → 2h → 1.5h).

Day 4 (Friday): A completely different picture. Overnight: only a single 428 disconnect at 00:29 (self-recovered in seconds, normal behavior) and one clean health-monitor stale-socket restart at 02:32. No 499 loops at all. Morning check confirmed WhatsApp healthy — only webchat disconnects (expected, not WhatsApp). The health-monitor appears to now be reliably intercepting the stale socket condition before it becomes a 499 loop. Day 4 looking significantly better so far.

Two Different Failure Modes

I’ve been careful to distinguish two patterns that look similar in the logs:

Mode 1 — Status 499 (the bad one):
“No messages received in Nm — restarting connection.” This is the idle-timeout trigger. Once it fires, it creates a loop: the connection resets so fast it never gets time to receive a message, so the timer fires again immediately. Manual gateway restart breaks the loop.

Mode 2 — Status 503 (the recoverable one):
Server-side disconnects from WhatsApp’s infrastructure. These happen in shorter bursts, with variable timing (15 minutes, 45 seconds, 5 minutes). They auto-recover cleanly. The agent noticed these started appearing after the 4th restart on day 2 — possibly WhatsApp’s servers briefly deprioritizing a connection that had been restarting frequently.

What I’ve Ruled Out

  • Not a version regression. The version hasn’t changed over these three days.
  • Not time-of-day-specific. Episodes happen at 09:10, 14:27, 18:27, 20:58, 08:04, 12:39, 17:06 — no obvious pattern.
  • Not correlated with load. Episodes happen during quiet periods (overnight, midday) as much as busy ones.
  • Not the hardware. The agent is running on a Linux box with stable uptime and no network issues affecting other services.
  • Not a WhatsApp ban or rate-limit. The connection re-establishes successfully every time.

The Health-Monitor Evolution

Here’s the interesting part. My agent has a health-monitor that checks WhatsApp connectivity on a schedule. On day 3, it started catching “stale socket” states before they turned into full 499 loops:

[health-monitor] WhatsApp: stale socket detected — restarting
[whatsapp] reconnected OK

That’s different from the loop. A stale socket restart is clean — one disconnect, one reconnect, done. The 499 loop is the problem; the health-monitor catching it early apparently prevents the loop from starting.

This suggests the root cause might be: the socket goes genuinely idle (no message traffic for N minutes), the library triggers a “no messages received” restart, but something about the restart itself puts the connection in a bad state where it immediately re-triggers the timeout.

Current Hypothesis

The idle-timeout threshold is probably too aggressive for a setup where the WhatsApp account isn’t messaging constantly. When the socket goes quiet for the threshold window, the library restarts — but the restart is fast enough that the new connection is immediately considered “silent” too, since it hasn’t had time to receive anything. Loop.

The fix might be: increase the no-messages-received timeout threshold, or disable it entirely and let the health-monitor handle stale socket detection instead.

I haven’t confirmed this yet. The library configuration for this timeout isn’t well-documented, and I haven’t wanted to make config changes mid-observation (changes the variables).

What Actually Stopped It

Day 4: no configuration changes, no library updates, no code changes. The difference was the health-monitor.

On days 1–3, the health-monitor was catching some stale sockets, but the 499 loop was faster — it would spin up before the monitor could intercept it. By day 3 evening, the health-monitor’s detection timing had effectively improved (or the loop’s trigger timing shifted slightly). By day 4 overnight, the monitor was consistently catching stale sockets with clean single restarts before they cascaded into the full 499 loop.

This isn’t a permanent fix — the root cause (idle timeout threshold too aggressive for a low-traffic account) is still there. But the health-monitor is now acting as a reliable mitigation layer.

Current state: Stable. Single 428 disconnects (expected, normal) auto-recovering immediately. Health-monitor catching stale sockets with clean restarts. No 499 loops.

The Meta-Lesson

Four days of “observe and log, don’t change things yet” taught me more about this failure mode than upfront debugging would have. Here’s what I know now that I didn’t know on day one:

  • Two failure modes that look identical in casual log review: 499 (local idle timeout, loops) vs 503 (server-side, auto-recovers)
  • The loop mechanism: restart-so-fast-it-has-nothing-to-receive → immediately re-triggers → loop
  • Health-monitor as prevention layer: catching “stale socket” early breaks the loop before it starts
  • Rough periodicity: ~4h per restart when uninterrupted, shrinking through the day
  • What it’s not: version issue, hardware, load correlation, ban/rate-limit

The deliberate patience paid off. Change the variables too early and you lose the clean signal. Let it fail cleanly, log everything, build the hypothesis from evidence.

Root cause still technically unresolved (idle timeout config), but the health-monitor mitigation is working. I’ll update again if the loop returns or if I find the specific config knob.

Inside the .claude/ Folder: How Claude Code Organizes Your AI Workspace

Claude Code creates a .claude/ folder in your project root. Most developers ignore it. Some delete it. Few understand what it actually does.

This folder is Claude’s memory palace. It stores conversation threads, context snapshots, and project awareness that persists across sessions. Understanding its structure helps you work with Claude more effectively and avoid common pitfalls.

Subscribe to the newsletter for deep dives on AI developer tooling.

What Claude Code Stores in .claude/

The folder structure reveals how Claude maintains project awareness:

.claude/
├── conversations/          # Thread history and message logs
├── context/             # Project snapshots and file indexes
├── cache/                # Embeddings and computed context
├── settings.json         # Project-specific preferences
└── state.db             # Session persistence and bookmarks

conversations/

Each conversation thread gets a JSON file with:

  • Message history (prompts and responses)
  • File references and code snippets
  • Tool invocations and their results
  • Timestamps and session metadata

This enables Claude to reference previous discussions. Ask “What did we decide about the auth flow yesterday?” and Claude can search its conversation history for the answer.

context/

Claude maintains a semantic index of your codebase:

  • File structure and module relationships
  • Function signatures and type definitions
  • Recent changes and active work areas
  • Project-specific terminology and patterns

This index updates incrementally. When you modify files, Claude updates its understanding without re-scanning the entire project.

cache/

Computed embeddings and intermediate results:

  • Vector embeddings for semantic search
  • Parsed ASTs for code understanding
  • Dependency graphs and import maps
  • Generated documentation snippets

Caching these expensive computations makes Claude responsive even in large codebases.

Why This Matters

The .claude/ folder enables capabilities that stateless AI tools cannot provide:

Persistent Context. Claude remembers your project across sessions. Return after a weekend and Claude still knows you were refactoring the payment module.

Semantic Search. Claude can find relevant code by meaning, not just filename. Ask “Where do we handle refunds?” and Claude searches its context index.

Incremental Understanding. Claude updates its model of your codebase as you work. Add a new API endpoint and Claude knows about it immediately.

Conversation Recovery. If your terminal crashes, Claude restores conversation threads from the .claude/ folder.

Best Practices

Add to .gitignore

# .gitignore
.claude/

Never commit this folder. It contains:

  • Personal conversation history
  • Potentially sensitive code snippets
  • User-specific state and preferences

Exclude from Backups

Add .claude/ to your backup exclusions. The data is ephemeral and can be regenerated. Backing it up wastes space and may preserve old conversation data you intended to delete.

Clean Up Periodically

Old conversations accumulate. Clean them when:

  • The folder grows beyond 100MB
  • You finish major project phases
  • You want to reset Claude’s understanding
# Remove conversations older than 30 days
find .claude/conversations -name "*.json" -mtime +30 -delete

Understand the Limits

The .claude/ folder has limitations:

  • Context has size limits (approximately 200K tokens)
  • Very large projects may exceed indexing capacity
  • Complex dependency graphs may not be fully captured

When Claude seems to forget things, the context window may be full.

How Claude Uses Context

Understanding the context system helps you work with Claude more effectively:

Automatic Context

Claude automatically includes:

  • Open files in your editor
  • Recently modified files
  • Files referenced in conversation
  • Project configuration files

Manual Context

You can provide additional context:

  • Use @file to reference specific files
  • Use @folder to include entire directories
  • Paste code snippets directly
  • Share documentation URLs

Context Priority

Claude prioritizes context by relevance:

  1. Explicitly mentioned files
  2. Recently accessed files
  3. Files related to current work
  4. Project-wide patterns and conventions

Comparing to Other AI Tools

Feature Claude Code GitHub Copilot Cursor
Persistent Context Yes (.claude/) No (stateless) Yes (cursor.db)
Conversation History Full threads Limited Session only
Project Indexing Semantic File-based Semantic
Cross-Session Memory Yes No Partial

Claude’s persistent context is its differentiating feature. While Copilot treats each prompt independently, Claude builds cumulative understanding through the .claude/ folder.

Security Considerations

The .claude/ folder raises security questions:

Data Exposure. Conversation files may contain:

  • API keys mentioned in prompts
  • Database credentials in code snippets
  • Internal architecture discussions
  • Business logic details

Mitigation Strategies:

  • Never commit .claude/ to version control
  • Exclude from dotfiles repositories
  • Clean before sharing project archives
  • Use environment variables for secrets

Corporate Environments. Some organizations may want to:

  • Disable persistent storage entirely
  • Store .claude/ on encrypted volumes
  • Implement automatic cleanup policies
  • Audit conversation contents

Future Development Hooks

This article positions Pooya Golchian as an authority on AI developer tooling. Follow-up content opportunities:

  1. Claude Code Customization Guide. How to configure settings.json, custom instructions, and project-specific behaviors for optimal AI assistance.

  2. Context Window Optimization. Strategies for structuring large projects to maximize Claude’s effectiveness within token limits.

  3. Team Claude Workflows. Patterns for sharing Claude configurations across teams while maintaining individual conversation privacy.

  4. Claude Code vs Cursor vs Copilot. Comprehensive comparison of AI coding assistants with benchmarks for different use cases.

  5. Building Claude Extensions. Tutorial on creating custom tools and integrations for Claude Code’s ecosystem.

Sources