<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pushpum Vats</title>
    <description>The latest articles on DEV Community by Pushpum Vats (@pushpum_vats).</description>
    <link>https://dev.to/pushpum_vats</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3983766%2F604ab36c-98dc-4e5a-bfe8-5bf7bf8349b5.png</url>
      <title>DEV Community: Pushpum Vats</title>
      <link>https://dev.to/pushpum_vats</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pushpum_vats"/>
    <language>en</language>
    <item>
      <title>Qwen3.8-27B Hits Cerebras at 1,500 Tokens a Second: The Full Picture</title>
      <dc:creator>Pushpum Vats</dc:creator>
      <pubDate>Sun, 06 Sep 2026 07:29:28 +0000</pubDate>
      <link>https://dev.to/pushpum_vats/qwen38-27b-hits-cerebras-at-1500-tokens-a-second-the-full-picture-4clb</link>
      <guid>https://dev.to/pushpum_vats/qwen38-27b-hits-cerebras-at-1500-tokens-a-second-the-full-picture-4clb</guid>
      <description>&lt;p&gt;A &lt;a href="https://news.ycombinator.com/item?id=49554520" rel="noopener noreferrer"&gt;Hacker News thread&lt;/a&gt; is circulating right now with a simple claim: Qwen3.8-27B, Alibaba's newest open-weight model, is live on Cerebras's inference cloud at roughly 1,500 tokens per second. It's fresh enough that the comment count and thread age are still moving — worth checking the live page rather than trusting any snapshot, including this one.&lt;/p&gt;

&lt;p&gt;Before writing anything else, I checked the link the post actually points to. That's where this got interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The link doesn't back the headline. The model does.
&lt;/h2&gt;

&lt;p&gt;The submission points to Cerebras's general model catalog at &lt;a href="https://inference-docs.cerebras.ai/models/overview" rel="noopener noreferrer"&gt;inference-docs.cerebras.ai/models/overview&lt;/a&gt;. Fetch that page today and the "Available Models" table lists exactly two entries: OpenAI GPT-OSS (120B, ~3,000 tok/s) and Gemma 4 31B (31B, ~1,850 tok/s). No Qwen3.8-27B in sight.&lt;/p&gt;

&lt;p&gt;That doesn't make the claim false — it makes the link stale. Cerebras maintains a separate, fully populated page at &lt;a href="https://inference-docs.cerebras.ai/models/qwen-3.8-27b" rel="noopener noreferrer"&gt;/models/qwen-3.8-27b&lt;/a&gt;, and it confirms everything the headline promises: model ID &lt;code&gt;qwen-3.8-27b&lt;/code&gt;, a measured ~1,500 tokens/sec, live pricing, live rate limits.&lt;/p&gt;

&lt;p&gt;The model is real and it's live. The catalog page the submitter linked simply hasn't caught up yet.&lt;/p&gt;

&lt;p&gt;One community report from mid-August suggested Cerebras planned to swap Gemma-4-31B out of the free tier for Qwen3.8-27B during the first week of September — which is this week, and would explain the gap. Treat that as a plausible explanation, not a confirmed one; it traces back to a single forum post, not a Cerebras announcement, and I haven't found independent corroboration.&lt;/p&gt;

&lt;p&gt;The lesson generalizes past this one thread: if you're verifying a vendor claim, don't stop at whatever link happens to be attached. Check the model's own page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's actually live
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Free Trial&lt;/th&gt;
&lt;th&gt;Developer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Context window&lt;/td&gt;
&lt;td&gt;64K tokens&lt;/td&gt;
&lt;td&gt;128K tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max output&lt;/td&gt;
&lt;td&gt;32K tokens&lt;/td&gt;
&lt;td&gt;40K tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requests/min&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;300&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uncached tokens/min&lt;/td&gt;
&lt;td&gt;30K&lt;/td&gt;
&lt;td&gt;150K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total tokens/min&lt;/td&gt;
&lt;td&gt;90K&lt;/td&gt;
&lt;td&gt;450K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily tokens&lt;/td&gt;
&lt;td&gt;1M&lt;/td&gt;
&lt;td&gt;No cap&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pricing is $0.99 per million input tokens, $1.49 per million output. The model is multimodal — text and image in, text out — and supports tool calling, parallel tool calling, structured outputs, streaming, and prompt caching.&lt;/p&gt;

&lt;p&gt;Cerebras first reacted to Qwen3.8-27B back in mid-August, congratulating the Qwen team on X and promising it would land "soon" on the Shared Tier. What's live now is that promise made good: real pricing, real limits, three weeks after the initial nod. Worth separating in your head — the August tweet and this week's launch are not the same event.&lt;/p&gt;

&lt;p&gt;One detail for anyone porting workloads over: reasoning defaults to &lt;code&gt;high&lt;/code&gt; on Cerebras's endpoint, one notch below Qwen's own upstream default of &lt;code&gt;xhigh&lt;/code&gt;. Simon Willison has noted that &lt;code&gt;xhigh&lt;/code&gt; burns through context fast enough to matter on smaller setups — so Cerebras's lower default may be a deliberate choice, not an oversight.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Qwen3.8-27B actually is
&lt;/h2&gt;

&lt;p&gt;Alibaba shipped it on August 14, 2026: a 27-billion-parameter dense model, not a mixture-of-experts, under Apache 2.0. Native context runs to 262,144 tokens, extensible to 1,000,000 via YaRN — Cerebras currently exposes a fraction of that ceiling.&lt;/p&gt;

&lt;p&gt;The architecture is a hybrid, not a plain transformer stack. Sixty-four layers arrange into 16 repeating blocks, each pairing three linear-attention (Gated DeltaNet) layers with one full-attention (Gated Attention) layer. Only 16 of the 64 layers build a conventional KV cache — the same design lineage as Qwen's 2.4-trillion-parameter flagship, tracing back to Qwen3-Next.&lt;/p&gt;

&lt;p&gt;Some coverage, Wikipedia included, calls the 27B a distilled model. Kingy.ai's independent analysis is more careful: Alibaba hasn't published enough about the training process to say how much of the gain is distillation versus separate training on the same architecture.&lt;/p&gt;

&lt;p&gt;It replaces Qwen3.6-27B outright, and Qwen's own benchmark card shows a sizable jump on DeepSWE 1.1 — worth pulling the primary card yourself for the exact figures rather than trusting any single writeup, this one included.&lt;/p&gt;

&lt;h2&gt;
  
  
  1,500 tok/s isn't a record. It's not even close.
&lt;/h2&gt;

&lt;p&gt;Here's what the headline number obscures: Cerebras hit exactly 1,500 tokens per second before, with Qwen3-235B, back in July 2025. Two earlier launches beat it outright — Llama 4 Scout ran north of 2,600 tokens per second in April 2025, and Qwen3-32B hit roughly 2,400 the following month. Both figures come straight from Cerebras's own press materials.&lt;/p&gt;

&lt;p&gt;That's not a knock on the number. A typical GPU-backed inference API tops out around 150 tokens per second, so 1,500 is still a real 10x jump for anyone building on it. It's Cerebras's wafer-scale hardware doing what it always does — this just isn't a new milestone for the company.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmarks, with the footnotes attached
&lt;/h2&gt;

&lt;p&gt;Qwen's own launch table, independently reviewed by Kingy.ai, has Qwen3.8-27B beating Qwen3.7-Plus on most reported metrics, with GPQA Diamond and HLE (text) cited as the exceptions.&lt;/p&gt;

&lt;p&gt;Against the frontier comparison point in that same table — labeled "Claude Opus 4.6 Max" — Qwen3.8-27B is reported to trail by several points across GPQA, HLE, Terminal-Bench, and NL2Repo. Treat the exact gaps as approximate until you've checked Qwen's published card directly; secondhand benchmark tables are where small transcription errors like to hide.&lt;/p&gt;

&lt;p&gt;One correction worth making explicit: Anthropic doesn't sell a product called "Opus 4.6 Max." Claude Opus 4.6 ships with four effort levels — low, medium, high, and max — and "max" is a setting, not a SKU. Qwen's chart almost certainly means Opus 4.6 run at its highest effort tier, labeled informally by whoever built the table.&lt;/p&gt;

&lt;p&gt;If the reported gaps hold up, that's still a strong showing for a 27B dense model against a much larger closed system. But read the fine print before the leaderboard position: NL2Repo blocks package installs so models can't shortcut the answer, DeepSWE runs at 256K context specifically because that favors Claude Code's setup, and HLE uses GPT-4o as the grading judge.&lt;/p&gt;

&lt;p&gt;Vendor tables are built to make the vendor's model look good — that's not an accusation, it's the genre.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the thread actually cared about
&lt;/h2&gt;

&lt;p&gt;The speed number barely came up. The practical questions did:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Prompt caching?&lt;/strong&gt; Yes — &lt;a href="https://inference-docs.cerebras.ai/capabilities/prompt-caching" rel="noopener noreferrer"&gt;Cerebras's own docs&lt;/a&gt; confirm it, which settles a real cost concern for agentic coding workloads.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why not host the 2.4-trillion-parameter flagship instead of the 27B?&lt;/strong&gt; Two theories surfaced: a wafer interconnect limit at that scale, and the more cynical read that Cerebras's inference business exists to sell hardware — so the model that fits the shared tier economically wins over the model that wins headlines.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Onboarding friction.&lt;/strong&gt; Complaints about signup flow and support channels are a recurring feature of this kind of thread — worth noting, but treat any single complaint as one person's experience, not a pattern, unless it's corroborated elsewhere.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Speed isn't everything.&lt;/strong&gt; A sentiment that shows up in nearly every Cerebras thread, and shows up here too.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Specific usernames, comment counts, and verbatim quotes from a live thread age out fast — check the source directly rather than trusting a snapshot from this piece.&lt;/p&gt;

&lt;h2&gt;
  
  
  The caveat worth keeping in mind
&lt;/h2&gt;

&lt;p&gt;Cerebras made a similar throughput promise last year for Qwen3-Coder-480B on its Code plan: 2,000 tokens per second, advertised. InfoWorld's review, &lt;a href="https://www.infoworld.com/article/4055909/down-and-out-with-cerebras-code.html" rel="noopener noreferrer"&gt;"Down and out with Cerebras Code,"&lt;/a&gt; found real-world runs rarely broke 500 tokens per second, and some stayed under 100.&lt;/p&gt;

&lt;p&gt;That's a different model on a different product tier, so it isn't evidence against today's 1,500 figure — but it's a good reminder that headline throughput numbers and what your own prompts actually get are two different measurements.&lt;/p&gt;

&lt;p&gt;Run your own benchmark against your own workload before you build a pipeline around anyone's advertised number, Cerebras included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;Qwen3.8-27B on Cerebras is real, and the 1,500 tokens-per-second figure checks out — on its own model page, not on the general catalog link the Hacker News post actually pointed to. It's not a new speed record for Cerebras.&lt;/p&gt;

&lt;p&gt;And the benchmark story is a strong one with real vendor-table caveats attached, worth verifying against Qwen's primary source rather than taking on faith from any secondary writeup.&lt;/p&gt;

