<?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: Naveen Gopalakrishna</title>
    <description>The latest articles on DEV Community by Naveen Gopalakrishna (@naveenneog).</description>
    <link>https://dev.to/naveenneog</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%2F4023553%2Fdba2483f-64d2-4c6d-8481-753abf1b2fe4.jpg</url>
      <title>DEV Community: Naveen Gopalakrishna</title>
      <link>https://dev.to/naveenneog</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/naveenneog"/>
    <language>en</language>
    <item>
      <title>Claude Desktop on Microsoft Foundry: Third-Party Inference Mode, Entra ID Sign-In, and What Claude Design Cannot Do</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Thu, 13 Aug 2026 14:04:57 +0000</pubDate>
      <link>https://dev.to/naveenneog/claude-desktop-on-microsoft-foundry-third-party-inference-mode-entra-id-sign-in-and-what-claude-33lo</link>
      <guid>https://dev.to/naveenneog/claude-desktop-on-microsoft-foundry-third-party-inference-mode-entra-id-sign-in-and-what-claude-33lo</guid>
      <description>&lt;p&gt;After getting &lt;a href="https://naveenneog.github.io/AI4Good/2026/08/13/claude-code-on-microsoft-foundry/" rel="noopener noreferrer"&gt;Claude Code onto Foundry&lt;/a&gt;&lt;br&gt;
and then &lt;a href="https://naveenneog.github.io/AI4Good/2026/08/13/claude-code-governance-apim-foundry/" rel="noopener noreferrer"&gt;governing it with an APIM gateway&lt;/a&gt;,&lt;br&gt;
the obvious next question is the GUI: &lt;strong&gt;can Claude Desktop use my own Foundry deployment too?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Short answer: &lt;strong&gt;yes&lt;/strong&gt;, through a documented third-party inference mode — and it can point at the&lt;br&gt;
same governed gateway. But it is an &lt;strong&gt;admin-deployed configuration&lt;/strong&gt;, not something a developer&lt;br&gt;
switches on, and one popular Claude feature is explicitly excluded.&lt;/p&gt;

&lt;p&gt;Verified on &lt;strong&gt;13 Aug 2026&lt;/strong&gt; against Claude Desktop &lt;strong&gt;1.25927.0&lt;/strong&gt; on Windows.&lt;/p&gt;


&lt;h2&gt;
  
  
  What "out of the box" actually gives you
&lt;/h2&gt;

&lt;p&gt;Install it and the first run is unambiguous:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Anthropic.Claude&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg73ktwyz4hs5trdl3tup.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fg73ktwyz4hs5trdl3tup.png" alt="Claude Desktop first run on Windows" width="800" height="799"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sign-in offers Google or email — an &lt;strong&gt;Anthropic account&lt;/strong&gt;. There is no "use my own endpoint" box,&lt;br&gt;
and no Azure sign-in:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuu39gndjalqw57lgkthi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuu39gndjalqw57lgkthi.png" alt="Claude Desktop sign-in offering Continue with Google or email, i.e. an Anthropic account" width="800" height="799"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the default, and it is why people assume Desktop cannot do BYO-model. The capability&lt;br&gt;
exists — it is just not reachable from the UI on a stock install.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 1 — Confirm your build supports third-party mode
&lt;/h2&gt;

&lt;p&gt;Third-party inference landed in Desktop &lt;strong&gt;1.2581.0&lt;/strong&gt;. Before configuring anything, check the&lt;br&gt;
shipping app actually carries the subsystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$asar&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class="s2"&gt;\AnthropicClaude\app-&amp;lt;version&amp;gt;\resources\app.asar"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;System.Text.Encoding&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;UTF8.GetString&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;IO.File&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;ReadAllBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$asar&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;@(&lt;/span&gt;&lt;span class="s1"&gt;'inferenceProvider'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'inferenceFoundryResource'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'inferenceGatewayOidc'&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="s2"&gt;"{0,-28} {1}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$k&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="n"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$text&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="n"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;]::&lt;/span&gt;&lt;span class="n"&gt;Escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Count&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;On 1.25927.0 this returns matches for all of them. The provider enum is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gateway | anthropic | bedrock | mantle | vertex | foundry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and the credential kinds are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;static | helper-script | interactive | vendor-profile | oauth | workforce
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;inferenceProvider&lt;/code&gt; is the master switch — the app's own description is &lt;em&gt;"Selects the inference&lt;br&gt;
backend. Setting this key activates third-party mode."&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 2 — Register an Entra app for Desktop
&lt;/h2&gt;

&lt;p&gt;Desktop signs the user in directly against Entra ID, so it needs its own app registration with&lt;br&gt;
delegated access to the Cognitive Services data plane.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--display-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Claude Desktop (Foundry)"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--is-fallback-public-client&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--public-client-redirect-uris&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ms-appx-web://microsoft.aad.brokerplugin/&amp;lt;client-id&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ConvertFrom-Json&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# delegated user_impersonation on Azure Cognitive Services&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;permission&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://cognitiveservices.azure.com&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--api-permissions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;user_impersonation-scope-id&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Scope&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;permission&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;admin-consent&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appId&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the redirect URI shape: &lt;code&gt;ms-appx-web://microsoft.aad.brokerplugin/&amp;lt;client-id&amp;gt;&lt;/code&gt; is what the&lt;br&gt;
&lt;strong&gt;broker&lt;/strong&gt; flow needs on Windows. Use broker if you have Conditional Access policies that require&lt;br&gt;
a compliant or managed device — it routes through Web Account Manager rather than a browser, so&lt;br&gt;
the device claim is satisfied.&lt;/p&gt;

&lt;p&gt;The admin-consent step needs a Privileged Role Administrator. If you are not one, this is where&lt;br&gt;
you stop and open a ticket — it was where I stopped in my own tenant.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 3 — Push the configuration
&lt;/h2&gt;

&lt;p&gt;This is the part that surprises people: &lt;strong&gt;3P settings do not live in&lt;br&gt;
&lt;code&gt;claude_desktop_config.json&lt;/code&gt;.&lt;/strong&gt; That file is for MCP servers and cosmetics. Inference&lt;br&gt;
configuration is managed configuration, deployed the way an enterprise deploys policy.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Managed location&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HKLM\SOFTWARE\Policies\Claude&lt;/code&gt; (machine) or &lt;code&gt;HKCU\SOFTWARE\Policies\Claude&lt;/code&gt; (user), values as &lt;code&gt;REG_SZ&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.mobileconfig&lt;/code&gt; via MDM, domain &lt;code&gt;com.anthropic.claudefordesktop&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/etc/claude-desktop/managed-settings.json&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The app keeps its own state separately under &lt;code&gt;%LOCALAPPDATA%\Claude-3p\configLibrary\&lt;/code&gt; — the&lt;br&gt;
&lt;code&gt;-3p&lt;/code&gt; suffix is literal, and its presence is a quick way to tell whether a machine has ever run&lt;br&gt;
in third-party mode.&lt;/p&gt;
&lt;h3&gt;
  
  
  Pointing straight at Foundry
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HKLM:\SOFTWARE\Policies\Claude"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-Item&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Out-Null&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;New-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inferenceProvider&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;-Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"foundry"&lt;/span&gt;&lt;span class="w"&gt;                    &lt;/span&gt;&lt;span class="nt"&gt;-PropertyType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inferenceFoundryResource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-foundry-resource&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nt"&gt;-PropertyType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inferenceFoundryTenantId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;tenant-id&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="nt"&gt;-PropertyType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inferenceFoundryClientId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;app-registration-client-id&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PropertyType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inferenceFoundryAuthFlow&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"broker"&lt;/span&gt;&lt;span class="w"&gt;                      &lt;/span&gt;&lt;span class="nt"&gt;-PropertyType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;New-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inferenceModels&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'[{"name":"claude-sonnet-5"},{"name":"claude-opus-5"}]'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PropertyType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;inferenceFoundryResource&lt;/code&gt; is the bare resource name — the app builds&lt;br&gt;
&lt;code&gt;&amp;lt;resource&amp;gt;.services.ai.azure.com&lt;/code&gt; from it, exactly like Claude Code's&lt;br&gt;
&lt;code&gt;ANTHROPIC_FOUNDRY_RESOURCE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;inferenceFoundryAuthFlow&lt;/code&gt; accepts &lt;code&gt;device-code&lt;/code&gt; (default), &lt;code&gt;browser&lt;/code&gt; or &lt;code&gt;broker&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Or point at your governed gateway
&lt;/h3&gt;

&lt;p&gt;If you already run the &lt;a href="https://naveenneog.github.io/AI4Good/2026/08/13/claude-code-governance-apim-foundry/" rel="noopener noreferrer"&gt;APIM governance gateway&lt;/a&gt;,&lt;br&gt;
Desktop can go through it and inherit the same per-developer budgets, tiering and chargeback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inferenceProvider          = gateway
inferenceGatewayBaseUrl    = https://&amp;lt;your-apim&amp;gt;.azure-api.net/claude
inferenceCredentialKind    = oauth
inferenceGatewayOidcAuthFlow = broker
inferenceGatewayOidc       = {"clientId":"&amp;lt;app-id&amp;gt;",
                              "issuer":"https://login.microsoftonline.com/&amp;lt;tenant&amp;gt;/v2.0",
                              "bearerTokenType":"access_token",
                              "scopes":"https://cognitiveservices.azure.com/.default"}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;inferenceGatewayAuthScheme&lt;/code&gt; defaults to &lt;code&gt;bearer&lt;/code&gt;, which is what the gateway's&lt;br&gt;
&lt;code&gt;validate-azure-ad-token&lt;/code&gt; policy expects. This is the option I would pick for a team: one&lt;br&gt;
enforcement point for both Claude Code and Claude Desktop.&lt;/p&gt;
&lt;h3&gt;
  
  
  If you cannot use registry policy
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;inferenceCredentialHelper&lt;/code&gt; points at an executable that prints a bearer token to stdout — the&lt;br&gt;
same pattern as the token helper in the Claude Code setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;New-ItemProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$k&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inferenceCredentialHelper&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-Value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:\ProgramData\claude\get-foundry-token.cmd"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PropertyType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The app sets &lt;code&gt;CLAUDE_HELPER_CONTEXT&lt;/code&gt; so the helper can tell an interactive start from a&lt;br&gt;
mid-session refresh. That makes Key Vault, Windows Hello or &lt;code&gt;az account get-access-token&lt;/code&gt; all&lt;br&gt;
viable sources.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 4 — Verify
&lt;/h2&gt;

&lt;p&gt;Once policy is applied and the app restarts, its diagnostics report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Effective mode:          3P
inferenceProvider set:   yes
Provider:                foundry
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it still says &lt;code&gt;1P&lt;/code&gt;, the policy was not read. On a corporate-managed device that is the most&lt;br&gt;
likely failure — see below.&lt;/p&gt;


&lt;h2&gt;
  
  
  The thing that will stop you on a managed laptop
&lt;/h2&gt;

&lt;p&gt;On my own machine, &lt;strong&gt;both&lt;/strong&gt; policy hives were locked by device management:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HKLM\SOFTWARE\Policies\Claude : Requested registry access is not allowed.
HKCU\SOFTWARE\Policies\Claude : Access to the registry key is denied.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is not a bug — it is the point. &lt;code&gt;SOFTWARE\Policies&lt;/code&gt; is reserved for administratively&lt;br&gt;
deployed policy, and a managed device refuses hand-editing of it. &lt;strong&gt;Third-party mode is designed&lt;br&gt;
to be pushed by Intune or Group Policy, not enabled by a developer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Practically: package the keys as an Intune configuration profile (or a &lt;code&gt;.mobileconfig&lt;/code&gt; on macOS)&lt;br&gt;
and target the same Entra group you use to entitle Claude Code. Entitlement and configuration&lt;br&gt;
then travel together.&lt;/p&gt;




&lt;h2&gt;
  
  
  Claude Design: the honest answer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Claude Design&lt;/strong&gt; — Anthropic's visual design surface, launched April 2026 — &lt;strong&gt;cannot&lt;/strong&gt; be pointed&lt;br&gt;
at Foundry. It is explicitly excluded from third-party mode: Chat, Cowork and Code are supported&lt;br&gt;
in 3P, and features that depend on Anthropic-hosted inference are not.&lt;/p&gt;

&lt;p&gt;That is a product boundary, not a configuration gap. Its design-system ingestion, rendering&lt;br&gt;
pipeline and Figma/Adobe integrations are Anthropic-side services, so there is nothing to&lt;br&gt;
re-point. If you need design generation on your own deployment, you call the Claude models in&lt;br&gt;
Foundry programmatically and build the surface yourself.&lt;/p&gt;

&lt;p&gt;Worth knowing before someone promises it in a rollout plan.&lt;/p&gt;




&lt;h2&gt;
  
  
  What runs where, once you are done
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Runs on your Foundry deployment?&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code CLI&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;code&gt;CLAUDE_CODE_USE_FOUNDRY=1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code VS Code extension&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;same settings file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Desktop — Chat&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;3P mode, admin-deployed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Desktop — Cowork / Code&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;3P mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Desktop — &lt;strong&gt;Design&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Anthropic-hosted only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;claude.ai web&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;Anthropic-hosted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The pattern underneath all three posts
&lt;/h2&gt;

&lt;p&gt;Anthropic has converged on one idea across the product line: &lt;strong&gt;the model backend is a deployment&lt;br&gt;
decision, not a product decision.&lt;/strong&gt; Claude Code exposes it as environment variables, Desktop&lt;br&gt;
exposes it as managed configuration, and both accept a gateway in the middle.&lt;/p&gt;

&lt;p&gt;For a platform team that means you can put a single governed endpoint in front of everything, and&lt;br&gt;
choose per surface whether it points at Foundry directly or through your gateway. The developer&lt;br&gt;
experience does not change either way — which is exactly what you want from governance.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;💻 &lt;strong&gt;Governance accelerator:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/claude-code-foundry-gateway" rel="noopener noreferrer"&gt;https://github.com/naveenneog/claude-code-foundry-gateway&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📘 &lt;strong&gt;Claude Code on Foundry:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/AI4Good/2026/08/13/claude-code-on-microsoft-foundry/" rel="noopener noreferrer"&gt;step-by-step&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📗 &lt;strong&gt;Governing it with APIM:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/AI4Good/2026/08/13/claude-code-governance-apim-foundry/" rel="noopener noreferrer"&gt;per-developer budgets and chargeback&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>azure</category>
      <category>ai</category>
      <category>devops</category>
      <category>windows</category>
    </item>
    <item>
      <title>Governing Claude Code with Azure API Management: Per-Developer Budgets, Tiering and Chargeback on Microsoft Foundry</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Thu, 13 Aug 2026 13:20:35 +0000</pubDate>
      <link>https://dev.to/naveenneog/governing-claude-code-with-azure-api-management-per-developer-budgets-tiering-and-chargeback-on-4gnf</link>
      <guid>https://dev.to/naveenneog/governing-claude-code-with-azure-api-management-per-developer-budgets-tiering-and-chargeback-on-4gnf</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F81vrx12g8nwgg0tvdpow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F81vrx12g8nwgg0tvdpow.png" alt="Architecture: developer's Entra ID token to an APIM AI gateway, which validates identity, applies tiered token budgets, emits chargeback metrics, then calls Microsoft Foundry with its own managed identity"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://naveenneog.github.io/AI4Good/2026/08/13/claude-code-on-microsoft-foundry/" rel="noopener noreferrer"&gt;Running Claude Code on Foundry&lt;/a&gt;&lt;br&gt;
solves the credential problem for one developer: &lt;code&gt;az login&lt;/code&gt; and you're in, no API key anywhere.&lt;/p&gt;

&lt;p&gt;Scaling that to a team surfaces a different problem. Going direct to Foundry means &lt;strong&gt;every&lt;br&gt;
developer needs &lt;code&gt;Cognitive Services User&lt;/code&gt; on the resource&lt;/strong&gt;, and that gives you no rate limit, no&lt;br&gt;
budget, no tiering, and usage data only at the resource level — you can see that $4,000 of tokens&lt;br&gt;
were spent, but not by whom.&lt;/p&gt;

&lt;p&gt;This post puts an &lt;strong&gt;Azure API Management AI gateway&lt;/strong&gt; in front. Developers end up holding no&lt;br&gt;
Foundry role at all.&lt;/p&gt;

&lt;p&gt;Everything below was built and executed on &lt;strong&gt;13 Aug 2026&lt;/strong&gt;: APIM &lt;strong&gt;Basic v2&lt;/strong&gt;, Claude Code&lt;br&gt;
&lt;strong&gt;v2.1.223&lt;/strong&gt;, &lt;code&gt;claude-sonnet-5&lt;/code&gt; and &lt;code&gt;claude-opus-5&lt;/code&gt; in East US 2.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Skip the manual steps:&lt;/strong&gt; the whole thing is packaged as a one-command accelerator —&lt;br&gt;
&lt;a href="https://github.com/naveenneog/claude-code-foundry-gateway" rel="noopener noreferrer"&gt;&lt;strong&gt;github.com/naveenneog/claude-code-foundry-gateway&lt;/strong&gt;&lt;/a&gt;.&lt;br&gt;
Steps 1–7 below are what it automates, and worth reading before you run it.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  What you get
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Who may use Claude Code&lt;/td&gt;
&lt;td&gt;Entra ID group membership&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;403&lt;/strong&gt; with an actionable message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiered budgets&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;llm-token-limit&lt;/code&gt; per tier&lt;/td&gt;
&lt;td&gt;standard vs premium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-developer rate limit&lt;/td&gt;
&lt;td&gt;tokens/minute keyed on &lt;code&gt;oid&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;429&lt;/strong&gt; + &lt;code&gt;Retry-After&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-developer daily budget&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;token-quota&lt;/code&gt; + period&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;403&lt;/strong&gt; until reset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Runaway-agent protection&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rate-limit-by-key&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;request ceiling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Chargeback&lt;/td&gt;
&lt;td&gt;&lt;code&gt;llm-emit-token-metric&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;tokens per named person&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No credential sprawl&lt;/td&gt;
&lt;td&gt;gateway managed identity&lt;/td&gt;
&lt;td&gt;nothing to leak or rotate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2yhg64sdzceaqvasjosx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2yhg64sdzceaqvasjosx.png" alt="Four governance checks passing: entitled developer served at standard tier, second identity served at premium tier, exhausted budget throttled with HTTP 429 and Retry-After, and chargeback attribution showing tokens per developer"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Why per-developer metering actually works here
&lt;/h2&gt;

&lt;p&gt;This is the load-bearing detail, and I established it by intercepting real traffic rather than&lt;br&gt;
assuming.&lt;/p&gt;

&lt;p&gt;Claude Code's Foundry mode does not invent a credential. It calls the Azure SDK's&lt;br&gt;
&lt;code&gt;DefaultAzureCredential&lt;/code&gt;, so the request carries the &lt;strong&gt;developer's own Entra ID token&lt;/strong&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;"aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://cognitiveservices.azure.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"oid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"43cc5304-..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"upn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"naveen.g@contoso.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"appid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"04b07795-8ddb-461a-bbee-02f9e1bf7b46"&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;Three consequences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identity is unforgeable.&lt;/strong&gt; &lt;code&gt;oid&lt;/code&gt; is signed by Entra. A developer cannot spoof a colleague,
and a shared credential cannot exist because there is no credential to share.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No client-side auth work.&lt;/strong&gt; No app registration, no custom audience, no device-code flow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offboarding is free.&lt;/strong&gt; Remove the person from the group and access stops. Nothing is revoked
at the endpoint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Claude Code also sends an &lt;code&gt;x-claude-code-session-id&lt;/code&gt; header, which makes a useful metric&lt;br&gt;
dimension.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 1 — Deploy the gateway
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The SKU decision matters more than anything else in this post.&lt;/strong&gt; APIM's &lt;code&gt;llm-*&lt;/code&gt; policies parse&lt;br&gt;
the &lt;strong&gt;Anthropic Messages API&lt;/strong&gt; shape &lt;strong&gt;only on v2 tiers&lt;/strong&gt;. On classic Developer/Basic/Standard/&lt;br&gt;
Premium the policies apply happily and count zero tokens forever — budgets never trip and you&lt;br&gt;
believe you are governed when you are not.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;az apim create&lt;/code&gt; has no v2 support, so deploy with ARM/Bicep:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;deployment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;rg-claude-gateway&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--template-file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;infra/main.bicep&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--parameters&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;foundryAccountName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;foundry&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;publisherEmail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;you&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;contoso.com&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Basic v2 provisions in about &lt;strong&gt;four minutes&lt;/strong&gt;; classic tiers take 30–45.&lt;/p&gt;

&lt;p&gt;The template enables a &lt;strong&gt;system-assigned managed identity&lt;/strong&gt; — the identity that will reach&lt;br&gt;
Foundry.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 2 — Let the gateway in, and nobody else
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$apimMi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;apim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;apim&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;rg&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;identity.principalId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsv&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cognitiveservices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;foundry&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;rg&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsv&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;assignment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--assignee-object-id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$apimMi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--assignee-principal-type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ServicePrincipal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cognitive Services User"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then the step that actually closes the bypass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;assignment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;delete&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--assignee&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;developer-oid&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cognitive Services User"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Until developers lose that role they can point Claude Code straight at Foundry and skip every&lt;br&gt;
control below. Leave it assigned only to the gateway.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 3 — Entitlement lives in Entra ID
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--display-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;claude-code-standard&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--mail-nickname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;claude-code-standard&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--display-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;claude-code-premium&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;--mail-nickname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;claude-code-premium&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Entitlement belongs in Entra because that is where joiner/mover/leaver already runs. Adding&lt;br&gt;
someone to &lt;code&gt;claude-code-premium&lt;/code&gt; is an action your identity team can take, audit and review —&lt;br&gt;
whereas an allowlist in a config file drifts and is nobody's job.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 4 — Import Foundry as an API
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;apim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;rg&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--service-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;apim&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--api-id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;claude-foundry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;claude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--service-url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;foundry&amp;gt;.services.ai.azure.com/anthropic"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--protocols&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--subscription-required&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;false&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;apim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;rg&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--service-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;apim&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--api-id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;claude-foundry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--operation-id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--display-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Create Message"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--url-template&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/v1/messages"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;--subscription-required false&lt;/code&gt; is deliberate: authorization comes from the Entra token, not an&lt;br&gt;
APIM subscription key. Claude Code has no reliable way to send a custom key header, and a shared&lt;br&gt;
key would destroy per-person attribution anyway.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 5 — The policy
&lt;/h2&gt;

