<?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: Mir Mursalin Ankur</title>
    <description>The latest articles on DEV Community by Mir Mursalin Ankur (@mir_mursalin_ankur).</description>
    <link>https://dev.to/mir_mursalin_ankur</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%2F1253577%2Fab3d1472-b6cf-4c16-994c-cf7c24c8104e.jpeg</url>
      <title>DEV Community: Mir Mursalin Ankur</title>
      <link>https://dev.to/mir_mursalin_ankur</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mir_mursalin_ankur"/>
    <language>en</language>
    <item>
      <title>Build Your Own IDE Extension: A Real-World Guide From Idea to Publish (VS Code and Others)</title>
      <dc:creator>Mir Mursalin Ankur</dc:creator>
      <pubDate>Mon, 14 Sep 2026 15:29:12 +0000</pubDate>
      <link>https://dev.to/mir_mursalin_ankur/build-your-own-ide-extension-a-real-world-guide-from-idea-to-publish-vs-code-and-others-2j6e</link>
      <guid>https://dev.to/mir_mursalin_ankur/build-your-own-ide-extension-a-real-world-guide-from-idea-to-publish-vs-code-and-others-2j6e</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The most useful software you'll ever write might be the kind only you needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every developer has one. That small thing your editor &lt;em&gt;almost&lt;/em&gt; does — the command you retype ten times a day, the info you keep switching to a browser to check, the tool you love that lives in a terminal while your actual work lives three windows away. You've tolerated it for months. Maybe years.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can fix that yourself — in a weekend, without being an expert.&lt;/strong&gt; An IDE extension is not a mystical artifact maintained by people smarter than you. It's a small program plus a form that tells the editor where to plug it in. If you can write a script, you can write an extension.&lt;/p&gt;

&lt;p&gt;I recently went through this end to end. I maintain a CLI tool called &lt;a href="https://github.com/encryptioner/branchdiff-releases" rel="noopener noreferrer"&gt;branchdiff&lt;/a&gt; — it runs a local web UI for reviewing pull requests. The UI was fine, but using it meant a terminal, a command, a browser tab, a context switch. So I built a VS Code extension that starts the server, shows the numbers in the sidebar, and embeds the whole UI in an editor tab. Along the way I hit everything the tutorials don't warn you about: a status bar icon that silently refuses to render, links that open &lt;em&gt;nothing&lt;/em&gt; with no error, a marketplace account that needs an Azure subscription before it needs an extension.&lt;/p&gt;

&lt;p&gt;This guide is the general version of that journey — it works for whatever your use case is, and it doesn't stop at VS Code. There's a full field guide to the rest of the editor world (&lt;a href="https://zed.dev" rel="noopener noreferrer"&gt;Zed&lt;/a&gt;, &lt;a href="https://www.jetbrains.com/" rel="noopener noreferrer"&gt;JetBrains&lt;/a&gt;, &lt;a href="https://neovim.io" rel="noopener noreferrer"&gt;Neovim&lt;/a&gt;) near the end, because the most important decision you'll make is &lt;em&gt;which editor to target first&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;If you're not a developer, read the next section anyway — it explains what extensions actually are, in plain language, and honestly, that's the part most developers would benefit from too.&lt;/p&gt;




&lt;h2&gt;
  
  
  What an Extension Actually Is
&lt;/h2&gt;

&lt;p&gt;Strip away the branding and every IDE extension system on earth answers the same three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Where does it plug in?&lt;/strong&gt; — a &lt;em&gt;manifest&lt;/em&gt;, a small config file where you declare: I add a command, a keyboard shortcut, a sidebar panel, a setting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When does it wake up?&lt;/strong&gt; — editors don't run your code constantly. They load it when something you declared happens: the user runs your command, opens a certain file type, or the editor finishes starting up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What can it touch?&lt;/strong&gt; — an API the editor hands your code, and a sandbox deciding how far your code can reach.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the non-developers: think browser extensions, but for the program people &lt;em&gt;write code&lt;/em&gt; in. The editor is like a phone, extensions are apps, and the marketplace is the app store. Same shape, different device. And just like phone apps, most extensions are tiny — one command, one panel, one quality-of-life fix.&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%2F2hpxyagyudfc5wuc78vs.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%2F2hpxyagyudfc5wuc78vs.png" alt="Diagram showing how a VS Code extension plugs into the editor: the package.json manifest declares contributions and activation events, src/extension.ts holds activate and deactivate functions, both feed into the extension host process, which surfaces them as Command Palette entries, status bar items, sidebar panels, and webview tabs" width="800" height="191"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That diagram is the whole mental model for VS Code. Two files do almost everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/strong&gt; — the manifest. It's a form your extension fills out: "here's my command, here's its keyboard shortcut, here's a setting users can tweak."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;src/extension.ts&lt;/code&gt;&lt;/strong&gt; — your code. One function runs when the editor activates you (&lt;code&gt;activate&lt;/code&gt;), one runs on shutdown (&lt;code&gt;deactivate&lt;/code&gt;). Everything else is you calling editor APIs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One detail worth knowing because it makes extensions &lt;em&gt;safe to experiment with&lt;/em&gt;: your code runs in a separate process the editor babysits, called the extension host. If your extension crashes, the editor doesn't. You cannot brick your setup by writing a bad extension. Worst case, you disable it. This is why the barrier to trying is much lower than it looks.&lt;/p&gt;

&lt;h3&gt;
  
  
  The shapes extensions come in
&lt;/h3&gt;

&lt;p&gt;One more thing before the code: extensions come in a handful of well-known shapes, and knowing them makes your own idea easier to place. Almost everything on a marketplace is one of these:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Shape&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;You've met it as&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Command or tool&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adds a button that does a thing&lt;/td&gt;
&lt;td&gt;The shape this post builds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Syntax highlighter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Teaches the editor to colorize a language it doesn't know yet&lt;/td&gt;
&lt;td&gt;Grammar extensions for niche languages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Theme&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Restyles the whole editor — colors, file icons&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://marketplace.visualstudio.com/items?itemName=zhuangtongfa.Material-theme" rel="noopener noreferrer"&gt;One Dark Pro&lt;/a&gt; and ten thousand cousins&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snippet pack&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Three letters expand into thirty lines of boilerplate&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rafce&lt;/code&gt; → a full React component&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Formatter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rewrites your code to one agreed style on save&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://prettier.io" rel="noopener noreferrer"&gt;Prettier&lt;/a&gt;, &lt;a href="https://github.com/psf/black" rel="noopener noreferrer"&gt;Black&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Linter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Draws the squiggles and warnings while you type&lt;/td&gt;
&lt;td&gt;&lt;a href="https://eslint.org" rel="noopener noreferrer"&gt;ESLint&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real language intelligence — autocomplete, go-to-definition, hover docs&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/rust-lang/rust-analyzer" rel="noopener noreferrer"&gt;rust-analyzer&lt;/a&gt;, &lt;a href="https://github.com/microsoft/pyright" rel="noopener noreferrer"&gt;Pyright&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Debugger&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Breakpoints and stepping for a runtime, inside the editor&lt;/td&gt;
&lt;td&gt;Debug adapters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bridge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A doorway to a tool that lives outside the editor&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://www.docker.com" rel="noopener noreferrer"&gt;Docker&lt;/a&gt;, &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.vscode-remoteextensionpack" rel="noopener noreferrer"&gt;Remote SSH&lt;/a&gt;, &lt;a href="https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens" rel="noopener noreferrer"&gt;GitLens&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The good news in that table: &lt;strong&gt;themes and snippet packs are just JSON&lt;/strong&gt;. No code, no API, no extension host — a color palette or a pile of templates in a manifest. Plenty of people earn "published extension author" without writing a line of TypeScript, and that's a perfectly honest way in.&lt;/p&gt;

&lt;p&gt;At the serious end, highlighters and language servers teach an editor an entire language. A highlighter is a grammar — VS Code uses TextMate grammars, while newer editors like Zed use &lt;a href="https://tree-sitter.github.io/tree-sitter/" rel="noopener noreferrer"&gt;tree-sitter&lt;/a&gt;, which understands the actual structure of your code instead of pattern-matching it. A language server speaks the &lt;a href="https://microsoft.github.io/language-server-protocol/" rel="noopener noreferrer"&gt;Language Server Protocol&lt;/a&gt;: write it once, and &lt;em&gt;every&lt;/em&gt; LSP-capable editor on earth can hire it, not just VS Code.&lt;/p&gt;

&lt;p&gt;This post builds the first shape and the last one — the command and the bridge — because that's where "fix my own annoyance" ideas live. But if your itch is colors or boilerplate, congratulations: your weekend just got shorter.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before You Write Code: Audit Your Idea
&lt;/h2&gt;

&lt;p&gt;Most failed extensions die here, not in the code. Two minutes of honesty saves a weekend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your idea is probably good if it's one of these:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A repeated action&lt;/strong&gt; — you do the same 3–4 steps daily (copy file path, transform it, paste it somewhere, open a tool). A command plus a keyboard shortcut turns that into one keypress.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glanceable information&lt;/strong&gt; — build status, server health, ticket numbers, whatever you'd otherwise open a browser tab to check. A status bar item or sidebar row makes it ambient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A bridge&lt;/strong&gt; — a tool you love (a CLI, a local server, an internal dashboard) that lives outside the editor. The extension doesn't replace the tool; it hands you a doorway to it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Your idea is probably bad if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Settings or keybindings already do it.&lt;/strong&gt; Embarrassing numbers of "extensions" are re-implementations of built-in configuration. Search the editor's settings first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An existing extension already does it.&lt;/strong&gt; Search the marketplace twice — once for what it's called, once for what it &lt;em&gt;does&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a whole app.&lt;/strong&gt; If your extension needs its own navigation, accounts, and a database, ask whether it should be a web app with an extension as a thin doorway instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The thin-client principle
&lt;/h3&gt;

&lt;p&gt;When I planned the branchdiff extension, the obvious-looking path was to rebuild the review UI natively inside VS Code — native diffs, native comment threads, the works. The APIs exist. I said no, and it's the decision I'd repeat every time.&lt;/p&gt;

&lt;p&gt;The extension doesn't re-implement anything. It &lt;strong&gt;embeds&lt;/strong&gt; the existing web UI inside an editor tab, and every sidebar row deep-links to the exact section of it. One UI, two viewports. If the editor can host a web view, the same app appears inside it; everywhere else, one click opens the right page in a browser.&lt;/p&gt;

&lt;p&gt;Why this matters for you: if your tool has any real interface, &lt;strong&gt;an extension should be a bridge to it, not a second copy of it.&lt;/strong&gt; Otherwise you're maintaining two frontends forever — and if you ever target a second editor, four. The extension's actual jobs are the cheap ones: start the thing, show the numbers, open the right place. That's a weekend. A native re-implementation is a quarter.&lt;/p&gt;




&lt;h2&gt;
  
  
  The VS Code Path
&lt;/h2&gt;

&lt;p&gt;VS Code first, for three reasons: the API is genuinely pleasant, TypeScript does half the work for you, and &lt;strong&gt;one VS Code extension covers way more editors than VS Code.&lt;/strong&gt; &lt;a href="https://cursor.com" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt;, &lt;a href="https://windsurf.com" rel="noopener noreferrer"&gt;Windsurf&lt;/a&gt;, &lt;a href="https://vscodium.com" rel="noopener noreferrer"&gt;VSCodium&lt;/a&gt;, and friends are all built on VS Code's open-source core, and they install extensions from the same second store. Build once, reach nearly everyone. (Details in the shipping section.)&lt;/p&gt;

&lt;h3&gt;
  
  
  The fifteen-minute skeleton
&lt;/h3&gt;

&lt;p&gt;You need Node.js installed. Then, in an empty folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;--package&lt;/span&gt; yo &lt;span class="nt"&gt;--package&lt;/span&gt; generator-code &lt;span class="nt"&gt;--&lt;/span&gt; yo code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the official scaffolder. It asks a handful of questions — name, identifier, TypeScript (say yes) — and hands you a working extension with a sample command.&lt;/p&gt;

&lt;p&gt;To see it alive: open the folder in VS Code and press &lt;strong&gt;F5&lt;/strong&gt;. A &lt;em&gt;second&lt;/em&gt; VS Code window appears — the Extension Development Host — with your extension loaded in it. Press &lt;code&gt;Ctrl+Shift+P&lt;/code&gt; (that menu is the Command Palette), type your command's name, run it. A notification pops up. Congratulations, that's the whole loop.&lt;/p&gt;

&lt;p&gt;You'll live in this loop: edit, F5, test in the host window, &lt;code&gt;Developer: Reload Window&lt;/code&gt; to pick up changes. It's fast and it's the same for every extension you'll ever write. No emulator downloads, no signing certs, no device registration. Compare that to any other platform you've developed for.&lt;/p&gt;

&lt;h3&gt;
  
  
  The manifest: where half the work happens
&lt;/h3&gt;

&lt;p&gt;Here's the part that surprises people: a lot of "extension development" is filling in &lt;code&gt;package.json&lt;/code&gt;. You don't code a command palette entry or a keyboard shortcut — you &lt;em&gt;declare&lt;/em&gt; them, and the editor builds them for you:&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;"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;"deploy-buddy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publisher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"yourname"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"engines"&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;"vscode"&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.85.0"&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;"activationEvents"&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;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./out/extension.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contributes"&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;"commands"&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;"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;"deploybuddy.deploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deploy current branch"&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;"keybindings"&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;"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;"deploybuddy.deploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ctrl+alt+d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"when"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"editorTextFocus"&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;"configuration"&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Deploy Buddy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"deploybuddy.environmentUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"default"&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://staging.example.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;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Where deployments go"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That JSON gives you: a Command Palette entry, a keyboard shortcut that only fires when you're focused in the editor, and a settings screen — the settings UI is generated from that &lt;code&gt;configuration&lt;/code&gt; block, free.&lt;/p&gt;

&lt;p&gt;Two manifest notes that save real pain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;engines.vscode&lt;/code&gt; must match the editor you test in.&lt;/strong&gt; A mismatch is the number-one reason a fresh extension's command simply doesn't appear. If your command is missing, check this before anything else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modern VS Code infers most activation events&lt;/strong&gt; from what you contribute — declare a command and the editor knows to load you when it runs. You rarely hand-write &lt;code&gt;activationEvents&lt;/code&gt; anymore; an empty array is normal for command-driven extensions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Your first command
&lt;/h3&gt;

&lt;p&gt;The code side, complete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vscode&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ExtensionContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;disposable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;registerCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deploybuddy.deploy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;async &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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;editor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;activeTextEditor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;editor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showErrorMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Open a file first&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;branch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;git.currentBranch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showInformationMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Deploying &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;…`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// your actual work here&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;disposable&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;deactivate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it and notice what it &lt;em&gt;isn't&lt;/em&gt;: there's no framework, no build orchestration you must understand, no lifecycle diagram to memorize. The editor calls &lt;code&gt;activate&lt;/code&gt;, you register things, done. &lt;code&gt;subscriptions.push&lt;/code&gt; is just "clean this up when I'm unloaded" — copy the pattern and move on.&lt;/p&gt;

&lt;h3&gt;
  
  
  The five surfaces you'll actually use
&lt;/h3&gt;

&lt;p&gt;The VS Code API is huge, but almost every useful extension is built from five surfaces. Know these and you can read anyone's extension:&lt;/p&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;What it is&lt;/th&gt;
&lt;th&gt;Use it for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Command Palette&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Ctrl+Shift+P&lt;/code&gt; entries&lt;/td&gt;
&lt;td&gt;Anything the user does occasionally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Quick Pick&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;showQuickPick()&lt;/code&gt; — the fuzzy-search dropdown&lt;/td&gt;
&lt;td&gt;Picking one thing from a list: environments, branches, tickets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Status bar&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;createStatusBarItem()&lt;/code&gt; — text in the bottom bar&lt;/td&gt;
&lt;td&gt;One number or word, always visible: build status, server health&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sidebar panel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;createTreeView()&lt;/code&gt; — a tree in the activity bar&lt;/td&gt;
&lt;td&gt;Structured info: multiple items, live counts, expandable rows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Webview&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;createWebviewPanel()&lt;/code&gt; — a real browser tab &lt;em&gt;inside&lt;/em&gt; the editor&lt;/td&gt;
&lt;td&gt;Anything visual: dashboards, forms, embedding an existing web UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A status bar item is three lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createStatusBarItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;vscode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StatusBarAlignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;$(rocket) staging&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tooltip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Current deploy target — click to switch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;deploybuddy.deploy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;$(rocket)&lt;/code&gt; is a &lt;strong&gt;codicon&lt;/strong&gt; — a built-in icon from VS Code's icon font. Remember that phrase; it stars in the gotchas section.&lt;/p&gt;

&lt;p&gt;If your extension wraps an existing local web tool, the webview is the finish line: point an iframe in a webview panel at &lt;code&gt;http://localhost:PORT&lt;/code&gt; and your entire UI now lives in an editor tab. Chromium treats localhost as trustworthy, so the http iframe doesn't trip security rules.&lt;/p&gt;

&lt;p&gt;One refinement worth stealing: reuse a single "main" panel and repoint its &lt;code&gt;src&lt;/code&gt; on navigation instead of spawning a tab per click — it behaves like a browser tab, not a pile of them. &lt;em&gt;Embed, don't rebuild&lt;/em&gt; — that's the thin-client principle in code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lessons that cost me an afternoon each
&lt;/h3&gt;

&lt;p&gt;These are the things the docs mention in one buried line and reality teaches loudly. Consider this section the fee I already paid on your behalf.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Status bar icons are codicons only.&lt;/strong&gt; &lt;code&gt;StatusBarItem&lt;/code&gt; cannot render a custom SVG — the API simply has no &lt;code&gt;iconPath&lt;/code&gt; on it. You use the built-in codicon font (&lt;code&gt;$(rocket)&lt;/code&gt;, &lt;code&gt;$(git-compare)&lt;/code&gt;, &lt;code&gt;$(sync)&lt;/code&gt;) or you use text. Custom icons go in the sidebar and editor tabs, which &lt;em&gt;do&lt;/em&gt; accept SVGs — but the rules differ per surface: the activity bar alpha-masks your SVG and repaints it in the theme color (draw with &lt;code&gt;currentColor&lt;/code&gt; and punched holes), while editor-tab icons want a &lt;code&gt;{light, dark}&lt;/code&gt; pair because the tab background flips with the theme. I verified this the hard way, against the latest type definitions, after wondering why nothing rendered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Manifest changes sometimes need a full restart, not a reload.&lt;/strong&gt; Some &lt;code&gt;contributes&lt;/code&gt; entries — icons, view containers, menus — are only read when the extension host starts. &lt;code&gt;Developer: Reload Window&lt;/code&gt; picks up your &lt;em&gt;code&lt;/em&gt;; if a new icon or view stubbornly refuses to appear, fully quit the host window and relaunch. This one had me convinced my icon path was wrong for far too long.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webviews are sandboxed, and they fail silently.&lt;/strong&gt; A webview is a locked-down browser. &lt;code&gt;window.open()&lt;/code&gt; and &lt;code&gt;target="_blank"&lt;/code&gt; links do &lt;em&gt;nothing&lt;/em&gt; — no error, no console spam, just silence. If your embedded UI needs to open things, your webview code must &lt;code&gt;postMessage&lt;/code&gt; the URL to the extension, which decides: same-server link → open a new webview tab; external link → &lt;code&gt;vscode.env.openExternal()&lt;/code&gt;. Also set &lt;code&gt;retainContextWhenHidden: true&lt;/code&gt; when you create the panel, or the webview's state resets every time the user switches tabs. There's a memory cost; for a dashboard it's worth it.&lt;/p&gt;

&lt;p&gt;Two more traps live inside that same iframe. Keystrokes land in it, where the editor's keybindings can't see them — your embedded app has to forward the workbench escapes (&lt;code&gt;Ctrl+Shift+P&lt;/code&gt;, &lt;code&gt;Ctrl+P&lt;/code&gt;, the sidebar toggles) to the extension over &lt;code&gt;postMessage&lt;/code&gt;, and the extension replays a short whitelist of commands, nothing more.&lt;/p&gt;

&lt;p&gt;And the editor's theme only reaches the iframe after a message round trip, so a fresh load briefly falls back to the &lt;em&gt;OS&lt;/em&gt; theme — pass it in the iframe's URL (&lt;code&gt;?theme=dark&lt;/code&gt;) and apply it with an inline script before your stylesheets load, or dark-theme editors on light-OS machines flash the wrong theme on first paint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows spawns need a shell.&lt;/strong&gt; If your extension launches a local tool and it works on your Mac but dies for Windows users: npm-installed CLIs are &lt;code&gt;.cmd&lt;/code&gt; shim files on Windows, and those can't be spawned directly. Pass &lt;code&gt;shell: true&lt;/code&gt; in your spawn options. This is the classic "works on my machine" bug of extension development, and Windows users will find it for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adopt, don't duplicate, long-running processes.&lt;/strong&gt; If your extension starts a server, check whether one is already running for this workspace first, and attach to it. Otherwise every window reload spawns another copy, ports fight, and users end up with zombie processes. My extension reads a small registry file the CLI maintains; a healthy match means adopt, otherwise spawn. Ten lines of code, entire class of bugs gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One source of truth for repeated lists.&lt;/strong&gt; I had the same menu in two places — a sidebar panel and a quick pick — hand-maintained separately. They drifted: the quick pick was missing rows and had a copy-pasted wrong icon on one entry. The fix was boring and permanent: both surfaces now render from one array in one file. Any list that appears twice in your UI will drift; make it appear once in code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing like it matters
&lt;/h3&gt;

&lt;p&gt;The F5 loop is for your eyes. For real coverage, VS Code ships a test runner that downloads an actual VS Code binary and runs your tests inside the real extension host — not a mock, the actual editor, headless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; @vscode/test-electron
&lt;span class="c"&gt;# package.json: "test": "node out/test/runTest.js" (the scaffold has this)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it's "a real editor, headless," it also runs in CI on all three operating systems. If you ship to strangers, do this: a test that asserts your commands are registered and your endpoints answer catches the entire class of "the extension loaded but nothing works" bug reports.&lt;/p&gt;

&lt;p&gt;One quirk to know before it bites: the test host rejects a module that merely runs itself — it must export a &lt;code&gt;run()&lt;/code&gt; function. Then keep a small manual checklist for what automation can't see — icons rendering, panels looking right, keyboard shortcuts firing. Two lists, both short.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shipping It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Start with just a file: the &lt;code&gt;.vsix&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Everything in VS Code extension land packages into one file format: &lt;code&gt;.vsix&lt;/code&gt;. It's a zip with your code and manifest inside, installable directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @vscode/vsce
vsce package
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now there's a &lt;code&gt;your-extension-1.0.0.vsix&lt;/code&gt; file. Anyone — including future you — installs it via the Extensions panel's &lt;code&gt;⋯&lt;/code&gt; menu → &lt;strong&gt;Install from VSIX…&lt;/strong&gt;, or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code &lt;span class="nt"&gt;--install-extension&lt;/span&gt; your-extension-1.0.0.vsix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not skip past this. &lt;strong&gt;A &lt;code&gt;.vsix&lt;/code&gt; file is a completely legitimate end state.&lt;/strong&gt; Your team's shared tooling, an internal extension that will never be public, a personal scratch extension for your own workflow — package it, drop it in a shared drive or a GitHub release, done. Two things to know at this stage: a &lt;code&gt;.vscodeignore&lt;/code&gt; file keeps junk out of the package (only ship your &lt;code&gt;dist/&lt;/code&gt; bundle, manifest, README, license), and manually installed &lt;code&gt;.vsix&lt;/code&gt; files never auto-update — installing the next version's file over it just works.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Marketplace — and the Azure detour nobody warns you about
&lt;/h3&gt;

&lt;p&gt;The main app store is the Visual Studio Marketplace. Publishing is one command (&lt;code&gt;vsce publish&lt;/code&gt;) behind two accounts, and the accounts are the hard part:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a publisher&lt;/strong&gt; on the Marketplace — an ID that permanently identifies you and your extensions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a Personal Access Token on Azure DevOps.&lt;/strong&gt; Yes, Azure. The Marketplace's auth lives there, not on the Marketplace site. The token must be scoped to &lt;em&gt;All accessible organizations&lt;/em&gt; with the &lt;em&gt;Marketplace → Manage&lt;/em&gt; scope — pick anything else and you get a bare 403 that doesn't tell you why.&lt;/li&gt;
&lt;li&gt;And the genuinely absurd part: &lt;strong&gt;creating an Azure DevOps organization can require linking an Azure subscription&lt;/strong&gt;, and even a free-tier Azure account wants a card for identity verification. To publish a free extension. Plan for this detour; it's the single most common place extension publishing stalls out.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One more thing to calendar: &lt;strong&gt;Microsoft is retiring global PATs on December 1, 2026&lt;/strong&gt;, and the going-forward path is Entra ID-based auth (workload identity federation in CI, &lt;code&gt;vsce publish --azure-credential&lt;/code&gt;). If you're setting up publishing fresh, set it up the new way from day one and skip the token-renewal treadmill entirely.&lt;/p&gt;

&lt;p&gt;Marketplace rules that bite people, from the official docs and from experience:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Versions must strictly increase.&lt;/strong&gt; Published a broken version? You can't overwrite it — you fix it by shipping the &lt;em&gt;next&lt;/em&gt; version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The listing is a snapshot.&lt;/strong&gt; Your README, icon, and description on the store update only when you publish. There is no "refresh listing" button.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No SVGs in listings&lt;/strong&gt; — icon, badges, and README images must be PNG/other raster with https URLs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Removal is forever.&lt;/strong&gt; Unpublishing removes the extension from every user, and extension names stay reserved after removal. Prefer leaving an old version up.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Open VSX: one extra step, five more editors
&lt;/h3&gt;

&lt;p&gt;Microsoft's Marketplace terms only allow &lt;em&gt;Microsoft builds&lt;/em&gt; of VS Code to use it. &lt;a href="https://cursor.com" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt;, &lt;a href="https://windsurf.com" rel="noopener noreferrer"&gt;Windsurf&lt;/a&gt;, &lt;a href="https://vscodium.com" rel="noopener noreferrer"&gt;VSCodium&lt;/a&gt;, &lt;a href="https://www.gitpod.io" rel="noopener noreferrer"&gt;Gitpod&lt;/a&gt;, &lt;a href="https://theia-ide.org" rel="noopener noreferrer"&gt;Theia&lt;/a&gt; — all built on VS Code's open-source core, all banned from the main store. They share a second store instead: &lt;strong&gt;&lt;a href="https://open-vsx.org" rel="noopener noreferrer"&gt;Open VSX&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And it takes the &lt;em&gt;same &lt;code&gt;.vsix&lt;/code&gt;&lt;/em&gt; you already built. One package, two stores, every major editor covered:&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%2Frdjnjiuqdpoevjrklmby.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%2Frdjnjiuqdpoevjrklmby.png" alt="Diagram showing one vsix file flowing to three destinations: the VS Code Marketplace serving VS Code users, Open VSX serving Cursor, Windsurf, VSCodium, Gitpod, Theia and Antigravity users, and direct Install-from-VSX reaching any editor without a store, with Open VSX also noted as usable by VS Code itself" width="715" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open VSX setup is its own small saga, so you don't hit it blind: you sign in with GitHub, then discover that's not enough — you need an &lt;strong&gt;Eclipse Foundation account&lt;/strong&gt; (separate registration), sign the publisher agreement, and &lt;em&gt;link your GitHub account inside your Eclipse profile&lt;/em&gt;. Skip that last link and login fails with a bare redirect that looks like an OAuth bug. The actual reason lives in a background request (&lt;code&gt;eclipse-missing-github-id&lt;/code&gt;) that the page never shows you.&lt;/p&gt;

&lt;p&gt;Once in: create a namespace matching your publisher ID exactly, generate a token, &lt;code&gt;npx ovsx publish&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the entire distribution strategy for a solo developer: build one &lt;code&gt;.vsix&lt;/code&gt;, publish it twice, and every editor from stock VS Code to Cursor can install your work.&lt;/p&gt;




&lt;h2&gt;
  
  
  Beyond VS Code: A Field Guide
&lt;/h2&gt;

&lt;p&gt;VS Code has the gentlest on-ramp, but it's not the only editor people love. Here's the honest state of each — what you can build, what you can't, and what it costs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zed: small, sharp, sandboxed
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://zed.dev" rel="noopener noreferrer"&gt;Zed&lt;/a&gt; takes the opposite bet from VS Code on extension power. Extensions are written in &lt;strong&gt;Rust&lt;/strong&gt;, compiled to &lt;strong&gt;WebAssembly&lt;/strong&gt;, and run in a sandbox. The manifest is an &lt;code&gt;extension.toml&lt;/code&gt; file; you test locally by installing your folder as a "dev extension," and publishing means opening a pull request against Zed's central extensions repository — with a license file at the root of your repo (open source is required) and CI checking your submission.&lt;/p&gt;

&lt;p&gt;What Zed extensions can be: &lt;strong&gt;language support&lt;/strong&gt; (tree-sitter grammars, language servers you can download on demand), &lt;strong&gt;themes and icon themes&lt;/strong&gt;, &lt;strong&gt;snippets&lt;/strong&gt;, &lt;strong&gt;debug adapters&lt;/strong&gt;, and &lt;strong&gt;MCP context servers&lt;/strong&gt; — the integration path for connecting Zed's assistant to your own tools.&lt;/p&gt;

&lt;p&gt;What they still can't be: &lt;strong&gt;anything with its own UI.&lt;/strong&gt; No sidebars, no panels, no status bar items of your own, and no general "draw a window" API. Extensions &lt;em&gt;can&lt;/em&gt; reach outside the sandbox a little, through narrowly-scoped capability grants — running specific commands (&lt;code&gt;process:exec&lt;/code&gt;), downloading files from specific hosts, installing named npm packages — but with nowhere to render a result, that power changes little. The Visual Extension API is an open proposal; until it lands, Zed extensions are ingredients, not apps.&lt;/p&gt;

&lt;p&gt;So what do you do if you love Zed and want your tool closer? Two honest answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tasks.json&lt;/code&gt;.&lt;/strong&gt; Zed's task runner can execute your CLI with a keystroke, and its terminal renders printed &lt;code&gt;http://localhost:…&lt;/code&gt; URLs as clickable links. A config file, not code — and suddenly your tool is one keypress away, opening in the browser at exactly the right page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An MCP context server.&lt;/strong&gt; If the integration should be &lt;em&gt;smart&lt;/em&gt; — tools and prompts the assistant can call — that's the sanctioned path, and it's genuinely useful even without any custom UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is exactly the route I mapped for branchdiff-in-Zed: no extension at all, just task definitions and clickable URLs. Sometimes the right extension is a config file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cursor, Windsurf, VSCodium: already done
&lt;/h3&gt;

&lt;p&gt;Nothing new to build. They're VS Code-family editors pulling from Open VSX — your published extension reaches them the moment it's on that second store. One &lt;code&gt;.vsix&lt;/code&gt; installs into any of them directly too (&lt;code&gt;cursor --install-extension …&lt;/code&gt; works exactly like &lt;code&gt;code --install-extension …&lt;/code&gt;). If your extension talks to local servers or CLIs, do test in each — forks have their own quirks — but it's QA work, not development work.&lt;/p&gt;

&lt;h3&gt;
  
  
  JetBrains: a different planet, respectfully
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.jetbrains.com/idea/" rel="noopener noreferrer"&gt;IntelliJ&lt;/a&gt;, &lt;a href="https://www.jetbrains.com/pycharm/" rel="noopener noreferrer"&gt;PyCharm&lt;/a&gt;, &lt;a href="https://www.jetbrains.com/webstorm/" rel="noopener noreferrer"&gt;WebStorm&lt;/a&gt; and the family run on the JVM, and so do their plugins — &lt;strong&gt;Kotlin or Java&lt;/strong&gt;, built with Gradle via the IntelliJ Platform Gradle Plugin (2.x is the current line; the old DevKit workflow is deprecated), usually starting from JetBrains' official plugin template. Publishing goes to the JetBrains Marketplace.&lt;/p&gt;

&lt;p&gt;The trade is the inverse of Zed's: you get &lt;em&gt;full&lt;/em&gt; power — plugins there are first-class citizens that can rebuild any part of the IDE's UI — in exchange for a much heavier platform. You're learning a desktop-application framework, with the debugger, the IDE setup, and the build times to match. If your day job lives in a JetBrains IDE and your idea needs real UI inside the editor, it's worth the climb. For everyone else, it's the last stop on the tour, not the first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Neovim: no ceremony, by design
&lt;/h3&gt;

&lt;p&gt;Neovim's answer to "what's the extension system?" is "your config &lt;em&gt;is&lt;/em&gt; the extension system." Plugins are Lua modules; there's no manifest, no store, no review — plugins are git repositories, installed by a plugin manager like &lt;a href="https://github.com/folke/lazy.nvim" rel="noopener noreferrer"&gt;lazy.nvim&lt;/a&gt;, loaded by users who opt in. Total freedom, zero hand-holding: you can do anything, including break everything, and packaging/distribution is entirely your problem. If you live in Neovim, a plugin for your own workflow can be an evening; just know you're building for the smallest (but most devoted) audience of this list.&lt;/p&gt;

&lt;h3&gt;
  
  
  The comparison, at a glance
&lt;/h3&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;VS Code family&lt;/th&gt;
&lt;th&gt;&lt;a href="https://zed.dev" rel="noopener noreferrer"&gt;Zed&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://www.jetbrains.com/" rel="noopener noreferrer"&gt;JetBrains&lt;/a&gt;&lt;/th&gt;
&lt;th&gt;&lt;a href="https://neovim.io" rel="noopener noreferrer"&gt;Neovim&lt;/a&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TypeScript/JavaScript&lt;/td&gt;
&lt;td&gt;Rust → WASM&lt;/td&gt;
&lt;td&gt;Kotlin/Java&lt;/td&gt;
&lt;td&gt;Lua&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Custom UI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Webviews, panels, status bar&lt;/td&gt;
&lt;td&gt;None yet (open RFC)&lt;/td&gt;
&lt;td&gt;Full IDE-grade UI&lt;/td&gt;
&lt;td&gt;Full TUI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Distribution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Marketplace + Open VSX&lt;/td&gt;
&lt;td&gt;PR to central repo&lt;/td&gt;
&lt;td&gt;JetBrains Marketplace&lt;/td&gt;
&lt;td&gt;Git repos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Effort to first result&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An evening&lt;/td&gt;
&lt;td&gt;An evening (non-UI)&lt;/td&gt;
&lt;td&gt;A weekend or two&lt;/td&gt;
&lt;td&gt;An evening&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Reach&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every VS Code-based editor&lt;/td&gt;
&lt;td&gt;Zed users&lt;/td&gt;
&lt;td&gt;All JetBrains IDEs&lt;/td&gt;
&lt;td&gt;Neovim users&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  So Which One First?
&lt;/h2&gt;

&lt;p&gt;The decision is less about favorite editor and more about reach-per-effort:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default answer: VS Code, then Open VSX.&lt;/strong&gt; One TypeScript codebase, two publish commands, and you've covered stock VS Code &lt;em&gt;plus&lt;/em&gt; Cursor, Windsurf, and VSCodium — the largest developer audience on earth for extension work. Nothing else comes close.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If your team lives in JetBrains IDEs&lt;/strong&gt; and the idea needs to live inside the editor, go Kotlin and budget the learning curve — it's the only sanctioned way in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If your tool's real home is a web UI&lt;/strong&gt;, build the thinnest VS Code bridge (embed + deep links), and for Zed don't build at all — tasks plus clickable URLs cover it today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If it's just for you&lt;/strong&gt;, the calculus collapses: build for whatever you actually use, ship it as a &lt;code&gt;.vsix&lt;/code&gt; or a config file, skip the stores entirely. Software that serves one user well is not a failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And whatever you pick — start absurdly small. One command that does one thing you actually do daily. Ship that, use it for a week, and let the extension tell you what it wants to become next. The branchdiff extension started as "start the server without typing a command." The sidebar counts, the embedded tab, the quick picks — those were all &lt;em&gt;pull&lt;/em&gt;, added because daily use kept asking for them. I've never seen an extension ruined by starting too small. The big-bang ones, I've seen plenty.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;The gap between "my editor should do that" and "my editor does that" is shorter than it's ever looked. A manifest and one function gets you running; five surfaces cover almost every idea; a &lt;code&gt;.vsix&lt;/code&gt; file is a real distribution channel even if you never touch a store; and two stores get you nearly every editor people use in 2026.&lt;/p&gt;

&lt;p&gt;The concrete path from here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Today:&lt;/strong&gt; run &lt;code&gt;npx --package yo --package generator-code -- yo code&lt;/code&gt;, press F5, change the sample command's message to something that makes you smile. You're now an extension developer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This week:&lt;/strong&gt; pick the one repetitive thing you actually do daily, and make a command for it. Add a keybinding in the manifest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When it's useful:&lt;/strong&gt; &lt;code&gt;vsce package&lt;/code&gt; and install the &lt;code&gt;.vsix&lt;/code&gt; properly — you'll feel the difference the first time it's just &lt;em&gt;there&lt;/em&gt; after a restart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the world needs it:&lt;/strong&gt; publish — Marketplace for VS Code, Open VSX for everything VS Code-shaped, and remember the Azure detour is a rite of passage, not a sign you're doing it wrong.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The most useful software you'll ever write might be the kind only you needed. There's exactly one way to find out.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sources and Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://code.visualstudio.com/api/get-started/your-first-extension" rel="noopener noreferrer"&gt;Your First Visual Studio Extension&lt;/a&gt; — official scaffolding-to-F5 walkthrough&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://code.visualstudio.com/api/extension-capabilities/overview" rel="noopener noreferrer"&gt;Extension Capabilities Overview&lt;/a&gt; — the official map of extension kinds: themes, formatters, linters, debuggers, snippets, and the rest&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://code.visualstudio.com/api/working-with-extensions/publishing-extension" rel="noopener noreferrer"&gt;Publishing Extensions&lt;/a&gt; — vsce, publishers, tokens, and the PAT-to-Entra transition&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://open-vsx.org" rel="noopener noreferrer"&gt;Open VSX&lt;/a&gt; — the marketplace for VS Code-family editors&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://zed.dev/docs/extensions/capabilities" rel="noopener noreferrer"&gt;Zed Extension Capabilities&lt;/a&gt; and the &lt;a href="https://zed.dev/docs/extensions/publishing/publishing-guide.html" rel="noopener noreferrer"&gt;Zed Publishing Guide&lt;/a&gt; — what sandboxed extensions may do and how submissions work&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/zed-industries/zed/discussions/53403" rel="noopener noreferrer"&gt;Visual Extension API RFC (Zed discussion #53403)&lt;/a&gt; — the still-open proposal for custom UI in Zed&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://plugins.jetbrains.com/docs/intellij/developing-plugins.html" rel="noopener noreferrer"&gt;IntelliJ Platform Plugin SDK&lt;/a&gt; and the &lt;a href="https://github.com/JetBrains/intellij-platform-gradle-plugin" rel="noopener noreferrer"&gt;IntelliJ Platform Gradle Plugin 2.x&lt;/a&gt; — the JetBrains path&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/encryptioner/branchdiff-releases" rel="noopener noreferrer"&gt;branchdiff's extension R&amp;amp;D notes&lt;/a&gt; — the full engineering log behind the case study in this post&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  End:
&lt;/h2&gt;

&lt;p&gt;That's all!&lt;/p&gt;

&lt;p&gt;I hope you've found the article useful. You should try building your own extension if you haven't already — start with the one command you actually need. Feel free to share your thoughts and what you built in the comments below.&lt;/p&gt;

&lt;p&gt;Check more on&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://encryptioner.github.io" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://encryptioner.github.io/blogs/" rel="noopener noreferrer"&gt;Blogs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/mir-mursalin-ankur" rel="noopener noreferrer"&gt;Linkedin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Encryptioner" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/AnkurMursalin" rel="noopener noreferrer"&gt;X (Twitter)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nerddevs.com/author/ankur/" rel="noopener noreferrer"&gt;Nerddevs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>vscode</category>
      <category>extensions</category>
      <category>development</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Your Desk in Your Pocket — Free Remote Control for Mac &amp; Ubuntu, Same Room or Anywhere on Earth</title>
      <dc:creator>Mir Mursalin Ankur</dc:creator>
      <pubDate>Thu, 27 Aug 2026 01:56:31 +0000</pubDate>
      <link>https://dev.to/mir_mursalin_ankur/your-desk-in-your-pocket-free-remote-control-for-mac-ubuntu-same-room-or-anywhere-on-earth-l12</link>
      <guid>https://dev.to/mir_mursalin_ankur/your-desk-in-your-pocket-free-remote-control-for-mac-ubuntu-same-room-or-anywhere-on-earth-l12</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;The gap between having a thought worth acting on and acting on it shouldn't cost a walk to the desk — or a flight home.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A build's running upstairs. You're on the couch, and something needs doing — reply to that message, nudge the agent, check whether the tests passed. Later that week you're in a coffee shop, same thought, except now the machine is kilometers away.&lt;/p&gt;

&lt;p&gt;This post builds a phone-to-desktop remote control in three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 1 — Same network.&lt;/strong&gt; macOS Screen Sharing, Ubuntu &lt;code&gt;x11vnc&lt;/code&gt;, one phone app. Set this up first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 2 — Over the internet.&lt;/strong&gt; The same rig from a coffee shop or hotel, via a mesh VPN. Re-addressing, not a rebuild.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3 — Deep dive.&lt;/strong&gt; The protocol, the 8-character password cap, why VNC triggers hotkeys when SSH can't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is free, and once wired, needs nothing further from you. Set it up on a Saturday, and the couch becomes a valid place to get something done — and so does another city.&lt;/p&gt;

&lt;p&gt;The phone keyboard's own &lt;strong&gt;voice typing works straight through this rig&lt;/strong&gt; (Part 1), so dictation comes along for free.&lt;/p&gt;

&lt;p&gt;The one table worth internalizing — &lt;strong&gt;the same phone app drives both machines&lt;/strong&gt;, one saved entry per box:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;The job&lt;/th&gt;
&lt;th&gt;Ubuntu machine&lt;/th&gt;
&lt;th&gt;macOS machine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Share + control the screen&lt;/td&gt;
&lt;td&gt;x11vnc&lt;/td&gt;
&lt;td&gt;built-in Screen Sharing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phone VNC client&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;RealVNC Viewer&lt;/strong&gt; (Android/iOS)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;RealVNC Viewer&lt;/strong&gt; — same app, second entry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Address (Part 1)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;ubuntu-lan-ip&amp;gt;:5900&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;mac-lan-ip&amp;gt;:5900&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Address (Part 2)&lt;/td&gt;
&lt;td&gt;tailnet IP, permanent&lt;/td&gt;
&lt;td&gt;tailnet IP, permanent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;x11vnc -storepasswd&lt;/code&gt; one&lt;/td&gt;
&lt;td&gt;the 8-character VNC password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS-specific extras&lt;/td&gt;
&lt;td&gt;none needed&lt;/td&gt;
&lt;td&gt;none needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Security and license notes for every tool close the post.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1 — Same network: the phone drives the desktop
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why VNC, and not just SSH
&lt;/h3&gt;

&lt;p&gt;If you've remoted into a machine before, it was probably SSH — and SSH can't do this job. An SSH session drops you into a &lt;em&gt;new&lt;/em&gt; text shell: it can't show you the desktop you left running, can't click anything, can't type into the apps already open, and can't fire global hotkeys. It's a parallel door into the basement.&lt;/p&gt;

&lt;p&gt;VNC is a mirror plus a hand: the phone sees the &lt;strong&gt;actual desktop&lt;/strong&gt; — your apps, your windows, exactly as you left them — and every tap and keystroke is injected as a &lt;strong&gt;real input event&lt;/strong&gt;, indistinguishable to every app and every hotkey listener from sitting at the keyboard. That property is what makes the whole rig work: switch apps, run shortcuts, check a build, drag a slider — all with the same mechanism you'd use in person.&lt;/p&gt;

&lt;p&gt;Both halves are standard: macOS and Ubuntu can each serve VNC with built-in or one-command-free tooling, and any standards-compliant client can connect. That's why one phone app is enough for both machines.&lt;/p&gt;

&lt;h3&gt;
  
  
  The macOS machine: Screen Sharing, already installed
&lt;/h3&gt;

&lt;p&gt;The Mac ships with the VNC server — this is mostly a matter of turning it on. This route was shaken down live on a Sequoia Mac with an Android phone in hand: Screen Sharing answering on port 5900, a phone client driving the screen, everything below marked "tested live" came off that machine.&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%2Ftnw5znzlwqmiu02cco73.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%2Ftnw5znzlwqmiu02cco73.png" alt="Diagram: Android phone running RealVNC Viewer connects over home WiFi to a Mac. Touch and soft-keyboard input from the client is carried by the VNC protocol over port 5900 into macOS's built-in Screen Sharing server, which injects real mouse and keyboard events into the desktop input stack — GUI apps, terminals, and any global hotkey listener all respond as if the inputs were physical. Keyboard commits — typing, glide input, voice typing — send as keystrokes." width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The macOS control path — the server is already installed; one toggle and a VNC password turn it on.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;System Settings → General → Sharing → Screen Sharing&lt;/strong&gt; — flip it on. Keep it plain Screen Sharing: if &lt;strong&gt;Remote Management&lt;/strong&gt; is the enabled one instead, legacy-VNC clients get served the login window even while you're logged in, and typing into that over VNC drops focus (tested live — Remote Management off, Screen Sharing on, and the phone landed straight on the desktop; logging in over VNC works too, for the after-reboot case).&lt;/li&gt;
&lt;li&gt;For a &lt;em&gt;third-party&lt;/em&gt; client (RealVNC Viewer — anything not Apple's own Screen Sharing app), click the ⓘ next to Screen Sharing and enable &lt;strong&gt;"VNC viewers may control screen with password"&lt;/strong&gt;, then set that password. Without it, macOS expects Apple's own authentication handshake and most phone clients fail to connect at all.&lt;/li&gt;
&lt;li&gt;Connect from the phone to &lt;code&gt;&amp;lt;mac-lan-ip&amp;gt;:5900&lt;/code&gt;, password = the VNC password from step 2. Same separate-from-WiFi-password logic as below on Ubuntu — it controls who can drive the Mac, not who can join the network.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Or skip the GUI for step 1: &lt;code&gt;sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.screensharing.plist&lt;/code&gt; — but step 2 has no equally clean CLI path, so most of this toggle lives in System Settings either way. There's also a CLI for the whole thing (&lt;code&gt;.../ARDAgent.app/Contents/Resources/kickstart -activate -configure -access -on -clientopts -setvnclegacy -vnclegacy yes -setvncpw &amp;lt;pw&amp;gt; -restart -agent -privs -all&lt;/code&gt;), but on Sequoia it sets privileges and the password while &lt;strong&gt;refusing to actually open the service&lt;/strong&gt; — it prints &lt;em&gt;"Screen Sharing or Remote Management must be enabled from System Settings or via MDM"&lt;/em&gt; and the port stays closed until you flip the GUI toggle once. Count on ending in System Settings regardless.&lt;/p&gt;

&lt;p&gt;Three auth behaviors worth knowing before a phone client rejects a correct-looking password:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The VNC password is effectively 8 characters.&lt;/strong&gt; System Settings happily accepts a longer one, but legacy VNC auth is a &lt;a href="https://datatracker.ietf.org/doc/html/rfc6143#section-7.2.2" rel="noopener noreferrer"&gt;DES challenge-response&lt;/a&gt; capped at 8 — if the client keeps rejecting the password you set, the first 8 characters are the password. (Part 3 opens up the mechanism, for the curious.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS serves two auth types side by side&lt;/strong&gt;: Mac account auth (a username + your Mac login password, Apple's ARD-style handshake) &lt;em&gt;and&lt;/em&gt; the legacy VNC password. A client looping on "enter VNC credentials" is often answering the wrong one — the server offers both, and which one you get depends on the client's negotiation. Note Screen Sharing and Remote Management each have their &lt;strong&gt;own&lt;/strong&gt; VNC-password setting; only the one for the service you actually enabled is consulted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RealVNC Viewer connects with the plain VNC password, username blank&lt;/strong&gt; — tested live. That's the configuration this post standardizes on, and it's why the same app works against the Ubuntu box too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you &lt;em&gt;don't&lt;/em&gt; need, relative to Ubuntu below: no display-number hunting, no Xauthority chase, no autostart file, no reconnect/key-repeat flag tuning — Apple's server handles those correctly by default. And one genuine upgrade: Screen Sharing is a system daemon that serves the &lt;strong&gt;login window&lt;/strong&gt; too, so after a reboot you can VNC in and log in remotely — the Ubuntu route can't do that (x11vnc only starts after a graphical login).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Ubuntu machine: x11vnc
&lt;/h3&gt;

&lt;p&gt;x11vnc shares and drives the &lt;strong&gt;existing&lt;/strong&gt; desktop session — the one you're actually logged into, with your apps already open — rather than spawning a fresh virtual one. It's in the standard repos:&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%2Fvwz2tx1mulioo8ot4i10.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%2Fvwz2tx1mulioo8ot4i10.png" alt="Diagram: Android phone running RealVNC Viewer connects over home WiFi to an Ubuntu desktop. Touch and soft-keyboard input from the client is carried by the VNC protocol over port 5900 into x11vnc, which injects real mouse and keyboard events into the existing X11 session — GUI apps, terminals, and any global hotkey listener all respond as if the inputs were physical." width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Ubuntu control path — x11vnc turns the desktop session you're already logged into into a VNC server.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; x11vnc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Run it
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;x11vnc &lt;span class="nt"&gt;-display&lt;/span&gt; :&amp;lt;real-display-number&amp;gt; &lt;span class="nt"&gt;-auth&lt;/span&gt; guess &lt;span class="nt"&gt;-usepw&lt;/span&gt; &lt;span class="nt"&gt;-shared&lt;/span&gt; &lt;span class="nt"&gt;-repeat&lt;/span&gt; &lt;span class="nt"&gt;-forever&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find &lt;code&gt;&amp;lt;real-display-number&amp;gt;&lt;/code&gt; with &lt;code&gt;who&lt;/code&gt; (look for &lt;code&gt;&amp;lt;user&amp;gt;  :N  &amp;lt;date&amp;gt;&lt;/code&gt;) from a terminal that's actually part of the graphical session.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First run is interactive: no password file exists yet, so &lt;code&gt;-usepw&lt;/code&gt; prompts you to set one. Always do this — never run x11vnc without auth.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-forever&lt;/code&gt; keeps listening after the first client disconnects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-shared&lt;/code&gt; matters.&lt;/strong&gt; Without it, x11vnc allows exactly one client at a time — a stale half-open connection from a WiFi blip or app switch makes every reconnect fail with &lt;code&gt;refusing new client ... Not shared &amp;amp; existing client&lt;/code&gt; until you restart the server. With it, reconnects just work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-repeat&lt;/code&gt; matters too.&lt;/strong&gt; x11vnc turns &lt;em&gt;off&lt;/em&gt; the X server's key autorepeat by default while a client keyboard is active — its log even says so (&lt;code&gt;active keyboard: turning X autorepeat off&lt;/code&gt;). Without this flag, holding an arrow key (or any key) from the phone does nothing beyond the first tap — no repeat, one character per touch, forever. &lt;code&gt;-repeat&lt;/code&gt; keeps autorepeat on so a held key behaves like a physical one. Only drop it if your specific client does its own repeat and you start seeing doubled characters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-auth guess&lt;/code&gt;&lt;/strong&gt; (x11vnc ≥0.9.9) finds the right Xauthority cookie itself. Modern GNOME/gdm sessions often keep it at &lt;code&gt;/run/user/&amp;lt;uid&amp;gt;/gdm/Xauthority&lt;/code&gt;, not the default &lt;code&gt;~/.Xauthority&lt;/code&gt; — hardcoding the default path is a common source of a silent auth failure.&lt;/li&gt;
&lt;li&gt;There's no &lt;code&gt;-localhost=no&lt;/code&gt; flag — that's not real; &lt;code&gt;-localhost&lt;/code&gt; (no value) &lt;em&gt;restricts&lt;/em&gt; to loopback, and passing &lt;code&gt;=no&lt;/code&gt; gets rejected as unrecognized. Allowing LAN connections is the default when you omit the flag entirely.&lt;/li&gt;
&lt;li&gt;Scope the port to your LAN: &lt;code&gt;sudo ufw allow from &amp;lt;your-subnet&amp;gt; to any port 5900&lt;/code&gt; — never expose it to the internet. If you've never touched &lt;code&gt;ufw&lt;/code&gt; before and only use SSH &lt;em&gt;outbound&lt;/em&gt; to other machines (not inbound into this one), you don't need &lt;code&gt;sudo ufw allow OpenSSH&lt;/code&gt; — that rule only matters for a device that accepts incoming SSH connections; ufw's default policy already allows all outbound traffic regardless of any inbound rule you add.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What the VNC password actually is&lt;/strong&gt; — easy to conflate with the WiFi password, but it's a separate thing: it's set by you via &lt;code&gt;x11vnc -storepasswd&lt;/code&gt; (not your router), it controls who can drive &lt;em&gt;this&lt;/em&gt; desktop once already on the network (not who can join the network), and it doesn't change when you switch WiFi networks — phone and desktop just need to be on the &lt;em&gt;same&lt;/em&gt; network at connection time, whichever one that is. Like the Mac's, it's capped at 8 effective characters by the classic VNC auth protocol.&lt;/p&gt;

&lt;h4&gt;
  
  
  Persist across reboots — the autostart file
&lt;/h4&gt;

&lt;p&gt;There's no separate script to write here — GNOME (and most desktops) autostarts anything dropped as a &lt;code&gt;.desktop&lt;/code&gt; file in &lt;code&gt;~/.config/autostart/&lt;/code&gt;, once per graphical login, which is the earliest point a display actually exists to serve. Set the password once first (&lt;code&gt;x11vnc -storepasswd&lt;/code&gt;), then create this file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.config/autostart/x11vnc.desktop
&lt;/span&gt;&lt;span class="nn"&gt;[Desktop Entry]&lt;/span&gt;
&lt;span class="py"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;Application&lt;/span&gt;
&lt;span class="py"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;x11vnc&lt;/span&gt;
&lt;span class="py"&gt;Exec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;x11vnc -display :&amp;lt;N&amp;gt; -auth guess -rfbauth /home/&amp;lt;you&amp;gt;/.vnc/passwd -shared -repeat -forever&lt;/span&gt;
&lt;span class="py"&gt;X-GNOME-Autostart-enabled&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;N&amp;gt;&lt;/code&gt; is your real display number from &lt;code&gt;who&lt;/code&gt; (see above); &lt;code&gt;&amp;lt;you&amp;gt;&lt;/code&gt; is your Linux username — &lt;code&gt;Exec=&lt;/code&gt; needs the absolute path, &lt;code&gt;~&lt;/code&gt; isn't expanded there. &lt;code&gt;X-GNOME-Autostart-enabled=true&lt;/code&gt; is what actually flips it on; a &lt;code&gt;.desktop&lt;/code&gt; file without that line sits inert. Verify it's live any time with &lt;code&gt;pgrep -af x11vnc&lt;/code&gt; after a fresh login — no separate "did it start" step needed beyond that.&lt;/p&gt;

&lt;p&gt;One consequence of the after-login design: if the machine reboots (or loses power) and sits at a lock screen, nothing's listening yet — someone has to log in locally once before the phone can reach it. The Mac's Screen Sharing doesn't have that limitation; this is the one thing it does better.&lt;/p&gt;

&lt;h3&gt;
  
  
  The phone client: RealVNC Viewer (one app, both machines)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.realvnc.com/en/connect/download/viewer/" rel="noopener noreferrer"&gt;RealVNC Viewer&lt;/a&gt;&lt;/strong&gt; (free on Android and iOS) is the client this post standardizes on, for one reason that matters more than any feature list: &lt;strong&gt;it speaks the classic VNC authentication that both servers above serve&lt;/strong&gt;, so the same app — the same UI, the same gestures, the same saved-entries list — drives the Mac and the Ubuntu box. Two entries, one muscle memory. On the Mac side that's live-tested end to end; against x11vnc it's the same standard handshake (the one &lt;code&gt;x11vnc -storepasswd&lt;/code&gt; sets), so it connects the same way.&lt;/p&gt;

&lt;p&gt;Worth knowing where the line sits: RealVNC the &lt;em&gt;company&lt;/em&gt; sells a cloud-connected ecosystem (account, device list, "connect from anywhere"). None of that is needed here — the Viewer app connects &lt;strong&gt;directly&lt;/strong&gt;, machine-to-machine over your LAN, with no account and nothing routed through anyone's cloud. Don't sign in; just add an address and connect. The same company's download page markets a comparison table implying classic direct connections to third-party servers are unsupported — the Mac test above (direct LAN, Apple's server, plain VNC password) says otherwise, and that's the configuration this post uses. (On iOS, RealVNC's VNC Viewer app is the equivalent client for either machine.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it now — first win.&lt;/strong&gt; Server on (either machine), Viewer installed: add an entry with the machine's LAN IP and port &lt;code&gt;5900&lt;/code&gt;, connect, enter the VNC password with the username blank. Your desktop should fill the phone's screen, and a tap anywhere should move the real cursor. If it does, the rig works — everything left in this part is technique. If it doesn't, the Reference section's troubleshooting tables are ordered by likelihood; start at the top.&lt;/p&gt;

&lt;h4&gt;
  
  
  One session, multiple monitors
&lt;/h4&gt;

&lt;p&gt;If the desktop has more than one display, nothing changes on the phone side: the VNC session mirrors &lt;strong&gt;the whole desktop, both monitors side by side&lt;/strong&gt;, as one wide screen. Swipe/pan across to move between them, pinch out to see both at once, pinch in to work one monitor at full zoom — the monitors are just regions of one mirrored framebuffer. Verified live from the phone (a browser on one monitor, a terminal on the other, both driven in the same session):&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%2Fbfg3t0wcug5x5qu0s3ig.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%2Fbfg3t0wcug5x5qu0s3ig.jpg" alt="Android phone in landscape running RealVNC Viewer, driving a dual-monitor desktop: a browser with a document on the left monitor and a terminal on the right monitor, both visible side by side in the single VNC session" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Works identically against both machines — the Mac's Screen Sharing serves all attached displays, and x11vnc serves the entire X session (&lt;code&gt;:0&lt;/code&gt; spans whatever monitors it spans). One practical note: at phone scale a two-monitor desktop is wide, so the pan-and-pinch rhythm from the gestures section below is the everyday motion, not a workaround.&lt;/p&gt;

&lt;h3&gt;
  
  
  Driving the desktop from the phone
&lt;/h3&gt;

&lt;p&gt;Everything here is VNC-client behavior, not OS behavior — it works the same against the Ubuntu machine and the Mac. The connection details, in one place:&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;Ubuntu machine&lt;/th&gt;
&lt;th&gt;macOS machine&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;RealVNC Viewer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;RealVNC Viewer&lt;/strong&gt; — same app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Address&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;ubuntu-lan-ip&amp;gt;:5900&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;mac-lan-ip&amp;gt;:5900&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;x11vnc -storepasswd&lt;/code&gt; one&lt;/td&gt;
&lt;td&gt;the 8-character VNC password&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both passwords are separate from your WiFi password and unaffected by switching networks — phone and desktop just need to be on the &lt;em&gt;same&lt;/em&gt; network at connection time, whichever one that is.&lt;/p&gt;

&lt;p&gt;VNC just mirrors real mouse and keyboard input — there's no VNC-specific "select an app" step, it behaves exactly like sitting at the desktop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Focus a window&lt;/strong&gt;: tap it directly in the mirrored screen — a real click.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type&lt;/strong&gt;: tap the field to focus it, tap the keyboard icon to bring up the phone's soft keyboard. Everything typed sends as real keystrokes — no special "VNC mode."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Modifier combos&lt;/strong&gt; (Ctrl+key, Alt+key, Cmd+key): use the client's extra-keys toolbar, the row of dedicated &lt;code&gt;Ctrl&lt;/code&gt;/&lt;code&gt;Alt&lt;/code&gt;/&lt;code&gt;Shift&lt;/code&gt;/&lt;code&gt;Esc&lt;/code&gt;/&lt;code&gt;Tab&lt;/code&gt; buttons you tap to hold, then tap the other key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Switching between applications
&lt;/h4&gt;

&lt;p&gt;Three ways, in the order actually worth trying on a phone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The desktop's hot corner (no keys needed at all)&lt;/strong&gt;: on GNOME, tap the very top-left pixel of the mirrored screen, just under where "Activities" would show — the overview opens with every open window as a thumbnail; tap the one you want. macOS has the same feature: System Settings → Desktop &amp;amp; Dock → Hot Corner (e.g. top-left → Mission Control). This is the one to reach for first: it needs no modifier key, works even if your VNC client's extra-keys toolbar doesn't expose &lt;code&gt;Tab&lt;/code&gt; (some builds don't), and is one tap instead of a hold-plus-tap combo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alt+Tab / Cmd+Tab, if your client has both keys&lt;/strong&gt;: hold &lt;code&gt;Alt&lt;/code&gt; (or &lt;code&gt;Cmd&lt;/code&gt; on the Mac) and tap &lt;code&gt;Tab&lt;/code&gt; from the extra-keys toolbar to cycle, same as at a physical keyboard — same toggle-then-tap mechanism as the multi-key shortcuts below. Falls back to the hot corner if &lt;code&gt;Tab&lt;/code&gt; isn't in your toolbar's default row.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Between terminals/shells specifically&lt;/strong&gt;: don't alt-tab or hot-corner between separate GUI terminal windows at all if you can help it — hunting for the right thumbnail among several is more taps than it's worth. Use the desktop's window overview (hot corner or Alt+Tab) instead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three are the same underlying trick: VNC sends real input events, so every desktop-level shortcut or gesture you'd use at the keyboard works identically from the phone.&lt;/p&gt;

&lt;h4&gt;
  
  
  Multi-key shortcuts, e.g. &lt;code&gt;Super+A&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;A touchscreen can't physically hold two keys down at once, so the extra-keys toolbar buttons don't work like a normal press-and-release key — they're &lt;strong&gt;toggles&lt;/strong&gt;. Tapping a modifier arms it (it stays visually pressed/highlighted); tapping the next key sends both together as one combo, and the modifier releases automatically right after.&lt;/p&gt;

&lt;p&gt;To fire &lt;code&gt;Super+A&lt;/code&gt; (or any modifier combo — &lt;code&gt;Ctrl+Shift+T&lt;/code&gt;, &lt;code&gt;Alt+F4&lt;/code&gt;, &lt;code&gt;Cmd+Q&lt;/code&gt; on the Mac):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the extra-keys toolbar (the keyboard/gear icon in the client's in-session toolbar).&lt;/li&gt;
&lt;li&gt;Tap the &lt;strong&gt;Super&lt;/strong&gt;/&lt;strong&gt;Win&lt;/strong&gt;/&lt;strong&gt;Cmd&lt;/strong&gt; key icon (labeled &lt;code&gt;Super&lt;/code&gt;, &lt;code&gt;Win&lt;/code&gt;, &lt;code&gt;Cmd&lt;/code&gt;, or shown as a ⊞-style icon depending on client) — it stays highlighted, armed.&lt;/li&gt;
&lt;li&gt;Tap &lt;code&gt;A&lt;/code&gt; on the soft keyboard — the combo fires as &lt;code&gt;Super+A&lt;/code&gt;, and the modifier releases on its own.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Same mechanism for stacking more than one modifier — tap &lt;code&gt;Ctrl&lt;/code&gt;, tap &lt;code&gt;Shift&lt;/code&gt;, then tap the letter key, and all three go through together. If the client's default extra-keys row doesn't show a Super/Win button, check its settings for a toolbar/keys customization option before assuming the key isn't supported — most VNC clients that expose Ctrl/Alt as toggles expose Super the same way.&lt;/p&gt;

&lt;h4&gt;
  
  
  Right-click, and where Enter/Backspace live
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Right-click&lt;/strong&gt; in RealVNC Viewer: &lt;strong&gt;tap with two fingers at once&lt;/strong&gt;. One finger tap = left click, two fingers = right, three = middle — and the click lands where the mouse cursor sits, not where your fingers touch (default mode drags the cursor with one finger, offset so you can see it). Two other gestures from the same official table worth knowing: two fingers dragged up/down = scroll, and &lt;strong&gt;double-tap + hold + drag&lt;/strong&gt; = select text or drag-and-drop. (&lt;a href="https://help.realvnc.com/hc/en-us/articles/360018541231-Using-RealVNC-Viewer-for-Mobile-to-control-a-remote-device" rel="noopener noreferrer"&gt;RealVNC's gesture reference&lt;/a&gt;.) If you're doing anything right-click-heavy, a &lt;strong&gt;Bluetooth mouse&lt;/strong&gt; pairs to the phone and its actual right button just works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enter and Backspace&lt;/strong&gt; — not special VNC buttons, just the ordinary keys on Android's own soft keyboard, same as any other key you tap. The extra-keys toolbar exists specifically for keys a &lt;em&gt;normal&lt;/em&gt; mobile keyboard doesn't have — Ctrl, Alt, Esc, Tab, arrows.&lt;/p&gt;

&lt;h4&gt;
  
  
  Scrolling — the most common gesture of all
&lt;/h4&gt;

&lt;p&gt;You'll scroll more than you'll click. &lt;strong&gt;Two fingers swiped up/down sends real mouse-wheel events&lt;/strong&gt; — so anything that scrolls at the desk scrolls from the phone, identically on the Mac and Ubuntu: web pages, chat logs, file lists, code editors. There's no VNC-specific scroll mode to enable; the two-finger swipe &lt;em&gt;is&lt;/em&gt; the wheel.&lt;/p&gt;

&lt;p&gt;The one place it needs help is the terminal, and only because terminals handle wheels oddly at the desk too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plain GUI terminal, Ubuntu&lt;/strong&gt;: the wheel scrolls the terminal's own scrollback in GNOME Terminal as usual — two-finger swipe works out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain Terminal on the Mac&lt;/strong&gt;: same — the wheel drives the window's scrollback natively.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a two-finger swipe ever pans the &lt;em&gt;view&lt;/em&gt; instead of scrolling the &lt;em&gt;app&lt;/em&gt;, you're zoomed out or in touch-drag mode — pinch back to 100% (or tap once to re-center the cursor) and the wheel behavior returns. And on a multi-monitor desktop, remember the two-finger swipe doubles as the pan across monitors when zoomed out.&lt;/p&gt;

&lt;h4&gt;
  
  
  The everyday actions: new tab, Ctrl+C, close
&lt;/h4&gt;

&lt;p&gt;The four things you'll do a hundred times, in one place. Modifier mechanics are the toggles from the multi-key section above — tap the modifier, it arms, tap the key, the combo fires:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Everyday action&lt;/th&gt;
&lt;th&gt;How, from the phone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Open a new terminal tab&lt;/strong&gt; on the desktop&lt;/td&gt;
&lt;td&gt;Same combo as at the desk: &lt;code&gt;Ctrl+Shift+T&lt;/code&gt; (Ubuntu's GNOME Terminal) — tap Ctrl, tap Shift, tap T. Mac Terminal: &lt;code&gt;Cmd+T&lt;/code&gt; (Super toggle, then T)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Right-click&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tap with two fingers at once (section above)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stop a running command (&lt;code&gt;Ctrl+C&lt;/code&gt;)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tap &lt;strong&gt;Ctrl&lt;/strong&gt; in the extra-keys row (it arms, stays highlighted), tap &lt;code&gt;c&lt;/code&gt; → sends &lt;code&gt;Ctrl+C&lt;/code&gt;, which &lt;strong&gt;interrupts the foreground process&lt;/strong&gt;. It closes nothing — not the tab, not the session&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Close the tab / end the shell&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;exit&lt;/code&gt;, or &lt;code&gt;Ctrl+D&lt;/code&gt; (Ctrl toggle + &lt;code&gt;d&lt;/code&gt;) — the thing people wrongly reach for &lt;code&gt;Ctrl+C&lt;/code&gt; to do. Different key, different job&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Done — end the phone session&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Just disconnect from the toolbar; everything keeps running on the desktop&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The pair worth memorizing: &lt;strong&gt;&lt;code&gt;Ctrl+C&lt;/code&gt; stops a program, &lt;code&gt;Ctrl+D&lt;/code&gt; ends the shell.&lt;/strong&gt; C is "interrupt what's running," D is "I'm leaving" — and neither ever touches the VNC session itself. (Running bVNC against the Ubuntu box instead of RealVNC Viewer? Same plays — its Ctrl toggle arms the same way, and its right-click is a hold-then-second-finger-tap gesture.)&lt;/p&gt;

&lt;p&gt;More of the everyday, grouped by who's holding the phone — every row uses the same three mechanics already covered (modifier toggles, the extra-keys row, touch gestures), just combined:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're developing:&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;Action&lt;/th&gt;
&lt;th&gt;From the phone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Copy / paste in a terminal&lt;/td&gt;
&lt;td&gt;Double-tap, hold the second tap, drag to select, then &lt;code&gt;Ctrl+Shift+C&lt;/code&gt; / &lt;code&gt;Ctrl+Shift+V&lt;/code&gt; — the terminal is the one app that needs the Shift; everywhere else plain &lt;code&gt;Ctrl+C&lt;/code&gt;/&lt;code&gt;Ctrl+V&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reopen a browser tab you closed by accident&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Ctrl+Shift+T&lt;/code&gt; — the same three-tap combo as "new terminal tab," but in the browser it resurrects the dead tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New / close browser tab&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Ctrl+T&lt;/code&gt; / &lt;code&gt;Ctrl+W&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jump to the address bar (or type a file path)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Ctrl+L&lt;/code&gt; — browser address bar, and GNOME Terminal's "type a path" prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run the last command again&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;↑&lt;/code&gt; from the extra-keys row, then Enter — no modifier needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search shell history by fragment&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Ctrl+R&lt;/code&gt;, type a fragment of the command, Enter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Close the focused app window&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Alt+F4&lt;/code&gt; (Linux) / &lt;code&gt;Cmd+Q&lt;/code&gt; (Mac)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lock the screen when you walk away&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Super+L&lt;/code&gt; (GNOME) / &lt;code&gt;Ctrl+Cmd+Q&lt;/code&gt; (Mac) — client stays connected&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;If you're exploring the machine:&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;Action&lt;/th&gt;
&lt;th&gt;From the phone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Middle-click paste (Linux's best-kept secret)&lt;/td&gt;
&lt;td&gt;Tap with three fingers at once — RealVNC's native middle click (or a Bluetooth mouse's middle button)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zoom the remote screen&lt;/td&gt;
&lt;td&gt;Pinch — client-side zoom, sharp on a phone's high-DPI panel, touches nothing on the desktop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scroll a terminal&lt;/td&gt;
&lt;td&gt;Two-finger swipe (works in most terminals; check scrollback settings if not)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Move a window&lt;/td&gt;
&lt;td&gt;Touch mode: drag its title bar like a real finger would&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dismiss a dialog / cancel a popup&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Esc&lt;/code&gt; from the extra-keys row — one tap, no modifiers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;If you're just using the computer:&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;Action&lt;/th&gt;
&lt;th&gt;From the phone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Passwords&lt;/td&gt;
&lt;td&gt;The phone keyboard's &lt;strong&gt;autofill&lt;/strong&gt; — your password manager fills straight into the desktop login field, nothing to type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Emoji into a desktop chat&lt;/td&gt;
&lt;td&gt;The phone keyboard's own emoji panel — arrives as real keystrokes, no desktop setup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Play / pause video&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Space&lt;/code&gt; — a key the phone keyboard already has&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long text (URLs, messages)&lt;/td&gt;
&lt;td&gt;Copy on the phone, paste with the keyboard's paste button — or dictate it (mic button, section below)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's the quiet payoff: if the phone's keyboard can produce it, the desktop receives it — and everything else is one armed modifier away.&lt;/p&gt;

&lt;h4&gt;
  
  
  Editing text in a terminal
&lt;/h4&gt;

&lt;p&gt;Faster than arrow-key nudging on touch, works on both machines: standard readline bindings work over VNC exactly like they do locally — &lt;code&gt;Ctrl+A&lt;/code&gt;/&lt;code&gt;Ctrl+E&lt;/code&gt; jump to line start/end, &lt;code&gt;Ctrl+W&lt;/code&gt; deletes the last word, &lt;code&gt;Ctrl+U&lt;/code&gt; clears back to cursor.&lt;/p&gt;

&lt;h4&gt;
  
  
  Typing on the desktop with your phone keyboard — including voice typing
&lt;/h4&gt;

&lt;p&gt;The soft keyboard that appears when you tap the keyboard icon is your phone's &lt;strong&gt;normal keyboard app&lt;/strong&gt; — whatever you already use, nothing new installed. Everything the keyboard app is good at carries over, because VNC just receives whatever the keyboard commits and sends it as real keystrokes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Glide typing / swipe input&lt;/strong&gt; — drag through the letters, same as texting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autocorrect and next-word prediction&lt;/strong&gt; — actively helpful in chat and email apps on the desktop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long-press for numbers and symbols&lt;/strong&gt; — long-press the top-row letters, same as anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clipboard&lt;/strong&gt; — copy on the phone (from anywhere), paste into the desktop app with the keyboard's paste button. Copy on the desktop, read it on the phone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multilingual typing&lt;/strong&gt; — your keyboard's per-language or multi-language modes work as usual; typing Bangla or any other language into a desktop app needs no desktop-side setup beyond the app accepting text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-handed / floating mode&lt;/strong&gt; — shrink the keyboard to a thumb-zone corner for couch use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice typing — the built-in speech-to-text&lt;/strong&gt; (tested live): any keyboard with a mic button (Gboard, SwiftKey, Samsung Keyboard) works over VNC. Tap the mic, speak, and the transcribed text arrives as committed keystrokes in whatever desktop app is focused — a commit message, a Slack reply, a doc — no extra rig, no second app, no setup. The catch: nearly every phone keyboard dictates through its maker's cloud speech service (Google/Microsoft/Samsung servers do the transcribing), so it's "free and zero-setup," not "offline." For most dictation, that's the right trade — just know which way it cuts before dictating anything sensitive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two cautions from the terminal-shaped parts of this rig:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Autocorrect + terminals don't mix.&lt;/strong&gt; Suggestions will happily "fix" flags, paths, and &lt;code&gt;git&lt;/code&gt; subcommands. Your keyboard app's settings (its own toolbar, or Settings → System → Languages &amp;amp; input) usually offer a suggestions/autocorrect toggle — disable it when a terminal is focused, or type flags carefully and proofread before Enter. There's no desktop-side guard; the keystrokes arrive already "corrected."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Voice typing lands wherever the cursor is.&lt;/strong&gt; Same rule as autocorrect, stronger stakes: check the focused window before you speak, because the words land as keystrokes the instant the keyboard commits them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  A note on global hotkeys
&lt;/h4&gt;

&lt;p&gt;Any app that listens for a global hotkey (a launcher, a window manager action, a screenshot tool, an app-specific shortcut utility) can be triggered from the phone — VNC delivers the key to the desktop's real input stack, and the listener fires. The practical rule for &lt;em&gt;which&lt;/em&gt; hotkey to pick for phone use: a &lt;strong&gt;single key with no modifier&lt;/strong&gt; (&lt;code&gt;End&lt;/code&gt;, &lt;code&gt;Insert&lt;/code&gt;, &lt;code&gt;Home&lt;/code&gt;, &lt;code&gt;Page Up/Down&lt;/code&gt;) beats any combo, because a combo means arming a toggle button and then tapping a second key — two taps instead of one, every single time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1 — Troubleshooting &amp;amp; security
&lt;/h2&gt;

&lt;p&gt;Everything above gets the phone driving either machine. What follows is what to check when a step doesn't behave, plus the security notes for Part 1 — read now, or bookmark for later.&lt;/p&gt;

&lt;h3&gt;
  
  
  If the phone client won't connect (macOS)
&lt;/h3&gt;

&lt;p&gt;Every failure below was either hit live on this rig or traced to a documented server behavior — check symptoms in order:&lt;/p&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&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client loops on "enter VNC credentials" with a password you know is right&lt;/td&gt;
&lt;td&gt;One of the two auth types being served (Mac-account vs VNC-password) is being answered with the wrong credential — or the password is longer than 8 characters and got silently truncated (DES cap)&lt;/td&gt;
&lt;td&gt;Enter the &lt;strong&gt;VNC password&lt;/strong&gt;, username blank; if longer than 8 chars, the first 8 are the real password&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nothing can connect at all&lt;/td&gt;
&lt;td&gt;"VNC viewers may control screen with password" never enabled — macOS is waiting for Apple's own auth&lt;/td&gt;
&lt;td&gt;Enable it via the ⓘ next to Screen Sharing (step 2 in the macOS setup above), set the password, retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connects but lands on the &lt;strong&gt;login window&lt;/strong&gt; instead of your desktop; typing into it drops focus&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Remote Management&lt;/strong&gt; is the enabled service, not plain Screen Sharing — legacy-VNC clients get served the login window under Remote Management (tested live)&lt;/td&gt;
&lt;td&gt;System Settings → General → Sharing → disable Remote Management, enable &lt;strong&gt;Screen Sharing&lt;/strong&gt;; reconnect and you land on the desktop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Worked yesterday, won't connect today (after a reboot)&lt;/td&gt;
&lt;td&gt;The Mac's LAN IP re-leased to a different address&lt;/td&gt;
&lt;td&gt;Recheck with &lt;code&gt;ipconfig getifaddr en0&lt;/code&gt;, update the saved entry — or set a DHCP reservation in the router&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Times out&lt;/strong&gt; — "but I'm on the same WiFi"&lt;/td&gt;
&lt;td&gt;Traffic never arrived: phone silently off the LAN, router guest/AP isolation, or a VPN on the phone — not a Mac-side failure&lt;/td&gt;
&lt;td&gt;Check the phone's WiFi details page (SSID + gateway match the router); watch &lt;code&gt;log stream --predicate 'process == "screensharingd"' --info&lt;/code&gt; while tapping Connect — nothing logged = it never arrived. &lt;strong&gt;"Refused"&lt;/strong&gt; = arrived, nothing listening → the server rows above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VNC view freezes/blank after the Mac sits idle&lt;/td&gt;
&lt;td&gt;The Mac auto-locked; the lock screen's password field fights phone-client keystrokes for focus&lt;/td&gt;
&lt;td&gt;Unlock once at the Mac's keyboard; for couch sessions set Lock Screen → "Require password…" to a long interval&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client rejects the correct password, non-RealVNC app&lt;/td&gt;
&lt;td&gt;Not every Android/iOS client can complete classic VNC auth against a Mac — some negotiate Apple's DH handshake and fail it with no override&lt;/td&gt;
&lt;td&gt;Standardize on &lt;strong&gt;RealVNC Viewer&lt;/strong&gt; (tested live against a Mac); verify the two auth-type behaviors above before blaming the password&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two closing notes on the macOS side. First, much of the "Sequoia is rough on third-party VNC clients" chatter traces back to the Remote Management behavior in the table above, not the OS itself — with plain Screen Sharing and the VNC password, RealVNC Viewer connected cleanly here through reboots and IP changes. Reports of screen-recording/share permissions demanding roughly-monthly reauthorization do exist; if a client won't connect, first try Apple's own Screen Sharing app from another Mac to isolate server-toggle vs client. Second, &lt;strong&gt;firewall&lt;/strong&gt;: macOS's application firewall allows the signed system Screen Sharing service through by default — no &lt;code&gt;ufw&lt;/code&gt;-style rule to add. The LAN-only hygiene advice still applies: don't port-forward 5900 out of your router, ever. Third: the timeout-vs-refused split from the Ubuntu section below applies to the Mac unchanged — "refused" is the server rows in this table, "timed out" is the phone's network path — and the Mac's ten-second instrument is &lt;code&gt;log stream --predicate 'process == "screensharingd"' --info&lt;/code&gt;, watched live while you tap Connect (it's the unified-log equivalent of Ubuntu's &lt;code&gt;journalctl&lt;/code&gt; watch: silence = the traffic never arrived).&lt;/p&gt;

&lt;p&gt;Session realities (tested across a reboot): everything server-side comes back after a restart, but the Mac's LAN IP can re-lease (ours changed on one reboot — if the saved phone entry stops connecting, recheck with &lt;code&gt;ipconfig getifaddr en0&lt;/code&gt;, or give the Mac a DHCP reservation in the router). And multiple displays arrive as one wide canvas — pinch out in the client and pan; VNC has no per-monitor concept.&lt;/p&gt;

&lt;h3&gt;
  
  
  If the phone won't connect (Ubuntu)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"Connection refused"&lt;/strong&gt; — port 5900 has nothing listening, x11vnc either never started or died on launch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pgrep &lt;span class="nt"&gt;-af&lt;/span&gt; x11vnc     &lt;span class="c"&gt;# nothing = not running&lt;/span&gt;
ss &lt;span class="nt"&gt;-tln&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;5900  &lt;span class="c"&gt;# nothing = nothing listening&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it's not running, re-run it and actually read what it prints. x11vnc fails fast on a bad flag or auth error and drops straight back to a clean-looking shell prompt — nothing visually distinguishes "crashed instantly" from "idle and fine" unless you read the output. The two causes that hit here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wrong display number.&lt;/strong&gt; &lt;code&gt;-display :0&lt;/code&gt; failed with an Xauthority error on a session that was actually &lt;code&gt;:1&lt;/code&gt;. Check with &lt;code&gt;who&lt;/code&gt; (&lt;code&gt;&amp;lt;user&amp;gt;  :1  &amp;lt;date&amp;gt;&lt;/code&gt; — the number after the colon) from a terminal that's part of the actual graphical session, not an unrelated SSH shell.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Xauthority not at the default path.&lt;/strong&gt; Modern GNOME/gdm often keeps it at &lt;code&gt;/run/user/&amp;lt;uid&amp;gt;/gdm/Xauthority&lt;/code&gt; instead of &lt;code&gt;~/.Xauthority&lt;/code&gt;. &lt;code&gt;-auth guess&lt;/code&gt; finds it automatically; the explicit fallback is &lt;code&gt;-auth /run/user/&amp;lt;uid&amp;gt;/gdm/Xauthority&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it's still refused after that: check &lt;code&gt;ufw status verbose&lt;/code&gt; actually shows the port-5900 rule as active, and re-check the desktop's LAN IP hasn't drifted from a DHCP re-lease. The one-glance version of that check is &lt;code&gt;hostname -I&lt;/code&gt; — every IPv4 on one line; ignore the &lt;code&gt;172.x.x.x&lt;/code&gt; ones (Docker and virtual-machine bridges) and read the &lt;code&gt;192.168.x.x&lt;/code&gt; or &lt;code&gt;10.x.x.x&lt;/code&gt; one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A timeout is a different animal than "refused" — and the phone's exact error text tells you which you have.&lt;/strong&gt; "Refused" means the phone's packets &lt;em&gt;reached&lt;/em&gt; the desk and nothing was listening — the server-side territory above. "Timed out" means the packets never arrived at all, which is almost always phone-side or network-side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The phone isn't actually on the same network.&lt;/strong&gt; Check the phone's WiFi details page, not just the icon: the SSID must match, and the &lt;em&gt;gateway&lt;/em&gt; should be your router's LAN address (ours is &lt;code&gt;192.168.68.1&lt;/code&gt;). A mismatched gateway — or a silent fallback to mobile data — puts the phone on a different path entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guest network or AP/client isolation.&lt;/strong&gt; Many routers isolate guests (and sometimes a separate 2.4 GHz network) from main-network devices: both phones connect to "the WiFi," neither can see the other. If the router admin panel calls it AP isolation, client isolation, or guest access, that's the switch to check.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A VPN running on the phone&lt;/strong&gt; can intercept or route away LAN traffic — turn it off for the LAN path (you only need it for the Part 2 path).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The instrument that settles all of it in ten seconds&lt;/strong&gt; — watch the server log live while you tap Connect on the phone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; x11vnc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing appears while the phone churns = traffic isn't arriving — work the list above. &lt;code&gt;Got connection from client &amp;lt;ip&amp;gt;&lt;/code&gt; followed by an auth failure = the network is fine, it's the password. A connection that appears and instantly drops = server-side — read what it prints (the &lt;code&gt;-shared&lt;/code&gt; lockout in the flags section above looks exactly like this).&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 1 security recap
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep port 5900 LAN-scoped.&lt;/strong&gt; &lt;code&gt;ufw allow from &amp;lt;your-subnet&amp;gt; to any port 5900&lt;/code&gt; on Ubuntu; on macOS the firewall already permits Screen Sharing — and on &lt;em&gt;both&lt;/em&gt;, the rule that matters is at the router: never port-forward 5900 to the internet. If you ever need in from outside your LAN, that's a VPN job (Part 2 builds exactly that), not a port-forward job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VNC passwords are weak by protocol.&lt;/strong&gt; Classic VNC auth is a DES challenge-response &lt;strong&gt;capped at 8 characters&lt;/strong&gt; — both servers in this post use it. On a firewalled home LAN that's an acceptable trade; anywhere shared, tunnel VNC through SSH or use x11vnc's TLS/VeNCrypt options.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cloud in the control path.&lt;/strong&gt; RealVNC Viewer connects direct, machine-to-machine; nothing in Part 1 routes through or requires anyone's servers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the whole control rig: the phone can see and drive either machine, from anywhere in the house. One constraint remains — phone and desktop must share a network. Part 2 removes it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2 — Over the internet: the same rig, anywhere
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Wait — this only works on my WiFi. What if I'm at a coffee shop? What if the desktop is at home and I'm not?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the question Part 1 leaves hanging. This part answers it: same phone, same desktop, same free tools — working from a coffee shop, a hotel room, or the back seat of a cab on mobile data, with nothing exposed to the open internet and nothing added to the bill.&lt;/p&gt;

&lt;h3&gt;
  
  
  First, the check most people skip: can you even port-forward?
&lt;/h3&gt;

&lt;p&gt;The traditional answer to "reach my home machine from outside" is: open the router's admin panel, forward port 5900 to the desktop, point the phone at your public IP. Before considering that path, run a five-minute check — because on a large share of modern connections it's dead on arrival:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the router admin panel (usually &lt;code&gt;192.168.1.1&lt;/code&gt; or &lt;code&gt;192.168.0.1&lt;/code&gt; — your gateway address) and note its &lt;strong&gt;WAN IP&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;From the desktop, ask the internet what IP it sees: &lt;code&gt;curl ifconfig.me&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Compare. &lt;strong&gt;Same address&lt;/strong&gt; → your connection has a real public IP; port-forwarding is at least &lt;em&gt;possible&lt;/em&gt;. &lt;strong&gt;Different address&lt;/strong&gt;, or the router's WAN IP somewhere in &lt;code&gt;100.64.0.0&lt;/code&gt;–&lt;code&gt;100.127.255.255&lt;/code&gt; → you're behind &lt;a href="https://datatracker.ietf.org/doc/html/rfc6598" rel="noopener noreferrer"&gt;&lt;strong&gt;CGNAT&lt;/strong&gt;&lt;/a&gt; (Carrier-Grade NAT): your router's "public" side is itself a private address inside your ISP's shared NAT layer. Inbound port-forwards simply won't route to you — no router configuration changes that, because the NAT layer that drops the traffic belongs to the ISP, not you.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;CGNAT is now standard on many fiber and 5G home connections, which is exactly why "just forward the port" advice from 2010 blog posts fails silently today: everything on your side is configured correctly, and the packets still never arrive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Even if you can port-forward, don't — not raw VNC
&lt;/h3&gt;

&lt;p&gt;Suppose the check comes back clean and you &lt;em&gt;do&lt;/em&gt; have a real public IP. Port 5900 open to the whole internet is still the wrong move:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Classic VNC auth is a single shared password&lt;/strong&gt; — no usernames, no rate limiting, no second factor, no lockout. A 1990s-era protocol assumption wearing modern exposure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5900 is a top-tier scanning target.&lt;/strong&gt; Internet-wide scanners map the whole IPv4 space continuously; an open VNC port gets probed within hours. Every probe is a password-guess against an 8-character-effective secret (Part 3 explains the cap).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One password = one blast radius.&lt;/strong&gt; The VNC password doesn't just "view the screen" — it drives the desktop. A correct guess is a full remote session, as if the attacker sat down at your keyboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the real goal was never "forward port 5900." It's this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Put the phone and the desktop on the same private network no matter where either of them physically is — and let VNC keep believing it's on a LAN, exactly like Part 1.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The options
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Setup effort&lt;/th&gt;
&lt;th&gt;Ongoing cost&lt;/th&gt;
&lt;th&gt;Survives CGNAT&lt;/th&gt;
&lt;th&gt;Exposes anything raw to the internet&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tailscale (mesh VPN)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low — install, log in, done&lt;/td&gt;
&lt;td&gt;Free (personal plan)&lt;/td&gt;
&lt;td&gt;Yes — NAT traversal built in, falls back to encrypted relay&lt;/td&gt;
&lt;td&gt;No — nothing public-facing, no opened router ports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WireGuard, self-hosted rendezvous&lt;/td&gt;
&lt;td&gt;Medium-high — you run a reachable relay yourself&lt;/td&gt;
&lt;td&gt;Free if you own a suitable always-on box; else a cheap VPS&lt;/td&gt;
&lt;td&gt;Only if the relay has a real public IP&lt;/td&gt;
&lt;td&gt;The relay host does&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH reverse tunnel to a VPS&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;~$4–6/mo VPS or a free-tier instance&lt;/td&gt;
&lt;td&gt;Yes — desktop initiates outbound&lt;/td&gt;
&lt;td&gt;The VPS's SSH port (harden it)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare Tunnel&lt;/td&gt;
&lt;td&gt;Low-medium&lt;/td&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;Yes — &lt;code&gt;cloudflared&lt;/code&gt; runs outbound&lt;/td&gt;
&lt;td&gt;No inbound port, but traffic transits Cloudflare's edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port forward + DDNS&lt;/td&gt;
&lt;td&gt;Low &lt;em&gt;if&lt;/em&gt; CGNAT check passes&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; — the one CGNAT kills outright&lt;/td&gt;
&lt;td&gt;Yes, directly — the whole point of the approach&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rest of this part builds the first row. The alternatives get a short treatment in the Reference section below ("Escape hatches").&lt;/p&gt;

&lt;h3&gt;
  
  
  Tailscale: what it is and why it fits
&lt;/h3&gt;

&lt;p&gt;Tailscale is a &lt;strong&gt;mesh VPN built on WireGuard&lt;/strong&gt;. You install it on each device, log them into the same account (a "tailnet"), and every device gets a stable private address in the &lt;code&gt;100.x.y.z&lt;/code&gt; range. That address doesn't change when the device moves networks — laptop on café WiFi, phone on mobile data, desktop at home: same tailnet IPs, as if they were all plugged into one switch in your living room.&lt;/p&gt;

&lt;p&gt;The properties that matter for this rig:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NAT traversal is handled for you.&lt;/strong&gt; Tailscale tries to establish a direct peer-to-peer WireGuard tunnel first (even through NAT layers); when no direct path can be punched — double-NAT on both ends, strict firewalls — it transparently relays through &lt;strong&gt;DERP servers&lt;/strong&gt;, Tailscale's encrypted relays. Relayed traffic is still end-to-end encrypted; the relay carries ciphertext it cannot read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing is publicly exposed.&lt;/strong&gt; No router ports open, no public IP needed, inbound scanning finds nothing. The desktop's VNC port stays reachable only from inside the tailnet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The protocols don't change.&lt;/strong&gt; VNC on 5900, same password, same client app — the rig's software never learns a VPN exists. This is a networking-layer swap; Part 1's setup survives untouched.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The trade-off:&lt;/strong&gt; the LAN-only rig was "no cloud dependency" in the strictest sense. Tailscale adds a third-party &lt;em&gt;coordination&lt;/em&gt; service — its control plane handles identity and connection setup. In the common direct-connection case, no VNC pixels ever touch Tailscale's servers; in the relay-fallback case their servers carry them, encrypted, unreadable in transit. If that trade-off ever stops being acceptable, &lt;a href="https://github.com/juanfont/headscale" rel="noopener noreferrer"&gt;Headscale&lt;/a&gt; — the self-hosted control-plane replacement — keeps everything else identical.&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%2Fzhr64cl3yebjyaehfh0m.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%2Fzhr64cl3yebjyaehfh0m.png" alt="Diagram: an Android phone on café WiFi or mobile data and a desktop at home behind a router, each running Tailscale with a permanent 100.x.y.z tailnet address. RealVNC Viewer targets the desktop's tailnet IP on port 5900. A direct WireGuard tunnel (the common case) connects the two peers end-to-end encrypted; when no direct path can be punched, traffic falls back to a DERP relay that carries ciphertext only. No router ports are opened." width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Part 2 in one picture: the tailnet replaces "same WiFi" — VNC keeps believing it's on a LAN.&lt;/p&gt;

&lt;h4&gt;
  
  
  Is Tailscale safe to trust?
&lt;/h4&gt;

&lt;p&gt;The VNC password is one tailnet membership away from your desktop, so this matters. The short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;WireGuard crypto, not home-rolled.&lt;/strong&gt; Curve25519 key exchange, ChaCha20-Poly1305. Traffic is end-to-end encrypted between your two devices only. Private keys never leave the device — Tailscale's servers can't decrypt what flows between your phone and desktop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clients are open source.&lt;/strong&gt; Linux, macOS, Android, iOS, Windows, even the DERP relay server — all on GitHub. The closed piece is the coordination/control plane, which sees metadata (who connects to whom), never payloads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audited and scaled.&lt;/strong&gt; Recurring third-party reviews by &lt;a href="https://www.latacora.com/" rel="noopener noreferrer"&gt;Latacora&lt;/a&gt;, SOC 2 Type II certified. Millions of daily users; named customers include Duolingo, Instacart, Retool, Mercury.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account-side is solid.&lt;/strong&gt; SSO/MFA, per-device approval, ACLs, tailnet lock. The tailnet is your outer wall now, not the 8-character VNC password.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The remaining caveat: coordination is a third-party closed service. If that ever bothers you, &lt;a href="https://github.com/juanfont/headscale" rel="noopener noreferrer"&gt;Headscale&lt;/a&gt; — the self-hosted control-plane replacement — keeps everything else identical.&lt;/p&gt;

&lt;h4&gt;
  
  
  On the desktop
&lt;/h4&gt;

&lt;p&gt;Ubuntu and macOS both get the same one-liner or installer:&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;# Ubuntu&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://tailscale.com/install.sh | sh
&lt;span class="nb"&gt;sudo &lt;/span&gt;tailscale up
&lt;span class="c"&gt;# prints a login URL → open it → authenticate with your account&lt;/span&gt;
tailscale ip &lt;span class="nt"&gt;-4&lt;/span&gt;   &lt;span class="c"&gt;# note this: the desktop's permanent 100.x.y.z address&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;macOS: install from the Mac App Store or &lt;code&gt;brew install --cask tailscale&lt;/code&gt; (or &lt;code&gt;brew install tailscale&lt;/code&gt; for the CLI variant), log in from the menu-bar app, read the address from the menu bar or &lt;code&gt;tailscale ip -4&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then one firewall change on Ubuntu. The Part 1 rule was LAN-scoped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 192.168.68.0/24 to any port 5900 proto tcp   &lt;span class="c"&gt;# LAN-only (Part 1)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tailnet version keeps the same &lt;em&gt;shape&lt;/em&gt; — port 5900 reachable only from a network the phone is on — just against Tailscale's address space:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 100.64.0.0/10 to any port 5900 proto tcp     &lt;span class="c"&gt;# any tailnet device&lt;/span&gt;
&lt;span class="c"&gt;# or tighter, once the phone's tailnet IP is stable:&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from 100.&amp;lt;phone-tailnet-ip&amp;gt; to any port 5900 proto tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(That &lt;code&gt;100.64.0.0/10&lt;/code&gt; range is CGNAT address space by spec, which is exactly why Tailscale picked it — those addresses are never routable on the public internet, so tailnet traffic can't collide with anything real.)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;x11vnc&lt;/code&gt; itself: &lt;strong&gt;no change.&lt;/strong&gt; Same command, same flags, same &lt;code&gt;~/.vnc/passwd&lt;/code&gt;. macOS Screen Sharing: &lt;strong&gt;no change.&lt;/strong&gt; Same toggle, same VNC password. macOS's firewall needs no rule either — it permits Screen Sharing by default no matter which interface the traffic arrives on.&lt;/p&gt;

&lt;h4&gt;
  
  
  On the phone
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;Install &lt;strong&gt;Tailscale&lt;/strong&gt; from the Play Store, log into the same account, toggle it on. Read the phone's tailnet IP from the app (or the admin console).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RealVNC Viewer&lt;/strong&gt;: edit the saved connection, point it at the desktop's &lt;strong&gt;tailnet IP&lt;/strong&gt; instead of the LAN IP. Port still 5900. Same VNC password.&lt;/li&gt;
&lt;li&gt;That's it — no other app on the phone changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What changes, what doesn't
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;Part 1 (LAN)&lt;/th&gt;
&lt;th&gt;Part 2 (internet)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;x11vnc / Screen Sharing&lt;/td&gt;
&lt;td&gt;running&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;unchanged&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VNC port&lt;/td&gt;
&lt;td&gt;5900&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;unchanged&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VNC password&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;~/.vnc/passwd&lt;/code&gt; / Screen Sharing setting&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;unchanged&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Phone client&lt;/td&gt;
&lt;td&gt;RealVNC Viewer&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;unchanged app&lt;/strong&gt; — new target IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Desktop address phone dials&lt;/td&gt;
&lt;td&gt;LAN IP, re-check after router re-leases&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;tailnet IP — permanent&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ufw rule (Ubuntu)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;allow from 192.168.x.0/24&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;allow from 100.64.0.0/10&lt;/code&gt; (or the phone's tailnet IP)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;hotkeys, everything else&lt;/td&gt;
&lt;td&gt;working&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;unchanged&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In short: &lt;strong&gt;Part 2 is a re-addressing, not a rebuild.&lt;/strong&gt; Two new apps (Tailscale on each end), one firewall rule, one edited IP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verified from cellular
&lt;/h3&gt;

&lt;p&gt;The off-LAN path was tested end to end: phone on &lt;strong&gt;mobile data&lt;/strong&gt; (WiFi off) driving the desktop over VNC through the tailnet, on a &lt;strong&gt;direct&lt;/strong&gt; WireGuard path. &lt;code&gt;tailscale ping&lt;/code&gt; answered in ~66 ms — not even the relay was needed. The Mac's internet route has since been tested too, over the &lt;strong&gt;DERP relay&lt;/strong&gt; at ~125 ms, fully usable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you verify it on your own networks:&lt;/strong&gt; Connect, tap the hot corner, open a terminal, type a line, fire a &lt;code&gt;Ctrl+key&lt;/code&gt; combo. Every one of those exercising the real-input path is the proof the rig works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 2 — Security and alternatives
&lt;/h2&gt;

&lt;p&gt;That's everything needed to run off the LAN. What follows is the trust boundary to keep in mind, and where to look if Tailscale ever isn't the right fit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security recap
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The VNC port is now reachable from &lt;strong&gt;wherever your phone is&lt;/strong&gt; — the tailnet replaces "same WiFi" as the trust boundary. Keep the tailnet small: personal Tailscale plans allow a limited number of users and devices; use them for your devices only, not as a shared VPN for acquaintances.&lt;/li&gt;
&lt;li&gt;Tailscale's &lt;strong&gt;ACLs&lt;/strong&gt; can pin it further: phone may talk to desktop on 5900, nothing else, nobody else. Locking a two-device tailnet down to exactly one rule is an afternoon's reading, not a project.&lt;/li&gt;
&lt;li&gt;Still true from Part 1: VNC auth is one shared password. Over the tailnet that's acceptable to many people — an attacker needs tailnet membership &lt;em&gt;first&lt;/em&gt;, which means your Tailscale account, which has MFA and device approval. Layered, the 8-character cap stops being the outer wall.&lt;/li&gt;
&lt;li&gt;And the classic still applies: &lt;strong&gt;never&lt;/strong&gt; port-forward 5900 on the router to "make Tailscale unnecessary." You'd be re-opening exactly what Part 2 closed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  If Tailscale isn't the right fit
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SSH reverse tunnel to a VPS&lt;/strong&gt; — the fallback that removes the VPN vendor entirely: &lt;code&gt;ssh -R 5900:localhost:5900 user@your-vps&lt;/code&gt; (or a persistent &lt;code&gt;autossh&lt;/code&gt;/systemd unit). Outbound-only, so CGNAT is irrelevant; the phone dials the VPS. Costs a VPS (~$4–6/mo or free tier), needs key-only SSH and careful &lt;code&gt;GatewayPorts&lt;/code&gt;, and adds a box you must keep alive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headscale&lt;/strong&gt; — self-hosted Tailscale control plane, same clients, your server, your rules. The "nothing outside my control" property, at the price of running the coordination layer yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Tunnel&lt;/strong&gt; — &lt;code&gt;cloudflared&lt;/code&gt; outbound from the desktop; supports raw TCP (VNC included). Viable, but it's HTTP-shaped infrastructure repurposed for TCP, needs a domain and Access policies, and its trust model is "transits Cloudflare's edge." For two personal devices, Tailscale is the better-fitting tool.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 3 — Deep dive: how the rig actually works
&lt;/h2&gt;

&lt;p&gt;Part 1 said "trust me, taps become real keystrokes." Part 2 said "trust me, two devices behind two different NATs find each other, encrypted." This part cashes those checks. Nothing here is new setup — it's the working rig, opened up. Read it when something breaks and you want to know &lt;em&gt;why&lt;/em&gt;, or because you just like knowing how things work.&lt;/p&gt;

&lt;h3&gt;
  
  
  The wire protocol: RFB, and the 8-character password
&lt;/h3&gt;

&lt;p&gt;VNC speaks &lt;a href="https://datatracker.ietf.org/doc/html/rfc6143" rel="noopener noreferrer"&gt;&lt;strong&gt;RFB&lt;/strong&gt;&lt;/a&gt; (Remote Framebuffer). A session is a short fixed conversation, and knowing its shape explains several behaviors you met in Part 1 without explanation:&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%2F44vz3e4rsjp9itkfqh5q.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%2F44vz3e4rsjp9itkfqh5q.png" alt="Sequence diagram of the RFB protocol handshake between VNC client and server: version handshake exchanging RFB 003.008 strings, security negotiation where the server lists accepted auth types — macOS offering two at once, Mac-account and legacy VNC password — then DES challenge-response authentication where the password itself is the DES key and DES keys are 8 bytes so characters beyond 8 never enter the computation, then framebuffer negotiation and the steady stream of framebuffer updates server-to-client and pointer and key events client-to-server." width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The whole VNC session is this conversation — and step 3 is where the 8-character cap lives.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Version handshake&lt;/strong&gt; — client and server exchange &lt;code&gt;RFB 003.008&lt;/code&gt;-style strings and agree on a version.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security negotiation&lt;/strong&gt; — the server lists the auth types it will accept; the client picks one. This is where macOS offers &lt;em&gt;two at once&lt;/em&gt; — Mac-account (Apple's ARD-style handshake) and the legacy VNC password — and where a client that can only speak one of them dies with a rejected-correct-password loop. It negotiated the wrong door, not the wrong key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth, then framebuffer negotiation&lt;/strong&gt; — pixel format, encodings, and from there a stream of framebuffer updates (server → client) and pointer/key events (client → server).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The 8-character cap: classic VNC authentication is a &lt;a href="https://datatracker.ietf.org/doc/html/rfc6143#section-7.2.2" rel="noopener noreferrer"&gt;&lt;strong&gt;DES challenge-response&lt;/strong&gt;&lt;/a&gt;. The server sends a 16-byte challenge; the client encrypts it with DES using the &lt;em&gt;password itself&lt;/em&gt; as the key — and DES keys are 8 bytes. Whatever you type beyond 8 characters never enters the computation. Every VNC implementation that speaks classic auth inherits the cap: the Mac's Screen Sharing password (Part 1's "first 8 are the real password" gotcha) and x11vnc's &lt;code&gt;-rfbauth&lt;/code&gt; file alike. It's a protocol limitation, not a bug in either server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why VNC triggers hotkeys when SSH can't
&lt;/h3&gt;

&lt;p&gt;Part 1's rule was: "global hotkeys fire from the phone." Here's the mechanism underneath.&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%2Fdczmem98xjral5uksn1y.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%2Fdczmem98xjral5uksn1y.png" alt="Diagram comparing two remote-input paths into an Ubuntu desktop. Left: a VNC client's key events reach x11vnc, which calls the XTEST extension to inject input at the X server level, upstream of any application — events enter the same input stream as the physical keyboard driver, same keycodes, same grabs, same global hotkey listeners, so terminals, the window manager, and hotkey hooks all respond. Right: an SSH session delivers bytes to one process's stdin — no keyboard event as far as the OS is concerned, no window manager, no listeners, no GUI." width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same keystroke, two doors: VNC injects at the X server level (XTEST); SSH hands bytes to one process's stdin.&lt;/p&gt;

&lt;p&gt;When the phone's keyboard commits text, RealVNC Viewer sends RFB key events. On the desktop, x11vnc turns those into calls to &lt;a href="https://www.x.org/releases/X11R7.7/doc/xextproto/xtest.html" rel="noopener noreferrer"&gt;&lt;strong&gt;XTEST&lt;/strong&gt;&lt;/a&gt; — the X11 extension written for automated testing that injects input at the &lt;em&gt;server&lt;/em&gt; level, upstream of any application. XTEST-synthesized events enter the X server's input stream and are dispatched exactly like events from the physical keyboard driver: same keycodes, same grabs, same global hotkey listeners.&lt;/p&gt;

&lt;p&gt;That's the whole trick the rig rests on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A terminal sees keystrokes → typing works.&lt;/li&gt;
&lt;li&gt;The window manager sees key combos → &lt;code&gt;Super&lt;/code&gt;-shaped shortcuts work.&lt;/li&gt;
&lt;li&gt;Any global hotkey hook sees a real key event → launchers, screenshot tools, window-manager actions all fire from the phone.&lt;/li&gt;
&lt;li&gt;SSH, by contrast, delivers bytes to &lt;em&gt;one process's stdin&lt;/em&gt;. Nothing about an SSH keystroke is a keyboard event as far as the OS is concerned — no window manager, no listeners, no GUI. That's "the real reason this rig is VNC-based, not SSH-based," now with its gears visible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;macOS runs the same play with different plumbing: Screen Sharing injects events through the window server's event path — equivalent to a physical event to every listener, which is why the same hotkey rule held on the Mac (verified live: shortcuts fired from the phone exactly as from the keyboard).&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 2's layer: WireGuard, NAT traversal, and DERP
&lt;/h3&gt;

&lt;p&gt;The internet build adds one more layer to trace, and its mental model is small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.wireguard.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;WireGuard&lt;/strong&gt;&lt;/a&gt; is the encryption: each pair of tailnet devices shares cryptographic keys; traffic between them is a sealed UDP envelope no relay can open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NAT traversal&lt;/strong&gt; is the connection problem: both endpoints are (usually) behind NATs that reject unsolicited inbound packets. Tailscale's coordination plane tells each side what the other's observable address:port is, and the two sides simultaneously send packets &lt;em&gt;at each other&lt;/em&gt; — each side's outbound packet opens the pinhole in its own NAT that the other's inbound packets then slip through. That's "direct connection," and it's the common case.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://tailscale.com/blog/how-tailscale-works#encrypted-tcp-relays-derp" rel="noopener noreferrer"&gt;&lt;strong&gt;DERP&lt;/strong&gt;&lt;/a&gt; is the fallback for when no pinhole works (symmetric NAT on both ends, hostile firewalls): encrypted packets relayed through Tailscale's servers. Slower — an extra hop, and hop distance matters — but the payload stays end-to-end encrypted, so the relay is a post office, not a listener.&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%2F7r0to9myanytdjkuor9v.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%2F7r0to9myanytdjkuor9v.png" alt="Message sequence diagram: peer A and peer B each contact a rendezvous server first; the server tells each peer the other's observed address; A and B then send packets directly at each other so each side's outbound packet opens a pinhole in its own NAT for the other's reply to slip through, establishing a direct A-to-B path without the rendezvous server in the loop." width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The generic version of the same trick Tailscale's coordination plane runs before falling back to DERP.&lt;/p&gt;

&lt;p&gt;You can see which path you're on: &lt;code&gt;tailscale status&lt;/code&gt; on the desktop prints, per peer, &lt;code&gt;direct&lt;/code&gt; or &lt;code&gt;relay "..."&lt;/code&gt; — worth checking once when you set up, because it explains the latency you feel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3 — Troubleshooting
&lt;/h2&gt;

&lt;p&gt;The table below is for when something breaks — symptom to layer, so you debug in the right order instead of by guesswork:&lt;/p&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;Suspect layer&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;Phone can't connect to desktop at all (off-LAN)&lt;/td&gt;
&lt;td&gt;Tailnet&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tailscale status&lt;/code&gt; on both devices — is each online and showing the other? Then &lt;code&gt;tailscale ping &amp;lt;desktop-tailnet-ip&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same WiFi, correct IP, still times out (on-LAN)&lt;/td&gt;
&lt;td&gt;Phone network path&lt;/td&gt;
&lt;td&gt;Watch live while tapping Connect: `journalctl --user -f \&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tailnet up, VNC refuses&lt;/td&gt;
&lt;td&gt;Firewall / server&lt;/td&gt;
&lt;td&gt;Is x11vnc running? Does the ufw rule cover {% raw %}&lt;code&gt;100.64.0.0/10&lt;/code&gt;? (Mac: &lt;code&gt;sudo lsof -iTCP:5900 -sTCP:LISTEN&lt;/code&gt; — empty = Screen Sharing off.) &lt;code&gt;tailscale ping&lt;/code&gt; works but 5900 times out = firewall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VNC connects, password rejected&lt;/td&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;8-character DES cap (first 8 are real); Mac: VNC password, username blank&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screen connects but input feels laggy (off-LAN)&lt;/td&gt;
&lt;td&gt;Transport&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;tailscale status&lt;/code&gt; — relayed instead of direct? Relayed + packet loss = transport, not the rig&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Everything connects, nothing types&lt;/td&gt;
&lt;td&gt;Client keyboard plumbing&lt;/td&gt;
&lt;td&gt;Does the client send key events for text (IME commits)? RealVNC Viewer does (tested live); a client that injects nothing won't — try its key panel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works on LAN, never off-LAN&lt;/td&gt;
&lt;td&gt;Tailnet membership&lt;/td&gt;
&lt;td&gt;Both devices logged into the &lt;em&gt;same&lt;/em&gt; tailnet? Phone's Tailscale toggle actually on? (Android kills VPNs quietly — check the key icon)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ubuntu unreachable after a power cut&lt;/td&gt;
&lt;td&gt;x11vnc autostart&lt;/td&gt;
&lt;td&gt;x11vnc starts only after graphical login — someone must log in locally once (the Mac serves its login window over VNC; Ubuntu can't)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  Quick start after first setup
&lt;/h3&gt;

&lt;p&gt;The setup above happens once. This is the 30-second checklist for every session after — or for any "suddenly won't connect" moment. Run these on the desk.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Find the desk's addresses
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt; &lt;span class="nt"&gt;-I&lt;/span&gt;
&lt;span class="c"&gt;# All IPv4s on one line. Ignore 172.x.x.x (Docker/VM bridges). You want:&lt;/span&gt;
&lt;span class="c"&gt;#   LAN:       192.168.x.x or 10.x.x.x   &amp;lt;- changes sometimes (DHCP)&lt;/span&gt;
&lt;span class="c"&gt;#   Tailscale: 100.x.x.x                 &amp;lt;- never changes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Explicit variant, showing which interface carries which (interface names vary per machine — don't memorize them):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip &lt;span class="nt"&gt;-4&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; addr show scope global
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a Mac, the one-glance version is even shorter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ipconfig getifaddr en0    &lt;span class="c"&gt;# Wi-Fi IP, nothing else; empty = not on Wi-Fi (desktop Macs: try en1)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Tailscale side
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailscale ip &lt;span class="nt"&gt;-4&lt;/span&gt;      &lt;span class="c"&gt;# the desk's permanent address — what the phone should dial&lt;/span&gt;
tailscale status     &lt;span class="c"&gt;# every tailnet device + its IP; the phone's line must NOT say "offline"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A phone showing &lt;code&gt;offline, last seen …&lt;/code&gt; = its Tailscale VPN is off (Android's battery optimization kills it quietly) — nothing off-LAN will connect until it's back on.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Is the server up?
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pgrep &lt;span class="nt"&gt;-af&lt;/span&gt; x11vnc     &lt;span class="c"&gt;# expect: x11vnc -display :N -auth guess ... -shared -repeat -forever&lt;/span&gt;
ss &lt;span class="nt"&gt;-tln&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;5900  &lt;span class="c"&gt;# expect: LISTEN 0.0.0.0:5900&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a Mac, the server is &lt;code&gt;screensharingd&lt;/code&gt; — one command answers both questions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;lsof &lt;span class="nt"&gt;-iTCP&lt;/span&gt;:5900 &lt;span class="nt"&gt;-sTCP&lt;/span&gt;:LISTEN   &lt;span class="c"&gt;# expect: screensharingd LISTEN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty on either = the server side is down — Ubuntu: re-login graphically (autostart fires on login); Mac: System Settings → General → Sharing → Screen Sharing got flipped off — flip it back on (and re-check the "VNC viewers may control screen with password" setting).&lt;/p&gt;

&lt;h4&gt;
  
  
  4. What the phone should dial
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Address in RealVNC Viewer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Phone on the &lt;strong&gt;same WiFi&lt;/strong&gt; as the desk&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;&amp;lt;lan-ip&amp;gt;:5900&lt;/code&gt; (e.g. &lt;code&gt;192.168.1.42:5900&lt;/code&gt; — example only)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anywhere else (cellular, other WiFi)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;tailscale-ip&amp;gt;:5900&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Rule of thumb: &lt;strong&gt;just use the Tailscale address always&lt;/strong&gt; — it works on the same WiFi too, and it never changes.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. Watch it live while the phone retries
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;journalctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; x11vnc        &lt;span class="c"&gt;# Ubuntu&lt;/span&gt;
log stream &lt;span class="nt"&gt;--predicate&lt;/span&gt; &lt;span class="s1"&gt;'process == "screensharingd"'&lt;/span&gt; &lt;span class="nt"&gt;--info&lt;/span&gt;   &lt;span class="c"&gt;# Mac — same read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it, hit Connect on the phone, read the outcome: &lt;strong&gt;nothing appears&lt;/strong&gt; → traffic isn't arriving (Tailscale off, wrong address, wrong WiFi, firewall — steps 1, 2, 4) · &lt;strong&gt;"Got connection" then auth failure&lt;/strong&gt; → connection is fine, wrong password · &lt;strong&gt;phone says "refused"&lt;/strong&gt; → right network, wrong IP — the LAN IP changed.&lt;/p&gt;

&lt;p&gt;Even faster, from any machine that can already reach the desktop (tailnet or LAN) — the ten-second port check, no logs and no phone needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-vz&lt;/span&gt; &amp;lt;desktop-ip&amp;gt; 5900
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An instant &lt;strong&gt;"refused"&lt;/strong&gt; means the packets arrived and nothing is listening — server off, start it (on the Mac, check the Screen Sharing toggle; on Ubuntu, &lt;code&gt;pgrep -af x11vnc&lt;/code&gt;). &lt;strong&gt;Silence, then timeout&lt;/strong&gt; means a path problem — VPN down, firewall, wrong network. &lt;strong&gt;"succeeded"&lt;/strong&gt; means go: the phone will connect. &lt;code&gt;nc&lt;/code&gt; is the built-in on macOS (there's no &lt;code&gt;telnet&lt;/code&gt; there anymore); on Ubuntu it ships with the default &lt;code&gt;netcat-openbsd&lt;/code&gt; package. Verified live on the Mac's internet route — it's the command worth remembering from this section.&lt;/p&gt;

&lt;h4&gt;
  
  
  6. Why the LAN IP drifts, and the fix
&lt;/h4&gt;

&lt;p&gt;The router hands out LAN IPs by DHCP — after a router reboot or lease expiry the desk can get a new one, and the phone's saved entry keeps dialing the old address. Two permanent fixes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;DHCP reservation&lt;/strong&gt; in the router settings ("always give this MAC this IP") — pins the LAN IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the Tailscale IP&lt;/strong&gt; — it never changes, and works from anywhere, not just home WiFi.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Option 2 is the better default: save &lt;code&gt;&amp;lt;tailscale-ip&amp;gt;:5900&lt;/code&gt; as the primary entry and this whole class of breakage disappears.&lt;/p&gt;

&lt;h4&gt;
  
  
  7. Phone error text → cause
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phone shows&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;The connection timed out&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Packets never arrived: Tailscale off on the phone, wrong network, AP isolation, or firewall&lt;/td&gt;
&lt;td&gt;Check the phone's Tailscale/WiFi details (SSID + gateway), verify the address&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Connection refused&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Network reachable, but no server at that address — almost always a stale LAN IP&lt;/td&gt;
&lt;td&gt;Re-check current IPs (step 1); prefer the Tailscale IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Authentication failed / wrong password&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Connection fine, VNC password mismatch&lt;/td&gt;
&lt;td&gt;Re-enter it, or reset on the desk: &lt;code&gt;x11vnc -storepasswd&lt;/code&gt; then restart (Ubuntu); System Settings → General → Sharing → Screen Sharing ⓘ (Mac)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  Tools &amp;amp; licenses
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;License / cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/LibVNC/x11vnc" rel="noopener noreferrer"&gt;x11vnc&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;VNC server, Ubuntu&lt;/td&gt;
&lt;td&gt;GPL-2.0; actively maintained; past CVEs addressed by 0.9.17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS Screen Sharing&lt;/td&gt;
&lt;td&gt;VNC server, macOS&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.realvnc.com/en/connect/download/viewer/" rel="noopener noreferrer"&gt;RealVNC Viewer&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Phone client, both OSes&lt;/td&gt;
&lt;td&gt;Free for personal use; direct-connection mode, no account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://tailscale.com/" rel="noopener noreferrer"&gt;Tailscale&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Mesh VPN (Part 2)&lt;/td&gt;
&lt;td&gt;Free personal plan; WireGuard end-to-end, SOC 2 Type II&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/juanfont/headscale" rel="noopener noreferrer"&gt;Headscale&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Self-hosted control plane (escape hatch)&lt;/td&gt;
&lt;td&gt;Open source, BSD-3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Never port-forward 5900, on either OS, for any reason. Classic VNC auth is a DES challenge-response capped at 8 characters — fine behind a LAN rule or a tailnet, never fine exposed to the internet.&lt;/p&gt;




&lt;h3&gt;
  
  
  Let's Connect
&lt;/h3&gt;

&lt;p&gt;If you try any of this, I'd rather hear what broke than what worked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://encryptioner.github.io" rel="noopener noreferrer"&gt;encryptioner.github.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn&lt;/strong&gt;: &lt;a href="https://www.linkedin.com/in/mir-mursalin-ankur" rel="noopener noreferrer"&gt;Mir Mursalin Ankur&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/Encryptioner" rel="noopener noreferrer"&gt;@Encryptioner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X (Twitter)&lt;/strong&gt;: &lt;a href="https://twitter.com/AnkurMursalin" rel="noopener noreferrer"&gt;@AnkurMursalin&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical Writing&lt;/strong&gt;: &lt;a href="https://nerddevs.com/author/ankur/" rel="noopener noreferrer"&gt;Nerddevs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support&lt;/strong&gt;: &lt;a href="https://www.supportkori.com/mirmursalinankur" rel="noopener noreferrer"&gt;SupportKori&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>productivity</category>
      <category>remote</category>
      <category>control</category>
      <category>development</category>
    </item>
    <item>
      <title>Self-Review With AI Before You Open the PR — A Practical Workflow with branchdiff</title>
      <dc:creator>Mir Mursalin Ankur</dc:creator>
      <pubDate>Mon, 01 Jun 2026 15:40:37 +0000</pubDate>
      <link>https://dev.to/mir_mursalin_ankur/self-review-with-ai-before-you-open-the-pr-a-practical-workflow-with-branchdiff-2bn</link>
      <guid>https://dev.to/mir_mursalin_ankur/self-review-with-ai-before-you-open-the-pr-a-practical-workflow-with-branchdiff-2bn</guid>
      <description>&lt;p&gt;You know the moment. You push the branch, open the PR, and immediately see it — the undefined return on the refund path, the token logged to the console, the TODO that was supposed to be temporary six weeks ago. The reviewer catches it four hours later and you reply "good catch, fixing now" as if someone else wrote that line.&lt;/p&gt;

&lt;p&gt;The first reviewer on most pull requests should have been the author. Half the comments you will receive — the missing null check, the untested error branch, the duplicate logic that could be extracted, the import that now goes nowhere — are things you would have caught with one more careful read-through. You skip that read because you have been in the code for two days and your brain completes the sentences for you. You see what you meant to write, not what is on the page.&lt;/p&gt;

&lt;p&gt;This post is about closing that gap with a structured AI-assisted self-review &lt;em&gt;before&lt;/em&gt; the PR opens. Not to skip the human reviewer — to walk into the review with the obvious problems already gone, the test gaps already filled, and the PR description already written. So the reviewer's attention can land on what actually needs a second pair of eyes.&lt;/p&gt;

&lt;p&gt;The tool is &lt;strong&gt;branchdiff&lt;/strong&gt;: a local browser app that runs your diff on &lt;code&gt;localhost&lt;/code&gt;, stores everything in &lt;code&gt;~/.branchdiff/&lt;/code&gt;, and keeps the AI surface controlled through an explicit &lt;code&gt;branchdiff agent&lt;/code&gt; command API. Nothing leaves your machine until you decide to push it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why "before the PR" is the right moment
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2F33502e2joobnqjhf78y0.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.amazonaws.com%2Fuploads%2Farticles%2F33502e2joobnqjhf78y0.png" alt="Self-Review: Before vs. After Opening the PR"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you review &lt;em&gt;after&lt;/em&gt; opening the PR, every AI fix becomes noise: a force-push, a re-read for your reviewer, another commit in the audit trail. If a teammate is already mid-review when you discover the bug, you look careless. The patch that should have been in the original push becomes a distraction for everyone downstream.&lt;/p&gt;

&lt;p&gt;If you review &lt;em&gt;before&lt;/em&gt; opening the PR, the AI's output is a private workspace. You act on what matters, commit the fixes into your own history (often as &lt;code&gt;fixup!&lt;/code&gt; commits you squash before pushing), and the PR that goes up is cleaner from the first push. Reviewers see your real work, not your stream-of-consciousness corrections.&lt;/p&gt;

&lt;p&gt;There is a subtler benefit too: &lt;strong&gt;you learn from the dismissals&lt;/strong&gt;. Every time you push back on an AI comment with a real reason — "the wrapper handles null upstream", "this is intentionally synchronous because of write contention" — you are writing down the rationale you would otherwise leave implicit. That reason is exactly what the next reviewer needs in the PR description. The self-review session generates your PR description as a side effect.&lt;/p&gt;




&lt;h2&gt;
  
  
  The workflow in 4 steps
&lt;/h2&gt;

&lt;p&gt;Assume you have a &lt;code&gt;feature/payments&lt;/code&gt; branch off &lt;code&gt;main&lt;/code&gt; — twelve files changed, about three hundred lines added. You want to clean it up before requesting review.&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.amazonaws.com%2Fuploads%2Farticles%2Fvv33bj2fm0xz71naoc2t.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.amazonaws.com%2Fuploads%2Farticles%2Fvv33bj2fm0xz71naoc2t.png" alt="Self-Review Workflow in 4 Steps"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — open your own diff locally
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;branchdiff main feature/payments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A browser tab opens at &lt;code&gt;http://localhost:5391&lt;/code&gt;. Because you are comparing two named refs, this is a &lt;strong&gt;persistent session&lt;/strong&gt; — comments you (or the AI) post here survive new commits to either branch. You can iterate across multiple rounds of fixes without losing the review trail. If you want a fresh start, &lt;code&gt;--new&lt;/code&gt; archives the current session and creates a clean one; archived sessions stay queryable via &lt;code&gt;branchdiff review threads --session &amp;lt;id&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The diff renders with split or unified view, syntax highlighting for 150+ languages, a sidebar of changed files, and keyboard shortcuts (&lt;code&gt;j&lt;/code&gt;/&lt;code&gt;k&lt;/code&gt; for next/previous file, &lt;code&gt;n&lt;/code&gt;/&lt;code&gt;p&lt;/code&gt; for next/previous hunk).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2 — run an AI review pass
&lt;/h3&gt;

&lt;p&gt;If you use Claude Code, install the skills once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;branchdiff skill add        &lt;span class="c"&gt;# adds .claude/skills/branchdiff-{review,resolve}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in your Claude Code session:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/branchdiff-review main feature/payments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The skill calls &lt;code&gt;branchdiff agent diff&lt;/code&gt; to read the full diff, then posts inline comments via &lt;code&gt;branchdiff agent comment --file &amp;lt;path&amp;gt; --line &amp;lt;n&amp;gt; --body "[tag] ..."&lt;/code&gt;. Each comment carries a severity tag:&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.amazonaws.com%2Fuploads%2Farticles%2Fsjztqqo8bsoxwg8apznh.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.amazonaws.com%2Fuploads%2Farticles%2Fsjztqqo8bsoxwg8apznh.png" alt="Comment Tag Taxonomy"&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;Tag&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Typical action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[must-fix]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bug, security issue, broken contract&lt;/td&gt;
&lt;td&gt;Fix before pushing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[suggestion]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Better pattern, missing test, refactor opportunity&lt;/td&gt;
&lt;td&gt;Fix or dismiss with reason&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[nit]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Style, naming, minor inconsistency&lt;/td&gt;
&lt;td&gt;Dismiss if low priority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;[question]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AI is uncertain, needs context&lt;/td&gt;
&lt;td&gt;Reply with explanation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you use a different AI, the README ships a copy-paste prompt that drives the same &lt;code&gt;branchdiff agent&lt;/code&gt; command surface. Or pipe the context directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;branchdiff review context | claude &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"review for security and breaking changes"&lt;/span&gt;
branchdiff review run &lt;span class="nt"&gt;--exec&lt;/span&gt; &lt;span class="s2"&gt;"claude"&lt;/span&gt; &lt;span class="nt"&gt;--mode&lt;/span&gt; review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two improvements in v1.5.0 make this pass noticeably better:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Constructive tone.&lt;/strong&gt; The skill leads with the problem, not a judgment — "This returns &lt;code&gt;undefined&lt;/code&gt; when &lt;code&gt;amount === 0&lt;/code&gt;" rather than "This is wrong." Collaborative language. When AI comments read as observations rather than accusations, engineers actually fix the bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nth-time review awareness.&lt;/strong&gt; Before commenting, the skill reads resolved and dismissed threads. Resolved threads are not re-raised; dismissed ones are only re-flagged if there is new evidence. You can run &lt;code&gt;/branchdiff-review&lt;/code&gt; after every meaningful commit without re-litigating the same feedback.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 3 — read, triage, decide
&lt;/h3&gt;

&lt;p&gt;Open the browser. Each AI comment is anchored to a line in the diff, tag visible in the gutter, body inline. Four options per thread:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix it&lt;/strong&gt; — make the change in your editor, then resolve the thread (&lt;code&gt;branchdiff agent resolve &amp;lt;id&amp;gt; --summary "added null guard"&lt;/code&gt; or click &lt;em&gt;Resolve&lt;/em&gt; in the UI). The summary is optional but worth writing — future review passes and future reviewers can see what was done and why.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-fix it&lt;/strong&gt; — if you are using Claude Code, &lt;code&gt;/branchdiff-resolve&lt;/code&gt; reads open comments, applies mechanical fixes (rename, extract function, null check, missing test stub), and resolves threads with a summary. &lt;strong&gt;Read the diff before committing&lt;/strong&gt; — the AI occasionally takes a suggestion more literally than you intended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dismiss it&lt;/strong&gt; — the comment was wrong, out of scope, or already handled upstream. &lt;code&gt;branchdiff agent dismiss &amp;lt;id&amp;gt; --reason "..."&lt;/code&gt; records why. That reason is what nth-time awareness uses so the same point is not re-raised next pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reply to it&lt;/strong&gt; — for &lt;code&gt;[question]&lt;/code&gt; tags, &lt;code&gt;branchdiff agent reply &amp;lt;id&amp;gt; --body "..."&lt;/code&gt; lets you answer in-thread. Useful when the AI flagged unclear behaviour and you want the rationale on record for the next reviewer.&lt;/p&gt;

&lt;p&gt;The dismissal reason is the single most important habit to build. It keeps the AI honest across passes, and it leaves a paper trail you can point to when a reviewer asks "why is this line like that?"&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3.5 — use the review tools that make long diffs tractable
&lt;/h3&gt;

&lt;p&gt;The features that make a teammate's 40-file PR manageable pay equal dividends on your own branch:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Viewed counter + stale detection.&lt;/strong&gt; Mark files viewed (eye icon or right-click) as you walk through. The sidebar shows &lt;code&gt;12 / 24 viewed&lt;/code&gt;. Any file you marked viewed but later changed gets flipped to &lt;em&gt;stale&lt;/em&gt; with an amber dot — staleness is detected via an FNV-1a hash on the diff signature, so a rebase that does not change the content does not invalidate your markers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sidebar filters.&lt;/strong&gt; Nine filter chips — &lt;em&gt;Commented&lt;/em&gt;, &lt;em&gt;Uncommented&lt;/em&gt;, &lt;em&gt;Viewed&lt;/em&gt;, &lt;em&gt;Unviewed&lt;/em&gt;, &lt;em&gt;Stale&lt;/em&gt;, &lt;em&gt;Collapsed&lt;/em&gt;, &lt;em&gt;Expanded&lt;/em&gt;, &lt;em&gt;Staged&lt;/em&gt;, &lt;em&gt;Unstaged&lt;/em&gt; — stack with the search box. On the second pass after a set of fixes: &lt;em&gt;Filter → Stale&lt;/em&gt; shows exactly which files need re-reading.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collapse all / Expand all.&lt;/strong&gt; A quick high-level pass before diving in. Files with open comments are force-expanded so threads are never hidden behind a collapsed diff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Staged / unstaged toggle.&lt;/strong&gt; When reviewing your working tree before the first commit, flip between &lt;code&gt;git diff --staged&lt;/code&gt; and &lt;code&gt;git diff&lt;/code&gt; from the toolbar. File rows show inline status badges: &lt;strong&gt;S&lt;/strong&gt; (staged) and &lt;strong&gt;U&lt;/strong&gt; (unstaged).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full-file view + minimap.&lt;/strong&gt; The toolbar's &lt;em&gt;Full-file view&lt;/em&gt; opens a VS Code-style rendering of the entire file with all hunks in place. A &lt;strong&gt;minimap on the right&lt;/strong&gt; marks added, removed, and modified regions — scan a 1,000-line file at a glance and click straight to the change. Inline comments stay anchored when you switch from hunk view to full-file view. Markdown files get a &lt;strong&gt;Preview&lt;/strong&gt; toggle (v1.5.0) that renders both sides as formatted markdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commit detail page.&lt;/strong&gt; Click any commit in the history sidebar to open &lt;code&gt;/commit/:hash&lt;/code&gt; with the full SHA, parent links, file list with &lt;code&gt;+N / -N&lt;/code&gt; counts, and the same diff view. The back button preserves your position.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — focused passes for the risky parts
&lt;/h3&gt;

&lt;p&gt;The generalist review catches a wide range. These four pre-built workflows catch a narrower, deeper class of issue:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security audit&lt;/strong&gt; — looks only for injection (SQL, command, XSS, template, NoSQL, prompt), secret leaks, weak crypto, broken authorisation, deserialisation traps, path traversal, SSRF, and dependency risk. Skips style entirely. A 200-line auth diff produces five precise comments instead of fifty.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test coverage gaps&lt;/strong&gt; — for every new function, branch, or error path, checks the test directory and flags uncovered paths with a stub &lt;code&gt;it(...)&lt;/code&gt; suggestion. Priority: error branches → new public API → edge cases → happy path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breaking-change review&lt;/strong&gt; — classifies every change as breaking or non-breaking, drafts an UPGRADE.md snippet for breaking ones, flags schema migrations without a rollback path as &lt;code&gt;[must-fix]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependency review&lt;/strong&gt; — flags added or major-bumped packages with maintenance status, license compatibility, bundle-size delta, first-party alternatives, and known CVEs.&lt;/p&gt;




&lt;h2&gt;
  
  
  A realistic session on a 12-file payments branch
&lt;/h2&gt;

&lt;p&gt;Here is what &lt;code&gt;branchdiff agent list --status open&lt;/code&gt; might look like after the general review pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[must-fix]   src/billing/charge.ts:84      Refund path returns undefined when amount === 0
[must-fix]   src/api/webhooks.ts:42        Missing signature verification before parsing body
[suggestion] src/utils/format.ts:12        Replace toFixed(2) with Intl.NumberFormat for i18n
[suggestion] src/billing/charge.ts:140     No test covers partial-refund branch
[suggestion] src/billing/charge.ts:156     Extract retry logic — duplicated three times
[nit]        src/types.ts:7                Inconsistent enum casing
[question]   src/api/webhooks.ts:55        Should we 200 on duplicate webhook IDs or 409?
&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.amazonaws.com%2Fuploads%2Farticles%2Fagkvesdlepgturajuilx.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.amazonaws.com%2Fuploads%2Farticles%2Fagkvesdlepgturajuilx.png" alt="Sample Self-Review Session — Comment Breakdown"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two &lt;code&gt;[must-fix]&lt;/code&gt; items, three improvements, one nit to dismiss, one question to answer. Twenty minutes of cleanup. The PR you push is materially better than the one you would have pushed before lunch.&lt;/p&gt;

&lt;p&gt;The AI's general comment often summarises the change set in two or three sentences ready to paste into the PR description — that alone saves five minutes of staring at the PR form wondering how to explain what you did.&lt;/p&gt;

&lt;p&gt;And the dismissal trail matters beyond this session. When you dismiss the &lt;code&gt;[nit]&lt;/code&gt; with reason "team style is mixed casing for legacy enums — see ADR-014", that reason is on record. The next engineer who reads &lt;code&gt;src/types.ts&lt;/code&gt; and wonders about the inconsistency has an answer one &lt;code&gt;branchdiff&lt;/code&gt; session away.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to stay skeptical
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;AI is fallible.&lt;/strong&gt; It will flag non-issues and miss real bugs. The most common failure mode is confident-sounding wrong advice on async code — read those comments twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local context only.&lt;/strong&gt; The AI sees the diff and the files in your repo. It does not see runtime behaviour, production logs, or upstream service contracts. Those still need a human reviewer or an integration test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token budget.&lt;/strong&gt; For a 200-file refactor, point the AI at the riskiest ten files first — you can always run a second pass with a different focus.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not auto-resolve everything.&lt;/strong&gt; &lt;code&gt;/branchdiff-resolve&lt;/code&gt; is convenient, but read the patches before committing. The AI will occasionally "fix" something by deleting code instead of correcting it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI does not replace understanding your own change.&lt;/strong&gt; If you do not understand a chunk of code well enough to review it, no AI pass magically fills that gap.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ship the PR
&lt;/h2&gt;

&lt;p&gt;When the session is clean — open count at zero, threads resolved or dismissed with reasons attached — commit your fixes, squash any &lt;code&gt;fixup!&lt;/code&gt; commits, and push. Either open the PR on the platform or use the &lt;em&gt;Open a Pull Request&lt;/em&gt; button in the branchdiff toolbar (it appears automatically when no PR exists for the branch). The local session stays in &lt;code&gt;~/.branchdiff/&lt;/code&gt; for your own reference; nothing from the AI pass needs to land on the PR unless you explicitly push it.&lt;/p&gt;

&lt;p&gt;The reviewer who picks it up sees a tighter diff, fewer obvious bugs, a clearer description. Their attention can land on the parts that need real judgement: the architectural decision, the unclear contract, the edge case that matters in production but not in tests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick start
&lt;/h2&gt;

&lt;p&gt;Full install guide, changelog, and uninstall steps on the &lt;a href="https://encryptioner.github.io/branchdiff-releases/" rel="noopener noreferrer"&gt;branchdiff releases page&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @encryptioner/branchdiff
&lt;span class="c"&gt;# or: pip install branchdiff&lt;/span&gt;
&lt;span class="c"&gt;# or: brew tap encryptioner/branchdiff https://github.com/encryptioner/branchdiff-releases \&lt;/span&gt;
&lt;span class="c"&gt;#          &amp;amp;&amp;amp; brew install branchdiff&lt;/span&gt;

branchdiff skill add                         &lt;span class="c"&gt;# one-time, for Claude Code&lt;/span&gt;
branchdiff main feature/your-branch          &lt;span class="c"&gt;# opens local UI&lt;/span&gt;
&lt;span class="c"&gt;# in Claude Code:  /branchdiff-review&lt;/span&gt;
&lt;span class="c"&gt;# triage threads → fix → resolve → dismiss with reasons&lt;/span&gt;
&lt;span class="c"&gt;# squash fixup! commits → push branch → open PR&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Self-review is the part of the workflow you can improve without changing your team's process. Try it on the next branch you were about to push without looking at it again.&lt;/p&gt;




&lt;h2&gt;
  
  
  Let's Connect
&lt;/h2&gt;

&lt;p&gt;I am always excited to hear what you are building. If this guide helped, or if you have questions about building self-review habits into your workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://encryptioner.github.io" rel="noopener noreferrer"&gt;encryptioner.github.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn&lt;/strong&gt;: &lt;a href="https://www.linkedin.com/in/mir-mursalin-ankur" rel="noopener noreferrer"&gt;Mir Mursalin Ankur&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/Encryptioner" rel="noopener noreferrer"&gt;@Encryptioner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X (Twitter)&lt;/strong&gt;: &lt;a href="https://twitter.com/AnkurMursalin" rel="noopener noreferrer"&gt;@AnkurMursalin&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical Writing&lt;/strong&gt;: &lt;a href="https://nerddevs.com/author/ankur/" rel="noopener noreferrer"&gt;Nerddevs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support&lt;/strong&gt;: &lt;a href="https://www.supportkori.com/mirmursalinankur" rel="noopener noreferrer"&gt;SupportKori&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;branchdiff releases, install guide, and changelog: &lt;a href="https://encryptioner.github.io/branchdiff-releases/" rel="noopener noreferrer"&gt;encryptioner.github.io/branchdiff-releases&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>softwareengineering</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Graphify + code-review-graph: Build a Self-Updating Knowledge Graph for Claude Code and other AI Coding Agent</title>
      <dc:creator>Mir Mursalin Ankur</dc:creator>
      <pubDate>Sun, 17 May 2026 15:31:39 +0000</pubDate>
      <link>https://dev.to/mir_mursalin_ankur/graphify-code-review-graph-build-a-self-updating-knowledge-graph-for-claude-code-and-other-ai-j1m</link>
      <guid>https://dev.to/mir_mursalin_ankur/graphify-code-review-graph-build-a-self-updating-knowledge-graph-for-claude-code-and-other-ai-j1m</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Every developer working with LLMs on a large codebase eventually hits the same wall: context windows are finite, but codebases are not.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You start a new AI coding session, ask about the payment flow — and your agent starts re-reading dozens of files just to get oriented. Twenty thousand tokens evaporated before a single line of code is written. Multiply that by every session, every team member, every day.&lt;/p&gt;

&lt;p&gt;Two open-source tools solve this in different but complementary ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Graphify&lt;/strong&gt; — converts your folder into a queryable knowledge graph with community detection, Obsidian-compatible reports, and cross-file traversal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;code-review-graph (CRG)&lt;/strong&gt; — builds a SQLite-backed AST graph with blast-radius analysis, embedding-based semantic search, ~25 MCP tools (allow-listable to a working set of 8), and sub-second incremental updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guide walks through installing both tools, connecting them to any AI coding agent — Claude Code, Cursor, Gemini CLI, Windsurf, GitHub Copilot, and more — wiring auto-updates for code edited by humans, git commits, or the agent itself, and pairing everything with an Obsidian vault as a persistent memory layer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pick one, both, or none — every section degrades gracefully.&lt;/strong&gt; Each tool works standalone; the smart-grep-hook, session-start cheatsheet, and CLAUDE.md routing rules all detect what is present and adapt. Use just graphify if you want a pure CLI / zero-MCP setup. Use just CRG if you want embedding-aware semantic search and PR-grade impact tools. Use both for the full stack (CRG primary, graphify on miss). If neither is installed, the agent silently falls back to grep — no broken hooks, no errors.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;All commands in this guide were tested on Ubuntu and macOS across multiple real pnpm monorepos of varying sizes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real Numbers from Two Test Projects
&lt;/h2&gt;

&lt;p&gt;Before diving in, here's what both tools produced across two real codebases — one a full-stack TypeScript monorepo with 5 packages, the other a lighter frontend-only repo:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Graphify (AST-only)&lt;/th&gt;
&lt;th&gt;code-review-graph&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Files indexed (large)&lt;/td&gt;
&lt;td&gt;1,020&lt;/td&gt;
&lt;td&gt;1,052&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nodes (large)&lt;/td&gt;
&lt;td&gt;3,815&lt;/td&gt;
&lt;td&gt;5,780&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edges (large)&lt;/td&gt;
&lt;td&gt;4,830&lt;/td&gt;
&lt;td&gt;30,611&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files indexed (small)&lt;/td&gt;
&lt;td&gt;702&lt;/td&gt;
&lt;td&gt;711&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nodes (small)&lt;/td&gt;
&lt;td&gt;2,035&lt;/td&gt;
&lt;td&gt;2,773&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edges (small)&lt;/td&gt;
&lt;td&gt;2,357&lt;/td&gt;
&lt;td&gt;15,037&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Communities&lt;/td&gt;
&lt;td&gt;750 / 499&lt;/td&gt;
&lt;td&gt;28 wiki pages each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental update&lt;/td&gt;
&lt;td&gt;~10s (8 workers)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.425s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM tokens used&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;graphify-out/&lt;/code&gt; (JSON)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.code-review-graph/&lt;/code&gt; (SQLite)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  How Each Tool Works
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fyo9q8mfe5gtz74m1yllz.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.amazonaws.com%2Fuploads%2Farticles%2Fyo9q8mfe5gtz74m1yllz.png" alt="Pipeline Architecture: Graphify vs code-review-graph" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Graphify — Two-Pass Graph with Communities
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Code
    │
    ▼
Pass 1: Tree-sitter AST   ← 0 tokens, 25 languages
(classes, functions, imports, call graphs)
    │
    ▼
Pass 2: AI Extraction     ← only for PDFs, images, markdown (optional)
(semantic relationships via Claude subagents)
    │
    ▼
NetworkX Graph + Leiden Clustering
    │
    ├── graphify-out/graph.json       (queryable)
    ├── graphify-out/GRAPH_REPORT.md  (750 communities, Obsidian links)
    ├── graphify-out/graph.html       (interactive visual)
    └── graphify-out/cache/           (SHA256 per file)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each edge has a confidence tag:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tag&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Confidence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;EXTRACTED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Directly in AST&lt;/td&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INFERRED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reasonable deduction&lt;/td&gt;
&lt;td&gt;0.7–0.9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AMBIGUOUS&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Needs review&lt;/td&gt;
&lt;td&gt;&amp;lt;0.7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On the large monorepo: &lt;strong&gt;87% EXTRACTED · 13% INFERRED · 0% AMBIGUOUS&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  code-review-graph — Blast-Radius Graph with MCP
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Code (git-tracked files)
    │
    ▼
Tree-sitter AST (23 languages, 0 tokens)
    │
    ▼
SQLite (.code-review-graph/graph.db)
    │
    ├── Nodes: functions, classes, files
    ├── Edges: imports, calls, inheritance
    └── Full-text search index (FTS5)
    │
    ▼
~25 MCP tools available (allow-list to ~8 via CRG_TOOLS env — see "Strip Unused CRG Tools")
(semantic_search_nodes, query_graph, get_impact_radius, list_communities, ...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Install one or both — the rest of the guide shows where each section applies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ubuntu
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Pick what you need:&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;graphifyy         &lt;span class="c"&gt;# graphify CLI (note: two y's on PyPI)&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;code-review-graph &lt;span class="c"&gt;# CRG (CLI + MCP server, includes embeddings extra)&lt;/span&gt;

&lt;span class="c"&gt;# Verify (only the lines for installed tools):&lt;/span&gt;
graphify &lt;span class="nt"&gt;--help&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt;
code-review-graph &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  macOS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Via uv (fastest):&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install &lt;/span&gt;graphifyy           &lt;span class="c"&gt;# graphify only&lt;/span&gt;
uv tool &lt;span class="nb"&gt;install &lt;/span&gt;code-review-graph   &lt;span class="c"&gt;# CRG only — or run both lines for both&lt;/span&gt;

&lt;span class="c"&gt;# Or via pipx:&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;pipx
pipx &lt;span class="nb"&gt;install &lt;/span&gt;graphifyy
pipx &lt;span class="nb"&gt;install &lt;/span&gt;code-review-graph
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;PyPI quirk&lt;/strong&gt;: The package is &lt;code&gt;graphifyy&lt;/code&gt; (two y's). The CLI command after install is &lt;code&gt;graphify&lt;/code&gt; (one y).&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 1: Create Ignore Files
&lt;/h2&gt;

&lt;p&gt;Before building any graph, exclude noise from indexing. Place these at your project root.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.graphifyignore&lt;/code&gt;&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;node_modules/
dist/
build/
.pnpm-store/
coverage/
*.min.js
*.min.css
*.map
pnpm-lock.yaml
yarn.lock
*.lock
*.log
.env*
graphify-out/
.code-review-graph/
*.example.*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;.code-review-graphignore&lt;/code&gt;&lt;/strong&gt; (same content)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node_modules/
dist/
build/
.pnpm-store/
coverage/
*.min.js
*.min.css
*.map
pnpm-lock.yaml
yarn.lock
*.lock
*.log
.env*
graphify-out/
.code-review-graph/
*.example.*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: Build the Graphs Manually
&lt;/h2&gt;

&lt;h3&gt;
  
  
  code-review-graph
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/your-project

&lt;span class="c"&gt;# Full build (first time) — parses all files&lt;/span&gt;
code-review-graph build

&lt;span class="c"&gt;# Output (large monorepo):&lt;/span&gt;
&lt;span class="c"&gt;# Full build: 1052 files, 5780 nodes, 30611 edges (postprocess=full)&lt;/span&gt;

&lt;span class="c"&gt;# Output (smaller frontend repo):&lt;/span&gt;
&lt;span class="c"&gt;# Full build: 711 files, 2773 nodes, 15037 edges (postprocess=full)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Graphify (AST-only, no LLM cost)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/your-project

&lt;span class="c"&gt;# AST-only update (no API key required)&lt;/span&gt;
graphify update &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Output (large monorepo):&lt;/span&gt;
&lt;span class="c"&gt;# Rebuilt: 3815 nodes, 4830 edges, 750 communities&lt;/span&gt;
&lt;span class="c"&gt;# graph.json, graph.html and GRAPH_REPORT.md updated in graphify-out&lt;/span&gt;

&lt;span class="c"&gt;# Output (smaller repo):&lt;/span&gt;
&lt;span class="c"&gt;# Rebuilt: 2035 nodes, 2357 edges, 499 communities&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the richer semantic graph (PDFs, images, markdown — uses LLM):&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;# Full extraction with Claude subagents (requires ANTHROPIC_API_KEY)&lt;/span&gt;
graphify extract &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Register with Your AI Agent
&lt;/h2&gt;

&lt;h3&gt;
  
  
  code-review-graph (auto-configures 5 platforms)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code-review-graph &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single command:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writes &lt;code&gt;.mcp.json&lt;/code&gt; (Claude Code MCP server config)&lt;/li&gt;
&lt;li&gt;Writes &lt;code&gt;.cursor/mcp.json&lt;/code&gt;, &lt;code&gt;.opencode.json&lt;/code&gt;, Zed settings, &lt;code&gt;.cursorrules&lt;/code&gt;, &lt;code&gt;GEMINI.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Creates &lt;code&gt;.claude/skills/&lt;/code&gt; for Claude Code tool integration&lt;/li&gt;
&lt;li&gt;Installs hooks in &lt;code&gt;.claude/settings.json&lt;/code&gt; (move these out — see below)&lt;/li&gt;
&lt;li&gt;Installs a &lt;strong&gt;git pre-commit hook&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Updates &lt;code&gt;.gitignore&lt;/code&gt; to exclude &lt;code&gt;.code-review-graph/&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Post-install housekeeping:&lt;/strong&gt; &lt;code&gt;code-review-graph install&lt;/code&gt; is aggressive — it writes configs for every AI IDE it knows about. Most teams only use one. Add the noise to &lt;code&gt;.gitignore&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;# .gitignore additions
AGENTS.md
GEMINI.md
.mcp.json          # keep only .mcp.example.json
.cursorrules
.windsurfrules
.opencode.json
.kiro/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Three-file hook pattern:&lt;/strong&gt; &lt;code&gt;settings.json&lt;/code&gt; stays clean — permissions only, no hooks. Hooks go into example/local:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.claude/settings.example.json&lt;/code&gt;&lt;/strong&gt; (committed) — documents the full hook structure for teammates. Contains all graph hooks (Stop, SessionStart, PreToolUse). Copy from here to activate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.claude/settings.local.json&lt;/code&gt;&lt;/strong&gt; (gitignored) — your actual personal hooks that fire at runtime. Copy from &lt;code&gt;settings.example.json&lt;/code&gt; on first setup, then customize with env vars or secrets. You can also copy individual hooks into your &lt;strong&gt;global &lt;code&gt;~/.claude/settings.json&lt;/code&gt;&lt;/strong&gt; if you want them active across all projects.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.claude/settings.example.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;committed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;reference,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;shows&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;hook&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;structure&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Copy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;file&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.claude/settings.local.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;and&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;customize&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&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;"_comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CRG auto-update after AI finishes turn — once per turn, PID-guarded."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command -v code-review-graph &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; [ -d .code-review-graph ] &amp;amp;&amp;amp; { PF=/tmp/crg-claude.pid; if [ -f &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$PF&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; ] &amp;amp;&amp;amp; kill -0 &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$(cat &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$PF&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; 2&amp;gt;/dev/null; then true; else { code-review-graph update --skip-flows 2&amp;gt;/dev/null &amp;amp;&amp;amp; nohup code-review-graph embed &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp; } &amp;amp; echo $! &amp;gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$PF&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; fi; } || 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;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;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;span class="nl"&gt;"SessionStart"&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;"_comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CRG graph status on session open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command -v code-review-graph &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; code-review-graph status 2&amp;gt;/dev/null || 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;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"_comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Setup nudge — prompt to initialize if CLI installed but no graph built yet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"if command -v code-review-graph &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; git rev-parse --is-inside-work-tree &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; [ ! -d .code-review-graph ]; then printf '{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;systemMessage&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Graph tool installed but not yet initialized. Ask me to set up: code-review-graph (code-review-graph install)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}'; fi || 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;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"_comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Graph query cheatsheet — injected once per session (~150 tokens)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"if [ -f .code-review-graph/graph.db ] || [ -f graphify-out/graph.json ]; then STATS=&lt;/span&gt;&lt;span class="se"&gt;\"\"&lt;/span&gt;&lt;span class="s2"&gt;; TOOL_LINES=&lt;/span&gt;&lt;span class="se"&gt;\"\"&lt;/span&gt;&lt;span class="s2"&gt;; if [ -f .code-review-graph/graph.db ]; then STATS=$(python3 -c &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;import sqlite3; c=sqlite3.connect('.code-review-graph/graph.db'); n=c.execute('SELECT COUNT(*) FROM nodes').fetchone()[0]; e=c.execute('SELECT COUNT(*) FROM edges').fetchone()[0]; print(f'{n} nodes, {e} edges'); c.close()&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; 2&amp;gt;/dev/null || echo &lt;/span&gt;&lt;span class="se"&gt;\"\"&lt;/span&gt;&lt;span class="s2"&gt;); TOOL_LINES=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;  where is X defined    → semantic_search_nodes_tool(query=X)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  who calls X           → query_graph_tool(pattern=callers_of, target=X)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  pre-refactor blast    → get_impact_radius_tool(changed_files=[...])&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  community/cluster     → list_communities_tool()&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  code review context   → get_review_context_tool(changed_files=[...])&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; fi; if [ -f graphify-out/graph.json ]; then GFY_STATS=$(python3 -c &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;import json; g=json.load(open('graphify-out/graph.json')); nodes=g.get('nodes',[]); comms=len(set(n.get('community','') for n in nodes if n.get('community',''))); print(f'{len(nodes)} nodes, {comms} communities')&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; 2&amp;gt;/dev/null || echo &lt;/span&gt;&lt;span class="se"&gt;\"\"&lt;/span&gt;&lt;span class="s2"&gt;); [ -n &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$GFY_STATS&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; ] &amp;amp;&amp;amp; STATS=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;${STATS:+$STATS | }graphify: $GFY_STATS&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; TOOL_LINES=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;${TOOL_LINES:+$TOOL_LINES&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n}  CRG miss / explore    → graphify query '&amp;lt;term&amp;gt;' --graph graphify-out/graph.json&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  path A→B              → graphify path '&amp;lt;from&amp;gt;' '&amp;lt;to&amp;gt;' --graph graphify-out/graph.json&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; fi; printf '{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;hookSpecificOutput&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;hookEventName&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;SessionStart&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;additionalContext&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;GRAPH QUERY CHEATSHEET (%s) — use BEFORE Read/Grep/Bash-find on code:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n%s&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;nSkip graph for: .md .json .yml .log .jsonl configs cross-repo paths.&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;nOverride grep gate: append --graph-tried to any Bash command.&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}}' &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$STATS&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$TOOL_LINES&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; fi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;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;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .claude/settings.local.json — gitignored, actual runtime hooks&lt;/span&gt;
&lt;span class="c"&gt;# Copy from settings.example.json on first setup:&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .claude/settings.example.json .claude/settings.local.json
&lt;span class="c"&gt;# Then add any personal env vars or secrets&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why Stop, not PostToolUse?&lt;/strong&gt; &lt;code&gt;PostToolUse&lt;/code&gt; fires after &lt;em&gt;every&lt;/em&gt; individual file edit — if Claude makes 10 edits in one response, it fires 10 times. The &lt;code&gt;pgrep&lt;/code&gt; guard that prevents double-spawning has a race window of a few milliseconds, which is not enough to block concurrent spawns reliably. The result: multiple CRG Python processes pile up, load average hits 12+, RAM saturates, and the machine slows to a crawl. &lt;code&gt;Stop&lt;/code&gt; fires &lt;strong&gt;once&lt;/strong&gt; when the AI finishes its entire turn — all edits batched, single update triggered. The PID-file guard then prevents overlap &lt;em&gt;across&lt;/em&gt; turns (skip if the previous turn's update is still running). Zero pile-up by design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why graphify update is NOT in any Claude hook:&lt;/strong&gt; &lt;code&gt;graphify update&lt;/code&gt; takes ~10s+ on a real monorepo (even with SHA256 caching). Running it in &lt;code&gt;Stop&lt;/code&gt; or &lt;code&gt;PostToolUse&lt;/code&gt; would block the session or cause the process to hang in the background, accumulating across turns. It belongs only in git hooks (&lt;code&gt;post-commit&lt;/code&gt;, &lt;code&gt;post-checkout&lt;/code&gt;) where it runs as a fully detached &lt;code&gt;nohup&lt;/code&gt; process after the commit returns — the developer has already moved on, so the latency is invisible.&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;&lt;span class="c"&gt;# .claude/settings.local.json — gitignored, actual runtime hooks (copy from example)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.mcp.json&lt;/code&gt; it creates (keep as &lt;code&gt;.mcp.example.json&lt;/code&gt; only):&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;"mcpServers"&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;"code-review-graph"&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;"uvx"&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;"code-review-graph"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"serve"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdio"&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;h3&gt;
  
  
  Graphify (AI Agent Integration)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Adds graphify section to CLAUDE.md + PreToolUse hook (Claude Code)&lt;/span&gt;
graphify claude &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Graphify's &lt;code&gt;claude install&lt;/code&gt; adds a &lt;code&gt;PreToolUse&lt;/code&gt; hook that intercepts &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;rg&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt; commands and redirects the agent to &lt;code&gt;graphify query&lt;/code&gt; instead — turning search interception into graph navigation. For other agents: &lt;code&gt;code-review-graph install&lt;/code&gt; already writes equivalent config to &lt;code&gt;GEMINI.md&lt;/code&gt;, &lt;code&gt;AGENTS.md&lt;/code&gt;, &lt;code&gt;.cursorrules&lt;/code&gt;, and Zed settings.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;code&gt;graphify claude install&lt;/code&gt; writes the &lt;code&gt;PreToolUse&lt;/code&gt; hook into &lt;code&gt;.claude/settings.json&lt;/code&gt;. Move it to &lt;code&gt;.claude/settings.example.json&lt;/code&gt; (committed as a reference) and copy to &lt;code&gt;.claude/settings.local.json&lt;/code&gt; to activate it locally — not every teammate will have graphify installed, so it shouldn't fire automatically for everyone.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Auto-Update on Commit (the full picture)
&lt;/h3&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.amazonaws.com%2Fuploads%2Farticles%2Fjwt5sqmh30h4mejuc8fz.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.amazonaws.com%2Fuploads%2Farticles%2Fjwt5sqmh30h4mejuc8fz.png" alt="Auto-Update Triggers: PostToolUse, git commit, branch switch" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three independent triggers keep the graph fresh — install all of them so no edit path leaves it stale:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Hook location&lt;/th&gt;
&lt;th&gt;Tool&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;&lt;strong&gt;Claude finishes a turn&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;settings.example.json&lt;/code&gt; → &lt;code&gt;settings.local.json&lt;/code&gt; Stop hook&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;CRG only&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~0.425s — fast enough to run after every AI turn; PID-guarded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Any commit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.git/hooks/post-commit&lt;/code&gt; (or &lt;code&gt;.husky/post-commit&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;graphify + CRG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;terminal commits, IDE commits, other AI tools; graphify runs as background &lt;code&gt;nohup&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Branch switch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.git/hooks/post-checkout&lt;/code&gt; (or &lt;code&gt;.husky/post-checkout&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;graphify + CRG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;smart: ≤5 files diff → incremental update; &amp;gt;5 files or new branch → full rebuild; background &lt;code&gt;nohup&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;graphify is NOT in the Claude Stop hook.&lt;/strong&gt; &lt;code&gt;graphify update&lt;/code&gt; takes ~10s+ on large monorepos — too slow for an AI turn hook. It would pile up, hang in the background, and saturate CPU/RAM (observed: 3 stuck processes at 65–73% CPU each). Use git hooks instead — the developer has already moved on by the time graphify finishes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Graphify ships its own installer for the git side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;graphify hook &lt;span class="nb"&gt;install&lt;/span&gt;   &lt;span class="c"&gt;# writes post-commit + post-checkout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Code-review-graph does not.&lt;/strong&gt; Its &lt;code&gt;install&lt;/code&gt; command writes a &lt;code&gt;pre-commit&lt;/code&gt; hook that runs &lt;code&gt;detect-changes --brief&lt;/code&gt; (a status warning before the commit lands) but no &lt;code&gt;post-commit&lt;/code&gt; hook to update the SQLite graph after. Without one, &lt;code&gt;.code-review-graph/graph.db&lt;/code&gt; only refreshes when Claude touches a file — every terminal/IDE/other-tool commit drifts.&lt;/p&gt;

&lt;p&gt;Add the post-commit update yourself. Both forms are detached so &lt;code&gt;git commit&lt;/code&gt; returns immediately.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Resource guard required.&lt;/strong&gt; Graph rebuilds are CPU-intensive. Without guards, multiple rebuilds can pile up (observed: 3 concurrent graphify processes at 65–73% CPU each, load average 12+, RAM saturated). Every hook below includes a &lt;code&gt;_resources_ok&lt;/code&gt; check (CPU ≤ 50% of cores, memory ≥ 2 GB free) and a &lt;code&gt;pgrep&lt;/code&gt; process deduplication guard. If either check fails, the rebuild silently skips — the next commit retriggers a fresh attempt.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Plain git project — append to &lt;code&gt;.git/hooks/post-commit&lt;/code&gt;:&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;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# Knowledge graph tools are optional — each section silently skips if tools are absent.&lt;/span&gt;

&lt;span class="c"&gt;# Returns 0 (ok) when CPU load and free memory are within acceptable limits.&lt;/span&gt;
&lt;span class="c"&gt;# CPU: 1-min load average must be &amp;lt;= 50% of logical core count (Linux/macOS)&lt;/span&gt;
&lt;span class="c"&gt;#      or aggregate load percentage &amp;lt;= 50% (Windows).&lt;/span&gt;
&lt;span class="c"&gt;# Memory: effectively available memory must be &amp;gt;= 2048 MB on all platforms.&lt;/span&gt;
_resources_ok&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;_os&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_os&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
    &lt;/span&gt;Linux&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;_nproc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;nproc &lt;/span&gt;2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'^processor'&lt;/span&gt; /proc/cpuinfo 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;1&lt;span class="si"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;_cpu_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_nproc&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'NR==1 { print ($1 / n &amp;lt;= 0.50) ? "1" : "0" }'&lt;/span&gt; /proc/loadavg 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;_cpu_ok&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[graph hook] Skipping rebuild — CPU load above 50% threshold"&lt;/span&gt;
        &lt;span class="k"&gt;return &lt;/span&gt;1
      &lt;span class="k"&gt;fi
      &lt;/span&gt;&lt;span class="nv"&gt;_mem_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^MemAvailable:/ { print ($2 / 1024 &amp;gt;= 2048) ? "1" : "0" }'&lt;/span&gt; /proc/meminfo 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;_mem_ok&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[graph hook] Skipping rebuild — available memory below 2 GB threshold"&lt;/span&gt;
        &lt;span class="k"&gt;return &lt;/span&gt;1
      &lt;span class="k"&gt;fi&lt;/span&gt;
      &lt;span class="p"&gt;;;&lt;/span&gt;
    Darwin&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;_nproc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;sysctl &lt;span class="nt"&gt;-n&lt;/span&gt; hw.logicalcpu 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;1&lt;span class="si"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;_cpu_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;sysctl &lt;span class="nt"&gt;-n&lt;/span&gt; vm.loadavg 2&amp;gt;/dev/null | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_nproc&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s1"&gt;'{ gsub(/[{}]/, ""); print ($1 / n &amp;lt;= 0.50) ? "1" : "0" }'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;_cpu_ok&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[graph hook] Skipping rebuild — CPU load above 50% threshold"&lt;/span&gt;
        &lt;span class="k"&gt;return &lt;/span&gt;1
      &lt;span class="k"&gt;fi
      &lt;/span&gt;&lt;span class="nv"&gt;_page_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;sysctl &lt;span class="nt"&gt;-n&lt;/span&gt; hw.pagesize 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;4096&lt;span class="si"&gt;)&lt;/span&gt;
      &lt;span class="nv"&gt;_mem_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;vm_stat 2&amp;gt;/dev/null | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;ps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_page_size&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'
        /Pages free:/        { gsub(/\./, "", $3); free = $3 + 0 }
        /Pages inactive:/    { gsub(/\./, "", $3); inactive = $3 + 0 }
        /Pages speculative:/ { gsub(/\./, "", $3); spec = $3 + 0 }
        END { print ((free + inactive + spec) * ps / 1048576 &amp;gt;= 2048) ? "1" : "0" }
      '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;_mem_ok&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[graph hook] Skipping rebuild — available memory below 2 GB threshold"&lt;/span&gt;
        &lt;span class="k"&gt;return &lt;/span&gt;1
      &lt;span class="k"&gt;fi&lt;/span&gt;
      &lt;span class="p"&gt;;;&lt;/span&gt;
    MINGW&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;MSYS&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;CYGWIN&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; powershell.exe &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
        &lt;/span&gt;&lt;span class="nv"&gt;_cpu_pct&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;powershell.exe &lt;span class="nt"&gt;-NoProfile&lt;/span&gt; &lt;span class="nt"&gt;-Command&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
          &lt;span class="s2"&gt;"(Get-CimInstance Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
          2&amp;gt;/dev/null | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\r\n '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
        &lt;span class="nv"&gt;_cpu_over&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;_cpu_pct&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{ print ($1 &amp;gt; 50) ? "1" : "0" }'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_cpu_over&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
          &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[graph hook] Skipping rebuild — CPU load above 50% threshold"&lt;/span&gt;
          &lt;span class="k"&gt;return &lt;/span&gt;1
        &lt;span class="k"&gt;fi
        &lt;/span&gt;&lt;span class="nv"&gt;_mem_mb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;powershell.exe &lt;span class="nt"&gt;-NoProfile&lt;/span&gt; &lt;span class="nt"&gt;-Command&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
          &lt;span class="s2"&gt;"[math]::Round((Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory / 1024)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
          2&amp;gt;/dev/null | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\r\n '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
        &lt;span class="nv"&gt;_mem_low&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;_mem_mb&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;9999&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{ print ($1 &amp;lt; 2048) ? "1" : "0" }'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_mem_low&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
          &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[graph hook] Skipping rebuild — available memory below 2 GB threshold"&lt;/span&gt;
          &lt;span class="k"&gt;return &lt;/span&gt;1
        &lt;span class="k"&gt;fi
      fi&lt;/span&gt;
      &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;esac&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;0
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Skip during rebase/merge/cherry-pick&lt;/span&gt;
&lt;span class="nv"&gt;GIT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--git-dir&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GIT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/rebase-merge"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GIT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/rebase-apply"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GIT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/MERGE_HEAD"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GIT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/CHERRY_PICK_HEAD"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0

&lt;span class="c"&gt;# ── graphify: rebuild graph after commit ─────────────────────────────────────&lt;/span&gt;
&lt;span class="c"&gt;# Entire block skips if graphify is not installed.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; graphify &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;CHANGED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; HEAD~1 HEAD 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; HEAD 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;GRAPHIFY_BIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; graphify&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAPHIFY_BIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;*&lt;/span&gt;.exe&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;_SHEBANG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
      &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="nv"&gt;_SHEBANG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAPHIFY_BIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^#![[:space:]]*//'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;esac&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_SHEBANG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;*&lt;/span&gt;/env&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;GRAPHIFY_PYTHON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;_SHEBANG&lt;/span&gt;&lt;span class="p"&gt;#*/env &lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
      &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="nv"&gt;GRAPHIFY_PYTHON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_SHEBANG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;esac&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAPHIFY_PYTHON&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
      &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;[!&lt;/span&gt;a-zA-Z0-9/_.@-]&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;GRAPHIFY_PYTHON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
    &lt;span class="k"&gt;esac&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAPHIFY_PYTHON&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAPHIFY_PYTHON&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import graphify"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nv"&gt;GRAPHIFY_PYTHON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
      &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; python3 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import graphify"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;GRAPHIFY_PYTHON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"python3"&lt;/span&gt;
      &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAPHIFY_PYTHON&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; python &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import graphify"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;GRAPHIFY_PYTHON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"python"&lt;/span&gt;
    &lt;span class="k"&gt;fi
    if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAPHIFY_PYTHON&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; pgrep &lt;span class="nt"&gt;-qf&lt;/span&gt; &lt;span class="s1"&gt;'graphify'&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; _resources_ok&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GRAPHIFY_CHANGED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="nv"&gt;_LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.cache/graphify-rebuild.log"&lt;/span&gt;
      &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[graphify hook] launching background rebuild (log: &lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
      &lt;span class="nb"&gt;nohup timeout &lt;/span&gt;300 &lt;span class="nv"&gt;$GRAPHIFY_PYTHON&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
import os, sys
from pathlib import Path
changed_raw = os.environ.get('GRAPHIFY_CHANGED', '')
changed = [Path(f.strip()) for f in changed_raw.strip().splitlines() if f.strip()]
if not changed:
    sys.exit(0)
print(f'[graphify hook] {len(changed)} file(s) changed - rebuilding graph...')
try:
    from graphify.watch import _rebuild_code
    _force = os.environ.get('GRAPHIFY_FORCE', '').lower() in ('1', 'true', 'yes')
    _rebuild_code(Path('.'), force=_force)
except Exception as exc:
    print(f'[graphify hook] Rebuild failed: {exc}')
    sys.exit(1)
"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;lt; /dev/null &amp;amp;
      &lt;span class="nb"&gt;disown &lt;/span&gt;2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
    &lt;/span&gt;&lt;span class="k"&gt;fi
  fi
fi&lt;/span&gt;

&lt;span class="c"&gt;# ── code-review-graph: incrementally update MCP graph index after commit ────&lt;/span&gt;
&lt;span class="c"&gt;# Uses `update --skip-flows` (incremental, sub-second) rather than `build`&lt;/span&gt;
&lt;span class="c"&gt;# (full rebuild — minutes on large repos).&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; code-review-graph &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; .code-review-graph &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; pgrep &lt;span class="nt"&gt;-qf&lt;/span&gt; &lt;span class="s1"&gt;'code-review-graph update'&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; _resources_ok&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;_LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.cache/code-review-graph-update.log"&lt;/span&gt;
  &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[code-review-graph] Commit detected - launching background update (log: &lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
  &lt;span class="nb"&gt;nohup &lt;/span&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'code-review-graph update --skip-flows &amp;amp;&amp;amp; code-review-graph embed'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;lt; /dev/null &amp;amp;
  &lt;span class="nb"&gt;disown &lt;/span&gt;2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Husky project — create &lt;code&gt;.husky/post-commit&lt;/code&gt;&lt;/strong&gt; (Husky v9+ runs &lt;code&gt;.husky/&amp;lt;hookname&amp;gt;&lt;/code&gt; directly; no &lt;code&gt;.husky/_/&lt;/code&gt; shim editing needed). The content is identical to the plain git version above — copy the entire script. Always prefer &lt;code&gt;update --skip-flows&lt;/code&gt; (incremental, sub-second) over &lt;code&gt;build&lt;/code&gt; (full rebuild — minutes on large repos):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .husky/post-commit — same content as .git/hooks/post-commit above&lt;/span&gt;
&lt;span class="c"&gt;# (copy the full script from the plain git example)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify both graphs caught up to the latest commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;%ci                          &lt;span class="c"&gt;# last commit timestamp&lt;/span&gt;
&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; %Sm graphify-out/graph.json              &lt;span class="c"&gt;# macOS&lt;/span&gt;
&lt;span class="nb"&gt;stat&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; %Sm .code-review-graph/graph.db          &lt;span class="c"&gt;# macOS&lt;/span&gt;
&lt;span class="c"&gt;# stat -c %y graphify-out/graph.json             # Linux equivalent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both file mtimes should be at or after the commit timestamp. If &lt;code&gt;graph.db&lt;/code&gt; lags, the post-commit hook isn't firing — check &lt;code&gt;chmod +x&lt;/code&gt; on the hook file and that &lt;code&gt;pnpm install&lt;/code&gt; ran (so Husky's &lt;code&gt;prepare&lt;/code&gt; script wired up &lt;code&gt;.husky/&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Agent Config: trigger-list pattern (&lt;code&gt;CLAUDE.md&lt;/code&gt; / &lt;code&gt;AGENTS.md&lt;/code&gt; / &lt;code&gt;GEMINI.md&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Rather than pasting the full tool docs into your agent config file (which bloats every session's context), create a dedicated &lt;code&gt;docs/agent/knowledge-graph.md&lt;/code&gt; and add a trigger-list pointer. This works the same way across agents — Claude Code reads &lt;code&gt;CLAUDE.md&lt;/code&gt;, Gemini CLI reads &lt;code&gt;GEMINI.md&lt;/code&gt;, Codex and OpenAI agents read &lt;code&gt;AGENTS.md&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Knowledge Graph&lt;/span&gt;

&lt;span class="gs"&gt;**Read `docs/agent/knowledge-graph.md` whenever you:**&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Answer any architecture, cross-module, or "how does X work" question
&lt;span class="p"&gt;-&lt;/span&gt; Plan to grep, find, or glob through the codebase
&lt;span class="p"&gt;-&lt;/span&gt; Need to understand an unfamiliar module or trace a call chain
&lt;span class="p"&gt;-&lt;/span&gt; Are about to refactor or change something with unclear blast radius

The doc covers graphify (community detection, path tracing, GRAPH_REPORT.md), code-review-graph (MCP tools, impact analysis), when to use each, and the full auto-update pipeline.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Place this block in &lt;code&gt;CLAUDE.md&lt;/code&gt; for Claude Code, &lt;code&gt;GEMINI.md&lt;/code&gt; for Gemini CLI, or &lt;code&gt;AGENTS.md&lt;/code&gt; for Codex/OpenAI-compatible agents — the trigger-list pattern works the same across all of them. A vague "read before exploring unfamiliar code" pointer is easy for any agent to skip. Explicit trigger conditions — especially "plan to grep" — activate the graph habit reliably. The full reference lives in &lt;code&gt;docs/agent/knowledge-graph.md&lt;/code&gt; and is only loaded when actually needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enforcing Graph-First: The Smart Grep Hook
&lt;/h3&gt;

&lt;p&gt;The trigger-list in &lt;code&gt;CLAUDE.md&lt;/code&gt; is a soft nudge — in practice, agents still reach for grep by default even with both knowledge graphs initialized and the pointer in place. Soft warnings alone do not reliably change behaviour.&lt;/p&gt;

&lt;p&gt;The fix is a smarter &lt;code&gt;PreToolUse&lt;/code&gt; hook that doesn't just warn — it &lt;strong&gt;answers the question inside the rejection&lt;/strong&gt;, and handles every scenario gracefully: no graph, cross-repo grep, multi-repo registries.&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.amazonaws.com%2Fuploads%2Farticles%2Flee5t5fzr8d6z5pnhvf6.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.amazonaws.com%2Fuploads%2Farticles%2Flee5t5fzr8d6z5pnhvf6.png" alt="Smart Grep Hook Decision Ladder" width="800" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision ladder (first match wins, zero false positives):&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;Situation&lt;/th&gt;
&lt;th&gt;Hook behaviour&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;No graph in this repo, no registered siblings&lt;/td&gt;
&lt;td&gt;Silent pass — zero noise&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Non-code target (&lt;code&gt;.md&lt;/code&gt;, &lt;code&gt;.json&lt;/code&gt;, &lt;code&gt;.log&lt;/code&gt;, &lt;code&gt;.jsonl&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Silent pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;--graph-tried&lt;/code&gt; flag in command&lt;/td&gt;
&lt;td&gt;Silent pass — explicit override&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grep targets abs path outside CWD + that dir has graph&lt;/td&gt;
&lt;td&gt;Query that graph, show result as context, allow grep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grep targets abs path outside CWD + no graph found&lt;/td&gt;
&lt;td&gt;Silent pass — grep is the right tool here&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No local graph + registered sibling repo matches pattern&lt;/td&gt;
&lt;td&gt;Show sibling result as context, allow grep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First grep this session + local graph hit&lt;/td&gt;
&lt;td&gt;Show answer &lt;strong&gt;and&lt;/strong&gt; allow grep (one-shot lesson)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;First grep this session + no graph hit&lt;/td&gt;
&lt;td&gt;Allow grep, suggest MCP tool for next time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subsequent grep + local graph hit&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Deny with answer inline&lt;/strong&gt; — zero retry loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subsequent grep + no graph hit&lt;/td&gt;
&lt;td&gt;Silent pass&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key insight: &lt;strong&gt;the deny message IS the answer&lt;/strong&gt;. The costly retry loop (deny → model reads error → model tries graph → gets answer = 2 API calls) collapses to 1.&lt;/p&gt;

&lt;p&gt;Three graph sources, consulted in priority order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Local graph&lt;/strong&gt; — &lt;code&gt;.code-review-graph/graph.db&lt;/code&gt; in current working directory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-repo graph&lt;/strong&gt; — walk up from any absolute path in the command to find the nearest &lt;code&gt;.code-review-graph/&lt;/code&gt; directory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-repo registry&lt;/strong&gt; — &lt;code&gt;code-review-graph repos&lt;/code&gt; for globally registered repos&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All lookups use the &lt;code&gt;nodes_fts&lt;/code&gt; FTS5 full-text index — sub-millisecond, no external process, no network. Falls back to a &lt;code&gt;LIKE&lt;/code&gt; scan if FTS returns empty.&lt;/p&gt;

&lt;p&gt;Save &lt;code&gt;~/.claude/scripts/smart-grep-hook.sh&lt;/code&gt;:&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;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="c"&gt;# ~/.claude/scripts/smart-grep-hook.sh&lt;/span&gt;
&lt;span class="c"&gt;# Adaptive graph-first grep interceptor — CRG (SQLite FTS5) + graphify (JSON)&lt;/span&gt;
&lt;span class="c"&gt;# Decision ladder (first match wins):&lt;/span&gt;
&lt;span class="c"&gt;#   1. Not a search command              → pass silently&lt;/span&gt;
&lt;span class="c"&gt;#   2. --graph-tried override            → pass silently&lt;/span&gt;
&lt;span class="c"&gt;#   3. Non-code target (.md/.json/…)    → pass silently&lt;/span&gt;
&lt;span class="c"&gt;#   4. Cross-repo abs path:&lt;/span&gt;
&lt;span class="c"&gt;#      a. Target has CRG or graphify     → query both, show context, allow grep&lt;/span&gt;
&lt;span class="c"&gt;#      b. No graph in target             → pass silently&lt;/span&gt;
&lt;span class="c"&gt;#   5. No local graph of either type:&lt;/span&gt;
&lt;span class="c"&gt;#      a. CRG registered repo match     → show context, allow grep&lt;/span&gt;
&lt;span class="c"&gt;#      b. Global merged graphify match  → show context, allow grep&lt;/span&gt;
&lt;span class="c"&gt;#      c. No match                      → pass silently&lt;/span&gt;
&lt;span class="c"&gt;#   6. Local graph(s) present — session-aware gating:&lt;/span&gt;
&lt;span class="c"&gt;#      a. First grep + hit              → show result, allow (one-shot lesson)&lt;/span&gt;
&lt;span class="c"&gt;#      b. First grep + miss             → allow, suggest tool for next time&lt;/span&gt;
&lt;span class="c"&gt;#      c. Subsequent + hit              → answering deny (result inline, no retry)&lt;/span&gt;
&lt;span class="c"&gt;#      d. Subsequent + miss             → pass silently&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-uo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;INPUT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;CMD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$INPUT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',d).get('command',''))"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# ── Tier 1-3: fast exits ──────────────────────────────────────────────────────&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CMD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;" rg "&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;"   rg "&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;ripgrep&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;" fd "&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;" ack "&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;" ag "&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;find&lt;span class="se"&gt;\ &lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CMD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="nt"&gt;--graph-tried&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;"# graph-checked"&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;"GRAPH_TRIED=1"&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;esac&lt;/span&gt;
&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CMD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;.md&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.json&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.yml&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.yaml&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.log&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.jsonl&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.txt&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.csv&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="k"&gt;*&lt;/span&gt;node_modules&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="s2"&gt;"/.git/"&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;/dist/&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;/build/&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;/.next/&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;/__pycache__/&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="p"&gt;;;&lt;/span&gt; &lt;span class="k"&gt;esac&lt;/span&gt;

json_esc&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import json,sys; print(json.dumps(sys.stdin.read())[1:-1])"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# ── CRG: SQLite FTS5 query (sub-ms) ──────────────────────────────────────────&lt;/span&gt;
query_crg&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;pat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  python3 - &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pat&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$db&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PYEOF&lt;/span&gt;&lt;span class="sh"&gt;' 2&amp;gt;/dev/null
import sqlite3, sys, os
pat, db = sys.argv[1], sys.argv[2]
if not os.path.exists(db): sys.exit(0)
try:
    c = sqlite3.connect(f"file:{db}?mode=ro", uri=True, timeout=3)
    rows = c.execute(
        "SELECT n.kind, n.name, n.file_path, n.line_start "
        "FROM nodes_fts f JOIN nodes n ON n.id=f.rowid WHERE nodes_fts MATCH ? LIMIT 5",
        (pat,)).fetchall()
    if not rows:
        rows = c.execute(
            "SELECT kind, name, file_path, line_start FROM nodes WHERE name LIKE ? LIMIT 5",
            (f'%{pat}%',)).fetchall()
    c.close()
    for kind, name, path, line in rows:
        print(f'[crg] {kind}  {name}  →  {path}:{line}')
except: pass
&lt;/span&gt;&lt;span class="no"&gt;PYEOF
&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# ── graphify: JSON in-memory search (~5ms for 4k nodes) ──────────────────────&lt;/span&gt;
query_graphify&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;pat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  python3 - &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pat&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PYEOF&lt;/span&gt;&lt;span class="sh"&gt;' 2&amp;gt;/dev/null
import json, sys, os
pat_low, gfile = sys.argv[1].lower(), sys.argv[2]
if not os.path.exists(gfile): sys.exit(0)
try:
    g = json.load(open(gfile))
    results = []
    for n in g.get('nodes', []):
        label = n.get('label', '')
        if pat_low in label.lower() or pat_low in n.get('id', '').lower():
            src = n.get('source_file', '')
            loc = n.get('source_location', '') or n.get('line', '')
            kind = n.get('file_type', 'node')
            community = n.get('community', '')
            results.append(f'[graphify] {kind}  {label}  →  {src}:{loc}  (community {community})')
    for r in results[:5]: print(r)
except: pass
&lt;/span&gt;&lt;span class="no"&gt;PYEOF
&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# ── Query both graphs at a given root ─────────────────────────────────────────&lt;/span&gt;
query_at_root&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;pat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;crg_db&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;root&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.code-review-graph/graph.db"&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;gfy_json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;root&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/graphify-out/graph.json"&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$crg_db&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;   &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; out+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;query_crg &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$crg_db&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pat&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$gfy_json&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; out+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;query_graphify &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$gfy_json&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pat&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'/^[[:space:]]*$/d'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# ── Walk up to nearest repo with any graph ────────────────────────────────────&lt;/span&gt;
find_graph_root&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"/"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;/.code-review-graph/graph.db"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;/graphify-out/graph.json"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;return
    &lt;/span&gt;&lt;span class="nv"&gt;d&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# ── Extract search pattern ────────────────────────────────────────────────────&lt;/span&gt;
&lt;span class="nv"&gt;PATTERN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CMD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
import sys, shlex
cmd = sys.stdin.read().strip()
try: parts = shlex.split(cmd)
except: parts = cmd.split()
bases = {'grep','rg','ripgrep','egrep','fgrep','ag','ack','fd'}
idx = next((i for i,p in enumerate(parts) if p.rsplit('/',1)[-1] in bases), -1)
if idx &amp;lt; 0: sys.exit(0)
for p in parts[idx+1:]:
    if not p.startswith('-') and '/' not in p and len(p) &amp;gt; 2: print(p[:60]); break
"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# ── Tier 4: cross-repo detection ──────────────────────────────────────────────&lt;/span&gt;
&lt;span class="nv"&gt;TARGET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CMD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
import sys, shlex, os
try: parts = shlex.split(sys.stdin.read())
except: parts = sys.stdin.read().split()
cwd = os.getcwd()
for p in parts:
    if p.startswith('/') and not p.startswith('-') and not p.startswith(cwd): print(p); break
"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;ROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find_graph_root &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TARGET&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATTERN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;RESULT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;query_at_root &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATTERN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Cross-repo graph hit (&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ROOT&lt;/span&gt;&lt;span class="p"&gt;##*/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) for '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PATTERN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;':&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RESULT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Grep proceeding — result shown as context. Open a session in that repo for deeper analysis."&lt;/span&gt;
      &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"%s"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | json_esc&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;fi
  fi
  &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0  &lt;span class="c"&gt;# always allow cross-repo grep&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# ── Local graph detection ─────────────────────────────────────────────────────&lt;/span&gt;
&lt;span class="nv"&gt;HAVE_CRG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;HAVE_GFY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; .code-review-graph/graph.db &lt;span class="o"&gt;]&lt;/span&gt;   &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;HAVE_CRG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; graphify-out/graph.json &lt;span class="o"&gt;]&lt;/span&gt;       &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;HAVE_GFY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1

&lt;span class="c"&gt;# ── Tier 5: no local graph → registry + global merged graph ───────────────────&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HAVE_CRG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HAVE_GFY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;EXTRA_RESULT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;

  &lt;span class="c"&gt;# 5a: CRG multi-repo registry&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; code-review-graph &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATTERN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;PATTERN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 2 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;REG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;code-review-graph repos 2&amp;gt;/dev/null | python3 - &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATTERN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PYEOF&lt;/span&gt;&lt;span class="sh"&gt;' 2&amp;gt;/dev/null
import sys, os, sqlite3
pat = sys.argv[1]; results = []
for line in sys.stdin.read().strip().splitlines():
    for token in line.split():
        if not token.startswith('/'): continue
        db = f"{token}/.code-review-graph/graph.db"
        if not os.path.exists(db): continue
        try:
            c = sqlite3.connect(f'file:{db}?mode=ro', uri=True, timeout=2)
            rows = c.execute(
                'SELECT n.kind, n.name, n.file_path, n.line_start '
                'FROM nodes_fts f JOIN nodes n ON n.id=f.rowid WHERE nodes_fts MATCH ? LIMIT 3',
                (pat,)).fetchall()
            c.close()
            for kind, name, fp, ln in rows:
                results.append(f'[crg:{token}] {kind}  {name}  →  {fp}:{ln}')
        except: pass
for r in results[:5]: print(r)
&lt;/span&gt;&lt;span class="no"&gt;PYEOF
&lt;/span&gt;    &lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; EXTRA_RESULT+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$REG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;

  &lt;span class="c"&gt;# 5b: global merged graphify (~/obsidian-vault/merged-graph.json)&lt;/span&gt;
  &lt;span class="nv"&gt;GLOBAL_GFY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/obsidian-vault/merged-graph.json"&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GLOBAL_GFY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATTERN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;PATTERN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 2 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;GRESULT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;query_graphify &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GLOBAL_GFY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATTERN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRESULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; EXTRA_RESULT+&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRESULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s1"&gt;$'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;
  &lt;span class="k"&gt;fi

  if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXTRA_RESULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;CLEAN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$EXTRA_RESULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'/^[[:space:]]*$/d'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"No local graph. Sibling/global graph hit for '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PATTERN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;':&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CLEAN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Grep proceeding. Open a session in the repo shown above for deeper analysis."&lt;/span&gt;
    &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"%s"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | json_esc&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# ── Tier 6: local graph(s) — session-aware gating ────────────────────────────&lt;/span&gt;
&lt;span class="nv"&gt;KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PWD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | md5 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PWD&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;md5sum &lt;/span&gt;2&amp;gt;/dev/null | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-c1-8&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.cache/claude-graph-hook"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DIR&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;SLOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/first-grep-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d_%H&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nv"&gt;RESULT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATTERN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${#&lt;/span&gt;&lt;span class="nv"&gt;PATTERN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 2 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;RESULT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;query_at_root &lt;span class="s2"&gt;"."&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATTERN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Adaptive tool hint — shows MCP tool (CRG) or graphify CLI or both&lt;/span&gt;
&lt;span class="nv"&gt;TOOL_HINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HAVE_CRG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;TOOL_HINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"semantic_search_nodes_tool(query='&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PATTERN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;')"&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HAVE_GFY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;GFY_HINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"graphify query '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PATTERN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' --graph graphify-out/graph.json"&lt;/span&gt;
  &lt;span class="nv"&gt;TOOL_HINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOOL_HINT&lt;/span&gt;:+&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOOL_HINT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="p"&gt; or &lt;/span&gt;&lt;span class="k"&gt;}${&lt;/span&gt;&lt;span class="nv"&gt;GFY_HINT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SLOT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;touch&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SLOT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Graph pre-answer for '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PATTERN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;':&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RESULT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;If enough — skip the grep. Running this time (one-shot). Future code-path greps denied when graph answers. Override: --graph-tried."&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"No graph hit for '&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PATTERN&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' — grep proceeding (one-shot). Next time try: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOOL_HINT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Append --graph-tried to bypass permanently."&lt;/span&gt;
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'{"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"%s"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | json_esc&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# Subsequent greps: answering deny if graph hits, else pass&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RESULT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Graph has this — no retry needed:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RESULT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Use: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TOOL_HINT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;. Append --graph-tried to override."&lt;/span&gt;
  &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"block","permissionDecisionReason":"%s"}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | json_esc&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="k"&gt;fi

&lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'[graph-hook] no result for "%s"\n'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PATTERN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DIR&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/bypass.log"&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it in &lt;code&gt;settings.example.json&lt;/code&gt; (then copy to &lt;code&gt;settings.local.json&lt;/code&gt; or &lt;code&gt;~/.claude/settings.json&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bash ~/.claude/scripts/smart-grep-hook.sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pair with a concrete SessionStart cheatsheet&lt;/strong&gt; — injects a query→tool map once per session (~150 tokens, prevents most reflexive greps before they happen). Fires only when a graph exists in the current project, silent otherwise:&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;"SessionStart"&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;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"if [ -f .code-review-graph/graph.db ] || [ -f graphify-out/graph.json ]; then STATS=''; TOOL_LINES=''; if [ -f .code-review-graph/graph.db ]; then STATS=$(python3 -c &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;import sqlite3; c=sqlite3.connect('.code-review-graph/graph.db'); n=c.execute('SELECT COUNT(*) FROM nodes').fetchone()[0]; e=c.execute('SELECT COUNT(*) FROM edges').fetchone()[0]; print(f'{n} nodes, {e} edges'); c.close()&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; 2&amp;gt;/dev/null || echo ''); TOOL_LINES=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;  where is X defined    → semantic_search_nodes_tool(query=X)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  who calls X           → query_graph_tool(pattern=callers_of, target=X)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  pre-refactor blast    → get_impact_radius_tool(changed_files=[...])&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  community/cluster     → list_communities_tool()&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  code review context   → get_review_context_tool(changed_files=[...])&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; fi; if [ -f graphify-out/graph.json ]; then GFY_STATS=$(python3 -c &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;import json; g=json.load(open('graphify-out/graph.json')); nodes=g.get('nodes',[]); comms=len(set(n.get('community','') for n in nodes if n.get('community',''))); print(f'{len(nodes)} nodes, {comms} communities')&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; 2&amp;gt;/dev/null || echo ''); [ -n &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$GFY_STATS&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; ] &amp;amp;&amp;amp; STATS=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;${STATS:+$STATS | }graphify: $GFY_STATS&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; TOOL_LINES=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;${TOOL_LINES:+$TOOL_LINES&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n}  CRG miss / explore    → graphify query '&amp;lt;term&amp;gt;' --graph graphify-out/graph.json&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  path A→B              → graphify path '&amp;lt;from&amp;gt;' '&amp;lt;to&amp;gt;' --graph graphify-out/graph.json&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; fi; printf '{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;hookSpecificOutput&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;hookEventName&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;SessionStart&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;additionalContext&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;GRAPH QUERY CHEATSHEET (%s) — use BEFORE Read/Grep/Bash-find on code:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n%s&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;nSkip graph for: .md .json .yml .log .jsonl configs cross-repo paths.&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;nOverride grep gate: append --graph-tried to any Bash command.&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}}' &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$STATS&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$TOOL_LINES&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; fi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;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;&lt;strong&gt;Why &lt;code&gt;--graph-tried&lt;/code&gt; beats a hard block:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;13 characters. The agent adds it when it has already consulted the graph or when the query targets content the AST graph cannot index (string literals, config values, log text, content in another language not indexed). The hook detects the flag and passes silently — legitimate grep is never more than one retry away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-repo and no-graph: always allow, never penalise:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The hook is non-blocking in every case where the graph can't definitively help. Cross-repo greps get graph context shown as additional info, not a gate. Repos without any graph see zero hook overhead. The only hard deny is when the local graph has the exact answer — and even then, &lt;code&gt;--graph-tried&lt;/code&gt; is the immediate escape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session state (hourly bucket per repo):&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;~/.cache/claude-graph-hook/
├── first-grep-&amp;lt;repo-hash&amp;gt;-&amp;lt;YYYYMMDDHH&amp;gt;   ← one-shot allowance marker
└── bypass.log                             ← audit: greps that passed with no graph answer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The allowance resets hourly. The &lt;code&gt;bypass.log&lt;/code&gt; is an audit trail — recurring patterns signal queries worth adding to a custom graph index or documentation.&lt;/p&gt;




&lt;h3&gt;
  
  
  Real Token Cost Comparison
&lt;/h3&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.amazonaws.com%2Fuploads%2Farticles%2Fju2gv2n98a8gq7eiol4k.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.amazonaws.com%2Fuploads%2Farticles%2Fju2gv2n98a8gq7eiol4k.png" alt="Token Cost Per Lookup: grep vs graphify vs CRG" width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All numbers measured live on a 1336-node TypeScript monorepo, embeddings enabled (&lt;code&gt;code-review-graph embed&lt;/code&gt; run once, then incremental after each edit).&lt;/p&gt;

&lt;h4&gt;
  
  
  Symbol and concept lookups
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Query&lt;/th&gt;
&lt;th&gt;grep&lt;/th&gt;
&lt;th&gt;graphify query&lt;/th&gt;
&lt;th&gt;CRG keyword&lt;/th&gt;
&lt;th&gt;CRG semantic&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Where is &lt;code&gt;createThread&lt;/code&gt;?"&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;594 tokens&lt;/strong&gt; (2374 chars, 24 lines)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1532 tokens&lt;/strong&gt; (165 nodes, BFS depth=2)&lt;/td&gt;
&lt;td&gt;115 tokens&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;115 tokens&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"uiState persistence layer"&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1069 tokens&lt;/strong&gt; (4274 chars, 43 lines)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1552 tokens&lt;/strong&gt; (87 nodes)&lt;/td&gt;
&lt;td&gt;miss&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100 tokens&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"comment submit handler"&lt;/td&gt;
&lt;td&gt;114 tokens (narrow query)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1538 tokens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;miss&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100 tokens&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"what imports api.ts?"&lt;/td&gt;
&lt;td&gt;437 tokens (filenames only)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1563 tokens&lt;/strong&gt; (378 nodes)&lt;/td&gt;
&lt;td&gt;125 tokens (41 results)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;125 tokens&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"who calls &lt;code&gt;createThread&lt;/code&gt;?"&lt;/td&gt;
&lt;td&gt;~1109 tokens + manual filter&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1532 tokens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;80 tokens&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80 tokens&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"error handling in server"&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;2220 chars&lt;/strong&gt; (noise: auto-generated .react-router types)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1538 tokens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;477 chars (source only)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;477 chars&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Hidden grep cost:&lt;/strong&gt; after a grep hit the model usually reads the matched file. &lt;code&gt;comment-form.tsx&lt;/code&gt; = 909 tokens. &lt;code&gt;comment-thread.tsx&lt;/code&gt; = 1397 tokens. A typical grep + follow-up read costs &lt;strong&gt;1300–2500 tokens per question&lt;/strong&gt;. CRG returns &lt;code&gt;file:line&lt;/code&gt; — no follow-up read needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signal quality gap:&lt;/strong&gt; grep returns every file containing the pattern — including &lt;code&gt;dist/&lt;/code&gt;, generated &lt;code&gt;.react-router/types/&lt;/code&gt;, auto-compiled &lt;code&gt;.d.ts&lt;/code&gt;, and &lt;code&gt;node_modules&lt;/code&gt; leakage. CRG indexes only what Tree-sitter parses as source. For the "error handling" query: grep returned 20 lines from auto-generated type files before any real source. CRG returned 5 precise hits: &lt;code&gt;sendError&lt;/code&gt; and &lt;code&gt;withJsonBody&lt;/code&gt; in &lt;code&gt;http-utils.ts&lt;/code&gt;, &lt;code&gt;ErrorBoundary&lt;/code&gt; in &lt;code&gt;root.tsx&lt;/code&gt; and &lt;code&gt;diff.tsx&lt;/code&gt;. Zero noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;graphify query — right tool, wrong use case:&lt;/strong&gt; BFS depth=2 returns 87–378 nodes (~1500 tokens) regardless of query specificity. For &lt;em&gt;symbol lookup&lt;/em&gt; ("where is createThread?"), this is 15× more expensive than CRG semantic. For &lt;em&gt;neighborhood exploration&lt;/em&gt; ("what connects to this module when I don't know the codebase?"), the BFS output is exactly what you want — a visual map of relationships. The smart-grep hook intercepts it for symbol queries and routes through CRG; if CRG misses, graphify query proceeds. Use &lt;code&gt;graphify query&lt;/code&gt; deliberately for exploration, not as a default lookup tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why graphify appears less in session logs than you'd expect:&lt;/strong&gt; In a measured multi-hour session on a 1336-node monorepo, graphify had 2 actual code lookups vs CRG's 36 graph tool calls. This is correct behaviour, not underuse. CRG with embeddings covers 90%+ of targeted symbol and concept queries at 100–350 tokens — graphify BFS for the same query returns 1,500 tokens. The session was targeted feature work on a &lt;em&gt;known&lt;/em&gt; codebase; both tools appropriately routed to CRG. Graphify's value surface is: unfamiliar codebase onboarding, cross-community architecture tracing, and path exploration between distant modules — none of which were needed. If the session had started with "I've never seen this codebase, orient me", graphify would have dominated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The CRG-miss → grep fallback gap:&lt;/strong&gt; When &lt;code&gt;semantic_search_nodes_tool&lt;/code&gt; returns 0 results (happened 3× for exact symbol names like &lt;code&gt;validateRef&lt;/code&gt;, &lt;code&gt;parseUnifiedDiff&lt;/code&gt;), the current cheatsheet doesn't tell the agent to try &lt;code&gt;graphify query&lt;/code&gt; next. The agent falls back to grep, which returned 6–29 chars of near-nothing. The correct fallback chain is: &lt;code&gt;CRG semantic (miss) → graphify query '&amp;lt;term&amp;gt;' → grep&lt;/code&gt;. Add this line to your CLAUDE.md trigger list or SessionStart cheatsheet to close the gap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;CRG 0 results → graphify query &lt;span class="s1"&gt;'&amp;lt;term&amp;gt;'&lt;/span&gt; &lt;span class="nt"&gt;--graph&lt;/span&gt; graphify-out/graph.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  What grep cannot do at all
&lt;/h4&gt;

&lt;p&gt;Some questions require multi-hop graph traversal. Grep answers the &lt;em&gt;text search&lt;/em&gt; question; it cannot answer the &lt;em&gt;dependency&lt;/em&gt; question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario: "What breaks if I change &lt;code&gt;server.ts&lt;/code&gt;?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With grep you would need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read &lt;code&gt;server.ts&lt;/code&gt; header to find what it exports (~828 chars)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grep -rn "startServer" packages/&lt;/code&gt; to find 1-hop callers (~715 chars)&lt;/li&gt;
&lt;li&gt;For each caller found, grep again for their importers (~309+ chars)&lt;/li&gt;
&lt;li&gt;Aggregate manually — still missing transitive re-exports and inferred dependencies&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Total: &lt;strong&gt;3 separate operations, 1852+ chars, still incomplete.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;get_impact_radius_tool&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"summary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Blast radius for server.ts:
  - 21 nodes directly changed
  - 500 nodes impacted (within 2 hops)
  - 135 additional files affected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"risk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"high"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"key_entities"&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;"completer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"promptBranch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"fetchDefaultReviewers"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1 call, 274 chars, complete 2-hop picture, risk rating included.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Grep reaches one level of importers in one pass. To get two levels, you need to loop. To get re-exports of re-exports, you need to understand the module graph. &lt;code&gt;get_impact_radius_tool&lt;/code&gt; does all of this from the pre-built AST graph — sub-second.&lt;/p&gt;

&lt;p&gt;This matters most before refactoring: touching a widely-imported file without an impact check risks breaking 135 files silently.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;get_review_context_tool&lt;/code&gt; — best single call for code review
&lt;/h4&gt;

&lt;p&gt;Before any code review, this one call returns: risk level, impacted node count, impacted file count, and test gaps — all in ~111 tokens (session-measured average; range 80–270 depending on depth). No follow-up reads needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;get_review_context_tool&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;changed_files&lt;/span&gt;&lt;span class="o"&gt;=[&lt;/span&gt;&lt;span class="s2"&gt;"packages/git/src/threads.ts"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;, &lt;span class="nv"&gt;depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"minimal"&lt;/span&gt;, &lt;span class="nv"&gt;include_source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

→ Risk: high
→ 359 impacted nodes &lt;span class="k"&gt;in &lt;/span&gt;126 files
→ test_gaps: 15
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare with manually: read file (828 tokens) + grep importers (715 tokens) + read callers to assess risk (1400+ tokens) = &lt;strong&gt;2943 tokens, incomplete&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Add it to your review workflow via the SessionStart cheatsheet — it fires once per session at ~150 tokens total, pointing the agent at this tool first for any PR or diff review.&lt;/p&gt;

&lt;h4&gt;
  
  
  The winner by query type
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────┬────────────────────────────────┬──────────────────────────────┐
│ Query type                     │ Best tool                      │ Est. tokens                  │
├────────────────────────────────┼────────────────────────────────┼──────────────────────────────┤
│ Find function/class X          │ semantic_search_nodes_tool     │ ~100                         │
│ Who calls X?                   │ query_graph_tool(callers_of)   │ ~80                          │
│ What imports file X?           │ query_graph_tool(importers)    │ ~125 (41 results)            │
│ Concept graph traversal        │ traverse_graph_tool(query=X)   │ ~1000 (measured avg, depth=3) │
│ Pre-refactor impact check      │ get_impact_radius_tool         │ ~70, complete 2-hop          │
│ Code review / PR impact        │ get_review_context_tool        │ ~111 (measured avg), risk + test gaps │
│ Module/behavioral clusters     │ list_communities_tool          │ ~170 (12 communities)        │
│ CRG miss / neighborhood explore│ graphify query '&amp;lt;term&amp;gt;'        │ ~1500 (BFS — use sparingly)  │
│ Exact path A→B hop-by-hop      │ graphify path '&amp;lt;A&amp;gt;' '&amp;lt;B&amp;gt;'      │ ~200                         │
│ String/regex in code body      │ grep (# --graph-tried)         │ varies                       │
│ Config/JSON/log values         │ grep — graph can't index these │ varies                       │
└────────────────────────────────┴────────────────────────────────┴──────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Real session measurement: complete token accounting
&lt;/h4&gt;

&lt;p&gt;The per-query numbers above cover tool result sizes. A complete picture must also include the hidden context costs each approach carries: MCP schema overhead, required setup reads, and cache dynamics. These numbers were measured from a live multi-hour coding session on the same 1336-node monorepo (578 total API calls).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hidden context overhead per approach:&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;Approach&lt;/th&gt;
&lt;th&gt;Schema/setup overhead&lt;/th&gt;
&lt;th&gt;Per-call result&lt;/th&gt;
&lt;th&gt;How it's paid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;grep&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0 tokens&lt;/strong&gt; (none)&lt;/td&gt;
&lt;td&gt;~550 tok/call&lt;/td&gt;
&lt;td&gt;per call, direct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;graphify CLI&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0 tokens&lt;/strong&gt; (it's a Bash call)&lt;/td&gt;
&lt;td&gt;~292 tok/call&lt;/td&gt;
&lt;td&gt;per call, direct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ GRAPH_REPORT.md&lt;/td&gt;
&lt;td&gt;+458 tok (partial read, measured)&lt;/td&gt;
&lt;td&gt;one-time&lt;/td&gt;
&lt;td&gt;Read into context once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CRG MCP tools&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~6,000 tok always&lt;/strong&gt; (~25 tool schemas, default) — drops to ~1,800 tok with &lt;code&gt;CRG_TOOLS&lt;/code&gt; allow-list (8 tools)&lt;/td&gt;
&lt;td&gt;~69–1,359/call&lt;/td&gt;
&lt;td&gt;written to cache on cold start, then served from cache at 10× lower cost&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Honest full-session accounting (37 graph tool calls):&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;What&lt;/th&gt;
&lt;th&gt;With CRG&lt;/th&gt;
&lt;th&gt;With grep+Read&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Schema/setup overhead&lt;/td&gt;
&lt;td&gt;~6,000 tok (cached)&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tool result tokens&lt;/td&gt;
&lt;td&gt;14,167 tok (measured)&lt;/td&gt;
&lt;td&gt;110,335 tok (estimated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~20,167 tok&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~110,335 tok&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Net saving&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~90,168 tokens — 5.5× cheaper after schema overhead&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;CRG schema tokens are written to the prompt cache on the first API call of a session (cold start: 59,310 tokens including system prompt + all tool schemas). After that they're served from &lt;code&gt;cache_read&lt;/code&gt; at $0.30/MTok — one-tenth the cost of new input ($3.00/MTok). The &lt;code&gt;get_architecture_overview_tool&lt;/code&gt; block saved 33,000 tokens from a single denied call; that alone exceeds the CRG schema overhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache economics for the full session:&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;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold-start cache_write (call 1)&lt;/td&gt;
&lt;td&gt;59,310 tokens — full context incl. all schemas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total cache_write across session&lt;/td&gt;
&lt;td&gt;3,056,502 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold-start share of writes&lt;/td&gt;
&lt;td&gt;1.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total cache_read&lt;/td&gt;
&lt;td&gt;50,942,267 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New input tokens&lt;/td&gt;
&lt;td&gt;7,694 tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Est. session cost (Sonnet 4.x)&lt;/td&gt;
&lt;td&gt;~$36 (cache_write $11.46 + cache_read $15.28 + output $9.49)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Estimated cost without CRG (same session, grep+Read instead): the additional 90K tool result tokens would each need to be cache-written then read — adding roughly $0.34 in cache_write + $2.70 in cache_read per 100K tokens. More importantly, a larger context window means cache blocks expire and must be re-created more often, compounding costs across 578 calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Three-way comparison including all hidden costs (37 lookup calls, measured session):&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;                      grep          graphify CLI      CRG MCP
─────────────────────────────────────────────────────────────
Schema overhead       0 tok         0 tok             ~6,000 tok
                      (none)        (Bash call,       (~25 tool schemas,
                                     no MCP schema)    always in context)
Setup read            0 tok         +458 tok          0 tok
                                    (GRAPH_REPORT.md,
                                     one partial read,
                                     measured)
Per-call result       ~550 tok      ~292 tok          ~69–1,359 tok
Follow-up Read?       +~1,842 tok   not needed        not needed
                      (usually yes)

37 calls — total cost:
  Tool results        ~20,350 tok   ~10,804 tok       14,167 tok (measured)
  + overhead          0             +458 tok          +6,000 tok
  ─────────────────────────────────────────────────────────────
  TOTAL               ~20,350 tok   ~11,262 tok       ~20,167 tok

vs grep+Read equiv:   110,335 tok   110,335 tok       110,335 tok
Net saving            ~5.4×         ~9.8×             ~5.5×
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;graphify CLI is the most token-efficient per lookup&lt;/strong&gt; when full overhead is counted — zero MCP schema tax, no follow-up Read needed, compact output. Its limitation is output size at BFS scale (87–378 nodes, ~1,500 tokens) and needing GRAPH_REPORT.md for orientation. For targeted symbol lookup CRG wins; for schema-free cost-conscious exploration graphify wins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CRG's 6K schema overhead pays back fast&lt;/strong&gt;: the blocked &lt;code&gt;get_architecture_overview_tool&lt;/code&gt; call alone saved 33,000 tokens — 5× the entire schema overhead — from a single hook denial in this session. After cold-start, schemas cost $0.30/MTok (cache_read) not $3.00/MTok (new input).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Graphify vs CRG split:&lt;/strong&gt; graphify had 2 actual code lookups; CRG had 36 tool calls. This is the right ratio for targeted feature work on a known codebase — CRG semantic search (100–350 tok) dominated because the work was precise. Graphify BFS (~1,500 tok) is for orientation on unfamiliar ground. Same session on an unknown codebase would flip the ratio.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strip Unused CRG Tools (Cuts Schema Overhead 70%)
&lt;/h3&gt;

&lt;p&gt;CRG registers &lt;strong&gt;25 MCP tools by default&lt;/strong&gt; but a typical session uses ~8. Every unused tool still costs always-loaded schema tokens. CRG ships with a built-in allow-list filter — &lt;code&gt;_apply_tool_filter&lt;/code&gt; removes tools at server boot via &lt;code&gt;mcp.local_provider.remove_tool(name)&lt;/code&gt;. Filtered tools cannot be invoked at all, so the explicit PreToolUse block on &lt;code&gt;get_architecture_overview_tool&lt;/code&gt; becomes redundant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set &lt;code&gt;CRG_TOOLS&lt;/code&gt; in &lt;code&gt;~/.claude.json&lt;/code&gt; mcpServers env (or pass &lt;code&gt;serve --tools ...&lt;/code&gt;):&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="nl"&gt;"mcpServers"&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;"code-review-graph"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stdio"&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;"uvx"&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;"--with"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"code-review-graph[embeddings]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"code-review-graph"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"serve"&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;"CRG_TOOLS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"semantic_search_nodes_tool,query_graph_tool,get_impact_radius_tool,traverse_graph_tool,list_communities_tool,get_community_tool,get_review_context_tool,list_graph_stats_tool"&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;&lt;strong&gt;The 8-tool working set covers 95% of session usage:&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;Tool&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;semantic_search_nodes_tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;"where is X defined" — embedding match, ~100–350 tok&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;query_graph_tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;callers/callees/relations — &lt;code&gt;pattern=callers_of&lt;/code&gt;, ~80 tok&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_impact_radius_tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pre-refactor blast radius&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;traverse_graph_tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;dependency walk (keep depth ≤ 3)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_communities_tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;architecture overview, ~170 tok for 12 clusters&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_community_tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cluster drill-down&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_review_context_tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;code review with risk + test gaps, ~111 tok avg&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_graph_stats_tool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sanity check / status&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Dropped (17 tools):&lt;/strong&gt; &lt;code&gt;get_architecture_overview_tool&lt;/code&gt; (33k tok output — never useful), &lt;code&gt;refactor_tool&lt;/code&gt; / &lt;code&gt;apply_refactor_tool&lt;/code&gt; (use git), &lt;code&gt;get_minimal_context_tool&lt;/code&gt;, &lt;code&gt;get_docs_section_tool&lt;/code&gt;, &lt;code&gt;get_wiki_page_tool&lt;/code&gt;, &lt;code&gt;find_large_functions_tool&lt;/code&gt;, &lt;code&gt;list_flows_tool&lt;/code&gt; / &lt;code&gt;get_flow_tool&lt;/code&gt; / &lt;code&gt;get_affected_flows_tool&lt;/code&gt; (flow analysis is rare), &lt;code&gt;get_hub_nodes_tool&lt;/code&gt; / &lt;code&gt;get_bridge_nodes_tool&lt;/code&gt; (covered by communities), &lt;code&gt;get_knowledge_gaps_tool&lt;/code&gt;, &lt;code&gt;get_surprising_connections_tool&lt;/code&gt;, &lt;code&gt;get_suggested_questions_tool&lt;/code&gt;, &lt;code&gt;list_repos_tool&lt;/code&gt; / &lt;code&gt;cross_repo_search_tool&lt;/code&gt; (single-repo work).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token math:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before: ~6,000 tok schema overhead (25 tools, full descriptions + params, always loaded after cold-start)&lt;/li&gt;
&lt;li&gt;After: ~1,800 tok (8 tools)&lt;/li&gt;
&lt;li&gt;Savings: &lt;strong&gt;~4,200 tok per session, ~70% schema reduction&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Bonus: Claude Code's deferred-tool layer further lazy-loads param schemas via &lt;code&gt;ToolSearch&lt;/code&gt; — only the description metadata stays always-loaded, so real overhead is even lower&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lazy loading reality check:&lt;/strong&gt; MCP protocol itself does not support lazy-loading tool schemas — all are listed at startup. Claude Code adds a deferred-tool layer on top: tool &lt;em&gt;descriptions&lt;/em&gt; are exposed for matching, but full param schemas only load when &lt;code&gt;ToolSearch&lt;/code&gt; resolves a tool by name. So stripping at the CRG layer cuts the always-loaded description set; the Claude Code layer cuts the params. They compound.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;By case:&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;Workflow&lt;/th&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Targeted symbol lookup on known codebase&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;semantic_search_nodes_tool&lt;/code&gt; → fallback to &lt;code&gt;query_graph_tool&lt;/code&gt; → fallback to &lt;code&gt;graphify query&lt;/code&gt; → grep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pre-refactor blast&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;get_impact_radius_tool&lt;/code&gt; + &lt;code&gt;traverse_graph_tool(depth=2)&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Architecture orientation&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;list_communities_tool&lt;/code&gt; → &lt;code&gt;get_community_tool&lt;/code&gt; (NEVER &lt;code&gt;get_architecture_overview_tool&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code review on a PR&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;get_review_context_tool(changed_files=[...])&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-repo work&lt;/td&gt;
&lt;td&gt;re-add &lt;code&gt;cross_repo_search_tool&lt;/code&gt;, &lt;code&gt;list_repos_tool&lt;/code&gt; to allow-list&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flow / DAG analysis&lt;/td&gt;
&lt;td&gt;re-add &lt;code&gt;list_flows_tool&lt;/code&gt;, &lt;code&gt;get_flow_tool&lt;/code&gt;, &lt;code&gt;get_affected_flows_tool&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The concurrent session had zero graph tools:&lt;/strong&gt; a parallel session on the same codebase used only grep+Read (15 grep calls, 22 reads). Cold start: 60,610 tokens (same MCP schemas loaded — schemas are always present regardless of whether graph tools are used). Tool result tokens were proportionally higher per lookup, with no structured relationship data returned.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Bypassing the smart-grep hook:&lt;/strong&gt; append &lt;code&gt;# --graph-tried&lt;/code&gt; as a shell &lt;em&gt;comment&lt;/em&gt; (not a flag) to pass silently: &lt;code&gt;grep -rn "TODO" src/ # --graph-tried&lt;/code&gt;. On macOS, ugrep rejects &lt;code&gt;--graph-tried&lt;/code&gt; as an unknown flag — always use it as a comment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Expensive Calls to Avoid
&lt;/h3&gt;

&lt;p&gt;Some graph tool calls look useful but are token-budget traps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;get_architecture_overview_tool&lt;/code&gt;&lt;/strong&gt; — returns 131,000 chars (~33,000 tokens), exceeds any usable context budget. &lt;strong&gt;Best fix: strip it via &lt;code&gt;CRG_TOOLS&lt;/code&gt; allow-list&lt;/strong&gt; (see "Strip Unused CRG Tools" above) so it cannot be called at all. If you can't strip it, block via PreToolUse hook. Alternative: &lt;code&gt;list_communities_tool&lt;/code&gt; first (~170 tokens for 12 clusters), then &lt;code&gt;get_community_tool&lt;/code&gt; for any cluster you want to drill into.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;graphify query '&amp;lt;term&amp;gt;'&lt;/code&gt;&lt;/strong&gt; — BFS depth=2 returns 87–378 nodes (~1500 tokens) regardless of specificity. The smart-grep hook intercepts it: if CRG has a matching symbol it blocks with the CRG result inline (~100 tokens); if CRG misses it allows graphify to proceed but warns about cost. This is the right split — CRG for symbol lookup, graphify for neighborhood exploration when you genuinely want to see what's connected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;traverse_graph_tool&lt;/code&gt; on hub nodes without budget&lt;/strong&gt; — takes a &lt;code&gt;query&lt;/code&gt; string (not source/target nodes), does concept-based traversal, and at depth=5+ on hub nodes (&lt;code&gt;server.ts&lt;/code&gt;, &lt;code&gt;api.ts&lt;/code&gt;) returns thousands of nodes. Keep &lt;code&gt;depth ≤ 3&lt;/code&gt; and &lt;code&gt;token_budget ≤ 1500&lt;/code&gt;. For a specific A→B path, use &lt;code&gt;graphify path '&amp;lt;A&amp;gt;' '&amp;lt;B&amp;gt;'&lt;/code&gt; instead — it returns the shortest hop chain in ~200 tokens. For "who calls X", use &lt;code&gt;query_graph_tool(pattern=callers_of, target=X)&lt;/code&gt; (~80 tokens).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool stripping &amp;gt; hook blocking.&lt;/strong&gt; The cleanest enforcement is the &lt;code&gt;CRG_TOOLS&lt;/code&gt; allow-list shown earlier — stripped tools are removed from the MCP server entirely, so they cannot be invoked and their schemas never enter context. The PreToolUse hook block is only useful if you cannot edit the MCP server env (e.g., shared config).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;graphify query&lt;/code&gt; interception lives inside &lt;code&gt;smart-grep-hook.sh&lt;/code&gt; and requires no extra config — it fires via the existing Bash PreToolUse hook.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making This Work for Any Project
&lt;/h3&gt;

&lt;p&gt;The hooks in &lt;code&gt;settings.example.json&lt;/code&gt; are designed to be portable — they silently no-op when the relevant tool or graph data isn't present. This means you can copy them to &lt;code&gt;settings.local.json&lt;/code&gt; per-project, or into &lt;code&gt;~/.claude/settings.json&lt;/code&gt; to activate across all projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the hooks do (each row is a no-op when the relevant tool/file is missing):&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;Hook&lt;/th&gt;
&lt;th&gt;What fires&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;Resource-safe?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stop&lt;/td&gt;
&lt;td&gt;CRG &lt;code&gt;update + embed&lt;/code&gt; (background, PID-guarded — silent if not installed/initialized)&lt;/td&gt;
&lt;td&gt;Once after AI finishes each turn (all edits batched)&lt;/td&gt;
&lt;td&gt;PID file prevents overlap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PreToolUse&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;smart-grep-hook.sh&lt;/code&gt; (graph-first routing — adaptive: CRG only, graphify only, both, or silent fallback)&lt;/td&gt;
&lt;td&gt;Before every grep/find/graphify query&lt;/td&gt;
&lt;td&gt;Read-only, no resource concern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PreToolUse&lt;/td&gt;
&lt;td&gt;Linter config guard&lt;/td&gt;
&lt;td&gt;Before writing ESLint/Prettier/Biome&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SessionStart&lt;/td&gt;
&lt;td&gt;Graph cheatsheet (adaptive: shows CRG tools, graphify CLI, both, or nothing)&lt;/td&gt;
&lt;td&gt;Session open, only if graph exists&lt;/td&gt;
&lt;td&gt;Read-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SessionStart&lt;/td&gt;
&lt;td&gt;Setup nudge (only when CLI installed but graph not initialized)&lt;/td&gt;
&lt;td&gt;Session open&lt;/td&gt;
&lt;td&gt;Read-only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;What the git hooks do (resource-guarded, all platforms):&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;Hook&lt;/th&gt;
&lt;th&gt;What fires&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;th&gt;Guards&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;post-commit&lt;/td&gt;
&lt;td&gt;graphify rebuild + CRG update + embed (both background nohup)&lt;/td&gt;
&lt;td&gt;Every commit&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;_resources_ok&lt;/code&gt; + &lt;code&gt;pgrep&lt;/code&gt; + &lt;code&gt;timeout 300&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;post-checkout&lt;/td&gt;
&lt;td&gt;graphify rebuild + CRG update/build (smart: ≤5 files incremental, &amp;gt;5 full)&lt;/td&gt;
&lt;td&gt;Branch switch only&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;_resources_ok&lt;/code&gt; + &lt;code&gt;pgrep&lt;/code&gt; + &lt;code&gt;timeout 300&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pre-commit&lt;/td&gt;
&lt;td&gt;Branch protection (blocks direct commits to master/development/release branches)&lt;/td&gt;
&lt;td&gt;Every commit attempt&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;graphify is not in the Stop hook.&lt;/strong&gt; Running &lt;code&gt;graphify update&lt;/code&gt; after every AI turn causes long-running background processes that accumulate, spike CPU, and exhaust swap. graphify update fires only via git hooks (&lt;code&gt;post-commit&lt;/code&gt;, &lt;code&gt;post-checkout&lt;/code&gt;) where it runs detached after the developer's action — invisible latency.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Why no &lt;code&gt;get_architecture_overview_tool&lt;/code&gt; block hook?&lt;/strong&gt; The recommended setup uses the &lt;code&gt;CRG_TOOLS&lt;/code&gt; allow-list to strip that tool entirely (see "Strip Unused CRG Tools"). A stripped tool cannot be invoked, so the explicit block hook becomes redundant. If you cannot or do not want to strip, add the PreToolUse block as a fallback.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Project &lt;code&gt;.claude/settings.json&lt;/code&gt;&lt;/strong&gt; is only for project-specific &lt;em&gt;permissions&lt;/em&gt; — which package-manager commands to allow, which config files to protect. Graph hooks go in &lt;code&gt;settings.example.json&lt;/code&gt; (committed reference) and &lt;code&gt;settings.local.json&lt;/code&gt; (live copy) — or your global &lt;code&gt;~/.claude/settings.json&lt;/code&gt; if you want them everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One-time setup per project — pick what you have installed:&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;&lt;span class="c"&gt;# CRG only (embedding-aware, MCP integration)&lt;/span&gt;
code-review-graph &lt;span class="nb"&gt;install
&lt;/span&gt;code-review-graph embed

&lt;span class="c"&gt;# Graphify only (pure CLI, zero MCP overhead)&lt;/span&gt;
graphify init &lt;span class="nb"&gt;.&lt;/span&gt;
graphify update &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Both — run all four; the smart-grep-hook routes CRG-first, graphify on miss&lt;/span&gt;

&lt;span class="c"&gt;# Git hooks — graphify ONLY updates via git hooks (too slow for Claude hooks)&lt;/span&gt;
code-review-graph hook &lt;span class="nb"&gt;install&lt;/span&gt;   &lt;span class="c"&gt;# post-commit: CRG update + embed (also runs in Stop hook)&lt;/span&gt;
graphify hook &lt;span class="nb"&gt;install&lt;/span&gt;            &lt;span class="c"&gt;# post-commit + post-checkout: graphify update (background nohup)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After setup: every time the AI finishes a turn, the Stop hook fires once — &lt;strong&gt;CRG only&lt;/strong&gt; updates in background (PID-guarded, ~0.425s, no pile-up). Every commit triggers both git hooks. graphify never runs from a Claude hook — only from git hooks. If only one tool is installed, only that one updates. If neither is installed, hooks are silent no-ops.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Verify Your Setup
&lt;/h2&gt;

&lt;p&gt;Run the checks for whichever tools you installed. Skip the rows for tools you did not install — there is no requirement to have both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check CLI installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Either or both:&lt;/span&gt;
graphify &lt;span class="nt"&gt;--help&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-3&lt;/span&gt;            &lt;span class="c"&gt;# graphify only&lt;/span&gt;
code-review-graph &lt;span class="nt"&gt;--version&lt;/span&gt;          &lt;span class="c"&gt;# CRG only&lt;/span&gt;
&lt;span class="c"&gt;# code-review-graph 2.3.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check graph outputs exist
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Graphify (skip if not installed):&lt;/span&gt;
&lt;span class="nb"&gt;ls &lt;/span&gt;graphify-out/
&lt;span class="c"&gt;# cache  graph.html  graph.json  GRAPH_REPORT.md  manifest.json&lt;/span&gt;

&lt;span class="c"&gt;# CRG (skip if not installed):&lt;/span&gt;
code-review-graph status
&lt;span class="c"&gt;# Nodes: 5780  Edges: 30611  Files: 1052&lt;/span&gt;
&lt;span class="c"&gt;# Languages: bash, javascript, vue, typescript&lt;/span&gt;
&lt;span class="c"&gt;# Last updated: 2026-05-05T18:29:51&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check GRAPH_REPORT.md freshness
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-15&lt;/span&gt; graphify-out/GRAPH_REPORT.md
&lt;span class="c"&gt;# ## Corpus Check&lt;/span&gt;
&lt;span class="c"&gt;# - 1020 files · ~1,587,186 words&lt;/span&gt;
&lt;span class="c"&gt;# - Verdict: corpus is large enough that graph structure adds value.&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# ## Summary&lt;/span&gt;
&lt;span class="c"&gt;# - 3815 nodes · 4830 edges · 750 communities (533 shown, 217 thin omitted)&lt;/span&gt;
&lt;span class="c"&gt;# - Extraction: 87% EXTRACTED · 13% INFERRED · 0% AMBIGUOUS&lt;/span&gt;
&lt;span class="c"&gt;#&lt;/span&gt;
&lt;span class="c"&gt;# ## Graph Freshness&lt;/span&gt;
&lt;span class="c"&gt;# - Built from commit: `4968d67a`&lt;/span&gt;
&lt;span class="c"&gt;# - Run `git rev-parse HEAD` and compare to check if the graph is stale.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check Stop hook is in settings.json (or settings.local.json)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool .claude/settings.json | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A5&lt;/span&gt; &lt;span class="s1"&gt;'"Stop"'&lt;/span&gt;
&lt;span class="c"&gt;# Should show the CRG PID-guard command&lt;/span&gt;
&lt;span class="c"&gt;# PostToolUse should be empty ([]) — graph updates moved to Stop hook&lt;/span&gt;

&lt;span class="c"&gt;# Verify PostToolUse is cleared (no graph update commands there):&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool .claude/settings.local.json | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A3&lt;/span&gt; &lt;span class="s1"&gt;'"PostToolUse"'&lt;/span&gt;
&lt;span class="c"&gt;# Should show: "PostToolUse": []&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check MCP server config
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; .mcp.example.json
&lt;span class="c"&gt;# Should show: code-review-graph serve entry&lt;/span&gt;
&lt;span class="c"&gt;# Copy to .mcp.json locally if it doesn't exist yet:&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .mcp.example.json .mcp.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check git hooks
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Husky projects (hooks live in .husky/, not .husky/_/)&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"graphify"&lt;/span&gt; .husky/post-commit
&lt;span class="c"&gt;# 1 (or more)&lt;/span&gt;

&lt;span class="c"&gt;# Plain git projects&lt;/span&gt;
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-5&lt;/span&gt; .git/hooks/pre-commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test incremental update speed
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;time &lt;/span&gt;code-review-graph update &lt;span class="nt"&gt;--skip-flows&lt;/span&gt;
&lt;span class="c"&gt;# real  0m0.425s  ← CRG incremental speed (runs in Stop hook after each AI turn)&lt;/span&gt;

&lt;span class="nb"&gt;time &lt;/span&gt;graphify update &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="c"&gt;# SHA256 cache skips unchanged files — subsequent runs are near-instant&lt;/span&gt;
&lt;span class="c"&gt;# Note: graphify is slower (~10s on large repos) — this is why it moved to Stop&lt;/span&gt;
&lt;span class="c"&gt;# hook (once per turn) rather than PostToolUse (once per file edit)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Test vault sync (if vault is configured)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp &lt;/span&gt;graphify-out/GRAPH_REPORT.md ai-vault/graphify/GRAPH_REPORT.md
&lt;span class="nb"&gt;ls &lt;/span&gt;ai-vault/graphify/
&lt;span class="c"&gt;# GRAPH_REPORT.md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5: Query the Graphs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Graphify CLI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# BFS traversal — find connections between concepts&lt;/span&gt;
graphify query &lt;span class="s2"&gt;"what connects payment to enrollment"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--graph&lt;/span&gt; graphify-out/graph.json &lt;span class="nt"&gt;--budget&lt;/span&gt; 1500

&lt;span class="c"&gt;# Depth-first traversal for flow tracing&lt;/span&gt;
graphify query &lt;span class="s2"&gt;"auth flow"&lt;/span&gt; &lt;span class="nt"&gt;--dfs&lt;/span&gt; &lt;span class="nt"&gt;--graph&lt;/span&gt; graphify-out/graph.json

&lt;span class="c"&gt;# Shortest path between two nodes&lt;/span&gt;
graphify path &lt;span class="s2"&gt;"UserDashboard.vue"&lt;/span&gt; &lt;span class="s2"&gt;"PaymentService"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--graph&lt;/span&gt; graphify-out/graph.json

&lt;span class="c"&gt;# Plain-language explanation of a node&lt;/span&gt;
graphify explain &lt;span class="s2"&gt;"UserDashboard.vue"&lt;/span&gt; &lt;span class="nt"&gt;--graph&lt;/span&gt; graphify-out/graph.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output from &lt;code&gt;graphify explain&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;Node: UserDashboard.vue
  Source:    packages/client/src/views/Dashboard
  Community: 239
  Degree:    3

Connections (3):
  --&amp;gt; UserDashboard()  [imports_from] [EXTRACTED]
  --&amp;gt; handleApiError() [contains]     [EXTRACTED]
  --&amp;gt; AuthService      [calls]        [INFERRED]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Inside Your AI Agent
&lt;/h3&gt;

&lt;p&gt;After &lt;code&gt;graphify install&lt;/code&gt; and &lt;code&gt;code-review-graph install&lt;/code&gt;, your AI agent gets graph tools automatically. For Claude Code, a &lt;code&gt;SessionStart&lt;/code&gt; hook shows the graph status on every session open. Cursor, Gemini CLI, and Windsurf pick up the MCP server from &lt;code&gt;.mcp.json&lt;/code&gt; instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# code-review-graph status on session open:
Nodes: 5780  Edges: 30611  Files: 1052
Languages: bash, javascript, vue, typescript
Last updated: 2026-05-05T18:29:51
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 6: Auto-Update Strategies
&lt;/h2&gt;

&lt;p&gt;The graph is only valuable if it reflects current code. Here are the four strategies, from manual to fully automatic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strategy A: Manual Update (Always Available)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Incremental (only changed files — fast)&lt;/span&gt;
code-review-graph update        &lt;span class="c"&gt;# 0.425s on ~1000-file project&lt;/span&gt;
graphify update &lt;span class="nb"&gt;.&lt;/span&gt;               &lt;span class="c"&gt;# SHA256 cache, AST-only&lt;/span&gt;

&lt;span class="c"&gt;# Full rebuild (when switching branches or major refactors)&lt;/span&gt;
code-review-graph build
graphify update &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--force&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Strategy B: Auto-Update via Git Hooks (Recommended for graphify)
&lt;/h3&gt;

&lt;p&gt;graphify update belongs in git hooks, not in Claude Code hooks. The incremental rebuild takes ~10s+ on a real monorepo — too slow for a per-turn trigger. Git hooks fire at the right moment: after a commit or branch switch, when the developer has already moved on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;graphify hook &lt;span class="nb"&gt;install&lt;/span&gt;   &lt;span class="c"&gt;# installs post-commit + post-checkout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The post-commit hook runs the rebuild &lt;strong&gt;in the background&lt;/strong&gt; (detached &lt;code&gt;nohup&lt;/code&gt; process) so &lt;code&gt;git commit&lt;/code&gt; returns immediately. Rebuild logs go to &lt;code&gt;~/.cache/graphify-rebuild.log&lt;/code&gt;. The post-checkout hook fires on every &lt;code&gt;git checkout &amp;lt;branch&amp;gt;&lt;/code&gt; and rebuilds for the new branch state.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;only&lt;/strong&gt; recommended automatic trigger for &lt;code&gt;graphify update&lt;/code&gt; — do not add it to &lt;code&gt;Stop&lt;/code&gt;, &lt;code&gt;PostToolUse&lt;/code&gt;, or &lt;code&gt;SessionStart&lt;/code&gt; hooks.&lt;/p&gt;

&lt;p&gt;The hooks installed by &lt;code&gt;graphify hook install&lt;/code&gt; — both run as detached &lt;code&gt;nohup&lt;/code&gt; so the git operation returns immediately. &lt;strong&gt;Add the &lt;code&gt;_resources_ok&lt;/code&gt; function&lt;/strong&gt; (shown in the post-commit hook above) to the top of each hook file to prevent resource exhaustion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.git/hooks/post-commit&lt;/code&gt;&lt;/strong&gt; — see the full resource-guarded version in the "Auto-Update on Commit" section above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;.git/hooks/post-checkout&lt;/code&gt;&lt;/strong&gt; (or &lt;code&gt;.husky/post-checkout&lt;/code&gt; for Husky projects):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# Knowledge graph tools are optional — each section silently skips if tools are absent.&lt;/span&gt;

&lt;span class="c"&gt;# _resources_ok function — copy from the post-commit hook above&lt;/span&gt;
&lt;span class="c"&gt;# (CPU ≤ 50% of logical cores, memory ≥ 2 GB free — same implementation)&lt;/span&gt;

&lt;span class="nv"&gt;PREV_HEAD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;NEW_HEAD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;
&lt;span class="nv"&gt;BRANCH_SWITCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$3&lt;/span&gt;

&lt;span class="c"&gt;# Only run on branch switches, not file checkouts&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BRANCH_SWITCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s2"&gt;"1"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0

&lt;span class="c"&gt;# Skip during rebase/merge/cherry-pick&lt;/span&gt;
&lt;span class="nv"&gt;GIT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--git-dir&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GIT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/rebase-merge"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GIT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/rebase-apply"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GIT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/MERGE_HEAD"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GIT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/CHERRY_PICK_HEAD"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0

&lt;span class="c"&gt;# Smart rebuild threshold — shared by graphify and CRG sections below&lt;/span&gt;
&lt;span class="nv"&gt;_ZERO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0000000000000000000000000000000000000000"&lt;/span&gt;
&lt;span class="nv"&gt;_CHANGED_COUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PREV_HEAD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW_HEAD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;_FULL_REBUILD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PREV_HEAD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_ZERO&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;_CHANGED_COUNT&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-gt&lt;/span&gt; 5 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;_FULL_REBUILD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;

&lt;span class="c"&gt;# ── graphify: force-rebuild graph after branch switch ────────────────────────&lt;/span&gt;
&lt;span class="c"&gt;# Entire block skips if graphify is not installed or graph has never been built.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; graphify &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"graphify-out"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="c"&gt;# (Python interpreter detection — same as post-commit hook)&lt;/span&gt;
  &lt;span class="nv"&gt;GRAPHIFY_BIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; graphify&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="c"&gt;# ... shebang detection omitted for brevity — copy from post-commit hook ...&lt;/span&gt;
  &lt;span class="nv"&gt;GRAPHIFY_PYTHON&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"python3"&lt;/span&gt;  &lt;span class="c"&gt;# fallback&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAPHIFY_PYTHON&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; pgrep &lt;span class="nt"&gt;-qf&lt;/span&gt; &lt;span class="s1"&gt;'graphify'&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; _resources_ok&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;_LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.cache/graphify-rebuild.log"&lt;/span&gt;
    &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_FULL_REBUILD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GRAPHIFY_FORCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
    echo&lt;/span&gt; &lt;span class="s2"&gt;"[graphify] Branch switched (&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;_CHANGED_COUNT&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; files diff, full=&lt;/span&gt;&lt;span class="nv"&gt;$_FULL_REBUILD&lt;/span&gt;&lt;span class="s2"&gt;) - launching background rebuild (log: &lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
    &lt;span class="nb"&gt;nohup timeout &lt;/span&gt;300 &lt;span class="nv"&gt;$GRAPHIFY_PYTHON&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
from graphify.watch import _rebuild_code
from pathlib import Path
import os, sys
try:
    _force = os.environ.get('GRAPHIFY_FORCE', '').lower() in ('1', 'true', 'yes')
    _rebuild_code(Path('.'), force=_force)
except Exception as exc:
    print(f'[graphify] Rebuild failed: {exc}')
    sys.exit(1)
"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;lt; /dev/null &amp;amp;
    &lt;span class="nb"&gt;disown &lt;/span&gt;2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
  &lt;/span&gt;&lt;span class="k"&gt;fi
fi&lt;/span&gt;

&lt;span class="c"&gt;# ── code-review-graph: update MCP graph index after branch switch ────────────&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; code-review-graph &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; .code-review-graph &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; pgrep &lt;span class="nt"&gt;-qf&lt;/span&gt; &lt;span class="s1"&gt;'code-review-graph update\|code-review-graph build'&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; _resources_ok&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;_LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.cache/code-review-graph-update.log"&lt;/span&gt;
  &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_FULL_REBUILD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"true"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[code-review-graph] Branch switched (full rebuild: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;_CHANGED_COUNT&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; files diff) - launching background build (log: &lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
    &lt;span class="nb"&gt;nohup &lt;/span&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'code-review-graph build'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;lt; /dev/null &amp;amp;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[code-review-graph] Branch switched (incremental: &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;_CHANGED_COUNT&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; files diff) - launching background update (log: &lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
    &lt;span class="nb"&gt;nohup &lt;/span&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'code-review-graph update --skip-flows &amp;amp;&amp;amp; code-review-graph embed'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;lt; /dev/null &amp;amp;
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;disown &lt;/span&gt;2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key differences from post-commit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only runs on actual branch switches (&lt;code&gt;BRANCH_SWITCH=1&lt;/code&gt; check)&lt;/li&gt;
&lt;li&gt;Smart rebuild: ≤5 files diff → incremental update; &amp;gt;5 files or new branch → full rebuild&lt;/li&gt;
&lt;li&gt;CRG uses &lt;code&gt;build&lt;/code&gt; (full SQLite rebuild) for large diffs, &lt;code&gt;update --skip-flows&lt;/code&gt; for small ones&lt;/li&gt;
&lt;li&gt;graphify sets &lt;code&gt;GRAPHIFY_FORCE=true&lt;/code&gt; for large diffs to force full AST reparse&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Strategy C: Git Hooks (On Commit + Branch Switch)
&lt;/h3&gt;

&lt;p&gt;Both tools install git hooks automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# code-review-graph: pre-commit hook in .git/hooks/pre-commit&lt;/span&gt;
code-review-graph &lt;span class="nb"&gt;install&lt;/span&gt;

&lt;span class="c"&gt;# graphify: installs hooks in .git/hooks/ — for Husky projects, copy the hook logic to .husky/post-commit&lt;/span&gt;
graphify hook &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The graphify post-commit hook runs the rebuild &lt;strong&gt;in the background&lt;/strong&gt; (detached process) so &lt;code&gt;git commit&lt;/code&gt; returns immediately. Rebuild logs go to &lt;code&gt;~/.cache/graphify-rebuild.log&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Strategy D: Claude Code Hooks (AI-Driven Updates)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Only CRG belongs in Claude hooks. graphify does not.&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;Tool&lt;/th&gt;
&lt;th&gt;Claude hook?&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;code-review-graph&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ Stop hook&lt;/td&gt;
&lt;td&gt;~0.425s incremental — safe after every AI turn&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;graphify&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ No Claude hook&lt;/td&gt;
&lt;td&gt;~10s+ on large monorepos — causes hanging background processes, CPU spike, swap exhaustion&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When an AI agent edits files, the &lt;code&gt;Stop&lt;/code&gt; hook triggers a CRG incremental update. &lt;code&gt;code-review-graph install&lt;/code&gt; writes hooks into &lt;code&gt;.claude/settings.json&lt;/code&gt; initially — move them out. The right home is &lt;code&gt;.claude/settings.example.json&lt;/code&gt; (committed reference for teammates) and &lt;code&gt;.claude/settings.local.json&lt;/code&gt; (your live runtime copy, gitignored).&lt;/p&gt;

&lt;p&gt;Use the fallback-safe version that silently no-ops if the CLI isn't installed:&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;0.425s per update&lt;/strong&gt;, CRG runs after every AI turn without blocking the agent's workflow.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;&lt;code&gt;settings.example.json&lt;/code&gt; hooks&lt;/strong&gt; (copy to &lt;code&gt;settings.local.json&lt;/code&gt; for this project, or to &lt;code&gt;~/.claude/settings.json&lt;/code&gt; for all projects):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Stop"&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;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command -v code-review-graph &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; [ -d .code-review-graph ] &amp;amp;&amp;amp; { PF=/tmp/crg-claude.pid; if [ -f &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$PF&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; ] &amp;amp;&amp;amp; kill -0 &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$(cat &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$PF&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; 2&amp;gt;/dev/null; then true; else { code-review-graph update --skip-flows 2&amp;gt;/dev/null &amp;amp;&amp;amp; nohup code-review-graph embed &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp; } &amp;amp; echo $! &amp;gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$PF&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; fi; } || 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;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;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;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bash ~/.claude/scripts/smart-grep-hook.sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"SessionStart"&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;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command -v code-review-graph &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; code-review-graph status 2&amp;gt;/dev/null || 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;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"if command -v code-review-graph &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; git rev-parse --is-inside-work-tree &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; [ ! -d .code-review-graph ]; then printf '{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;systemMessage&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Graph tool installed but not yet initialized. Ask me to set up: code-review-graph (code-review-graph install)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}'; fi || 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;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"if [ -f .code-review-graph/graph.db ] || [ -f graphify-out/graph.json ]; then STATS=&lt;/span&gt;&lt;span class="se"&gt;\"\"&lt;/span&gt;&lt;span class="s2"&gt;; TOOL_LINES=&lt;/span&gt;&lt;span class="se"&gt;\"\"&lt;/span&gt;&lt;span class="s2"&gt;; if [ -f .code-review-graph/graph.db ]; then STATS=$(python3 -c &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;import sqlite3; c=sqlite3.connect('.code-review-graph/graph.db'); n=c.execute('SELECT COUNT(*) FROM nodes').fetchone()[0]; e=c.execute('SELECT COUNT(*) FROM edges').fetchone()[0]; print(f'{n} nodes, {e} edges'); c.close()&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; 2&amp;gt;/dev/null || echo &lt;/span&gt;&lt;span class="se"&gt;\"\"&lt;/span&gt;&lt;span class="s2"&gt;); TOOL_LINES=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;  where is X defined    → semantic_search_nodes_tool(query=X)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  who calls X           → query_graph_tool(pattern=callers_of, target=X)&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  pre-refactor blast    → get_impact_radius_tool(changed_files=[...])&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  community/cluster     → list_communities_tool()&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  code review context   → get_review_context_tool(changed_files=[...])&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; fi; if [ -f graphify-out/graph.json ]; then GFY_STATS=$(python3 -c &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;import json; g=json.load(open('graphify-out/graph.json')); nodes=g.get('nodes',[]); comms=len(set(n.get('community','') for n in nodes if n.get('community',''))); print(f'{len(nodes)} nodes, {comms} communities')&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; 2&amp;gt;/dev/null || echo &lt;/span&gt;&lt;span class="se"&gt;\"\"&lt;/span&gt;&lt;span class="s2"&gt;); [ -n &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$GFY_STATS&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; ] &amp;amp;&amp;amp; STATS=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;${STATS:+$STATS | }graphify: $GFY_STATS&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; TOOL_LINES=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;${TOOL_LINES:+$TOOL_LINES&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n}  CRG miss / explore    → graphify query '&amp;lt;term&amp;gt;' --graph graphify-out/graph.json&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n  path A→B              → graphify path '&amp;lt;from&amp;gt;' '&amp;lt;to&amp;gt;' --graph graphify-out/graph.json&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; fi; printf '{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;hookSpecificOutput&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;hookEventName&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;SessionStart&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;additionalContext&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;GRAPH QUERY CHEATSHEET (%s) — use BEFORE Read/Grep/Bash-find on code:&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;n%s&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;nSkip graph for: .md .json .yml .log .jsonl configs cross-repo paths.&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s2"&gt;nOverride grep gate: append --graph-tried to any Bash command.&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}}' &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$STATS&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;$TOOL_LINES&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; fi"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"timeout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;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;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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why Stop, not PostToolUse?&lt;/strong&gt; &lt;code&gt;PostToolUse&lt;/code&gt; fires after &lt;em&gt;every&lt;/em&gt; individual file edit — if Claude makes 10 edits in one response, it fires 10 times. The PID-file guard has a race window, so concurrent CRG Python processes pile up. &lt;code&gt;Stop&lt;/code&gt; fires &lt;strong&gt;once&lt;/strong&gt; when the AI finishes its entire turn — all edits batched, single update, PID-guarded. Zero pile-up by design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;PostToolUse: []&lt;/code&gt;?&lt;/strong&gt; Explicitly empty. Previous setups used PostToolUse for CRG updates. Clearing it prevents stale hook configs from double-triggering with the Stop hook.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 7: Generate Additional Outputs
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Resource exhaustion prevention&lt;/strong&gt; — all git hooks above include a &lt;code&gt;_resources_ok()&lt;/code&gt; guard that checks CPU load (≤ 50% of logical cores) and available memory (≥ 2 GB) before spawning any background rebuild. If either threshold is exceeded, the rebuild silently skips and logs why. This prevents a real problem: rapid-fire commits (amend chains, interactive rebase) spawning multiple concurrent graphify/CRG processes — observed at 3 stuck processes at 65–73% CPU each, load average 12+, RAM saturated, machine unresponsive. The &lt;code&gt;pgrep&lt;/code&gt; guard adds process-level deduplication (if a previous rebuild is still running, skip). The &lt;code&gt;timeout 300&lt;/code&gt; ensures no background process lives longer than 5 minutes. Together these three guards ensure graph rebuilds are always non-blocking and device-safe.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Wiki (Markdown pages per community)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code-review-graph wiki
&lt;span class="c"&gt;# Wiki: 28 new pages → .code-review-graph/wiki/&lt;/span&gt;

graphify tree &lt;span class="nt"&gt;--graph&lt;/span&gt; graphify-out/graph.json
&lt;span class="c"&gt;# D3 collapsible-tree HTML&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Interactive Visualization
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code-review-graph visualize
&lt;span class="c"&gt;# → .code-review-graph/graph.html&lt;/span&gt;

&lt;span class="c"&gt;# graphify-out/graph.html is generated automatically during build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Blast-Radius Analysis
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# What does this commit affect?&lt;/span&gt;
code-review-graph detect-changes &lt;span class="nt"&gt;--base&lt;/span&gt; HEAD~1 &lt;span class="nt"&gt;--brief&lt;/span&gt;

&lt;span class="c"&gt;# Risk-scored output:&lt;/span&gt;
&lt;span class="c"&gt;# Analyzed 3 changed file(s)&lt;/span&gt;
&lt;span class="c"&gt;# - 2 changed function(s)/class(es)&lt;/span&gt;
&lt;span class="c"&gt;# - 1 affected flow(s)&lt;/span&gt;
&lt;span class="c"&gt;# - Overall risk score: 0.42&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 8: Obsidian Vault (Visual Graph + Persistent Memory)
&lt;/h2&gt;

&lt;p&gt;The Obsidian layer adds a &lt;strong&gt;human-readable, visual knowledge graph&lt;/strong&gt; on top of your codebase. You've probably seen screenshots shared in the Claude Code / graphify community — a force-directed graph of hundreds of connected nodes. That's Obsidian's Graph View rendering one &lt;code&gt;.md&lt;/code&gt; file per symbol, linked by imports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install Obsidian:&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;&lt;span class="c"&gt;# Ubuntu&lt;/span&gt;
snap &lt;span class="nb"&gt;install &lt;/span&gt;obsidian

&lt;span class="c"&gt;# macOS&lt;/span&gt;
brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; obsidian
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Three Vault Modes
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Browser only&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;graphify-out/graph.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One-off exploration, no Obsidian needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Project vault&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;project&amp;gt;/ai-vault/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Single project, clean wikilinks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Global vault&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/obsidian-vault/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Multiple projects + cross-repo merged view&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h3&gt;
  
  
  Mode 1: Browser Only (Zero Setup)
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;graph.html&lt;/code&gt; file graphify generates is a self-contained interactive D3 force graph — drag nodes, zoom, click to inspect. No Obsidian required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xdg-open graphify-out/graph.html   &lt;span class="c"&gt;# Linux&lt;/span&gt;
open graphify-out/graph.html        &lt;span class="c"&gt;# macOS&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Mode 2: Project Vault
&lt;/h3&gt;

&lt;p&gt;The project vault lives at &lt;code&gt;./ai-vault/&lt;/code&gt; inside the project (gitignored). It has two layers:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1 — Basic (GRAPH_REPORT.md only):&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;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ai-vault/&lt;span class="o"&gt;{&lt;/span&gt;graphify,permanent,logs,chats&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;graphify-out/GRAPH_REPORT.md ai-vault/graphify/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you the community report with wikilinks — useful for navigating clusters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2 — Rich vault (one .md per symbol):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The GRAPH_REPORT.md uses wikilinks like &lt;code&gt;[[_COMMUNITY_Community 0]]&lt;/code&gt;, but those target files don't exist yet. To make Obsidian's Graph View actually light up with a full network, generate individual note files for every node and community:&lt;/p&gt;

&lt;p&gt;Save this script to &lt;code&gt;~/.graphify/gen-obsidian-vault.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
gen-obsidian-vault.py — Generate Obsidian notes from graphify graph.json.

Usage:
  # Project vault
  python3 ~/.graphify/gen-obsidian-vault.py --project-root /path/to/project

  # Global vault (multiple projects)
  python3 ~/.graphify/gen-obsidian-vault.py &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s"&gt;    --project-root /path/to/project &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s"&gt;    --vault ~/obsidian-vault &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s"&gt;    --project-name my-project

  # Merged cross-repo graph
  python3 ~/.graphify/gen-obsidian-vault.py &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s"&gt;    --merged-graph ~/obsidian-vault/merged-graph.json &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s"&gt;    --vault ~/obsidian-vault &lt;/span&gt;&lt;span class="se"&gt;\
&lt;/span&gt;&lt;span class="s"&gt;    --project-name merged
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;MAX_LINKS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_build_adjacency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tgt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;relation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;tgt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;tgt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tgt&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inc&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_node_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nodes_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node_pfx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comm_pfx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;nodes_dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;nid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;# &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**File:** `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;source_file&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;`  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**Location:** &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;source_location&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**Type:** &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;file_type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**Community:** [[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;comm_pfx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/Community &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;community&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;|Community &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;community&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nid&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;## Imports / Depends On&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tgt_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nid&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="n"&gt;MAX_LINKS&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tgt_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- [[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;node_pfx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tgt_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tgt_id&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]] `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;`&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nid&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;## Used By&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;src_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;nid&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="n"&gt;MAX_LINKS&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- [[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;node_pfx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;src_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;src_id&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]] `&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rel&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;`&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nodes_dir&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_community_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comm_dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node_pfx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;comm_pfx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;comm_dir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;nid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;community&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mids&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;# Community &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;**Members:** &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;## Members&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;mids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- [[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;node_pfx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;mid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cross&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;community&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;mids&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;community&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;cross&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;community&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;mids&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;community&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cross&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;## Connected Communities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- [[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;comm_pfx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/Community &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;|Community &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cross&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;comm_dir&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Community &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cid&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vault_root&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;project_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;graph_json_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;graph_json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;graph_json_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;graph_json_path&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;graphify-out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;graph.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;graph_json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;graph_json&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; not found. Run &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;graphify update .&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; first.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;graph_json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nodes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
    &lt;span class="n"&gt;links&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;links&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_build_adjacency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;vault&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vault_root&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;expanduser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vault_root&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ai-vault&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vault&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;project_name&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;project_name&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;vault&lt;/span&gt;
    &lt;span class="n"&gt;pfx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;project_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;project_name&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="nf"&gt;_node_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nodes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pfx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;nodes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pfx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;communities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;n_comm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_community_files&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;communities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pfx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;nodes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pfx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;communities&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;project_root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;graphify-out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GRAPH_REPORT.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;gdir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;graphify&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;gdir&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\[\[_COMMUNITY_Community (\d+)\|Community \1\]\]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pfx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;communities/Community &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;|Community &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gdir&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GRAPH_REPORT.md&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  ✓ &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; nodes · &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;links&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; links · &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n_comm&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; communities → &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--project-root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--vault&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--project-name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--merged-graph&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vault&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;merged_graph&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.graphify
&lt;span class="c"&gt;# save the script above to ~/.graphify/gen-obsidian-vault.py, then:&lt;/span&gt;

python3 ~/.graphify/gen-obsidian-vault.py &lt;span class="nt"&gt;--project-root&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;span class="c"&gt;# ✓ 3815 nodes · 4830 links · 749 communities → ./ai-vault&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generates &lt;code&gt;ai-vault/nodes/&lt;/code&gt; (one &lt;code&gt;.md&lt;/code&gt; per symbol) and &lt;code&gt;ai-vault/communities/&lt;/code&gt; (one &lt;code&gt;.md&lt;/code&gt; per cluster). Each file contains wikilinks to its neighbors — Obsidian's Graph View draws the edges automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ai-vault/
├── .obsidian/               ← Obsidian config (auto-created)
├── nodes/                   ← one .md per function / class / file  ← NEW
├── communities/             ← one .md per module cluster           ← NEW
├── graphify/
│   └── GRAPH_REPORT.md      ← synced from graphify-out/
├── permanent/               ← Architecture decisions (human notes)
├── logs/                    ← Session records
└── CLAUDE.md                ← vault-level agent instructions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open in Obsidian → press &lt;code&gt;Ctrl+G&lt;/code&gt; → you get the full network.&lt;/p&gt;




&lt;h3&gt;
  
  
  Mode 3: Global Vault (Multiple Projects)
&lt;/h3&gt;

&lt;p&gt;One Obsidian window, all projects. Each project goes into its own subfolder with namespaced wikilinks — necessary because two projects often share node IDs (shared packages like &lt;code&gt;dto&lt;/code&gt;, &lt;code&gt;types&lt;/code&gt;, &lt;code&gt;utilities&lt;/code&gt;).&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;# Add each project to the global vault&lt;/span&gt;
python3 ~/.graphify/gen-obsidian-vault.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project-root&lt;/span&gt; /path/to/project-a &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vault&lt;/span&gt; ~/obsidian-vault &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project-name&lt;/span&gt; project-a

python3 ~/.graphify/gen-obsidian-vault.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project-root&lt;/span&gt; /path/to/project-b &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vault&lt;/span&gt; ~/obsidian-vault &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project-name&lt;/span&gt; project-b
&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;~/obsidian-vault/
├── 00-Welcome.md           ← optional index note
├── project-a/
│   ├── nodes/
│   ├── communities/
│   └── graphify/GRAPH_REPORT.md
├── project-b/
│   ├── nodes/
│   ├── communities/
│   └── graphify/GRAPH_REPORT.md
└── merged/                 ← optional cross-repo combined view
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Filter by project in Graph View:&lt;/strong&gt; Graph View sidebar → Filters → &lt;code&gt;path:project-a/nodes&lt;/code&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Cross-Repo Merged View
&lt;/h3&gt;

&lt;p&gt;When projects share types or utilities, merging their graphs reveals bridge nodes — code that affects both sides.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;graphify merge-graphs &lt;span class="se"&gt;\&lt;/span&gt;
  /path/to/project-a/graphify-out/graph.json &lt;span class="se"&gt;\&lt;/span&gt;
  /path/to/project-b/graphify-out/graph.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--out&lt;/span&gt; ~/obsidian-vault/merged-graph.json

python3 ~/.graphify/gen-obsidian-vault.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--merged-graph&lt;/span&gt; ~/obsidian-vault/merged-graph.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vault&lt;/span&gt; ~/obsidian-vault &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project-name&lt;/span&gt; merged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nodes in &lt;code&gt;merged/communities/&lt;/code&gt; that link to both projects are your highest-impact change points — changing them ripples across repos.&lt;/p&gt;




&lt;h3&gt;
  
  
  Switching Between Vaults
&lt;/h3&gt;

&lt;p&gt;This is a common pain point: clicking &lt;code&gt;X&lt;/code&gt; quits Obsidian entirely and the next launch reopens the same vault.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Switch inside Obsidian (always works):&lt;/strong&gt;&lt;br&gt;
Click the &lt;strong&gt;vault icon&lt;/strong&gt; at the very bottom-left of the sidebar (looks like a small building/safe). A panel lists all known vaults — click to switch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open two vaults simultaneously (Obsidian v1.x+):&lt;/strong&gt;&lt;br&gt;
Open the vault switcher → hold &lt;code&gt;Ctrl&lt;/code&gt; while clicking a vault → opens in a new window. Both are live at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;From the terminal — Linux snap install:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;obsidian://&lt;/code&gt; URI scheme and &lt;code&gt;xdg-open&lt;/code&gt; do not work with snap-installed Obsidian because snap doesn't register the protocol with the system. Use a small helper script instead.&lt;/p&gt;

&lt;p&gt;Save &lt;code&gt;~/.graphify/obs.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Open a specific Obsidian vault from the terminal (snap-compatible).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;OBSIDIAN_CONFIG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;~/snap/obsidian/current/.config/obsidian/obsidian.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;expanduser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_load&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OBSIDIAN_CONFIG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;OBSIDIAN_CONFIG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vaults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;OBSIDIAN_CONFIG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mkdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parents&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exist_ok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;OBSIDIAN_CONFIG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;open_vault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vault_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vault_path&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;expanduser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;vaults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vaults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="n"&gt;vid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;vid&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;vid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;vaults&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;vid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Registered: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;vaults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;vaults&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;vaults&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;vid&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;open&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="nf"&gt;_save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Opening: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/snap/bin/obsidian&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;start_new_session&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEVNULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DEVNULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Usage: obs.py &amp;lt;vault-path&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;open_vault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This registers the vault in Obsidian's JSON config and launches Obsidian pointing to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shell aliases (add to &lt;code&gt;~/.zshrc&lt;/code&gt; on macOS, &lt;code&gt;~/.bashrc&lt;/code&gt; on Linux):&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;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;obs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'python3 ~/.graphify/obs.py ~/obsidian-vault'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;obs-project&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'python3 ~/.graphify/obs.py /path/to/your-project/ai-vault'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then: &lt;code&gt;source ~/.zshrc&lt;/code&gt; (or &lt;code&gt;source ~/.bashrc&lt;/code&gt; on Linux) and use &lt;code&gt;obs&lt;/code&gt; / &lt;code&gt;obs-project&lt;/code&gt; from any terminal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS (non-snap):&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;open &lt;span class="s2"&gt;"obsidian://open?vault=obsidian-vault"&lt;/span&gt;
&lt;span class="c"&gt;# or&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;obs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'open "obsidian://open?vault=obsidian-vault"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Practical tip — avoid switching entirely:&lt;/strong&gt;&lt;br&gt;
Keep the global vault as your default. Inside Graph View, use the path filter to scope to one project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Graph View sidebar → Filters → &lt;code&gt;path:my-project/nodes&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This shows only that project's symbols without switching vaults.&lt;/p&gt;


&lt;h3&gt;
  
  
  Graph View Filters
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Filter&lt;/th&gt;
&lt;th&gt;Shows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;path:nodes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All code symbols&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;path:my-project/nodes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One project's symbols (global vault)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;path:communities&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Module clusters only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;path:permanent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Architecture decisions (human notes)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;path:logs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Session records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-path:nodes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Human notes only, no code graph&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h3&gt;
  
  
  &lt;code&gt;ai-vault/CLAUDE.md&lt;/code&gt; (vault-level agent instructions — Claude Code example; use &lt;code&gt;AGENTS.md&lt;/code&gt; or &lt;code&gt;GEMINI.md&lt;/code&gt; for other agents)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Session Commands&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`/resume`&lt;/span&gt; — Read the latest log in &lt;span class="sb"&gt;`logs/`&lt;/span&gt; to restore context
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`/save`&lt;/span&gt; — Write a timestamped session summary to &lt;span class="sb"&gt;`logs/YYYY-MM-DD-HH-MM.md`&lt;/span&gt;

&lt;span class="gu"&gt;## Navigation&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`graphify/GRAPH_REPORT.md`&lt;/span&gt; — Codebase graph report
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`permanent/`&lt;/span&gt; — Architecture decisions and atomic notes
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`logs/`&lt;/span&gt; — Session records

&lt;span class="gu"&gt;## Graph Usage&lt;/span&gt;
Before answering architecture questions, read &lt;span class="sb"&gt;`graphify/GRAPH_REPORT.md`&lt;/span&gt;.
Use &lt;span class="sb"&gt;`graphify query`&lt;/span&gt;, &lt;span class="sb"&gt;`graphify path`&lt;/span&gt;, and &lt;span class="sb"&gt;`graphify explain`&lt;/span&gt; from the project root.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Keeping the Vault Fresh
&lt;/h3&gt;

&lt;p&gt;Git hooks keep &lt;code&gt;graph.json&lt;/code&gt; current automatically. Refresh the Obsidian notes after significant changes:&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;# Project vault&lt;/span&gt;
python3 ~/.graphify/gen-obsidian-vault.py &lt;span class="nt"&gt;--project-root&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Global vault entry&lt;/span&gt;
python3 ~/.graphify/gen-obsidian-vault.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project-root&lt;/span&gt; /path/to/project &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vault&lt;/span&gt; ~/obsidian-vault &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project-name&lt;/span&gt; my-project

&lt;span class="c"&gt;# Rebuild merged view&lt;/span&gt;
graphify merge-graphs &lt;span class="se"&gt;\&lt;/span&gt;
  /path/to/a/graphify-out/graph.json &lt;span class="se"&gt;\&lt;/span&gt;
  /path/to/b/graphify-out/graph.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--out&lt;/span&gt; ~/obsidian-vault/merged-graph.json &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
python3 ~/.graphify/gen-obsidian-vault.py &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--merged-graph&lt;/span&gt; ~/obsidian-vault/merged-graph.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vault&lt;/span&gt; ~/obsidian-vault &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project-name&lt;/span&gt; merged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add vault regen to git hooks
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Husky projects:&lt;/strong&gt; create &lt;code&gt;.husky/post-commit&lt;/code&gt; (git-tracked; note &lt;code&gt;.husky/_/&lt;/code&gt; is gitignored and dead code — the &lt;code&gt;h&lt;/code&gt; wrapper always exits before any appended code there runs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plain git projects:&lt;/strong&gt; use &lt;code&gt;.git/hooks/post-commit&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;The hook is structured in two independent sections so teammates without these tools see zero overhead — each section guards itself and silently no-ops if the tool isn't present:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="c"&gt;# Knowledge graph tools are optional — each section silently skips if tools are absent.&lt;/span&gt;

&lt;span class="c"&gt;# _resources_ok function — copy from the post-commit hook above&lt;/span&gt;
&lt;span class="c"&gt;# (CPU ≤ 50% of logical cores, memory ≥ 2 GB free, all platforms)&lt;/span&gt;

&lt;span class="nv"&gt;GIT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--git-dir&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GIT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/rebase-merge"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GIT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/rebase-apply"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GIT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/MERGE_HEAD"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GIT_DIR&lt;/span&gt;&lt;span class="s2"&gt;/CHERRY_PICK_HEAD"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;0

&lt;span class="c"&gt;# ── graphify: rebuild graph after commit ─────────────────────────────────────&lt;/span&gt;
&lt;span class="c"&gt;# Entire block skips if graphify is not installed.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; graphify &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;CHANGED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; HEAD~1 HEAD 2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; git diff &lt;span class="nt"&gt;--name-only&lt;/span&gt; HEAD 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="c"&gt;# (Python interpreter detection omitted for brevity — see full hook above)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$GRAPHIFY_PYTHON&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; pgrep &lt;span class="nt"&gt;-qf&lt;/span&gt; &lt;span class="s1"&gt;'graphify'&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; _resources_ok&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;GRAPHIFY_CHANGED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CHANGED&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="nv"&gt;_LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.cache/graphify-rebuild.log"&lt;/span&gt;
      &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[graphify hook] launching background rebuild (log: &lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
      &lt;span class="nb"&gt;nohup timeout &lt;/span&gt;300 &lt;span class="nv"&gt;$GRAPHIFY_PYTHON&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
import os, sys
from pathlib import Path
changed_raw = os.environ.get('GRAPHIFY_CHANGED', '')
changed = [Path(f.strip()) for f in changed_raw.strip().splitlines() if f.strip()]
if not changed:
    sys.exit(0)
from graphify.watch import _rebuild_code
_rebuild_code(Path('.'))
"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;lt; /dev/null &amp;amp;
      &lt;span class="nb"&gt;disown &lt;/span&gt;2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
    &lt;/span&gt;&lt;span class="k"&gt;fi
  fi
fi&lt;/span&gt;

&lt;span class="c"&gt;# ── code-review-graph: incrementally update MCP graph index after commit ────&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;command&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; code-review-graph &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; .code-review-graph &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; pgrep &lt;span class="nt"&gt;-qf&lt;/span&gt; &lt;span class="s1"&gt;'code-review-graph update'&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; _resources_ok&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nv"&gt;_LOG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HOME&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/.cache/code-review-graph-update.log"&lt;/span&gt;
  &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;dirname&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[code-review-graph] Commit detected - launching background update (log: &lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt;
  &lt;span class="nb"&gt;nohup &lt;/span&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'code-review-graph update --skip-flows &amp;amp;&amp;amp; code-review-graph embed'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_LOG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;&amp;amp;1 &amp;lt; /dev/null &amp;amp;
  &lt;span class="nb"&gt;disown &lt;/span&gt;2&amp;gt;/dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;timeout 300&lt;/code&gt;?&lt;/strong&gt; graphify &lt;code&gt;_rebuild_code&lt;/code&gt; has no internal deadline. On large repos it can hang indefinitely if it hits a file lock, encoding error, or memory pressure. &lt;code&gt;timeout 300&lt;/code&gt; auto-kills the background process after 5 minutes — the git commit returns immediately either way, and the next commit retriggers a fresh attempt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;_resources_ok&lt;/code&gt;?&lt;/strong&gt; Without it, rapid-fire commits (amend chains, rebase, interactive rebase) spawn multiple concurrent graphify processes. Observed: 3 stuck processes at 65–73% CPU each, load average 12+, RAM exhausted. The resource check ensures a rebuild only starts when the machine can handle it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Create the same file at &lt;code&gt;.husky/post-checkout&lt;/code&gt; with one change: wrap the graphify block with &lt;code&gt;if command -v graphify &amp;gt; /dev/null 2&amp;gt;&amp;amp;1 &amp;amp;&amp;amp; [ -d "graphify-out" ]&lt;/code&gt; (force-rebuild only when the graph has been built before). Mark both executable: &lt;code&gt;chmod +x .husky/post-commit .husky/post-checkout&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What "safe to commit" means here:&lt;/strong&gt; a developer with none of these tools runs exactly the 4 rebase guards and exits 0 — no Python spawned, no directories created, no errors.&lt;/p&gt;




&lt;h2&gt;
  
  
  Complete Auto-Update Flow
&lt;/h2&gt;

&lt;p&gt;After full setup, the pipeline looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AI agent session opens (Claude Code shown — other agents use equivalent plugin hooks)
    → SessionStart: code-review-graph status
    → SessionStart: setup nudge (only if CLI installed but no graph built yet)
    → SessionStart: graph cheatsheet injected (query→tool map, ~150 tokens, once per session)

AI agent searches the codebase (grep / rg / find)
    → PreToolUse: smart-grep-hook.sh intercepts
        ├── no graph / non-code target / --graph-tried flag → pass silently
        ├── first grep this session
        │     ├── graph has answer → show answer in context, allow grep (one-shot lesson)
        │     └── graph has no answer → allow grep, suggest MCP tool
        └── subsequent grep
              ├── graph has answer → deny with answer inline (zero retry, saves 2000+ tokens)
              └── graph has no answer → pass silently

AI agent edits a file
    → Stop hook (once per full AI turn): code-review-graph update --skip-flows (0.425s)
    [graphify does NOT update here — too slow for per-turn trigger]

Code committed
    → post-commit: _resources_ok() check
        ├── CPU &amp;gt; 50% or memory &amp;lt; 2 GB → skip rebuild, log why
        ├── pgrep: graphify already running → skip
        └── resources OK → graphify update . (background nohup, timeout 300)
    → post-commit: _resources_ok() check
        ├── pgrep: CRG already running → skip
        └── resources OK → code-review-graph update + embed (background nohup)

Branch switched
    → post-checkout: diff count = git diff --name-only PREV NEW | wc -l
        ├── ≤5 files OR same-base branch switch
        │     ├── graphify: incremental update (SHA256 cached, fast) [resource-guarded]
        │     └── CRG: update --skip-flows + embed [resource-guarded]
        └── &amp;gt;5 files OR brand-new branch
              ├── graphify: force full rebuild (GRAPHIFY_FORCE=true) [resource-guarded]
              └── CRG: build (full SQLite rebuild) [resource-guarded]

Result: developer commits or switches branches → both graphs update in background (if device has resources).
        Agent edits files → CRG updates after each AI turn.
        Agent searches → graph answers before grep fires.
        Device overloaded → rebuilds silently skip, next commit retries.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;blockquote&gt;
&lt;p&gt;Each block below is independent — install only the tools you want. Pure CLI setup → graphify only. Embedding + MCP setup → CRG only. Full stack → both.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Either, both, or pick one:&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;graphify              &lt;span class="c"&gt;# graphify (CLI only)&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;code-review-graph     &lt;span class="c"&gt;# CRG (CLI + MCP server)&lt;/span&gt;
&lt;span class="c"&gt;# Or via uvx / pipx — see the Installation section earlier in this guide&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Project Setup (run once per project, for each tool you installed)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create ignore files (only the ones you need)&lt;/span&gt;
&lt;span class="nb"&gt;touch&lt;/span&gt; .graphifyignore                 &lt;span class="c"&gt;# graphify only&lt;/span&gt;
&lt;span class="nb"&gt;touch&lt;/span&gt; .code-review-graphignore        &lt;span class="c"&gt;# CRG only&lt;/span&gt;

&lt;span class="c"&gt;# Build graphs manually&lt;/span&gt;
graphify update &lt;span class="nb"&gt;.&lt;/span&gt;           &lt;span class="c"&gt;# graphify: JSON graph, AST-only (no tokens)&lt;/span&gt;
code-review-graph build     &lt;span class="c"&gt;# CRG: SQLite graph (fast, no tokens)&lt;/span&gt;

&lt;span class="c"&gt;# Register with your AI agent (run only the lines for installed tools)&lt;/span&gt;
graphify claude &lt;span class="nb"&gt;install&lt;/span&gt;     &lt;span class="c"&gt;# graphify hooks + CLAUDE.md trigger&lt;/span&gt;
graphify hook &lt;span class="nb"&gt;install&lt;/span&gt;       &lt;span class="c"&gt;# graphify post-commit hook&lt;/span&gt;
code-review-graph &lt;span class="nb"&gt;install&lt;/span&gt;   &lt;span class="c"&gt;# CRG MCP server + skills + hooks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Verify (run only the rows for tools you installed)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code-review-graph status            &lt;span class="c"&gt;# CRG: node/edge counts&lt;/span&gt;
&lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-15&lt;/span&gt; graphify-out/GRAPH_REPORT.md  &lt;span class="c"&gt;# graphify: corpus + freshness&lt;/span&gt;
&lt;span class="nb"&gt;time &lt;/span&gt;code-review-graph update &lt;span class="nt"&gt;--skip-flows&lt;/span&gt;   &lt;span class="c"&gt;# CRG: should be &amp;lt;1s&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool .claude/settings.local.json | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A3&lt;/span&gt; PostToolUse
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Daily Commands (each line targets one tool — use what is relevant)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Manual incremental update&lt;/span&gt;
graphify update &lt;span class="nb"&gt;.&lt;/span&gt;           &lt;span class="c"&gt;# graphify: SHA256 incremental&lt;/span&gt;
code-review-graph update    &lt;span class="c"&gt;# CRG: 0.4s&lt;/span&gt;

&lt;span class="c"&gt;# Query (graphify CLI)&lt;/span&gt;
graphify query &lt;span class="s2"&gt;"auth flow"&lt;/span&gt; &lt;span class="nt"&gt;--graph&lt;/span&gt; graphify-out/graph.json
graphify path &lt;span class="s2"&gt;"ComponentA"&lt;/span&gt; &lt;span class="s2"&gt;"ServiceB"&lt;/span&gt; &lt;span class="nt"&gt;--graph&lt;/span&gt; graphify-out/graph.json
graphify explain &lt;span class="s2"&gt;"MyService"&lt;/span&gt; &lt;span class="nt"&gt;--graph&lt;/span&gt; graphify-out/graph.json

&lt;span class="c"&gt;# Blast-radius review (CRG)&lt;/span&gt;
code-review-graph detect-changes &lt;span class="nt"&gt;--base&lt;/span&gt; HEAD~1 &lt;span class="nt"&gt;--brief&lt;/span&gt;

&lt;span class="c"&gt;# Status (CRG)&lt;/span&gt;
code-review-graph status

&lt;span class="c"&gt;# Wiki / visualization (CRG)&lt;/span&gt;
code-review-graph wiki
code-review-graph visualize
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Watch Mode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;graphify watch &lt;span class="nb"&gt;.&lt;/span&gt;                  &lt;span class="c"&gt;# continuous auto-rebuild (graphify)&lt;/span&gt;
code-review-graph watch           &lt;span class="c"&gt;# continuous auto-rebuild (crg)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Why Not a Vector Database?
&lt;/h2&gt;

&lt;p&gt;Code navigation is fundamentally relational — &lt;code&gt;UserController&lt;/code&gt; calls &lt;code&gt;AuthService&lt;/code&gt; which imports &lt;code&gt;TokenRepository&lt;/code&gt;. This is a directed graph, not a bag of vectors.&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;Knowledge Graph&lt;/th&gt;
&lt;th&gt;Vector DB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Code structure&lt;/td&gt;
&lt;td&gt;Topology-exact&lt;/td&gt;
&lt;td&gt;Approximate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Setup&lt;/td&gt;
&lt;td&gt;No embedding pipeline&lt;/td&gt;
&lt;td&gt;Embedding + chunking + sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hallucination&lt;/td&gt;
&lt;td&gt;None (AST is deterministic)&lt;/td&gt;
&lt;td&gt;Can return similar-but-wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost (indexing)&lt;/td&gt;
&lt;td&gt;0 tokens (AST mode)&lt;/td&gt;
&lt;td&gt;Embedding cost per file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incremental update&lt;/td&gt;
&lt;td&gt;0.4s (SQLite diff)&lt;/td&gt;
&lt;td&gt;Re-embed changed chunks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exact symbol lookup&lt;/td&gt;
&lt;td&gt;Use grep/LSP (still best)&lt;/td&gt;
&lt;td&gt;Often worse&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Knowledge graphs excel at &lt;strong&gt;"how does X relate to Y"&lt;/strong&gt; questions. For exact symbol lookup, grep and LSP still win — and both tools' &lt;code&gt;PreToolUse&lt;/code&gt; hooks (or equivalent agent config) redirect the agent toward the graph for structural questions while leaving grep for exact matches.&lt;/p&gt;




&lt;h2&gt;
  
  
  Files Created / Modified
&lt;/h2&gt;

&lt;p&gt;After completing this setup, here's what changes in each project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Committed to repo:&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;your-project/
├── .graphifyignore                    ← new
├── .code-review-graphignore           ← new
├── .mcp.example.json                  ← new (reference config for teammates)
├── CLAUDE.md                          ← 2-line pointer to docs/agent/knowledge-graph.md
├── .claude/
│   ├── settings.json                  ← permissions only, no hooks
│   ├── settings.example.json          ← new (hook reference: Stop CRG update + SessionStart cheatsheet + setup nudge)
│   └── skills/                        ← new (code-review-graph skills relevant to the repo)
├── .husky/                            ← Husky hooks (if using Husky)
│   ├── pre-commit                     ← branch protection (blocks direct commits to protected branches)
│   ├── post-commit                    ← graphify + CRG rebuild (resource-guarded, background nohup)
│   └── post-checkout                  ← graphify + CRG rebuild on branch switch (resource-guarded, smart incremental/full)
└── docs/agent/knowledge-graph.md      ← new (full tool reference)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Gitignored (local only, not committed):&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;your-project/
├── .mcp.json                          ← personal MCP config (copy from .mcp.example.json)
├── .claude/settings.local.json        ← live personal hooks (copy from settings.example.json)
├── graphify-out/                      ← generated (graph.json, GRAPH_REPORT.md, graph.html)
├── .code-review-graph/                ← generated (SQLite db, wiki, visualization)
├── ai-vault/                          ← Obsidian vault (personal, lives inside the project)
├── AGENTS.md / GEMINI.md              ← tool-generated, IDE-specific
├── .cursorrules / .windsurfrules      ← IDE-specific
└── .kiro/ / .opencode.json            ← IDE-specific
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;New teammate setup&lt;/strong&gt; (copy example files and activate):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .mcp.example.json .mcp.json
&lt;span class="nb"&gt;cp&lt;/span&gt; .claude/settings.example.json .claude/settings.local.json
&lt;span class="c"&gt;# Or copy hooks into ~/.claude/settings.json to activate across all projects&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/safishamsi/graphify" rel="noopener noreferrer"&gt;Graphify&lt;/a&gt;&lt;/strong&gt; — AI coding assistant skill (Claude Code, Codex, OpenCode, Cursor, Gemini CLI, and more). Turn any folder of code, SQL schemas, R scripts, shell scripts, docs, papers, images, or videos into a queryable knowledge graph. App code + database schema + infrastructure in one graph.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/tirth8205/code-review-graph" rel="noopener noreferrer"&gt;code-review-graph&lt;/a&gt;&lt;/strong&gt; — Local knowledge graph for Claude Code. Builds a persistent map of your codebase so Claude reads only what matters — 6.8× fewer tokens on reviews and up to 49× on daily coding tasks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.analyticsvidhya.com/blog/2026/04/graphify-guide/" rel="noopener noreferrer"&gt;From Karpathy's LLM Wiki to Graphify: AI Memory Layers are Here&lt;/a&gt;&lt;/strong&gt; — Analytics Vidhya article on the evolution and implementation of knowledge graphs for AI systems.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Let's Connect
&lt;/h2&gt;

&lt;p&gt;I'm always excited to hear about what you're building! If you found this guide helpful, have questions, or just want to share your ai setup strategy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://encryptioner.github.io" rel="noopener noreferrer"&gt;encryptioner.github.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn&lt;/strong&gt;: &lt;a href="https://www.linkedin.com/in/mir-mursalin-ankur" rel="noopener noreferrer"&gt;Mir Mursalin Ankur&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/Encryptioner" rel="noopener noreferrer"&gt;@Encryptioner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X (Twitter)&lt;/strong&gt;: &lt;a href="https://twitter.com/AnkurMursalin" rel="noopener noreferrer"&gt;@AnkurMursalin&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical Writing&lt;/strong&gt;: &lt;a href="https://nerddevs.com/author/ankur/" rel="noopener noreferrer"&gt;Nerddevs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support&lt;/strong&gt;: &lt;a href="https://www.supportkori.com/mirmursalinankur" rel="noopener noreferrer"&gt;SupportKori&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>tooling</category>
    </item>
    <item>
      <title>branchdiff + GitHub &amp; Bitbucket: A Local Lens for the Pull Request Workflow You Already Have</title>
      <dc:creator>Mir Mursalin Ankur</dc:creator>
      <pubDate>Sun, 10 May 2026 11:32:50 +0000</pubDate>
      <link>https://dev.to/mir_mursalin_ankur/branchdiff-github-bitbucket-a-local-lens-for-the-pull-request-workflow-you-already-have-20mm</link>
      <guid>https://dev.to/mir_mursalin_ankur/branchdiff-github-bitbucket-a-local-lens-for-the-pull-request-workflow-you-already-have-20mm</guid>
      <description>&lt;p&gt;Here is a number nobody tracks but everyone feels: the number of tab switches in a single code review.&lt;/p&gt;

&lt;p&gt;Open the PR. Switch to your editor to search for the function being changed. Switch back to the PR to find your place. Click a file in the sidebar. Scroll to the relevant hunk. The author pushes a fix commit — the diff resets to the top. Switch to Slack to tell them you were mid-read. Switch back. Find your place again. Copy a snippet into your AI assistant. Paste the AI's reply back as a comment. Tab-switch to the PR. Tab-switch back because you forgot which file you were on.&lt;/p&gt;

&lt;p&gt;By the time you post your review, you have made twenty tab switches to do ten minutes of actual reading. That friction is not a big problem. It is a hundred small ones. And small frictions are the exact things that cause engineers to put review off until Friday afternoon when they are too tired to read carefully.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;branchdiff is not a replacement for GitHub or Bitbucket.&lt;/strong&gt; It is a local browser app that sits beside your editor and handles the reading, marking, commenting, and lifecycle actions — while every click that matters (merge gate, approval, audit trail, CI status) still happens on the platform, through the same APIs you would use anyway. The PR on the platform is still the source of truth. branchdiff is just a better cockpit for the part of review that does not benefit from being a cloud round-trip.&lt;/p&gt;

&lt;p&gt;This post explains how those two layers fit together — what lives where, and why the seam is designed the way it is.&lt;/p&gt;




&lt;h2&gt;
  
  
  The mental model: source of truth vs. working surface
&lt;/h2&gt;

&lt;p&gt;Think of the PR on GitHub or Bitbucket as the &lt;strong&gt;source of truth&lt;/strong&gt;. The review history, approvals, merge state, CI status, branch protection, and audit trail all live there. Nothing in branchdiff changes any of that. Your team's review process, your release process, and your compliance posture keep working exactly the way they always have.&lt;/p&gt;

&lt;p&gt;branchdiff is the &lt;strong&gt;working surface&lt;/strong&gt; for the part of review that is fundamentally local — reading the diff, marking files as you go, drafting comments, navigating between files, and running an AI pass. When you are ready, you sync the comments to the PR with one click and the platform takes over again.&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.amazonaws.com%2Fuploads%2Farticles%2Fegcgpyirrqfmu1e7d7bs.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.amazonaws.com%2Fuploads%2Farticles%2Fegcgpyirrqfmu1e7d7bs.png" alt="Source of Truth vs. Working Surface" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The split in practice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;GitHub / Bitbucket&lt;/th&gt;
&lt;th&gt;branchdiff&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Merge gating, branch protection&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI / status checks&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approvals &amp;amp; required reviewers&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final review comments&lt;/td&gt;
&lt;td&gt;✓ (after sync)&lt;/td&gt;
&lt;td&gt;✓ (drafted here)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reading the diff with full context&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Marking files reviewed / stale&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Running AI review or resolve&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PR lifecycle clicks (approve, merge)&lt;/td&gt;
&lt;td&gt;✓ (canonical)&lt;/td&gt;
&lt;td&gt;✓ (proxied via API)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything in the right column is a convenience layer. Everything in the left column is still authoritative. Uninstall branchdiff tomorrow and your team's review process keeps working — you go back to clicking on the PR page. That is the design constraint the whole tool is built around.&lt;/p&gt;




&lt;h2&gt;
  
  
  Opening any PR locally in seconds
&lt;/h2&gt;

&lt;p&gt;Point branchdiff at a PR URL on either platform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;branchdiff https://github.com/owner/repo/pull/123
branchdiff https://bitbucket.org/workspace/repo/pull-requests/45
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It resolves the base and head refs, runs &lt;code&gt;git fetch&lt;/code&gt; if needed, and opens a browser tab at &lt;code&gt;http://localhost:5391&lt;/code&gt; with the diff rendered — syntax highlighting for 150+ languages, split or unified view, a sidebar of changed files, keyboard navigation (&lt;code&gt;j&lt;/code&gt;/&lt;code&gt;k&lt;/code&gt; next/previous file, &lt;code&gt;n&lt;/code&gt;/&lt;code&gt;p&lt;/code&gt; next/previous hunk). The PR on the platform is untouched.&lt;/p&gt;

&lt;p&gt;Named-ref comparisons (&lt;code&gt;main..feature&lt;/code&gt;) get a &lt;strong&gt;persistent review session&lt;/strong&gt; backed by a local SQLite file in &lt;code&gt;~/.branchdiff/&lt;/code&gt;. Inline comments survive new commits — same idea as a GitHub PR thread, but stored on your machine, readable instantly, without a round-trip. If the author force-pushes mid-review, your view markers and drafts stay intact.&lt;/p&gt;

&lt;p&gt;Multiple sessions are the default, not a workaround. Each unique ref pair opens on its own port — the second session on &lt;code&gt;5392&lt;/code&gt;, the third on &lt;code&gt;5393&lt;/code&gt;, and so on. Reviewing a teammate's PR while working on your own branch in another tab is just two browser tabs. &lt;code&gt;branchdiff list&lt;/code&gt; shows everything running.&lt;/p&gt;




&lt;h2&gt;
  
  
  PR lifecycle from the toolbar — a remote control, not a parallel database
&lt;/h2&gt;

&lt;p&gt;The toolbar shows a dropdown for the open PR with the actions you would otherwise click on the platform. It is &lt;strong&gt;not&lt;/strong&gt; a separate database of PR state. It is a remote control. Each action calls &lt;code&gt;gh&lt;/code&gt; for GitHub or the Bitbucket REST API for Bitbucket, and the canonical PR records the action with the same timestamp it would have had if you had clicked on the website.&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.amazonaws.com%2Fuploads%2Farticles%2F130ovc40k04h23ga0d8h.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.amazonaws.com%2Fuploads%2Farticles%2F130ovc40k04h23ga0d8h.png" alt="PR Lifecycle Action Groups" width="800" height="310"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The dropdown groups four kinds of action:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review actions&lt;/strong&gt; — Approve, Request Changes, Comment. The Comment action posts a regular PR comment via &lt;code&gt;gh pr comment&lt;/code&gt; (fixed in v1.5.1; the previous version mistakenly created a formal review object). Request Changes allows an optional comment; if you provide nothing, a default message is posted because both APIs require a review body.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Merge action&lt;/strong&gt; — with a strategy picker. GitHub offers &lt;em&gt;Merge commit&lt;/em&gt;, &lt;em&gt;Squash&lt;/em&gt;, and &lt;em&gt;Rebase&lt;/em&gt;; Bitbucket offers &lt;em&gt;Merge commit&lt;/em&gt;, &lt;em&gt;Squash&lt;/em&gt;, and &lt;em&gt;Fast-forward&lt;/em&gt; (added in v1.5.1). Branch protection rules and required status checks still apply — if the PR cannot merge, the platform refuses and branchdiff surfaces the error inline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;State actions&lt;/strong&gt; — Close, Reopen (only when valid), Mark as Draft, Mark Ready for Review.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Metadata actions&lt;/strong&gt; — Edit title and description, Sync comments, Open in browser.&lt;/p&gt;

&lt;p&gt;The dropdown header carries a &lt;strong&gt;state dot&lt;/strong&gt; (green for open, purple for merged, red for closed) and a row of &lt;strong&gt;reviewer pills&lt;/strong&gt; showing each reviewer's latest state — approved, changes requested, commented, pending, or dismissed. Destructive actions (merge, close, request changes) show a confirmation dialog; errors stay inline in the dialog so a typed message is not lost when the network blips.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comment sync — both directions, both opt-in
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Push local comments to the PR.&lt;/strong&gt; Draft comments in branchdiff (manually or via an AI pass), then click the PR badge in the toolbar and choose &lt;strong&gt;Push to PR&lt;/strong&gt;. Each single-comment thread is posted as an inline review comment. Duplicates (same file, line, body) are skipped. Multi-reply threads stay local because the platform APIs do not map cleanly to them.&lt;/p&gt;

&lt;p&gt;A status toast tells you what happened: &lt;code&gt;Pushed 3, skipped 1 duplicate, skipped 1 multi-reply thread&lt;/code&gt;. Failures keep the local thread intact so you can fix the anchor and retry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pull PR comments into branchdiff.&lt;/strong&gt; Same dialog, &lt;strong&gt;Pull from PR&lt;/strong&gt;. Existing review comments come down as local threads anchored to the same lines, with author and timestamp preserved. This is what makes branchdiff practical for re-review — open the PR locally on day three, pull the comments, see every thread inline, mark them resolved as the author addresses them, push the resolutions back.&lt;/p&gt;

&lt;p&gt;Sync requires your local HEAD to match the PR head and your working tree to be clean. branchdiff surfaces both constraints in the dialog with a one-line explanation (&lt;code&gt;git pull --rebase&lt;/code&gt;, &lt;code&gt;git stash&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;This is the only cloud round-trip in the whole tool. No telemetry, no remote diff service. Wipe &lt;code&gt;~/.branchdiff/&lt;/code&gt; and there is nothing in any backend with your data.&lt;/p&gt;




&lt;h2&gt;
  
  
  A concrete 90-second flow
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fxlsdj6pp8ld5cjg4x487.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.amazonaws.com%2Fuploads%2Farticles%2Fxlsdj6pp8ld5cjg4x487.png" alt="90-Second PR Review Flow" width="800" height="240"&gt;&lt;/a&gt;&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;# Teammate posts: "PR is up — https://github.com/acme/api/pull/482"&lt;/span&gt;
branchdiff https://github.com/acme/api/pull/482
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Browser opens in about three seconds. Toolbar shows &lt;code&gt;#482&lt;/code&gt; with a green dot. Reviewer pills: one teammate already approved, one pending. You scroll the diff, mark a few files viewed, drop two &lt;code&gt;[suggestion]&lt;/code&gt; comments and one &lt;code&gt;[must-fix]&lt;/code&gt;. Click &lt;code&gt;#482&lt;/code&gt; → &lt;strong&gt;Push to PR&lt;/strong&gt;. Toast: "Pushed 3 comments." Click &lt;strong&gt;Approve&lt;/strong&gt; in the dropdown. The toolbar updates to show your green pill.&lt;/p&gt;

&lt;p&gt;Total wall-clock time: 90 seconds from "teammate posts link" to "PR has your review on it." The work of &lt;em&gt;reading&lt;/em&gt; the diff is unchanged. Everything around it shrunk.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters for AI-assisted review
&lt;/h2&gt;

&lt;p&gt;AI is now part of almost every review, whether the team has formalised it or not. Engineers paste diffs into ChatGPT, run Copilot review, pipe &lt;code&gt;git diff&lt;/code&gt; into &lt;code&gt;claude&lt;/code&gt;. Done ad-hoc, that AI usage drifts — different people use different prompts, comments do not land on the right lines, and the context never makes it back to the PR.&lt;/p&gt;

&lt;p&gt;branchdiff gives that AI usage a controlled, repeatable surface: the same diff the human is looking at, the same line numbers, the same comment threads, with explicit &lt;code&gt;branchdiff agent&lt;/code&gt; commands so the AI cannot wander outside the review. Every AI comment lands on a real line, with a severity tag, in the same session as your manual comments. You can dismiss, resolve, or push selectively. The AI cannot post directly to the PR — it goes through the same command surface you use, and you stay the gate.&lt;/p&gt;

&lt;p&gt;The next two posts in this series go deeper: self-review on your own branch before the PR opens, and AI-assisted review of teammates' PRs that syncs back to the canonical thread.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it honestly saves you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fewer tab switches.&lt;/strong&gt; Diff, comments, AI assistant, and PR lifecycle all live on one local page that loads in milliseconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A consistent UX across GitHub and Bitbucket.&lt;/strong&gt; Review keystrokes are the same on either side — engineers who straddle both platforms stop having to learn two UIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No re-reading after a force-push.&lt;/strong&gt; Viewed / stale tracking remembers which files you have already looked at. You re-read only the parts that actually changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A predictable AI surface.&lt;/strong&gt; Systematic, tagged, dismissable. Not an ad-hoc paste-into-ChatGPT that leaves no trace.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where it stops
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No CI.&lt;/strong&gt; The platform's checks are still the gate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cloud storage.&lt;/strong&gt; Wipe &lt;code&gt;~/.branchdiff/&lt;/code&gt; and you lose local drafts that have not been pushed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No multi-reply thread push.&lt;/strong&gt; Back-and-forth conversations belong on the PR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No branch protection bypass.&lt;/strong&gt; Merge actions still respect required reviews and status checks on the platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No full PR page replacement.&lt;/strong&gt; The PR description, linked issues, and CI status block still live on the platform. &lt;em&gt;Open in browser&lt;/em&gt; in the toolbar is one click away.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tool is intentionally small. It does the part that benefits from being local, and gets out of the way of the part that benefits from being on the platform.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it on a PR you already have open
&lt;/h2&gt;

&lt;p&gt;Install from the &lt;a href="https://encryptioner.github.io/branchdiff-releases/" rel="noopener noreferrer"&gt;branchdiff releases page&lt;/a&gt; — install instructions, changelogs, and uninstall steps are all there. Quickstart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @encryptioner/branchdiff
&lt;span class="c"&gt;# or: pip install branchdiff&lt;/span&gt;
&lt;span class="c"&gt;# or: brew tap encryptioner/branchdiff https://github.com/encryptioner/branchdiff-releases \&lt;/span&gt;
&lt;span class="c"&gt;#          &amp;amp;&amp;amp; brew install branchdiff&lt;/span&gt;

branchdiff https://github.com/your-org/your-repo/pull/123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click the PR badge in the toolbar, watch the reviewer pills populate, push a comment back, check that it appears on the PR. The point is not "branchdiff vs. GitHub" — it is to see where your current review workflow keeps a tab open that does not need to be open.&lt;/p&gt;




&lt;h2&gt;
  
  
  Let's Connect
&lt;/h2&gt;

&lt;p&gt;I am always excited to hear what you are building. If you found this guide helpful, have questions, or just want to share your code review setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://encryptioner.github.io" rel="noopener noreferrer"&gt;encryptioner.github.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn&lt;/strong&gt;: &lt;a href="https://www.linkedin.com/in/mir-mursalin-ankur" rel="noopener noreferrer"&gt;Mir Mursalin Ankur&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/Encryptioner" rel="noopener noreferrer"&gt;@Encryptioner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X (Twitter)&lt;/strong&gt;: &lt;a href="https://twitter.com/AnkurMursalin" rel="noopener noreferrer"&gt;@AnkurMursalin&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical Writing&lt;/strong&gt;: &lt;a href="https://nerddevs.com/author/ankur/" rel="noopener noreferrer"&gt;Nerddevs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support&lt;/strong&gt;: &lt;a href="https://www.supportkori.com/mirmursalinankur" rel="noopener noreferrer"&gt;SupportKori&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;branchdiff releases, changelog, install &amp;amp; uninstall guide: &lt;a href="https://encryptioner.github.io/branchdiff-releases/" rel="noopener noreferrer"&gt;encryptioner.github.io/branchdiff-releases&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>productivity</category>
      <category>showdev</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Claude Code Configuration Blueprint: The Complete Guide for Production Teams</title>
      <dc:creator>Mir Mursalin Ankur</dc:creator>
      <pubDate>Sun, 15 Mar 2026 05:01:20 +0000</pubDate>
      <link>https://dev.to/mir_mursalin_ankur/claude-code-configuration-blueprint-the-complete-guide-for-production-teams-557p</link>
      <guid>https://dev.to/mir_mursalin_ankur/claude-code-configuration-blueprint-the-complete-guide-for-production-teams-557p</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Configure Claude Code once — get security, reusable workflows, specialized agents,&lt;br&gt;
and cross-session intelligence across every project.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Who This Is For
&lt;/h2&gt;

&lt;p&gt;You've used Claude Code on a project or two. You know the basics. Now you want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stop Claude from touching secrets, credentials, or system files&lt;/li&gt;
&lt;li&gt;Share team standards via git without leaking personal preferences&lt;/li&gt;
&lt;li&gt;Create reusable workflows (spec, plan, implement, review, ship)&lt;/li&gt;
&lt;li&gt;Make Claude remember lessons across sessions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guide gives you copy-paste configs and explains the &lt;strong&gt;why&lt;/strong&gt; behind each decision.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Configuration Layers
&lt;/h2&gt;

&lt;p&gt;Claude Code loads configuration from multiple layers, each with a specific scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Personal (not in any repo)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;~/.claude/settings.json        → Security (deny list for all projects)&lt;/span&gt;
  &lt;span class="s"&gt;~/.claude/CLAUDE.md            → Personal workflow preferences&lt;/span&gt;
  &lt;span class="s"&gt;~/.claude/skills/*/SKILL.md    → Reusable workflows&lt;/span&gt;
  &lt;span class="s"&gt;~/.claude/commands/*/SKILL.md  → Convenience shortcuts&lt;/span&gt;
  &lt;span class="s"&gt;~/.claude/rules/*.md           → File-specific patterns (glob matching)&lt;/span&gt;
  &lt;span class="s"&gt;~/.claude/agents/*.md          → Domain specialists&lt;/span&gt;

&lt;span class="na"&gt;Team-shared (committed to git)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;.claude/settings.json          → Project permissions&lt;/span&gt;
  &lt;span class="s"&gt;CLAUDE.md                      → Coding standards, architecture&lt;/span&gt;
  &lt;span class="s"&gt;.claude/rules/*.md             → Team-specific patterns&lt;/span&gt;

&lt;span class="na"&gt;Personal overrides (gitignored)&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;.claude/settings.local.json    → Your project overrides&lt;/span&gt;
  &lt;span class="s"&gt;CLAUDE.local.md                → Your project preferences&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key rule:&lt;/strong&gt; Your project CLAUDE.md must be self-contained. Teammates don't have your global skills, agents, or personal CLAUDE.md — global files are your personal toolkit, project files are the team playbook.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1: Security — The Permission System
&lt;/h2&gt;

&lt;p&gt;Permissions follow a strict hierarchy: &lt;strong&gt;Deny&lt;/strong&gt; (always blocks) → &lt;strong&gt;Ask&lt;/strong&gt; (prompts you) → &lt;strong&gt;Allow&lt;/strong&gt; (auto-approved). Deny always wins — this is what makes the system trustworthy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern Syntax
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bash(exact command)       → matches only that exact command
Bash(command *)           → matches command with any arguments (space before *)
Read(path/to/file)        → matches exact file
Read(path/**/*.json)      → matches glob pattern
Read(~/.ssh/**)           → ~ expands to home directory on any OS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Space matters:&lt;/strong&gt; &lt;code&gt;Bash(ls *)&lt;/code&gt; matches &lt;code&gt;ls -la&lt;/code&gt; but NOT &lt;code&gt;lsof&lt;/code&gt;. &lt;code&gt;Bash(ls*)&lt;/code&gt; matches both.&lt;/p&gt;

&lt;h3&gt;
  
  
  Global Settings (&lt;code&gt;~/.claude/settings.json&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Glob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Grep"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git status:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git diff:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(nvm use:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(node:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(ls:*)"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ask"&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;"Bash(git push:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git commit:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(pnpm install:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(docker:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(curl:*)"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&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;"Bash(rm -rf:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(sudo:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git push --force:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(.env.*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(~/.ssh/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(~/.aws/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(~/Library/Keychains/**)"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cross-OS coverage:&lt;/strong&gt; The deny list covers macOS (&lt;code&gt;~/Library/Keychains&lt;/code&gt;), Linux (&lt;code&gt;~/.local/share/keyrings&lt;/code&gt;), and Windows WSL (&lt;code&gt;/mnt/c/Users/*/AppData&lt;/code&gt;). One global config works on any machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Project Settings (&lt;code&gt;.claude/settings.example.json&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allow"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Read"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Edit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Glob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Grep"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(pnpm build:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash(pnpm test:*)"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&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;"Read(.env)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(.env.*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(node_modules/**)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Read(dist/**)"&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;Add to &lt;code&gt;.gitignore&lt;/code&gt;: &lt;code&gt;.claude/*&lt;/code&gt; and &lt;code&gt;!.claude/*.example*&lt;/code&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: CLAUDE.md — Teaching Claude Your Codebase
&lt;/h2&gt;

&lt;p&gt;A well-written CLAUDE.md turns Claude from a generic assistant into a team member who knows your conventions, pain points, and architecture.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Instruction Budget Problem
&lt;/h3&gt;

&lt;p&gt;Frontier LLMs reliably follow roughly &lt;strong&gt;150–200 instructions&lt;/strong&gt; total. Claude Code's system prompt consumes ~50 slots before your CLAUDE.md loads. Instruction degradation is &lt;strong&gt;uniform&lt;/strong&gt; — every low-value rule you add actively makes your high-value rules less likely to be followed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brevity isn't nice — it directly affects instruction compliance.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Template
&lt;/h3&gt;

&lt;p&gt;Write it for Claude, not humans. Don't duplicate your README.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# CLAUDE.md&lt;/span&gt;

&lt;span class="gu"&gt;## Commands&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`pnpm dev`&lt;/span&gt; — Start development server
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`pnpm build`&lt;/span&gt; — Production build
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`pnpm lint-fix`&lt;/span&gt; — Lint and fix all packages

&lt;span class="gu"&gt;## Architecture&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`packages/client`&lt;/span&gt; — Vue 3 frontend
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`packages/server`&lt;/span&gt; — Express API
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`packages/shared`&lt;/span&gt; — Types, DTOs, utilities

&lt;span class="gu"&gt;## Coding Standards&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; TypeScript strict mode, no &lt;span class="sb"&gt;`any`&lt;/span&gt; types
&lt;span class="p"&gt;-&lt;/span&gt; Absolute imports only (no &lt;span class="sb"&gt;`../../`&lt;/span&gt;)
&lt;span class="p"&gt;-&lt;/span&gt; API payloads: declare as typed constants

&lt;span class="gu"&gt;## PR Review Rules&lt;/span&gt;
&lt;span class="p"&gt;1.&lt;/span&gt; &lt;span class="gs"&gt;**Type Safety**&lt;/span&gt; — Never use &lt;span class="sb"&gt;`any`&lt;/span&gt;. Typed constants for API payloads
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Dead Code**&lt;/span&gt; — Remove unused imports, variables, i18n keys
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Error Handling**&lt;/span&gt; — Wrap all API calls in try/catch

&lt;span class="gu"&gt;## Critical Gotchas&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Always use projection in DB queries — never fetch entire documents
&lt;span class="p"&gt;-&lt;/span&gt; Server error messages MUST come from constant files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Progressive Disclosure
&lt;/h3&gt;

&lt;p&gt;For larger projects, use a &lt;strong&gt;progressive disclosure&lt;/strong&gt; pattern — put detailed docs in a separate directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Detailed Documentation&lt;/span&gt;
When working on specific areas, read the relevant doc first:
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`docs/Agent/database_schema.md`&lt;/span&gt; — Data model and relationships
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`docs/Agent/payment_flow.md`&lt;/span&gt; — Payment gateway integration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What Works vs. What Doesn't
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Do&lt;/th&gt;
&lt;th&gt;Don't&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Include commands Claude needs&lt;/td&gt;
&lt;td&gt;Include formatting rules (use linter + hooks)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Document non-obvious patterns&lt;/td&gt;
&lt;td&gt;Document obvious language features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;List real PR review pain points&lt;/td&gt;
&lt;td&gt;Duplicate your README&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep it under 200 lines&lt;/td&gt;
&lt;td&gt;Include aspirational rules nobody follows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Part 3: Rules — Enforcing Patterns with Globs
&lt;/h2&gt;

&lt;p&gt;For patterns that must apply across specific file types, use &lt;strong&gt;rules&lt;/strong&gt; with glob matching. Place them in &lt;code&gt;~/.claude/rules/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Common coding mistakes — enforced across all projects&lt;/span&gt;
&lt;span class="na"&gt;globs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.ts,*.tsx,*.vue,*.js,*.jsx"&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Coding Gotchas&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Never use relative imports (&lt;span class="sb"&gt;`../../`&lt;/span&gt;) — always absolute paths
&lt;span class="p"&gt;-&lt;/span&gt; Never introduce &lt;span class="sb"&gt;`any`&lt;/span&gt; types — find or create the proper type
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Claude edits files matching the glob, these rules apply automatically.&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;CLAUDE.md&lt;/th&gt;
&lt;th&gt;Rules (&lt;code&gt;~/.claude/rules/&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scope&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per-project&lt;/td&gt;
&lt;td&gt;Global (all projects)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Content&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Architecture, commands, standards&lt;/td&gt;
&lt;td&gt;File-specific patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trigger&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Loads when working in directory&lt;/td&gt;
&lt;td&gt;Loads when file matches glob&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Teaching your codebase&lt;/td&gt;
&lt;td&gt;Enforcing patterns across projects&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For teams: commit project-specific rules to &lt;code&gt;.claude/rules/&lt;/code&gt; in your repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4: Plugins &amp;amp; Agents — Instant Capabilities
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Plugins
&lt;/h3&gt;

&lt;p&gt;Plugins are pre-built packages from the community. One install command gives you agents, MCP servers, and skills:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude plugins:install context7 superpowers code-review feature-dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Recommended stack:&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;Plugin&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;context7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Live, version-specific documentation lookup&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;feature-dev&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Codebase exploration + architecture design agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;superpowers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;TDD, debugging, brainstorming, parallel agents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;code-review&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Multi-agent PR review with confidence scoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;playwright&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Browser automation and E2E testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;commit-commands&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Streamlined git commit, push, and PR creation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;MCP Servers&lt;/strong&gt; — Many plugins include MCP (Model Context Protocol) servers that bridge Claude Code with external systems (databases, APIs, task trackers). No additional configuration needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom Marketplaces&lt;/strong&gt; — Add team-specific or community plugins:&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;"extraKnownMarketplaces"&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;"my-team"&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;"source"&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;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"repo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-team/claude-plugins"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Agents
&lt;/h3&gt;

&lt;p&gt;Agents are pre-configured specialists you can call for specific tasks. Unlike plugins (which add tools), agents add expertise.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Example Agents&lt;/th&gt;
&lt;th&gt;When to Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Engineering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;backend-architect&lt;/code&gt;, &lt;code&gt;frontend-developer&lt;/code&gt;, &lt;code&gt;ai-engineer&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Domain-specific architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Testing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;test-writer-fixer&lt;/code&gt;, code-review agents&lt;/td&gt;
&lt;td&gt;Test strategy, bug fixing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;analytics-reporter&lt;/code&gt;, &lt;code&gt;finance-tracker&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Data analysis, reporting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Development&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;code-explorer&lt;/code&gt;, &lt;code&gt;code-architect&lt;/code&gt;, &lt;code&gt;rapid-prototyper&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Codebase understanding, MVPs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Instead of explaining React Server Components patterns, say &lt;em&gt;"Use the frontend-developer agent to build this with Server Components."&lt;/em&gt; The agent already knows the patterns, has React-specific tools, and follows frontend best practices automatically.&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;Plugins&lt;/th&gt;
&lt;th&gt;Agents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;What they add&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tools, MCP servers, skills&lt;/td&gt;
&lt;td&gt;Domain expertise and focus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Best for&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Adding capabilities&lt;/td&gt;
&lt;td&gt;Complex tasks requiring depth&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Part 5: Skills &amp;amp; Commands — Building Workflows
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Skills vs. Commands
&lt;/h3&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;
&lt;strong&gt;Commands&lt;/strong&gt; (&lt;code&gt;~/.claude/commands/&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Skills&lt;/strong&gt; (&lt;code&gt;~/.claude/skills/&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Convenience shortcuts&lt;/td&gt;
&lt;td&gt;Workflow orchestration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/commit&lt;/code&gt;, &lt;code&gt;/loop&lt;/code&gt;, &lt;code&gt;/rewind&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/spec&lt;/code&gt;, &lt;code&gt;/implement&lt;/code&gt;, &lt;code&gt;/grill&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scope&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single operations&lt;/td&gt;
&lt;td&gt;Multi-step processes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Use Case&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Quick actions&lt;/td&gt;
&lt;td&gt;Structured workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use a &lt;strong&gt;command&lt;/strong&gt; for quick shortcuts. Use a &lt;strong&gt;skill&lt;/strong&gt; for multi-step workflows with decision points.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Spec-to-Ship Workflow
&lt;/h3&gt;

&lt;p&gt;The real power of skills comes from chaining them into a complete development workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/scope (optional) → /spec → /grill (auto) → /plan-work → /grill (auto) → /implement → /verify → /ship
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The ceremony scales with the risk:&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;Task Type&lt;/th&gt;
&lt;th&gt;Workflow&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;New product&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/scope&lt;/code&gt; → full chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Feature&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full chain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Minor enhancement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Light spec → plan → implement → verify → ship&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bug fix&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Bug spec or skip → plan → implement → verify → ship&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hotfix&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Inline plan → implement → verify → ship&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Key Skills
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Skill&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;/scope&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rapid product scoping — forces "ONE thing" question, maps user journey, identifies cost drivers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/spec&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clarified technical spec with Change Log for tracking mid-implementation updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/grill&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hard-critic review through 7 lenses (security, architecture, testing, etc.) with severity levels&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/plan-work&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Phase-by-phase implementation plan traced to spec requirements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/implement&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Executes plans with mid-implementation discovery handling (minor/moderate/significant tiers)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/verify&lt;/code&gt; → &lt;code&gt;/pre-review&lt;/code&gt; → &lt;code&gt;/ship&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Type check → spec requirement walkthrough → branch management + PR creation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Ticket Directory Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;ai/TICKET-146/&lt;/span&gt;
&lt;span class="s"&gt;├── requirements/&lt;/span&gt;
&lt;span class="s"&gt;│   ├── original-requirement.md&lt;/span&gt;    &lt;span class="c1"&gt;# Raw requirement (verbatim)&lt;/span&gt;
&lt;span class="s"&gt;│   ├── spec.md&lt;/span&gt;                    &lt;span class="c1"&gt;# Clarified technical spec&lt;/span&gt;
&lt;span class="s"&gt;│   └── grill-log.md&lt;/span&gt;              &lt;span class="c1"&gt;# Review findings&lt;/span&gt;
&lt;span class="s"&gt;├── plans/&lt;/span&gt;
&lt;span class="s"&gt;│   ├── overview.md&lt;/span&gt;               &lt;span class="c1"&gt;# High-level plan&lt;/span&gt;
&lt;span class="s"&gt;│   ├── phase-1-data-model.md&lt;/span&gt;     &lt;span class="c1"&gt;# Detailed phase plan&lt;/span&gt;
&lt;span class="s"&gt;│   └── grill-log.md&lt;/span&gt;              &lt;span class="c1"&gt;# Plan review findings&lt;/span&gt;
&lt;span class="s"&gt;└── tests/&lt;/span&gt;
    &lt;span class="s"&gt;└── manual-test-cases.csv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure makes work &lt;strong&gt;resumable across sessions&lt;/strong&gt; — future sessions read spec/plan files to pick up where you left off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Building Your Own Skills
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-skill&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Use when the user says "plan this" or presents a multi-file task&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;

&lt;span class="gh"&gt;# Skill Title&lt;/span&gt;
&lt;span class="gu"&gt;## When to Use&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [Trigger conditions]

&lt;span class="gu"&gt;## Process&lt;/span&gt;
&lt;span class="gu"&gt;### Step 1: [Name]&lt;/span&gt;
[Instructions Claude follows]

&lt;span class="gu"&gt;## Rules&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; [Hard constraints]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Three tips:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write instructions TO Claude, not documentation ABOUT a process&lt;/li&gt;
&lt;li&gt;Include trigger phrases in the description&lt;/li&gt;
&lt;li&gt;Keep each skill focused — one workflow, one skill&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Part 6: Advanced Patterns — Hooks, Subagents, Worktrees
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Hooks — Automated Quality Gates
&lt;/h3&gt;

&lt;p&gt;Hooks run automatically at lifecycle events. Two types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;prompt&lt;/code&gt;&lt;/strong&gt; — Claude evaluates a condition and acts (block, remind, warn)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;command&lt;/code&gt;&lt;/strong&gt; — Runs a shell command directly (always executes)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PreToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"If this is a git commit, verify lint and type-check have been run. If not, block with: 'Run lint and type-check before committing.'"&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;"PostToolUse"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Write|Edit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"If the file is TypeScript and more than 3 TS files have been edited since the last type-check, remind: 'Consider running type-check before continuing.'"&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;"Notification"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"matcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"hooks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"notify-send 'Claude Code' 'Awaiting your input'"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Use hooks instead of CLAUDE.md for linting rules&lt;/strong&gt; — LLMs are expensive compared to linters. Hooks free up instruction budget for things only CLAUDE.md can teach.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subagents — Keeping Context Clean
&lt;/h3&gt;

&lt;p&gt;For complex tasks, spawn subagents to handle research, exploration, or parallel work. Subagents run in separate contexts and return distilled findings — keeping your main conversation focused on implementation.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Research&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Investigating a library before using it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Exploration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Understanding a large codebase section&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Parallel work&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Running independent analyses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Heavy debugging&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Deep investigation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code review&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multiple reviewers checking different aspects&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Many skills use subagents automatically. &lt;code&gt;/grill&lt;/code&gt; spawns review agents for different aspects. &lt;code&gt;/implement&lt;/code&gt; may spawn research agents for unfamiliar libraries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Worktrees — Isolated Development
&lt;/h3&gt;

&lt;p&gt;For feature work that needs isolation, use &lt;strong&gt;git worktrees&lt;/strong&gt;. Claude can create and manage worktrees, giving you a clean environment for each branch.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Benefit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multiple features&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Work on several branches simultaneously&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Parallel testing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Test approaches in isolated environments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Code review&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Review PR branches while keeping main stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hotfixes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fix production without interrupting feature work&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Part 7: Automation — Auto-Invocation &amp;amp; Memory
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Auto-Invocation — Skills That Trigger Themselves
&lt;/h3&gt;

&lt;p&gt;Both plugins and skills support &lt;strong&gt;auto-invocation&lt;/strong&gt; — Claude reads the skill's description and triggers it automatically when your request matches. You don't need to remember slash commands.&lt;/p&gt;

&lt;p&gt;To make auto-invocation reliable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Be specific with trigger phrases&lt;/strong&gt; — vague descriptions lead to false activations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Include negative triggers&lt;/strong&gt; — "Do NOT use for TDD projects — use /plan-tdd instead"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with natural language&lt;/strong&gt; — try different phrasings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example: With &lt;code&gt;/grill&lt;/code&gt; configured to trigger on "grill this", "poke holes", or "stress test this", simply saying &lt;em&gt;"poke holes in this spec"&lt;/em&gt; auto-triggers the workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Auto Memory — Cross-Session Intelligence
&lt;/h3&gt;

&lt;p&gt;Claude Code maintains a persistent memory directory per project at &lt;code&gt;~/.claude/projects/&amp;lt;project-path&amp;gt;/memory/&lt;/code&gt;. The &lt;code&gt;MEMORY.md&lt;/code&gt; file (first 200 lines) loads into every conversation automatically — no manual setup needed.&lt;/p&gt;

&lt;p&gt;Claude updates this file as it learns your project: architectural patterns, common mistakes, file paths that matter. The &lt;code&gt;/ship&lt;/code&gt; skill captures learnings at the end of each ticket automatically, so memory grows organically across sessions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 8: Putting It All Together
&lt;/h2&gt;

&lt;h3&gt;
  
  
  New Project Setup (5 Minutes)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Create &lt;code&gt;CLAUDE.md&lt;/code&gt; — commands, architecture, coding standards, PR review rules, critical gotchas&lt;/li&gt;
&lt;li&gt;Create &lt;code&gt;.claude/settings.example.json&lt;/code&gt; — project-specific deny list&lt;/li&gt;
&lt;li&gt;Optionally create &lt;code&gt;.claude/rules/*.md&lt;/code&gt; — project-specific patterns with glob matching&lt;/li&gt;
&lt;li&gt;Add to &lt;code&gt;.gitignore&lt;/code&gt;: &lt;code&gt;.claude/*&lt;/code&gt; and &lt;code&gt;!.claude/*.example*&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ensure &lt;code&gt;~/.claude/settings.json&lt;/code&gt; has the global security deny list&lt;/li&gt;
&lt;li&gt;Install plugins: &lt;code&gt;claude plugins:install context7 superpowers code-review feature-dev&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  How the Layers Work in Practice
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Read .env.local"             → Project deny  → BLOCKED
"pnpm add lodash"             → Ask rule      → Prompts you
"git push --force"            → Global deny   → BLOCKED (even with --dangerously-skip-permissions)
"Read src/components/App.vue" → Allow         → AUTO-APPROVED
"This test is failing"        → Auto-invoke   → debugging skill triggers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Quick Reference
&lt;/h3&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;Shared?&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/settings.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Global security (deny list)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.claude/CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Personal workflow preferences&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.claude/skills/*/SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Custom workflows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.claude/commands/*/SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Convenience shortcuts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.claude/rules/*.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;File-specific patterns (glob)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.claude/agents/*.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Domain specialists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugins&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Pre-built agents + MCP + skills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.claude/settings.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Team project permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.claude/rules/*.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Team-specific patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CLAUDE.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Team coding standards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.claude/projects/*/memory/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Cross-session memory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Layer your configuration by scope and shareability:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Global settings&lt;/strong&gt; protect your system — secrets, credentials, destructive commands&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project settings&lt;/strong&gt; protect project resources — config files, build artifacts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CLAUDE.md&lt;/strong&gt; encodes team standards (keep it under 200 lines — every low-value instruction degrades high-value ones)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rules&lt;/strong&gt; enforce file-specific patterns with glob matching — language conventions, gotchas&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agents&lt;/strong&gt; provide domain expertise — backend, frontend, AI, testing, operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plugins&lt;/strong&gt; add instant capabilities — code review, testing, documentation, MCP servers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills&lt;/strong&gt; encode workflows (multi-step) — &lt;strong&gt;Commands&lt;/strong&gt; handle shortcuts (single actions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hooks&lt;/strong&gt; automate quality gates — free up CLAUDE.md instruction slots&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subagents&lt;/strong&gt; keep context clean — offload research, exploration, parallel analysis&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Worktrees&lt;/strong&gt; enable isolated development — multiple branches without switching&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt; captures lessons learned — no reteaching the same thing twice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-invocation&lt;/strong&gt; makes skills trigger themselves — no slash commands needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The spec-to-ship workflow isn't bureaucracy. It's front-loading the thinking so implementation is mechanical. A grilled spec catches the bug that would have taken a day to debug. A phased plan prevents the "I changed 47 files and nothing type-checks" disaster.&lt;/p&gt;

&lt;p&gt;Configure once. Benefit on every session, every project, every team member.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Official:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.anthropic.com/en/docs/claude-code" rel="noopener noreferrer"&gt;Claude Code Documentation&lt;/a&gt; — Permissions, hooks, settings, skills&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/claude-code/blob/main/plugins/README.md" rel="noopener noreferrer"&gt;Plugins README&lt;/a&gt; — Plugin marketplace&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Community:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.humanlayer.dev/blog/writing-a-good-claude-md" rel="noopener noreferrer"&gt;Writing a Good CLAUDE.md — HumanLayer&lt;/a&gt; — Instruction budgets research&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aiorg.dev/blog/claude-code-hooks" rel="noopener noreferrer"&gt;Claude Code Hooks: Complete Guide — aiorg.dev&lt;/a&gt; — 20+ hook examples&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.eesel.ai/blog/claude-code-permissions" rel="noopener noreferrer"&gt;Claude Code Permissions Guide — eesel.ai&lt;/a&gt; — Security setup&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Let's Connect
&lt;/h2&gt;

&lt;p&gt;I'm always excited to hear about what you're building! If you found this guide helpful, have questions, or just want to share your Claude Code journey:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://encryptioner.github.io" rel="noopener noreferrer"&gt;encryptioner.github.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn&lt;/strong&gt;: &lt;a href="https://www.linkedin.com/in/mir-mursalin-ankur" rel="noopener noreferrer"&gt;Mir Mursalin Ankur&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/Encryptioner" rel="noopener noreferrer"&gt;@Encryptioner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X (Twitter)&lt;/strong&gt;: &lt;a href="https://twitter.com/AnkurMursalin" rel="noopener noreferrer"&gt;@AnkurMursalin&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical Writing&lt;/strong&gt;: &lt;a href="https://nerddevs.com/author/ankur/" rel="noopener noreferrer"&gt;Nerddevs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>code</category>
      <category>development</category>
    </item>
    <item>
      <title>Navigating Bangladesh's Software Industry: A Practical Guide for Developers in 2026</title>
      <dc:creator>Mir Mursalin Ankur</dc:creator>
      <pubDate>Tue, 20 Jan 2026 15:39:57 +0000</pubDate>
      <link>https://dev.to/mir_mursalin_ankur/navigating-bangladeshs-software-industry-a-practical-guide-for-developers-in-2026-22ob</link>
      <guid>https://dev.to/mir_mursalin_ankur/navigating-bangladeshs-software-industry-a-practical-guide-for-developers-in-2026-22ob</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.amazonaws.com%2Fuploads%2Farticles%2Fo6gvoqyf2h9teckk7njs.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.amazonaws.com%2Fuploads%2Farticles%2Fo6gvoqyf2h9teckk7njs.png" alt="avigating Bangladesh's Software Industry - Practical Guide for Developers" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bangladesh's software industry has grown rapidly over the past decade. From $1.3 billion in exports to &lt;strong&gt;2,650+ ICT companies&lt;/strong&gt; employing hundreds of thousands of developers, the sector has transformed from a niche outsourcing destination into a serious global technology hub. The country now boasts &lt;strong&gt;over 1 million active freelancers&lt;/strong&gt; (2026), making it a global freelancing powerhouse.&lt;/p&gt;

&lt;p&gt;But the landscape has changed dramatically. &lt;strong&gt;The entry-level market is saturated.&lt;/strong&gt; Senior talent is scarce. And the post-2024 reality—marked by internet shutdowns, tax policy uncertainty, and global competition—means the easy growth is over.&lt;/p&gt;

&lt;p&gt;This guide is about action—not analysis.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Brutal Truth: Too Many Developers, Not Enough Jobs
&lt;/h2&gt;

&lt;p&gt;Let's start with what's actually happening in Bangladesh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We have a massive oversupply problem.&lt;/strong&gt; Every year, hundreds of universities and colleges pump out thousands of computer science graduates. Add to that the hundreds of online training institutes, bootcamps, and YouTube tutorials creating "developers" in 3-6 months.&lt;/p&gt;

&lt;p&gt;The result? A glut of low-skill junior developers fighting for the same entry-level positions.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Experience Level&lt;/th&gt;
&lt;th&gt;Jobs Available&lt;/th&gt;
&lt;th&gt;Candidates&lt;/th&gt;
&lt;th&gt;Competition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Entry-level&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~12,000&lt;/td&gt;
&lt;td&gt;30,000+&lt;/td&gt;
&lt;td&gt;2.5x oversubscribed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mid-level&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~8,000&lt;/td&gt;
&lt;td&gt;20,000&lt;/td&gt;
&lt;td&gt;2.5x oversubscribed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Senior&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~3,000&lt;/td&gt;
&lt;td&gt;5,000&lt;/td&gt;
&lt;td&gt;1.7x oversubscribed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Specialized&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~1,000&lt;/td&gt;
&lt;td&gt;500&lt;/td&gt;
&lt;td&gt;0.5x (talent shortage)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; If you're doing what everyone else is doing (basic web development, tutorial-heavy learning, no specialization), you're competing with 2-3 other people for every job.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Is Happening: The Training Glut
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The University Problem
&lt;/h3&gt;

&lt;p&gt;Bangladesh has &lt;strong&gt;150+ universities&lt;/strong&gt; offering computer science and engineering programs. Many of these programs are outdated, teaching curriculums from 5-10 years ago. Students graduate knowing theory but unable to build real applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common issues:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Professors who haven't worked in the industry in decades&lt;/li&gt;
&lt;li&gt;Outdated technologies (teaching Java applets in 2026)&lt;/li&gt;
&lt;li&gt;Zero emphasis on practical projects&lt;/li&gt;
&lt;li&gt;No industry exposure or internships&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Online Course Explosion
&lt;/h3&gt;

&lt;p&gt;YouTube, Udemy, Coursera, and local platforms have made programming education accessible to everyone. But this has created a new problem: &lt;strong&gt;tutorial developers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Thousands of graduates complete 20+ tutorials but can't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build an original project without following instructions&lt;/li&gt;
&lt;li&gt;Debug their own code&lt;/li&gt;
&lt;li&gt;Write clean, maintainable code&lt;/li&gt;
&lt;li&gt;Explain technical decisions&lt;/li&gt;
&lt;li&gt;Work in a team environment&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Bootcamp Industry
&lt;/h3&gt;

&lt;p&gt;Dozens of "become a full-stack developer in 3 months" bootcamps promise quick careers. They teach students to copy-paste code, pass interviews, and get jobs. But these developers struggle when faced with real-world complexity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; Companies are flooded with resumes from graduates who look good on paper but can't deliver. They've stopped trusting fresh graduates and now require 2-3 years of experience—even for "entry-level" positions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The New Reality: Global Competition + AI
&lt;/h2&gt;

&lt;p&gt;Just when Bangladesh's oversupply problem was peaking, two global shifts made things harder:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Remote Work Saturation
&lt;/h3&gt;

&lt;p&gt;During COVID, remote work exploded. Bangladeshi developers could finally access international jobs. But this also meant they were now competing directly with developers from India, Pakistan, Eastern Europe, and the Philippines—all of whom offer similar rates.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. AI Coding Tools
&lt;/h3&gt;

&lt;p&gt;AI tools like ChatGPT, GitHub Copilot, and Claude have changed the entry-level equation globally. A Stanford study found that &lt;strong&gt;employment for software developers aged 22-25 dropped nearly 20% between 2022-2025&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this hits Bangladesh hard:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Entry-level tech hiring globally decreased 25% in 2024&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;70% of hiring managers believe AI can do the work of interns&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Companies asking: "Why hire a junior for $90K when GitHub Copilot costs $10?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tasks that junior developers used to do—writing boilerplate code, fixing bugs, writing test scripts—are now what AI tools handle well.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Career Paths That Actually Work
&lt;/h2&gt;

&lt;p&gt;Despite the challenges, there are still paths to a successful career. You just need to be strategic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path 1: Specialization Premium&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pick a high-value niche (embedded systems, AI/ML, security, DevOps)&lt;/li&gt;
&lt;li&gt;Become exceptional in it&lt;/li&gt;
&lt;li&gt;Charge 2-3x what generalist developers charge&lt;/li&gt;
&lt;li&gt;Competition: Low&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it works:&lt;/strong&gt; Specialized work is harder to automate and outsources can't easily replicate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Path 2: International Remote&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build skills for global market&lt;/li&gt;
&lt;li&gt;Work directly for US/European companies&lt;/li&gt;
&lt;li&gt;Earn in USD (3-5x local rates)&lt;/li&gt;
&lt;li&gt;Competition: Medium (requires English and self-marketing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it works:&lt;/strong&gt; Local market is saturated; global market has talent shortages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Path 3: Local Premium&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Join top outsourcing companies (Cefalo, Enosis, Optimizely, Cheq)&lt;/li&gt;
&lt;li&gt;Work your way up through established career ladders&lt;/li&gt;
&lt;li&gt;Earn local premium rates&lt;/li&gt;
&lt;li&gt;Competition: High (everyone wants these jobs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why it works:&lt;/strong&gt; These companies have established client relationships and can pay more&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The AI Reality: Don't Ignore It, Don't Overrate It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;85% of developers globally use AI tools&lt;/strong&gt;, but here's what most don't realize:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The challenges:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;46% distrust AI accuracy&lt;/strong&gt; (only 3% "highly trust" it)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;66% are frustrated by "AI solutions that are almost right, but not quite"&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;45% say debugging AI-generated code takes longer&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;67% spend more time debugging AI code&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;68% spend more time fixing security issues in AI code&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The reality:&lt;/strong&gt; AI makes you faster at routine tasks but can slow you down overall when you account for verification and debugging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What this means for you:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Learn AI tools&lt;/strong&gt; – GitHub Copilot, Cursor, ChatGPT, Claude, Gemini are industry standards&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;But don't depend on them&lt;/strong&gt; – Understand what the code is actually doing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus on what AI can't do&lt;/strong&gt; – Architecture, security, business logic, client communication&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Developers resist using AI for high-responsibility tasks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deployment and monitoring:&lt;/strong&gt; 76% don't use AI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project planning:&lt;/strong&gt; 69% don't use AI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's where your value lives.&lt;/p&gt;




&lt;h2&gt;
  
  
  Action Plan: Your First 5 Years
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Year 1: Foundations + Specialization Choice&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Months 1-6:&lt;/strong&gt; Pick your lane (don't try to do everything)

&lt;ul&gt;
&lt;li&gt;Web/Mobile? Generalist path, more competition&lt;/li&gt;
&lt;li&gt;Embedded Systems? Fewer competitors, higher pay&lt;/li&gt;
&lt;li&gt;AI/ML? Growing field, severe talent shortage&lt;/li&gt;
&lt;li&gt;DevOps/Cloud? Critical infrastructure, hard to find&lt;/li&gt;
&lt;li&gt;Security/Biometrics? Trust-sensitive, premium pay&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Months 7-12:&lt;/strong&gt; Build portfolio while learning

&lt;ul&gt;
&lt;li&gt;3-5 substantial projects (not tutorials)&lt;/li&gt;
&lt;li&gt;Learn AI tools—but don't depend on them&lt;/li&gt;
&lt;li&gt;Start freelancing on Upwork for real experience&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Year 2-3: First Job + Skill Deepening&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Target companies based on your chosen path&lt;/li&gt;
&lt;li&gt;Build real-world experience&lt;/li&gt;
&lt;li&gt;Start specializing deeper&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Salary target:&lt;/strong&gt; 30,000-50,000 taka/month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Year 3-5: Seniority or Pivot&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Option A:&lt;/strong&gt; Grow in current company toward senior roles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Option B:&lt;/strong&gt; Jump to higher-paying company&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Option C:&lt;/strong&gt; Go full remote international&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Salary target:&lt;/strong&gt; 80,000-150,000+ taka/month&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Specialization Deep Dives
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Embedded Systems &amp;amp; IoT&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why pays premium:&lt;/strong&gt; Specialized knowledge, fewer engineers, global talent shortage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Market demand:&lt;/strong&gt; ~2,921 embedded systems job openings on major job portals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key employers:&lt;/strong&gt; Samsung R&amp;amp;D Institute Bangladesh, Teton Electronics, BJIT Embedded, SIMEC, Subra Systems, Ninos IT Solution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills needed:&lt;/strong&gt; C/C++, firmware development, microcontrollers, RTOS, hardware-software integration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Salary range:&lt;/strong&gt; 60,000-180,000+ taka/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to expertise:&lt;/strong&gt; 12-18 months&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AI/ML &amp;amp; Data Science&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why pays premium:&lt;/strong&gt; Global talent shortage, high value work&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills needed:&lt;/strong&gt; Python, TensorFlow/PyTorch, SQL, statistics, MLOps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Salary range:&lt;/strong&gt; 100,000-200,000+ taka/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to expertise:&lt;/strong&gt; 18-24 months&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DevOps &amp;amp; Cloud&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why pays premium:&lt;/strong&gt; Critical infrastructure, hard to find qualified people&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills needed:&lt;/strong&gt; Linux, AWS/Azure/GCP, Docker, Kubernetes, CI/CD, scripting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Salary range:&lt;/strong&gt; 120,000-250,000+ taka/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to expertise:&lt;/strong&gt; 12-18 months&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security &amp;amp; Biometrics&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Why pays premium:&lt;/strong&gt; Trust-sensitive roles, regulatory requirements&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Key employers:&lt;/strong&gt; Tiger IT, security roles at banks/telecom&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skills needed:&lt;/strong&gt; Cryptography, network security, ethical hacking, compliance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Salary range:&lt;/strong&gt; 90,000-150,000+ taka/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time to expertise:&lt;/strong&gt; 12-18 months&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Targeting the Right Companies
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tier 1: International Outsourcing (Premium Pay)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cefalo:&lt;/strong&gt; Norwegian market, 65,000-220,000 taka/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enosis:&lt;/strong&gt; US/Europe clients, 50,000-220,000 taka/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optimizely BD:&lt;/strong&gt; US company, 70,000-300,000 taka/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheq BD:&lt;/strong&gt; US cybersecurity, 70,000-300,000 taka/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tero Labs:&lt;/strong&gt; 120,000-150,000 taka/month&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tier 2: Top Local Companies&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Brain Station 23:&lt;/strong&gt; 60,000-220,000 taka/month (800+ employees)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BJIT:&lt;/strong&gt; Japanese market, 30,000-220,000 taka/month (80% work from Japan)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selise:&lt;/strong&gt; Swiss-Bangladeshi operations (~$37.9M revenue)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tier 3: High-Growth Startups&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pathao:&lt;/strong&gt; 65,000-180,000 taka/month + equity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chaldal:&lt;/strong&gt; 70,000-180,000 taka/month&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ShopUp, Shikho, Arogga:&lt;/strong&gt; Competitive packages + equity upside&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Salary Breakdown: The Real Numbers (2024-2025)
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Salary Range (BDT/month)&lt;/th&gt;
&lt;th&gt;Salary Range (USD/month)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trainee/Intern&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0-15,000&lt;/td&gt;
&lt;td&gt;$0-135&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Junior Developer&lt;/strong&gt; (0-2 years)&lt;/td&gt;
&lt;td&gt;20,000-45,000&lt;/td&gt;
&lt;td&gt;$180-405&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Software Engineer&lt;/strong&gt; (2-5 years)&lt;/td&gt;
&lt;td&gt;50,000-80,000&lt;/td&gt;
&lt;td&gt;$450-720&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Senior Engineer&lt;/strong&gt; (5+ years)&lt;/td&gt;
&lt;td&gt;100,000-220,000&lt;/td&gt;
&lt;td&gt;$900-1,980&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tech Lead/Principal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;165,000-300,000&lt;/td&gt;
&lt;td&gt;$1,485-2,700&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The Remote Work Premium:&lt;/strong&gt; Engineers working remotely for international companies earn substantially more (~345,000 taka/month average)—2-4x higher than local salaries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Training: What Actually Works in 2026
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;For Foundations:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BITM (BASIS):&lt;/strong&gt; Industry-aligned, most credible for placement&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;University CS programs:&lt;/strong&gt; Top-tier only (BUET, DU, KUET, RUET, NSU, BRAC, AIUB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poridhi.io:&lt;/strong&gt; Bangla-language, AI/ML tracks, 250+ hands-on labs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;For Practical Skills:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Build real projects, not tutorials&lt;/strong&gt; – This is the #1 differentiator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub portfolio&lt;/strong&gt; – Quality over quantity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freelancing on Upwork&lt;/strong&gt; – Real client experience&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open source contributions&lt;/strong&gt; – Demonstrates collaboration skills&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AI Tools (Learn, Don't Depend):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Copilot, Claude, Gemini, or Cursor&lt;/strong&gt; – Industry standard tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practice validation&lt;/strong&gt; – Review AI code for security and correctness&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Understand first, then automate&lt;/strong&gt; – Strong fundamentals matter more than AI skills&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Red Flags:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Courses that only teach syntax without projects&lt;/li&gt;
&lt;li&gt;"Become a full-stack developer in 3 months" claims&lt;/li&gt;
&lt;li&gt;No portfolio/real-world application&lt;/li&gt;
&lt;li&gt;Instructors who haven't worked in the industry&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"AI will replace developers"&lt;/strong&gt; – No, it won't. But developers who use AI will replace those who don't.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common Mistakes to Avoid
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1:&lt;/strong&gt; Tutorial addiction&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem:&lt;/strong&gt; You've completed 20 tutorials but can't build anything original&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; After learning basics, stop tutorials and build original projects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2:&lt;/strong&gt; Jack of all trades&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem:&lt;/strong&gt; You know a little of everything, expert in nothing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Pick one stack and go deep&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3:&lt;/strong&gt; Ignoring the market reality&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem:&lt;/strong&gt; "If I learn MERN stack, I'll get a job"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Look at what companies actually need. 30,000+ people know MERN. Fewer know embedded systems or DevOps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4:&lt;/strong&gt; Relying on AI too much&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem:&lt;/strong&gt; Copy-pasting AI code without understanding it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Always validate. Security vulnerabilities in AI code are common.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mistake 5:&lt;/strong&gt; Waiting for the "perfect job"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem:&lt;/strong&gt; 6 months unemployed, waiting for top company&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Take the good-enough job, build skills, move up later&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Mistake 6:&lt;/strong&gt; No specialization&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem:&lt;/strong&gt; Doing what everyone else is doing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Pick a niche. Embedded systems, security, DevOps, AI/ML—anything but generic web development.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Where the Industry Is Headed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short-term (2026-2027):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Post-2024 stabilization (after internet shutdown, white paper, tax changes)&lt;/li&gt;
&lt;li&gt;Entry-level jobs remain tough; senior/specialized roles in high demand&lt;/li&gt;
&lt;li&gt;AI adoption accelerates—teams that use AI effectively will win&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Medium-term (2027-2029):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Developer role transforms from "coder" to "problem solver"&lt;/li&gt;
&lt;li&gt;Companies that don't hire juniors will eventually have no seniors&lt;/li&gt;
&lt;li&gt;New career pathways emerge for AI-augmented developers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Long-term (2029-2031):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Either: Software becomes a top export category ($5B+)&lt;/li&gt;
&lt;li&gt;Or: Stagnation at current levels ($1-2B)&lt;/li&gt;
&lt;li&gt;Difference depends on: Investment + Education quality + Policy stability&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Bangladesh's software industry is at an inflection point. The easy growth is over. The market is saturated with junior developers. AI has disrupted entry-level jobs globally. But this isn't the end—it's a transformation.&lt;/p&gt;

&lt;p&gt;The developers who thrive in 2026 and beyond will be the ones who:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Specialize&lt;/strong&gt; in areas where there's actual demand&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build genuine skills&lt;/strong&gt;—not tutorial certificates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use AI tools&lt;/strong&gt; without depending on them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Think architecturally&lt;/strong&gt;, not just syntactically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communicate effectively&lt;/strong&gt; (English + technical explanation)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The free ride ended in 2024. What comes next depends on the choices you make.&lt;/p&gt;

&lt;p&gt;For those who build genuine skills and create real value: The opportunity is substantial.&lt;/p&gt;

&lt;p&gt;For those who want shortcuts: The door is closing.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Choose wisely.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Learning Platforms:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://poridhi.io" rel="noopener noreferrer"&gt;Poridhi.io&lt;/a&gt; - Bangla Software Engineering Courses&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://bitm.org.bd" rel="noopener noreferrer"&gt;BITM - BASIS Institute of Technology &amp;amp; Management&lt;/a&gt; - Industry-aligned training&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.coursera.org" rel="noopener noreferrer"&gt;Coursera&lt;/a&gt; - University-level courses&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.edx.org" rel="noopener noreferrer"&gt;edX&lt;/a&gt; - MIT, Harvard courses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AI Coding Tools (Learn, Don't Depend):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/features/copilot" rel="noopener noreferrer"&gt;GitHub Copilot&lt;/a&gt; - Industry standard&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cursor.sh" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt; - AI-native IDE&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://chat.openai.com" rel="noopener noreferrer"&gt;ChatGPT&lt;/a&gt; - Code review and explanation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://claude.ai" rel="noopener noreferrer"&gt;Claude&lt;/a&gt; - Advanced coding assistant with long context&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gemini.google.com/" rel="noopener noreferrer"&gt;Gemini&lt;/a&gt; - Google's AI assistant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Job Platforms:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.upwork.com" rel="noopener noreferrer"&gt;Upwork&lt;/a&gt; - Freelance work&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.toptal.com" rel="noopener noreferrer"&gt;Toptal&lt;/a&gt; - Elite freelance network&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.turing.com" rel="noopener noreferrer"&gt;Turing&lt;/a&gt; - Remote US jobs&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.facebook.com/groups/1336177993759909/" rel="noopener noreferrer"&gt;CSE Job Bangladesh Facebook Group&lt;/a&gt; - Local jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Career Development:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.linkedin.com" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; - Build your professional profile&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; - Showcase your code&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://stackoverflow.com" rel="noopener noreferrer"&gt;Stack Overflow&lt;/a&gt; - Learn and help others&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Bangladesh Industry Data (2026):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://vivasoftltd.com/best-software-companies-in-bangladesh/" rel="noopener noreferrer"&gt;Vivasoft: Top 20 Best Software Companies in Bangladesh 2026&lt;/a&gt; (January 6, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.ontiktechnology.com/blog/top-software-companies-in-bangladesh" rel="noopener noreferrer"&gt;Ontik Technology: Top 25 Software Companies in Bangladesh 2026&lt;/a&gt; (January 7, 2026)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.nextzen.com.bd/blog/top-10-best-software-companies-in-bangladesh-2026" rel="noopener noreferrer"&gt;Nextzen: Top 10 Best Software Companies in Bangladesh 2026&lt;/a&gt; (January 10, 2026)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kaz.com.bd/blog/top-ai-developers-in-bangladesh-a-practical-guide-for-global-businesses-2026" rel="noopener noreferrer"&gt;Kaz Software: Top AI Developers in Bangladesh 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vocal.media/journal/the-rise-of-freelancing-in-bangladesh-shaping-the-digital-economy-in-2026" rel="noopener noreferrer"&gt;Vocal Media: The Rise of Freelancing in Bangladesh 2026&lt;/a&gt; - Over 1 million active freelancers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AI Impact &amp;amp; Job Market Data:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://digitaleconomy.stanford.edu/app/uploads/2025/11/CanariesintheCoalMine_Nov25.pdf" rel="noopener noreferrer"&gt;Stanford Digital Economy Study: Canaries in the Coal Mine (2025)&lt;/a&gt; - 20% decline for devs aged 22-25&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.cio.com/article/4062024/demand-for-junior-developers-softens-as-ai-takes-over.html" rel="noopener noreferrer"&gt;CIO: Demand for Junior Developers Softens as AI Takes Over&lt;/a&gt; - Entry-level hiring down 25% YoY&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://survey.stackoverflow.co/2025/ai" rel="noopener noreferrer"&gt;Stack Overflow Developer Survey 2025 - AI Section&lt;/a&gt; - 84% adoption, sentiment data&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.weforum.org/stories/2026/01/software-developers-ai-work/" rel="noopener noreferrer"&gt;World Economic Forum: Software Developers and AI Work (January 2026)&lt;/a&gt; - Learning priorities and trends&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://devops.com/survey-sees-wider-adoption-of-ai-coding-tools-creating-more-devops-challenges/" rel="noopener noreferrer"&gt;DevOps.com: AI Coding Tools Survey (January 2026)&lt;/a&gt; - Daily usage statistics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Salary &amp;amp; Company Data:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tahanima.github.io/salary-ranges-offered-by-bangladeshi-software-companies-for-different-positions/" rel="noopener noreferrer"&gt;Salary ranges offered by Bangladeshi Software Companies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ibos.io/best-software-companies-in-bangladesh-2026/" rel="noopener noreferrer"&gt;Best Software Companies in Bangladesh 2026 | iBOS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This guide is based on publicly available information and industry analysis as of January 2026. Specific situations vary by individual skills, location, and market conditions.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  End
&lt;/h2&gt;

&lt;p&gt;That's all!&lt;/p&gt;

&lt;p&gt;I hope you've found the article useful. I'll write more about the software engineering, career opportunities and overall ecosystem. Feel free to share your thoughts.&lt;/p&gt;

&lt;p&gt;Check more on&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://encryptioner.github.io" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/mir-mursalin-ankur" rel="noopener noreferrer"&gt;Linkedin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Encryptioner" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/AnkurMursalin" rel="noopener noreferrer"&gt;X (Twitter)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nerddevs.com/author/ankur/" rel="noopener noreferrer"&gt;Nerddevs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>career</category>
      <category>softwaredevelopment</category>
      <category>bangladesh</category>
      <category>developers</category>
    </item>
    <item>
      <title>Inside Bangladesh's Software Industry: Companies, Models, and Opportunities</title>
      <dc:creator>Mir Mursalin Ankur</dc:creator>
      <pubDate>Thu, 15 Jan 2026 19:19:47 +0000</pubDate>
      <link>https://dev.to/mir_mursalin_ankur/inside-bangladeshs-software-industry-companies-models-and-opportunities-5a2c</link>
      <guid>https://dev.to/mir_mursalin_ankur/inside-bangladeshs-software-industry-companies-models-and-opportunities-5a2c</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.amazonaws.com%2Fuploads%2Farticles%2Fpjd7m2q1k91if6b4lnvl.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.amazonaws.com%2Fuploads%2Farticles%2Fpjd7m2q1k91if6b4lnvl.png" alt="Inside Bangladesh's Software Industry: Companies, Models, and Opportunities" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you think about software outsourcing destinations, Bangladesh probably isn't the first name that comes to mind. But it should be.&lt;/p&gt;

&lt;p&gt;Beside the &lt;strong&gt;Bay of Bengal&lt;/strong&gt;, this country of 170 million people has built a diverse software ecosystem with companies doing remarkable work—from government systems that serve millions to products used by British Council and ACCA for global examinations.&lt;/p&gt;

&lt;p&gt;Let me walk you through the companies, what they actually do, and the business models that work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Industry at a Glance
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Current State (2024-2025):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;$1.3 billion+&lt;/strong&gt; in annual ICT exports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;$840 million&lt;/strong&gt; in official software export revenue (FY 2023-24)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;4,500+&lt;/strong&gt; registered IT/software companies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;400,000+&lt;/strong&gt; professionals employed&lt;/li&gt;
&lt;li&gt;Exports to &lt;strong&gt;80+ countries&lt;/strong&gt; worldwide&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Growth:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;From $26 million in 2008 to over $1 billion today—a &lt;strong&gt;40x increase&lt;/strong&gt; in 16 years&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Bangladesh's Software Industry Exists
&lt;/h2&gt;

&lt;p&gt;Bangladesh has pulled off something remarkable over the past fifteen years. The government made a deliberate bet on IT services as an export industry, offering tax breaks, building infrastructure, and pushing digitization across public services. The result? An industry that went from essentially nothing to generating over a billion dollars annually.&lt;/p&gt;

&lt;p&gt;Here's what makes the model work: Bangladesh offers highly skilled English-speaking developers at competitive rates that deliver exceptional value. A senior software engineer in Dhaka might earn 100,000+ taka monthly (roughly $900+)—professional compensation that enables access to world-class engineering talent at reasonable investment levels.&lt;/p&gt;

&lt;p&gt;For American or European companies, the value proposition is compelling. You're not "saving money"—you're accessing talented engineers who bring strong technical skills, problem-solving abilities, and dedication to building quality products. The time zone works perfectly too—Bangladesh is well-positioned for both European and US clients, enabling real-time collaboration when needed.&lt;/p&gt;

&lt;p&gt;This created multiple distinct types of software businesses beyond just traditional outsourcing. Let's break them down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Complete Business Model Breakdown
&lt;/h2&gt;

&lt;p&gt;Looking across Bangladesh's software landscape, you can categorize companies into &lt;strong&gt;seven main business models&lt;/strong&gt;—not just the two or three most people talk about:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Business Model&lt;/th&gt;
&lt;th&gt;% of Industry&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Top Companies&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pure Outsourcing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;td&gt;Provide developers/teams to international clients on time-and-materials or fixed-price projects&lt;/td&gt;
&lt;td&gt;Brain Station 23, BJIT, Cefalo, Enosis, Vivasoft&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Government Contracts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;Specialize in public sector projects, national infrastructure, digitization initiatives&lt;/td&gt;
&lt;td&gt;Datasoft, Tiger IT, OrangeBD, Tappware, SoftBD&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Product Companies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;td&gt;Build and sell own software (SaaS, licenses, marketplace products)&lt;/td&gt;
&lt;td&gt;BDTask, Ollyo, REVE Systems, NerdDevs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hybrid Models&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8%&lt;/td&gt;
&lt;td&gt;Combine client work with own products for diversification&lt;/td&gt;
&lt;td&gt;Brain Station 23, NerdDevs, Kaz Software&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tech Startups&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;VC-backed growth companies building consumer/enterprise platforms&lt;/td&gt;
&lt;td&gt;bKash, Pathao, ShopUp, Chaldal, Shikho&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Embedded Systems&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5%&lt;/td&gt;
&lt;td&gt;Firmware, IoT, hardware-software integration for electronics/devices&lt;/td&gt;
&lt;td&gt;Samsung R&amp;amp;D, BJIT Embedded, Teton Electronics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Freelance-to-Company&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;6%&lt;/td&gt;
&lt;td&gt;Companies that started as freelancers on Upwork/Fiverr and grew into agencies&lt;/td&gt;
&lt;td&gt;Hundreds of small agencies across Dhaka/Chittagong&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Digital Services&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;4%&lt;/td&gt;
&lt;td&gt;Software + SEO, content marketing, social media management&lt;/td&gt;
&lt;td&gt;Bizcope, Red Sparrow Digital, HYPE DHAKA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let's dive into each category with the major players you need to know.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Government Contract Giants
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Datasoft Systems Bangladesh Limited
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://datasoftbd.com" rel="noopener noreferrer"&gt;https://datasoftbd.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Founded:&lt;/strong&gt; 1998&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Employees:&lt;/strong&gt; 300+&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certification:&lt;/strong&gt; CMMI Level 5 (first in Bangladesh)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Datasoft was the first mover in Bangladesh's software industry, and they focused on something many others avoided—government contracts. That turned out to be prescient.&lt;/p&gt;

&lt;p&gt;Walk into any government office in Bangladesh and there's a decent chance you'll encounter Datasoft's work. They built the national ID system, the e-passport system, biometric registration systems for multiple agencies, voter registration platforms, and border control infrastructure. These aren't small projects—they're nation-building scale, often worth tens or hundreds of millions of taka.&lt;/p&gt;

&lt;p&gt;Their flagship commercial product, &lt;strong&gt;Microfin 360&lt;/strong&gt; (launched 2008), targets microfinance institutions and NGOs. Given Bangladesh's massive microfinance sector (pioneered by Grameen Bank), this was smart positioning. The product has since expanded across the region to Nepal, Bhutan, Canada, and India.&lt;/p&gt;

&lt;p&gt;What Datasoft understood early was that government contracts, while bureaucratic and sometimes slow-paying, provide stable long-term revenue. Once you're embedded in a ministry's infrastructure, replacement becomes politically and technically difficult. That stickiness matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Salary Range:&lt;/strong&gt; 30,000-180,000 taka/month+ for software engineers&lt;/p&gt;

&lt;h3&gt;
  
  
  Tiger IT Bangladesh Limited
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://www.tigerit.com" rel="noopener noreferrer"&gt;https://www.tigerit.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Founded:&lt;/strong&gt; 2006&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Employees:&lt;/strong&gt; 100+&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specialization:&lt;/strong&gt; Biometric and identity management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Datasoft is the generalist in government contracts, Tiger IT is the specialist. They focus almost exclusively on security, identity management, and biometric systems—the stuff that requires security clearances and deep technical expertise.&lt;/p&gt;

&lt;p&gt;This is the company government agencies call when they need fingerprint identification systems, automated fingerprint identification systems (AFIS), facial recognition, or secure document management. Their client list includes defense and security agencies, which means many of their projects aren't publicly discussed.&lt;/p&gt;

&lt;p&gt;Tiger IT has carved out a defensible niche. Biometric systems aren't something you can easily commodity outsource—they require specialized knowledge, security protocols, and trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Salary Range:&lt;/strong&gt; 90,000-180,000 taka/month+ for software engineers&lt;/p&gt;

&lt;h3&gt;
  
  
  The Government Contract Controversy
&lt;/h3&gt;

&lt;p&gt;No discussion of Bangladesh's software industry is complete without addressing the elephant in the room: government procurement controversies.&lt;/p&gt;

&lt;p&gt;Between 2019 and 2024, the a2i (Aspire to Innovate) project—a major government digitalization initiative supported by UNDP—procured software through 46 separate contracts. An investigation found that just four companies received 23 of those 46 contracts (50%).&lt;/p&gt;

&lt;p&gt;The companies were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tappware Solutions:&lt;/strong&gt; 6 contracts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SoftBD:&lt;/strong&gt; 6 contracts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business Automation Limited:&lt;/strong&gt; 6 contracts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OrangeBD&lt;/strong&gt; (Orange IT): 5 contracts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Allegations emerged of a "syndicate" where procurement conditions were allegedly tailor-made to benefit specific companies. This isn't unique to Bangladesh—government tech procurement is notoriously prone to corruption globally. But it illustrates why some companies focus intensely on government contracts while others avoid them entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  The International Outsourcing Leaders
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Brain Station 23
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://brainstation-23.com" rel="noopener noreferrer"&gt;https://brainstation-23.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Founded:&lt;/strong&gt; 2006&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Employees:&lt;/strong&gt; 800+ (700+ software engineers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certifications:&lt;/strong&gt; CMMI Level 3, ISO 9001, ISO 27001
&lt;strong&gt;Projects Completed:&lt;/strong&gt; 2,000+&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you had to pick one company that represents Bangladesh's software ambitions, it's probably Brain Station 23. They're the largest by headcount, the most diverse in services, and have the strongest international presence with offices in the USA, UK, Australia, Nigeria, and across Europe and the Middle East.&lt;/p&gt;

&lt;p&gt;Brain Station 23 does everything. Banking and fintech (they built CityTouch for City Bank, which processes over $3 billion in transactions), e-commerce platforms (the Shwapno supermarket system), ERP implementations (official Odoo partner), learning management systems (Moodle partner with their own Proctoring Pro product), AI and machine learning solutions, AR/VR, mobile apps—the list goes on.&lt;/p&gt;

&lt;p&gt;Their client roster reads like a who's who: PayPal, Nissan, British Telecom, Unilever, City Bank, AB Bank, HSBC Bangladesh, MetLife, Grameenphone, Robi, BAT Bangladesh, and multiple pharmaceutical companies.&lt;/p&gt;

&lt;p&gt;What makes Brain Station 23 interesting is their &lt;strong&gt;hybrid model&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~50% international outsourcing&lt;/li&gt;
&lt;li&gt;~30% own product development (like Intellifriend, their AI time-tracking app)&lt;/li&gt;
&lt;li&gt;~20% local enterprise solutions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Salary Range:&lt;/strong&gt; 60,000-220,000 taka/month+ for software engineers&lt;/p&gt;

&lt;h3&gt;
  
  
  BJIT Group
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://bjitgroup.com" rel="noopener noreferrer"&gt;https://bjitgroup.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Founded:&lt;/strong&gt; 2007&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Employees:&lt;/strong&gt; 800+&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specialization:&lt;/strong&gt; Japanese market&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;BJIT took a different approach—instead of trying to serve everyone, they became the go-to partner for Japanese companies. About 80% of their work comes from Japan, with the rest split between Singapore and other markets.&lt;/p&gt;

&lt;p&gt;This specialization makes sense. The Japanese market is huge, culturally specific, and historically underserved by South Asian outsourcing. Companies like BJIT that invest in understanding Japanese business culture, communication styles, and quality expectations can command premium rates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Salary Range:&lt;/strong&gt; 30,000-220,000 taka/month+ for software engineers&lt;/p&gt;

&lt;h3&gt;
  
  
  Cefalo Bangladesh Ltd.
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://www.cefalo.com" rel="noopener noreferrer"&gt;https://www.cefalo.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specialization:&lt;/strong&gt; Norwegian market&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Like BJIT with Japan, Cefalo focused almost exclusively on Norway. They provide full-time capable developers to Norwegian businesses and have built a reputation as one of the best workplaces in Bangladesh.&lt;/p&gt;

&lt;p&gt;The salaries reflect Norwegian client budgets:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Salary Range:&lt;/strong&gt; 65,000-220,000 taka/month+ (senior engineers can earn 185,000-220,000 taka)&lt;/p&gt;

&lt;p&gt;That senior engineer salary is exceptional for Bangladesh—roughly $1,700-$2,000/month. Competitive value for Norwegian clients while representing top-tier local compensation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enosis Solutions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://enosisbd.com" rel="noopener noreferrer"&gt;https://enosisbd.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Founded:&lt;/strong&gt; 2010&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Employees:&lt;/strong&gt; 200+&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus:&lt;/strong&gt; US and European markets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enosis went the American and European route, focusing on web and mobile application development, cloud solutions, and IT consulting using agile methodologies. About 70% of revenue comes from US clients, 20% from Europe, 10% elsewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Salary Range:&lt;/strong&gt; 50,000-220,000 taka/month+ (well-structured career ladder with clear levels)&lt;/p&gt;

&lt;h3&gt;
  
  
  Vivasoft Limited
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://vivasoftltd.com" rel="noopener noreferrer"&gt;https://vivasoftltd.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Founded:&lt;/strong&gt; 2010&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Employees:&lt;/strong&gt; 300+ (180+ dedicated engineers)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus:&lt;/strong&gt; Team augmentation and offshore office setup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Vivasoft positioned themselves uniquely—instead of just doing custom development, they focus on team augmentation (providing dedicated developers to extend existing teams) and helping companies set up full offshore offices in Bangladesh.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Salary Range:&lt;/strong&gt; 25,000-220,000 taka/month+ for software engineers&lt;/p&gt;

&lt;h3&gt;
  
  
  Selise Digital Platforms
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://selisegroup.com" rel="noopener noreferrer"&gt;https://selisegroup.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revenue:&lt;/strong&gt; ~$37.9 million&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Headquarters:&lt;/strong&gt; Zürich, Switzerland (with Bangladesh operations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ranking:&lt;/strong&gt; #2 Top Software Company in Bangladesh 2025&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Selise is a global IT delivery organization with significant Bangladesh operations. They specialize in advanced computing, AI, data science, and digital platform creation. As a "Swiss tech partner for digital transformation," they bring European quality standards to Bangladeshi talent.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Product Companies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  NerdDevs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://nerddevs.com" rel="noopener noreferrer"&gt;https://nerddevs.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Product Website:&lt;/strong&gt; &lt;a href="https://biddaan.com" rel="noopener noreferrer"&gt;https://biddaan.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Founded:&lt;/strong&gt; 2015&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Employees:&lt;/strong&gt; 20+ (small but impactful)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specialization:&lt;/strong&gt; EdTech and messaging solutions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NerdDevs represents a different model—staying small, focusing intensely on specific problems, and building both client work and own products simultaneously.&lt;/p&gt;

&lt;p&gt;Their flagship partnership achievement is &lt;strong&gt;TestReach&lt;/strong&gt;, an online assessment platform that ACCA (Association of Chartered Certified Accountants), British Council, CIPS, and iSQI use for global examinations. If you've taken a professional certification exam in the UK, there's a decent chance NerdDevs wrote part of the software.&lt;/p&gt;

&lt;p&gt;They also built &lt;strong&gt;Genius&lt;/strong&gt; and other messaging apps, which are mostly used in the US market that handles 4-5 million messages monthly—managing that volume reliably requires serious engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For the Bangladesh market, their major products include:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Biddaan&lt;/strong&gt; (&lt;a href="https://biddaan.com" rel="noopener noreferrer"&gt;https://biddaan.com&lt;/a&gt;) - Their flagship Learning Management System specifically designed for Bangladeshi &amp;amp; International educational institutions. It handles student tracking, progress notes, institution management, and the full administrative stack educational organizations need. The interface is multilingual, which matters—most LMS solutions are English-only, creating barriers for local schools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Salary Range:&lt;/strong&gt; 30,000-220,000 taka/month+ for software engineers&lt;/p&gt;

&lt;h3&gt;
  
  
  BDTask Limited
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://www.bdtask.com" rel="noopener noreferrer"&gt;https://www.bdtask.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus:&lt;/strong&gt; Ready-made software products&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While most Bangladeshi companies do client work, BDTask went the opposite direction—they build ready-made software and sell it globally through marketplaces like Envato.&lt;/p&gt;

&lt;p&gt;Their product catalog includes accounting software, inventory management, POS systems, school management, hospital management, hotel management, restaurant management, HR and payroll systems—basically every business management need you can think of.&lt;/p&gt;

&lt;p&gt;This is a different business model entirely. Instead of getting paid for hours worked or projects completed, they sell licenses—think $50-$500 per copy, with customers in Europe, North America, and Southeast Asia buying directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ollyo (MuktoSoft)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://ollyo.com" rel="noopener noreferrer"&gt;https://ollyo.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Founded:&lt;/strong&gt; 2010&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus:&lt;/strong&gt; WordPress and Joomla ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ollyo took a similar product approach but focused narrowly on WordPress plugins, themes, Joomla extensions, and open-source tools. They're an Envato partner, meaning their products sell through Envato's massive marketplace.&lt;/p&gt;

&lt;p&gt;Products include WordPress plugins, Joomla templates, no-code website builders, e-learning platforms, e-commerce solutions, and a free open-source icon library. Some of their work is used by thousands of websites globally.&lt;/p&gt;

&lt;h3&gt;
  
  
  REVE Systems Ltd.
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://www.revechat.com" rel="noopener noreferrer"&gt;https://www.revechat.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus:&lt;/strong&gt; VoIP and communications platform software&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;REVE Systems built telecommunications software—VoIP platforms, communication tools, and enterprise communications solutions used globally. This is highly specialized work requiring understanding of telecommunications protocols, real-time communication, and handling large-scale voice/video traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Salary Range:&lt;/strong&gt; 50,000-150,000 taka/month+ for software engineers&lt;/p&gt;




&lt;h2&gt;
  
  
  The Embedded Systems &amp;amp; IoT Specialists
&lt;/h2&gt;

&lt;p&gt;This is a specialized but critical segment of Bangladesh's software industry—companies working on firmware, embedded systems, IoT solutions, and hardware-software integration. These engineers bridge the gap between software code and physical devices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Samsung R&amp;amp;D Institute Bangladesh (SRBD)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://research.samsung.com/srbd" rel="noopener noreferrer"&gt;https://research.samsung.com/srbd&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Founded:&lt;/strong&gt; February 2011&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus:&lt;/strong&gt; Embedded systems, IoT, cellular modem technology&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Samsung chose Bangladesh for its first-ever R&amp;amp;D hub in the country—a strong endorsement of the local engineering talent. SRBD works on cutting-edge embedded systems including cellular modem systems, IoT hardware, and intelligent embedded devices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What they work on:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embedded software and systems engineering&lt;/li&gt;
&lt;li&gt;Cellular modem systems development&lt;/li&gt;
&lt;li&gt;IoT hardware and connectivity systems&lt;/li&gt;
&lt;li&gt;Firmware for Samsung's global product line&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; When Samsung—one of the world's largest electronics companies—sets up an R&amp;amp;D center in your country, it's a validation of your engineering talent. SRBD employs some of Bangladesh's most skilled embedded systems engineers working on products used globally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Teton Electronics / Teton Private Ltd.
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://tetonelectronics.com" rel="noopener noreferrer"&gt;https://tetonelectronics.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus:&lt;/strong&gt; Firmware development and embedded systems services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Teton Electronics provides comprehensive firmware development services, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Programming and updating embedded firmware&lt;/li&gt;
&lt;li&gt;Integration for different types of microcontrollers and processors&lt;/li&gt;
&lt;li&gt;Hardware-software integration solutions&lt;/li&gt;
&lt;li&gt;Custom embedded systems design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They work across various industries requiring specialized firmware solutions—from consumer electronics to industrial applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  BJIT Embedded Systems Division
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://www.bjit.co.jp" rel="noopener noreferrer"&gt;https://www.bjit.co.jp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus:&lt;/strong&gt; IoT &amp;amp; Embedded Engineering&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;BJIT, known for its Japanese market specialization, has a dedicated embedded systems division offering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IoT &amp;amp; Embedded Engineering&lt;/li&gt;
&lt;li&gt;Embedded Application Development&lt;/li&gt;
&lt;li&gt;Mechanical Design&lt;/li&gt;
&lt;li&gt;Hardware-software integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Their Japanese clients demand precision and reliability—qualities that have driven BJIT to develop strong embedded systems capabilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subra Systems Limited
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Specialization:&lt;/strong&gt; Embedded systems and industrial automation&lt;/p&gt;

&lt;p&gt;Subra Systems focuses on embedded solutions for industrial applications, where reliability and real-time performance are critical. They work on automation systems, control systems, and industrial IoT solutions.&lt;/p&gt;

&lt;h3&gt;
  
  
  SIMEC System Ltd.
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Specialization:&lt;/strong&gt; Embedded systems and firmware development&lt;/p&gt;

&lt;p&gt;SIMEC provides embedded systems services across various sectors, working on firmware design, implementation, and optimization for different hardware platforms.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ninos IT Solution
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://www.ninositsolution.com" rel="noopener noreferrer"&gt;https://www.ninositsolution.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specialization:&lt;/strong&gt; Cutting-edge embedded systems design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ninos IT Solution specializes in designing and developing embedded systems with seamless integration capabilities. They work on projects requiring deep hardware and software knowledge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Embedded Systems Job Market:&lt;/strong&gt;&lt;br&gt;
The demand for embedded systems engineers in Bangladesh is substantial, with approximately 2,921 embedded systems job openings listed on major job portals. Skills in high demand include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embedded software engineering&lt;/li&gt;
&lt;li&gt;IoT development&lt;/li&gt;
&lt;li&gt;LoRaWAN device development&lt;/li&gt;
&lt;li&gt;Firmware design and implementation&lt;/li&gt;
&lt;li&gt;Hardware-software integration&lt;/li&gt;
&lt;li&gt;Robotics and automation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Salary Ranges:&lt;/strong&gt; 60,000-180,000 taka/month+ for embedded systems engineers (higher due to specialized skills)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Embedded Systems Matter:&lt;/strong&gt;&lt;br&gt;
As the world becomes more connected with smart devices, IoT sensors, and intelligent electronics, embedded systems expertise becomes increasingly valuable. Bangladeshi companies are positioning themselves to serve this growing global market with skilled engineers who understand both hardware and software.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Tech Startup Scene (The Real Growth Engine)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  bKash
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Industry:&lt;/strong&gt; Mobile Financial Services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ranking:&lt;/strong&gt; #3 Top Startup in Bangladesh&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;bKash isn't just a startup—it's practically a utility. As Bangladesh's leading mobile financial services provider, bKash has transformed how millions of Bangladeshis send, receive, and store money. With international investment and attention, bKash has attracted significant funding and become one of Bangladesh's most successful tech exports.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pathao
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Founded:&lt;/strong&gt; 2015&lt;br&gt;
&lt;strong&gt;Founders:&lt;/strong&gt; Hussain Elius, Shifat Adnan, Fahad&lt;br&gt;
&lt;strong&gt;Ranking:&lt;/strong&gt; #6 Top Startup in Bangladesh&lt;/p&gt;

&lt;p&gt;Pathao is Bangladesh's "super-app" offering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ride-sharing services (bikes, cars, cycles)&lt;/li&gt;
&lt;li&gt;Food delivery&lt;/li&gt;
&lt;li&gt;Logistics and parcel delivery&lt;/li&gt;
&lt;li&gt;Created over &lt;strong&gt;300,000 jobs&lt;/strong&gt; in eight years&lt;/li&gt;
&lt;li&gt;Serving &lt;strong&gt;60,000 small businesses&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Over &lt;strong&gt;8 million users&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Salary Range:&lt;/strong&gt; 65,000-180,000 taka/month+ for software engineers&lt;/p&gt;

&lt;h3&gt;
  
  
  ShopUp
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Ranking:&lt;/strong&gt; #1 Startup in Bangladesh&lt;/p&gt;

&lt;p&gt;ShopUp focuses on B2B commerce platform, helping small retailers and businesses source inventory. As Bangladesh's top-ranked startup, ShopUp has attracted significant investor attention and represents the maturation of Bangladesh's startup ecosystem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chaldal
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Industry:&lt;/strong&gt; E-commerce grocery&lt;/p&gt;

&lt;p&gt;Chaldal has revolutionized grocery shopping in Bangladesh's major cities with its online grocery platform. They've built sophisticated logistics technology and inventory management systems that compete with international players.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Salary Range:&lt;/strong&gt; 70,000-180,000 taka/month+ for software engineers&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Notable Startups:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shikho&lt;/strong&gt; - EdTech platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arogga&lt;/strong&gt; - Healthcare/medicine delivery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PriyoShop Retail&lt;/strong&gt; - B2B retail platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shohoz&lt;/strong&gt; - Transportation and logistics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Startup Ecosystem Growth:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bangladesh's startup investment grew &lt;strong&gt;1,116% year-over-year&lt;/strong&gt; to &lt;strong&gt;$119.9 million in H1 2025&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;698+ top startups currently operating&lt;/li&gt;
&lt;li&gt;Middle class growing at 10% per annum to reach 34 million by 2025&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2024 Startup Investments That Matter:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sombhab&lt;/strong&gt; (tech startup): $1 million investment from Singapore's Cocoon Capital&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blue-Tech&lt;/strong&gt;: $100,000+ foreign investment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bangladesh Startup Summit 2024&lt;/strong&gt;: July 27-28 at Bangladesh Computer Council, theme "Smart Bangladesh, Limitless Possibilities"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AdTech Recognition&lt;/strong&gt;: 7 Bangladeshi EdTech startups ranked in South Asia's top 100&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Freelance-to-Company Model (Hidden Giants)
&lt;/h2&gt;

&lt;p&gt;This is the category nobody talks about but it's MASSIVE. Hundreds of software companies in Bangladesh started as individual freelancers on &lt;strong&gt;Upwork&lt;/strong&gt;, &lt;strong&gt;Fiverr&lt;/strong&gt;, &lt;strong&gt;Freelancer.com&lt;/strong&gt;, and &lt;strong&gt;oDesk&lt;/strong&gt; (now Upwork).&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Developer starts freelancing on Upwork/Fiverr while in university or early career&lt;/li&gt;
&lt;li&gt;Builds reputation, gets 5-star reviews, starts earning $2,000-$5,000/month&lt;/li&gt;
&lt;li&gt;Gets more work than they can handle alone&lt;/li&gt;
&lt;li&gt;Hires friends/junior developers&lt;/li&gt;
&lt;li&gt;Registers a company&lt;/li&gt;
&lt;li&gt;Continues taking contracts through platforms + direct clients&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Characteristics of these companies:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5-50 employees typically&lt;/li&gt;
&lt;li&gt;Heavy focus on web development (WordPress, Laravel, React, Node.js)&lt;/li&gt;
&lt;li&gt;Strong presence on Upwork, Fiverr, Freelancer.com&lt;/li&gt;
&lt;li&gt;Often specialize in specific niches (e-commerce sites, real estate portals, booking systems)&lt;/li&gt;
&lt;li&gt;Salaries: 20,000-150,000 taka/month+ typically&lt;/li&gt;
&lt;li&gt;Revenue: $50,000-$500,000 annually for established agencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this model works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Low barrier to entry (no sales team needed—platforms bring clients)&lt;/li&gt;
&lt;li&gt;International clients from day one&lt;/li&gt;
&lt;li&gt;Payment security through platforms&lt;/li&gt;
&lt;li&gt;Can build reputation faster than cold outreach&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Notable platforms used:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upwork (most popular for software development)&lt;/li&gt;
&lt;li&gt;Fiverr (quick projects, WordPress fixes, small websites)&lt;/li&gt;
&lt;li&gt;Freelancer.com (competitive bidding model)&lt;/li&gt;
&lt;li&gt;PeoplePerHour, Guru (alternative platforms)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Digital Marketing + Software Companies
&lt;/h2&gt;

&lt;p&gt;A growing category of companies combines software development with digital marketing services. These companies often build websites, then handle SEO, content creation, social media marketing, and paid advertising for clients.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leading Digital Marketing Agencies:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Bizcope&lt;/strong&gt; (&lt;a href="https://www.bizcope.com/" rel="noopener noreferrer"&gt;https://www.bizcope.com/&lt;/a&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global SEO and digital marketing agency&lt;/li&gt;
&lt;li&gt;Services: SEO, web development, content production, 360-degree digital marketing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Red Sparrow Digital&lt;/strong&gt; (&lt;a href="https://www.redsparrowdigital.com/" rel="noopener noreferrer"&gt;https://www.redsparrowdigital.com/&lt;/a&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full-stack digital marketing agency&lt;/li&gt;
&lt;li&gt;Web design &amp;amp; development, SEO, comprehensive marketing solutions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;HYPE DHAKA&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Communications agency&lt;/li&gt;
&lt;li&gt;Services: SEO, email marketing, social media marketing, website development&lt;/li&gt;
&lt;li&gt;100% positive client reviews according to Clutch.co&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Marketorr&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One of the fastest-growing digital marketing agencies&lt;/li&gt;
&lt;li&gt;Deep SEO expertise and full-funnel marketing strategies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Implevista&lt;/strong&gt; (&lt;a href="https://digital.implevista.com/" rel="noopener noreferrer"&gt;https://digital.implevista.com/&lt;/a&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Expert SEO, PPC, and social media marketing (SMM) solutions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this model matters:&lt;/strong&gt;&lt;br&gt;
Many Bangladeshi businesses don't just need software—they need the complete digital package. These companies provide that integrated solution, often at competitive rates compared to Western agencies.&lt;/p&gt;




&lt;h2&gt;
  
  
  Salary Comparison: The Real Numbers
&lt;/h2&gt;

&lt;p&gt;Let me give you the unvarnished truth about salaries in Bangladesh's software industry. These are ranges based on current market data (2024-2025):&lt;/p&gt;

&lt;h3&gt;
  
  
  By Experience Level:
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Salary Range (BDT/month)&lt;/th&gt;
&lt;th&gt;Salary Range (USD/month)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trainee/Intern&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0-15,000&lt;/td&gt;
&lt;td&gt;$0-135&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Junior Developer&lt;/strong&gt; (0-2 years)&lt;/td&gt;
&lt;td&gt;20,000-45,000&lt;/td&gt;
&lt;td&gt;$180-405&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Software Engineer&lt;/strong&gt; (2-5 years)&lt;/td&gt;
&lt;td&gt;50,000-80,000&lt;/td&gt;
&lt;td&gt;$450-720&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Senior Engineer&lt;/strong&gt; (5+ years)&lt;/td&gt;
&lt;td&gt;100,000-220,000&lt;/td&gt;
&lt;td&gt;$900-1,980&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tech Lead/Principal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;165,000-300,000&lt;/td&gt;
&lt;td&gt;$1,485-2,700&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  By Company Type:
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Company Type&lt;/th&gt;
&lt;th&gt;Junior&lt;/th&gt;
&lt;th&gt;Mid-Level&lt;/th&gt;
&lt;th&gt;Senior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Local Outsourcing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;25,000-35,000&lt;/td&gt;
&lt;td&gt;50,000-70,000&lt;/td&gt;
&lt;td&gt;100,000-150,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Western Outsourcing&lt;/strong&gt; (Cefalo, Enosis)&lt;/td&gt;
&lt;td&gt;50,000-80,000&lt;/td&gt;
&lt;td&gt;70,000-130,000&lt;/td&gt;
&lt;td&gt;185,000-220,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;International Companies&lt;/strong&gt; (Optimizely, Cheq)&lt;/td&gt;
&lt;td&gt;70,000+&lt;/td&gt;
&lt;td&gt;120,000+&lt;/td&gt;
&lt;td&gt;250,000-300,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Tech Startups&lt;/strong&gt; (Pathao, Chaldal)&lt;/td&gt;
&lt;td&gt;65,000+&lt;/td&gt;
&lt;td&gt;70,000-100,000&lt;/td&gt;
&lt;td&gt;155,000-220,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Product Companies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;30,000-50,000&lt;/td&gt;
&lt;td&gt;60,000-90,000&lt;/td&gt;
&lt;td&gt;100,000-220,000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Key Observations:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. The Entry-Level Reality:&lt;/strong&gt;&lt;br&gt;
Many companies pay trainee and intern positions 0-10,000 taka monthly (effectively free or near-free labor). It's controversial but common—companies view it as paid training rather than productive work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The International Company Premium:&lt;/strong&gt;&lt;br&gt;
Companies with international headquarters pay significantly more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Optimizely: Senior Software Engineer at 250,000-300,000 taka/month+&lt;/li&gt;
&lt;li&gt;Cheq Inc.: Senior QA Automation Engineer at 180,000-300,000 taka/month+&lt;/li&gt;
&lt;li&gt;Tero Labs: Engineer at 120,000-150,000 taka/month+&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. The Startup Competitiveness:&lt;/strong&gt;&lt;br&gt;
Local tech companies (Pathao, Chaldal) pay competitively because they compete directly with international employers for talent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Remote Work Premium:&lt;/strong&gt;&lt;br&gt;
Engineers working remotely for international companies earn substantially more:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~$41,465 (average for remote software developers in Bangladesh)&lt;/li&gt;
&lt;li&gt;~$46,135 (median annual salary for remote software engineers)&lt;/li&gt;
&lt;li&gt;That's 2-4x higher than local salaries&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Complete Company Directory
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Government &amp;amp; Enterprise Solutions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Datasoft Systems Bangladesh:&lt;/strong&gt; &lt;a href="https://datasoftbd.com" rel="noopener noreferrer"&gt;https://datasoftbd.com&lt;/a&gt; - CMMI Level 5, government contracts specialist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiger IT Bangladesh:&lt;/strong&gt; &lt;a href="https://www.tigerit.com" rel="noopener noreferrer"&gt;https://www.tigerit.com&lt;/a&gt; - Biometric and security systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Southtech Limited:&lt;/strong&gt; &lt;a href="https://www.southtech.com.bd" rel="noopener noreferrer"&gt;https://www.southtech.com.bd&lt;/a&gt; - Banking and ERP solutions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LeadSoft Bangladesh:&lt;/strong&gt; Financial and insurance platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  International Outsourcing Leaders
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Brain Station 23:&lt;/strong&gt; &lt;a href="https://brainstation-23.com" rel="noopener noreferrer"&gt;https://brainstation-23.com&lt;/a&gt; - Largest diversified player, 800+ employees&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Selise Digital Platforms:&lt;/strong&gt; &lt;a href="https://selisegroup.com" rel="noopener noreferrer"&gt;https://selisegroup.com&lt;/a&gt; - #2 ranked, Swiss-Bangladeshi operations, $37.9M revenue&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BJIT Group:&lt;/strong&gt; &lt;a href="https://bjitgroup.com" rel="noopener noreferrer"&gt;https://bjitgroup.com&lt;/a&gt; - Japan market specialist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cefalo Bangladesh:&lt;/strong&gt; &lt;a href="https://www.cefalo.com" rel="noopener noreferrer"&gt;https://www.cefalo.com&lt;/a&gt; - Norway market specialist&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enosis Solutions:&lt;/strong&gt; &lt;a href="https://enosisbd.com" rel="noopener noreferrer"&gt;https://enosisbd.com&lt;/a&gt; - US/Europe agile development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vivasoft Limited:&lt;/strong&gt; &lt;a href="https://vivasoftltd.com" rel="noopener noreferrer"&gt;https://vivasoftltd.com&lt;/a&gt; - Team augmentation and offshore setup&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Product Companies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;NerdDevs:&lt;/strong&gt; &lt;a href="https://nerddevs.com" rel="noopener noreferrer"&gt;https://nerddevs.com&lt;/a&gt; - EdTech and messaging solutions

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Biddaan LMS:&lt;/strong&gt; &lt;a href="https://biddaan.com" rel="noopener noreferrer"&gt;https://biddaan.com&lt;/a&gt; - Learning management system for Bangladesh&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BDTask Limited:&lt;/strong&gt; &lt;a href="https://www.bdtask.com" rel="noopener noreferrer"&gt;https://www.bdtask.com&lt;/a&gt; - Ready-made software products&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ollyo (MuktoSoft):&lt;/strong&gt; &lt;a href="https://ollyo.com" rel="noopener noreferrer"&gt;https://ollyo.com&lt;/a&gt; - WordPress/Joomla ecosystem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REVE Systems:&lt;/strong&gt; &lt;a href="https://www.revechat.com" rel="noopener noreferrer"&gt;https://www.revechat.com&lt;/a&gt; - VoIP and communications platforms&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Embedded Systems &amp;amp; IoT Companies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Samsung R&amp;amp;D Institute Bangladesh:&lt;/strong&gt; &lt;a href="https://research.samsung.com/srbd" rel="noopener noreferrer"&gt;https://research.samsung.com/srbd&lt;/a&gt; - Embedded systems, IoT, cellular modem (founded 2011)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teton Electronics:&lt;/strong&gt; &lt;a href="https://tetonelectronics.com" rel="noopener noreferrer"&gt;https://tetonelectronics.com&lt;/a&gt; - Firmware development and embedded systems services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BJIT Embedded Systems:&lt;/strong&gt; &lt;a href="https://www.bjit.co.jp" rel="noopener noreferrer"&gt;https://www.bjit.co.jp&lt;/a&gt; - IoT &amp;amp; Embedded Engineering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subra Systems Limited:&lt;/strong&gt; Embedded systems and industrial automation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SIMEC System Ltd.:&lt;/strong&gt; Embedded systems and firmware development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ninos IT Solution:&lt;/strong&gt; &lt;a href="https://www.ninositsolution.com" rel="noopener noreferrer"&gt;https://www.ninositsolution.com&lt;/a&gt; - Cutting-edge embedded systems design&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tech Startups (Growth Stage)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;bKash:&lt;/strong&gt; &lt;a href="https://www.bkash.com" rel="noopener noreferrer"&gt;https://www.bkash.com&lt;/a&gt; - Mobile financial services (#3 startup)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pathao:&lt;/strong&gt; &lt;a href="https://pathao.com" rel="noopener noreferrer"&gt;https://pathao.com&lt;/a&gt; - Super-app: ride-sharing, food delivery, logistics (#6 startup)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ShopUp:&lt;/strong&gt; #1 ranked startup - B2B commerce platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chaldal:&lt;/strong&gt; &lt;a href="https://chaldal.com" rel="noopener noreferrer"&gt;https://chaldal.com&lt;/a&gt; - E-commerce grocery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shikho:&lt;/strong&gt; EdTech platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arogga:&lt;/strong&gt; Healthcare/medicine delivery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PriyoShop Retail:&lt;/strong&gt; B2B retail platform&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shohoz:&lt;/strong&gt; Transportation and logistics&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Digital Marketing + Software Agencies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bizcope:&lt;/strong&gt; &lt;a href="https://www.bizcope.com" rel="noopener noreferrer"&gt;https://www.bizcope.com&lt;/a&gt; - SEO, web development, content production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Red Sparrow Digital:&lt;/strong&gt; &lt;a href="https://www.redsparrowdigital.com" rel="noopener noreferrer"&gt;https://www.redsparrowdigital.com&lt;/a&gt; - Full-stack digital marketing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HYPE DHAKA:&lt;/strong&gt; SEO, email marketing, social media marketing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marketorr:&lt;/strong&gt; Fastest-growing digital marketing agency&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implevista:&lt;/strong&gt; &lt;a href="https://digital.implevista.com" rel="noopener noreferrer"&gt;https://digital.implevista.com&lt;/a&gt; - SEO, PPC, SMM solutions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Smaller Specialists
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kaz Software:&lt;/strong&gt; &lt;a href="https://www.kaz.com.bd" rel="noopener noreferrer"&gt;https://www.kaz.com.bd&lt;/a&gt; - Enterprise custom development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nascenia Limited:&lt;/strong&gt; &lt;a href="https://www.nascenia.com" rel="noopener noreferrer"&gt;https://www.nascenia.com&lt;/a&gt; - SaaS product engineering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Solution Innovators:&lt;/strong&gt; &lt;a href="https://www.dsibd.com" rel="noopener noreferrer"&gt;https://www.dsibd.com&lt;/a&gt; - E-commerce and ERP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dream71 Bangladesh:&lt;/strong&gt; Mobile games and creative apps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PipilikaSoft:&lt;/strong&gt; Education management systems&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dcastalia Limited:&lt;/strong&gt; Top 10 ranked software company&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Government Contractors (Under Investigation)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OrangeBD (Orange IT):&lt;/strong&gt; Infrastructure and cybersecurity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tappware Solutions:&lt;/strong&gt; Software development and maintenance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SoftBD:&lt;/strong&gt; Software development services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business Automation Limited:&lt;/strong&gt; Process automation solutions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  International Companies with Bangladesh Operations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Optimizely Bangladesh:&lt;/strong&gt; Global optimization platform office&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Samsung R&amp;amp;D Bangladesh:&lt;/strong&gt; Samsung research and development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cheq Inc.:&lt;/strong&gt; Cybersecurity platform&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  IT Training Institutes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BASIS Institute of Technology &amp;amp; Management (BITM):&lt;/strong&gt; bitm.org.bd&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creative IT Institute:&lt;/strong&gt; Web development, MERN stack, digital marketing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dewan ICT Institute:&lt;/strong&gt; #1 ranked IT training institute&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;European IT Institute:&lt;/strong&gt; Professional IT training, Dhanmondi&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Softopark IT Institute:&lt;/strong&gt; Career-boosting IT programs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BASIS&lt;/strong&gt; (Bangladesh Association of Software &amp;amp; Information Services): &lt;a href="https://basis.org.bd" rel="noopener noreferrer"&gt;https://basis.org.bd&lt;/a&gt; - Industry association&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DoICT&lt;/strong&gt; (Directorate of ICT): &lt;a href="https://doict.gov.bd" rel="noopener noreferrer"&gt;https://doict.gov.bd&lt;/a&gt; - Government ICT initiatives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Salary Data:&lt;/strong&gt; &lt;a href="https://tahanima.github.io/salary-ranges-offered-by-bangladeshi-software-companies-for-different-positions/" rel="noopener noreferrer"&gt;https://tahanima.github.io/salary-ranges-offered-by-bangladeshi-software-companies-for-different-positions/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What This All Means
&lt;/h2&gt;

&lt;p&gt;Bangladesh's software industry has reached an interesting inflection point. The early stage of just doing outsourcing for international clients is maturing into something more sophisticated.&lt;/p&gt;

&lt;p&gt;Companies like Brain Station 23, NerdDevs, and Selise show it's possible to move up the value chain—not just writing code someone else specifies, but building products, solving complex problems, and working with prestigious international clients. TestReach (NerdDevs' platform) being used by British Council and ACCA isn't just outsourcing—it's being the technical partner for critical global infrastructure.&lt;/p&gt;

&lt;p&gt;The government digitalization push, despite procurement controversies, created a domestic market for software. Companies like Datasoft and Tiger IT built genuinely hard technology (national ID systems, biometric platforms) that required serious engineering.&lt;/p&gt;

&lt;p&gt;The product companies (BDTask, Ollyo, Biddaan) demonstrate that Bangladeshi developers can compete globally in marketplaces and create solutions used by thousands of businesses and schools.&lt;/p&gt;

&lt;p&gt;The startup ecosystem (bKash, Pathao, ShopUp, Chaldal) shows that world-class consumer platforms can be built from Bangladesh for Bangladesh—and eventually for emerging markets globally.&lt;/p&gt;

&lt;p&gt;But challenges remain real. Salaries lag far behind international standards. A "senior" engineer in Bangladesh earning $2,000/month would be entry-level pay in the US or Europe. This creates brain drain—talented developers leave for opportunities abroad. Companies struggle to retain their best engineers once they get two-three years experience and realize their market value elsewhere.&lt;/p&gt;

&lt;p&gt;Infrastructure issues persist. Power grid losses, internet reliability in secondary cities, and reliance on Dhaka for most operations limit growth. The skills gap in advanced technologies (AI, machine learning, blockchain) means many companies can't compete for cutting-edge work.&lt;/p&gt;

&lt;p&gt;The procurement corruption allegations around government contracts raise questions about whether that sector is sustainable long-term without serious reform.&lt;/p&gt;

&lt;p&gt;Still, the trajectory is impressive. From essentially nothing twenty years ago to a billion-dollar industry employing hundreds of thousands, with serious companies working on real problems for real international clients—that's meaningful progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Future: Where Bangladesh's Software Industry Is Headed
&lt;/h2&gt;

&lt;p&gt;The software industry Bangladesh built over fifteen years is real, substantial, and growing. It's not Silicon Valley, and it's not trying to be. It's something different—a developing country using its advantages (cost, talent, English proficiency) to build an export industry while simultaneously digitizing its own economy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What to watch in 2026-2030:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI and ML Adoption:&lt;/strong&gt; As global demand for AI talent explodes, Bangladesh's value proposition could drive growth—if the skills gap can be addressed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Startup Exits:&lt;/strong&gt; Bangladesh has yet to see a major IPO or acquisition. When bKash, Pathao, or ShopUp exit, it will unlock capital and validate the ecosystem&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Government Reform:&lt;/strong&gt; If procurement becomes transparent and merit-based, government contracts could drive sustainable growth rather than creating dependencies&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Regional Expansion:&lt;/strong&gt; Bangladeshi companies are well-positioned to serve other emerging markets in South Asia, Southeast Asia, and Africa&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Remote Work:&lt;/strong&gt; As remote work becomes normalized, Bangladeshi developers can work directly for international companies at international rates—reducing brain drain&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Product Success:&lt;/strong&gt; BDTask, Ollyo, and NerdDevs are showing that products can scale. More success stories here could shift the industry from services to products&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Freelance Professionalization:&lt;/strong&gt; The freelance-to-company pipeline will likely formalize, creating more sustainable agencies rather than individual freelancer outfits&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The story of Bangladesh's software industry isn't just about affordable services. It's about a country leveraging its human capital to participate in the global digital economy. It's about companies like Datasoft building national infrastructure, NerdDevs powering global examinations, and Pathao transforming transportation.&lt;/p&gt;

&lt;p&gt;It's about 400,000 professionals building their futures one line of code at a time.&lt;/p&gt;

&lt;p&gt;That's a story worth paying attention to.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Industry Statistics &amp;amp; Market Data
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.statista.com/outlook/tmo/software/bangladesh" rel="noopener noreferrer"&gt;Software - Bangladesh | Statista Market Forecast&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.mordorintelligence.com/industry-reports/bangladesh-ict-market" rel="noopener noreferrer"&gt;Bangladesh ICT Market Analysis | Mordor Intelligence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://basis.org.bd/public/files/publication/60d707ec45811__Software%2520&amp;amp;%2520IT%2520Services%2520Catalog%25202021-min.pdf" rel="noopener noreferrer"&gt;BASIT Software and IT Services Catalog 2021&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.tbsnews.net/bangladesh/export-earnings-technology-sector-exceeds-13-billion-palak-370579" rel="noopener noreferrer"&gt;Export earnings in technology sector exceeds $1.3 billion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.tbsnews.net/economy/5b-ict-export-target-missed-huge-margin-heres-why-1281591" rel="noopener noreferrer"&gt;$5b ICT export target missed by huge margin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.daily-sun.com/5/787382" rel="noopener noreferrer"&gt;Software companies earn $840m in export revenue in FY24&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://basis.org.bd/public/files/publication/5e123f2d5a6c6ba96136a3b168568073f9800e5b0f5b9.pdf" rel="noopener noreferrer"&gt;IT AND ITES INDUSTRY OVERVIEW - BASIS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Salary &amp;amp; Compensation Data
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://tahanima.github.io/salary-ranges-offered-by-bangladeshi-software-companies-for-different-positions/" rel="noopener noreferrer"&gt;Salary ranges offered by Bangladeshi Software Companies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.levels.fyi/t/software-engineer/levels/senior/locations/dhaka-bgd" rel="noopener noreferrer"&gt;Senior Software Engineer Salary in Dhaka | Levels.fyi&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.glassdoor.com/Salaries/bangladesh-senior-software-engineer-salary-SRCH_IL.0,10_IN27_KO11,35.htm" rel="noopener noreferrer"&gt;Senior Software Engineer Salary | Glassdoor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.payscale.com/research/BD/Job=Senior_Software_Engineer/Salary" rel="noopener noreferrer"&gt;Average Senior Software Engineer Salary in Bangladesh | PayScale&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://arc.dev/salaries/software-engineers-in-bangladesh" rel="noopener noreferrer"&gt;Remote Software Engineer Salary in Bangladesh | Arc.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://geniusee.com/single-blog/cost-of-outsourcing-software-development" rel="noopener noreferrer"&gt;Cost of outsourcing software development by country 2025&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Company Rankings &amp;amp; Lists
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ibos.io/best-software-companies-in-bangladesh-2026/" rel="noopener noreferrer"&gt;Best Software Companies in Bangladesh 2026 | iBOS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.kaz.com.bd/blog/best-software-companies-in-bangladesh" rel="noopener noreferrer"&gt;Top 25 Best Software Companies in Bangladesh 2026 | Kaz Software&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vivasoftltd.com/best-software-companies-in-bangladesh/" rel="noopener noreferrer"&gt;Top 20 Best Software Companies in Bangladesh 2026 | Vivasoft&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dcastalia.com/blog/top-10-software-company-in-bangladesh/" rel="noopener noreferrer"&gt;Top 10 Software Companies in Bangladesh 2025 | Dcastalia&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/pulse/100-bangladeshi-software-companies-profile-sagore-sarker" rel="noopener noreferrer"&gt;100 Bangladeshi Software Companies | LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Startup Ecosystem
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.startupblink.com/top-startups/bangladesh" rel="noopener noreferrer"&gt;Top Startups in Bangladesh | StartupBlink&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tipsoi.pro/top-10-tech-startups-in-bangladesh/" rel="noopener noreferrer"&gt;Top 10 Tech Startups In Bangladesh 2026 | TipsOI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.nucamp.co/blog/coding-bootcamp-bangladesh-bgd-bangladeshs-top-10-startups-that-tech-professionals-should-watch-out-for-in-2024" rel="noopener noreferrer"&gt;Bangladesh's Top 10 Startups | Nucamp&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lightcastlepartners.com/wp-content/uploads/2020/07/Bangladesh-Startup-Ecosystem-Report-2020-1.pdf" rel="noopener noreferrer"&gt;Bangladesh Startup Ecosystem Report | LightCastle Partners&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Digital Marketing Agencies
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://clutch.co/bd/agencies/content-marketing" rel="noopener noreferrer"&gt;Top Content Marketing Companies in Bangladesh | Clutch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://superbcompanies.com/categories/seo-companies-in-bangladesh/" rel="noopener noreferrer"&gt;Top 20 SEO Companies in Bangladesh 2026 | SuperbCompanies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://techbehemoths.com/companies/content-marketing/bangladesh" rel="noopener noreferrer"&gt;Top 20+ Content Marketing Agencies in Bangladesh | TechBehemoths&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Outsourcing Comparisons
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://proquantic.com/blog/software-development-outsourcing-countries" rel="noopener noreferrer"&gt;Top 10 Southeast Asian Countries for Software Outsourcing | Proquantic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://geniusee.com/single-blog/cost-of-outsourcing-software-development" rel="noopener noreferrer"&gt;Cost of outsourcing software development by country 2025 | Geniusee&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vivasoftltd.com/south-asian-countries-to-hire-software-development-team/" rel="noopener noreferrer"&gt;Top South Asian Countries to Hire Software Development | Vivasoft&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://innovatureinc.com/top-outsourcing-countries-to-watch-in-2025/" rel="noopener noreferrer"&gt;Top 10 Outsourcing Countries to Watch in 2025 | Innovature&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Freelance &amp;amp; Marketplace Resources
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://viserx.com/blog/web-development/freelancing-websites-bangladesh" rel="noopener noreferrer"&gt;Top 15 Freelancing Websites in Bangladesh For 2026 | ViserX&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.elevatepay.co/blog/freelancing-websites-in-bangladesh" rel="noopener noreferrer"&gt;Top 7 Freelancing Websites in Bangladesh | ElevatePay&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.truelancer.com/freelancers-in-bangladesh" rel="noopener noreferrer"&gt;Best Freelancers in Bangladesh | Truelancer&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://graphicdesigneye.com/best-upwork-alternatives/" rel="noopener noreferrer"&gt;Best Upwork Alternatives | Graphic Design Eye&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Embedded Systems &amp;amp; IoT
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ensun.io/search/embedded-systems/bangladesh" rel="noopener noreferrer"&gt;Top Embedded Systems Companies in Bangladesh | Ensun&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://research.samsung.com/srbd" rel="noopener noreferrer"&gt;Samsung R&amp;amp;D Institute Bangladesh | Samsung Research&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tetonelectronics.com/firmware-development-services/" rel="noopener noreferrer"&gt;Firmware Development Services | Teton Electronics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.bjit.co.jp/en/service/iot-embedded/" rel="noopener noreferrer"&gt;IoT &amp;amp; Embedded Engineering | BJIT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ninositsolution.com/bangladesh/embedded-systems-company" rel="noopener noreferrer"&gt;Embedded Systems Company in Bangladesh | Ninos IT Solution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://tetonelectronics.com/top-10-it-company/" rel="noopener noreferrer"&gt;Top 10 IT Company in Bangladesh | Teton Electronics&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Government &amp;amp; Policy
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://bida.gov.bd/investment-sector/it-it-enabled-services" rel="noopener noreferrer"&gt;The IT &amp;amp; ITES Goldmine – Bangladesh is Open for Business | BIDA&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://doict.gov.bd" rel="noopener noreferrer"&gt;DoICT | Directorate of ICT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://file-rangpur.portal.gov.bd/files/pbs2.dinajpur.gov.bd/files/1885c0a0_28a4_4fcc_8d4d_dcd7ce23fd8b/7b684f19a15dfcd0f542382764572486.pdf" rel="noopener noreferrer"&gt;National Strategy for Artificial Intelligence Bangladesh&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Company Reviews &amp;amp; Salary Data (Bangla Sources)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://deshimula.com" rel="noopener noreferrer"&gt;Deshi Mula - Company Review Platform&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/arman_aru/"&gt;বাংলাদেশে সফটওয়্যার ও ওয়েব ডেভেলপমেন্টের চাহিদা, বেতন ও ... | Dev.to&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Academic &amp;amp; Research
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://journalwjarr.com/sites/default/files/fulltext_pdf/WJARR-2025-3840.pdf" rel="noopener noreferrer"&gt;Absent of software regulation and regulator in Bangladesh | Journal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.researchgate.net/publication/364315458_Bangladesh_and_USA_ICT_frontier_to_explore" rel="noopener noreferrer"&gt;Bangladesh and USA: ICT frontier to explore | ResearchGate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ifc.org/content/dam/ifc/doc/2025/bangladesh-country-private-sector-diagnostic-en.pdf" rel="noopener noreferrer"&gt;Bangladesh: Country Private Sector Diagnostic | IFC&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This blog is based on publicly available information as of January 2026. Company details, salaries, and situations may change. All salary figures are approximate based on employee reports and job postings.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  End
&lt;/h2&gt;

&lt;p&gt;That's all!&lt;/p&gt;

&lt;p&gt;I hope you've found the article useful. I'll write more about the software engineering and it's business ecosystem. Feel free to share your thoughts.&lt;/p&gt;

&lt;p&gt;Check more on&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://encryptioner.github.io" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/mir-mursalin-ankur" rel="noopener noreferrer"&gt;Linkedin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Encryptioner" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/AnkurMursalin" rel="noopener noreferrer"&gt;X (Twitter)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nerddevs.com/author/ankur/" rel="noopener noreferrer"&gt;Nerddevs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>software</category>
      <category>businesss</category>
      <category>bangladesh</category>
      <category>news</category>
    </item>
    <item>
      <title>The Complete Guide to Software Business Models: How Tech Companies Actually Make Money</title>
      <dc:creator>Mir Mursalin Ankur</dc:creator>
      <pubDate>Sat, 10 Jan 2026 15:03:06 +0000</pubDate>
      <link>https://dev.to/mir_mursalin_ankur/the-complete-guide-to-software-business-models-how-tech-companies-actually-make-money-20dg</link>
      <guid>https://dev.to/mir_mursalin_ankur/the-complete-guide-to-software-business-models-how-tech-companies-actually-make-money-20dg</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.amazonaws.com%2Fuploads%2Farticles%2Ftiuhwehruocdxi88aedn.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.amazonaws.com%2Fuploads%2Farticles%2Ftiuhwehruocdxi88aedn.png" alt="How Tech Companies Actually Make Money" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've ever wondered how your favorite apps stay free, why some software charges monthly while others are one-time purchases, or how tech giants like Google and Apple structure their businesses, you're in the right place.&lt;/p&gt;

&lt;p&gt;The software industry has evolved far beyond simple "buy once, use forever" models. Today's tech landscape features dozens of sophisticated business models, each designed to solve specific problems and create value in unique ways. Understanding these models isn't just for entrepreneurs—it helps you make smarter decisions as a consumer, investor, or anyone navigating the digital economy.&lt;/p&gt;

&lt;p&gt;Let's break down the most important business models shaping the software world today.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Foundation: Core Business Model Families
&lt;/h2&gt;

&lt;p&gt;Think of these as the main categories that most tech businesses fall into. While companies often mix and match elements, understanding these fundamentals helps you see the bigger picture.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subscription: The Predictable Revenue Machine
&lt;/h3&gt;

&lt;p&gt;Remember when you bought Microsoft Office once and used it for years? Those days are largely gone. The subscription model has become the dominant force in software, and for good reason—it works incredibly well for both companies and customers.&lt;/p&gt;

&lt;p&gt;Here's how it works: instead of paying a large upfront fee, you pay a smaller amount regularly—usually monthly or annually. Netflix pioneered this for entertainment, but it's everywhere now. Adobe Creative Cloud, Spotify, Microsoft 365, and thousands of business software tools all use subscriptions.&lt;/p&gt;

&lt;p&gt;Why did this model take over? For companies, it creates predictable, recurring revenue. Instead of hunting for new customers every month, they can focus on keeping existing ones happy. A company with 10,000 subscribers paying $10 monthly knows it has roughly $100,000 coming in next month, making planning and investment much easier.&lt;/p&gt;

&lt;p&gt;For customers, the benefits are real too. Lower upfront costs mean less risk when trying new software. You're not committing $500 upfront—maybe just $10 for the first month. Plus, subscriptions typically include automatic updates, cloud storage, and customer support. The software company stays invested in keeping you happy because if you cancel, their revenue drops.&lt;/p&gt;

&lt;p&gt;The numbers tell the story. A well-run subscription business might see 90-95% of customers renew each year. That compounds powerfully over time. After three years, a company might still have 75% of its original customers, all while adding new ones. This creates what investors call an "annuity stream"—revenue that keeps flowing with less effort over time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Freemium: The Generous Hook
&lt;/h3&gt;

&lt;p&gt;Freemium is everywhere, even if you don't realize it. The name combines "free" and "premium"—give away the basic product for free, then charge for advanced features.&lt;/p&gt;

&lt;p&gt;Think about Spotify, Canva, or Slack. All offer genuinely useful free tiers that millions of people use daily without ever paying. But eventually, enough users hit limitations that matter to them—whether it's offline listening, premium templates, or message history—and convert to paid plans.&lt;/p&gt;

&lt;p&gt;The economics seem counterintuitive at first. How can you make money giving away your product? The key is that digital products have near-zero marginal cost. Serving one more free user costs almost nothing—no manufacturing, shipping, or materials. So if you can convert even 2-5% of free users to paying customers, the math works beautifully.&lt;/p&gt;

&lt;p&gt;Dropbox famously grew this way. They gave everyone 2GB of free storage—enough to be useful, but eventually, most people needed more. Some users upgraded immediately, others took years, but the free tier kept them engaged the whole time. Today, Dropbox has millions of paying customers who started with that free 2GB.&lt;/p&gt;

&lt;p&gt;The challenge is finding the right balance. Make the free tier too generous, and no one upgrades. Make it too restrictive, and people never experience the value. The best freemium products hook users with the free tier, then create natural "aha moments" that drive upgrades.&lt;/p&gt;

&lt;h3&gt;
  
  
  Transaction and Commission: Taking a Cut
&lt;/h3&gt;

&lt;p&gt;Some of the world's largest tech companies don't sell software at all—they connect buyers and sellers and take a percentage of each transaction.&lt;/p&gt;

&lt;p&gt;Airbnb doesn't own hotels. Uber doesn't own cars. Etsy doesn't make crafts. But they've built billion-dollar businesses by creating marketplaces where transactions happen, then taking a slice of each one.&lt;/p&gt;

&lt;p&gt;The magic of this model is that it scales with customer success. When an Airbnb host makes more money, Airbnb makes more money. When Stripe processes more payments for businesses, Stripe earns more. The incentives align perfectly.&lt;/p&gt;

&lt;p&gt;Transaction rates vary widely. Stripe and PayPal charge around 2.9% plus 30 cents per transaction. Airbnb takes about 3% from hosts and 14% from guests. The App Store and Google Play take 15-30% of app sales. These percentages might seem small, but when you're processing billions in transactions, they add up fast.&lt;/p&gt;

&lt;p&gt;The tricky part is reaching critical mass. A marketplace with few buyers won't attract sellers, and vice versa. This "chicken and egg problem" means transaction-based businesses often operate at a loss initially, subsidizing one or both sides to build momentum. Once they reach critical mass, though, network effects create powerful moats—the more users join, the more valuable the platform becomes, attracting even more users.&lt;/p&gt;

&lt;h3&gt;
  
  
  Advertising: Free for Users, Paid by Brands
&lt;/h3&gt;

&lt;p&gt;If you're not paying for the product, you are the product. This saying captures the advertising model perfectly.&lt;/p&gt;

&lt;p&gt;Google Search, Facebook, Instagram, YouTube, and most of the internet you use daily are free because advertisers pay for your attention. These companies have gotten incredibly sophisticated at targeting ads based on your behavior, demographics, and interests, making those ads more valuable to advertisers.&lt;/p&gt;

&lt;p&gt;The numbers are staggering. Google made over $200 billion from ads in 2023. Meta (Facebook/Instagram) made another $130 billion. These aren't small businesses with ads on the side—advertising IS their business.&lt;/p&gt;

&lt;p&gt;For this model to work, you need massive scale. A blog with 1,000 readers might make a few dollars from ads. A platform with 100 million users makes millions. That's why ad-supported businesses focus obsessively on growth and engagement—more eyeballs and more time spent equals more ad revenue.&lt;/p&gt;

&lt;p&gt;The model has its critics. Privacy concerns have grown as tracking becomes more sophisticated. Regulations like GDPR in Europe are forcing changes. And users increasingly turn to ad blockers or paid ad-free alternatives. Still, for many services—particularly social media—advertising remains the dominant model because users won't pay directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pay-As-You-Go: Usage-Based Pricing
&lt;/h3&gt;

&lt;p&gt;Some software charges based on exactly what you use, like your electricity bill. This usage-based model has exploded with cloud computing.&lt;/p&gt;

&lt;p&gt;Amazon Web Services (AWS), the backbone of much of the internet, bills by the computing power, storage, and bandwidth you consume. Use more, pay more. Use less, pay less. The same applies to Twilio (charged per text message or phone call), Stripe (per transaction), and many developer tools.&lt;/p&gt;

&lt;p&gt;This model aligns costs with value better than almost anything else. A startup spending $100 monthly on AWS can scale to $100,000 when they grow, without renegotiating contracts or switching providers. The pricing grows naturally with their business.&lt;/p&gt;

&lt;p&gt;For customers, it removes the fear of overpaying. You're never paying for capacity you don't need. For companies, it means revenue automatically scales with customer success—a win-win.&lt;/p&gt;

&lt;p&gt;The downside is unpredictability. Companies can't forecast usage-based revenue as easily as subscriptions. A customer might spend $1,000 one month and $5,000 the next. This variability makes planning harder. Many companies now combine fixed subscriptions with usage-based pricing to balance predictability and flexibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  Marketplaces: Connecting Supply and Demand
&lt;/h3&gt;

&lt;p&gt;Marketplaces are fascinating because they create value without creating products. eBay doesn't manufacture goods. Upwork doesn't do the freelance work. Airbnb doesn't provide the accommodations.&lt;/p&gt;

&lt;p&gt;Instead, they build platforms that reduce friction between buyers and sellers. They handle payments, build trust through reviews, provide insurance, and create discovery mechanisms. This infrastructure is valuable enough that both sides willingly pay fees.&lt;/p&gt;

&lt;p&gt;The best marketplaces solve real pain points. Before Airbnb, renting your apartment to strangers was risky and complicated. Before Uber, hailing a cab in many cities was frustrating. The marketplace didn't just connect supply and demand—it made transactions trustworthy and convenient.&lt;/p&gt;

&lt;p&gt;Two-sided marketplaces face unique challenges. You need enough supply to attract demand and enough demand to attract supply. Successful marketplaces often subsidize one side initially. Uber spent billions on driver incentives to ensure riders could always find a car. Once established, network effects made the platform nearly impossible to displace.&lt;/p&gt;

&lt;h3&gt;
  
  
  Open Source and Open Core: The Generosity Paradox
&lt;/h3&gt;

&lt;p&gt;Some of the most valuable software companies give away their core product for free—completely free, not freemium. How does that work?&lt;/p&gt;

&lt;p&gt;The open-core model releases the basic software as open source (anyone can use, modify, and see the code for free) but charges for enterprise features, support, or managed services.&lt;/p&gt;

&lt;p&gt;GitLab is a perfect example. Their core product is free and open source. Millions of developers use it daily without paying a cent. But large companies pay substantial fees for advanced features like enterprise security, compliance tools, and dedicated support.&lt;/p&gt;

&lt;p&gt;This model builds massive adoption and community goodwill while still generating revenue from those who need enterprise capabilities. It's particularly effective for developer tools because developers love open source and will advocate for tools they already use when their companies need to make purchasing decisions.&lt;/p&gt;

&lt;p&gt;Red Hat pioneered this model with Linux, eventually selling to IBM for $34 billion. MongoDB, Elastic, and many others have followed similar paths. The key is offering enough value in the free version to drive adoption while reserving genuinely valuable enterprise features for paying customers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Reference: Business Model Comparison
&lt;/h2&gt;

&lt;p&gt;Here's a comprehensive comparison of the major software business models to help you quickly understand their key differences:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Revenue Structure&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;Key Metrics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Subscription&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Recurring monthly/annual fees&lt;/td&gt;
&lt;td&gt;Products with ongoing value, sticky workflows&lt;/td&gt;
&lt;td&gt;Predictable revenue, customer retention, compounding growth&lt;/td&gt;
&lt;td&gt;Churn management, subscription fatigue&lt;/td&gt;
&lt;td&gt;Netflix, Spotify, Adobe CC, Microsoft 365&lt;/td&gt;
&lt;td&gt;MRR, ARR, Churn, LTV, NRR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Freemium&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free tier + paid upgrades&lt;/td&gt;
&lt;td&gt;Products that scale with usage/team size&lt;/td&gt;
&lt;td&gt;Low friction acquisition, viral growth, large user base&lt;/td&gt;
&lt;td&gt;Low conversion (2-5%), high support costs for free users&lt;/td&gt;
&lt;td&gt;Spotify, Canva, Slack, Dropbox&lt;/td&gt;
&lt;td&gt;Free-to-paid conversion, activation rate, time to convert&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transaction/Commission&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;% of each transaction&lt;/td&gt;
&lt;td&gt;Marketplaces, payment processing, two-sided platforms&lt;/td&gt;
&lt;td&gt;Scales with customer success, aligned incentives&lt;/td&gt;
&lt;td&gt;Chicken-egg problem, needs critical mass&lt;/td&gt;
&lt;td&gt;Uber, Airbnb, Stripe, Etsy, App Store&lt;/td&gt;
&lt;td&gt;GMV, take rate, liquidity, active buyers/sellers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Advertising&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;CPM/CPC from brands&lt;/td&gt;
&lt;td&gt;Mass-market consumer products&lt;/td&gt;
&lt;td&gt;Free for users drives scale, massive revenue potential&lt;/td&gt;
&lt;td&gt;Privacy concerns, ad blockers, regulatory risk&lt;/td&gt;
&lt;td&gt;Google, Facebook, YouTube, Instagram&lt;/td&gt;
&lt;td&gt;DAU/MAU, engagement time, CPC/CPM, revenue per user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Usage-Based&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pay per unit consumed&lt;/td&gt;
&lt;td&gt;Cloud infrastructure, APIs, developer tools&lt;/td&gt;
&lt;td&gt;Fair pricing, scales with customer growth&lt;/td&gt;
&lt;td&gt;Unpredictable revenue, harder forecasting&lt;/td&gt;
&lt;td&gt;AWS, Twilio, Zapier, Stripe&lt;/td&gt;
&lt;td&gt;Usage volume, consumption trends, expansion revenue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Marketplace&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fees from buyers/sellers or both&lt;/td&gt;
&lt;td&gt;Supply-demand matching, peer-to-peer transactions&lt;/td&gt;
&lt;td&gt;Network effects, strong moat at scale&lt;/td&gt;
&lt;td&gt;Critical mass challenge, complex trust-building&lt;/td&gt;
&lt;td&gt;eBay, Upwork, Airbnb, Uber, Etsy&lt;/td&gt;
&lt;td&gt;GMV, take rate, buyer/seller ratio, transaction frequency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Open Core&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free OSS + paid enterprise features&lt;/td&gt;
&lt;td&gt;Developer tools, infrastructure software&lt;/td&gt;
&lt;td&gt;Community trust, rapid adoption, developer advocacy&lt;/td&gt;
&lt;td&gt;Converting OSS users to paid is challenging&lt;/td&gt;
&lt;td&gt;GitLab, MongoDB, Red Hat, Elastic&lt;/td&gt;
&lt;td&gt;Enterprise conversion rate, community growth, support revenue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API-First&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per-call or % of transaction value&lt;/td&gt;
&lt;td&gt;Technical infrastructure, developer platforms&lt;/td&gt;
&lt;td&gt;Deep integration, switching costs, usage scales automatically&lt;/td&gt;
&lt;td&gt;Technical audience, requires developer trust&lt;/td&gt;
&lt;td&gt;Stripe, Twilio, Plaid, SendGrid&lt;/td&gt;
&lt;td&gt;API calls, transaction volume, developer adoption&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Platform Ecosystem&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multiple revenue streams (hardware + services + subscriptions)&lt;/td&gt;
&lt;td&gt;Companies with product breadth&lt;/td&gt;
&lt;td&gt;Lock-in, multiple monetization touchpoints, high LTV&lt;/td&gt;
&lt;td&gt;Requires huge capital, slow to build, complex coordination&lt;/td&gt;
&lt;td&gt;Apple, Google, Microsoft, Amazon&lt;/td&gt;
&lt;td&gt;Cross-product adoption, ecosystem retention, ARPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Creator Economy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;% of creator earnings or subscription&lt;/td&gt;
&lt;td&gt;Content creator monetization platforms&lt;/td&gt;
&lt;td&gt;Aligns with creator success, viral growth&lt;/td&gt;
&lt;td&gt;Content moderation, payment processing complexity&lt;/td&gt;
&lt;td&gt;Patreon, Substack, YouTube, OnlyFans&lt;/td&gt;
&lt;td&gt;Active creators, creator earnings, fan spend per creator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;White-Label&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Wholesale licensing fees&lt;/td&gt;
&lt;td&gt;B2B infrastructure, platform tech&lt;/td&gt;
&lt;td&gt;Rapid scaling through partners, reduced CAC&lt;/td&gt;
&lt;td&gt;Lower margins, less customer control, brand dilution&lt;/td&gt;
&lt;td&gt;WordPress, white-label SaaS&lt;/td&gt;
&lt;td&gt;Partner count, reseller revenue, partner retention&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data as Service&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Subscription for data access&lt;/td&gt;
&lt;td&gt;Companies with unique, valuable data&lt;/td&gt;
&lt;td&gt;High margins, recurring revenue, defensible&lt;/td&gt;
&lt;td&gt;Privacy/ethics concerns, data quality maintenance&lt;/td&gt;
&lt;td&gt;Bloomberg, weather services, analytics platforms&lt;/td&gt;
&lt;td&gt;Data accuracy, subscriber count, data refresh rate&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Choosing Based on Your Situation
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your Situation&lt;/th&gt;
&lt;th&gt;Recommended Model(s)&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Consumer app, viral potential&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Freemium + Ads&lt;/td&gt;
&lt;td&gt;Low barrier, mass scale required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;B2B software, clear ROI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Subscription (tiered)&lt;/td&gt;
&lt;td&gt;Predictable revenue, willingness to pay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Marketplace idea&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Transaction/Commission&lt;/td&gt;
&lt;td&gt;Scales with transaction volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Developer tool&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open Core + API/Usage&lt;/td&gt;
&lt;td&gt;Developers prefer OSS, enterprises pay for support&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Infrastructure/technical&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Usage-based/API&lt;/td&gt;
&lt;td&gt;Fair pricing, scales naturally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Content/media&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ads + Creator Economy split&lt;/td&gt;
&lt;td&gt;Users won't pay directly, creators need monetization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Enterprise focus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Subscription + White-label&lt;/td&gt;
&lt;td&gt;High contract values, predictable revenue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Platform vision&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ecosystem approach&lt;/td&gt;
&lt;td&gt;Maximize LTV through multiple touchpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Revenue Predictability Comparison
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;High Predictability:  Subscription │────────────────────────────────│ 95%+
                        SaaS Tiered │─────────────────────────│ 90%+

Medium Predictability: Freemium→Sub │──────────────│ 70-80%
                        Usage-Based (large customers) │────────│ 60-70%

Low Predictability:   Usage-Based (small customers) │────│ 50-60%
                        Transaction/Commission │──│ 40-50%
                        Advertising │─│ 30-40%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Customer Acquisition Cost (CAC) Recovery Time
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Typical CAC Recovery&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Freemium&lt;/td&gt;
&lt;td&gt;12-24+ months&lt;/td&gt;
&lt;td&gt;Most users never pay, long conversion cycles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advertising&lt;/td&gt;
&lt;td&gt;Immediate (but per-user value is low)&lt;/td&gt;
&lt;td&gt;Revenue starts immediately but margins thin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subscription&lt;/td&gt;
&lt;td&gt;6-18 months&lt;/td&gt;
&lt;td&gt;Predictable monthly payments accelerate recovery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usage-Based&lt;/td&gt;
&lt;td&gt;3-12 months&lt;/td&gt;
&lt;td&gt;Depends on customer adoption speed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transaction&lt;/td&gt;
&lt;td&gt;Varies wildly&lt;/td&gt;
&lt;td&gt;Scales with customer transaction volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise Subscription&lt;/td&gt;
&lt;td&gt;12-36 months&lt;/td&gt;
&lt;td&gt;High CAC but large contracts justify it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Specialized Software Business Models
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Frcmme507mgzul87n9w7j.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.amazonaws.com%2Fuploads%2Farticles%2Frcmme507mgzul87n9w7j.png" alt="Specialized, Emerging and Creative Models" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Beyond the main categories, several specialized models have emerged for specific contexts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Software as a Service (SaaS) Variations
&lt;/h3&gt;

&lt;p&gt;SaaS deserves its own deep dive because it's become the default for business software. But not all SaaS is created equal. Let's look at the main pricing variations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flat-rate SaaS&lt;/strong&gt; charges everyone the same amount—simple but limiting. Basecamp famously charges $99 monthly regardless of team size. This simplicity is refreshing in a world of complex pricing tiers, though it means large companies and small teams pay the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tiered pricing&lt;/strong&gt; offers multiple plans at different prices. Think Slack's Free, Pro, Business, and Enterprise tiers. This captures more value from customers with different needs and budgets. A startup might pay $8 per user monthly while an enterprise pays $15+.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-user pricing&lt;/strong&gt; scales costs with team size. Most SaaS tools use this—Zoom, Microsoft 365, and thousands of others charge per user per month. It's straightforward and aligns costs with value, though teams sometimes share accounts to save money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Usage-based SaaS&lt;/strong&gt; charges for consumption, not seats. Zapier charges based on tasks automated. AWS charges for computing resources used. This works beautifully when value correlates directly with usage.&lt;/p&gt;

&lt;p&gt;Many modern SaaS companies use &lt;strong&gt;hybrid models&lt;/strong&gt;, combining elements. You might have a base platform fee, per-user charges, and usage fees for certain features. Complexity increases but so does revenue optimization.&lt;/p&gt;

&lt;h3&gt;
  
  
  API-First Business Models
&lt;/h3&gt;

&lt;p&gt;APIs (Application Programming Interfaces) have become products themselves. Companies like Stripe, Twilio, and Plaid built billion-dollar businesses by offering APIs that developers integrate into their applications.&lt;/p&gt;

&lt;p&gt;Stripe's API makes it incredibly easy to accept payments. Instead of building payment infrastructure from scratch—dealing with banks, fraud prevention, and compliance—developers add a few lines of code and start processing payments immediately. Stripe handles the complexity and charges per transaction.&lt;/p&gt;

&lt;p&gt;This model works because it solves hard technical problems and packages them as simple APIs. Developers pay for convenience, reliability, and not having to become experts in payments, communications, or banking infrastructure.&lt;/p&gt;

&lt;p&gt;The economics are compelling. Once you build the API infrastructure, serving additional customers costs very little. A company processing $1 million in payments pays the same percentage as one processing $100 million, but Stripe's costs don't increase proportionally. At scale, margins are excellent.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data as a Service
&lt;/h3&gt;

&lt;p&gt;Some companies have realized their data itself is valuable. Bloomberg built an empire selling financial data. Weather companies sell forecasts to businesses. Analytics platforms sell aggregated insights.&lt;/p&gt;

&lt;p&gt;This model requires unique data that others don't have and customers who will pay for it. Bloomberg can charge thousands monthly because traders need real-time market data to make billion-dollar decisions. The value is clear.&lt;/p&gt;

&lt;p&gt;The challenge is often privacy and ethics. Companies must aggregate and anonymize data carefully. Selling individual user data creates backlash, but selling anonymized trends and insights can be legitimate and valuable.&lt;/p&gt;

&lt;h3&gt;
  
  
  White-Label and Licensing
&lt;/h3&gt;

&lt;p&gt;Some software companies don't sell to end users at all—they license their technology to other companies who rebrand and resell it.&lt;/p&gt;

&lt;p&gt;WordPress powers 40%+ of the internet, but most users don't know or care. Web hosting companies like Bluehost and GoDaddy offer "WordPress hosting," adding their branding and support. WordPress benefits through ecosystem growth.&lt;/p&gt;

&lt;p&gt;White-label SaaS lets companies offer software products without building them. A marketing agency might white-label email marketing software, offering it to clients under their own brand. The underlying software company charges a wholesale rate, and the agency marks it up.&lt;/p&gt;

&lt;p&gt;This model scales quickly because each licensing partner becomes a sales channel. The tradeoff is lower margins and less direct customer relationships.&lt;/p&gt;

&lt;h2&gt;
  
  
  Emerging and Creative Models
&lt;/h2&gt;

&lt;p&gt;Innovation in business models continues. Here are some newer patterns worth understanding.&lt;/p&gt;

&lt;h3&gt;
  
  
  Platform Ecosystems: Building Worlds, Not Products
&lt;/h3&gt;

&lt;p&gt;The most valuable tech companies don't sell products—they create ecosystems that lock users in through interconnectivity and network effects.&lt;/p&gt;

&lt;p&gt;Apple's ecosystem is the classic example. An iPhone works beautifully alone, but add an iPad, MacBook, Apple Watch, and AirPods, and you're deeply embedded. Messages sync across devices. AirPods switch seamlessly between them. Files move effortlessly through AirDrop. Leaving means losing all that integration.&lt;/p&gt;

&lt;p&gt;Apple doesn't just sell hardware—they sell the App Store (30% commission), Apple Music (subscription), iCloud storage (subscription), Apple TV+ (subscription), and more. Each product makes the others more valuable. This is ecosystem thinking at its finest.&lt;/p&gt;

&lt;p&gt;Google does the same with Android, Chrome, Search, Gmail, Drive, and YouTube. Once you're using several Google services, switching becomes increasingly difficult. Your data, preferences, and workflows are all integrated.&lt;/p&gt;

&lt;p&gt;Building ecosystems requires patience and capital. You need multiple products that genuinely work better together. But once established, ecosystems create powerful moats. Customers stay not because they can't leave but because leaving would mean losing substantial value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Platform Cooperatives: The Democratic Alternative
&lt;/h3&gt;

&lt;p&gt;While most platforms are owned by shareholders seeking maximum profit, a small but growing movement advocates for platform cooperatives—platforms owned by users or workers.&lt;/p&gt;

&lt;p&gt;Imagine if Uber were owned by drivers. Or if a social network were owned by its users. Instead of optimizing for investor returns, these platforms could optimize for user welfare.&lt;/p&gt;

&lt;p&gt;Examples are emerging. Stocksy (stock photography) is owned by contributing artists. Resonate (music streaming) is owned by musicians and listeners. These cooperatives still need to generate revenue and cover costs, but profits are distributed among stakeholders rather than external shareholders.&lt;/p&gt;

&lt;p&gt;The model addresses concerns about platform power and wealth concentration. Whether it can scale to compete with venture-backed giants remains uncertain, but it represents an interesting alternative approach to platform building.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creator Economy Models
&lt;/h3&gt;

&lt;p&gt;The rise of creators—YouTubers, podcasters, newsletter writers, course creators—has spawned new business models.&lt;/p&gt;

&lt;p&gt;Platforms like Patreon let creators charge fans directly for access to exclusive content. Substack enables writers to run paid newsletters, keeping 90% of revenue. YouTube shares ad revenue with creators. OnlyFans takes a percentage of creator earnings.&lt;/p&gt;

&lt;p&gt;These platforms succeeded by recognizing that creators needed infrastructure for payments, hosting, and fan management. By providing these tools, they could capture a percentage of the growing creator economy without creating content themselves.&lt;/p&gt;

&lt;p&gt;The model works because it aligns incentives. When creators succeed, the platform succeeds. Unlike traditional media where companies employed creators, these platforms enable independent creators while taking a cut.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bundling and Cross-Selling
&lt;/h3&gt;

&lt;p&gt;Some companies win not through individual products but through packaging multiple offerings together.&lt;/p&gt;

&lt;p&gt;Microsoft 365 bundles Word, Excel, PowerPoint, Teams, OneDrive, and more for one price. Individually, these might cost $15-30 monthly each. Bundled, they're $12.50 per user. Customers get better value, Microsoft gets stickier relationships and higher total revenue per customer.&lt;/p&gt;

&lt;p&gt;Adobe Creative Cloud similarly bundles Photoshop, Illustrator, Premiere Pro, and 20+ other tools. Professional designers might use several daily. Buying individually would cost hundreds monthly; bundled, it's $55.&lt;/p&gt;

&lt;p&gt;Bundling increases switching costs. A customer might consider alternatives to Photoshop, but finding replacements for their entire Adobe workflow is much harder.&lt;/p&gt;

&lt;p&gt;The risk is that customers only value one or two bundle components. If they're paying for 10 tools but only using 2, competitors offering just those 2 tools for less might win.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Choose the Right Model
&lt;/h2&gt;

&lt;p&gt;With so many options, how should a company choose? Several factors matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audience and market:&lt;/strong&gt; Consumer apps rarely work with direct payments—users expect free or very cheap. Business software can command higher prices because the ROI is clear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product nature:&lt;/strong&gt; Some products fit certain models naturally. Developer tools work well with usage-based pricing. Creative tools suit subscriptions. Marketplaces obviously need transaction models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Competition:&lt;/strong&gt; In crowded markets, freemium helps acquire users despite competition. In blue ocean markets, you might charge from day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Growth goals:&lt;/strong&gt; Venture-backed startups often prioritize growth over revenue, making freemium attractive. Bootstrapped companies need revenue sooner, favoring paid models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Margins and economics:&lt;/strong&gt; High-margin products (pure software) support freemium better than low-margin ones (with human services or heavy infrastructure costs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customer acquisition costs:&lt;/strong&gt; If it costs $1,000 to acquire a customer, you need a business model that generates substantially more than $1,000 per customer over time. Subscriptions work well here; one-time purchases might not.&lt;/p&gt;

&lt;p&gt;Most successful companies eventually use hybrid models. Slack is freemium with tiered subscriptions. AWS has subscriptions for some services, usage-based pricing for others. Apple combines hardware sales, commissions, subscriptions, and ecosystem lock-in.&lt;/p&gt;

&lt;p&gt;The key is starting simple, learning from customers, and evolving thoughtfully rather than adopting every trendy model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategic Considerations
&lt;/h2&gt;

&lt;p&gt;Beyond choosing a model, several strategic factors determine success.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding Your Key Players
&lt;/h3&gt;

&lt;p&gt;Every market has dominant players whose strategies shape the landscape. In cloud computing, AWS, Google Cloud, and Microsoft Azure set pricing and features. In social media, Meta and Google define the advertising model.&lt;/p&gt;

&lt;p&gt;Understanding these players helps you identify opportunities. Are they ignoring a segment? Is there a feature gap? Could you serve customers they're overpricing?&lt;/p&gt;

&lt;p&gt;Sometimes the best strategy is being acquired by a major player. Instagram and WhatsApp became worth billions because Facebook (now Meta) needed to acquire their users and eliminate competition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Opportunities in the Current Market
&lt;/h3&gt;

&lt;p&gt;Several trends create opportunities for new entrants:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Remote work&lt;/strong&gt; has exploded demand for collaboration software, virtual events, and distributed team management. Companies that solve remote work pain points well have grown dramatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI and automation&lt;/strong&gt; are transforming every industry. Tools that help businesses implement AI without deep expertise are proliferating. The companies building the best AI wrappers and integrations will capture substantial value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy and data ownership&lt;/strong&gt; concerns are creating demand for alternatives to ad-supported services. Paid search engines, encrypted messaging, and privacy-focused analytics are emerging.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vertical SaaS&lt;/strong&gt; targets specific industries with tailored solutions rather than horizontal tools for everyone. Software for restaurants, medical practices, or law firms can command premium prices by solving industry-specific problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No-code and low-code&lt;/strong&gt; platforms democratize software creation. As these mature, new types of businesses become possible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Weaknesses and Risks to Navigate
&lt;/h3&gt;

&lt;p&gt;No business model is perfect. Understanding weaknesses helps you mitigate them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subscription fatigue&lt;/strong&gt; is real. Consumers and businesses are pushback against dozens of monthly charges. Some users are choosing one-time purchases or reducing subscriptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platform dependence&lt;/strong&gt; means your business can change overnight if Apple, Google, or Amazon changes policies. Many businesses have seen revenue disappear when App Store rules changed or Amazon adjusted search algorithms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Competition and commoditization&lt;/strong&gt; affect every model. As markets mature, differentiation becomes harder and prices compress. What once commanded premium prices becomes a race to the bottom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retention challenges&lt;/strong&gt; plague subscription businesses. Losing 5% of customers monthly compounds into losing more than half your base in a year. Customer success and continuous value delivery are critical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Threats on the Horizon
&lt;/h3&gt;

&lt;p&gt;Several external factors could disrupt current models:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regulation&lt;/strong&gt; is increasing globally. Europe's Digital Markets Act forces changes to App Store fees and default browser settings. Privacy regulations restrict advertising practices. Future regulations could reshape entire business models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Economic downturns&lt;/strong&gt; hit different models differently. Subscription businesses often see churn spikes in recessions. Ad-supported businesses see revenue drop as marketing budgets shrink. Transaction-based businesses decline with overall economic activity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technology shifts&lt;/strong&gt; can make entire categories obsolete. Mobile apps disrupted desktop software. Cloud computing disrupted on-premise software. AI might disrupt both. Companies must continuously evolve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security and privacy breaches&lt;/strong&gt; destroy trust and can kill companies. As data becomes more central to business models, protecting it becomes existential.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tax and Legal Considerations
&lt;/h3&gt;

&lt;p&gt;Business models have different tax and legal implications that matter enormously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Digital services taxation&lt;/strong&gt; varies globally. Selling software to customers in Europe requires understanding VAT (typically 17-27%). Some US states tax SaaS, others don't. Compliance complexity grows with geographic expansion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revenue recognition&lt;/strong&gt; rules differ by model. Subscriptions recognize revenue over time as services are delivered. One-time sales recognize revenue immediately. Marketplaces have specific rules for gross versus net revenue recognition. Getting this wrong can have serious accounting and legal consequences.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data privacy compliance&lt;/strong&gt; affects advertising and data businesses particularly. GDPR in Europe, CCPA in California, and similar laws globally create compliance obligations. Violations bring substantial fines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contractor versus employee classification&lt;/strong&gt; matters for platform businesses. Uber, Lyft, and others face ongoing legal battles about whether drivers are contractors or employees. The distinction affects costs massively and has business model implications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metrics That Matter
&lt;/h3&gt;

&lt;p&gt;Different business models require tracking different metrics.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;subscription businesses&lt;/strong&gt;, the critical metrics are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monthly Recurring Revenue (MRR) and Annual Recurring Revenue (ARR)&lt;/li&gt;
&lt;li&gt;Customer Acquisition Cost (CAC)&lt;/li&gt;
&lt;li&gt;Lifetime Value (LTV)&lt;/li&gt;
&lt;li&gt;Churn rate (monthly percentage of customers lost)&lt;/li&gt;
&lt;li&gt;Net Revenue Retention (revenue from existing customers including expansions minus churn)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For &lt;strong&gt;marketplace businesses&lt;/strong&gt;, track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Gross Merchandise Value (GMV) - total transaction volume&lt;/li&gt;
&lt;li&gt;Take rate (percentage you keep)&lt;/li&gt;
&lt;li&gt;Active buyers and sellers&lt;/li&gt;
&lt;li&gt;Liquidity (how quickly supply meets demand)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For &lt;strong&gt;advertising businesses&lt;/strong&gt;, watch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Daily/Monthly Active Users (DAU/MAU)&lt;/li&gt;
&lt;li&gt;Engagement metrics (time spent, actions taken)&lt;/li&gt;
&lt;li&gt;Cost Per Click (CPC) or Cost Per Thousand Impressions (CPM)&lt;/li&gt;
&lt;li&gt;Revenue per user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For &lt;strong&gt;freemium businesses&lt;/strong&gt;, monitor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free to paid conversion rate&lt;/li&gt;
&lt;li&gt;Activation rate (percentage of signups who experience core value)&lt;/li&gt;
&lt;li&gt;Time to conversion&lt;/li&gt;
&lt;li&gt;Feature adoption by tier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Understanding which metrics matter for your model helps you optimize the right things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Examples and Lessons
&lt;/h2&gt;

&lt;p&gt;Theory is important, but let's look at how this plays out in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zoom&lt;/strong&gt; started as a better video conferencing tool in a crowded market. They offered generous free tiers (40-minute meetings for up to 100 participants) while charging businesses for unlimited meeting length, more participants, and features like recording.&lt;/p&gt;

&lt;p&gt;The freemium model drove explosive growth. Millions tried Zoom for free during the pandemic. Many upgraded when remote work became permanent. By 2023, Zoom had over 467,000 customers with more than 10 employees, most paying substantial subscriptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stripe&lt;/strong&gt; could have sold their payment processing API as a monthly subscription. Instead, they chose transaction-based pricing—2.9% plus 30 cents per charge. This aligned perfectly with customer success. Startups pay almost nothing initially, but as they grow, Stripe grows with them. Some of Stripe's largest customers process billions in payments annually, generating millions in fees.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Notion&lt;/strong&gt; brilliantly combined freemium, subscription tiers, and viral growth. The product is genuinely useful for free, encouraging personal adoption. As teams form and need collaboration features, they upgrade. Users love Notion enough to tell others, driving organic growth. The result: a $10 billion valuation built largely through product-led growth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Epic Games&lt;/strong&gt; challenged the App Store model directly. They bypassed Apple's 30% commission by offering direct payment options in Fortnite. Apple banned Fortnite, leading to lawsuits and regulatory scrutiny. While Epic hasn't fully won, they've sparked global debate about fair platform fees and helped drive regulatory changes.&lt;/p&gt;

&lt;p&gt;These examples show that business models aren't just theoretical—they're strategic decisions with massive implications.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Future of Software Business Models
&lt;/h2&gt;

&lt;p&gt;Where is this all heading? Several trends seem clear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI integration&lt;/strong&gt; will become table stakes. Every software category will incorporate AI capabilities. The question becomes: do you charge extra for AI features, include them in existing tiers, or use AI as a moat? Different companies will choose different approaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unbundling and rebundling&lt;/strong&gt; will continue. Markets oscillate between integrated suites and best-of-breed point solutions. Currently, we're seeing both: some companies bundle more features (Notion adding docs, wikis, databases), while others unbundle (focused tools that do one thing excellently).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Privacy-first models&lt;/strong&gt; may gain traction as users tire of ad-supported services that monetize attention. Paid alternatives to free, ad-supported tools are emerging. Whether consumers actually pay remains to be seen, but the shift is beginning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consumption-based pricing&lt;/strong&gt; will likely expand beyond cloud infrastructure. As companies seek to align costs with value more precisely, usage-based pricing makes sense for more categories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platform regulation&lt;/strong&gt; will reshape marketplace and ecosystem models. Regulators globally are scrutinizing platform power, app store fees, and default settings. Changes seem inevitable, though specifics remain uncertain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web3 and blockchain&lt;/strong&gt; introduce new possibilities. Decentralized platforms owned by token holders rather than shareholders, creator tokens that let fans invest in creators directly, and NFTs that enable new digital ownership models are all experiments in progress. Most will fail, but some innovations may stick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: It's About Value Creation, Not Just Revenue
&lt;/h2&gt;

&lt;p&gt;After exploring dozens of business models, one truth becomes clear: the best model is the one that creates and captures value most effectively for your specific situation.&lt;/p&gt;

&lt;p&gt;Subscription models succeed when they continuously deliver value, keeping customers engaged month after month. Freemium works when the free tier genuinely helps users while premium features solve real pain points. Marketplaces thrive when they reduce friction and enable transactions that wouldn't happen otherwise.&lt;/p&gt;

&lt;p&gt;The companies that win long-term don't just choose a model—they obsess over delivering value, then find business models that capture some portion of that value sustainably.&lt;/p&gt;

&lt;p&gt;Understanding these models helps you make better decisions whether you're building software, investing in tech companies, or simply using digital products. You'll recognize when you're in a freemium acquisition funnel, understand why that SaaS company is pushing annual plans so hard, and see the strategic logic behind platform ecosystem moves.&lt;/p&gt;

&lt;p&gt;The software industry continues evolving rapidly. New models emerge, existing ones adapt, and what works today might not work tomorrow. But the fundamental principle remains: create real value for real people, then build a business model that captures enough value to sustain and grow while keeping customers happy.&lt;/p&gt;

&lt;p&gt;That's the real art of software business models—finding that sustainable balance where everyone wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  End
&lt;/h2&gt;

&lt;p&gt;That's all!&lt;/p&gt;

&lt;p&gt;I hope you've found the article useful. I'll write more about the software business &amp;amp; related organizations &amp;amp; ecosystem soon. Feel free to share your thoughts.&lt;/p&gt;

&lt;p&gt;Check more on&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://encryptioner.github.io" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/mir-mursalin-ankur" rel="noopener noreferrer"&gt;Linkedin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Encryptioner" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/AnkurMursalin" rel="noopener noreferrer"&gt;X (Twitter)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nerddevs.com/author/ankur/" rel="noopener noreferrer"&gt;Nerddevs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>softwarebusiness</category>
      <category>startup</category>
      <category>product</category>
      <category>freemium</category>
    </item>
    <item>
      <title>Publishing Your First NPM Package: A Real-World Guide That Actually Helps</title>
      <dc:creator>Mir Mursalin Ankur</dc:creator>
      <pubDate>Sat, 22 Nov 2025 02:54:55 +0000</pubDate>
      <link>https://dev.to/mir_mursalin_ankur/publishing-your-first-npm-package-a-real-world-guide-that-actually-helps-4l4</link>
      <guid>https://dev.to/mir_mursalin_ankur/publishing-your-first-npm-package-a-real-world-guide-that-actually-helps-4l4</guid>
      <description>&lt;p&gt;You know that moment when you’ve written the &lt;em&gt;same utility function&lt;/em&gt; for the third time across different projects and suddenly think: &lt;strong&gt;“Wait… why am I doing this again?”&lt;/strong&gt;&lt;br&gt;
Yeah — that’s exactly how NPM packages quietly sneak into your life.&lt;/p&gt;

&lt;p&gt;I recently went through the process of turning a simple HTML-to-PDF generator into a proper NPM package. What started as a quick utility became a small adventure — a mix of excitement, challenges, and learning-by-doing. A little confusing at times, but absolutely worth it.&lt;/p&gt;

&lt;p&gt;And here’s the twist: NPM has updated quite a few things lately — &lt;strong&gt;deprecated classic tokens, changed their UI, and even updated how org-scoped packages behave&lt;/strong&gt;. So if you’ve followed older tutorials, you’ve probably felt that “why isn’t this working?” frustration.&lt;/p&gt;

&lt;p&gt;This guide is different.&lt;br&gt;
It’s based on real steps, real mistakes, and the little details nobody tells you about until you’re already stuck.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why Even Bother Publishing a Package?
&lt;/h2&gt;

&lt;p&gt;Publishing a package does three magical things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reusability&lt;/strong&gt;: Stop copy-pasting code between projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Helping others&lt;/strong&gt;: Someone out there is fighting the same problem right now&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning&lt;/strong&gt;: You'll dive into open-source workflow, versioning, CI/CD, and documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus, there's something genuinely satisfying about typing &lt;code&gt;npm install your-package-name&lt;/code&gt; and seeing your own work install. It's a tiny piece of software craftsmanship that feels really good.&lt;/p&gt;


&lt;h2&gt;
  
  
  Understanding Scopes and Naming (Before You Start)
&lt;/h2&gt;

&lt;p&gt;NPM packages can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unscoped&lt;/strong&gt;: &lt;code&gt;your-package-name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoped&lt;/strong&gt;: &lt;code&gt;@yourname/your-package-name&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what matters: if your *NPM username *is &lt;code&gt;cooldev&lt;/code&gt;, then &lt;code&gt;@cooldev/anything&lt;/code&gt; is automatically your namespace. You don't need to create an organization unless your scope differs from your username.&lt;/p&gt;

&lt;p&gt;Check if a name is available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm view @yourname/your-package-name
&lt;span class="c"&gt;# 404 means it's free to use&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 1: Set Up the Project Structure
&lt;/h2&gt;

&lt;p&gt;Start fresh. Create a clean directory and initialize it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;your-package-name
&lt;span class="nb"&gt;cd &lt;/span&gt;your-package-name
git init
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep things organized with a &lt;code&gt;src/&lt;/code&gt; folder for your code.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Bring in Your Source Files
&lt;/h2&gt;

&lt;p&gt;Copy your working code into the new package structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; src
&lt;span class="nb"&gt;cp&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; /path/to/your-project/src/lib/your-library/&lt;span class="k"&gt;*&lt;/span&gt; src/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isolation keeps your package clean and focused.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Configure Your package.json
&lt;/h2&gt;

&lt;p&gt;This is the heart of your package. Update these critical fields:&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;"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;"@yourname/your-package-name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&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.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"types"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dist/index.d.ts"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"repository"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"git"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/yourname/your-package-name.git"&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;"bugs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/yourname/your-package-name/issues"&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;"homepage"&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://github.com/yourname/your-package-name#readme"&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;Make sure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;name&lt;/code&gt; is unique (check NPM first)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;repository.url&lt;/code&gt; points to your GitHub repo&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bugs.url&lt;/code&gt; gives people a place to report issues&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;homepage&lt;/code&gt; links to documentation&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 4: Connect to GitHub
&lt;/h2&gt;

&lt;p&gt;Get your code into version control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Initial commit: My Library to NPM Package"&lt;/span&gt;
git branch &lt;span class="nt"&gt;-M&lt;/span&gt; master
git remote add origin https://github.com/yourname/your-package-name.git
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5: Test Your Build Locally
&lt;/h2&gt;

&lt;p&gt;Before publishing anything, make sure it actually works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the secret weapon most beginners don't know about — test your package locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm pack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a &lt;code&gt;.tgz&lt;/code&gt; file — the exact same file that would go to NPM.&lt;/p&gt;

&lt;p&gt;Now test it in another project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; /path/to/your-package-1.0.0.tgz
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches so many issues. Seriously, multiple problems only showed up after testing in a fresh environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 6: Login to NPM Account
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fivqhro4sryihvx38gm33.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.amazonaws.com%2Fuploads%2Farticles%2Fivqhro4sryihvx38gm33.png" alt="NPM Profile Page" width="299" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can login to npm from any browser by going to &lt;a href="https://www.npmjs.com/login" rel="noopener noreferrer"&gt;npmjs.com&lt;/a&gt;. Login is also straightforward by command line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll be prompted for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Username&lt;/li&gt;
&lt;li&gt;Password&lt;/li&gt;
&lt;li&gt;Email&lt;/li&gt;
&lt;li&gt;One-time password (if you have 2FA enabled)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 7: Choose Your Publishing Strategy
&lt;/h2&gt;

&lt;p&gt;You have two ways to publish your package:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Manual Publishing&lt;/strong&gt; - Quick start, publish from command line&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated CI/CD&lt;/strong&gt; - Professional, publish on git tag (recommended)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's start with manual to get you published quickly, then set up automation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Manual Publishing (Quick Start)
&lt;/h3&gt;

&lt;p&gt;Login to NPM from your terminal then publish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm login
npm publish &lt;span class="nt"&gt;--access&lt;/span&gt; public
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--access public&lt;/code&gt; flag is required for scoped packages (like &lt;code&gt;@yourname/package-name&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;That's it. Your package is live on NPM!&lt;/p&gt;

&lt;p&gt;But here's the thing: manual publishing is error-prone. You might forget to build, or push the wrong version, or skip tests. Let's set up automation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 8: Set Up Automated Publishing (Recommended)
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. &lt;strong&gt;Classic NPM tokens are deprecated&lt;/strong&gt; — many old tutorials still reference them, which is confusing. You need to create a &lt;strong&gt;Granular Access Token&lt;/strong&gt; instead. Then use github &lt;code&gt;CI/CD&lt;/code&gt; workflow to publish the changes to &lt;code&gt;npm&lt;/code&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.amazonaws.com%2Fuploads%2Farticles%2Fnxwe12gt48mbng7u9tjy.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.amazonaws.com%2Fuploads%2Farticles%2Fnxwe12gt48mbng7u9tjy.png" alt="NPM Classic Token Deprecation Notice" width="312" height="119"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating a Granular Access Token
&lt;/h3&gt;

&lt;p&gt;This token lets GitHub Actions publish your package automatically. Here's how:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Go to &lt;a href="https://www.npmjs.com/login" rel="noopener noreferrer"&gt;npmjs.com&lt;/a&gt; and login&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Navigate to your profile → &lt;strong&gt;Access Tokens&lt;/strong&gt; → &lt;strong&gt;Generate New Token&lt;/strong&gt; → Select &lt;strong&gt;Granular Access Token&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Configure the token:&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;Token Name: GitHub Actions CI/CD
Expiration: 90 days (or your preference)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Set permissions:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Under "Packages and scopes":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Select &lt;strong&gt;Read and write&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Choose either:

&lt;ul&gt;
&lt;li&gt;"All packages" (easier), OR&lt;/li&gt;
&lt;li&gt;Select your specific package &lt;code&gt;@yourname/your-package-name&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;4. Copy the token immediately&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You won't be able to see it again! It starts with &lt;code&gt;npm_...&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Add to GitHub Secrets:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go to your GitHub repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Settings&lt;/code&gt; → &lt;code&gt;Secrets and variables&lt;/code&gt; → &lt;code&gt;Actions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click "New repository secret"&lt;/li&gt;
&lt;li&gt;Name: &lt;code&gt;NPM_TOKEN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Value: Paste your token&lt;/li&gt;
&lt;li&gt;Click "Add secret"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now GitHub Actions can publish on your behalf.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 9: Understanding Semantic Versioning
&lt;/h2&gt;

&lt;p&gt;Before we automate, let's talk about versioning. It's not just numbers — it's a promise to your users:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;When to Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npm version patch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.0.0 → 1.0.1&lt;/td&gt;
&lt;td&gt;Bug fixes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npm version minor&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.0.0 → 1.1.0&lt;/td&gt;
&lt;td&gt;New features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npm version major&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.0.0 → 2.0.0&lt;/td&gt;
&lt;td&gt;Breaking changes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's the magic: each &lt;code&gt;npm version&lt;/code&gt; command does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Updates version in &lt;code&gt;package.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Creates a git commit&lt;/li&gt;
&lt;li&gt;Creates a git tag (like &lt;code&gt;v1.0.1&lt;/code&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That git tag is what triggers automated publishing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 10: Create the GitHub Actions Workflow
&lt;/h2&gt;

&lt;p&gt;Now let's set up the automation. This workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Triggers when you push a version tag (like &lt;code&gt;v1.2.3&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Runs your tests and builds&lt;/li&gt;
&lt;li&gt;Verifies the version matches&lt;/li&gt;
&lt;li&gt;Publishes to NPM with provenance (security feature)&lt;/li&gt;
&lt;li&gt;Creates a GitHub release&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create &lt;code&gt;.github/workflows/publish.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish to NPM&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v*'&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
      &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm/action-setup@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;9.0.0&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;18.20.0'&lt;/span&gt;
          &lt;span class="na"&gt;registry-url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://registry.npmjs.org'&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm install --frozen-lockfile&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm run typecheck&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm run build&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Verify version matches tag&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;TAG="${GITHUB_REF#refs/tags/}"&lt;/span&gt;
          &lt;span class="s"&gt;VERSION="${TAG#v}"&lt;/span&gt;
          &lt;span class="s"&gt;FILE_VERSION=$(node -p "require('./package.json').version")&lt;/span&gt;
          &lt;span class="s"&gt;if [ "$VERSION" != "$FILE_VERSION" ]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "Version mismatch: tag=$VERSION, package.json=$FILE_VERSION"&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish to NPM&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;NODE_AUTH_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.NPM_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pnpm publish --access public --provenance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important notes about this workflow:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;permissions: id-token: write&lt;/code&gt; enables NPM provenance (proves your package came from your GitHub repo)&lt;/li&gt;
&lt;li&gt;The version verification step prevents accidental publishes with mismatched versions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--frozen-lockfile&lt;/code&gt; ensures consistent installs&lt;/li&gt;
&lt;li&gt;Adjust &lt;code&gt;pnpm&lt;/code&gt; to &lt;code&gt;npm&lt;/code&gt; if you're not using pnpm&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 11: Publishing with the Automated Workflow
&lt;/h2&gt;

&lt;p&gt;Now that automation is set up, here's your new publishing workflow:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Make sure all changes are committed:&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;git status
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: add new feature"&lt;/span&gt;
git push origin master
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Create and push a version tag:&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;&lt;span class="c"&gt;# For a patch release (bug fixes)&lt;/span&gt;
npm version patch
git push origin master &lt;span class="nt"&gt;--tags&lt;/span&gt;

&lt;span class="c"&gt;# For a minor release (new features)&lt;/span&gt;
npm version minor
git push origin master &lt;span class="nt"&gt;--tags&lt;/span&gt;

&lt;span class="c"&gt;# For a major release (breaking changes)&lt;/span&gt;
npm version major
git push origin master &lt;span class="nt"&gt;--tags&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Watch it happen:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to your GitHub repository → "Actions" tab&lt;/li&gt;
&lt;li&gt;You'll see the workflow running&lt;/li&gt;
&lt;li&gt;Within a few minutes, your package is published!&lt;/li&gt;
&lt;li&gt;GitHub automatically creates a release too&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pro tip:&lt;/strong&gt; The moment you push the tag, GitHub Actions takes over. No manual &lt;code&gt;npm publish&lt;/code&gt; needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 12: Control What Gets Published
&lt;/h2&gt;

&lt;p&gt;Want to see exactly what goes into your package?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm pack &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Typically you want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dist/&lt;/code&gt; (your compiled code)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;README.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LICENSE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;code&gt;.npmignore&lt;/code&gt; to exclude:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source files (&lt;code&gt;src/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Tests&lt;/li&gt;
&lt;li&gt;Config files&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.github/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Development dependencies&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 13: Write Documentation That Actually Helps
&lt;/h2&gt;

&lt;p&gt;Your README is the first thing people see. Make it count:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Must-haves:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Quick installation command&lt;/li&gt;
&lt;li&gt;Basic usage example (copy-paste ready)&lt;/li&gt;
&lt;li&gt;API reference&lt;/li&gt;
&lt;li&gt;Common problems and solutions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Nice-to-haves:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Advanced examples&lt;/li&gt;
&lt;li&gt;Browser vs Node.js differences&lt;/li&gt;
&lt;li&gt;Performance tips&lt;/li&gt;
&lt;li&gt;Contribution guidelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good documentation increases adoption way more than perfect code does.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 14: Licensing and Collaboration
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Choose a license:&lt;/strong&gt; MIT is the most permissive and common for open-source libraries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable collaboration:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; with guidelines&lt;/li&gt;
&lt;li&gt;Use GitHub Issues for bug reports&lt;/li&gt;
&lt;li&gt;Consider GitHub Discussions for questions&lt;/li&gt;
&lt;li&gt;Tag releases properly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Note on releases:&lt;/strong&gt;&lt;br&gt;
With GitHub Actions set up, you don't need to manually create releases. The workflow automatically creates a GitHub release when you push a version tag. But you can still edit the release notes afterward to add detailed changelogs.&lt;/p&gt;

&lt;p&gt;If it is important to create &lt;code&gt;Releases&lt;/code&gt; manually, follow this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to your repo's "Releases" tab&lt;/li&gt;
&lt;li&gt;Click "Create new release"&lt;/li&gt;
&lt;li&gt;Tag it as &lt;code&gt;v1.x.x&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Add a changelog describing what changed&lt;/li&gt;
&lt;li&gt;Publish&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once the tag is pushed, GitHub Actions takes over and handles publishing.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 15: When Things Go Wrong
&lt;/h2&gt;

&lt;p&gt;Yes, mistakes happen. Accidentally published version 1.1.0 instead of 1.0.3? Been there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Within 24 hours:&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;npm unpublish @scope/package@version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After 24 hours:&lt;/strong&gt;&lt;br&gt;
NPM won't let you unpublish for safety reasons. Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm deprecate @scope/package@version &lt;span class="s2"&gt;"Broken release, use 1.0.4 instead"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then publish the correct version and update your changelog.&lt;/p&gt;

&lt;p&gt;Wait a minute or two after unpublishing — NPM's cache takes time to update.&lt;/p&gt;

&lt;p&gt;Lesson learned: always double-check before tagging and pushing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus: One-Command Publishing Scripts
&lt;/h2&gt;

&lt;p&gt;Want to make publishing even easier? Create these helper scripts that combine versioning and pushing in one command:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;publish-patch.sh:&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;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
npm version patch
git push origin master &lt;span class="nt"&gt;--tags&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;publish-minor.sh:&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;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
npm version minor
git push origin master &lt;span class="nt"&gt;--tags&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;publish-major.sh:&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;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
npm version major
git push origin master &lt;span class="nt"&gt;--tags&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make them executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x publish-&lt;span class="k"&gt;*&lt;/span&gt;.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now publishing is as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./publish-patch.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script bumps the version, creates the tag, and pushes it. GitHub Actions sees the tag and handles the rest — testing, building, and publishing to NPM automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Best Practices Checklist
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before Publishing:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Test with &lt;code&gt;npm pack&lt;/code&gt; and install locally&lt;/li&gt;
&lt;li&gt;✅ Run all tests and type checks&lt;/li&gt;
&lt;li&gt;✅ Update README with latest features&lt;/li&gt;
&lt;li&gt;✅ Add or update CHANGELOG&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Package Quality:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Include TypeScript types (if applicable)&lt;/li&gt;
&lt;li&gt;✅ Add a LICENSE file&lt;/li&gt;
&lt;li&gt;✅ Set up CI/CD for automated testing&lt;/li&gt;
&lt;li&gt;✅ Use &lt;code&gt;.npmignore&lt;/code&gt; to keep package lean&lt;/li&gt;
&lt;li&gt;✅ Include usage examples in README&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Maintenance:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Respond to issues promptly&lt;/li&gt;
&lt;li&gt;✅ Tag releases with semantic versions&lt;/li&gt;
&lt;li&gt;✅ Keep dependencies updated&lt;/li&gt;
&lt;li&gt;✅ Document breaking changes clearly&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Real-World Example: From Hobby Project to Published Package
&lt;/h2&gt;

&lt;p&gt;This guide is based on a real-world transformation: turning an HTML-to-PDF generator from a single-application solution into a framework-agnostic package that any developer can use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The package:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/@encryptioner/html-to-pdf-generator" rel="noopener noreferrer"&gt;&lt;code&gt;@encryptioner/html-to-pdf-generator&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it does:&lt;/strong&gt; Modern multi-page PDF generator from HTML content with smart pagination and styling support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The transformation journey:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Started as tightly coupled code in a single project&lt;/li&gt;
&lt;li&gt;Extracted the core logic into a standalone library&lt;/li&gt;
&lt;li&gt;Made it framework-agnostic (works with React, Vue, vanilla JS, Node.js)&lt;/li&gt;
&lt;li&gt;Added proper TypeScript types&lt;/li&gt;
&lt;li&gt;Set up automated publishing with GitHub Actions&lt;/li&gt;
&lt;li&gt;Documented everything in a useful README&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The outcome:&lt;/strong&gt;&lt;br&gt;
A package that solves a real problem for developers who need to generate clean, multi-page PDFs from HTML without fighting with browser print dialogs or complex PDF libraries.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @encryptioner/html-to-pdf-generator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This transition from "code that works for one project" to "code that works for everyone" demonstrates what successful package publishing looks like. The technical steps matter, but the mindset shift matters more — thinking about edge cases, documentation, and user experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Publishing your first NPM package is a rite of passage. It's messy, confusing, and occasionally frustrating — but incredibly rewarding.&lt;/p&gt;

&lt;p&gt;The moment you see someone install your package, report a bug, or (even better) contribute a feature — it all clicks. You're not just writing code anymore. You're building something that helps others build things.&lt;/p&gt;

&lt;p&gt;Don't wait for the perfect moment or the perfect code. Start with something useful, document it well, and iterate. The tools are better than ever, and the community is welcoming.&lt;/p&gt;

&lt;p&gt;And when something breaks at 2 AM (it will), don't panic. Unpublish within 24 hours, fix it, document what happened, and keep going.&lt;/p&gt;

&lt;p&gt;The open-source world needs more builders. So go ahead — publish that package you've been thinking about.&lt;/p&gt;

&lt;p&gt;Happy publishing! 🚀&lt;/p&gt;




&lt;h2&gt;
  
  
  Let's Connect
&lt;/h2&gt;

&lt;p&gt;I'm always excited to hear about what you're building! If you found this guide helpful, have questions, or just want to share your NPM publishing journey:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://encryptioner.github.io" rel="noopener noreferrer"&gt;encryptioner.github.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn&lt;/strong&gt;: &lt;a href="https://www.linkedin.com/in/mir-mursalin-ankur" rel="noopener noreferrer"&gt;Mir Mursalin Ankur&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/Encryptioner" rel="noopener noreferrer"&gt;@Encryptioner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X (Twitter)&lt;/strong&gt;: &lt;a href="https://twitter.com/AnkurMursalin" rel="noopener noreferrer"&gt;@AnkurMursalin&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical Writing&lt;/strong&gt;: &lt;a href="https://nerddevs.com/author/ankur/" rel="noopener noreferrer"&gt;Nerddevs&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop a message if you publish your first package using this guide — I'd love to check it out!&lt;/p&gt;

</description>
      <category>node</category>
      <category>npm</category>
      <category>development</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built My Own Shell for Fun. You Can Too</title>
      <dc:creator>Mir Mursalin Ankur</dc:creator>
      <pubDate>Fri, 01 Aug 2025 14:07:27 +0000</pubDate>
      <link>https://dev.to/mir_mursalin_ankur/i-built-my-own-shell-for-fun-you-can-too-56j3</link>
      <guid>https://dev.to/mir_mursalin_ankur/i-built-my-own-shell-for-fun-you-can-too-56j3</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.amazonaws.com%2Fuploads%2Farticles%2Fueqeskye4zs3fqh5yiiu.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.amazonaws.com%2Fuploads%2Farticles%2Fueqeskye4zs3fqh5yiiu.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ever wondered what happens when you type &lt;code&gt;ls&lt;/code&gt; and hit Enter? I did too. So I built my own shell to find out.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Why" Behind the Madness
&lt;/h2&gt;

&lt;p&gt;As a lead software engineer, I've spent years working with shells—Bash, Zsh—but I never really understood what was happening under the hood. Sure, I knew about processes, file descriptors, and system calls, but the actual mechanics of how a shell works? That was a black box.&lt;/p&gt;

&lt;p&gt;Then I stumbled upon &lt;a href="https://codingchallenges.fyi/challenges/intro" rel="noopener noreferrer"&gt;codingchallenges.fyi&lt;/a&gt; and their &lt;a href="https://codingchallenges.fyi/challenges/challenge-shell" rel="noopener noreferrer"&gt;shell challenge&lt;/a&gt;. It gave motivation to uncover the invisible scene of most common functionalities. It was my chance to peel back the layers and understand the fundamentals that power every terminal session.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a Shell, Really?
&lt;/h2&gt;

&lt;p&gt;When you type &lt;code&gt;ls -la&lt;/code&gt;, here's what happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read&lt;/strong&gt;: The shell reads your input from stdin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Parse&lt;/strong&gt;: It breaks your command into tokens (&lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;-la&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute&lt;/strong&gt;: It finds the &lt;code&gt;ls&lt;/code&gt; program and runs it with &lt;code&gt;-la&lt;/code&gt; as arguments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wait&lt;/strong&gt;: It waits for the program to finish&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeat&lt;/strong&gt;: It shows a new prompt, ready for your next command&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Simple, right? But the devil is in the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Journey Begins: &lt;a href="https://github.com/Encryptioner/ccsh-shell" rel="noopener noreferrer"&gt;ccsh&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;I decided to build &lt;strong&gt;ccsh&lt;/strong&gt; (Compact C Shell)—a minimal but functional Unix-like shell. Here's what I learned along the way:&lt;/p&gt;

&lt;h2&gt;
  
  
  Why C?
&lt;/h2&gt;

&lt;p&gt;C is the lingua franca of Unix systems. Every Linux distribution, macOS, and BSD variant comes with a C compiler. This means my shell can run anywhere without dependencies.&lt;/p&gt;

&lt;p&gt;But more importantly, C gives me direct access to the system calls that power everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fork()&lt;/code&gt; and &lt;code&gt;exec()&lt;/code&gt; for process creation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pipe()&lt;/code&gt; for inter-process communication
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dup2()&lt;/code&gt; for file descriptor manipulation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;waitpid()&lt;/code&gt; for process synchronization&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;signal()&lt;/code&gt; for handling interrupts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the same system calls that Bash, Zsh, and every other shell use under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI as My Learning Partner
&lt;/h2&gt;

&lt;p&gt;Here's the honest truth: &lt;strong&gt;AI generated most of my code, but I understood every line.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  How AI Helped Me:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generated boilerplate code structure&lt;/strong&gt; - I didn't start from scratch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explained complex system calls&lt;/strong&gt; - When I was stuck on &lt;code&gt;fork()&lt;/code&gt; vs &lt;code&gt;exec()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugged memory management issues&lt;/strong&gt; - C's manual memory management is tricky&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suggested best practices&lt;/strong&gt; - Error handling, code organization, security considerations&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Key Insight:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;AI is a tool, not a replacement.&lt;/strong&gt; Understanding the fundamentals is crucial. Here's what I learned:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;80% understanding is enough&lt;/strong&gt; - If this isn't your core job, you don't need to memorize every system call&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI accelerates learning&lt;/strong&gt; - When you know the basics, AI can help you build faster&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fundamentals matter&lt;/strong&gt; - You still need to understand what &lt;code&gt;fork()&lt;/code&gt; does, even if AI wrote the code&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  My Approach:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with understanding&lt;/strong&gt; - Learn what each system call does conceptually&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use AI for implementation&lt;/strong&gt; - Let AI handle the boilerplate and edge cases&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review and modify&lt;/strong&gt; - Understand AI generated lines. If necessary, get clarification from alternate source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test and debug&lt;/strong&gt; - Break things to understand them better&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;"AI generated the code, but I understood most of the lines. That's the sweet spot."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Shell
&lt;/h2&gt;

&lt;p&gt;I chose to implement everything in a single &lt;code&gt;main.c&lt;/code&gt; file for simplicity. Here's how to build it:&lt;/p&gt;

&lt;h3&gt;
  
  
  What is a Makefile?
&lt;/h3&gt;

&lt;p&gt;A Makefile is a configuration file that tells the &lt;a href="https://faculty.cs.niu.edu/~hutchins/csci480/make.htm" rel="noopener noreferrer"&gt;make&lt;/a&gt; utility how to build your project. It's like a recipe that automates the compilation process. Instead of typing long gcc commands, you just run &lt;code&gt;make&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simple Build
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcc &lt;span class="nt"&gt;-Wall&lt;/span&gt; &lt;span class="nt"&gt;-Wextra&lt;/span&gt; &lt;span class="nt"&gt;-std&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;c99 &lt;span class="nt"&gt;-g&lt;/span&gt; main.c &lt;span class="nt"&gt;-o&lt;/span&gt; ccsh &lt;span class="nt"&gt;-lreadline&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What each flag does:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-Wall -Wextra&lt;/code&gt;: Enable all warning messages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-std=c99&lt;/code&gt;: Use C99 standard&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-g&lt;/code&gt;: Include debug symbols&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-lreadline&lt;/code&gt;: Link against the readline library (for command history)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  With Makefile
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="c"&gt;# Compiler and flags
&lt;/span&gt;&lt;span class="nv"&gt;CC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; gcc
&lt;span class="nv"&gt;CFLAGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-Wall&lt;/span&gt; &lt;span class="nt"&gt;-Wextra&lt;/span&gt; &lt;span class="nt"&gt;-std&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;c99 &lt;span class="nt"&gt;-g&lt;/span&gt;
&lt;span class="nv"&gt;LDFLAGS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nt"&gt;-lreadline&lt;/span&gt;

&lt;span class="c"&gt;# Build target: ccsh depends on main.c
&lt;/span&gt;&lt;span class="nl"&gt;ccsh&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;main.c&lt;/span&gt;
    &lt;span class="p"&gt;$(&lt;/span&gt;CC&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;$(&lt;/span&gt;CFLAGS&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; ccsh main.c &lt;span class="p"&gt;$(&lt;/span&gt;LDFLAGS&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Clean up build artifacts
&lt;/span&gt;&lt;span class="nl"&gt;clean&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ccsh

&lt;span class="c"&gt;# Install to system (optional)
&lt;/span&gt;&lt;span class="nl"&gt;install&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;ccsh&lt;/span&gt;
    &lt;span class="nb"&gt;cp &lt;/span&gt;ccsh ~/.local/bin/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CC = gcc&lt;/code&gt;: Define the compiler&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CFLAGS&lt;/code&gt;: Compiler flags for warnings and standards&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LDFLAGS&lt;/code&gt;: Linker flags for external libraries&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ccsh: main.c&lt;/code&gt;: Target &lt;code&gt;ccsh&lt;/code&gt; depends on &lt;code&gt;main.c&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;clean&lt;/code&gt;: Remove built files&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Building and Running
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build the shell&lt;/span&gt;
make

&lt;span class="c"&gt;# Run the shell&lt;/span&gt;
./ccsh

&lt;span class="c"&gt;# Clean up&lt;/span&gt;
make clean
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Cool Features I Added
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Built-in Commands
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/directory    &lt;span class="c"&gt;# Change directory&lt;/span&gt;
&lt;span class="nb"&gt;pwd&lt;/span&gt;                      &lt;span class="c"&gt;# Print working directory&lt;/span&gt;
&lt;span class="nb"&gt;exit&lt;/span&gt;                     &lt;span class="c"&gt;# Exit shell&lt;/span&gt;
&lt;span class="nb"&gt;jobs&lt;/span&gt;                     &lt;span class="c"&gt;# List background jobs&lt;/span&gt;
&lt;span class="nb"&gt;fg&lt;/span&gt; %1                    &lt;span class="c"&gt;# Bring job to foreground&lt;/span&gt;
&lt;span class="nb"&gt;help&lt;/span&gt;                     &lt;span class="c"&gt;# Show help information&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Alias System
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;ll&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ls -lah"&lt;/span&gt;       &lt;span class="c"&gt;# Create alias&lt;/span&gt;
&lt;span class="nb"&gt;alias&lt;/span&gt;                    &lt;span class="c"&gt;# List all aliases&lt;/span&gt;
&lt;span class="nb"&gt;unalias &lt;/span&gt;ll               &lt;span class="c"&gt;# Remove alias&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Background Job Management
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;10 &amp;amp;               &lt;span class="c"&gt;# Run in background&lt;/span&gt;
&lt;span class="nb"&gt;jobs&lt;/span&gt;                     &lt;span class="c"&gt;# List background jobs&lt;/span&gt;
&lt;span class="nb"&gt;fg&lt;/span&gt; %1                    &lt;span class="c"&gt;# Bring to foreground&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. I/O Redirection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; output.txt          &lt;span class="c"&gt;# Redirect output&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &amp;lt; input.txt          &lt;span class="c"&gt;# Redirect input&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"hello"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; log.txt  &lt;span class="c"&gt;# Append output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Globbing Support
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt;.txt                 &lt;span class="c"&gt;# List all .txt files&lt;/span&gt;
&lt;span class="nb"&gt;cp &lt;/span&gt;file?.txt backup/     &lt;span class="c"&gt;# Copy files matching pattern&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The "Aha!" Moments
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Process Groups and Sessions
&lt;/h3&gt;

&lt;p&gt;I never fully understood how &lt;code&gt;Ctrl+C&lt;/code&gt; works until I implemented it. When you press &lt;code&gt;Ctrl+C&lt;/code&gt;, the kernel sends a SIGINT to the entire process group. The shell needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a new process group for each command&lt;/li&gt;
&lt;li&gt;Handle signals appropriately&lt;/li&gt;
&lt;li&gt;Restore the shell's process group when the command finishes&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Environment Variables
&lt;/h3&gt;

&lt;p&gt;I always knew about &lt;code&gt;PATH&lt;/code&gt;, but seeing how the shell searches for executables was enlightening. It's just a simple loop through directories, checking if the file exists and is executable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Challenges I Faced
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Memory Management Nightmares
&lt;/h3&gt;

&lt;p&gt;C doesn't have garbage collection, so every &lt;code&gt;malloc()&lt;/code&gt; needs a corresponding &lt;code&gt;free()&lt;/code&gt;. With complex command parsing and multiple processes, memory leaks were a constant threat.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal Handling Complexity
&lt;/h3&gt;

&lt;p&gt;Getting &lt;code&gt;Ctrl+C&lt;/code&gt; to work correctly was surprisingly tricky. The shell needs to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ignore SIGINT for itself&lt;/li&gt;
&lt;li&gt;Forward SIGINT to child processes&lt;/li&gt;
&lt;li&gt;Restore signal handlers after child completion&lt;/li&gt;
&lt;li&gt;Handle signals during pipeline execution&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cross-Platform Quirks
&lt;/h3&gt;

&lt;p&gt;While C is portable, Unix systems have subtle differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Different signal behavior between Linux and macOS&lt;/li&gt;
&lt;li&gt;Varying limits on file descriptors&lt;/li&gt;
&lt;li&gt;Different implementations of &lt;code&gt;readline&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deep Dive: How C Talks to the Operating System
&lt;/h2&gt;

&lt;p&gt;Let's trace through what happens when you type &lt;code&gt;ls -la&lt;/code&gt; in my shell:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Fork-Exec Dance
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;pid_t&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fork&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Child process&lt;/span&gt;
    &lt;span class="n"&gt;execvp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expanded&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="n"&gt;expanded&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;perror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"execvp"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Parent process&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Background job&lt;/span&gt;
        &lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[%d] %d&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;job_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;add_job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Foreground job - wait for completion&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;waitpid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;status&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What's happening:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fork()&lt;/code&gt; creates an exact copy of the current process&lt;/li&gt;
&lt;li&gt;The child gets a new process ID but identical memory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;execvp()&lt;/code&gt; replaces the child's memory with the &lt;code&gt;ls&lt;/code&gt; program&lt;/li&gt;
&lt;li&gt;The parent waits for the child to finish&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. How Redirection Works
&lt;/h3&gt;

&lt;p&gt;When you type &lt;code&gt;ls &amp;gt; output.txt&lt;/code&gt;, here's what happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* Handle output redirection */&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outfile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;O_WRONLY&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;O_CREAT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="n"&gt;O_APPEND&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="n"&gt;O_TRUNC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outfile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mo"&gt;0644&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
        &lt;span class="n"&gt;perror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
        &lt;span class="n"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;dup2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;STDOUT_FILENO&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The magic is &lt;code&gt;dup2(fd, STDOUT_FILENO)&lt;/code&gt;. This makes file descriptor 1 (stdout) point to the same place as our file descriptor.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Globbing Implementation
&lt;/h3&gt;

&lt;p&gt;For &lt;code&gt;ls *.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;expand_globs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;expanded_args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;expanded_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;expanded_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;glob_t&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gl_offs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gl_pathc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gl_pathv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&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="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GLOB_TILDE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="cm"&gt;/* Expand ~ to home directory */&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strchr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sc"&gt;'*'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;strchr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sc"&gt;'?'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="cm"&gt;/* Pattern contains glob characters */&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;expanded_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="n"&gt;GLOB_APPEND&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="cm"&gt;/* Glob failed, use original argument */&lt;/span&gt;
                &lt;span class="n"&gt;expanded_args&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;expanded_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="cm"&gt;/* Add all matched files */&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gl_pathc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;j&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="n"&gt;expanded_args&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;expanded_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gl_pathv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="cm"&gt;/* No glob characters, use as-is */&lt;/span&gt;
            &lt;span class="n"&gt;expanded_args&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;expanded_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;expanded_args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;expanded_count&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cm"&gt;/* Free the glob structure */&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gl_pathv&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;globfree&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;results&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I Learned About Real Shells
&lt;/h2&gt;

&lt;p&gt;Building my own shell gave me a new appreciation for tools like Bash and Zsh. They handle edge cases I never considered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Complex quoting rules&lt;/li&gt;
&lt;li&gt;Job control with multiple background processes&lt;/li&gt;
&lt;li&gt;Signal handling in pipelines&lt;/li&gt;
&lt;li&gt;Performance optimizations&lt;/li&gt;
&lt;li&gt;Cross-platform compatibility&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;Building your own shell isn't just an academic exercise. It's a practical way to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Process management&lt;/strong&gt;: How the OS handles processes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I/O systems&lt;/strong&gt;: File descriptors, pipes, redirection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signal handling&lt;/strong&gt;: How programs communicate with the OS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System calls&lt;/strong&gt;: The interface between user and kernel space&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build systems&lt;/strong&gt;: How to create robust, portable software&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;If you're interested in low-level programming, I highly recommend building a shell. Start simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Basic command execution&lt;/li&gt;
&lt;li&gt;Add argument parsing&lt;/li&gt;
&lt;li&gt;Implement I/O redirection&lt;/li&gt;
&lt;li&gt;Add pipes&lt;/li&gt;
&lt;li&gt;Build in job control&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://codingchallenges.fyi/challenges/challenge-shell" rel="noopener noreferrer"&gt;codingchallenges.fyi shell challenge&lt;/a&gt; is an excellent starting point. you can also check&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Encryptioner/ccsh-shell" rel="noopener noreferrer"&gt;My Shell Implementation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://man7.org/linux/man-pages/" rel="noopener noreferrer"&gt;Unix Programming Manual&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I'm thinking about adding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tab completion&lt;/li&gt;
&lt;li&gt;Better error handling&lt;/li&gt;
&lt;li&gt;More built-in commands&lt;/li&gt;
&lt;li&gt;Scripting capabilities&lt;/li&gt;
&lt;li&gt;Performance optimizations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But honestly, the learning experience was the real reward. Understanding how the tools I use daily actually work has made me a better engineer.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The next time you type a command, take a moment to appreciate the complexity hidden behind that simple prompt. And maybe, just maybe, consider building your own shell to see what's really happening under the hood.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Happy coding! 🐚&lt;/em&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  End:
&lt;/h2&gt;

&lt;p&gt;That's all!&lt;/p&gt;

&lt;p&gt;I hope you've found the article useful. You should try building your own shell if you haven't already. Feel free to share your thoughts in the comments below.&lt;/p&gt;

&lt;p&gt;Check more on&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://encryptioner.github.io" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/mir-mursalin-ankur" rel="noopener noreferrer"&gt;Linkedin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Encryptioner" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/AnkurMursalin" rel="noopener noreferrer"&gt;X (Twitter)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nerddevs.com/author/ankur/" rel="noopener noreferrer"&gt;Nerddevs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>programming</category>
      <category>c</category>
      <category>shell</category>
      <category>ai</category>
    </item>
    <item>
      <title>Integrating Bitbucket MCP with Cursor: A Practical Guide for Developers</title>
      <dc:creator>Mir Mursalin Ankur</dc:creator>
      <pubDate>Mon, 21 Jul 2025 15:44:40 +0000</pubDate>
      <link>https://dev.to/mir_mursalin_ankur/integrating-bitbucket-mcp-with-cursor-a-practical-guide-for-developers-1e5b</link>
      <guid>https://dev.to/mir_mursalin_ankur/integrating-bitbucket-mcp-with-cursor-a-practical-guide-for-developers-1e5b</guid>
      <description>&lt;p&gt;As developers, we all want our workflows to be smooth and efficient. Managing repositories, reviewing pull requests, and keeping track of changes can get tedious—especially when it means switching between your editor and browser. Recently, I set up the Bitbucket MCP (Model Context Protocol) server with Cursor IDE, and it’s made these tasks much more convenient. &lt;/p&gt;

&lt;p&gt;While Cursor supports &lt;a href="https://docs.cursor.com/bugbot" rel="noopener noreferrer"&gt;bugbot&lt;/a&gt; for GitHub PR reviews, there’s nothing similar out-of-the-box for &lt;a href="https://bitbucket.org/" rel="noopener noreferrer"&gt;bitbucket&lt;/a&gt; users. Setting up Bitbucket MCP with Cursor changed that for me—and made my dev life a lot smoother.&lt;/p&gt;

&lt;p&gt;Here’s a practical guide based on my experience, with tips that apply even if you use other tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Integrate Bitbucket MCP with Cursor?
&lt;/h2&gt;

&lt;p&gt;The main benefit is reducing context switching. With Bitbucket MCP, you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;List and manage repositories&lt;/li&gt;
&lt;li&gt;Create, review, and approve pull requests&lt;/li&gt;
&lt;li&gt;Access commit history and diffs&lt;/li&gt;
&lt;li&gt;Adjust repository settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All from within Cursor, so you spend more time coding and less time navigating web UIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What You’ll Need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cursor IDE&lt;/li&gt;
&lt;li&gt;Node.js v16 or higher&lt;/li&gt;
&lt;li&gt;Bitbucket account with repository access&lt;/li&gt;
&lt;li&gt;Bitbucket Repository Access Token (not an app password)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step-by-Step Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Install the &lt;a href="https://github.com/MatanYemini/bitbucket-mcp" rel="noopener noreferrer"&gt;Bitbucket MCP Server&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;In your project directory, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; bitbucket-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use &lt;code&gt;npm&lt;/code&gt; if you prefer.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Configure MCP
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;.cursor/mcp.json&lt;/code&gt; file in your project root. Example:&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;"mcpServers"&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;"bitbucket"&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;"npx"&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;"BITBUCKET_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://api.bitbucket.org/2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"BITBUCKET_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-repository-access-token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"BITBUCKET_WORKSPACE"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-workspace"&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;"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;"bitbucket-mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace the placeholders with your actual token and workspace name (not the URL).&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Get a Bitbucket Access Token
&lt;/h3&gt;

&lt;p&gt;Follow &lt;a href="https://support.atlassian.com/bitbucket-cloud/docs/create-a-repository-access-token/" rel="noopener noreferrer"&gt;this guide&lt;/a&gt; to generate a token. Make sure it has &lt;strong&gt;Read/Write&lt;/strong&gt; permissions for repositories and pull requests. Use a repository access token, not an app password.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Test Your Credentials
&lt;/h3&gt;

&lt;p&gt;Before moving on, it’s a good idea to verify your token. Here’s a simple Node.js script:&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;axios&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenCredentials&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.bitbucket.org/2.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-workspace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-repository-access-token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &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="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tokenCredentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/workspaces/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tokenCredentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;workspace&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;headers&lt;/span&gt;&lt;span class="p"&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;Authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tokenCredentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Token is valid! Workspace:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Token test failed:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;statusText&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save it as &lt;code&gt;scripts/test-bitbucket-credentials.js&lt;/code&gt; and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node scripts/test-bitbucket-credentials.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see your workspace name, you’re set.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Restart Cursor
&lt;/h3&gt;

&lt;p&gt;Close and reopen Cursor to load the new MCP server. You should see Bitbucket MCP under Settings → Tools &amp;amp; Integrations → MCP Tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everyday Usage
&lt;/h2&gt;

&lt;p&gt;Once set up, you can run Bitbucket commands from Cursor’s agent chat or command palette. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;List repos: &lt;code&gt;/bitbucket listRepositories&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Get repo details: &lt;code&gt;/bitbucket getRepository --workspace your-workspace --repo-slug your-repo&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Manage pull requests: &lt;code&gt;/bitbucket getPullRequests --workspace your-workspace --repo-slug your-repo&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also create or approve PRs, check commit diffs, and manage branching models—all from your editor.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP server not loading?&lt;/strong&gt; Double-check your &lt;code&gt;.cursor/mcp.json&lt;/code&gt; and environment variables.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth errors?&lt;/strong&gt; Make sure you’re using a repository access token (not an app password) and that it hasn’t expired.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commands not recognized?&lt;/strong&gt; Restart Cursor and verify the MCP server is loaded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to avoid confirmation prompts for MCP tools, enable “Auto-run MCP Tools” in Cursor’s chat settings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep Dive: How MCP Works (and How the Cursor and Bitbucket MCP Server Uses It)
&lt;/h2&gt;

&lt;p&gt;To really understand the power of this integration, it helps to know what’s happening under the hood.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is MCP?
&lt;/h3&gt;

&lt;p&gt;MCP stands for Model Context Protocol. It’s an open protocol designed to let tools (like editors, IDEs, or AI agents) communicate with code hosts (like GitHub, GitLab, Bitbucket, etc.) in a standardized way. Instead of every tool building its own custom integration for each code host, MCP defines a common set of commands and data structures for things like repositories, pull requests, branches, and commits.&lt;/p&gt;

&lt;p&gt;Think of MCP as a universal translator for developer tools. It lets your editor ask questions (“What pull requests are open?”) or take actions (“Create a new branch”) in a way that works across different platforms, as long as there’s an MCP server for that platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Does the Bitbucket MCP Server Leverage MCP?
&lt;/h3&gt;

&lt;p&gt;The Bitbucket MCP server is an implementation of this protocol, acting as a bridge between Cursor and Bitbucket’s API. Here’s how it works in practice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Command Flow&lt;/strong&gt;: When you run a Bitbucket command in Cursor (like listing repositories or creating a pull request), Cursor sends a standardized MCP request to the Bitbucket MCP server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translation Layer&lt;/strong&gt;: The MCP server receives this request and translates it into the appropriate Bitbucket API call, handling authentication and formatting as needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Exchange&lt;/strong&gt;: The server fetches the data from Bitbucket, then converts the response back into the MCP format so Cursor can display it in a consistent way, regardless of the underlying code host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt;: The MCP server uses your Bitbucket repository access token to authenticate API requests securely, so you never have to expose your credentials directly to Cursor or other tools.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This architecture means you get a seamless, unified experience in your editor, while the heavy lifting of talking to Bitbucket’s API is handled by the MCP server. It also makes it easier for new tools and platforms to integrate in the future—just implement the MCP protocol, and you’re good to go.&lt;/p&gt;

&lt;p&gt;In summary, MCP is the glue that connects your editor to your code host, and the Bitbucket MCP server is the translator that makes it all work smoothly for Bitbucket users.&lt;/p&gt;

&lt;h3&gt;
  
  
  How AI Chat Agents of Cursor Leverage MCP
&lt;/h3&gt;

&lt;p&gt;One of the most powerful aspects of MCP is how it enables AI chat agents (like those in Cursor) to interact with your repositories in a smart, automated way. Here’s how it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User Request&lt;/strong&gt;: You ask the AI agent to do something—like review a pull request, summarize recent changes, or list open issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Command Generation&lt;/strong&gt;: The agent formulates a standardized MCP command based on your request (for example, "get pull request details").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Retrieval&lt;/strong&gt;: The MCP server fetches the relevant data from Bitbucket, translates it into the MCP format, and returns it to the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Processing&lt;/strong&gt;: The agent analyzes the structured data—such as code diffs, comments, or commit history—and generates a human-friendly summary, review, or action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Action or Insight&lt;/strong&gt;: The agent presents its findings to you (e.g., a PR review summary, a list of suggested changes, or even automates a merge if you approve).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because MCP provides a consistent, structured interface, the AI agent doesn’t need to know the details of Bitbucket’s API. This makes it much easier to build powerful, context-aware automation and insights directly into your development workflow, regardless of which code host you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Broader Lessons
&lt;/h2&gt;

&lt;p&gt;Even if you use GitHub, GitLab, or another platform, the principle is the same: integrating your tools saves time and helps you stay focused. Investing a little effort in setup pays off in smoother workflows and fewer distractions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Integrating Bitbucket MCP with Cursor has made my daily work more efficient and less fragmented. If you’re looking to streamline your repo management, I recommend giving it a try—or finding similar integrations for your stack. The right tools should help you focus on what matters: building great software.&lt;/p&gt;

&lt;h2&gt;
  
  
  End
&lt;/h2&gt;

&lt;p&gt;That's all!&lt;/p&gt;

&lt;p&gt;I hope you've found the article useful. You should try to use &lt;code&gt;mcp server&lt;/code&gt; if you haven't already. It has many interesting use cases. Feel free to share your thoughts and experiences.&lt;/p&gt;

&lt;p&gt;Check more on&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://encryptioner.github.io" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/mir-mursalin-ankur" rel="noopener noreferrer"&gt;Linkedin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Encryptioner" rel="noopener noreferrer"&gt;Github&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://twitter.com/AnkurMursalin" rel="noopener noreferrer"&gt;X (Twitter)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nerddevs.com/author/ankur/" rel="noopener noreferrer"&gt;Nerddevs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>automation</category>
      <category>productivity</category>
      <category>devchallenge</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