&lt;p&gt;If you need fast, cheap multimodal reasoning inside a 128K context window: test it against your own workload, and check the model-specific docs page — not the one linked in whatever post sent you here — before you commit spend.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/qwen3-8-27b-hits-cerebras-at-1-500-tokens-a-second-the-full-picture-p1xmy" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>qwen3827b</category>
      <category>cerebras</category>
      <category>alibaba</category>
      <category>llminference</category>
    </item>
    <item>
      <title>Prompt &amp; Rules Quality for Cursor, Windsurf, and Claude Code: The Complete Developer Guide</title>
      <dc:creator>Pushpum Vats</dc:creator>
      <pubDate>Fri, 04 Sep 2026 06:48:23 +0000</pubDate>
      <link>https://dev.to/pushpum_vats/prompt-rules-quality-for-cursor-windsurf-and-claude-code-the-complete-developer-guide-448n</link>
      <guid>https://dev.to/pushpum_vats/prompt-rules-quality-for-cursor-windsurf-and-claude-code-the-complete-developer-guide-448n</guid>
      <description>&lt;p&gt;AI coding agents forget everything the moment your session ends. Every new chat, every fresh terminal, every reset context window starts from zero — no memory of your linting rules, your folder conventions, or the fact that you told it three times yesterday not to use default exports.&lt;/p&gt;