&lt;p&gt;Limits live in named values so changing a budget is a config edit, not a redeployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;tpm-standard&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="n"&gt;quota-standard&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;tpm-premium&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;80000&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;quota-premium&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;calls-per-minute&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The policy itself, in order:&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="nt"&gt;&amp;lt;validate-azure-ad-token&lt;/span&gt; &lt;span class="na"&gt;tenant-id=&lt;/span&gt;&lt;span class="s"&gt;"{{tenant-id}}"&lt;/span&gt; &lt;span class="na"&gt;output-token-variable-name=&lt;/span&gt;&lt;span class="s"&gt;"jwt"&lt;/span&gt;
                         &lt;span class="na"&gt;failed-validation-httpcode=&lt;/span&gt;&lt;span class="s"&gt;"401"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;audiences&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;audience&amp;gt;&lt;/span&gt;https://cognitiveservices.azure.com&lt;span class="nt"&gt;&amp;lt;/audience&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/audiences&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/validate-azure-ad-token&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;set-variable&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"userId"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"@(((Jwt)context.Variables["&lt;/span&gt;&lt;span class="err"&gt;jwt"]).Claims.GetValueOrDefault("oid",""))"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;set-variable&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"tier"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"@{
    var oid = "&lt;/span&gt;&lt;span class="err"&gt;,"&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="err"&gt;(string)context.Variables["userId"]&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="err"&gt;",";&lt;/span&gt;
    &lt;span class="err"&gt;if&lt;/span&gt; &lt;span class="err"&gt;(("{{allow-premium}}").Contains(oid))&lt;/span&gt;  &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="err"&gt;return&lt;/span&gt; &lt;span class="err"&gt;"premium";&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;
    &lt;span class="err"&gt;if&lt;/span&gt; &lt;span class="err"&gt;(("{{allow-standard}}").Contains(oid))&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt; &lt;span class="err"&gt;return&lt;/span&gt; &lt;span class="err"&gt;"standard";&lt;/span&gt; &lt;span class="err"&gt;}&lt;/span&gt;
    &lt;span class="err"&gt;return&lt;/span&gt; &lt;span class="err"&gt;"denied";&lt;/span&gt;
&lt;span class="err"&gt;}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;llm-token-limit&lt;/span&gt; &lt;span class="na"&gt;counter-key=&lt;/span&gt;&lt;span class="s"&gt;"@((string)context.Variables["&lt;/span&gt;&lt;span class="err"&gt;userId"])"&lt;/span&gt;
                 &lt;span class="na"&gt;tokens-per-minute=&lt;/span&gt;&lt;span class="s"&gt;"{{tpm-standard}}"&lt;/span&gt; &lt;span class="na"&gt;estimate-prompt-tokens=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;
                 &lt;span class="na"&gt;retry-after-header-name=&lt;/span&gt;&lt;span class="s"&gt;"Retry-After"&lt;/span&gt;
                 &lt;span class="na"&gt;remaining-tokens-header-name=&lt;/span&gt;&lt;span class="s"&gt;"x-ratelimit-remaining-tokens"&lt;/span&gt;
                 &lt;span class="na"&gt;tokens-consumed-header-name=&lt;/span&gt;&lt;span class="s"&gt;"x-tokens-consumed"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;llm-emit-token-metric&lt;/span&gt; &lt;span class="na"&gt;namespace=&lt;/span&gt;&lt;span class="s"&gt;"claudecode"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dimension&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"User"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"@((string)context.Variables["&lt;/span&gt;&lt;span class="err"&gt;userUpn"])"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;dimension&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Tier"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"@((string)context.Variables["&lt;/span&gt;&lt;span class="err"&gt;tier"])"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/llm-emit-token-metric&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;authentication-managed-identity&lt;/span&gt; &lt;span class="na"&gt;resource=&lt;/span&gt;&lt;span class="s"&gt;"https://cognitiveservices.azure.com"&lt;/span&gt;
                                 &lt;span class="na"&gt;output-token-variable-name=&lt;/span&gt;&lt;span class="s"&gt;"msi-token"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;@("Bearer " + (string)context.Variables["msi-token"])&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four things worth knowing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The audience is &lt;code&gt;https://cognitiveservices.azure.com&lt;/code&gt;&lt;/strong&gt; — that is what Claude Code requests
a token for, so that is what the gateway must accept. No app registration required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;estimate-prompt-tokens="false"&lt;/code&gt;&lt;/strong&gt; bills actual usage from the response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;XML comments cannot contain &lt;code&gt;--&lt;/code&gt;.&lt;/strong&gt; APIM rejects the policy with an error that never
mentions comments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The developer's token is discarded&lt;/strong&gt;, not forwarded. Foundry sees only the gateway identity.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sentinel commas around each object id in the allowlists make &lt;code&gt;contains()&lt;/code&gt; exact, so one id cannot&lt;br&gt;
partially match another.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 6 — Sync group membership
&lt;/h2&gt;

&lt;p&gt;The gateway cannot read group membership from the caller's token: the Cognitive Services audience&lt;br&gt;
is a first-party Microsoft resource whose token has no configurable &lt;code&gt;groups&lt;/code&gt; claim. Two options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Sync&lt;/strong&gt; (default)&lt;/td&gt;
&lt;td&gt;a script writes object ids into named values&lt;/td&gt;
&lt;td&gt;no tenant admin needed; membership lags by one sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Live Graph lookup&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;policy calls Graph per request, cached&lt;/td&gt;
&lt;td&gt;no lag; needs admin consent for &lt;code&gt;GroupMember.Read.All&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I hit &lt;code&gt;Authorization_RequestDenied&lt;/code&gt; on the Graph grant — it needs a Privileged Role Administrator&lt;br&gt;
— so the accelerator ships the sync approach and documents the Graph upgrade.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;/scripts/Sync-ClaudeAccess.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ApimName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;apim&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;rg&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Service principals in a group are invisible to a delegated token without &lt;code&gt;Application.Read.All&lt;/code&gt;,&lt;br&gt;
so CI identities are passed explicitly with &lt;code&gt;-AdditionalPremiumOids&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 7 — Chargeback telemetry
&lt;/h2&gt;