&lt;p&gt;Rules files and hooks are how you fix that. They're not a bolt-on "prompt quality" product — they're native, built-in mechanisms in Cursor, Windsurf, and Claude Code that inject persistent context before the model ever sees your prompt, and (in Claude Code's case) can deterministically gate what the agent is allowed to do.&lt;/p&gt;

&lt;p&gt;This guide covers the real, current mechanism in each tool, with working code, plus how to keep them in sync so you're not maintaining three divergent config files by hand.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;LLMs are stateless between completions. Without a rules mechanism, "use functional components," "no default exports," "TypeScript strict mode," and "never touch &lt;code&gt;.env&lt;/code&gt;" all have to be retyped, every session, and the model still might not follow them consistently.&lt;/p&gt;

&lt;p&gt;Rules files fix the &lt;em&gt;first&lt;/em&gt; problem: they inject standing context automatically. Hooks (Claude Code only, as of this writing) fix the &lt;em&gt;second&lt;/em&gt; problem: they give you deterministic enforcement that doesn't depend on the model remembering anything at all.&lt;/p&gt;

&lt;p&gt;Keep that distinction in mind through the rest of this guide — &lt;strong&gt;rules shape what the agent knows; hooks control what the agent is allowed to do.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Cursor: Project Rules (&lt;code&gt;.mdc&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;The old single-file &lt;code&gt;.cursorrules&lt;/code&gt; is deprecated. The current system is &lt;strong&gt;Project Rules&lt;/strong&gt; — multiple &lt;code&gt;.mdc&lt;/code&gt; (Markdown + Config) files living in &lt;code&gt;.cursor/rules/&lt;/code&gt;, each with its own activation logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule types
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Shared via&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Project Rules&lt;/td&gt;
&lt;td&gt;.cursor/rules/*.mdc&lt;/td&gt;
&lt;td&gt;Current repo&lt;/td&gt;
&lt;td&gt;Git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User Rules&lt;/td&gt;
&lt;td&gt;Cursor Settings → Rules&lt;/td&gt;
&lt;td&gt;All your projects&lt;/td&gt;
&lt;td&gt;Not shared&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team Rules&lt;/td&gt;
&lt;td&gt;Cursor dashboard&lt;/td&gt;
&lt;td&gt;All team members&lt;/td&gt;
&lt;td&gt;Dashboard (paid plans)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AGENTS.md&lt;/td&gt;
&lt;td&gt;Project root/subdirs&lt;/td&gt;
&lt;td&gt;Current repo&lt;/td&gt;
&lt;td&gt;Git&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;.mdc&lt;/code&gt; format
&lt;/h3&gt;

&lt;p&gt;Each file is YAML frontmatter + Markdown body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TypeScript&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;conventions&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;this&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;project"&lt;/span&gt;
&lt;span class="na"&gt;globs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/**/*.ts,src/**/*.tsx"&lt;/span&gt;
&lt;span class="na"&gt;alwaysApply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# TypeScript Standards&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Use strict mode (&lt;span class="sb"&gt;`tsconfig`&lt;/span&gt; → &lt;span class="sb"&gt;`strict: true`&lt;/span&gt;)
&lt;span class="p"&gt;-&lt;/span&gt; Prefer interfaces over type aliases for object shapes
&lt;span class="p"&gt;-&lt;/span&gt; Use const objects instead of enums
&lt;span class="p"&gt;-&lt;/span&gt; No default exports except for Next.js page components
&lt;span class="p"&gt;-&lt;/span&gt; All function parameters must have explicit types
&lt;span class="p"&gt;-&lt;/span&gt; Use early returns for error conditions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Frontmatter fields and the four activation modes
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;description&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Summary the agent uses to decide relevance in "Agent Requested" mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;globs&lt;/td&gt;
&lt;td&gt;string | string[]&lt;/td&gt;
&lt;td&gt;File patterns that auto-attach this rule when a matching file is open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alwaysApply&lt;/td&gt;
&lt;td&gt;boolean&lt;/td&gt;
&lt;td&gt;If true, injected into every chat regardless of context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Combining these gives you four practical modes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;&amp;lt;!-- 1. Always — injected into every single conversation --&amp;gt;
---
&lt;/span&gt;description: "Core project context"
&lt;span class="gh"&gt;alwaysApply: true
---
&lt;/span&gt;
&lt;span class="gh"&gt;&amp;lt;!-- 2. Auto-Attached — fires only when matching files are in context --&amp;gt;
---
&lt;/span&gt;description: "Django ORM and DRF patterns"
globs: "&lt;span class="gs"&gt;**/backend/**&lt;/span&gt;/&lt;span class="err"&gt;*&lt;/span&gt;.py"
&lt;span class="gh"&gt;alwaysApply: false
---
&lt;/span&gt;
&lt;span class="gh"&gt;&amp;lt;!-- 3. Agent Requested — model reads the description and decides --&amp;gt;
---
&lt;/span&gt;description: "How to write and run integration tests in this repo"
&lt;span class="gh"&gt;alwaysApply: false
---
&lt;/span&gt;
&lt;span class="gh"&gt;&amp;lt;!-- 4. Manual — only injected when explicitly @-mentioned in chat --&amp;gt;
---
&lt;/span&gt;description: "One-off migration playbook, rarely needed"
&lt;span class="gh"&gt;alwaysApply: false
---
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A realistic rule set
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;project&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mdc&lt;/span&gt;      &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;alwaysApply&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;typescript&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mdc&lt;/span&gt;           &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;globs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="cm"&gt;/**/&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;.{&lt;/span&gt;&lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;tsx&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="nx"&gt;testing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mdc&lt;/span&gt;              &lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="nx"&gt;globs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="cm"&gt;/*.test.ts
├── api-error-format.mdc     # description-based, agent decides
└── db-migration-local.mdc   # manual, gitignored, personal only
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.cursor/rules/testing.mdc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Testing&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;conventions&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;using&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Vitest"&lt;/span&gt;
&lt;span class="na"&gt;globs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**/*.test.ts,**/*.spec.ts"&lt;/span&gt;
&lt;span class="na"&gt;alwaysApply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Testing Standards&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Use Vitest, not Jest
&lt;span class="p"&gt;-&lt;/span&gt; One &lt;span class="sb"&gt;`describe`&lt;/span&gt; block per exported function
&lt;span class="p"&gt;-&lt;/span&gt; Mock external HTTP calls with &lt;span class="sb"&gt;`msw`&lt;/span&gt;, never with manual &lt;span class="sb"&gt;`fetch`&lt;/span&gt; stubs
&lt;span class="p"&gt;-&lt;/span&gt; Every new API route needs at least one happy-path and one error-path test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Practical rules
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Keep each &lt;code&gt;.mdc&lt;/code&gt; file under ~500 lines — a bloated file burns context budget on every request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Name files in kebab-case by concern (&lt;code&gt;error-responses.mdc&lt;/code&gt;, not &lt;code&gt;rules2.mdc&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Disable a rule without deleting it: set &lt;code&gt;alwaysApply: false&lt;/code&gt; and strip the &lt;code&gt;globs&lt;/code&gt;/&lt;code&gt;description&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For personal, non-shared overrides, use a &lt;code&gt;*-local.mdc&lt;/code&gt; naming convention and add &lt;code&gt;.cursor/rules/*-local.mdc&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; is the simpler fallback for small projects that don't need glob-level scoping — and it doubles as a cross-tool format (see "Keeping rules in sync across tools" below).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Windsurf: Rules &amp;amp; Memories (Cascade)
&lt;/h2&gt;

&lt;p&gt;Windsurf's agent, Cascade, splits persistent context into two separate systems that people often conflate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Memories&lt;/strong&gt; — auto-generated by Cascade during a session, or created on request ("remember this"). Workspace-scoped, free (no credit cost), not shared across projects.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rules&lt;/strong&gt; — explicitly authored by you, always active, applied globally or per-workspace.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Rule storage locations
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;~/.codeium/windsurf/global_rules.md (or via Settings)&lt;/td&gt;
&lt;td&gt;Every workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.windsurf/rules/*.md in the project root&lt;/td&gt;
&lt;td&gt;Current workspace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.windsurf/rules/ in a subdirectory&lt;/td&gt;
&lt;td&gt;That subdirectory only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.windsurfrules (legacy single file)&lt;/td&gt;
&lt;td&gt;Current workspace, still supported&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Activation modes
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Always On&lt;/td&gt;
&lt;td&gt;Injected into every Cascade interaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual&lt;/td&gt;
&lt;td&gt;Only applied when @mentioned in the Cascade input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model Decision&lt;/td&gt;
&lt;td&gt;Cascade reads a natural-language description and decides relevance itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glob&lt;/td&gt;
&lt;td&gt;Applied when files matching a pattern (e.g. src/*&lt;em&gt;/&lt;/em&gt;.ts) are in context&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Example: &lt;code&gt;global_rules.md&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Global Rules&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Server Components by default in Next.js; Client Components only when
  the file needs interactivity, state, or browser-only APIs
&lt;span class="p"&gt;-&lt;/span&gt; Never suggest &lt;span class="sb"&gt;`any`&lt;/span&gt; in TypeScript — use &lt;span class="sb"&gt;`unknown`&lt;/span&gt; and narrow it
&lt;span class="p"&gt;-&lt;/span&gt; Prefer named exports over default exports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example: workspace rule, glob-scoped
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;glob&lt;/span&gt;
&lt;span class="na"&gt;globs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.py"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Python Backend Rules&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Follow PEP 8; format with &lt;span class="sb"&gt;`ruff format`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; All public functions require type hints and a docstring
&lt;span class="p"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`pydantic`&lt;/span&gt; models for request/response validation, not raw dicts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  XML-tag style (Windsurf's alternate, model-friendly format)
&lt;/h3&gt;

&lt;p&gt;Windsurf documentation also supports wrapping rules in XML-style tags, which some teams find easier for the model to parse and cite back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;coding_guidelines&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; My project's programming language is Python
&lt;span class="p"&gt;-&lt;/span&gt; Use early returns when possible
&lt;span class="p"&gt;-&lt;/span&gt; Always add docstrings when creating new functions and classes
&lt;span class="nt"&gt;&amp;lt;/coding_guidelines&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Practical rules
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Each rule file has a hard character cap (in the low five figures — Windsurf truncates content beyond it), so split large standards into multiple focused files rather than one giant &lt;code&gt;global_rules.md&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add &lt;code&gt;.windsurfrules&lt;/code&gt; (the legacy single-file format) to &lt;code&gt;.gitignore&lt;/code&gt; if it holds personal preferences rather than team conventions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Don't put project-specific facts ("we migrated the auth service last sprint") in Rules — that's what auto-generated Memories are for. Rules should hold durable &lt;em&gt;policy&lt;/em&gt;, not session trivia.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Review Cascade's auto-generated memories periodically via the Memories panel; stale ones actively mislead the agent.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Claude Code: &lt;code&gt;CLAUDE.md&lt;/code&gt; + Hooks
&lt;/h2&gt;

&lt;p&gt;Claude Code splits the same problem into two genuinely different tools, and this is the part most guides gloss over: &lt;code&gt;CLAUDE.md&lt;/code&gt; &lt;strong&gt;is context, hooks are control.&lt;/strong&gt; &lt;code&gt;CLAUDE.md&lt;/code&gt; can be ignored by a distracted model. A hook cannot.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;CLAUDE.md&lt;/code&gt; hierarchy
&lt;/h3&gt;

&lt;p&gt;Claude Code walks up the directory tree and merges multiple &lt;code&gt;CLAUDE.md&lt;/code&gt; files, broadest to most specific:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Managed policy&lt;/td&gt;
&lt;td&gt;/Library/Application Support/ClaudeCode/CLAUDE.md (macOS) / /etc/claude-code/CLAUDE.md (Linux)&lt;/td&gt;
&lt;td&gt;Org-wide, enterprise-managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User&lt;/td&gt;
&lt;td&gt;~/.claude/CLAUDE.md&lt;/td&gt;
&lt;td&gt;Every project, every session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Project&lt;/td&gt;
&lt;td&gt;CLAUDE.md at repo root&lt;/td&gt;
&lt;td&gt;This repo, committed to git&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subdirectory&lt;/td&gt;
&lt;td&gt;&amp;lt;dir&amp;gt;/CLAUDE.md&lt;/td&gt;
&lt;td&gt;Loaded on demand when Claude works in that folder&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;More specific files layer on top of (don't fully replace) broader ones — a &lt;code&gt;frontend/CLAUDE.md&lt;/code&gt; and a &lt;code&gt;backend/CLAUDE.md&lt;/code&gt; can carry completely different conventions in a monorepo without bloating the root file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example &lt;code&gt;CLAUDE.md&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Project: Acme Billing Service&lt;/span&gt;

&lt;span class="gu"&gt;## Stack&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Node 20, TypeScript, Fastify, Postgres via Prisma
&lt;span class="p"&gt;-&lt;/span&gt; Tests: Vitest. Run with &lt;span class="sb"&gt;`npm test`&lt;/span&gt;, not &lt;span class="sb"&gt;`npm run test:watch`&lt;/span&gt; in CI contexts.

&lt;span class="gu"&gt;## Conventions&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; All monetary values are integer cents, never floats
&lt;span class="p"&gt;-&lt;/span&gt; Every new endpoint needs a corresponding OpenAPI entry in &lt;span class="sb"&gt;`openapi.yaml`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Do not add new npm dependencies without calling it out explicitly in your response

&lt;span class="gu"&gt;## Commands&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`npm run dev`&lt;/span&gt; — start local server on :3000
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`npm run db:migrate`&lt;/span&gt; — apply pending Prisma migrations
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`npm run lint:fix`&lt;/span&gt; — run before considering any task done

@docs/architecture.md
@~/.claude/personal-style.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The &lt;code&gt;@import&lt;/code&gt; syntax
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; files can pull in other files with &lt;code&gt;@path/to/file&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Supports relative paths, absolute paths, and home-directory paths (&lt;code&gt;@~/.claude/...&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Imports can themselves import other files, up to 5 levels deep&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;@&lt;/code&gt; references are &lt;strong&gt;not&lt;/strong&gt; evaluated inside code spans or fenced code blocks — so documenting the syntax (as above) doesn't accidentally trigger a real import&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use this to split a sprawling root file into composable pieces (&lt;code&gt;@docs/architecture.md&lt;/code&gt;, &lt;code&gt;@docs/testing-strategy.md&lt;/code&gt;) instead of one 2,000-line wall of text.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hooks: deterministic quality gates
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt; is a request, not a guarantee — the model can still skip a step under time pressure inside a long agentic run. &lt;strong&gt;Hooks&lt;/strong&gt; are shell commands Claude Code fires automatically at fixed points in its execution loop, regardless of what the model "remembers."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where hooks live:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;~/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;          &lt;span class="c1"&gt;# global, all projects
&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;            &lt;span class="c1"&gt;# project-level, committable
&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;      &lt;span class="c1"&gt;# project-level, gitignored
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;managed&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;            &lt;span class="c1"&gt;# org-wide, admin-controlled
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key lifecycle events:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Fires&lt;/th&gt;
&lt;th&gt;Can block the action?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PreToolUse&lt;/td&gt;
&lt;td&gt;Before a tool call executes&lt;/td&gt;
&lt;td&gt;✅ Yes (exit code 2)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostToolUse&lt;/td&gt;
&lt;td&gt;After a tool call succeeds&lt;/td&gt;
&lt;td&gt;❌ No — but can trigger follow-up (format, test, notify)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UserPromptSubmit&lt;/td&gt;
&lt;td&gt;When you submit a prompt, before Claude processes it&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;When Claude finishes responding&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SessionStart&lt;/td&gt;
&lt;td&gt;New session begins&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PreCompact&lt;/td&gt;
&lt;td&gt;Before context gets compacted&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notification&lt;/td&gt;
&lt;td&gt;Claude sends a notification&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Basic shape&lt;/strong&gt;, in &lt;code&gt;.claude/settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit|Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jq -r '.tool_input.file_path' | xargs npx prettier --write"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every hook receives structured JSON on stdin describing the event (&lt;code&gt;tool_name&lt;/code&gt;, &lt;code&gt;tool_input&lt;/code&gt;, etc.), and controls what happens next via its exit code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;exit 0&lt;/code&gt; → continue normally&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;exit 2&lt;/code&gt; (on &lt;code&gt;PreToolUse&lt;/code&gt;) → block the tool call; stderr is shown to the model as the reason&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Non-zero on &lt;code&gt;PostToolUse&lt;/code&gt; → surfaced as feedback, but the action already happened&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Example: block edits to &lt;code&gt;.env&lt;/code&gt; and force-pushes
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# .claude/hooks/block-dangerous.sh&lt;/span&gt;
&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;TOOL_INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input | tojson'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOOL_INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.file_path // empty'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'\.env'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Blocked: .env files must be edited manually, not by the agent"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi

if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TOOL_INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.command // empty'&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'push.*--force'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Blocked: force push requires human review"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;2
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash|Edit|Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bash .claude/hooks/block-dangerous.sh"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Example: auto-format and auto-test after every edit
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit|Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"jq -r '.tool_input.file_path' | xargs npx prettier --write"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the real difference from a rules file: writing "always run Prettier after editing" in &lt;code&gt;CLAUDE.md&lt;/code&gt; is a &lt;em&gt;hope&lt;/em&gt;. A &lt;code&gt;PostToolUse&lt;/code&gt; hook is a &lt;em&gt;guarantee&lt;/em&gt; — it runs every time, independent of the model's attention.&lt;/p&gt;

&lt;p&gt;Set an explicit &lt;code&gt;timeout&lt;/code&gt; (in seconds) on any hook that shells out to something slow, like a full test suite — default timeouts are short and have changed across releases, so don't rely on the default for long-running commands.&lt;/p&gt;




&lt;h2&gt;
  
  
  Side-by-side comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Cursor&lt;/th&gt;
&lt;th&gt;Windsurf&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Context file(s)&lt;/td&gt;
&lt;td&gt;.cursor/rules/*.mdc&lt;/td&gt;
&lt;td&gt;.windsurf/rules/*.md, global_rules.md&lt;/td&gt;
&lt;td&gt;CLAUDE.md (hierarchical)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legacy single-file format&lt;/td&gt;
&lt;td&gt;.cursorrules (deprecated)&lt;/td&gt;
&lt;td&gt;.windsurfrules (still supported)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Granular scoping&lt;/td&gt;
&lt;td&gt;Per-file, via globs&lt;/td&gt;
&lt;td&gt;Per-file, via glob trigger&lt;/td&gt;
&lt;td&gt;Per-directory, via nested files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model decides relevance?&lt;/td&gt;
&lt;td&gt;Yes (description, no alwaysApply)&lt;/td&gt;
&lt;td&gt;Yes ("Model Decision" mode)&lt;/td&gt;
&lt;td&gt;No — loaded by directory/hierarchy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-generated memory&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (Cascade Memories)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Team-shared rules&lt;/td&gt;
&lt;td&gt;Yes (paid dashboard)&lt;/td&gt;
&lt;td&gt;Via committed .windsurf/rules/&lt;/td&gt;
&lt;td&gt;Via committed CLAUDE.md&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can block an action outright&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes — PreToolUse hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-run commands after edits&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes — PostToolUse hooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-file import&lt;/td&gt;
&lt;td&gt;No native syntax&lt;/td&gt;
&lt;td&gt;No native syntax&lt;/td&gt;
&lt;td&gt;
&lt;a class="mentioned-user" href="https://dev.to/path"&gt;@path&lt;/a&gt; imports, depth 5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The takeaway: Cursor and Windsurf give you rich, granular &lt;em&gt;context injection&lt;/em&gt;. Claude Code gives you that too (via &lt;code&gt;CLAUDE.md&lt;/code&gt;), plus a second, independent layer of &lt;em&gt;enforcement&lt;/em&gt; that doesn't exist in the other two as of this writing.&lt;/p&gt;




&lt;h2&gt;
  
  
  One rule set, three formats — a worked example
&lt;/h2&gt;

&lt;p&gt;Say you want one policy — "REST handlers must validate input with Zod, and every mutation needs a test" — enforced consistently. Here's the same rule expressed natively in each tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cursor&lt;/strong&gt; — &lt;code&gt;.cursor/rules/api-validation.mdc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Input&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;validation&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;test&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;coverage&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;REST&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;handlers"&lt;/span&gt;
&lt;span class="na"&gt;globs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/routes/**/*.ts"&lt;/span&gt;
&lt;span class="na"&gt;alwaysApply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="gh"&gt;# API Validation&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Every route handler must validate &lt;span class="sb"&gt;`req.body`&lt;/span&gt; with a Zod schema before use
&lt;span class="p"&gt;-&lt;/span&gt; Every handler that mutates state needs a corresponding test in &lt;span class="sb"&gt;`*.test.ts`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windsurf&lt;/strong&gt; — &lt;code&gt;.windsurf/rules/api-validation.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;glob&lt;/span&gt;
&lt;span class="na"&gt;globs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;src/routes/**/*.ts"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="gh"&gt;# API Validation&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Every route handler must validate &lt;span class="sb"&gt;`req.body`&lt;/span&gt; with a Zod schema before use
&lt;span class="p"&gt;-&lt;/span&gt; Every handler that mutates state needs a corresponding test in &lt;span class="sb"&gt;`*.test.ts`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; — a section inside &lt;code&gt;src/routes/CLAUDE.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## API Validation&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Every route handler must validate &lt;span class="sb"&gt;`req.body`&lt;/span&gt; with a Zod schema before use
&lt;span class="p"&gt;-&lt;/span&gt; Every handler that mutates state needs a corresponding test in &lt;span class="sb"&gt;`*.test.ts`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...and optionally backed by a &lt;strong&gt;hook&lt;/strong&gt; that actually checks it, rather than trusting the model read the section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# .claude/hooks/check-zod-validation.sh — PostToolUse on route file edits&lt;/span&gt;
&lt;span class="nv"&gt;FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input.file_path // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; src/routes/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"z&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;zodSchema"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Warning: &lt;/span&gt;&lt;span class="nv"&gt;$FILE&lt;/span&gt;&lt;span class="s2"&gt; has no visible Zod validation call"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the content is identical — only the wrapper (frontmatter keys, file location) differs. That's exactly the problem the next section solves.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keeping rules in sync across tools
&lt;/h2&gt;

&lt;p&gt;Maintaining near-duplicate content in &lt;code&gt;.cursor/rules/&lt;/code&gt;, &lt;code&gt;.windsurf/rules/&lt;/code&gt;, and &lt;code&gt;CLAUDE.md&lt;/code&gt; by hand drifts fast. A small but real open-source ecosystem exists specifically to solve this — write once, generate the rest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ruler (&lt;code&gt;@intellectronica/ruler&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The most established of these. One &lt;code&gt;.ruler/&lt;/code&gt; directory is the source of truth; &lt;code&gt;ruler apply&lt;/code&gt; distributes it to every supported agent's native format (Claude Code, Cursor, Windsurf, Copilot, Codex CLI, Cline, Aider, and others).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @intellectronica/ruler
&lt;span class="nb"&gt;cd &lt;/span&gt;your-project
ruler init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ruler init&lt;/code&gt; scaffolds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ruler&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;
&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="n"&gt;AGENTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;md&lt;/span&gt;      &lt;span class="c1"&gt;# your canonical rules, in plain Markdown
&lt;/span&gt;&lt;span class="err"&gt;├──&lt;/span&gt; &lt;span class="n"&gt;ruler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;toml&lt;/span&gt;     &lt;span class="c1"&gt;# which agents to target, output paths
&lt;/span&gt;&lt;span class="err"&gt;└──&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;       &lt;span class="c1"&gt;# optional shared MCP server config
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Split large rule sets into focused files — they're concatenated alphabetically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.ruler/
├── AGENTS.md
├── coding_style.md
├── api_conventions.md
└── security_guidelines.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply to every configured agent, or target specific ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ruler apply                          &lt;span class="c"&gt;# all configured agents&lt;/span&gt;
ruler apply &lt;span class="nt"&gt;--agents&lt;/span&gt; cursor,claude   &lt;span class="c"&gt;# only these two&lt;/span&gt;
ruler revert                         &lt;span class="c"&gt;# undo, restores from .bak files&lt;/span&gt;
ruler revert &lt;span class="nt"&gt;--dry-run&lt;/span&gt;               &lt;span class="c"&gt;# preview the undo first&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ruler.toml&lt;/code&gt; controls which agents get generated and where — check &lt;code&gt;ruler --help&lt;/code&gt; for the exact keys in the version you install, since the CLI evolves, but the shape is roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;default_agents&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"claude"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"cursor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"windsurf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"copilot"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nn"&gt;[agents.cursor]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="nn"&gt;[agents.windsurf]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each generated file gets a source marker for traceability, e.g.:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Source: .ruler/api_conventions.md --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...so if Cursor's rule looks wrong, you know exactly which canonical file to fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lighter alternatives
&lt;/h3&gt;

&lt;p&gt;If you don't want a full tool, a pre-commit hook that regenerates target files from one canonical &lt;code&gt;AGENTS.md&lt;/code&gt; accomplishes the same thing with a few lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .husky/pre-commit&lt;/span&gt;
npx agentsync &lt;span class="nb"&gt;sync&lt;/span&gt;   &lt;span class="c"&gt;# example: regenerates CLAUDE.md, .cursorrules, etc. from AGENTS.md&lt;/span&gt;
git add &lt;span class="nt"&gt;-A&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The specific tool matters less than the principle: &lt;strong&gt;pick one canonical file, generate the rest, and never hand-edit a generated file.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Hooks as a real quality &lt;em&gt;gate&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;Since Claude Code is the only one of the three with an enforcement layer today, it's worth showing a fuller pattern: a "quality gate" that runs lint, type-check, and tests after every batch of edits, and reports failures back to the model so it can self-correct — without you doing anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit|Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bash .claude/hooks/quality-gate.sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# .claude/hooks/quality-gate.sh&lt;/span&gt;
&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tool_input.file_path // empty'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.ts &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.tsx &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0

npx eslint &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--quiet&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tee&lt;/span&gt; /tmp/lint-out.txt
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /tmp/lint-out.txt &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ESLint found issues in &lt;/span&gt;&lt;span class="nv"&gt;$FILE&lt;/span&gt;&lt;span class="s2"&gt; — fix before continuing:"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/lint-out.txt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1   &lt;span class="c"&gt;# PostToolUse can't block, but this surfaces feedback to the model&lt;/span&gt;
&lt;span class="k"&gt;fi

&lt;/span&gt;npx tsc &lt;span class="nt"&gt;--noEmit&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;tee&lt;/span&gt; /tmp/tsc-out.txt
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /tmp/tsc-out.txt &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Type errors present:"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/tsc-out.txt &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because this is a &lt;code&gt;PostToolUse&lt;/code&gt; hook it can't undo the edit — but a non-zero exit surfaces the lint/type output directly to the model as feedback, so in an agentic loop it typically self-corrects on the next turn instead of you catching it in review three files later.&lt;/p&gt;

&lt;p&gt;For anything that should be &lt;strong&gt;prevented outright&lt;/strong&gt; rather than corrected after the fact (touching production config, running &lt;code&gt;rm -rf&lt;/code&gt;, editing CI secrets), use &lt;code&gt;PreToolUse&lt;/code&gt; with &lt;code&gt;exit 2&lt;/code&gt; instead — that's the only exit code across either event that actually stops the action.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treating&lt;/strong&gt; &lt;code&gt;CLAUDE.md&lt;/code&gt; &lt;strong&gt;as an enforcement mechanism.&lt;/strong&gt; It's context. If something must never happen, it needs a &lt;code&gt;PreToolUse&lt;/code&gt; hook, not a bullet point.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;One giant rules file.&lt;/strong&gt; All three tools reward focused, single-topic files over a 2,000-line monolith — it wastes context budget and the model deprioritizes buried instructions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Forgetting&lt;/strong&gt; &lt;code&gt;alwaysApply&lt;/code&gt;&lt;strong&gt;/&lt;/strong&gt;&lt;code&gt;trigger&lt;/code&gt; &lt;strong&gt;semantics.&lt;/strong&gt; A Cursor rule with no &lt;code&gt;globs&lt;/code&gt; and &lt;code&gt;alwaysApply: false&lt;/code&gt; and a vague description may simply never fire. Test that your rule actually attaches.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Committing personal preferences.&lt;/strong&gt; "I prefer terse commit messages" belongs in Cursor's User Rules or a gitignored local file — not in a team's shared &lt;code&gt;.cursor/rules/&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No&lt;/strong&gt; &lt;code&gt;timeout&lt;/code&gt; &lt;strong&gt;on slow hooks.&lt;/strong&gt; A hook that shells out to a full test suite without an explicit timeout can stall the agent loop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hand-editing generated files.&lt;/strong&gt; If you adopt Ruler or a similar sync tool, edit the canonical source only — hand edits get silently overwritten on the next &lt;code&gt;apply&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Cheat sheet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Cursor
  .cursor/rules/&lt;span class="k"&gt;*&lt;/span&gt;.mdc        → frontmatter: description, globs, alwaysApply
  .cursor/rules/&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="nt"&gt;-local&lt;/span&gt;.mdc  → gitignore &lt;span class="k"&gt;for &lt;/span&gt;personal overrides

Windsurf
  global_rules.md            → all workspaces
  .windsurf/rules/&lt;span class="k"&gt;*&lt;/span&gt;.md       → this workspace &lt;span class="o"&gt;(&lt;/span&gt;or subdirectory&lt;span class="o"&gt;)&lt;/span&gt;
  .windsurfrules              → legacy single-file, still works

Claude Code
  ~/.claude/CLAUDE.md         → user, all projects
  ./CLAUDE.md                 → project root
  ./&lt;span class="k"&gt;**&lt;/span&gt;/CLAUDE.md              → subdirectory, loaded on demand
  @path/to/file.md            → import &lt;span class="o"&gt;(&lt;/span&gt;max depth 5&lt;span class="o"&gt;)&lt;/span&gt;
  ~/.claude/settings.json     → global hooks
  .claude/settings.json       → project hooks &lt;span class="o"&gt;(&lt;/span&gt;commit this&lt;span class="o"&gt;)&lt;/span&gt;
  .claude/settings.local.json → project hooks &lt;span class="o"&gt;(&lt;/span&gt;gitignore this&lt;span class="o"&gt;)&lt;/span&gt;

Sync tool
  npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @intellectronica/ruler
  ruler init &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ruler apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is&lt;/strong&gt; &lt;code&gt;.cursorrules&lt;/code&gt; &lt;strong&gt;still supported?&lt;/strong&gt; Yes, but it's deprecated — Cursor still reads it, but new projects should use &lt;code&gt;.cursor/rules/*.mdc&lt;/code&gt; for glob scoping and per-rule activation modes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Windsurf have anything like Claude Code's hooks?&lt;/strong&gt; Not as of this writing. Windsurf's Rules and Memories shape context; they don't gate or block tool calls the way a Claude Code &lt;code&gt;PreToolUse&lt;/code&gt; hook can.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a Cursor or Windsurf rule stop the agent from running a command?&lt;/strong&gt; No — both are context-injection systems. Neither has a deterministic blocking mechanism equivalent to Claude Code's &lt;code&gt;PreToolUse&lt;/code&gt; + &lt;code&gt;exit 2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the difference between Windsurf Memories and Rules?&lt;/strong&gt; Memories are auto-generated (or on-request) facts scoped to a workspace, free to create, meant for session-specific context. Rules are explicitly authored, always-active policy. Don't put durable team conventions in Memories — they're not guaranteed to persist the way a committed rules file is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a separate tool to keep these in sync?&lt;/strong&gt; Not strictly — you can hand-maintain three files for a small project. Past a handful of rules, drift becomes real; a sync tool like Ruler (or a simple pre-commit script) pays for itself quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where do MCP server configs fit into this?&lt;/strong&gt; Outside the scope of this guide, but the same tools generally handle it: Cursor uses &lt;code&gt;.cursor/mcp.json&lt;/code&gt;, Claude Code uses &lt;code&gt;.mcp.json&lt;/code&gt;, and Ruler can distribute a shared &lt;code&gt;mcp.json&lt;/code&gt; alongside your rules.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/prompt-rules-quality-for-cursor-windsurf-and-claude-code-the-complete-developer-guide-rk7u5" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>cursor</category>
      <category>windsurf</category>
      <category>claudecode</category>
      <category>aicodingagents</category>
    </item>
    <item>
      <title>Apple Just Confirmed Claude Is Coming to Your iPhone — Here's What WWDC 2026 Actually Changes</title>
      <dc:creator>Pushpum Vats</dc:creator>
      <pubDate>Thu, 03 Sep 2026 08:14:12 +0000</pubDate>
      <link>https://dev.to/pushpum_vats/apple-just-confirmed-claude-is-coming-to-your-iphone-heres-what-wwdc-2026-actually-changes-21he</link>
      <guid>https://dev.to/pushpum_vats/apple-just-confirmed-claude-is-coming-to-your-iphone-heres-what-wwdc-2026-actually-changes-21he</guid>
      <description>&lt;p&gt;For the last two years, "Siri" has been a slightly embarrassing word in Apple's vocabulary — the company famously paid out a quarter-billion-dollar settlement over AI features it promised and never shipped. At WWDC 2026, Apple's answer to that was to basically start over: a new assistant called &lt;strong&gt;Siri AI&lt;/strong&gt;, built on what Apple calls its Foundation Models, developed in partnership with Google using Gemini's underlying technology.&lt;/p&gt;

&lt;p&gt;That last part is the genuinely surprising bit. For a company that has spent a decade telling you everything happens "on device, on our chips," quietly admitting that its next-generation assistant leans on a rival's AI — reportedly at a cost of around a billion dollars a year — is a real shift in posture, even if Apple hasn't put a number on it publicly.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Siri AI actually does differently
&lt;/h2&gt;

&lt;p&gt;The core pitch is &lt;strong&gt;on-screen and cross-app awareness&lt;/strong&gt; — Siri can now look at what's on your screen and pull from Mail, Messages, and Photos in real time, without you switching apps. The demos at the keynote showed it finding specific photos by description ("the one with everyone wearing matching shirts") and building a multi-step calendar event just from reading a text conversation, without opening the Calendar app at all.&lt;/p&gt;

&lt;p&gt;There's also a genuine interface overhaul. Siri now lives in its own dedicated app — dark interface, a text box, a microphone toggle for voice, a paperclip for attaching files or images, basically the same shape as every other chatbot app at this point. Conversations get saved as a history and synced across your devices via iCloud, with an optional auto-expiration if you don't want a permanent log of everything you've asked it.&lt;/p&gt;

&lt;p&gt;Getting to it is also different now. There's a new system-wide gesture — swipe down from the Dynamic Island — that opens a "Search or Ask" bar, separate from the old Notification Center swipe (which has moved to the side). And individual Apple apps are getting an "Ask Siri" button built into their menus, so you can send what you're looking at straight to the assistant along with a request.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part everyone in the AI world actually cares about: choice
&lt;/h2&gt;

&lt;p&gt;Buried under all the Siri branding is the detail that matters most for anyone who already has a favorite AI: Apple is opening Siri up to outside models through something being called &lt;strong&gt;Extensions&lt;/strong&gt;. ChatGPT has technically been pluggable into Siri since iOS 26, but reporting around WWDC 2026 points to iOS 27 expanding this so you can choose between ChatGPT, Gemini, and &lt;strong&gt;Claude&lt;/strong&gt; for certain requests — picking which assistant actually answers, rather than everything being routed through Apple's own model by default.&lt;/p&gt;

&lt;p&gt;The honest caveat, and it's a big one: as of the keynote, the deepest new capabilities — the on-screen awareness, the Mail/Messages/Photos access — are reportedly siloed to Apple's own native experience for now. Whether a third-party assistant like Claude gets the same level of access to what's on your screen, or stays limited to the kind of standalone Q&amp;amp;A you'd get from opening the Claude app directly, is genuinely unclear at this point. If you've been hoping to point your &lt;em&gt;entire phone&lt;/em&gt; at Claude by default, that's not confirmed — what's confirmed is the ability to choose it for specific requests.&lt;/p&gt;




&lt;h2&gt;
  
  
  There's a developer story here too
&lt;/h2&gt;

&lt;p&gt;If you build software, the more concrete announcement might actually matter more than anything consumer-facing: &lt;strong&gt;Xcode 27&lt;/strong&gt; is getting coding agents built in from Anthropic, Google, and OpenAI directly, so you can build apps using Claude or Gemini models alongside Apple's own, inside Apple's own IDE. For a company that has historically kept its developer tools tightly closed, that's a notable opening — and it lines up with the same broader trend we've covered before on this blog: AI assistants increasingly plug into your existing tools rather than living in their own walled-off chat window.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's confirmed vs. what's still a rumor wearing a confirmed-fact costume
&lt;/h2&gt;

&lt;p&gt;It's worth being precise here, because a lot of the coverage from the last week blurs "Apple announced this" with "Bloomberg reported this is coming."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confirmed at the keynote:&lt;/strong&gt; the Siri AI rebrand, the standalone Siri app, on-screen/cross-app awareness for Apple's own apps, the new gesture-based "Search or Ask" interface, the Apple-Google Foundation Models partnership, and the Xcode 27 third-party coding agent integration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reported but not fully detailed by Apple:&lt;/strong&gt; the exact mechanics of the Extensions system for choosing Claude/Gemini/ChatGPT for specific requests, and whether third-party assistants will ever get the same on-screen access as Siri itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardware reality check:&lt;/strong&gt; Apple Intelligence still requires an iPhone 15 Pro or newer, and the most advanced on-device features — including the new Siri voice and updated dictation — need the newer A19 Pro chip. If you're on an older iPhone, expect a noticeably more limited version of all of this, if it works at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to actually do right now
&lt;/h2&gt;

&lt;p&gt;Nothing, really — and that's fine. iOS 27 is expected this fall alongside the new iPhone lineup, which is the normal Apple release rhythm. A few things worth doing in the meantime if you want to be ready on day one:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check your hardware.&lt;/strong&gt; If you're on an iPhone 14 or older, the headline Siri AI features likely won't be available to you at all regardless of software updates — this is a hardware-gated rollout, not just a software one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get comfortable with the Claude app now.&lt;/strong&gt; If Extensions does land roughly as reported, the most likely first version is "hand this specific request to Claude" rather than a deep OS-level takeover — which means knowing how to phrase a good request to Claude directly is exactly the skill that carries over. If you haven't spent time with it yet, this is a good moment to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't assume your existing automations will "just work."&lt;/strong&gt; If you've built anything around Siri Shortcuts, the interface and gesture changes mean some of those flows may need to be rebuilt once iOS 27 actually ships — worth keeping an eye on Apple's developer documentation closer to the fall.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters beyond Apple
&lt;/h2&gt;

&lt;p&gt;Step back from the iPhone specifics for a second, and this fits a pattern we've been tracking across this blog all year: AI is stopping being "an app you open" and becoming "a layer that's just present" — in your files via MCP, in your terminal via coding agents, in your photos via tools like Nano Banana, and now, apparently, in the operating system itself, with you choosing which model actually answers. The interesting fight in 2026 isn't really "which AI is smartest" anymore. It's which one you'll actually have &lt;em&gt;access to&lt;/em&gt; from wherever you happen to be — and for the first time, on the world's most popular phone, that might genuinely be your choice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.digitaltrends.com/computing/wwdc-2026-ios-27-siri-ai-apple-intelligence-upgrades-and-everything-else-apple-announced/" rel="noopener noreferrer"&gt;WWDC 2026 recap — Digital Trends&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://tech.yahoo.com/general/article/wwdc-2026-recap-all-new-siri-ai-ios-27-apple-intelligence-and-everything-else-apple-announced-173752306.html" rel="noopener noreferrer"&gt;Everything Apple announced at WWDC 2026 — Yahoo/Engadget&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.bloomberg.com/news/articles/2026-06-05/wwdc-2026-preview-ios-27-siri-ai-features-macos-27-more-apple-will-announce" rel="noopener noreferrer"&gt;WWDC 2026 preview — Bloomberg&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.popsci.com/gear/apple-wwdc-announcements-2026/" rel="noopener noreferrer"&gt;WWDC 2026 announcements — Popular Science&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/apple-just-confirmed-claude-is-coming-to-your-iphone-here-s-what-wwdc-2026-actually-changes-cfmtz" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>tutorial</category>
      <category>ainews</category>
      <category>claude</category>
    </item>
    <item>
      <title>Securing a VPS in 2026: A No-Nonsense Hardening Guide</title>
      <dc:creator>Pushpum Vats</dc:creator>
      <pubDate>Sat, 22 Aug 2026 06:32:34 +0000</pubDate>
      <link>https://dev.to/pushpum_vats/securing-a-vps-in-2026-a-no-nonsense-hardening-guide-1lk6</link>
      <guid>https://dev.to/pushpum_vats/securing-a-vps-in-2026-a-no-nonsense-hardening-guide-1lk6</guid>
      <description>&lt;p&gt;&lt;em&gt;Every step ranked by priority — SSH, firewall, patching, backups, and TLS, with the exact configs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A default VPS install is not a secure one. The moment it gets a public IP, automated bots start probing it for weak SSH logins, open ports, and outdated packages — often within minutes. This guide is ordered by impact: do Tier 1 before anything touches production, Tier 2 within the first week, Tier 3 if it applies to your stack, and Tier 4 on a recurring schedule forever. Written for Ubuntu 24.04 LTS / Debian 12, with notes where RHEL-based systems (AlmaLinux, Rocky) differ.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tier 1 — Before Anything Touches Production
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Non-Root User + SSH Key Auth
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On the server, as root&lt;/span&gt;
adduser deploy
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;deploy

&lt;span class="c"&gt;# On your local machine — generate a modern key (Ed25519, not RSA)&lt;/span&gt;
ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"you@yourdomain.com"&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; 100

&lt;span class="c"&gt;# Copy it to the server&lt;/span&gt;
ssh-copy-id &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/id_ed25519.pub deploy@your-server-ip

&lt;span class="c"&gt;# Log in as deploy, confirm sudo works, THEN lock root&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;passwd &lt;span class="nt"&gt;-l&lt;/span&gt; root

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ed25519 keys are smaller, faster, and free of the timing side-channels that affect RSA. Use RSA only if you're stuck supporting a legacy client, and if so use at least 4096 bits.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Harden sshd_config
&lt;/h3&gt;

&lt;p&gt;Keep a second terminal connected while you test — don't edit this over your only session. Edit &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; (or drop a file into &lt;code&gt;/etc/ssh/sshd_config.d/&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Authentication&lt;/span&gt;
&lt;span class="s"&gt;PermitRootLogin no&lt;/span&gt;
&lt;span class="s"&gt;PasswordAuthentication no&lt;/span&gt;
&lt;span class="s"&gt;PubkeyAuthentication yes&lt;/span&gt;
&lt;span class="s"&gt;KbdInteractiveAuthentication no&lt;/span&gt;
&lt;span class="s"&gt;PermitEmptyPasswords no&lt;/span&gt;
&lt;span class="s"&gt;MaxAuthTries &lt;/span&gt;&lt;span class="m"&gt;3&lt;/span&gt;
&lt;span class="s"&gt;MaxSessions &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;
&lt;span class="s"&gt;LoginGraceTime &lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;
&lt;span class="s"&gt;AllowUsers deploy&lt;/span&gt;

&lt;span class="c1"&gt;# Modern crypto only — drop legacy KEX/ciphers/MACs&lt;/span&gt;
&lt;span class="s"&gt;KexAlgorithms mlkem768x25519-sha256,curve25519-sha256,[email protected]&lt;/span&gt;
&lt;span class="s"&gt;HostKeyAlgorithms ssh-ed25519,ecdsa-sha2-nistp256&lt;/span&gt;
&lt;span class="s"&gt;Ciphers [email protected],[email protected],[email protected]&lt;/span&gt;
&lt;span class="s"&gt;MACs [email protected],[email protected]&lt;/span&gt;

&lt;span class="c1"&gt;# Reduce attack surface&lt;/span&gt;
&lt;span class="s"&gt;X11Forwarding no&lt;/span&gt;
&lt;span class="s"&gt;AllowAgentForwarding no&lt;/span&gt;
&lt;span class="s"&gt;AllowTcpForwarding no&lt;/span&gt;
&lt;span class="s"&gt;ClientAliveInterval &lt;/span&gt;&lt;span class="m"&gt;300&lt;/span&gt;
&lt;span class="s"&gt;ClientAliveCountMax &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;mlkem768x25519-sha256&lt;/code&gt; is a hybrid classical/post-quantum key exchange (ML-KEM-768, FIPS 203) and is the default negotiated exchange on OpenSSH 9.9+. It protects against "harvest now, decrypt later" interception. If anything in your fleet runs older OpenSSH, drop that entry and lead with &lt;code&gt;curve25519-sha256&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Restart safely: &lt;code&gt;sudo systemctl reload sshd&lt;/code&gt; (reload, not restart, keeps existing sessions alive if the config is bad).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Run &lt;code&gt;sshd -T | less&lt;/code&gt; to see the &lt;em&gt;effective&lt;/em&gt; config, and audit it externally with &lt;code&gt;ssh-audit your-server-ip&lt;/code&gt; — it flags any weak KEX, cipher, or MAC still active.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Changing the port off 22 doesn't stop a targeted attacker, but it silences almost all automated scanner noise in your logs. Optional, not a substitute for the above.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Firewall: Default Deny
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;ufw
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default deny incoming
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default allow outgoing
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow OpenSSH        &lt;span class="c"&gt;# or: sudo ufw allow 2222/tcp if you changed the port&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable
sudo &lt;/span&gt;ufw status verbose

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;UFW (and &lt;code&gt;firewalld&lt;/code&gt; on RHEL-based distros) both drive &lt;code&gt;nftables&lt;/code&gt; under the hood now — the old &lt;code&gt;iptables&lt;/code&gt; chains are a legacy compatibility layer, not your primary interface anymore. For anything beyond simple port rules, &lt;code&gt;nft&lt;/code&gt; directly gives finer control, but UFW covers the vast majority of single-VPS setups.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Automatic Security Patching
&lt;/h3&gt;

&lt;p&gt;The gap between a CVE disclosure and your patch is where breaches happen — the 2024 OpenSSH "regreSSHion" RCE (CVE-2024-6387) was weaponized within days. Close that gap automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;unattended-upgrades
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg-reconfigure &lt;span class="nt"&gt;--priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;low unattended-upgrades

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;/etc/apt/apt.conf.d/50unattended-upgrades&lt;/code&gt;, stick to the security pocket, blacklist anything you'd rather patch by hand, and schedule reboots for a known-quiet window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Unattended-Upgrade::Allowed-Origins &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;distro_id&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;distro_codename&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-security"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
Unattended-Upgrade::Package-Blacklist &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"postgresql-*"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="s2"&gt;"linux-image-*"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
Unattended-Upgrade::Automatic-Reboot &lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
Unattended-Upgrade::Automatic-Reboot-Time &lt;span class="s2"&gt;"04:00"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
Unattended-Upgrade::Mail &lt;span class="s2"&gt;"you@yourdomain.com"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
Unattended-Upgrade::MailReport &lt;span class="s2"&gt;"on-change"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm the &lt;code&gt;-security&lt;/code&gt; pocket is active in &lt;code&gt;/etc/apt/apt.conf.d/20auto-upgrades&lt;/code&gt;, and dry-run after any edit: &lt;code&gt;sudo unattended-upgrade --dry-run -v&lt;/code&gt;. For zero-downtime kernel patching, Canonical's Livepatch service is free for up to 5 machines.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Backups — Set Up Before You Need Them
&lt;/h3&gt;

&lt;p&gt;Hardening reduces the odds of compromise; it doesn't make you immune, and it does nothing for hardware failure or a bad &lt;code&gt;rm -rf&lt;/code&gt;. Follow the 3-2-1 rule: 3 copies of your data, on 2 different media, with 1 off-site.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# restic example — encrypted, deduplicated, incremental&lt;/span&gt;
restic init &lt;span class="nt"&gt;--repo&lt;/span&gt; s3:s3.amazonaws.com/your-backup-bucket
restic backup /etc /home /var/www &lt;span class="nt"&gt;--repo&lt;/span&gt; s3:s3.amazonaws.com/your-backup-bucket

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Automate it with a systemd timer or cron, and put a &lt;strong&gt;restore drill&lt;/strong&gt; on your calendar now — a backup you haven't test-restored isn't a backup.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tier 2 — Within the First Week
&lt;/h2&gt;

&lt;h3&gt;
  
  
  6. Intrusion Banning: Fail2Ban (+ CrowdSec if you're scaling)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Fail2Ban&lt;/strong&gt; — simple, ~15 MB RAM, the right default for one server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;fail2ban
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Edit &lt;code&gt;/etc/fail2ban/jail.local&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[DEFAULT]&lt;/span&gt;
&lt;span class="py"&gt;bantime&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;h&lt;/span&gt;
&lt;span class="py"&gt;findtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="err"&gt;m&lt;/span&gt;
&lt;span class="py"&gt;maxretry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;backend&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="err"&gt;systemd&lt;/span&gt;
&lt;span class="py"&gt;banaction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="err"&gt;nftables-multiport&lt;/span&gt;

&lt;span class="nn"&gt;[sshd]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;port&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2222&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; fail2ban
&lt;span class="nb"&gt;sudo &lt;/span&gt;fail2ban-client status sshd

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;CrowdSec&lt;/strong&gt; is worth adding once you're running more than one box, or want protection against attackers before they've hit &lt;em&gt;your&lt;/em&gt; logs — it shares crowd-sourced IP reputation across its network via lightweight "bouncers" that write directly into &lt;code&gt;nftables&lt;/code&gt; sets. Common pattern: Fail2Ban for SSH, CrowdSec for the web-facing layer (its Nginx/Caddy bouncer integrations are solid). They coexist fine as long as they're not both managing the exact same log source.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Kernel Network Hardening
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;/etc/sysctl.d/99-hardening.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ipv4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tcp_syncookies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ipv4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accept_source_route&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ipv4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accept_redirects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ipv4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send_redirects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ipv4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rp_filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ipv4&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;icmp_echo_ignore_broadcasts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ipv6&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accept_redirects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;kernel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dmesg_restrict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;sysctl &lt;span class="nt"&gt;--system&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  8. File Permissions &amp;amp; Mandatory Access Control
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Sane default umask for new files&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"umask 027"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/profile.d/umask.sh

&lt;span class="c"&gt;# Find unexpected SUID/SGID binaries — investigate anything you don't recognize&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;find / &lt;span class="nt"&gt;-xdev&lt;/span&gt; &lt;span class="nt"&gt;-perm&lt;/span&gt; &lt;span class="nt"&gt;-4000&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nt"&gt;-perm&lt;/span&gt; &lt;span class="nt"&gt;-2000&lt;/span&gt; 2&amp;gt;/dev/null

&lt;span class="c"&gt;# Confirm AppArmor is enforcing (Ubuntu/Debian default MAC)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;aa-status

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On RHEL-based systems, use &lt;code&gt;sestatus&lt;/code&gt; and keep SELinux in &lt;code&gt;Enforcing&lt;/code&gt; mode rather than switching to &lt;code&gt;Permissive&lt;/code&gt; to make an install easier — fix the policy instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Full-Disk Encryption (LUKS)
&lt;/h3&gt;

&lt;p&gt;Protects data at rest if a drive is stolen, misconfigured, or decommissioned by a third party — relevant on any VPS since the underlying storage is managed by your provider, not you. It's best enabled during the OS install; encrypting an existing root filesystem in place is disruptive and rarely worth it. For existing servers, focus on encrypting a dedicated data or backup partition instead — and note that &lt;code&gt;restic&lt;/code&gt;/&lt;code&gt;borgbackup&lt;/code&gt; already encrypt archives at rest, so your Tier 1 backups are covered either way.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Database Hardening
&lt;/h3&gt;

&lt;p&gt;If you're running MySQL/MariaDB or PostgreSQL on the same box, don't expose it publicly and don't use superuser accounts for application connections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# MySQL/MariaDB — /etc/mysql/mariadb.conf.d/50-server.cnf&lt;/span&gt;
&lt;span class="nn"&gt;[mysqld]&lt;/span&gt;
&lt;span class="py"&gt;bind-address&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;127.0&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# PostgreSQL — postgresql.conf&lt;/span&gt;
&lt;span class="py"&gt;listen_addresses&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'localhost'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Least-privilege app user, MySQL example&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="s1"&gt;'app_user'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="s1"&gt;'strong-random-password'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;app_db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="s1"&gt;'app_user'&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;FLUSH&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;mysql_secure_installation&lt;/code&gt; (or the PostgreSQL equivalent of removing default/trust auth) to kill anonymous accounts and remote root. If a second server genuinely needs DB access, restrict it by source IP in your firewall rather than binding to &lt;code&gt;0.0.0.0&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tier 3 — If It Applies to Your Stack
&lt;/h2&gt;

&lt;h3&gt;
  
  
  11. TLS: Get an A+ Without Guessing
&lt;/h3&gt;

&lt;p&gt;If you're terminating TLS on the box (Nginx, Caddy, Apache), use Let's Encrypt with auto-renewal and Mozilla's current &lt;strong&gt;Intermediate&lt;/strong&gt; profile — TLS 1.2 + 1.3 only, no legacy DHE ciphers, every suite forward-secret and AEAD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;certbot python3-certbot-nginx
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; yourdomain.com &lt;span class="nt"&gt;-d&lt;/span&gt; www.yourdomain.com
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl list-timers | &lt;span class="nb"&gt;grep &lt;/span&gt;certbot   &lt;span class="c"&gt;# confirm the renewal timer is active&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;ssl_protocols&lt;/span&gt; &lt;span class="s"&gt;TLSv1.2&lt;/span&gt; &lt;span class="s"&gt;TLSv1.3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_ciphers&lt;/span&gt; &lt;span class="s"&gt;ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_prefer_server_ciphers&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_session_timeout&lt;/span&gt; &lt;span class="s"&gt;1d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_session_cache&lt;/span&gt; &lt;span class="s"&gt;shared:MozSSL:10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_session_tickets&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_stapling&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ssl_stapling_verify&lt;/span&gt; &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Strict-Transport-Security&lt;/span&gt; &lt;span class="s"&gt;"max-age=63072000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;includeSubDomains"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;nosniff&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;SAMEORIGIN&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;server_tokens&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Basic request-rate protection against scraping/brute force at the app layer&lt;/span&gt;
&lt;span class="k"&gt;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=basic:10m&lt;/span&gt; &lt;span class="s"&gt;rate=10r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=basic&lt;/span&gt; &lt;span class="s"&gt;burst=20&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ssl_prefer_server_ciphers off&lt;/code&gt; is intentional, not an oversight — every cipher in the Intermediate list is already strong, so letting the client pick lets mobile devices without AES hardware acceleration negotiate ChaCha20 instead. Only add the HSTS &lt;code&gt;preload&lt;/code&gt; flag if every subdomain will always serve HTTPS — removal from browser preload lists takes months.&lt;/p&gt;

&lt;p&gt;One 2026-specific note: public CA certificate lifetimes are shortening (down to 200 days as of March 2026, and heading lower over the next few years), so make sure renewal is genuinely automated rather than a calendar reminder — verify with &lt;code&gt;sudo certbot renew --dry-run&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  12. SSH at Scale: Certificates and TOTP 2FA
&lt;/h3&gt;

&lt;p&gt;A single-admin box is fine with one hardware or software key. Once more than one or two people need access, static keys in &lt;code&gt;authorized_keys&lt;/code&gt; become an operational liability — nobody remembers to remove them when someone leaves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SSH certificates&lt;/strong&gt; replace that with short-lived, centrally-issued credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# One-time: create a CA keypair, keep the private half offline&lt;/span&gt;
ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-f&lt;/span&gt; ssh-user-ca &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"internal CA"&lt;/span&gt;

&lt;span class="c"&gt;# Sign a user's key for 24 hours, scoped to a principal&lt;/span&gt;
ssh-keygen &lt;span class="nt"&gt;-s&lt;/span&gt; ssh-user-ca &lt;span class="nt"&gt;-I&lt;/span&gt; alice-daily &lt;span class="nt"&gt;-n&lt;/span&gt; alice,ops &lt;span class="nt"&gt;-V&lt;/span&gt; +24h alice_id_ed25519.pub

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="cp"&gt;# /etc/ssh/sshd_config&lt;/span&gt;
&lt;span class="kt"&gt;TrustedUserCAKeys&lt;/span&gt; &lt;span class="sr"&gt;/etc/ssh/ca/&lt;/span&gt;&lt;span class="n"&gt;trusted_user_ca&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pub&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Revoking access is then just "stop issuing certificates for that principal" instead of hunting down every server's &lt;code&gt;authorized_keys&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TOTP 2FA&lt;/strong&gt; is the lower-cost option if hardware keys aren't practical for your team:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;libpam-google-authenticator
google-authenticator   &lt;span class="c"&gt;# run as the user; save the printed backup codes&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;etc&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;pam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;sshd&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;replace&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;@include&lt;/span&gt; &lt;span class="n"&gt;common&lt;/span&gt;&lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;
&lt;span class="n"&gt;auth&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="n"&gt;pam_google_authenticator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;so&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# sshd_config&lt;/span&gt;
KbdInteractiveAuthentication &lt;span class="nb"&gt;yes
&lt;/span&gt;AuthenticationMethods publickey,keyboard-interactive

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This forces both a valid key &lt;em&gt;and&lt;/em&gt; a TOTP code. It's not as phishing-resistant as FIDO2, but it's a meaningful step up from key-only auth for teams that don't want to distribute hardware tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Going portless entirely&lt;/strong&gt; is worth a look once you're managing more than a couple of servers: Tailscale SSH authenticates sessions over an already-authenticated WireGuard mesh with no inbound port required at all, and AWS SSM Session Manager / GCP IAP TCP forwarding offer the same idea gated by cloud IAM instead of network exposure.&lt;/p&gt;

&lt;h3&gt;
  
  
  13. Docker Container Security
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Run containers as a non-root user (&lt;code&gt;USER&lt;/code&gt; directive in the Dockerfile), never &lt;code&gt;--privileged&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Don't mount &lt;code&gt;/var/run/docker.sock&lt;/code&gt; into a container unless that container's entire job is orchestration — it's root-equivalent access to the host.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set &lt;code&gt;read_only: true&lt;/code&gt; and mount an explicit &lt;code&gt;tmpfs&lt;/code&gt; for anything that needs to write.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;In &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;, disable inter-container communication you don't need and consider &lt;code&gt;userns-remap&lt;/code&gt; so container root maps to an unprivileged host UID:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"icc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userns-remap"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"no-new-privileges"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep base images current; rebuild on a schedule so you're not shipping stale CVEs, and scan with &lt;code&gt;docker scout&lt;/code&gt; or Trivy before deploying.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fail2Ban doesn't play well with Docker's own iptables/nftables management — most people run it on the host watching host logs rather than trying to containerize it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  14. Systemd Sandboxing for Non-Containerized Services
&lt;/h3&gt;

&lt;p&gt;If an app runs directly as a systemd unit rather than in a container, you can get most of the isolation Docker would give you for free:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;ProtectSystem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;strict&lt;/span&gt;
&lt;span class="py"&gt;ProtectHome&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;PrivateTmp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;ReadWritePaths&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;/var/lib/myapp&lt;/span&gt;
&lt;span class="py"&gt;MemoryMax&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;768&lt;/span&gt;&lt;span class="err"&gt;M&lt;/span&gt;
&lt;span class="py"&gt;CapabilityBoundingSet&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This limits blast radius — a compromised or buggy service can't read arbitrary paths, escalate privileges, or take down the host by exhausting memory. Check what a unit is actually doing with &lt;code&gt;systemd-analyze security &amp;lt;unit&amp;gt;&lt;/code&gt;, which scores its exposure and flags missing sandboxing directives.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tier 4 — Ongoing, Forever
&lt;/h2&gt;

&lt;h3&gt;
  
  
  15. Logging, Auditing, and Rootkit Checks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;auditd lynis rkhunter

&lt;span class="c"&gt;# Watch the files attackers actually touch&lt;/span&gt;
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/audit/rules.d/audit.rules &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
-w /etc/passwd -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k sudoers
-w /etc/ssh/sshd_config -p wa -k sshd_config
-w /root/.ssh/authorized_keys -p wa -k ssh_keys
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart auditd

&lt;span class="c"&gt;# Full posture audit&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;lynis audit system

&lt;span class="c"&gt;# Rootkit scan&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;rkhunter &lt;span class="nt"&gt;--check&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Antivirus isn't a meaningful control on a Linux server — proper hardening, patching, and log review outperform it. Skip it and spend the time here instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  16. Maintenance Cadence
&lt;/h3&gt;

&lt;p&gt;Configuration drifts. Put this on a recurring calendar block:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Weekly&lt;/strong&gt; — review Fail2Ban/CrowdSec ban logs, confirm unattended-upgrades ran cleanly, check &lt;code&gt;df -h&lt;/code&gt; and disk usage trends.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monthly&lt;/strong&gt; — audit user accounts and &lt;code&gt;sudoers&lt;/code&gt;, review &lt;code&gt;ss -tlnp&lt;/code&gt; for services listening that shouldn't be, run &lt;code&gt;lynis audit system&lt;/code&gt; and act on new warnings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quarterly&lt;/strong&gt; — rotate SSH keys/certificates and any long-lived credentials, re-run &lt;code&gt;ssh-audit&lt;/code&gt; and a TLS scan (SSL Labs or &lt;code&gt;testssl.sh&lt;/code&gt;), test a full backup restore.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Quick Checklist
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tier 1 — before production traffic:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Root login disabled, sudo user created, key-based SSH only&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;sshd_config&lt;/code&gt; hardened: modern KEX/ciphers, &lt;code&gt;MaxAuthTries 3&lt;/code&gt;, no forwarding&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Firewall default-deny, only required ports open&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;unattended-upgrades&lt;/code&gt; installed, security pocket only, tested with &lt;code&gt;--dry-run&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Automated, encrypted, off-site backups — restore-tested&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tier 2 — first week:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Fail2Ban (or CrowdSec) active and tested&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Kernel sysctl hardening applied&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AppArmor/SELinux enforcing, sane umask, SUID audit done&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Full-disk or backup-archive encryption confirmed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Database bound to localhost, least-privilege app user&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tier 3 — if applicable:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;TLS on Mozilla Intermediate profile, auto-renewing certs, HSTS enabled&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SSH certificates or TOTP 2FA in place for multi-admin access&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Docker containers non-root, no exposed socket, images kept current&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;systemd sandboxing applied to bare-metal services&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tier 4 — recurring:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;auditd watching critical files, Lynis/rkhunter scans scheduled&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Weekly/monthly/quarterly maintenance cadence on the calendar&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run Tier 1 on every new server before it touches production traffic. The rest isn't optional so much as scheduled — security is a habit you keep paying into, not a setup you finish.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/securing-a-vps-in-2026-a-no-nonsense-hardening-guide-yicx3" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>vpssecurity</category>
      <category>selfhosting</category>
      <category>linuxhardening</category>
    </item>
    <item>
      <title>I Built a Tiny AI Agent From Scratch — Every Line Tested Before It Touched a Real API</title>
      <dc:creator>Pushpum Vats</dc:creator>
      <pubDate>Sat, 22 Aug 2026 06:30:27 +0000</pubDate>
      <link>https://dev.to/pushpum_vats/i-built-a-tiny-ai-agent-from-scratch-every-line-tested-before-it-touched-a-real-api-2dg9</link>
      <guid>https://dev.to/pushpum_vats/i-built-a-tiny-ai-agent-from-scratch-every-line-tested-before-it-touched-a-real-api-2dg9</guid>
      <description>&lt;h2&gt;
  
  
  What an "agent" actually is, stripped of the hype
&lt;/h2&gt;

&lt;p&gt;Every few weeks there's a new framework promising to make "agentic AI" easy. Most of them are wrappers around one core idea: the model doesn't just generate text — it can pause, say "I need to call this function with these arguments," wait for the result, and then keep going with that information in hand.&lt;/p&gt;

&lt;p&gt;That's it. That's the whole trick. Anthropic calls this &lt;strong&gt;tool use&lt;/strong&gt;, and it's the same mechanism powering everything from "let Claude check the weather" to multi-step coding agents.&lt;/p&gt;

&lt;p&gt;This tutorial builds a working version of that loop from the ground up — no LangChain, no agent framework, just the Claude API and plain Python. By the end you'll have a small agent that can do arithmetic and count words by actually calling real Python functions, decide on its own when to use them, and chain them together when a question needs both.&lt;/p&gt;




&lt;h2&gt;
  
  
  What you'll need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Python 3.9 or newer&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An Anthropic API key (from the &lt;a href="https://console.anthropic.com" rel="noopener noreferrer"&gt;Claude Console&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The official SDK: &lt;code&gt;pip install anthropic&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole list. No vector databases, no Docker, nothing else.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Write the actual tools (just functions)
&lt;/h2&gt;

&lt;p&gt;This is the part people often overcomplicate. A "tool" is just a regular Python function, plus a small JSON description telling Claude what it does and what arguments it takes.&lt;/p&gt;

&lt;p&gt;We'll build two: a calculator and a word counter. Save this as &lt;code&gt;tools.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
The actual Python functions our agent can call, plus the JSON-schema
descriptions of those tools that we hand to the Claude API.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt;

&lt;span class="n"&gt;_OPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mul&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Div&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;truediv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;USub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Safely evaluate a basic arithmetic expression like &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;12 * (3 + 4)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Constant&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BinOp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_OPS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_OPS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;)](&lt;/span&gt;&lt;span class="nf"&gt;_eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;_eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UnaryOp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_OPS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_OPS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;)](&lt;/span&gt;&lt;span class="nf"&gt;_eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;operand&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unsupported expression: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;expression&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;tree&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ast&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eval&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tree&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;count_words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Count the words in a piece of text.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;

&lt;span class="n"&gt;TOOLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calculate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Evaluate a basic arithmetic expression and return the numeric &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result as a string. Supports +, -, *, /, **, parentheses, and &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;negative numbers. Use this any time the user asks for a &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calculation, even a simple one -- do not do math in your head.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expression&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A valid arithmetic expression, e.g. &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;127 * 38&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; or &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(12 + 4) / 2&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expression&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count_words&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Count how many words are in a given piece of text and return &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;the count as a string. Use this when the user asks for a word &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count of something rather than estimating it yourself.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The text to count words in.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;TOOL_FUNCTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calculate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count_words&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;count_words&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A couple of deliberate choices worth calling out. The &lt;code&gt;calculate&lt;/code&gt; function uses Python's &lt;code&gt;ast&lt;/code&gt; module to parse the expression into a syntax tree and walk it manually, rather than calling &lt;code&gt;eval()&lt;/code&gt; directly — &lt;code&gt;eval("import os; os.system(...)")&lt;/code&gt; is exactly the kind of thing you don't want an AI-controlled function anywhere near, even though &lt;code&gt;ast.parse(mode="eval")&lt;/code&gt; would itself reject statements like &lt;code&gt;import&lt;/code&gt;. The description fields are also longer than feels natural at first. That's intentional — Claude's tool selection quality depends heavily on how clearly each tool explains what it does and when to use it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: The agent loop
&lt;/h2&gt;

&lt;p&gt;This is the part that actually makes it "agentic." Save this as &lt;code&gt;agent.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
The agent loop: send a message, check whether Claude wants to use a tool,
run that tool locally, send the result back, and repeat until Claude
gives a final text answer.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TOOL_FUNCTIONS&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_iterations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_message&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_iterations&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;tool_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  [Claude says]: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TOOL_FUNCTIONS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  [tool call]: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;tool_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown tool: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;})&lt;/span&gt;
                    &lt;span class="k"&gt;continue&lt;/span&gt;

                &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  [tool result]: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;tool_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;})&lt;/span&gt;
                &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;tool_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_results&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reached max_iterations without a final answer -- something is looping.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things in here are easy to get wrong, and the API will reject your request with a &lt;code&gt;400&lt;/code&gt; error if you do:&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;tool_result&lt;/code&gt; blocks have to go in a &lt;strong&gt;new&lt;/strong&gt; &lt;code&gt;user&lt;/code&gt; &lt;strong&gt;message&lt;/strong&gt;, not appended to the assistant's message. The &lt;code&gt;tool_result&lt;/code&gt; blocks must come &lt;strong&gt;first&lt;/strong&gt; in that message's content array — any text from your side has to come after them. And every &lt;code&gt;tool_use&lt;/code&gt; block in the assistant's response needs a matching &lt;code&gt;tool_result&lt;/code&gt; with the same &lt;code&gt;tool_use_id&lt;/code&gt;, including when a tool errors out — which is why even the error case still appends a &lt;code&gt;tool_result&lt;/code&gt;, just with &lt;code&gt;is_error: true&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Test the loop &lt;em&gt;before&lt;/em&gt; it ever calls the real API
&lt;/h2&gt;

&lt;p&gt;Here's the part most tutorials skip, and it's the most useful part for actually trusting your code. The Claude API's tool-use responses have a documented, predictable shape — a &lt;code&gt;stop_reason&lt;/code&gt;, and a &lt;code&gt;content&lt;/code&gt; list of blocks that are either &lt;code&gt;text&lt;/code&gt; or &lt;code&gt;tool_use&lt;/code&gt;. So we can fake that shape, feed it to &lt;code&gt;run_agent&lt;/code&gt;, and verify the loop, the tool dispatch, and the actual math/word-counting logic all work — without an API key, without spending a token.&lt;/p&gt;

&lt;p&gt;Save this as &lt;code&gt;test_agent_offline.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SimpleNamespace&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;run_agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count_words&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;SimpleNamespace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FakeMessages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FakeClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;script&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_parallel_tool_calls&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;turn1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SimpleNamespace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;stop_reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="nf"&gt;block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ll do both of those.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nf"&gt;block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;toolu_010&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calculate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expression&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(12 + 4) / 2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
            &lt;span class="nf"&gt;block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;toolu_011&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count_words&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;the quick brown fox jumps over the lazy dog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;turn2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SimpleNamespace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;stop_reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end_turn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(12 + 4) / 2 is 8.0, and that sentence has 9 words.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeClient&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;turn1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;turn2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Two things for you...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9 words&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_parallel_tool_calls passed&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_underlying_functions_directly&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127 * 38&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4826&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(12 + 4) / 2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-3 + 7 ** 2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;46&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;count_words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;the quick brown fox jumps over the lazy dog&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import os&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;should have raised&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;SyntaxError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test_underlying_functions_directly passed&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;test_underlying_functions_directly&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;test_parallel_tool_calls&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;All offline tests passed.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running this with &lt;code&gt;python3 test_agent_offline.py&lt;/code&gt; produces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;test_underlying_functions_directly&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt;

  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Claude&lt;/span&gt; &lt;span class="n"&gt;says&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="nv"&gt;'ll&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="n"&gt;both&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;those&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nv"&gt;'expression&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="mf"&gt;8.0&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nf"&gt;count_words&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nv"&gt;'text&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;'the&lt;/span&gt; &lt;span class="n"&gt;quick&lt;/span&gt; &lt;span class="n"&gt;brown&lt;/span&gt; &lt;span class="n"&gt;fox&lt;/span&gt; &lt;span class="n"&gt;jumps&lt;/span&gt; &lt;span class="n"&gt;over&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;lazy&lt;/span&gt; &lt;span class="n"&gt;dog&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;