&lt;p&gt;Two settings, both of which fail &lt;em&gt;silently&lt;/em&gt; if you miss them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the APIM diagnostic needs &lt;strong&gt;&lt;code&gt;metrics: true&lt;/code&gt;&lt;/strong&gt;, or &lt;code&gt;llm-emit-token-metric&lt;/code&gt; emits nothing and the
metric namespace never appears&lt;/li&gt;
&lt;li&gt;Application Insights needs &lt;strong&gt;&lt;code&gt;CustomMetricsOptedInType: WithDimensions&lt;/code&gt;&lt;/strong&gt;, or the metric arrives
as a bare total with no &lt;code&gt;User&lt;/code&gt; breakdown — exactly the part chargeback needs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;naveen.g@contoso.com      831 tokens
service-principal         728 tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The Azure CLI's &lt;code&gt;az monitor metrics list&lt;/code&gt; drops &lt;code&gt;--namespace&lt;/code&gt; for custom namespaces and reports&lt;br&gt;
"metric not found". Query the REST API instead.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 8 — Point Claude Code at the gateway
&lt;/h2&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;"env"&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;"CLAUDE_CODE_USE_FOUNDRY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ANTHROPIC_FOUNDRY_BASE_URL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;apim&amp;gt;.azure-api.net/claude"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ANTHROPIC_DEFAULT_SONNET_MODEL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ANTHROPIC_DEFAULT_OPUS_MODEL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ANTHROPIC_DEFAULT_HAIKU_MODEL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-5"&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;"availableModels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-5"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"enforceAvailableModels"&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;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;ANTHROPIC_FOUNDRY_BASE_URL&lt;/code&gt; and &lt;code&gt;ANTHROPIC_FOUNDRY_RESOURCE&lt;/code&gt; are mutually exclusive.&lt;/strong&gt;&lt;br&gt;
Setting both fails with &lt;code&gt;baseURL and resource are mutually exclusive&lt;/code&gt;. Drop the resource&lt;br&gt;
variable when you move to the gateway.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That one file covers the CLI and the VS Code extension. The panel opens straight into a usable&lt;br&gt;
prompt — no sign-in step, because the Entra credential is already resolved — and streaming works&lt;br&gt;
through the gateway unchanged:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftznu7w1vedr71dtxmagi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftznu7w1vedr71dtxmagi.png" alt="Claude Code panel in VS Code answering a question about a file through the governed gateway, having called its Glob and Read tools, with no sign-in prompt"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To stop developers editing their own settings, push the same JSON as &lt;strong&gt;managed settings&lt;/strong&gt;:&lt;br&gt;
&lt;code&gt;HKLM\SOFTWARE\Policies\ClaudeCode&lt;/code&gt; via Intune or Group Policy on Windows,&lt;br&gt;
&lt;code&gt;/Library/Application Support/ClaudeCode/managed-settings.json&lt;/code&gt; on macOS,&lt;br&gt;
&lt;code&gt;/etc/claude-code/managed-settings.json&lt;/code&gt; on Linux. Those also let you force &lt;code&gt;availableModels&lt;/code&gt; and&lt;br&gt;
block &lt;code&gt;bypassPermissions&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Onboarding a developer, end to end
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;member&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;claude-code-standard&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--member-id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;alice&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;contoso.com&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;/scripts/Sync-ClaudeAccess.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ApimName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;apim&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;rg&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Alice then runs &lt;code&gt;az login&lt;/code&gt;, drops in the settings file, and works. No key, no Foundry role, and&lt;br&gt;
she appears in chargeback from her first request.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Guests:&lt;/strong&gt; a B2B guest (a &lt;code&gt;#EXT#&lt;/code&gt; UPN) must use &lt;code&gt;az login --tenant &amp;lt;tenant-id&amp;gt;&lt;/code&gt;. A plain&lt;br&gt;
&lt;code&gt;az login&lt;/code&gt; lands them in their home tenant and the gateway returns 401. This caught me out&lt;br&gt;
onboarding a colleague.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  The accelerator
&lt;/h2&gt;

&lt;p&gt;All of the above is packaged as a solution accelerator:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/naveenneog/claude-code-foundry-gateway" rel="noopener noreferrer"&gt;github.com/naveenneog/claude-code-foundry-gateway&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/naveenneog/claude-code-foundry-gateway&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;claude-code-foundry-gateway&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;/deploy.ps1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With no arguments it discovers a Foundry account that has Claude deployments, maps the&lt;br&gt;
&lt;code&gt;sonnet&lt;/code&gt;/&lt;code&gt;opus&lt;/code&gt;/&lt;code&gt;haiku&lt;/code&gt; aliases to deployments that actually exist, deploys APIM + Log Analytics +&lt;br&gt;
Application Insights, creates the API and policy, grants the gateway identity its role, creates&lt;br&gt;
the Entra groups, syncs membership, and writes the &lt;code&gt;settings.json&lt;/code&gt; you hand to developers.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;./deploy.ps1 -WhatIf&lt;/code&gt; previews without changing anything. There is a &lt;strong&gt;Deploy to Azure&lt;/strong&gt; button&lt;br&gt;
for the portal path, &lt;code&gt;Show-Governance.ps1&lt;/code&gt; to verify all four controls, and an inspector proxy&lt;br&gt;
that shows exactly what Claude Code sends — that is how the identity model above was established&lt;br&gt;
rather than assumed.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Approx&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;APIM Basic v2, 1 unit&lt;/td&gt;
&lt;td&gt;~$250/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Log Analytics + App Insights&lt;/td&gt;
&lt;td&gt;ingestion-based, small at this volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude tokens&lt;/td&gt;
&lt;td&gt;Foundry CCU billing, unchanged by the gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Basic v2 is the cheapest tier that can enforce Anthropic token budgets at all. If that is too&lt;br&gt;
much for a pilot, run direct-to-Foundry first and add the gateway when a second team joins.&lt;/p&gt;




&lt;h2&gt;
  
  
  The honest summary
&lt;/h2&gt;

&lt;p&gt;The gateway buys you four things that direct access cannot: &lt;strong&gt;entitlement&lt;/strong&gt;, &lt;strong&gt;budgets&lt;/strong&gt;,&lt;br&gt;
&lt;strong&gt;throttling&lt;/strong&gt; and &lt;strong&gt;attribution&lt;/strong&gt;. It costs you an APIM instance and one more hop.&lt;/p&gt;

&lt;p&gt;For a single developer that trade is not worth it. For a team — especially one where "who spent&lt;br&gt;
the $4,000?" is a question someone will eventually ask — it is the difference between a tool you&lt;br&gt;
can roll out and a tool you have to keep explaining.&lt;/p&gt;

&lt;p&gt;The part I did not expect: none of it required changing how developers work. They still run&lt;br&gt;
&lt;code&gt;az login&lt;/code&gt; and &lt;code&gt;claude&lt;/code&gt;. The governance is entirely on the platform side.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;💻 &lt;strong&gt;Accelerator:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/claude-code-foundry-gateway" rel="noopener noreferrer"&gt;https://github.com/naveenneog/claude-code-foundry-gateway&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📘 &lt;strong&gt;Getting Claude Code on Foundry first:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/AI4Good/2026/08/13/claude-code-on-microsoft-foundry/" rel="noopener noreferrer"&gt;step-by-step setup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📗 &lt;strong&gt;Microsoft Learn — APIM &lt;code&gt;llm-token-limit&lt;/code&gt;:&lt;/strong&gt; &lt;a href="https://learn.microsoft.com/en-us/azure/api-management/llm-token-limit-policy" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/api-management/llm-token-limit-policy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📗 &lt;strong&gt;Microsoft Learn — &lt;code&gt;llm-emit-token-metric&lt;/code&gt;:&lt;/strong&gt; &lt;a href="https://learn.microsoft.com/en-us/azure/api-management/llm-emit-token-metric-policy" rel="noopener noreferrer"&gt;https://learn.microsoft.com/en-us/azure/api-management/llm-emit-token-metric-policy&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>azure</category>
      <category>ai</category>
      <category>devops</category>
      <category>governance</category>
    </item>
    <item>
      <title>Run Claude Code on Microsoft Foundry: Step-by-Step Setup for VS Code and the CLI</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Thu, 13 Aug 2026 09:19:02 +0000</pubDate>
      <link>https://dev.to/naveenneog/claude-code-on-microsoft-foundry-your-own-claude-deployment-entra-id-auth-no-api-key-4o3j</link>
      <guid>https://dev.to/naveenneog/claude-code-on-microsoft-foundry-your-own-claude-deployment-entra-id-auth-no-api-key-4o3j</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5b23azodifb6uqvfdtq4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5b23azodifb6uqvfdtq4.png" alt="Claude Code /status output showing API provider: Microsoft Foundry, the Foundry resource name, and the model resolved to claude-sonnet-5" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Claude Code can run against Claude models deployed in &lt;strong&gt;your own Microsoft Foundry resource&lt;/strong&gt;,&lt;br&gt;
authenticated with &lt;strong&gt;Microsoft Entra ID&lt;/strong&gt; — &lt;code&gt;az login&lt;/code&gt; on a workstation, managed identity on&lt;br&gt;
Azure compute. No Anthropic account, no API key on disk, no proxy.&lt;/p&gt;

&lt;p&gt;This is the procedure. Steps 1–3 are shared setup, &lt;strong&gt;Step 4 is the VS Code extension&lt;/strong&gt;, &lt;strong&gt;Step 5&lt;br&gt;
is the CLI&lt;/strong&gt;, Step 6 verifies. Steps 4 and 5 are independent — do either or both.&lt;/p&gt;

&lt;p&gt;Verified on &lt;strong&gt;13 Aug 2026&lt;/strong&gt;, Claude Code &lt;strong&gt;v2.1.223&lt;/strong&gt;, against &lt;code&gt;claude-sonnet-5&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;claude-opus-5&lt;/code&gt; in &lt;strong&gt;East US 2&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 0 — Find your values (start here)
&lt;/h2&gt;

&lt;p&gt;The whole setup needs five values. Here is what each one looks like, where it lives, and the&lt;br&gt;
command that prints it. Examples below are real values from my own subscription with a few&lt;br&gt;
characters masked.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Used for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subscription ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e839ff0f-532b-4828-****-****19d85&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;az account set&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resource group&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;rg-cont****hub&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;every &lt;code&gt;az ... -g&lt;/code&gt; flag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Foundry resource name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ai-contosohub****751908&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ANTHROPIC_FOUNDRY_RESOURCE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Region&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;eastus2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;checking model availability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment names&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;claude-sonnet-5&lt;/code&gt;, &lt;code&gt;claude-opus-5&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;the three model aliases&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Where they live in the Azure portal
&lt;/h3&gt;

&lt;p&gt;Open &lt;a href="https://portal.azure.com" rel="noopener noreferrer"&gt;portal.azure.com&lt;/a&gt; → search your Foundry resource by name →&lt;br&gt;
&lt;strong&gt;Overview&lt;/strong&gt;. Everything except the deployment names is on this one blade:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnyrs7e5i0odrbbu538ry.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnyrs7e5i0odrbbu538ry.png" alt="Azure portal Overview blade for a Foundry resource, with the resource name, resource group, subscription, subscription ID, location and API kind highlighted" width="800" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Read off:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource name&lt;/strong&gt; — the page title. &lt;code&gt;API Kind&lt;/code&gt; must say &lt;code&gt;AIServices&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource group&lt;/strong&gt; and &lt;strong&gt;Subscription&lt;/strong&gt; / &lt;strong&gt;Subscription ID&lt;/strong&gt; — in &lt;em&gt;Essentials&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location&lt;/strong&gt; — the region, for Step 1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keys and Endpoint&lt;/strong&gt; (left nav, under &lt;em&gt;Resource Management&lt;/em&gt;) — the endpoint URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access control (IAM)&lt;/strong&gt; (left nav) — where you add the role in Step 2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Deployment names&lt;/strong&gt; are not in the Azure portal. Click &lt;strong&gt;Go to Foundry portal&lt;/strong&gt; at the top of&lt;br&gt;
that blade, then &lt;strong&gt;View deployments&lt;/strong&gt; on the project home — or just use the CLI below, which is&lt;br&gt;
faster.&lt;/p&gt;
&lt;h3&gt;
  
  
  Or get everything from the CLI
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# subscription + tenant&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{subscription:id, tenant:tenantId, user:user.name}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# every Foundry resource you can see, with its resource group and region&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cognitiveservices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[?kind=='AIServices'].{name:name, rg:resourceGroup, region:location}"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# the deployment names on one of them&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cognitiveservices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;deployment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;rg&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or run &lt;a href="https://github.com/naveenneog/AI4Good/blob/main/assets/code/2026-08-13-claude-code-on-microsoft-foundry/Get-FoundryValues.ps1" rel="noopener noreferrer"&gt;&lt;code&gt;Get-FoundryValues.ps1&lt;/code&gt;&lt;/a&gt;,&lt;br&gt;
which finds every value, checks the RBAC role, and prints the exact config block to paste into&lt;br&gt;
Steps 4 and 5. With no arguments it scans your subscription and picks a resource that actually&lt;br&gt;
has Claude deployments. Add &lt;code&gt;-Mask&lt;/code&gt; when screenshotting or pasting into a ticket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\Get-FoundryValues.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Mask&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fap0o1fr9sidz4aydnrse.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fap0o1fr9sidz4aydnrse.png" alt="Get-FoundryValues.ps1 output listing the signed-in account, subscription and tenant, resource name and group, region, the Anthropic endpoint, both Claude deployment names, the data-plane role check, and the resulting settings.json block — all partially masked" width="800" height="825"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The last block is the config you need — copy it straight into Step 4.2 or 5.2.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Foundry resource, kind &lt;code&gt;AIServices&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;az cognitiveservices account show -n &amp;lt;res&amp;gt; -g &amp;lt;rg&amp;gt; --query kind -o tsv&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude models deployed&lt;/td&gt;
&lt;td&gt;Step 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Cognitive Services User&lt;/code&gt; on the resource&lt;/td&gt;
&lt;td&gt;Step 2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure CLI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;az --version&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js 18+&lt;/td&gt;
&lt;td&gt;&lt;code&gt;node --version&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VS Code 1.94+&lt;/td&gt;
&lt;td&gt;&lt;code&gt;code --version&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you don't yet know your resource name, resource group or deployment names, do &lt;strong&gt;Step 0&lt;/strong&gt; first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — Confirm your Claude deployments
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cognitiveservices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;deployment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;rg&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;strong&gt;deployment names&lt;/strong&gt; in the output. These are the names you configure later — they are&lt;br&gt;
yours, not Anthropic's catalogue names.&lt;/p&gt;

&lt;p&gt;To create a deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cognitiveservices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;deployment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;rg&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--deployment-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;claude-haiku-4-5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--model-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;claude-haiku-4-5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--model-version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--model-format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--sku-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GlobalStandard&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--sku-capacity&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To list what your region offers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cognitiveservices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;ConvertFrom-Json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;Where-Object&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="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'Anthropic'&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;ForEach-Object&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="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&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="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Sort-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Unique&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If deployment creation fails with a Marketplace error&lt;/strong&gt; — Claude models bill through Azure&lt;br&gt;
Marketplace. Internal, sandbox, CSP, student and sponsored-credit subscriptions are blocked&lt;br&gt;
from creating them:&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: Marketplace purchases are disabled for this subscription due to policy restrictions.
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Existing deployments keep working. Map the missing alias to a deployment you do have (Step 4.2).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 2 — Grant data-plane access
&lt;/h2&gt;

&lt;p&gt;Owner on the subscription is &lt;strong&gt;not&lt;/strong&gt; sufficient — that is control plane. Inference needs the&lt;br&gt;
data-plane role &lt;strong&gt;Cognitive Services User&lt;/strong&gt; (&lt;code&gt;a97b65f3-24c7-4388-baec-2e87135dc908&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cognitiveservices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;rg&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsv&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;assignment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--assignee&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;upn-or-object-id&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cognitive Services User"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;assignment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--assignee&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;you&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--include-inherited&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3 — Sign in with Entra ID
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Workstation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--subscription&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;subscription-id&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Azure compute (VM, Container App, DevBox) — managed identity:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az login &lt;span class="nt"&gt;--identity&lt;/span&gt;                          &lt;span class="c"&gt;# system-assigned&lt;/span&gt;
az login &lt;span class="nt"&gt;--identity&lt;/span&gt; &lt;span class="nt"&gt;--username&lt;/span&gt; &amp;lt;client-id&amp;gt;   &lt;span class="c"&gt;# user-assigned&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Assign the same role from Step 2 to the identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;assignment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--assignee-object-id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;managed-identity-principal-id&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--assignee-principal-type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ServicePrincipal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cognitive Services User"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$scope&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confirm a token is obtainable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;get-access-token&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://cognitiveservices.azure.com&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;expiresOn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsv&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Both &lt;code&gt;https://cognitiveservices.azure.com&lt;/code&gt; and &lt;code&gt;https://ai.azure.com&lt;/code&gt; are accepted scopes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Optional — prove the endpoint answers before involving Claude Code at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;get-access-token&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://cognitiveservices.azure.com&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;accessToken&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsv&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;Invoke-RestMethod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Method&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Post&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-Uri&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://&amp;lt;resource&amp;gt;.services.ai.azure.com/anthropic/v1/messages"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-Headers&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="nx"&gt;Authorization&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer &lt;/span&gt;&lt;span class="nv"&gt;$t&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'anthropic-version'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'2023-06-01'&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="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-ContentType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;-Body&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{"model":"claude-sonnet-5","max_tokens":32,"messages":[{"role":"user","content":"Reply with exactly: FOUNDRY-OK"}]}'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4 — VS Code extension setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  4.1 Install the extension
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--install-extension&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;anthropic.claude-code&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or: &lt;strong&gt;Ctrl+Shift+X&lt;/strong&gt; → search &lt;em&gt;Claude Code&lt;/em&gt; → &lt;strong&gt;Install&lt;/strong&gt; (publisher: Anthropic).&lt;/p&gt;

&lt;p&gt;The extension bundles its own engine. The npm CLI in Step 5 is not required for the panel to work.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.2 Configure
&lt;/h3&gt;

&lt;p&gt;Open &lt;strong&gt;Ctrl+Shift+P → &lt;code&gt;Preferences: Open User Settings (JSON)&lt;/code&gt;&lt;/strong&gt; and add:&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;"claudeCode.environmentVariables"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CLAUDE_CODE_USE_FOUNDRY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ANTHROPIC_FOUNDRY_RESOURCE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-resource-name&amp;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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ANTHROPIC_DEFAULT_OPUS_MODEL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-5"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ANTHROPIC_DEFAULT_SONNET_MODEL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-5"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ANTHROPIC_DEFAULT_HAIKU_MODEL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-haiku-4-5"&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;Rules:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ANTHROPIC_FOUNDRY_RESOURCE&lt;/code&gt; is the &lt;strong&gt;resource name&lt;/strong&gt;, not a URL. Claude Code expands it to
&lt;code&gt;https://&amp;lt;resource&amp;gt;.services.ai.azure.com/anthropic&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pin all three model aliases&lt;/strong&gt; to deployment names that exist. Foundry mode does no start-up
model check, so a wrong alias fails mid-task as &lt;code&gt;DeploymentNotFound&lt;/code&gt;, not at launch. If
&lt;code&gt;claude-haiku-4-5&lt;/code&gt; is not deployed, set the haiku alias to your Sonnet deployment.&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;no key&lt;/strong&gt;. Leaving &lt;code&gt;ANTHROPIC_FOUNDRY_API_KEY&lt;/code&gt; and &lt;code&gt;ANTHROPIC_FOUNDRY_AUTH_TOKEN&lt;/code&gt; unset
is what makes Claude Code fall through to &lt;code&gt;DefaultAzureCredential&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you also complete Step 5, &lt;code&gt;~/.claude/settings.json&lt;/code&gt; alone is sufficient for the extension and&lt;br&gt;
this block is redundant — I tested that by deleting it and re-running. Either location works.&lt;/p&gt;
&lt;h3&gt;
  
  
  4.3 Reload and open the panel
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Ctrl+Shift+P → &lt;code&gt;Developer: Reload Window&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ctrl+Shift+P → &lt;code&gt;Claude Code: Open in Side Bar&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The panel opens straight into a usable prompt. &lt;strong&gt;There is no sign-in step&lt;/strong&gt; — that absence is the&lt;br&gt;
first signal Entra ID resolved.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ki6zifkmw5qby0acvs4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2ki6zifkmw5qby0acvs4.png" alt="Claude Code panel open in VS Code showing a ready prompt with no sign-in required" width="799" height="510"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  4.4 Confirm it answers
&lt;/h3&gt;

&lt;p&gt;Type any question about the open folder and send it. The panel calls its normal file tools and&lt;br&gt;
answers from your Foundry deployment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9q0nfdyvskwewzbfcalf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9q0nfdyvskwewzbfcalf.png" alt="Claude Code panel answering a question about a file after calling its Glob and Read tools" width="799" height="510"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgacqr8xnvvke4aejw1ph.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgacqr8xnvvke4aejw1ph.gif" alt="Animated walkthrough of the Claude Code panel opening without a login prompt and answering a question about the codebase" width="800" height="511"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;/status&lt;/code&gt; does &lt;strong&gt;not&lt;/strong&gt; work in the panel — it returns &lt;em&gt;"/status isn't available in this&lt;br&gt;
environment."&lt;/em&gt; Use Step 5.3 to inspect the provider.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  Step 5 — Claude Code CLI setup
&lt;/h2&gt;
&lt;h3&gt;
  
  
  5.1 Install
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;npm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;anthropic-ai/claude-code&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--version&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  5.2 Configure
&lt;/h3&gt;

&lt;p&gt;Create or edit &lt;code&gt;%USERPROFILE%\.claude\settings.json&lt;/code&gt; (&lt;code&gt;~/.claude/settings.json&lt;/code&gt; on macOS/Linux):&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;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://json.schemastore.org/claude-code-settings.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"CLAUDE_CODE_USE_FOUNDRY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ANTHROPIC_FOUNDRY_RESOURCE"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-resource-name&amp;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;"ANTHROPIC_DEFAULT_OPUS_MODEL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ANTHROPIC_DEFAULT_SONNET_MODEL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-sonnet-5"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ANTHROPIC_DEFAULT_HAIKU_MODEL"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-haiku-4-5"&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;Same three rules as 4.2. For a one-off session, use environment variables instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;CLAUDE_CODE_USE_FOUNDRY&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;ANTHROPIC_FOUNDRY_RESOURCE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;your-resource-name&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.3 Verify the provider
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;status&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 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;"loggedIn"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"authMethod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"third_party"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"apiProvider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"foundry"&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 powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;doctor&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 plaintext"&gt;&lt;code&gt;CLAUDE_CODE_USE_FOUNDRY is set, so this session is using Microsoft Foundry
- Not connected to the Anthropic API (api.anthropic.com)
- Not signed in to claude.ai
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then start an interactive session and run &lt;code&gt;/status&lt;/code&gt; — it reports &lt;strong&gt;API provider: Microsoft&lt;br&gt;
Foundry&lt;/strong&gt; and the resource name (the screenshot at the top of this post).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.4 One headless check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Reply with exactly: FOUNDRY-OK"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--output-format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for &lt;code&gt;"provider":"foundry"&lt;/code&gt; and &lt;code&gt;"canonicalModel":"claude-sonnet-5"&lt;/code&gt; in &lt;code&gt;modelUsage&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6 — Verify end to end
&lt;/h2&gt;

&lt;p&gt;Run the seven-point check (&lt;a href="https://github.com/naveenneog/AI4Good/blob/main/assets/code/2026-08-13-claude-code-on-microsoft-foundry/Test-ClaudeFoundry.ps1" rel="noopener noreferrer"&gt;&lt;code&gt;Test-ClaudeFoundry.ps1&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;\Test-ClaudeFoundry.ps1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;rg&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzbmlgq2tucn74pk4akav.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzbmlgq2tucn74pk4akav.png" alt="Seven verification checks passing: Azure CLI sign-in, Entra ID token, Claude deployments found, Messages API responds, CLI installed, provider equals foundry, and an end-to-end Claude Code turn" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Azure CLI signed in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Entra ID data-plane token acquired&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Claude deployments exist on the resource&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/anthropic/v1/messages&lt;/code&gt; responds over Entra ID auth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Claude Code CLI installed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;claude auth status&lt;/code&gt; reports &lt;code&gt;foundry&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;A real Claude Code turn completes on Foundry&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It exits non-zero on failure, so it drops into CI unchanged.&lt;/p&gt;

&lt;p&gt;Confirm from the Azure side that traffic reached the resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$rid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;cognitiveservices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;rg&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsv&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;monitor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;metrics&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$rid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--metric&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;TokenTransaction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--start-time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Date&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToUniversalTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'yyyy-MM-ddTHH:mm:ssZ'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;--interval&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;PT5M&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--aggregation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Total&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Reference
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Endpoint&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST https://&amp;lt;resource&amp;gt;.services.ai.azure.com/anthropic/v1/messages
     Authorization: Bearer &amp;lt;entra-id-token&amp;gt;
     anthropic-version: 2023-06-01
     Content-Type: application/json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Foundry exposes Claude through the &lt;strong&gt;native Anthropic Messages API&lt;/strong&gt;, which is why no proxy is&lt;br&gt;
needed. Verified on a live resource:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/anthropic/v1/messages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/anthropic/v1/messages/count_tokens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/openai/v1/chat/completions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;404 &lt;code&gt;api_not_supported&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/models/chat/completions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;404 &lt;code&gt;api_not_supported&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/openai/deployments/&amp;lt;name&amp;gt;/chat/completions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;404 &lt;code&gt;api_not_supported&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/anthropic/v1/models&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;404 &lt;code&gt;api_not_supported&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Streaming (SSE), tool use and &lt;code&gt;count_tokens&lt;/code&gt; all work. &lt;code&gt;?api-version=&lt;/code&gt; is optional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment variables&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;Variable&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CLAUDE_CODE_USE_FOUNDRY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1&lt;/code&gt; enables Foundry mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ANTHROPIC_FOUNDRY_RESOURCE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Resource name; expands to the &lt;code&gt;services.ai.azure.com/anthropic&lt;/code&gt; base URL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ANTHROPIC_FOUNDRY_BASE_URL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Full base URL override (gateways)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ANTHROPIC_FOUNDRY_API_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Key auth instead of Entra ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ANTHROPIC_FOUNDRY_AUTH_TOKEN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pre-fetched bearer token; highest precedence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ANTHROPIC_DEFAULT_SONNET_MODEL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment behind the &lt;code&gt;sonnet&lt;/code&gt; alias&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ANTHROPIC_DEFAULT_OPUS_MODEL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment behind the &lt;code&gt;opus&lt;/code&gt; alias&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ANTHROPIC_DEFAULT_HAIKU_MODEL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deployment behind &lt;code&gt;haiku&lt;/code&gt; and background tasks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Auth precedence: &lt;code&gt;ANTHROPIC_FOUNDRY_AUTH_TOKEN&lt;/code&gt; → &lt;code&gt;ANTHROPIC_FOUNDRY_API_KEY&lt;/code&gt; →&lt;br&gt;
&lt;code&gt;DefaultAzureCredential&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;CLAUDE_CODE_USE_AZURE&lt;/code&gt; does not exist.&lt;/strong&gt; Scanning the v2.1.223 binary shows all five&lt;br&gt;
&lt;code&gt;*_FOUNDRY_*&lt;/code&gt; variables present and no &lt;code&gt;CLAUDE_CODE_USE_AZURE&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause → Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;DeploymentNotFound&lt;/code&gt; mid-task&lt;/td&gt;
&lt;td&gt;A model alias points at a deployment you do not have. Re-pin it (4.2 / 5.2).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 401 &lt;code&gt;invalid subscription key&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No usable &lt;code&gt;Authorization&lt;/code&gt; header. Check Step 2 role and Step 3 sign-in. &lt;code&gt;Authorization: Bearer&lt;/code&gt; wins when both it and &lt;code&gt;x-api-key&lt;/code&gt; are sent.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 404 &lt;code&gt;api_not_supported&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;OpenAI-shaped path. Claude deployments expose only &lt;code&gt;/anthropic/*&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extension prompts for Anthropic sign-in&lt;/td&gt;
&lt;td&gt;Foundry variables not visible to it. Set them in &lt;code&gt;claudeCode.environmentVariables&lt;/code&gt; or &lt;code&gt;~/.claude/settings.json&lt;/code&gt;, then &lt;strong&gt;Developer: Reload Window&lt;/strong&gt;. Shell exports do not reach the extension.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/status&lt;/code&gt; unavailable&lt;/td&gt;
&lt;td&gt;Panel-only limitation. Use &lt;code&gt;claude auth status&lt;/code&gt;, or &lt;code&gt;/status&lt;/code&gt; in a terminal session.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marketplace error on deployment create&lt;/td&gt;
&lt;td&gt;Subscription is blocked from Marketplace purchases. Reuse existing deployments or use a pay-as-you-go subscription.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows: helper returns a WSL error as the "token"&lt;/td&gt;
&lt;td&gt;In Git Bash a bare &lt;code&gt;az&lt;/code&gt; resolves to the WSL shim. Use &lt;code&gt;az.cmd&lt;/code&gt;. Note &lt;code&gt;command -v az.cmd&lt;/code&gt; also fails (bash ignores &lt;code&gt;PATHEXT&lt;/code&gt;), so run the candidate and validate the result starts with &lt;code&gt;eyJ&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Scripts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/naveenneog/AI4Good/blob/main/assets/code/2026-08-13-claude-code-on-microsoft-foundry/Get-FoundryValues.ps1" rel="noopener noreferrer"&gt;&lt;code&gt;Get-FoundryValues.ps1&lt;/code&gt;&lt;/a&gt; — finds every value in Step 0 and prints the config block (&lt;code&gt;-Mask&lt;/code&gt; to share safely)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/naveenneog/AI4Good/blob/main/assets/code/2026-08-13-claude-code-on-microsoft-foundry/Setup-ClaudeFoundry.ps1" rel="noopener noreferrer"&gt;&lt;code&gt;Setup-ClaudeFoundry.ps1&lt;/code&gt;&lt;/a&gt; — writes both config files, backs up what is already there&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/naveenneog/AI4Good/blob/main/assets/code/2026-08-13-claude-code-on-microsoft-foundry/Test-ClaudeFoundry.ps1" rel="noopener noreferrer"&gt;&lt;code&gt;Test-ClaudeFoundry.ps1&lt;/code&gt;&lt;/a&gt; — the seven checks in Step 6&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/naveenneog/AI4Good/blob/main/assets/code/2026-08-13-claude-code-on-microsoft-foundry/get-foundry-token.sh" rel="noopener noreferrer"&gt;&lt;code&gt;get-foundry-token.sh&lt;/code&gt;&lt;/a&gt; — optional token helper for CI (Azure CLI, then IMDS)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://code.claude.com/docs/en/microsoft-foundry" rel="noopener noreferrer"&gt;Anthropic — Claude Code on Microsoft Foundry&lt;/a&gt; ·&lt;br&gt;
&lt;a href="https://learn.microsoft.com/en-us/azure/foundry/foundry-models/how-to/use-foundry-models-claude" rel="noopener noreferrer"&gt;Microsoft Learn — Deploy and use Claude models in Foundry&lt;/a&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>ai</category>
      <category>devtools</category>
      <category>vscode</category>
    </item>
    <item>
      <title>SpeechBridge: Near-Realtime Speech Translation on Azure — Keyless, One-Command, and Honest About Its Latency</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Mon, 10 Aug 2026 14:21:33 +0000</pubDate>
      <link>https://dev.to/naveenneog/speechbridge-near-realtime-speech-translation-on-azure-keyless-one-command-and-honest-about-58ca</link>
      <guid>https://dev.to/naveenneog/speechbridge-near-realtime-speech-translation-on-azure-keyless-one-command-and-honest-about-58ca</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd2v5sghkplx8rwg7zst2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd2v5sghkplx8rwg7zst2.png" alt="SpeechBridge translating English to Hindi in real time — the speaker's panel shows "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;#AI4Good.&lt;/strong&gt; I built a live interpreter: you speak, and the person across the table hears&lt;br&gt;
you in their own language, roughly half a second later. Then I spent as much effort&lt;br&gt;
documenting where it must &lt;strong&gt;not&lt;/strong&gt; be used as I did making it fast.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Language is the quietest barrier there is. It decides who can ask a question in a meeting, who&lt;br&gt;
can follow a lesson, who gets served first at a counter. Machine interpretation cannot fix&lt;br&gt;
that — but a demo that puts a decent version of it in anyone's hands, free and open, is a&lt;br&gt;
useful thing to be able to point at.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;Two people who do not share a language sit at one laptop. Each speaks naturally; the other&lt;br&gt;
hears it in their own language, in a natural neural voice. Live captions stream in both&lt;br&gt;
languages while you are still talking.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;16 languages&lt;/strong&gt;, including English, Hindi, Kannada, Tamil, Telugu and Marathi alongside
Spanish, French, German, Italian, Portuguese, Japanese, Korean, Chinese, Russian and Arabic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bidirectional.&lt;/strong&gt; Hold the floor with &lt;code&gt;1&lt;/code&gt; or &lt;code&gt;2&lt;/code&gt;, talk, press &lt;code&gt;Esc&lt;/code&gt;. The other side hears
the translation spoken aloud.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Right-to-left aware&lt;/strong&gt;, so Arabic renders correctly rather than approximately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It shows you the real numbers.&lt;/strong&gt; Every turn displays three timings: time to caption, time
to settled translation, and time until the other person actually heard it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the part I care about most. "Realtime" is the easiest word in AI to say and&lt;br&gt;
the hardest to mean. So the interface just tells you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2uj3fnp3lyw1jc0o48t3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2uj3fnp3lyw1jc0o48t3.png" alt="The reverse direction — Hindi to English, "&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The whole thing runs on &lt;strong&gt;Azure AI Speech&lt;/strong&gt;, and the browser streams microphone audio&lt;br&gt;
&lt;strong&gt;directly&lt;/strong&gt; to Azure over a WebSocket. There's a small Express server, but audio never&lt;br&gt;
touches it — putting a backend in that path would add a hop to every audio frame and quietly&lt;br&gt;
destroy the latency budget. The server exists for exactly one job: minting credentials.&lt;/p&gt;
&lt;h3&gt;
  
  
  There are no API keys. There cannot be.
&lt;/h3&gt;

&lt;p&gt;Every Azure Speech sample starts by pasting a subscription key. That was impossible here — the&lt;br&gt;
tenant force-applies &lt;code&gt;disableLocalAuth=true&lt;/code&gt; to every Cognitive Services account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az cognitiveservices account keys list ...
→ ERROR: &lt;span class="o"&gt;(&lt;/span&gt;BadRequest&lt;span class="o"&gt;)&lt;/span&gt; Failed to list key. disableLocalAuth is &lt;span class="nb"&gt;set &lt;/span&gt;to be &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I created a brand-new resource to check whether it was a per-resource setting. It came back&lt;br&gt;
key-disabled too. So this is Azure Policy, not configuration, and the entire sample corpus is&lt;br&gt;
unusable.&lt;/p&gt;

&lt;p&gt;That turned out to be a gift. The design that survives is strictly better:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The server holds a &lt;strong&gt;managed identity&lt;/strong&gt; and gets a Microsoft Entra token.&lt;/li&gt;
&lt;li&gt;It exchanges that for a &lt;strong&gt;Speech-scoped token that dies in ten minutes&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The browser receives &lt;strong&gt;only&lt;/strong&gt; that token — never the Entra one, which every Cognitive
Services resource in reach would happily accept.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The project charter enforces this mechanically: browser code is &lt;em&gt;forbidden&lt;/em&gt; from importing&lt;br&gt;
&lt;code&gt;@azure/identity&lt;/code&gt;, and the build fails if it ever does. I grepped the shipped bundle to&lt;br&gt;
confirm.&lt;/p&gt;
&lt;h3&gt;
  
  
  Two landmines, documented so nobody else loses an afternoon
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Node's native &lt;code&gt;WebSocket&lt;/code&gt; silently breaks the Speech SDK.&lt;/strong&gt; Recognition died with&lt;br&gt;
&lt;code&gt;StatusCode: 1006&lt;/code&gt; and no explanation, while text-to-speech on the &lt;em&gt;same&lt;/em&gt; credential worked&lt;br&gt;
perfectly — which sends you hunting through auth and RBAC for an hour. The cause: modern Node&lt;br&gt;
exposes a global &lt;code&gt;WebSocket&lt;/code&gt; that negotiates over HTTP/2, which the Speech endpoints reject.&lt;br&gt;
&lt;code&gt;delete globalThis.WebSocket&lt;/code&gt; fixes it instantly. Browsers are unaffected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;TranslationRecognizer&lt;/code&gt; has no echo cancellation.&lt;/strong&gt; On open speakers, the app hears its own&lt;br&gt;
translation, translates &lt;em&gt;that&lt;/em&gt;, speaks it, and loops forever. My first fix — ignoring&lt;br&gt;
recognition results during playback — was reviewed and found insufficient, because results are&lt;br&gt;
filtered when they &lt;em&gt;arrive&lt;/em&gt;, not when the audio was &lt;em&gt;captured&lt;/em&gt;. Audio recorded during playback&lt;br&gt;
can finalise after the cooldown and be believed. The real fix is to mute the microphone track&lt;br&gt;
at the device.&lt;/p&gt;
&lt;h3&gt;
  
  
  Chasing the latency, and being wrong about it
&lt;/h3&gt;

&lt;p&gt;Time-to-heard started at &lt;strong&gt;1.2–2.4 seconds&lt;/strong&gt;. The measurements said synthesis was 75–85% of it,&lt;br&gt;
so the obvious move was switching to the recognizer's built-in fused synthesis. I benchmarked&lt;br&gt;
it instead of assuming:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Median to first audio&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Chained synthesizer (cold)&lt;/td&gt;
&lt;td&gt;2775 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Chained + pre-opened connection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2218 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fused (architectural rewrite)&lt;/td&gt;
&lt;td&gt;2032 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The middle row is the answer. Most of the cost wasn't the extra network round trip at all — it&lt;br&gt;
was a &lt;strong&gt;cold TLS and WebSocket handshake&lt;/strong&gt;, paid at the worst possible instant, right after the&lt;br&gt;
speaker stops. Opening that connection &lt;em&gt;while the user is still talking&lt;/em&gt;, when the wait is&lt;br&gt;
free, recovered three quarters of the available win for a few lines of code, and no rewrite.&lt;/p&gt;

&lt;p&gt;Time-to-heard is now &lt;strong&gt;0.4–0.7 seconds&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  One command to deploy
&lt;/h3&gt;

&lt;p&gt;It ships as an &lt;strong&gt;Azure Solution Accelerator&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;azd auth login &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; azd up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bicep provisions the AI Services account, a managed identity with the &lt;em&gt;Cognitive Services&lt;br&gt;
User&lt;/em&gt; role, a container registry, Container Apps, and monitoring — then builds the image&lt;br&gt;
&lt;strong&gt;remotely in ACR&lt;/strong&gt; (so you don't need Docker installed), creates the Microsoft Entra app&lt;br&gt;
registration, and turns on sign-in.&lt;/p&gt;
&lt;h3&gt;
  
  
  The bug that only a real deployment could find
&lt;/h3&gt;

&lt;p&gt;That role is &lt;em&gt;Cognitive Services User&lt;/em&gt;, and getting there cost me an hour.&lt;/p&gt;

&lt;p&gt;I had assigned &lt;strong&gt;Cognitive Services Speech User&lt;/strong&gt; — the obvious choice for a speech app, and&lt;br&gt;
what every instinct says is correct least-privilege. The deployed app failed every single&lt;br&gt;
token exchange:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;HTTP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"PermissionDenied"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"Principal does not have access to API/Operation."&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The role assignment existed. The client ID matched. The endpoint was right. It wasn't&lt;br&gt;
propagation — I retried for forty minutes.&lt;/p&gt;

&lt;p&gt;Listing the role's actual permissions explains it. Every data action &lt;em&gt;Cognitive Services&lt;br&gt;
Speech User&lt;/em&gt; grants sits beneath &lt;code&gt;accounts/SpeechServices/…&lt;/code&gt;, &lt;code&gt;CustomVoice&lt;/code&gt;, &lt;code&gt;TTSPlayer&lt;/code&gt;.&lt;br&gt;
&lt;strong&gt;Not one of them authorises &lt;code&gt;/sts/v1.0/issueToken&lt;/code&gt;&lt;/strong&gt;, which isn't under &lt;code&gt;SpeechServices&lt;/code&gt; at&lt;br&gt;
all. That's the endpoint my whole keyless design depends on.&lt;/p&gt;

&lt;p&gt;And here's why it hid so well: &lt;strong&gt;locally, everything worked perfectly.&lt;/strong&gt; Because locally I was&lt;br&gt;
signed in as a subscription &lt;strong&gt;Owner&lt;/strong&gt;, and inherited roles quietly supplied the permission the&lt;br&gt;
explicitly-assigned one lacked. The managed identity had no such inheritance. It was the first&lt;br&gt;
principal to ever exercise that role as actually written.&lt;/p&gt;

&lt;p&gt;A permission model verified only by a subscription Owner proves almost nothing.&lt;/p&gt;

&lt;p&gt;That's now the second bug on this project that survived template validation, unit tests and&lt;br&gt;
careful reading, and died the moment something real ran. Both had the same shape: &lt;strong&gt;my&lt;br&gt;
development environment was more privileged and more permissive than production.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I originally targeted App Service and it validated perfectly. Then I actually deployed it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;InternalSubscriptionIsOverQuotaForSku
Current Limit (Total VMs): 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;App Service isn't rate-limited in that subscription — it's zero, which is common in enterprise&lt;br&gt;
and sponsored subscriptions. Deploying for real also surfaced two bugs no amount of template&lt;br&gt;
validation could: the container couldn't pull its own image, and it bound to loopback inside&lt;br&gt;
the container so every request timed out. &lt;strong&gt;Validate is not deploy.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Verified, not asserted
&lt;/h3&gt;

&lt;p&gt;180 unit tests, and an end-to-end check that plays recorded speech into a real Chromium&lt;br&gt;
microphone and fails unless the translation comes out in the right script — &lt;strong&gt;in both&lt;br&gt;
directions&lt;/strong&gt;. Proving one direction proves half a product. The design tokens are contrast-&lt;br&gt;
tested too, so a colour that fails WCAG fails the build.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good — and the honest limits
&lt;/h2&gt;

&lt;p&gt;The upside is obvious: a free, open, self-hostable interpreter lowers a real barrier, and every&lt;br&gt;
Indian language in the catalogue is one that machine translation demos usually skip.&lt;/p&gt;

&lt;p&gt;But the most important file in the repository is &lt;code&gt;docs/RESPONSIBLE_AI.md&lt;/code&gt;, and it is mostly a&lt;br&gt;
list of places you should &lt;strong&gt;refuse&lt;/strong&gt; to use this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Medical&lt;/strong&gt; consultations, diagnoses, consent, medication instructions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legal&lt;/strong&gt; proceedings, contracts, statements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emergency and safety-critical&lt;/strong&gt; communication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Immigration, asylum, or law-enforcement&lt;/strong&gt; interviews&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are precisely the settings where interpretation matters most — and precisely where this&lt;br&gt;
fails worst. Machine translation is confidently wrong in ways humans are not. The interface&lt;br&gt;
shows translations as flat statements with no confidence signal, so a wrong one looks exactly&lt;br&gt;
like a right one. It guesses gender and formality when translating into languages that mark&lt;br&gt;
them, and being misgendered is not a trivial error. It splits sentences on pauses and carries&lt;br&gt;
no context between turns.&lt;/p&gt;

&lt;p&gt;An AI4Good project that only lists its benefits isn't being good, it's being marketed. If you&lt;br&gt;
deploy this: tell people they're being machine-translated before they speak, say plainly that&lt;br&gt;
it's a machine, and provide a human alternative for anything that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;💻 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/speechbridge" rel="noopener noreferrer"&gt;github.com/naveenneog/speechbridge&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;☁️ &lt;strong&gt;Deploy:&lt;/strong&gt; &lt;code&gt;azd up&lt;/code&gt; — keyless, authenticated, about two minutes&lt;/li&gt;
&lt;li&gt;🖥️ &lt;strong&gt;Local:&lt;/strong&gt; &lt;code&gt;npm install &amp;amp;&amp;amp; npm run dev&lt;/code&gt;, then &lt;code&gt;az login&lt;/code&gt;. No API key required.&lt;/li&gt;
&lt;li&gt;📖 &lt;strong&gt;The decisions:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/speechbridge/tree/main/docs/adr" rel="noopener noreferrer"&gt;docs/adr/&lt;/a&gt; — including the two landmines above, and every place a review found me wrong.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai4good</category>
      <category>azure</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
    <item>
      <title>SpendLens: Private, On-Device Bank-Statement Analysis — No Login, No Cloud, No LLM</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Thu, 30 Jul 2026 07:13:08 +0000</pubDate>
      <link>https://dev.to/naveenneog/spendlens-private-on-device-bank-statement-analysis-no-login-no-cloud-no-llm-bnp</link>
      <guid>https://dev.to/naveenneog/spendlens-private-on-device-bank-statement-analysis-no-login-no-cloud-no-llm-bnp</guid>
      <description>&lt;p&gt;&lt;a href="https://naveenneog.github.io/SpendLens/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3u3iqbqq5pk6jmam46b9.png" alt="SpendLens — a bank statement analyzed entirely on-device: reconciled totals, spend by category, and a daily-spend chart, marked Verified" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Day 17 of #AI4Good.&lt;/strong&gt; I used an AI coding agent to build a personal-finance app — and the single most important design decision was to keep AI, the cloud, and the network completely &lt;em&gt;out&lt;/em&gt; of it. Your bank statement is read, parsed, and reconciled on your own device. Nothing is uploaded. Nothing is stored anywhere but your phone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm &lt;strong&gt;&lt;a href="https://naveenneog.github.io/AI4Good/about/" rel="noopener noreferrer"&gt;Naveen Gopalakrishna&lt;/a&gt;&lt;/strong&gt; — an AI Global Black Belt at Microsoft, a compulsive builder after hours. Most #AI4Good builds put AI &lt;em&gt;in&lt;/em&gt; the product. &lt;strong&gt;SpendLens&lt;/strong&gt; is the opposite, on purpose: the "good" here is financial privacy, so the intelligence lives in the parser, not in a model, and your money never leaves the device.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Try it in the browser (nothing retained):&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/SpendLens/" rel="noopener noreferrer"&gt;naveenneog.github.io/SpendLens&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Android APK:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/SpendLens/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/SpendLens/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/SpendLens" rel="noopener noreferrer"&gt;github.com/naveenneog/SpendLens&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;SpendLens reads a bank-statement PDF and turns it into an honest picture of where your money went — &lt;strong&gt;without a bank login, an OTP, a screen-scrape, or an upload&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Import a PDF&lt;/strong&gt; through the native Android/iOS document picker (or drag one into the browser version). Password-protected statements are supported; the password is used in memory and never kept.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconciled summary&lt;/strong&gt; — total spent, total received, opening and closing balance, and date range, in Indian digit grouping with tabular figures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spend by category, daily-spend trend, and largest spends&lt;/strong&gt;, each with an accessible table/list equivalent and labels that never rely on colour alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Searchable, filterable transactions&lt;/strong&gt; with editable categories. Correct a category once and SpendLens remembers the merchant rule for next time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Saved statement history&lt;/strong&gt; on-device, and a one-tap &lt;strong&gt;delete everything&lt;/strong&gt; that really deletes everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detection for 78 RBI scheduled retail banks&lt;/strong&gt; — public-sector, private, small-finance, payments, and regional-rural — by official name, known aliases, and IFSC prefixes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cover image is the browser version analysing a fully synthetic Axis sample: an &lt;code&gt;Axis Bank Ltd.&lt;/code&gt; statement, marked &lt;strong&gt;Verified&lt;/strong&gt;, &lt;code&gt;Active only in this tab&lt;/code&gt;, spend broken out by category, and a daily-spend chart — all rendered without a single network request leaving the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The whole app is &lt;strong&gt;Flutter (Dart)&lt;/strong&gt; with a thin layer of native platform code, and it was built with &lt;strong&gt;GitHub Copilot CLI in autopilot mode&lt;/strong&gt; — the agent researched Indian statement formats, wrote the parser and tests, ran the builds, and drove an Android emulator smoke test end to end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On-device PDF text extraction.&lt;/strong&gt; Rather than ship a commercial PDF SDK, SpendLens calls the platform's own engine on a worker thread: &lt;strong&gt;PDFBox-Android (Kotlin)&lt;/strong&gt; on Android and &lt;strong&gt;PDFKit (Swift)&lt;/strong&gt; on iOS, over a method channel. Image-only / scanned PDFs are rejected explicitly (on-device OCR is a future module) instead of pretending they parsed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A deterministic parser — no ML, no LLM.&lt;/strong&gt; This is the heart of the trust model. The generic parser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recovers multiline transaction descriptions and handles the common Indian date formats;&lt;/li&gt;
&lt;li&gt;infers debit vs credit from the direction of consecutive running balances;&lt;/li&gt;
&lt;li&gt;checks the &lt;strong&gt;opening + credits − debits = closing&lt;/strong&gt; equation, and, when the statement declares its own debit/credit totals, validates those too — in the detected column order;&lt;/li&gt;
&lt;li&gt;preserves &lt;code&gt;DR&lt;/code&gt;, leading-minus, and zero-crossing overdraft balances;&lt;/li&gt;
&lt;li&gt;and returns an explicit &lt;strong&gt;Verified&lt;/strong&gt;, &lt;strong&gt;Needs review&lt;/strong&gt;, or &lt;strong&gt;Incomplete&lt;/strong&gt; verdict with the diagnostic attached.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It never converts a parse error into a success-shaped empty dashboard. If the numbers don't reconcile, the UI says so.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Money is integer paise, never floating point.&lt;/strong&gt; Every amount is stored and summed as an integer so ₹0.01 rounding drift can't creep into a total.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explainable categorization.&lt;/strong&gt; Categories come from transparent keyword rules with &lt;strong&gt;token-boundary matching&lt;/strong&gt;, so "Zuber" doesn't get filed under the ride brand "Uber," and your own merchant corrections always override the built-ins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local persistence.&lt;/strong&gt; Statements, edited categories, merchant rules, and settings live in &lt;strong&gt;SQLite&lt;/strong&gt; (analyses stored as JSON documents for schema agility). Category-rule updates and full data deletion are atomic transactions, and concurrent first-access is serialised through a single database-open future so startup can't race.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A custom native document picker.&lt;/strong&gt; &lt;code&gt;file_picker&lt;/code&gt; didn't compile cleanly against Flutter 3.44 / AGP 9, so SpendLens ships its own Storage Access Framework / &lt;code&gt;UIDocumentPickerViewController&lt;/code&gt; picker — which also avoids asking for broad storage permissions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A zero-retention web twin.&lt;/strong&gt; The GitHub Pages version does the same parsing in the browser with &lt;strong&gt;self-hosted PDF.js&lt;/strong&gt; and a Content-Security-Policy of &lt;code&gt;connect-src 'none'&lt;/code&gt; — no cookies, local storage, IndexedDB, service worker, analytics, or upload endpoint. A static &lt;code&gt;verify_web_privacy&lt;/code&gt; check runs in CI to keep it that way, and the analysis auto-clears after 15 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verified before shipping.&lt;/strong&gt; 40 automated tests (parser, bank registry, categorization, SQLite, controller, privacy, concurrency, widgets), a clean &lt;code&gt;flutter analyze&lt;/code&gt;, the web-privacy verifier, and a scripted Android emulator smoke that imports a synthetic statement, checks reconciliation, and confirms that "delete everything" survives a relaunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;Most "see where your money goes" apps ask you to connect your bank — handing a third party your credentials, or trusting an aggregator to hold your transaction history. SpendLens is a bet that you shouldn't have to. &lt;strong&gt;Everything happens on the device you already trust with the statement.&lt;/strong&gt; No account, no analytics SDK, no remote parser, no generative-AI dependency; Android backup/transfer is disabled and the iOS database is excluded from backups.&lt;/p&gt;

&lt;p&gt;And it's built for &lt;strong&gt;Indian&lt;/strong&gt; statements specifically — the date formats, the digit grouping, and coverage across 78 banks including the small-finance and regional-rural banks that mainstream tools ignore. When a layout is unfamiliar it degrades honestly to &lt;strong&gt;Needs review&lt;/strong&gt; rather than inventing certainty.&lt;/p&gt;

&lt;p&gt;The meta-point is the one I like most: an AI agent built a finance tool whose entire value proposition is that &lt;strong&gt;AI and the network never touch your money.&lt;/strong&gt; That's a version of #AI4Good worth keeping in the toolkit — using the agent to ship privacy, not to harvest it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Live (zero-retention browser analyzer):&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/SpendLens/" rel="noopener noreferrer"&gt;naveenneog.github.io/SpendLens&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Android APK:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/SpendLens/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/SpendLens/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/SpendLens" rel="noopener noreferrer"&gt;github.com/naveenneog/SpendLens&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;
        naveenneog
      &lt;/a&gt; / &lt;a href="https://github.com/naveenneog/SpendLens" rel="noopener noreferrer"&gt;
        SpendLens
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Privacy-first Flutter app that turns Indian bank-statement PDFs into on-device spending insights - no bank login, cloud upload, analytics, or LLMs.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;SpendLens&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;SpendLens is a privacy-first Flutter app that turns Indian bank-statement PDFs into understandable spending summaries without bank logins, cloud uploads, or LLMs.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Version 0.2&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;On-device PDF text extraction and deterministic parsing&lt;/li&gt;
&lt;li&gt;Detection registry for 78 RBI scheduled retail banks&lt;/li&gt;
&lt;li&gt;Reconciliation and partial-parse diagnostics&lt;/li&gt;
&lt;li&gt;Editable categories and remembered merchant rules&lt;/li&gt;
&lt;li&gt;Category, daily-spend, and large-spend views&lt;/li&gt;
&lt;li&gt;Searchable transactions and saved statement history&lt;/li&gt;
&lt;li&gt;Adaptive Android/iOS layouts, light/dark themes, and accessible chart alternatives&lt;/li&gt;
&lt;li&gt;Native Android and iOS document pickers with no broad storage permission&lt;/li&gt;
&lt;li&gt;Password-protected PDF handling&lt;/li&gt;
&lt;li&gt;Local SQLite persistence and complete data deletion&lt;/li&gt;
&lt;li&gt;Zero-retention GitHub Pages analyzer in &lt;code&gt;docs/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Self-hosted PDF.js with a CSP that forbids authored network connections&lt;/li&gt;
&lt;li&gt;Explicit browser-memory clear and 15-minute inactivity clearing&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Development&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Flutter is installed at &lt;code&gt;C:\Users\navg\flutter&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight highlight-source-powershell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-k"&gt;&amp;amp;&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;C:\Users\navg\flutter\bin\flutter.bat&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; pub get
&lt;span class="pl-k"&gt;&amp;amp;&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;C:\Users\navg\flutter\bin\flutter.bat&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; analyze
&lt;span class="pl-k"&gt;&amp;amp;&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;C:\Users\navg\flutter\bin\flutter.bat&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; test
&lt;span class="pl-k"&gt;&amp;amp;&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;C:\Users\navg\flutter\bin\flutter.bat&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; run&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;The current sideload artifact is:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;artifacts\SpendLens-v0.2.0.apk&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;It follows…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/naveenneog/SpendLens" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;em&gt;Part of the &lt;a href="https://naveenneog.github.io/AI4Good/2026/07/10/ai4good-an-app-a-day/" rel="noopener noreferrer"&gt;#AI4Good&lt;/a&gt; series — one build at a time, from a single laptop, with the "who does this help?" chosen first. &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/naveen-gopalakrishna-99863040/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;. #AI4Good&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>flutter</category>
      <category>privacy</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Overdrive: An Open-Source Skill That Makes Your Coding Agent Stop Settling for Correct</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Wed, 29 Jul 2026 08:51:47 +0000</pubDate>
      <link>https://dev.to/naveenneog/overdrive-an-open-source-skill-that-makes-your-coding-agent-stop-settling-for-correct-1kdl</link>
      <guid>https://dev.to/naveenneog/overdrive-an-open-source-skill-that-makes-your-coding-agent-stop-settling-for-correct-1kdl</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/naveenneog/overdrive" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fon59z1kbi982tb37f42z.png" alt="Overdrive — a capability amplifier for coding agents, installed with a single npx command" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Most of the gap between a mediocre answer and an exceptional one from the same model isn't capability. It's &lt;strong&gt;standards&lt;/strong&gt;. Overdrive is the standards, written down and installed.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx github:naveenneog/overdrive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm &lt;strong&gt;&lt;a href="https://naveenneog.github.io/AI4Good/about/" rel="noopener noreferrer"&gt;Naveen Gopalakrishna&lt;/a&gt;&lt;/strong&gt; — an AI Global Black Belt at Microsoft, a compulsive builder after hours. This #AI4Good release is a tool rather than an app: an open-source &lt;strong&gt;agent skill&lt;/strong&gt; that changes how hard your coding agent works.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💻 &lt;strong&gt;Source (MIT):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/overdrive" rel="noopener noreferrer"&gt;github.com/naveenneog/overdrive&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Install:&lt;/strong&gt; &lt;code&gt;npx github:naveenneog/overdrive&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;🤖 &lt;strong&gt;Works with:&lt;/strong&gt; GitHub Copilot CLI · Claude Code&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where it came from
&lt;/h2&gt;

&lt;p&gt;I'd collected a handful of those "show me the absolute ceiling of what you can do" prompts that get used to evaluate frontier models against each other. Five of them, in five completely unrelated domains:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Advanced web design&lt;/li&gt;
&lt;li&gt;Real-time 3D&lt;/li&gt;
&lt;li&gt;Motion graphics&lt;/li&gt;
&lt;li&gt;A playable game&lt;/li&gt;
&lt;li&gt;A private-wealth investment deck&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A deck and a shader have nothing in common. But reading the five side by side, the &lt;em&gt;shape&lt;/em&gt; of them was &lt;em&gt;identical&lt;/em&gt;. Every one of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;framed &lt;strong&gt;the ceiling&lt;/strong&gt;, not the requirement — "the best thing you can produce", explicitly compared against other models, explicitly credited to you by name&lt;/li&gt;
&lt;li&gt;told the model &lt;strong&gt;to decide and commit&lt;/strong&gt; rather than ask which direction you'd prefer, and said the ambition of that choice was itself part of the score&lt;/li&gt;
&lt;li&gt;named the &lt;strong&gt;boring, obvious, median answer&lt;/strong&gt; up front so it could be ruled out&lt;/li&gt;
&lt;li&gt;demanded &lt;strong&gt;research&lt;/strong&gt;, not memory&lt;/li&gt;
&lt;li&gt;offered a &lt;strong&gt;non-exhaustive buffet&lt;/strong&gt; of advanced techniques and asked for several, integrated&lt;/li&gt;
&lt;li&gt;held &lt;strong&gt;one dominant success criterion&lt;/strong&gt; above everything else — fun in thirty seconds; timing; 60fps; taste; defensibility&lt;/li&gt;
&lt;li&gt;required &lt;strong&gt;original assets&lt;/strong&gt; rather than borrowed ones&lt;/li&gt;
&lt;li&gt;ended with &lt;em&gt;"QA it by actually using it"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's not five prompts. That's one prompt wearing five costumes. So I extracted the skeleton and made it a skill.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it actually is
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Overdrive is a judgement layer, not a domain skill.&lt;/strong&gt; It doesn't know how to build a game or write a deck. It changes &lt;em&gt;how hard and how well&lt;/em&gt; the agent attacks whatever it was already going to do.&lt;/p&gt;

&lt;p&gt;Ten rules override the agent's normal defaults when it's on. The two that change the most behaviour:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Decide and commit — never ask.&lt;/strong&gt; Where the task leaves a choice open — concept, subject, genre, stack, angle — &lt;em&gt;you choose&lt;/em&gt;. The ambition of that choice is part of the score. Asking "which would you like?" is a wasted turn.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;QA by actually using it.&lt;/strong&gt; Run it, play it, scroll it, watch it, read it end to end, stress it. An unrun deliverable is an unfinished deliverable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The rest: frame the ceiling, name the median answer and refuse it, research instead of recalling, stack techniques rather than bolting one on, hold one dominant criterion, generate rather than borrow, survive inspection past the first screen, and red-team your own work then &lt;em&gt;fix what the attack found&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Seven phases
&lt;/h3&gt;

&lt;p&gt;The skill runs the work through: &lt;strong&gt;set the bar&lt;/strong&gt; (write &lt;code&gt;DOMAIN / CEILING / DOMINANT&lt;/code&gt; in three lines) → &lt;strong&gt;commit to a concept&lt;/strong&gt; in one sentence → &lt;strong&gt;research&lt;/strong&gt; → &lt;strong&gt;choose amplifiers&lt;/strong&gt; → &lt;strong&gt;build with the dominant criterion in front&lt;/strong&gt; → &lt;strong&gt;red-team&lt;/strong&gt; → &lt;strong&gt;QA for real&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  A rubric it has to report
&lt;/h3&gt;

&lt;p&gt;Before declaring done, the agent scores itself 1–5 and &lt;strong&gt;shows you the scores&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;Dimension&lt;/th&gt;
&lt;th&gt;5 =&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ambition of the choice&lt;/td&gt;
&lt;td&gt;The concept is impressive before a line is written&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dominant criterion&lt;/td&gt;
&lt;td&gt;The one thing that matters is genuinely nailed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technical depth&lt;/td&gt;
&lt;td&gt;Multiple advanced techniques, composed — not bolted on&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Craft / finish&lt;/td&gt;
&lt;td&gt;Nothing default; deliberate typography, timing, naming, structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Coherence&lt;/td&gt;
&lt;td&gt;One clear point of view carried through every part&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Originality&lt;/td&gt;
&lt;td&gt;Assets, analysis and structure are yours, not borrowed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Robustness&lt;/td&gt;
&lt;td&gt;Runs clean: no errors, no stutter, no overflow, no dead link&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Honesty&lt;/td&gt;
&lt;td&gt;Claims are sourced; estimates are labelled; limits are stated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Anything scoring ≤3 gets another pass. Scoring in the open is a forcing function — it's uncomfortable to hand someone a 3 you've written down yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's in the box
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The contract, the seven phases, the rubric, the failure modes, and when &lt;em&gt;not&lt;/em&gt; to use it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/amplifiers.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Nine domain packs — web, real-time 3D, motion graphics, games, audio, research &amp;amp; decks, systems, data viz, writing. Each with its dominant criterion, technique buffet, library shortlist and QA moves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/red-team.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Five questions, an inversion pass, a contrast pass, how to delegate the red team to a sub-agent, and the bar for what must get fixed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/qa-protocol.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-domain "actually use it" QA, a browser harness, and the ship checklist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/research-protocol.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;What always needs looking up, sourcing rules, and how to label an estimate as an estimate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;references/prompt-templates.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Paste-ready Overdrive prompts for when &lt;em&gt;you&lt;/em&gt; are the one prompting a model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scripts/qa_web.mjs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Playwright harness — console/page errors, failed requests, horizontal overflow per viewport, FPS sampling, screenshots&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Installing it
&lt;/h2&gt;

&lt;p&gt;One command, zero runtime dependencies — the installer is plain Node and copies seven files:&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;# everywhere it finds an agent&lt;/span&gt;
npx github:naveenneog/overdrive

&lt;span class="c"&gt;# or pick one&lt;/span&gt;
npx github:naveenneog/overdrive &lt;span class="nt"&gt;--claude&lt;/span&gt;
npx github:naveenneog/overdrive &lt;span class="nt"&gt;--copilot&lt;/span&gt;

&lt;span class="c"&gt;# per-repo instead of your home directory&lt;/span&gt;
npx github:naveenneog/overdrive &lt;span class="nt"&gt;--project&lt;/span&gt;

&lt;span class="c"&gt;# what's detected, what's already installed&lt;/span&gt;
npx github:naveenneog/overdrive &lt;span class="nt"&gt;--list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It lands in &lt;code&gt;~/.copilot/skills/overdrive&lt;/code&gt; and &lt;code&gt;~/.claude/skills/overdrive&lt;/code&gt;. There's &lt;code&gt;--force&lt;/code&gt;, &lt;code&gt;--dry-run&lt;/code&gt;, &lt;code&gt;--dir &amp;lt;path&amp;gt;&lt;/code&gt; and &lt;code&gt;--uninstall&lt;/code&gt; too.&lt;/p&gt;

&lt;p&gt;Then start a &lt;strong&gt;new&lt;/strong&gt; agent session and say &lt;strong&gt;"overdrive"&lt;/strong&gt; — or &lt;em&gt;"go all out"&lt;/em&gt;, &lt;em&gt;"show me the ceiling"&lt;/em&gt;, &lt;em&gt;"don't hold back"&lt;/em&gt;, &lt;em&gt;"make it a portfolio piece"&lt;/em&gt;, &lt;em&gt;"this is going public"&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Does it actually do anything?
&lt;/h2&gt;

&lt;p&gt;Fair question, and the honest answer is that a prompt-shaped tool is easy to fool yourself about. So here's the concrete case.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://naveenneog.github.io/Antikythera/" rel="noopener noreferrer"&gt;&lt;strong&gt;ANTIKYTHERA&lt;/strong&gt;&lt;/a&gt; title sequence — &lt;a href="https://naveenneog.github.io/AI4Good/2026/07/26/antikythera-generative-title-sequence/" rel="noopener noreferrer"&gt;written up here&lt;/a&gt; — was built under Overdrive from a single prompt. A 29-second documentary title sequence in one self-contained HTML file: procedural bronze gear geometry at authentic Antikythera tooth counts, a hand-written post-processing chain, a fully synthesised Web Audio score, no assets of any kind.&lt;/p&gt;

&lt;p&gt;But the output isn't the interesting part. &lt;strong&gt;Phase 7 is.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;QA by actually using it&lt;/em&gt; is trivial for a human and structurally impossible for an agent — it can't watch a film. So it built itself an eye: installed Playwright, exposed a transport API from the piece, captured 90 frames across the runtime, assembled them into a contact sheet, and looked at the whole edit as one image.&lt;/p&gt;

&lt;p&gt;That caught seven defects that reading the code never would have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a wrapper &lt;code&gt;div&lt;/code&gt;'s &lt;code&gt;z-index&lt;/code&gt; created a stacking context that &lt;strong&gt;isolated &lt;code&gt;mix-blend-mode&lt;/code&gt; from the canvas&lt;/strong&gt; — film grain rendered as raw grey static over everything&lt;/li&gt;
&lt;li&gt;GSAP read a CSS &lt;code&gt;translateY(112%)&lt;/code&gt; back as resolved &lt;em&gt;pixels&lt;/em&gt; into its &lt;code&gt;y&lt;/code&gt; cache, so the later &lt;code&gt;yPercent&lt;/code&gt; tween animated a different axis and &lt;strong&gt;the title never appeared&lt;/strong&gt; — while the tween reported &lt;code&gt;progress: 1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the eclipsing moon's circles sat at SVG origin instead of the orrery centre, so &lt;strong&gt;totality never happened&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;the title wrapped onto two lines mid-animation&lt;/li&gt;
&lt;li&gt;flash frames held 0.35s and read as an error rather than a cut&lt;/li&gt;
&lt;li&gt;the corona quad's square edge was visible against the starfield&lt;/li&gt;
&lt;li&gt;point-light intensities were in physical units on a renderer defaulting to legacy lighting, blowing an entire act to white&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of those is invisible in source and obvious in a screenshot. Without the "actually use it" clause, that piece ships broken and &lt;em&gt;looks&lt;/em&gt; finished in the diff.&lt;/p&gt;

&lt;p&gt;That's the mechanism. Not better code generation — &lt;strong&gt;better standards, and a refusal to declare done without evidence.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  When not to use it
&lt;/h2&gt;

&lt;p&gt;The skill says this itself, which I think matters more than the feature list. Overdrive costs time and tokens, and it explicitly tells the agent &lt;strong&gt;not&lt;/strong&gt; to apply it to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;trivial mechanical edits — renames, version bumps, typos&lt;/li&gt;
&lt;li&gt;anything you've said you want quick, minimal or throwaway&lt;/li&gt;
&lt;li&gt;hotfixes under time pressure, where correctness and reversibility beat ambition&lt;/li&gt;
&lt;li&gt;work with a hard external spec — an API contract, a compliance form — where invention is the enemy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A capability amplifier that fires on everything is just an expensive way to over-engineer a typo fix.&lt;/p&gt;




&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;Skills are the most underrated part of the current agent tooling, and they're just &lt;strong&gt;markdown in a folder&lt;/strong&gt;. No SDK, no plugin API, no build. Which means the thing that separates a good agent session from a great one is largely &lt;em&gt;writing down what "great" means&lt;/em&gt; — and that's a skill anyone can author, read, disagree with, and fork.&lt;/p&gt;

&lt;p&gt;Overdrive is MIT. If your standards differ from mine, rewrite the rubric — that's the point. The interesting artifact isn't my eight dimensions. It's that eight dimensions written down at all beats infinite capability with no bar.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;⚡ &lt;strong&gt;Install:&lt;/strong&gt; &lt;code&gt;npx github:naveenneog/overdrive&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source (MIT):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/overdrive" rel="noopener noreferrer"&gt;github.com/naveenneog/overdrive&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🎬 &lt;strong&gt;Built with it:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/Antikythera/" rel="noopener noreferrer"&gt;ANTIKYTHERA&lt;/a&gt; · &lt;a href="https://naveenneog.github.io/AI4Good/2026/07/26/antikythera-generative-title-sequence/" rel="noopener noreferrer"&gt;the write-up&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>devtools</category>
    </item>
    <item>
      <title>ANTIKYTHERA: A 29-Second Title Sequence in One HTML File — No Assets, No Build Step</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Sun, 26 Jul 2026 14:56:43 +0000</pubDate>
      <link>https://dev.to/naveenneog/antikythera-a-29-second-title-sequence-in-one-html-file-no-assets-no-build-step-4fj8</link>
      <guid>https://dev.to/naveenneog/antikythera-a-29-second-title-sequence-in-one-html-file-no-assets-no-build-step-4fj8</guid>
      <description>&lt;p&gt;&lt;a href="https://naveenneog.github.io/Antikythera/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fusfhweuhrnjr18pnvlxf.png" alt="ANTIKYTHERA — four frames from the title sequence: the bronze gear train, the X-ray scan, the total eclipse, and the title card"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One prompt. No assets. No build step. No server. One &lt;code&gt;index.html&lt;/code&gt; you can open by double-clicking it — and 29 seconds of motion design comes out.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm &lt;strong&gt;&lt;a href="https://naveenneog.github.io/AI4Good/about/" rel="noopener noreferrer"&gt;Naveen Gopalakrishna&lt;/a&gt;&lt;/strong&gt; — an AI Global Black Belt at Microsoft, a compulsive builder after hours. This #AI4Good build is a little different from the app-a-day entries: it isn't an app at all. It's a &lt;strong&gt;film&lt;/strong&gt;. And it's the most interesting capability test I've run at an AI coding agent so far.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Watch it:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/Antikythera/" rel="noopener noreferrer"&gt;naveenneog.github.io/Antikythera&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source (MIT):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/Antikythera" rel="noopener noreferrer"&gt;github.com/naveenneog/Antikythera&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The prompt
&lt;/h2&gt;

&lt;p&gt;This is the whole brief, verbatim. Nothing else was specified — not the subject, not the concept, not the look:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build one motion graphics piece that demonstrates the absolute ceiling of your
motion design capability. You choose the subject and the concept. That decision
is part of what's being evaluated.

Context you should know: this is going into a video comparing you against other
frontier models, shown to a large audience, and your work will be credited to you
by name. Every model gets this exact prompt. So go nuts and show people what
you're actually capable of.

Requirements:

1. You pick it. A title sequence, a product spot, a data story, an explainer, a
   brand identity in motion, a piece of pure abstract choreography — anything.
   Decide and commit. The ambition of the choice is part of the score.

2. It plays automatically on load, runs roughly 15-30 seconds, and holds on a
   final frame. Add a replay control. It must read as a finished, edited piece —
   not a looping animation demo.

3. Choreography is the whole test. Pull from whatever best showcases you —
   including but not limited to: kinetic typography and per-character animation,
   variable font animation, staggered reveal systems, easing with real authored
   weight, overshoot and anticipation, masking and reveal wipes, SVG path drawing
   and morphing, shape transitions, mix-blend-mode compositing, particle and
   physics-driven motion, 3D layers and camera moves, depth and parallax,
   shader-based transitions, displacement and distortion, grain and optical
   treatment, match cuts between scenes, sound-design-driven timing.

4. You have free reign to look up how to use any of these, and to pull in open
   source animation libraries freely — GSAP, anime.js, Three.js, Lottie, shader
   libraries, anything on a CDN. Add any other technique you think demonstrates
   more. The list is a starting point, not a boundary.

5. Timing is what separates real motion design from animation. Beats should land
   with intent, moments should breathe, and the piece should build. If everything
   moves at the same speed with the same easing, it has failed.

6. Single self-contained index.html. Any library via CDN. No build step, no server,
   no API keys.

7. No hotlinked video or stock footage. Everything generated in-browser.

8. QA it by watching it. Play it start to finish several times. Check that beats
   don't collide, nothing pops in unstyled, timing reads, and it holds 60fps
   throughout.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note requirement 1: &lt;strong&gt;the choice of subject is part of the score.&lt;/strong&gt; That's the genuinely interesting part of the test. Anyone can animate a logo. Picking &lt;em&gt;what deserves 29 seconds&lt;/em&gt; is a design judgement.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the Antikythera mechanism
&lt;/h2&gt;

&lt;p&gt;Copilot picked the &lt;strong&gt;Antikythera mechanism&lt;/strong&gt; — the bronze geared analog computer built around 150 BCE, lost in a shipwreck off the island of Antikythera around 60 BCE, and dredged up by sponge divers in 1901. It predicted eclipses, tracked the Metonic and Saros cycles, and modelled the motion of the sun and moon. Nothing of comparable mechanical complexity appears again in the archaeological record for &lt;strong&gt;over a thousand years&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The reasoning it gave for the choice is the bit I actually care about, because it's a &lt;em&gt;motion design&lt;/em&gt; argument rather than a "cool topic" argument:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It hands you two opposing motion vocabularies to choreograph against each other.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Register&lt;/th&gt;
&lt;th&gt;Behaviour&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Organic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;slow, sine-driven drift, long eases, nothing ever stops hard&lt;/td&gt;
&lt;td&gt;the sea floor&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mechanical&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;quantised, staccato, hard &lt;code&gt;expo&lt;/code&gt; landings, overshoot and settle&lt;/td&gt;
&lt;td&gt;the machine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The edit then cuts &lt;em&gt;between&lt;/em&gt; those two registers. The subject isn't decoration — it generates the grammar of the piece. That's the difference between choosing a topic and choosing a concept.&lt;/p&gt;




&lt;h2&gt;
  
  
  The edit
&lt;/h2&gt;

&lt;p&gt;Five acts, ~29 seconds, then it holds on the final frame.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;t&lt;/th&gt;
&lt;th&gt;Act&lt;/th&gt;
&lt;th&gt;What happens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;0.0s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Dark&lt;/td&gt;
&lt;td&gt;The frame closes to 2.39:1. Marine snow drifts through three parallax depth bands, light shafts sway, coordinates type on in the corner.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3.0s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;A raking light travels across frame and &lt;strong&gt;uncovers&lt;/strong&gt; a corroded bronze fragment — the light and the reveal mask are driven by the same value, so the light is physically the thing revealing the object.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;5.55s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Find&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Hard cut.&lt;/strong&gt; Register change to cold cyan. Corner reticles snap in with overshoot, data readouts stagger, a counter races to 82.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;6.5s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;A scan bar crosses the artifact and the gear line-art draws in &lt;strong&gt;exactly where the bar has already passed&lt;/strong&gt;. Greek inscription glyphs flicker on around the rim in random order.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;9.45s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Machine&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Hard cut.&lt;/strong&gt; Eight bronze gears fly forward out of the dark and land hard. A beat of anticipation — a tiny backwards creep — and the train starts to turn.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;11.2s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Four cycle cards cut over the mechanism: &lt;strong&gt;METONIC · SAROS · CALLIPPIC · EXELIGMOS&lt;/strong&gt;, each with a &lt;em&gt;different&lt;/em&gt; entrance, each with its number counting up.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;15.7s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;Everything accelerates, then &lt;strong&gt;slams to a dead stop&lt;/strong&gt; with a prismatic radial smear.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;16.5s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(silence)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cut to black. 0.36 seconds of nothing.&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;16.85s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The Sky&lt;/td&gt;
&lt;td&gt;The great gear's rim becomes the outer orbit. An orrery runs at real relative periods.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;20.0s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;The moon crosses the sun. Time decelerates. The light goes out, the stars come up — as they really do at totality.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;22.0s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Totality.&lt;/strong&gt; The piece stops moving for about a second.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;23.85s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Title&lt;/td&gt;
&lt;td&gt;The corona ring is squashed on one axis until it &lt;em&gt;is&lt;/em&gt; the rule under the title. &lt;strong&gt;ANTIKYTHERA&lt;/strong&gt; reveals per character.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspjgfy89rkbcqozfgtyl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fspjgfy89rkbcqozfgtyl.png" alt="The raking light uncovering the bronze fragment on the sea floor"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbz16qfm4l9f6qi9r9aur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbz16qfm4l9f6qi9r9aur.png" alt="The X-ray scan revealing the gear line-art inside the fragment"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Three match cuts carry the structure
&lt;/h3&gt;

&lt;p&gt;This is the part I'd point at if someone asked what "editing" means in a piece with no footage.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The artifact becomes its own X-ray.&lt;/strong&gt; Same SVG path, same position, same size — only the render register changes. The eye reads continuity, the brain reads &lt;em&gt;a new way of seeing the same object&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The great gear's rim becomes the outer orbit.&lt;/strong&gt; The camera lands at exactly &lt;code&gt;z = 43.7&lt;/code&gt; so the 223-tooth wheel's rim measures ~298px on screen; the outer orbit circle is 300px. For most of a second &lt;em&gt;both are on screen at once&lt;/em&gt; while the machine dissolves out from under the circle. The machine doesn't cut to the sky — it &lt;strong&gt;becomes&lt;/strong&gt; the sky.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The corona ring becomes the title rule.&lt;/strong&gt; One property: &lt;code&gt;scaleY → 0.0035&lt;/code&gt;. A circle with no height is a line. That line then slides down and is the rule under the title.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One circle, three meanings, across three acts.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the graphics were made
&lt;/h2&gt;

&lt;p&gt;Nothing is loaded. No image files, no video, no models, no HDRIs, no textures. Here's what that actually required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gear geometry from real tooth counts
&lt;/h3&gt;

&lt;p&gt;The gears aren't decorative cog shapes. Each is a &lt;code&gt;THREE.Shape&lt;/code&gt; whose outline is generated tooth by tooth, then extruded with &lt;code&gt;THREE.ExtrudeGeometry&lt;/code&gt; and bevelled. The tooth counts are the &lt;strong&gt;real surviving Antikythera counts&lt;/strong&gt; — 223 (the Saros wheel), 127 (Metonic), 64, 53, 48, 38, 32, 27 — laid out on a &lt;strong&gt;shared module&lt;/strong&gt;, so every pitch radius is &lt;code&gt;m · teeth / 2&lt;/code&gt; and the gears genuinely mesh when placed tangentially:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;GEAR_SPEC&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="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;223&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;z&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;th&lt;/span&gt;&lt;span class="p"&gt;:.&lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;// the great Saros wheel&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;z&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;th&lt;/span&gt;&lt;span class="p"&gt;:.&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;   &lt;span class="c1"&gt;// Metonic, on a layer behind&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;z&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;th&lt;/span&gt;&lt;span class="p"&gt;:.&lt;/span&gt;&lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;53&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;z&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.98&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;th&lt;/span&gt;&lt;span class="p"&gt;:.&lt;/span&gt;&lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;arms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;3&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="c1"&gt;// meshing distance is just the sum of the two pitch radii&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rP&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;rP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;px&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mesh&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Angular velocity is then &lt;code&gt;ω · (223 / teeth)&lt;/code&gt; with alternating direction — so the train turns at &lt;strong&gt;true ratios&lt;/strong&gt;. The little 27-tooth gear whirs; the great wheel barely creeps. You can feel the gearing without being told about it. Spoke cutouts are &lt;code&gt;THREE.Path&lt;/code&gt; holes punched into the same shape, which is why you can see the Metonic wheel turning &lt;em&gt;through&lt;/em&gt; the Saros wheel behind it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw6maaidf7gmropkv2uwg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw6maaidf7gmropkv2uwg.png" alt="The full mechanism — the 223-tooth Saros wheel with the 127-tooth Metonic wheel layered behind it"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The bronze surface, computed in JavaScript
&lt;/h3&gt;

&lt;p&gt;There's no texture file, so the corrosion is a &lt;strong&gt;value-noise heightfield&lt;/strong&gt; evaluated in JS at load:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An integer hash → smooth-interpolated value noise → 5-octave fBm heightfield.&lt;/li&gt;
&lt;li&gt;Screen-space normals taken from the heightfield's neighbours.&lt;/li&gt;
&lt;li&gt;A Lambert term against a fixed key direction, plus a tight specular power.&lt;/li&gt;
&lt;li&gt;A second low-frequency noise field distributes &lt;strong&gt;verdigris&lt;/strong&gt; patches (bronze → green patina).&lt;/li&gt;
&lt;li&gt;A third, higher-frequency field carves &lt;strong&gt;pits&lt;/strong&gt;, which also self-shadow — that's what sells the relief rather than making it look like printed noise.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's rasterised once into three canvases and reused three ways: as an &lt;strong&gt;SVG &lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt; fill&lt;/strong&gt; for the artifact, and as the &lt;strong&gt;&lt;code&gt;map&lt;/code&gt; + &lt;code&gt;roughnessMap&lt;/code&gt; + &lt;code&gt;bumpMap&lt;/code&gt;&lt;/strong&gt; on the gear material. One noise pass, one look, two rendering technologies.&lt;/p&gt;

&lt;h3&gt;
  
  
  An environment map with nothing to load
&lt;/h3&gt;

&lt;p&gt;Metal needs reflections, and hotlinking an HDRI was off the table. So the environment is &lt;em&gt;painted&lt;/em&gt;: a canvas gradient with a warm key blob and a cold cyan kicker, marked as an equirectangular texture and run through &lt;code&gt;PMREMGenerator&lt;/code&gt;. That's where the bronze's specular life comes from.&lt;/p&gt;

&lt;h3&gt;
  
  
  A hand-written post-processing chain
&lt;/h3&gt;

&lt;p&gt;No &lt;code&gt;EffectComposer&lt;/code&gt; — the whole chain is written out, which keeps the cost controllable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scene → half-float render target
      → bright pass (threshold 1.15)
      → two-scale separable gaussian bloom (¼ and 1/14 res, ping-pong)
      → one composite pass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single composite pass does &lt;strong&gt;barrel distortion, prismatic radial smear, chromatic aberration, colour grade, ACES tonemap, sRGB encode and vignette&lt;/strong&gt;. The expensive smear/aberration path — 8 taps × 3 channels — sits behind a &lt;em&gt;uniform&lt;/em&gt; branch, so it costs nothing except on the two or three beats that actually use it (the slam, the cuts).&lt;/p&gt;

&lt;h3&gt;
  
  
  Optical treatment: grain and gate weave
&lt;/h3&gt;

&lt;p&gt;Two details that do a disproportionate amount of work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Film grain&lt;/strong&gt; is eight pre-baked frames of &lt;em&gt;gaussian&lt;/em&gt; noise (three uniforms averaged — flat &lt;code&gt;Math.random()&lt;/code&gt; reads as video noise, not film) cycled at &lt;strong&gt;24fps, not 60&lt;/strong&gt;, with a randomised offset each swap, composited in &lt;code&gt;mix-blend-mode: overlay&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gate weave&lt;/strong&gt; — sub-pixel translation and a fraction of a degree of rotation applied to the whole frame, because projected film never sits perfectly still. It's almost subliminal and the piece feels dead without it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Three layers, one frame
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#gl    &amp;lt;canvas&amp;gt;  Three.js — particles, gears, starfield, corona, post chain
#vec   &amp;lt;svg&amp;gt;     line art — artifact, X-ray gears, orbits, the title rule
#type  &amp;lt;div&amp;gt;     DOM      — all typography
       grain / scanlines / halation / vignette / flash / letterbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typography deliberately stays in the &lt;strong&gt;DOM&lt;/strong&gt;: real font rendering, real variable-font axes, and it stays razor sharp regardless of the WebGL render scale. The title animates &lt;code&gt;font-variation-settings&lt;/code&gt; from &lt;code&gt;'wght' 900&lt;/code&gt; down to &lt;code&gt;420&lt;/code&gt; as the letters land — the letterforms visibly &lt;em&gt;cool&lt;/em&gt; from black to book weight while tracking in. That's a variable font doing something a static font simply cannot.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnrvmkp11i2rc9embqi08.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnrvmkp11i2rc9embqi08.png" alt="The final title card — Bodoni Moda tracking in over the rule, with a gear still turning behind"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How the audio was made
&lt;/h2&gt;

&lt;p&gt;Also no files. Every sound is synthesised with the &lt;strong&gt;Web Audio API&lt;/strong&gt; at runtime.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cue&lt;/th&gt;
&lt;th&gt;How it's built&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Sub drone&lt;/strong&gt; (the bed)&lt;/td&gt;
&lt;td&gt;Two detuned sawtooth oscillators at 40 / 40.35 Hz plus two sines, through a lowpass whose cutoff sweeps 110 → 340 → 150 Hz across the whole piece. A 0.09 Hz LFO on the gain gives it a slow tape-like breathing.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Impacts&lt;/strong&gt; (the cuts)&lt;/td&gt;
&lt;td&gt;A sine with a pitch envelope from 150 Hz down to 32 Hz in 0.34s, plus a band-passed noise transient for the click.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gear ticks&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;50ms noise bursts through a narrow band-pass (Q = 9) at 2.4 kHz, pitch-scaled per gear so the eight gears landing sound like eight &lt;em&gt;different&lt;/em&gt; pieces of metal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data blips&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Short square-wave pings through a lowpass, one per readout row.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Riser&lt;/strong&gt; (into the slam)&lt;/td&gt;
&lt;td&gt;White noise through a band-pass sweeping 300 Hz → 7 kHz exponentially, doubled by a sawtooth sweeping 70 → 430 Hz.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Celestial pad&lt;/strong&gt; (the sky)&lt;/td&gt;
&lt;td&gt;Five sine partials on a minor voicing, each doubled and detuned by 0.4%, with a long 1.9s fade in.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Reverse swell&lt;/strong&gt; (into the title)&lt;/td&gt;
&lt;td&gt;Band-passed noise with an &lt;em&gt;exponentially rising&lt;/em&gt; gain envelope, so it sounds like a reversed cymbal.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Reverb is a &lt;strong&gt;synthesised impulse response&lt;/strong&gt; — 3.4 seconds of stereo noise shaped by an exponential decay curve, fed into a &lt;code&gt;ConvolverNode&lt;/code&gt;. A plate reverb with no plate and no sample.&lt;/p&gt;

&lt;p&gt;The important part isn't the synthesis, though. It's that &lt;strong&gt;every cue is fired from the same GSAP timeline that drives the picture&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;SND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;A3&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.050&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// the impact IS the cut&lt;/span&gt;
&lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;SND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;riser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;A3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;5.30&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// riser into the slam&lt;/span&gt;
&lt;span class="nx"&gt;GEARS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;SND&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.55&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mf"&gt;0.16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// per gear landing&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sound and picture can't drift, because there's only one clock. Sound is &lt;strong&gt;off by default&lt;/strong&gt;, and the piece says so rather than hiding it: a speaker icon sits in the corner from the first frame, reading &lt;em&gt;turn on sound — headphones recommended&lt;/em&gt;. That click is the gesture that unlocks the &lt;code&gt;AudioContext&lt;/code&gt;, so no autoplay policy is ever fought. Turn it on part-way through and the drone &lt;strong&gt;joins at the point in its envelope the picture has already reached&lt;/strong&gt;, rather than restarting its five-second fade underneath Act III. The invitation steps back to a bare toggle after about eleven seconds, so it isn't sitting over the film for all 29 of them.&lt;/p&gt;

&lt;p&gt;It's still designed to read perfectly &lt;strong&gt;silent&lt;/strong&gt; — the sound is a layer on top of timing that already works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Timing: the actual test
&lt;/h2&gt;

&lt;p&gt;Requirement 5 said it plainly: &lt;em&gt;if everything moves at the same speed with the same easing, it has failed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;nothing in the piece uses a default ease.&lt;/strong&gt; There are ten hand-authored &lt;code&gt;CustomEase&lt;/code&gt; bezier curves, each with a job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;CE&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cinema&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;M0,0 C0.62,0 0.05,1 1,1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;        &lt;span class="c1"&gt;// long filmic settle&lt;/span&gt;
&lt;span class="nx"&gt;CE&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;slam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;   &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;M0,0 C0.04,0.92 0.09,1 1,1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// hard mechanical stop&lt;/span&gt;
&lt;span class="nx"&gt;CE&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;settle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;M0,0 C0.12,0.86 0.18,1.045 1,1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// land + micro overshoot&lt;/span&gt;
&lt;span class="nx"&gt;CE&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;punch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;M0,0 C0.02,1.18 0.28,0.99 1,1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// big overshoot&lt;/span&gt;
&lt;span class="nx"&gt;CE&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;antic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;M0,0 C0.34,-0.30 0.18,1 1,1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// pull back, then go&lt;/span&gt;
&lt;span class="nx"&gt;CE&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;choke&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;M0,0 C0.86,0.02 0.98,0.62 1,1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// slow start, late rush&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;antic&lt;/code&gt; is the one I like best: it dips &lt;em&gt;below zero&lt;/em&gt; before moving, so when the gear train starts turning it creeps backwards a few frames first. Classic animation anticipation, expressed as a bezier control point at &lt;code&gt;y = -0.30&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And the single most effective beat in the whole piece costs nothing at all: after the mechanism slams to a stop, there is a &lt;strong&gt;hard cut to black and 0.36 seconds of absolutely nothing&lt;/strong&gt; before the sky fades up. Silence and stillness, used as a beat. That's an editing decision, not an animation one.&lt;/p&gt;

&lt;p&gt;Everything is placed on one &lt;code&gt;gsap.timeline()&lt;/code&gt; at &lt;strong&gt;absolute&lt;/strong&gt; times, so cut points are authored rather than accumulated. GSAP writes to a plain state object; the renderer only ever &lt;em&gt;reads&lt;/em&gt; it. Picture logic and draw logic never touch each other.&lt;/p&gt;




&lt;h2&gt;
  
  
  How it was actually built — and what it says about capability
&lt;/h2&gt;

&lt;p&gt;The whole thing was built in a single session with &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt; in autopilot mode. The genuinely notable part wasn't the code generation. It was requirement 8: &lt;em&gt;QA it by watching it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An agent can't watch. So it built itself an eye:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Installed Playwright, exposed a small transport API (&lt;code&gt;SEQ.seek()&lt;/code&gt;, &lt;code&gt;SEQ.play()&lt;/code&gt;) from the piece.&lt;/li&gt;
&lt;li&gt;Captured &lt;strong&gt;90 frames&lt;/strong&gt; across the runtime — every 0.5s, plus extra samples clustered on the cut points.&lt;/li&gt;
&lt;li&gt;Assembled them into a &lt;strong&gt;contact sheet&lt;/strong&gt; and looked at the whole edit as one image.&lt;/li&gt;
&lt;li&gt;Measured frame pacing across a full real-time run (p50/p95/p99).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That loop caught seven real defects that reading the code would never have surfaced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bug&lt;/th&gt;
&lt;th&gt;Why it mattered&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A wrapper &lt;code&gt;div&lt;/code&gt;'s &lt;code&gt;z-index&lt;/code&gt; created a stacking context that &lt;strong&gt;isolated &lt;code&gt;mix-blend-mode&lt;/code&gt; from the canvas&lt;/strong&gt; — the film grain rendered as raw grey static over everything.&lt;/td&gt;
&lt;td&gt;Structurally invisible in code. Obvious in one screenshot.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GSAP read a CSS &lt;code&gt;transform: translateY(112%)&lt;/code&gt; back as &lt;strong&gt;resolved pixels&lt;/strong&gt; into its &lt;code&gt;y&lt;/code&gt; cache, so the later &lt;code&gt;yPercent&lt;/code&gt; tween animated a &lt;em&gt;different axis&lt;/em&gt; — &lt;strong&gt;the title never appeared at all.&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;The tween existed, was correctly positioned, and reported &lt;code&gt;progress: 1&lt;/code&gt;. Only the picture showed it was wrong.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The eclipsing moon's circles sat at SVG origin instead of the orrery centre — it flew off-screen and &lt;strong&gt;totality never happened&lt;/strong&gt;.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The title &lt;strong&gt;wrapped onto two lines&lt;/strong&gt; mid-track-in.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flash frames held ~0.35s — read as an error, not a cut. Now 2–3 frames.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The corona quad's &lt;strong&gt;square edge was visible&lt;/strong&gt; against the starfield.&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Point-light intensities were in physical units on a renderer defaulting to legacy lighting — &lt;strong&gt;the entire third act blew out to white.&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of those were in the QA harness rather than the piece, which is its own lesson: &lt;code&gt;waitForFunction&lt;/code&gt;'s rAF polling stalls under a software rasterizer, and GSAP's &lt;code&gt;seek()&lt;/code&gt; &lt;strong&gt;suppresses &lt;code&gt;onUpdate&lt;/code&gt; by default&lt;/strong&gt;, so scrub-based screenshots silently missed every &lt;code&gt;onUpdate&lt;/code&gt;-driven write. Both were diagnosed by instrumenting rather than guessing.&lt;/p&gt;

&lt;p&gt;Then, because the environment had no GPU at all (a software rasterizer — 15fps), it added &lt;strong&gt;adaptive resolution&lt;/strong&gt;: if rolling frame time exceeds 21.5ms the render scale steps down, at most three times, never changing composition, timing or colour. That doubled throughput on the bad machine and stays at full resolution on real hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's the capability worth pointing at.&lt;/strong&gt; Not "it wrote some Three.js." It made a design decision with a defensible rationale, built its own verification instrument when the requirement demanded one it didn't have, believed the evidence over its own code, and fixed things it could not have known were broken without looking.&lt;/p&gt;




&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;Two things, honestly.&lt;/p&gt;

&lt;p&gt;The first is &lt;strong&gt;access&lt;/strong&gt;. A 29-second title sequence of this kind is normally After Effects, Cinema 4D, a plugin stack, licences, and a render farm's worth of patience. This is one text file, three CDN links, and a browser. A student anywhere can open it, read every line, change the tooth counts, and watch what happens. There's no pipeline standing between curiosity and the thing.&lt;/p&gt;

&lt;p&gt;The second is the &lt;strong&gt;subject&lt;/strong&gt;. The Antikythera mechanism is a reminder that the ancient world was not simple, and that knowledge is genuinely fragile — this machine existed, worked, and then the entire idea of it vanished for a millennium. If a few thousand people watch 29 seconds and go and read about the Saros cycle afterwards, that's a good use of a title sequence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Watch it:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/Antikythera/" rel="noopener noreferrer"&gt;naveenneog.github.io/Antikythera&lt;/a&gt; — plays on load, ~29s; click the speaker to turn the score on (headphones recommended)&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source (MIT, one file):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/Antikythera" rel="noopener noreferrer"&gt;github.com/naveenneog/Antikythera&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🛠 &lt;strong&gt;Built with:&lt;/strong&gt; &lt;a href="https://threejs.org" rel="noopener noreferrer"&gt;Three.js r149&lt;/a&gt; · &lt;a href="https://gsap.com" rel="noopener noreferrer"&gt;GSAP + CustomEase&lt;/a&gt; · Web Audio API · Bodoni Moda / Archivo / JetBrains Mono · &lt;a href="https://github.com/features/copilot/cli" rel="noopener noreferrer"&gt;GitHub Copilot CLI&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One note if you go poking: it pins &lt;strong&gt;Three.js r149&lt;/strong&gt; on purpose. r149 is the last release that ships a real UMD &lt;code&gt;build/three.min.js&lt;/code&gt;. Everything after it is ES-module only, and module CORS blocks &lt;code&gt;file://&lt;/code&gt; — which would have quietly broken the "no build step, no server" constraint the moment anyone double-clicked the file.&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>threejs</category>
      <category>javascript</category>
      <category>creativecoding</category>
    </item>
    <item>
      <title>Cast in Bidri Metal: Four Ancient Karnataka Board Games, Reborn in 3D</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Tue, 21 Jul 2026 17:27:06 +0000</pubDate>
      <link>https://dev.to/naveenneog/cast-in-bidri-metal-four-ancient-karnataka-board-games-reborn-in-3d-4khg</link>
      <guid>https://dev.to/naveenneog/cast-in-bidri-metal-four-ancient-karnataka-board-games-reborn-in-3d-4khg</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flj68n7ospuqkbcgt6l2x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flj68n7ospuqkbcgt6l2x.png" alt="Four Karnataka board games rendered as Bidri metalwork app icons" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Before Ludo, before Snakes &amp;amp; Ladders left the subcontinent and came back Anglicised, our grandmothers drew boards in rice-flour on the courtyard floor and played games older than most kingdoms. This is an attempt to give four of Karnataka's own games a home that honours where they came from — &lt;strong&gt;and&lt;/strong&gt; looks good enough to keep on your phone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I'm &lt;strong&gt;Naveen Gopalakrishna&lt;/strong&gt; — an AI Global Black Belt at Microsoft, a compulsive builder after hours. This #AI4Good build is four games shipped together, each its own free web app and Android APK:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Game&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Play&lt;/th&gt;
&lt;th&gt;Get the APK&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ಸಾಲು ಮನೆ ಆಟ · Saalu Mane Ata&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Nine Men's Morris — align three, break one&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/SaaluManeAta/" rel="noopener noreferrer"&gt;play&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/naveenneog/SaaluManeAta/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ಆಡು ಹುಲಿ · Aadu Huli&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Goats &amp;amp; Tigers — the asymmetric hunt&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/AaduHuli/" rel="noopener noreferrer"&gt;play&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/naveenneog/AaduHuli/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ಚೌಕಾ ಬಾರಾ · Chowka Bara&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The cowrie-and-tokens race&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/ChowkaBara/" rel="noopener noreferrer"&gt;play&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/naveenneog/ChowkaBara/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ಅಳಗುಳಿ ಮನೆ · Alaguli Mane&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pallanguzhi — the sowing &amp;amp; counting game&lt;/td&gt;
&lt;td&gt;&lt;a href="https://naveenneog.github.io/AlaguliMane/" rel="noopener noreferrer"&gt;play&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://github.com/naveenneog/AlaguliMane/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The games, and where they come from
&lt;/h2&gt;

&lt;p&gt;None of these are inventions. They're games with centuries — sometimes millennia — of continuous play across South India.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Saalu Mane Ata&lt;/strong&gt; (Nine Men's Morris) is a &lt;em&gt;mill&lt;/em&gt; game — three nested squares, nine seeds a side. Form a connected line of three ("a mill") and you break a rival seed. Pure strategy, no dice: the same family of game scratched into the stone floors of temples at &lt;strong&gt;Aihole&lt;/strong&gt;. A game of foresight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aadu Huli&lt;/strong&gt; (Goats &amp;amp; Tigers) is an &lt;em&gt;asymmetric hunt&lt;/em&gt; on a triangular board — a few tigers leap to capture goats, while a herd of goats tries to hem the tigers in until they cannot move. The South Indian cousin of &lt;em&gt;Bagh-Chal&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chowka Bara&lt;/strong&gt; (Ashta Chamma) is a &lt;strong&gt;race&lt;/strong&gt; game played with &lt;strong&gt;cowrie shells&lt;/strong&gt; for dice on a cross-and-circle board, with safe squares. The ancestor Ludo forgot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alaguli Mane&lt;/strong&gt; (Pallanguzhi) is a &lt;strong&gt;mancala&lt;/strong&gt; — two rows of pits, seeds sown counter-clockwise, pits of four captured. No luck, just counting ahead. The carved wooden board is a genuine heirloom in Kannada and Tamil homes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each game ships with &lt;strong&gt;multiple "worlds"&lt;/strong&gt; — a &lt;em&gt;Realistic&lt;/em&gt; heritage board, an &lt;em&gt;Original&lt;/em&gt; glowing-3D reading, a &lt;em&gt;Modern&lt;/em&gt; interpretation, sometimes a &lt;em&gt;Fable&lt;/em&gt; world — and the one this post is really about.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fig6jjzid0wdsnj3pi3ho.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fig6jjzid0wdsnj3pi3ho.png" alt="The Kannada-first setup screen: pick a world, an opponent, a difficulty" width="800" height="978"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bidri world — the reason this build exists
&lt;/h2&gt;

&lt;p&gt;The spark for the whole project was a single question: &lt;em&gt;what would these games look like if a master craftsman of Bidar made them?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bidriware&lt;/strong&gt; is a 14th-century metal craft from &lt;strong&gt;Bidar, Karnataka&lt;/strong&gt;. A zinc-and-copper alloy is cast, engraved with floral and geometric &lt;em&gt;tarkashi&lt;/em&gt; patterns, inlaid with &lt;strong&gt;pure silver&lt;/strong&gt; wire, then blackened with a special soil from the Bidar fort — a soil that darkens the alloy to deep matte black but leaves the silver brilliant. The result is unmistakable: &lt;strong&gt;jet-black metal, glowing silver inlay.&lt;/strong&gt; A GI-tagged craft, and one of the most striking in the world.&lt;/p&gt;

&lt;p&gt;So every game got a &lt;strong&gt;bespoke Bidri board&lt;/strong&gt; — not a generic skin, but a mat whose inlay is designed around &lt;em&gt;that game's&lt;/em&gt; geometry:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fquwzc43arjoitt1jc7gk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fquwzc43arjoitt1jc7gk.jpg" alt="Bidri Saalu Mane Ata board" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F17gh4p2b2s05ludgg5rx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F17gh4p2b2s05ludgg5rx.jpg" alt="Bidri Aadu Huli board" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgdjw4xz8736bnob0o12w.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgdjw4xz8736bnob0o12w.jpg" alt="Bidri Chowka Bara board" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm5hrq0mqw72enlm6it1.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffm5hrq0mqw72enlm6it1.jpg" alt="Bidri Alaguli Mane board" width="800" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How we matched real artisan work
&lt;/h3&gt;

&lt;p&gt;Getting AI-generated art to actually &lt;em&gt;read as Bidri&lt;/em&gt; — and not as "generic ornate black-and-gold" — took a tight loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Study first.&lt;/strong&gt; Real Bidri is &lt;strong&gt;matte black, not glossy&lt;/strong&gt;, the inlay is &lt;strong&gt;silver, not gold&lt;/strong&gt;, the motifs are &lt;strong&gt;floral tarkashi and geometric bands&lt;/strong&gt;, and the craft lives in the &lt;em&gt;contrast&lt;/em&gt;. Those became hard constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt to the constraints.&lt;/strong&gt; Every render was pushed toward &lt;em&gt;"near-black blackened-brass ground, brilliant pure-silver inlay, delicate floral tarkashi engraving, museum-quality metalwork, soft studio light with rich reflections"&lt;/em&gt; using &lt;strong&gt;Azure &lt;code&gt;gpt-image-2&lt;/code&gt;&lt;/strong&gt;. Gold only as a thin rim highlight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Align the board to the mat.&lt;/strong&gt; The generated ornament had to sit &lt;em&gt;around&lt;/em&gt; the playable geometry — central design centred on the board, surrounding &lt;em&gt;tarkashi&lt;/em&gt; filling the field, the way a craftsman composes a real tray. The mat and a &lt;strong&gt;seamless table tile&lt;/strong&gt; were rendered as a set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Iterate on light &amp;amp; reflection.&lt;/strong&gt; Several passes went purely into how the silver catches light and how the black holds shadow, until each board looked photographed, not textured.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The same pipeline produced each game's &lt;strong&gt;app icon&lt;/strong&gt; — this game's own board, cast in Bidri. The Bidri work was distilled into a reusable &lt;strong&gt;"Bidri style" skill&lt;/strong&gt; so the look stays consistent across future builds.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The whole suite is deliberately &lt;strong&gt;boring where it counts&lt;/strong&gt; so it can live forever for free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pure static, mobile-first, Kannada-first.&lt;/strong&gt; No backend, no accounts, no tracking. Each game is a folder of HTML/JS/assets on &lt;strong&gt;GitHub Pages&lt;/strong&gt;. Kannada is the first language, English second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;3D in the browser.&lt;/strong&gt; A shared &lt;strong&gt;Three.js&lt;/strong&gt; core (&lt;code&gt;game3d&lt;/code&gt;) renders boards, pieces and worlds, tuned for a mid-range phone. Each game is a thin ruleset on top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four games, one drift-locked core.&lt;/strong&gt; ~36 shared modules stay &lt;strong&gt;byte-identical&lt;/strong&gt; across all four, enforced by a custom &lt;strong&gt;&lt;code&gt;rc-gate&lt;/code&gt;&lt;/strong&gt; that fails the build on any drift and runs the full &lt;strong&gt;unit-test matrix (668 tests: AH 206 · AM 161 · CB 152 · SMA 149)&lt;/strong&gt;, i18n coverage, JSON-schema contracts, language/voice-pack freshness, and an &lt;strong&gt;asset budget&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Six languages, packaged.&lt;/strong&gt; Kannada + English core; four more ship as &lt;strong&gt;downloadable text &amp;amp; voice packs&lt;/strong&gt; (an "asset diet" with a default-core build that fetches optional packs on demand).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Narration&lt;/strong&gt; via &lt;strong&gt;Azure Speech&lt;/strong&gt; neural voices, later repacked to cut size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APKs from the same web build.&lt;/strong&gt; &lt;strong&gt;Capacitor&lt;/strong&gt; wraps each &lt;code&gt;web/&lt;/code&gt; folder; &lt;strong&gt;GitHub Actions&lt;/strong&gt; builds a debug APK on every release (JDK 21, &lt;code&gt;cap add android&lt;/code&gt;, &lt;code&gt;gradlew assembleDebug&lt;/code&gt;) and attaches it automatically. Push to &lt;code&gt;main&lt;/code&gt; → Pages redeploys; publish a release → the APK builds itself.&lt;/li&gt;
&lt;li&gt;Art &amp;amp; icons: Azure &lt;code&gt;gpt-image-2&lt;/code&gt;. &lt;strong&gt;Everything is open source.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ckuo7udi9wf7gje1zoe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9ckuo7udi9wf7gje1zoe.png" alt="The world picker, including the Bidri " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Constraints make AI art authentic.&lt;/strong&gt; "Ornate Indian" gets you tourist-shop clip-art. &lt;em&gt;"Matte-black blackened alloy, pure-silver tarkashi, no gold except a rim highlight"&lt;/em&gt; gets you something a Bidar artisan would recognise. Research the craft, then encode it as non-negotiables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A drift gate is worth more than a test suite alone&lt;/strong&gt; when shipping four near-identical apps. The most valuable script in the repo is the one that refuses to let the four codebases diverge silently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship the culture first, the translation second.&lt;/strong&gt; Kannada-first (not "English with a toggle") changed how the whole UI reads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static + Capacitor is a superpower for heritage software.&lt;/strong&gt; One &lt;code&gt;web/&lt;/code&gt; folder becomes a free website &lt;em&gt;and&lt;/em&gt; an installable app, zero servers — exactly right for something meant to outlast any hosting bill.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tiny asset budget forces good decisions.&lt;/strong&gt; Packing optional languages/voices and fetching on demand kept the base app small without cutting features.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Free, offline, no account, open source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Saalu Mane Ata&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/SaaluManeAta/" rel="noopener noreferrer"&gt;play&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/SaaluManeAta/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/SaaluManeAta" rel="noopener noreferrer"&gt;source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aadu Huli&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/AaduHuli/" rel="noopener noreferrer"&gt;play&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/AaduHuli/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/AaduHuli" rel="noopener noreferrer"&gt;source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chowka Bara&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/ChowkaBara/" rel="noopener noreferrer"&gt;play&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/ChowkaBara/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/ChowkaBara" rel="noopener noreferrer"&gt;source&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alaguli Mane&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/AlaguliMane/" rel="noopener noreferrer"&gt;play&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/AlaguliMane/releases/latest" rel="noopener noreferrer"&gt;APK&lt;/a&gt; · &lt;a href="https://github.com/naveenneog/AlaguliMane" rel="noopener noreferrer"&gt;source&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have grandparents who played these on a courtyard floor, show them the Bidri board. That reaction is the whole point.&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>gamedev</category>
      <category>threejs</category>
      <category>opensource</category>
    </item>
    <item>
      <title>cordless v0.8: Tab Groups, Custom Launchers &amp; Bring-Your-Own Copilot</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Mon, 13 Jul 2026 14:51:24 +0000</pubDate>
      <link>https://dev.to/naveenneog/cordless-v08-tab-groups-custom-launchers-bring-your-own-copilot-2i0b</link>
      <guid>https://dev.to/naveenneog/cordless-v08-tab-groups-custom-launchers-bring-your-own-copilot-2i0b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;cordless&lt;/a&gt; is a CLI-first tool that manages your remote terminal / coding-agent sessions and puts them on your phone. &lt;strong&gt;v0.8&lt;/strong&gt; is about &lt;em&gt;organising&lt;/em&gt; the swarm: &lt;strong&gt;Chrome-mobile-style tab groups&lt;/strong&gt;, &lt;strong&gt;custom launchers&lt;/strong&gt; (bring any command as a profile), a built-in &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt; profile, &lt;strong&gt;renameable tabs&lt;/strong&gt;, and &lt;strong&gt;persisted scrollback history&lt;/strong&gt; so a reopened session after a reboot shows what it was doing. There's also a proper &lt;strong&gt;&lt;code&gt;cordless setup&lt;/code&gt;&lt;/strong&gt; installer. Six features, each on its own branch — designed with &lt;strong&gt;GPT-5.6 Sol&lt;/strong&gt;, built with &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftcwzc7t70e6lleusv1uv.png" alt="cordless v0.8 — tab groups, custom launchers, Copilot" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;v0.7 taught cordless to tell me &lt;em&gt;which&lt;/em&gt; of my eight agent sessions needs me. v0.8 is the next problem: once you're running that many, they stop fitting on the screen. You need to &lt;strong&gt;organise&lt;/strong&gt; them — like browser tabs, because that was the whole metaphor from day one. So this release is six features that make a big session swarm manageable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tab groups
&lt;/h2&gt;

&lt;p&gt;The headline. Sessions can now live in &lt;strong&gt;named, colored groups&lt;/strong&gt; — think "API migration", "Website", "Flaky tests" — exactly like Chrome mobile's tab groups.&lt;/p&gt;

&lt;p&gt;In the terminal dashboard, groups render as &lt;strong&gt;collapsible headers&lt;/strong&gt; with a live per-group waiting/session count:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;── Sessions (6) ──
▼ API migration          2 waiting · 3 sessions
  ! claude   build pipeline            waiting
  ● codex    api tests                 working
  ○ shell    api ~/src/api             idle
▶ Website                              2 sessions
▼ Ungrouped                            1 session
    ○ copilot scratch                  idle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;g&lt;/code&gt; for the group menu (new / assign / ungroup / collapse / rename / delete), and &lt;code&gt;f&lt;/code&gt; to cycle a &lt;strong&gt;smart-view filter&lt;/strong&gt; — &lt;code&gt;All · Attention · Claude · Codex · Copilot · Shell&lt;/code&gt;. Those filters are &lt;em&gt;views&lt;/em&gt;, not groups; collapse state is per-device. On the &lt;strong&gt;phone&lt;/strong&gt; you get the same idea as a chip strip above the tabs (All · Unread · one chip per group, with counts and color dots) plus rename/move in the details sheet.&lt;/p&gt;

&lt;p&gt;The design decision I kept going back and forth on — and where Sol was firm — was &lt;strong&gt;manual groups over continuous auto-grouping&lt;/strong&gt;. Auto-sorting your tabs by directory sounds clever until a session changes cwd and silently jumps groups. So groups are something you &lt;em&gt;choose&lt;/em&gt;; deleting one never kills its sessions, it just ungroups them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom launchers (and a built-in Copilot)
&lt;/h2&gt;

&lt;p&gt;Until now cordless had three profiles: &lt;code&gt;shell&lt;/code&gt;, &lt;code&gt;claude&lt;/code&gt;, &lt;code&gt;codex&lt;/code&gt;. v0.8 makes profiles &lt;strong&gt;yours&lt;/strong&gt;. Drop this in &lt;code&gt;~/.cordless/config.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="nl"&gt;"profiles"&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;"api"&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;"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;"pwsh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-NoLogo"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cwd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"C:/src/api"&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;"notes"&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;"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;"nvim"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"~/notes.md"&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;…and &lt;code&gt;cordless new api&lt;/code&gt; spawns it directly (resolved against the daemon's &lt;code&gt;PATH&lt;/code&gt;, not a shell string — no &lt;code&gt;sh -c&lt;/code&gt; injection surface). A missing executable doesn't crash anything; it shows as &lt;strong&gt;unavailable&lt;/strong&gt; with a reason, and &lt;code&gt;cordless profiles&lt;/code&gt; lists every launcher with its source and availability.&lt;/p&gt;

&lt;p&gt;And because the agent world moved, v0.8 ships a built-in &lt;strong&gt;&lt;code&gt;copilot&lt;/code&gt;&lt;/strong&gt; profile — the standalone &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt; — right next to &lt;code&gt;claude&lt;/code&gt; and &lt;code&gt;codex&lt;/code&gt;. The nice part: attention detection is now &lt;strong&gt;preset-driven&lt;/strong&gt;, so &lt;code&gt;copilot&lt;/code&gt; (and any custom profile you tag &lt;code&gt;attentionPreset: "agent"&lt;/code&gt;) gets the same &lt;em&gt;waiting / finished&lt;/em&gt; heuristics the other agents get, for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rename tabs
&lt;/h2&gt;

&lt;p&gt;Small feature, big daily quality-of-life. &lt;code&gt;cordless rename &amp;lt;id&amp;gt; "API migration"&lt;/code&gt;, or the dashboard &lt;code&gt;e&lt;/code&gt; key, or a long-press on the phone. Titles are Unicode-normalized, control-stripped and length-capped, an empty title restores the generated default, and the change &lt;strong&gt;broadcasts live&lt;/strong&gt; to every connected client (with a monotonic revision so a stale update can't clobber a newer one).&lt;/p&gt;

&lt;h2&gt;
  
  
  Persisted history — survive the reboot
&lt;/h2&gt;

&lt;p&gt;This is the one that turned into a proper engineering story. A cordless session already survives a &lt;em&gt;daemon restart&lt;/em&gt; (it reopens the same shell in the same directory). But it came back &lt;strong&gt;blank&lt;/strong&gt; — you lost the scrollback. v0.8 fixes that: the daemon persists a &lt;strong&gt;capped, plain-text&lt;/strong&gt; copy of each session's scrollback (2000 lines / 512 KB, gzipped, user-only) and, on restore, shows it as &lt;strong&gt;frozen context above&lt;/strong&gt; the reopened session with a &lt;code&gt;── session reopened after system restart ──&lt;/code&gt; banner.&lt;/p&gt;

&lt;p&gt;A couple of decisions worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Normalized text, not raw bytes.&lt;/strong&gt; Sol talked me out of persisting a truncated ANSI stream (it can start mid-escape-sequence and depends on terminal state you've discarded). We store logical lines from the headless terminal buffer instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't write it into the fresh terminal.&lt;/strong&gt; My first version injected the old scrollback &lt;em&gt;into&lt;/em&gt; the reopened shell — which promptly wiped it, because a shell like PowerShell clears its own screen on startup. So restored history is kept &lt;em&gt;beside&lt;/em&gt; the live session and rendered above it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save periodically, not just on shutdown.&lt;/strong&gt; A reboot doesn't send you a polite shutdown signal, so history is flushed every few seconds while the session runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bug that only failed on Linux
&lt;/h2&gt;

&lt;p&gt;Here's the part I'm glad CI caught. My persisted-history tests were &lt;strong&gt;green on Windows&lt;/strong&gt; and I shipped v0.8.0. The &lt;strong&gt;Linux and macOS&lt;/strong&gt; CLI builds went red on the exact same test.&lt;/p&gt;

&lt;p&gt;The cause is a lovely cross-platform trap. On a graceful stop, the daemon catches &lt;code&gt;SIGTERM&lt;/code&gt; and runs &lt;code&gt;shutdown()&lt;/code&gt;: it saves each session's history, then kills the PTYs. Killing a PTY fires its exit handler — and &lt;em&gt;that&lt;/em&gt; handler deleted the just-saved history &lt;strong&gt;and&lt;/strong&gt; rewrote the "reopen these on restart" manifest without the now-exited session. So on Linux the session came back with no history; on macOS it didn't come back at all.&lt;/p&gt;

&lt;p&gt;Why did Windows pass? Because on Windows &lt;code&gt;SIGTERM&lt;/code&gt; from another process is an &lt;strong&gt;uncatchable hard-kill&lt;/strong&gt; — &lt;code&gt;shutdown()&lt;/code&gt; never runs, the exit handler never fires, and the periodically-flushed history file just… survives. The platform that "worked" only worked by accident.&lt;/p&gt;

&lt;p&gt;The fix is a &lt;code&gt;_shuttingDown&lt;/code&gt; flag so the exit handler preserves history and the manifest during a graceful stop. What I like about it: I reproduced the Linux-only failure &lt;strong&gt;on Windows&lt;/strong&gt; by driving &lt;code&gt;shutdown()&lt;/code&gt; directly in a unit test (the harness's &lt;code&gt;SIGTERM&lt;/code&gt; can't), so the regression is now locked shut on every platform. Re-tagged, rebuilt, and v0.8.0 went out clean with the full asset set.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;Same loop, tightened further: &lt;strong&gt;me on GitHub Copilot CLI, GPT-5.6 Sol as the design partner&lt;/strong&gt; — and this time with a rule I'm keeping for good: &lt;strong&gt;every feature on its own branch, merged &lt;code&gt;--no-ff&lt;/code&gt;, with clean well-documented code&lt;/strong&gt;. The v0.8 program was six branches (&lt;code&gt;feature/persisted-history&lt;/code&gt;, &lt;code&gt;feature/custom-profiles&lt;/code&gt;, &lt;code&gt;feature/copilot-profile&lt;/code&gt;, &lt;code&gt;feature/session-rename&lt;/code&gt;, &lt;code&gt;feature/session-groups&lt;/code&gt;, &lt;code&gt;feature/group-ui-phone&lt;/code&gt;) plus a &lt;code&gt;fix/history-shutdown&lt;/code&gt;. The daemon's test harness grew to &lt;strong&gt;24 suites&lt;/strong&gt; — the pure profile/attention/render checks, live rename + groups over the WebSocket, the dashboard-driven resume regression, and persisted-history across a real restart — and CI runs all of it on Windows, Linux and macOS before building and smoke-testing the self-contained binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;The feeling I was chasing: open cordless with a dozen agents running and not feel underwater. The waiting ones surface (v0.7); the rest are &lt;strong&gt;filed into groups&lt;/strong&gt; you named, launched from &lt;strong&gt;profiles&lt;/strong&gt; you defined, titled how &lt;em&gt;you&lt;/em&gt; think about them — and if the box reboots overnight, each one comes back &lt;strong&gt;with its history intact&lt;/strong&gt;. It's the browser-tabs promise, finally delivered for a swarm of terminals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Live / docs:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;naveenneog.github.io/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;cordless CLI (Windows / Linux, no Node needed):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt; — then &lt;code&gt;cordless setup&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Android APK:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🧑‍💻 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Part of the &lt;a href="https://naveenneog.github.io/AI4Good/2026/07/10/ai4good-an-app-a-day/" rel="noopener noreferrer"&gt;#AI4Good&lt;/a&gt; series. Built one day at a time. — &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;@naveenneog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>cli</category>
      <category>terminal</category>
      <category>productivity</category>
    </item>
    <item>
      <title>cordless v0.7: Attention State — Know Which of Your 8 Agent Sessions Needs You</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Sat, 11 Jul 2026 18:26:03 +0000</pubDate>
      <link>https://dev.to/naveenneog/cordless-v07-attention-state-know-which-of-your-8-agent-sessions-needs-you-3bi</link>
      <guid>https://dev.to/naveenneog/cordless-v07-attention-state-know-which-of-your-8-agent-sessions-needs-you-3bi</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;cordless&lt;/a&gt; is a CLI-first tool that manages your remote terminal / coding-agent sessions and puts them on your phone. &lt;strong&gt;v0.7&lt;/strong&gt; adds the thing that actually matters when you're juggling many agents: &lt;strong&gt;per-session attention state&lt;/strong&gt;. The daemon watches each session's output and infers whether it's &lt;em&gt;working&lt;/em&gt;, &lt;em&gt;idle&lt;/em&gt;, or &lt;em&gt;waiting for you&lt;/em&gt; — then badges it, sorts attention-first, and can push a notification (ntfy / webhook) so you get pinged instead of babysitting. Also new: &lt;strong&gt;scrollback search&lt;/strong&gt;, &lt;strong&gt;copy last output&lt;/strong&gt;, and &lt;strong&gt;named workspaces&lt;/strong&gt;. Designed with &lt;strong&gt;GPT-5.6 Sol&lt;/strong&gt;, built with &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fccwkhdayuslouw1it0so.png" alt="cordless v0.7 — attention state" width="799" height="686"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the real problem with running eight coding agents at once: they keep &lt;strong&gt;stopping to ask you things&lt;/strong&gt;. "Apply these edits? (y/n)". "Run the tests?". One finishes; another hits an error; a third is still churning. You can't watch eight terminals. So you tab through them, and half the time you're staring at a session that's happily working while another has been blocked on a yes/no for four minutes.&lt;/p&gt;

&lt;p&gt;v0.7 is my answer to that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attention state
&lt;/h2&gt;

&lt;p&gt;cordless now tracks, for every session, two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;activity&lt;/strong&gt; — &lt;code&gt;working&lt;/code&gt;, &lt;code&gt;idle&lt;/code&gt;, or &lt;code&gt;exited&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;attention&lt;/strong&gt; — &lt;code&gt;waiting&lt;/code&gt; (a prompt), &lt;code&gt;bell&lt;/code&gt;, &lt;code&gt;finished&lt;/code&gt;, or nothing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The daemon infers this &lt;strong&gt;purely from the session's PTY output&lt;/strong&gt; — no shell integration, no hooks required. In the dashboard, sessions are &lt;strong&gt;badged and sorted attention-first&lt;/strong&gt;, so the one that needs you floats to the top with a &lt;code&gt;!&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;── Sessions (3) ──  1 need attention
▸ ! claude  review PR #1284            waiting
      Apply these edits to src/api? (y/n):
  ○ codex   codex ~/src/api            idle
  ○ shell   tests ~/src/app            idle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;c&lt;/code&gt; to mark it handled; sending input clears it automatically (answering &lt;em&gt;is&lt;/em&gt; handling it). On the phone you get the same badges. &lt;code&gt;cordless sessions --attention&lt;/code&gt; lists just the ones asking for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard part: what counts as "waiting"?
&lt;/h2&gt;

&lt;p&gt;This is where I leaned on Sol the most, because naive detection is a &lt;strong&gt;false-positive machine&lt;/strong&gt;. A silent build looks "stuck". A shell sitting at its prompt looks "ready" but isn't asking anything. &lt;code&gt;vim&lt;/code&gt; and &lt;code&gt;htop&lt;/code&gt; repaint constantly. The rules we landed on are deliberately conservative:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A trailing &lt;strong&gt;shell prompt&lt;/strong&gt; (&lt;code&gt;$&lt;/code&gt;, &lt;code&gt;#&lt;/code&gt;, &lt;code&gt;&amp;gt;&lt;/code&gt;, &lt;code&gt;❯&lt;/code&gt;) is &lt;em&gt;readiness&lt;/em&gt;, not attention. Never notify on it.&lt;/li&gt;
&lt;li&gt;A silent session is &lt;strong&gt;idle&lt;/strong&gt;, not waiting — a paused agent, a long build, and a sleeping process all look the same, so we don't guess.&lt;/li&gt;
&lt;li&gt;We only raise &lt;strong&gt;waiting&lt;/strong&gt; on &lt;strong&gt;high-confidence confirmation/agent prompts&lt;/strong&gt; — &lt;code&gt;(y/n)&lt;/code&gt;, "Continue?", "Do you want me to…?", "Enter your password:" — matched against the &lt;em&gt;last non-empty line&lt;/em&gt; after a short quiet window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alternate-screen&lt;/strong&gt; apps (vim/htop/less) suppress prompt heuristics entirely — better to miss a full-screen prompt than to cry wolf.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BEL&lt;/strong&gt; (&lt;code&gt;^G&lt;/code&gt;) is an explicit signal, so it's trusted — but ignored during a session's first few seconds and right after a keypress (startup and invalid-key beeps).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;finished&lt;/strong&gt; is heuristic and scoped to &lt;code&gt;claude&lt;/code&gt;/&lt;code&gt;codex&lt;/code&gt;: back at a shell prompt after real activity. It's labelled low-confidence and can be turned off.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole classifier is a pure, unit-tested module — 41 fixture checks across shells, pagers, and Claude/Codex-style prompts. And QA earned its keep: I caught a bug where a prompt appearing &lt;strong&gt;within two seconds of a keypress&lt;/strong&gt; was silently dropped (the input-grace guard also skipped the &lt;em&gt;whole&lt;/em&gt; quiet cycle). A cross-platform live test now locks that path shut.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get pinged — without any cloud
&lt;/h2&gt;

&lt;p&gt;Badges are great when you're looking. When you're not, v0.7 can &lt;strong&gt;notify&lt;/strong&gt; you. Point it at an &lt;strong&gt;ntfy&lt;/strong&gt; topic (the ntfy phone app pushes it) or a &lt;strong&gt;generic webhook&lt;/strong&gt; (Slack/Discord/automation) in &lt;code&gt;config.json&lt;/code&gt;, and cordless POSTs when a session transitions to waiting / bell / finished. &lt;code&gt;cordless notify test&lt;/code&gt; validates it.&lt;/p&gt;

&lt;p&gt;cordless owns &lt;strong&gt;no cloud&lt;/strong&gt; for this, and the anti-spam is strict (Sol was insistent): one notification per state revision, a 60-second per-session cooldown, a 5-per-minute global cap, optional quiet hours, and &lt;strong&gt;no terminal output in the payload&lt;/strong&gt; by default (it can contain code or secrets). Topic and webhook URLs are treated as secrets and never logged. Delivery is async with a short timeout — a broken webhook can never stall your terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three more coder niceties
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Copy last output:&lt;/strong&gt; &lt;code&gt;cordless output &amp;lt;id&amp;gt; --lines 50 --copy&lt;/code&gt; grabs a session's recent output to your clipboard (cross-platform).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrollback search:&lt;/strong&gt; &lt;code&gt;cordless search &amp;lt;id&amp;gt; "TypeError"&lt;/code&gt; scans a session's retained buffer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workspaces:&lt;/strong&gt; &lt;code&gt;cordless workspace save api-feature&lt;/code&gt; snapshots your running sessions (profile + cwd + title); &lt;code&gt;cordless workspace open api-feature&lt;/code&gt; relaunches the whole layout — "Claude on api, Codex on web, a tests shell" — in one command.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;Same loop, sharpened: &lt;strong&gt;me on GitHub Copilot CLI, Sol as the design partner&lt;/strong&gt; — and this sprint I kept it honest about &lt;em&gt;version control&lt;/em&gt;. Each feature was built on its &lt;strong&gt;own branch&lt;/strong&gt; (&lt;code&gt;feature/attention-state&lt;/code&gt;, &lt;code&gt;feature/notifications&lt;/code&gt;, &lt;code&gt;feature/output-search&lt;/code&gt;, &lt;code&gt;feature/workspaces&lt;/code&gt;), tested, and merged as a unit. The daemon's test harness grew to &lt;strong&gt;16 suites&lt;/strong&gt; — pure attention heuristics, live prompt/idle detection, notifier anti-spam + delivery (injected fetch, no network), scrollback, workspaces, plus the existing protocol/security/restore coverage — and CI runs all of it on Windows and Linux, then builds and smoke-tests the self-contained binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;The feeling I was after: glance at cordless (in the terminal or on my phone) and instantly know &lt;strong&gt;which agent needs me&lt;/strong&gt; — not by tabbing through eight sessions, but because the one waiting is at the top with a &lt;code&gt;!&lt;/code&gt; and, if I've wandered off, my phone buzzed. Everything else — search, copy, workspaces — is in service of the same thing: running a lot of agents without drowning in them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Live / docs:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;naveenneog.github.io/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;cordless CLI (Windows / Linux, no Node needed):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Android APK:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🧑‍💻 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Part of the #AI4Good series. Built one day at a time. — &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;@naveenneog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>cli</category>
      <category>terminal</category>
      <category>productivity</category>
    </item>
    <item>
      <title>cordless v0.6: Going CLI-First — Run It, Scan the QR, You're Paired</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Sat, 11 Jul 2026 17:17:06 +0000</pubDate>
      <link>https://dev.to/naveenneog/cordless-v06-going-cli-first-run-it-scan-the-qr-youre-paired-4lmo</link>
      <guid>https://dev.to/naveenneog/cordless-v06-going-cli-first-run-it-scan-the-qr-youre-paired-4lmo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;cordless&lt;/a&gt; manages your remote terminal / coding-agent (Claude Code, Codex) sessions and puts them on your phone. &lt;strong&gt;v0.6&lt;/strong&gt; makes it &lt;strong&gt;CLI-first&lt;/strong&gt;: run &lt;code&gt;cordless&lt;/code&gt; and it opens a full-screen terminal &lt;strong&gt;dashboard&lt;/strong&gt; whose starting screen shows a &lt;strong&gt;live pairing QR&lt;/strong&gt; — scan it, done. No separate &lt;code&gt;cordless pair&lt;/code&gt;, no GUI to babysit. And it now ships as &lt;strong&gt;one self-contained binary&lt;/strong&gt; (its own Node runtime &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;node-pty&lt;/code&gt; baked in), so there's nothing to install first. Designed in a running debate with &lt;strong&gt;GPT-5.6 Sol&lt;/strong&gt;, built with &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe19vugzqrrghoxoxkwz0.png" alt="cordless v0.6 — the CLI-first dashboard" width="799" height="735"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every version so far treated cordless as "a daemon plus a phone app." Then the owner said something that reframed the whole thing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"cordless should be a **CLI first&lt;/em&gt;&lt;em&gt;. Look at the design of a terminal app and get the features from that. I want an installer that starts cordless as a proper terminal with a QR to pair on its starting screen — right now I run &lt;code&gt;cordless pair&lt;/code&gt; separately and pair the desktop and mobile apps independently, which defeats the purpose."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a redesign, not a tweak. So I opened a long debate with Sol and rebuilt the front door.&lt;/p&gt;

&lt;h2&gt;
  
  
  cordless &lt;em&gt;is&lt;/em&gt; the terminal now
&lt;/h2&gt;

&lt;p&gt;Run &lt;code&gt;cordless&lt;/code&gt; with no arguments and you get the screen on that card: a brand banner, daemon + Tailscale status, your session list, and — front and center — a &lt;strong&gt;single-use pairing QR with a countdown&lt;/strong&gt;. Scan it with the phone app and you're paired. Press &lt;code&gt;p&lt;/code&gt; for a fresh code. No second command, no separate terminal.&lt;/p&gt;

&lt;p&gt;Borrowing from modern terminal apps (Warp, Windows Terminal, iTerm2) — but deliberately &lt;em&gt;not&lt;/em&gt; rebuilding one — the dashboard is a focused TUI: &lt;code&gt;↑/↓&lt;/code&gt; selects a session, &lt;code&gt;Enter&lt;/code&gt; attaches, &lt;code&gt;n&lt;/code&gt; starts a shell / Claude / Codex, &lt;code&gt;x&lt;/code&gt; kills, &lt;code&gt;d&lt;/code&gt; manages paired devices, &lt;code&gt;q&lt;/code&gt; leaves. It's a &lt;strong&gt;thin client of a persistent daemon&lt;/strong&gt;: quitting the dashboard (or closing the terminal) never stops your sessions or the phone connection — the daemon keeps owning the PTYs. Sol was firm on that boundary, and it's the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attach right there in your terminal
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Enter&lt;/code&gt; (or &lt;code&gt;cordless attach &amp;lt;id&amp;gt;&lt;/code&gt;) streams a session &lt;strong&gt;straight into your host terminal&lt;/strong&gt; — no xterm.js, no second renderer. Raw keystrokes go to the PTY, resize events forward, and the detach chord is &lt;code&gt;Ctrl-] d&lt;/code&gt;. It's a tiny tmux-like attach that reuses the exact replay/snapshot protocol the phone uses. The host terminal &lt;em&gt;is&lt;/em&gt; the renderer; cordless just pipes bytes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pairing is daemon-owned now
&lt;/h2&gt;

&lt;p&gt;The old &lt;code&gt;cordless pair&lt;/code&gt; minted a secret by writing a file directly. Sol flagged that: two processes minting into the same store invites races and inconsistent limits. So in v0.6 there's &lt;strong&gt;one&lt;/strong&gt; authenticated &lt;code&gt;pairing.create&lt;/code&gt; over the WebSocket, and it can only be called by a &lt;strong&gt;loopback-scoped credential from a real loopback socket&lt;/strong&gt; — the local machine's owner, never a remote phone. It's single-use, 256-bit, five-minute TTL, rate-limited, and capped. Both the dashboard and &lt;code&gt;cordless pair&lt;/code&gt; call the same path. A stolen phone token can't enroll new devices; only &lt;em&gt;you&lt;/em&gt;, at the machine, can.&lt;/p&gt;

&lt;h2&gt;
  
  
  One binary, no Node required
&lt;/h2&gt;

&lt;p&gt;The biggest ask hiding in "an installer that just works" is: &lt;strong&gt;don't make me install Node first.&lt;/strong&gt; So v0.6 ships a self-contained executable built with &lt;strong&gt;Node's Single Executable Application&lt;/strong&gt; support:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;esbuild&lt;/code&gt; bundles the whole CLI into one file (with &lt;code&gt;node-pty&lt;/code&gt; marked external).&lt;/li&gt;
&lt;li&gt;Node generates a SEA blob from that bundle.&lt;/li&gt;
&lt;li&gt;The blob is injected into a copy of the Node runtime → &lt;code&gt;cordless.exe&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;node-pty&lt;/code&gt; and the built web client ship &lt;strong&gt;beside&lt;/strong&gt; the exe under &lt;code&gt;resources/&lt;/code&gt;, and a small loader resolves the native module from there.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The trick that made it painless: &lt;code&gt;node-pty&lt;/code&gt; ships &lt;strong&gt;node-api prebuilds&lt;/strong&gt;, which are ABI-stable across Node versions — so there's no fragile per-version native rebuild. The result is a ~45 MB download that runs the dashboard, spawns real PTYs, and serves the web client for your phone, all with &lt;strong&gt;zero prerequisites&lt;/strong&gt;. CI builds and &lt;strong&gt;smoke-tests&lt;/strong&gt; it (start → spawn a PTY → stop) on Windows and Linux; macOS is built the same way but still has a runner-specific &lt;code&gt;node-pty&lt;/code&gt; spawn quirk I'm chasing, so it's flagged as pending for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;The same loop as the whole project — me on &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt;, &lt;strong&gt;Sol&lt;/strong&gt; as the design partner — but this time the pivot leaned hard on Sol's judgment: run the daemon in-process or as a persistent service? (service). How interactive should the TUI be for v0.6? (dashboard + minimal attach, not a pane framework). &lt;code&gt;pkg&lt;/code&gt; or Node SEA? (SEA, with node-pty external). Keeping the conversation &lt;strong&gt;stateful&lt;/strong&gt; meant Sol weighed each answer against every prior decision instead of re-litigating them. Ten test suites — protocol E2E, security headers, loopback-scope enforcement, daemon-owned pairing, the CLI client, dashboard rendering, and restore-across-restart — stay green on every push.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;The onboarding is finally one motion: &lt;strong&gt;install cordless, run it, scan the QR.&lt;/strong&gt; No Node, no &lt;code&gt;npm install -g&lt;/code&gt;, no separate pairing step, no GUI to keep open. It reads like a real terminal tool because it &lt;em&gt;is&lt;/em&gt; one — and the phone still gets the same live sessions. Turning a "daemon + app" into "a command you run" is exactly the kind of course-correction that only lands when you actually live with the thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Live / docs:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;naveenneog.github.io/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;cordless CLI (Windows / Linux, no Node needed):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Android APK:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🧑‍💻 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Part of the #AI4Good series. Built one day at a time. — &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;@naveenneog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>cli</category>
      <category>terminal</category>
      <category>node</category>
    </item>
    <item>
      <title>cordless v0.5: A Desktop App, a &gt;_&lt; Logo, and QR-First Security</title>
      <dc:creator>Naveen Gopalakrishna</dc:creator>
      <pubDate>Sat, 11 Jul 2026 09:14:05 +0000</pubDate>
      <link>https://dev.to/naveenneog/cordless-v05-a-desktop-app-a-logo-and-qr-first-security-617</link>
      <guid>https://dev.to/naveenneog/cordless-v05-a-desktop-app-a-logo-and-qr-first-security-617</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;cordless&lt;/a&gt; manages many remote terminal / coding-agent (Claude Code, Codex) sessions &lt;strong&gt;like browser tabs&lt;/strong&gt;, with sessions that survive disconnects. &lt;strong&gt;v0.5&lt;/strong&gt; adds a &lt;strong&gt;hardened Electron desktop app&lt;/strong&gt; (Windows &lt;code&gt;.exe&lt;/code&gt;, macOS &lt;code&gt;.dmg&lt;/code&gt;, Linux &lt;code&gt;.AppImage&lt;/code&gt;/&lt;code&gt;.deb&lt;/code&gt;), a proper &lt;strong&gt;&lt;code&gt;&amp;gt;_&amp;lt;&lt;/code&gt; brand logo&lt;/strong&gt; made with &lt;strong&gt;gpt-image-2&lt;/strong&gt;, and a &lt;strong&gt;security course-correction&lt;/strong&gt;: QR pairing stays the &lt;em&gt;only&lt;/em&gt; default way in, and the desktop's one-click "Connect to this computer" uses a &lt;strong&gt;loopback-only&lt;/strong&gt; credential that a Tailscale or LAN address is &lt;em&gt;rejected&lt;/em&gt; for. As always — designed with &lt;strong&gt;GPT-5.6 Sol&lt;/strong&gt;, driven with &lt;strong&gt;GitHub Copilot CLI&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8bxs4uylr2sczc802gk4.png" alt="cordless — now with a desktop app and a &gt;_&lt; logo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I &lt;a href="https://naveenneog.github.io/AI4Good/2026/07/10/cordless-remote-terminals-in-your-pocket/" rel="noopener noreferrer"&gt;shipped cordless&lt;/a&gt;, then made it &lt;a href="https://naveenneog.github.io/AI4Good/2026/07/11/cordless-v0.4-install-guide/" rel="noopener noreferrer"&gt;livable in v0.4&lt;/a&gt; — seamless resume, an in-app QR scanner, no more truncation. v0.5 is about the two things a "real" tool needs that a phone app alone can't give you: &lt;strong&gt;a proper desktop experience&lt;/strong&gt;, and &lt;strong&gt;an identity&lt;/strong&gt;. And it's where a security instinct paid off.&lt;/p&gt;

&lt;h2&gt;
  
  
  A face for the project: the &lt;code&gt;&amp;gt;_&amp;lt;&lt;/code&gt; logo
&lt;/h2&gt;

&lt;p&gt;cordless needed a mark. I generated one with &lt;strong&gt;gpt-image-2&lt;/strong&gt; on my Azure AI Foundry deployment — a small Python script (&lt;code&gt;tooling/gen_logo.py&lt;/code&gt;) that POSTs a prompt and writes back the base64 PNG. After iterating on a few concepts, the winner was almost too obvious: &lt;strong&gt;&lt;code&gt;&amp;gt;_&amp;lt;&lt;/code&gt;&lt;/strong&gt; — a shell prompt &lt;code&gt;&amp;gt;_&lt;/code&gt; that doubles as a happy little face — set in a blue→violet gradient inside a terminal-window frame.&lt;/p&gt;

&lt;p&gt;One gotcha worth remembering: &lt;strong&gt;gpt-image-2 won't render a transparent background&lt;/strong&gt; (you get an HTTP 400 if you ask). So I generated the mark on a dark background and &lt;strong&gt;luminance-keyed it transparent&lt;/strong&gt; with Pillow (&lt;code&gt;tooling/apply_logo.py&lt;/code&gt;) — the same script then resizes it into &lt;em&gt;every&lt;/em&gt; size the project needs: PWA icons (&lt;code&gt;icon-192/512&lt;/code&gt;, a maskable variant, apple-touch, favicons), the landing-page hero, and the full set of Android launcher / adaptive-icon / splash assets.&lt;/p&gt;

&lt;p&gt;A subtle bug fell out of this: the repo's &lt;code&gt;.gitignore&lt;/code&gt; had a blanket &lt;code&gt;*.png&lt;/code&gt; rule (to keep Playwright screenshots out), and it was &lt;strong&gt;silently swallowing the new app icons&lt;/strong&gt;. The APK and PWA would have shipped with missing icons. Two lines fixed it — &lt;code&gt;!client/public/*.png&lt;/code&gt;, &lt;code&gt;!desktop/build/*.png&lt;/code&gt; — a good reminder to always check &lt;code&gt;git status&lt;/code&gt; after adding binary assets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The desktop app — a hardened Electron shell
&lt;/h2&gt;

&lt;p&gt;The headline feature. When you're &lt;em&gt;at&lt;/em&gt; your dev box (or remoted into it), a phone is the wrong tool — you want a real window with a real keyboard. So v0.5 ships a &lt;strong&gt;desktop app&lt;/strong&gt; for Windows, macOS, and Linux.&lt;/p&gt;

&lt;p&gt;I designed the architecture in my running conversation with &lt;strong&gt;Sol&lt;/strong&gt;, and we landed on the pragmatic choice: the Electron window &lt;strong&gt;loads the daemon's own served page&lt;/strong&gt; at &lt;code&gt;http://127.0.0.1:&amp;lt;port&amp;gt;&lt;/code&gt;. Because it's same-origin to the daemon, there are &lt;strong&gt;zero&lt;/strong&gt; CORS/CSP changes — it's the exact same UI, sessions, tabs, and replay you already have, just in a window.&lt;/p&gt;

&lt;p&gt;The interesting part is the &lt;strong&gt;security hardening&lt;/strong&gt;, straight from Sol's review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;contextIsolation: true&lt;/code&gt;, &lt;code&gt;nodeIntegration: false&lt;/code&gt;, &lt;code&gt;sandbox: true&lt;/code&gt;, &lt;code&gt;webSecurity: true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Navigation is &lt;strong&gt;pinned&lt;/strong&gt; to the trusted loopback origin — &lt;code&gt;will-navigate&lt;/code&gt; and &lt;code&gt;setWindowOpenHandler&lt;/code&gt; reject anything else; all new windows, &lt;code&gt;&amp;lt;webview&amp;gt;&lt;/code&gt;s, and permission requests (camera, mic, geolocation…) are denied.&lt;/li&gt;
&lt;li&gt;The renderer gets a &lt;strong&gt;tiny&lt;/strong&gt; preload bridge and nothing else: &lt;code&gt;{ platform, getLocalCredential, startDaemon, retry }&lt;/code&gt;. No &lt;code&gt;fs&lt;/code&gt;, no shell, no &lt;code&gt;openExternal&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Every IPC call validates that the &lt;strong&gt;sender is one of our own trusted pages&lt;/strong&gt;. &lt;code&gt;startDaemon&lt;/code&gt; accepts &lt;strong&gt;no&lt;/strong&gt; path, command, or argument from the page — it resolves the installed CLI itself.&lt;/li&gt;
&lt;li&gt;If the daemon isn't running, the app shows a &lt;strong&gt;Start daemon / Retry&lt;/strong&gt; screen instead of a blank window.&lt;/li&gt;
&lt;li&gt;The port is resolved with a strict precedence — the credential's &lt;code&gt;server&lt;/code&gt;, then the daemon config, then &lt;code&gt;7443&lt;/code&gt; — and &lt;strong&gt;every&lt;/strong&gt; candidate is validated to be loopback &lt;code&gt;http&lt;/code&gt; (no &lt;code&gt;https&lt;/code&gt;, no userinfo, no path, no &lt;code&gt;localhost&lt;/code&gt; name ambiguity).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last bit of parsing is security-critical, so I pulled it into a pure module (&lt;code&gt;desktop/lib/resolve.js&lt;/code&gt;) and wrote &lt;strong&gt;24 unit tests&lt;/strong&gt; for it. It runs in CI on every push.&lt;/p&gt;

&lt;h2&gt;
  
  
  QR-first security — the course-correction that mattered
&lt;/h2&gt;

&lt;p&gt;Here's the moment I'm most glad about. While wiring the desktop app, I built a convenient "auto-connect on localhost" path. It worked. It was also &lt;strong&gt;wrong&lt;/strong&gt; — it quietly bypassed pairing. The rule for cordless is: &lt;strong&gt;QR / pairing-code is the one true way in.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I tore the auto-connect out and rebuilt it as an &lt;strong&gt;explicit, opt-in button&lt;/strong&gt;. The daemon now writes a &lt;code&gt;desktop-credential.json&lt;/code&gt; (mode &lt;code&gt;0600&lt;/code&gt;; it stores only the token's hash) tied to a device with &lt;strong&gt;&lt;code&gt;scope: "loopback"&lt;/code&gt;&lt;/strong&gt;. The &lt;code&gt;authenticate()&lt;/code&gt; path accepts that token &lt;strong&gt;only when the socket peer is &lt;code&gt;127.0.0.1&lt;/code&gt; / &lt;code&gt;::1&lt;/code&gt;&lt;/strong&gt; — checked against the real socket address, never a spoofable header. A Tailscale &lt;code&gt;100.x&lt;/code&gt; or LAN address is &lt;strong&gt;rejected outright&lt;/strong&gt;. In the UI, the &lt;strong&gt;🖥️ Connect to this computer&lt;/strong&gt; button only appears when the Electron bridge actually hands over a credential; open the same page in a plain browser and you see QR/code only.&lt;/p&gt;

&lt;p&gt;I verified it with a test that asserts exactly this: loopback IPs authenticate, and &lt;code&gt;100.64.x.x&lt;/code&gt; (Tailscale's range) does not. It's a &lt;em&gt;local convenience&lt;/em&gt; that is structurally incapable of being a &lt;em&gt;remote bypass&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ok    ip=127.0.0.1        authed=true   (expected true)
ok    ip=::1              authed=true   (expected true)
ok    ip=203.0.113.5      authed=false  (expected false)
ok    ip=100.64.1.2       authed=false  (expected false)   &amp;lt;- Tailscale, still rejected
=== LOOPBACK-SCOPE ENFORCEMENT PASS ===
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prerequisites &amp;amp; Tailscale, documented properly
&lt;/h2&gt;

&lt;p&gt;A fair complaint about the earlier docs: the &lt;strong&gt;prerequisites and Tailscale setup weren't written down clearly&lt;/strong&gt;. Fixed. The README and landing page now spell out, with Sol's help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prerequisites&lt;/strong&gt; — Node.js 22, the platform build tools &lt;code&gt;node-pty&lt;/code&gt; needs, Tailscale, and (on Windows) PowerShell 7, with download links.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailscale setup&lt;/strong&gt; — &lt;code&gt;tailscale up&lt;/code&gt; / &lt;code&gt;status&lt;/code&gt; / &lt;code&gt;ip -4&lt;/code&gt;, an optional device &lt;strong&gt;tag&lt;/strong&gt;, a copy-pasteable &lt;strong&gt;tailnet ACL&lt;/strong&gt; that locks TCP &lt;code&gt;7443&lt;/code&gt; to your own identity, and a &lt;strong&gt;Windows Firewall&lt;/strong&gt; rule that only opens &lt;code&gt;7443&lt;/code&gt; on the Tailscale adapter (&lt;code&gt;100.64.0.0/10&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resume your sessions&lt;/strong&gt; — &lt;code&gt;cordless install&lt;/code&gt; once, then open the desktop app or &lt;code&gt;http://localhost:7443&lt;/code&gt;; your tabs reattach, and after a reboot the daemon reopens them from its restore manifest.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it was built
&lt;/h2&gt;

&lt;p&gt;Same loop as the whole project: &lt;strong&gt;me on GitHub Copilot CLI, Sol as the design partner and reviewer&lt;/strong&gt; — and this time I kept the conversation &lt;em&gt;stateful&lt;/em&gt;, so Sol remembered every prior decision instead of re-litigating them. The desktop architecture, the Electron hardening checklist, and the loopback-credential rules all came out of that back-and-forth. CI does the heavy lifting: pushing a &lt;code&gt;v*&lt;/code&gt; tag builds the Android APK &lt;strong&gt;and&lt;/strong&gt; the three desktop installers on native GitHub runners and attaches them to the release. And &lt;code&gt;npm test&lt;/code&gt; is now a &lt;strong&gt;self-contained harness&lt;/strong&gt; that boots an isolated daemon and runs every suite — protocol E2E, security headers, the new desktop-credential + loopback-scope checks, and session-restore across a daemon restart. Seven suites, green.&lt;/p&gt;

&lt;h2&gt;
  
  
  The good
&lt;/h2&gt;

&lt;p&gt;v0.5 is the version where cordless stopped feeling like a phone experiment and started feeling like a product: it has a &lt;strong&gt;name and a face&lt;/strong&gt;, it runs as a &lt;strong&gt;real desktop app&lt;/strong&gt;, and — the part I care about most — the convenience features were built &lt;strong&gt;without&lt;/strong&gt; weakening the security model. The one-click local connect is genuinely handy, and it &lt;em&gt;cannot&lt;/em&gt; be turned into a remote hole. That's the bar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;▶️ &lt;strong&gt;Live / install the PWA:&lt;/strong&gt; &lt;a href="https://naveenneog.github.io/cordless/" rel="noopener noreferrer"&gt;naveenneog.github.io/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🖥️ &lt;strong&gt;Desktop app (Win/macOS/Linux):&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Android APK:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless/releases/latest" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/naveenneog/cordless" rel="noopener noreferrer"&gt;github.com/naveenneog/cordless&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Part of the #AI4Good series. Built one day at a time. — &lt;a href="https://github.com/naveenneog" rel="noopener noreferrer"&gt;@naveenneog&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai4good</category>
      <category>electron</category>
      <category>security</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