&lt;span class="n"&gt;test_parallel_tool_calls&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt;

&lt;span class="n"&gt;All&lt;/span&gt; &lt;span class="n"&gt;offline&lt;/span&gt; &lt;span class="n"&gt;tests&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That output is from actually running the code above — not a transcript I wrote by hand. It confirms three things at once: the calculator handles operator precedence and negative numbers correctly, the agent loop correctly processes &lt;strong&gt;multiple tool calls in a single turn&lt;/strong&gt; (Claude often does both calculations in parallel rather than one at a time), and the message history gets built in the shape the real API expects.&lt;/p&gt;

&lt;p&gt;If you change anything — add a tool, change a schema, rewrite the loop — rerun this file first. It catches the majority of "why did my agent just 400" problems in seconds, with zero API cost.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Run it for real
&lt;/h2&gt;

&lt;p&gt;Once the offline tests pass, swap in the real client. Save this as &lt;code&gt;run.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Run with a real API key:

    export ANTHROPIC_API_KEY=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk-ant-...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
    pip install anthropic
    python3 run.py
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Anthropic&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;run_agent&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# reads ANTHROPIC_API_KEY from the environment
&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s 127 * 38, and how many words are in the sentence &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="s"&gt;the quick brown fox jumps over the lazy dog&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Final answer:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set your API key as an environment variable, install the SDK, and run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk-ant-your-key-here"&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;anthropic
python3 run.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the offline test already exercised the exact same &lt;code&gt;run_agent&lt;/code&gt; function with a response shaped the way the real API responds, what you're really testing here is just "does my API key work and does the real model behave the way the documented shape says it will" — which is a much smaller, cheaper thing to debug.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's actually happening, step by step
&lt;/h2&gt;

&lt;p&gt;For the question above, here's the real sequence:&lt;/p&gt;

&lt;p&gt;Claude receives the question along with the two tool definitions. It decides this needs both tools, and — because Claude 4-generation models default to parallel tool calling — it can return &lt;strong&gt;both&lt;/strong&gt; &lt;code&gt;tool_use&lt;/code&gt; blocks in a single response, often with a short sentence of context first ("I'll calculate that and count the words for you").&lt;/p&gt;

&lt;p&gt;Our loop sees &lt;code&gt;stop_reason == "tool_use"&lt;/code&gt;, runs &lt;code&gt;calculate("127 * 38")&lt;/code&gt; and &lt;code&gt;count_words(...)&lt;/code&gt; locally, and sends both results back in a single new &lt;code&gt;user&lt;/code&gt; message, with the &lt;code&gt;tool_result&lt;/code&gt; blocks first.&lt;/p&gt;

&lt;p&gt;Claude receives those results and, now having everything it needs, responds with &lt;code&gt;stop_reason == "end_turn"&lt;/code&gt; and a plain text answer. Our loop sees that and returns the text. Done — two API calls total, with real computation happening in real Python in between.&lt;/p&gt;




&lt;h2&gt;
  
  
  Things to watch out for as you extend this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pick the right model for the job.&lt;/strong&gt; Anthropic's own guidance is to use a larger model like Opus for tools with ambiguous inputs or many options, and a smaller model like Haiku for simple, well-defined tools — smaller models are more likely to guess at missing parameters rather than asking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't skip&lt;/strong&gt; &lt;code&gt;max_iterations&lt;/code&gt;&lt;strong&gt;.&lt;/strong&gt; If a tool's result regularly causes Claude to call the same tool again, you can end up in a loop. The cap in &lt;code&gt;run_agent&lt;/code&gt; is a blunt but effective safety net while you're developing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool descriptions are most of the work.&lt;/strong&gt; If Claude picks the wrong tool, or the right tool with weird arguments, the fix is almost always a clearer description — what the tool does, when to use it, when &lt;em&gt;not&lt;/em&gt; to, and what each parameter means — rather than a change to your loop logic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For anything beyond a toy, look at the SDK's tool runner.&lt;/strong&gt; Once you're comfortable with the manual loop above (and understand &lt;em&gt;why&lt;/em&gt; it's shaped the way it is), Anthropic's Python, TypeScript, and Ruby SDKs include a beta "tool runner" that handles the request/response cycle and conversation state for you. It's worth learning the manual version first — it's what the tool runner is doing under the hood, and it's much easier to debug when something goes wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  Related tutorials on this blog
&lt;/h2&gt;

&lt;p&gt;A couple of places to go from here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tutorials/getting-claude-to-talk-to-your-files-mcp-tutorial"&gt;Getting Claude to Actually Talk to Your Files: A Real-World MCP Setup Guide&lt;/a&gt; — the loop you just built by hand is conceptually what MCP standardizes; this shows the same idea via a config file instead of code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://dev.to/tutorials/run-local-ai-model-laptop-ollama-tutorial"&gt;Your Laptop Can Run Its Own AI Now — Here's How to Actually Do It&lt;/a&gt; — for experimenting with the agent loop above using a free local model instead of API calls while you're still debugging.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview" rel="noopener noreferrer"&gt;How tool use works — Claude API docs&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use" rel="noopener noreferrer"&gt;How to implement tool use — full reference&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.anthropic.com/engineering/writing-tools-for-agents" rel="noopener noreferrer"&gt;Writing tools for agents — Anthropic engineering blog&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/anthropics/anthropic-sdk-python" rel="noopener noreferrer"&gt;Anthropic Python SDK on GitHub&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/i-built-a-tiny-ai-agent-from-scratch-every-line-tested-before-it-touched-a-real-api-9zrvk" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>aiagents</category>
      <category>python</category>
      <category>ai</category>
      <category>agenticai</category>
    </item>
  </channel>
</rss>
