<feed xmlns="http://www.w3.org/2005/Atom"> <id>https://batsov.com/</id><title>(think)</title><subtitle>Bozhidar Batsov's personal blog</subtitle> <updated>2026-03-31T12:28:36+03:00</updated> <author> <name>Bozhidar Batsov</name> <uri>https://batsov.com/</uri> </author><link rel="self" type="application/atom+xml" href="https://batsov.com/feed.xml"/><link rel="alternate" type="text/html" hreflang="en" href="https://batsov.com/"/> <generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator> <rights> © 2026 Bozhidar Batsov </rights> <icon>/assets/img/favicons/favicon.ico</icon> <logo>/assets/img/favicons/favicon-96x96.png</logo> <entry><title>Batppuccin: My Take on Catppuccin for Emacs</title><link href="https://batsov.com/articles/2026/03/29/batppuccin-my-take-on-catppuccin-for-emacs/" rel="alternate" type="text/html" title="Batppuccin: My Take on Catppuccin for Emacs" /><published>2026-03-29T10:00:00+03:00</published> <updated>2026-03-29T10:00:00+03:00</updated> <id>https://batsov.com/articles/2026/03/29/batppuccin-my-take-on-catppuccin-for-emacs/</id> <content type="html"><![CDATA[<p>I promised I’d take a break from building Tree-sitter major modes, and I meant it. So what better way to relax than to build… color themes? Yeah, I know. My idea of chilling is weird, but I genuinely enjoy working on random Emacs packages. Most of the time at least…</p><h2 id="some-background">Some Background</h2><p>For a very long time my go-to Emacs themes were <a href="https://github.com/bbatsov/zenburn-emacs">Zenburn</a> and <a href="https://github.com/bbatsov/solarized-emacs">Solarized</a> – both of which I maintain popular Emacs ports for. Zenburn was actually one of my very first open source projects (created way back in 2010, when Emacs 24 was brand new). It served me well for years.</p><p>But at some point I got bored. You know the feeling – you’ve been staring at the same color palette for so long that you stop seeing it. My experiments with other editors (Helix, Zed, VS Code) introduced me to <a href="https://github.com/enkia/tokyo-night-vscode-theme">Tokyo Night</a> and <a href="https://github.com/catppuccin/catppuccin">Catppuccin</a>, and they’ve been my daily drivers since then.</p><p>Eventually, I ended up creating my own Emacs ports of both. I’ve already published <a href="https://github.com/bbatsov/emacs-tokyo-themes">emacs-tokyo-themes</a>, and I’ll write more about that one down the road. Today is all about Catppuccin. (and by this I totally mean Batppuccin!)</p><h2 id="why-another-catppuccin-port">Why Another Catppuccin Port?</h2><p>There’s already an <a href="https://github.com/catppuccin/emacs">official Catppuccin theme for Emacs</a>, and it works. So why build another one? A few reasons.</p><p>The official port registers a single <code class="language-plaintext highlighter-rouge">catppuccin</code> theme and switches between flavors (Mocha, Macchiato, Frappe, Latte) via a global variable and a reload function. This is unusual by Emacs standards and breaks the normal <code class="language-plaintext highlighter-rouge">load-theme</code> workflow – theme-switching packages like <code class="language-plaintext highlighter-rouge">circadian.el</code> need custom glue code to work with it. It also loads color definitions from an external file in a way that fails when Emacs hasn’t marked the theme as safe yet, which means some users can’t load the theme at all.</p><p>Beyond the architecture, there are style guide issues. <code class="language-plaintext highlighter-rouge">font-lock-variable-name-face</code> is set to the default text color, making variables invisible. All <code class="language-plaintext highlighter-rouge">outline-*</code> levels use the same blue, so org-mode headings are flat. <code class="language-plaintext highlighter-rouge">org-block</code> forces green on all unstyled code. Several faces still ship with <code class="language-plaintext highlighter-rouge">#ff00ff</code> magenta placeholder colors. And there’s no support for popular packages like vertico, marginalia, transient, flycheck, or cider.</p><p>I think some of this comes from the official port trying to match the structure of the Neovim version, which makes sense for their cross-editor tooling but doesn’t sit well with how Emacs does things.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p><h2 id="meet-batppuccin">Meet Batppuccin</h2><p><a href="https://github.com/bbatsov/batppuccin-emacs">Batppuccin</a> is my opinionated take on Catppuccin for Emacs. The name is a play on my last name (Batsov) + Catppuccin.<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup> I guess you can think of this as <code class="language-plaintext highlighter-rouge">@bbatsov</code>’s Catppuccin… or perhaps Batman’s Catppuccin?</p><p>The key differences from the official port:</p><p><strong>Four proper themes.</strong> <code class="language-plaintext highlighter-rouge">batppuccin-mocha</code>, <code class="language-plaintext highlighter-rouge">batppuccin-macchiato</code>, <code class="language-plaintext highlighter-rouge">batppuccin-frappe</code>, and <code class="language-plaintext highlighter-rouge">batppuccin-latte</code> are all separate themes that work with <code class="language-plaintext highlighter-rouge">load-theme</code> out of the box. No special reload dance needed.</p><p><strong>Faithful to the style guide.</strong> Mauve for keywords, green for strings, blue for functions, peach for constants, sky for operators, yellow for types, overlay2 for comments, rosewater for the cursor. The rainbow heading cycle (red, peach, yellow, green, sapphire, lavender) makes org-mode and outline headings actually distinguishable.</p><p><strong>Broad face coverage.</strong> Built-in Emacs faces plus magit, vertico, corfu, marginalia, embark, orderless, consult, transient, flycheck, cider, company, doom-modeline, treemacs, web-mode, and more. No placeholder colors.</p><p><strong>Clean architecture.</strong> Shared infrastructure in <code class="language-plaintext highlighter-rouge">batppuccin-themes.el</code>, thin wrapper files for each flavor, color override mechanism, configurable heading scaling. The same pattern I use in <a href="https://github.com/bbatsov/zenburn-emacs">zenburn-emacs</a> and <a href="https://github.com/bbatsov/emacs-tokyo-night-theme">emacs-tokyo-night-theme</a>.</p><p>I didn’t really re-invent anything here - I just created a theme in a way I’m comfortable with.</p><blockquote class="prompt-info"><p>I’m not going to bother with screenshots here – it looks like Catppuccin, because it <em>is</em> Catppuccin. There are small visual differences if you know where to look (headings, variables, a few face tweaks), but most people wouldn’t notice them side by side. If you’ve seen Catppuccin, you know what to expect.</p></blockquote><h2 id="installation">Installation</h2><p>The easiest way to install it right now:</p><div class="language-emacs-lisp highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre><td class="rouge-code"><pre><span class="p">(</span><span class="nb">use-package</span> <span class="nv">batppuccin-mocha-theme</span>
  <span class="ss">:vc</span> <span class="p">(</span><span class="ss">:url</span> <span class="s">"https://github.com/bbatsov/batppuccin-emacs"</span> <span class="ss">:rev</span> <span class="ss">:newest</span><span class="p">)</span>
  <span class="ss">:config</span>
  <span class="p">(</span><span class="nv">load-theme</span> <span class="ss">'batppuccin-mocha</span> <span class="no">t</span><span class="p">))</span>
</pre></div></div><p>Replace <code class="language-plaintext highlighter-rouge">mocha</code> with <code class="language-plaintext highlighter-rouge">macchiato</code>, <code class="language-plaintext highlighter-rouge">frappe</code>, or <code class="language-plaintext highlighter-rouge">latte</code> for the other flavors. You can also switch interactively with <code class="language-plaintext highlighter-rouge">M-x batppuccin-select</code>.</p><h2 id="there-can-never-be-enough-theme-ports">There Can Never Be Enough Theme Ports</h2><p>I remember when Solarized was the hot new thing and there were something like five competing Emacs ports of it. People had strong opinions about which one got the colors right, which one had better org-mode support, which one worked with their favorite completion framework. And that was fine! Different ports serve different needs and different tastes.</p><p>The same applies here. The official Catppuccin port is perfectly usable for a lot of people. Batppuccin is for people who want something more idiomatic to Emacs, with broader face coverage and stricter adherence to the upstream style guide. Both can coexist happily.</p><p>I’ve said many times that for me the best aspect of Emacs is that you can tweak it infinitely to make it your own, so as far as I’m concerned having a theme that you’re the only user of is perfectly fine. That being said, I hope a few of you will appreciate my take on Catppuccin as well.</p><h2 id="wrapping-up">Wrapping Up</h2><p>This is an early release and there’s plenty of room for improvement. I’m sure there are faces I’ve missed, colors that could be tweaked, and packages that deserve better support. If you try it out and something looks off, please <a href="https://github.com/bbatsov/batppuccin-emacs/issues">open an issue</a> or send a PR.</p><p>I’m also curious – what are your favorite Emacs themes these days? Still rocking Zenburn? Converted to modus-themes? Something else entirely? I’d love to hear about it.</p><p>That’s all from me, folks! Keep hacking!</p><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1"><p>The official port uses Catppuccin’s <a href="https://github.com/catppuccin/toolbox/tree/main/whiskers">Whiskers</a> template tool to generate the Elisp from a <code class="language-plaintext highlighter-rouge">.tera</code> template, which is cool for keeping ports in sync across editors but means the generated code doesn’t follow Emacs conventions. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p><li id="fn:2"><p>Naming is hard, but it should also be fun! Also – I’m a huge fan of Batman. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p></ol></div>]]></content> <author> <name>bbatsov</name> </author> <summary>I promised I’d take a break from building Tree-sitter major modes, and I meant it. So what better way to relax than to build… color themes? Yeah, I know. My idea of chilling is weird, but I genuinely enjoy working on random Emacs packages. Most of the time at least… Some Background For a very long time my go-to Emacs themes were Zenburn and Solarized – both of which I maintain popular Emacs p...</summary> </entry> <entry><title>fsharp-ts-mode: A Modern Emacs Mode for F#</title><link href="https://batsov.com/articles/2026/03/27/fsharp-ts-mode-a-modern-emacs-mode-for-fsharp/" rel="alternate" type="text/html" title="fsharp-ts-mode: A Modern Emacs Mode for F#" /><published>2026-03-27T17:00:00+02:00</published> <updated>2026-03-27T17:00:00+02:00</updated> <id>https://batsov.com/articles/2026/03/27/fsharp-ts-mode-a-modern-emacs-mode-for-fsharp/</id> <content type="html"><![CDATA[<p>I’m pretty much done with the focused development push on <a href="https://github.com/bbatsov/neocaml">neocaml</a> – it’s reached a point where I’m genuinely happy using it daily and the remaining work is mostly incremental polish. So naturally, instead of taking a break I decided it was time to start another project that’s been living in the back of my head for a while: a proper Tree-sitter-based F# mode for Emacs.</p><p>Meet <a href="https://github.com/bbatsov/fsharp-ts-mode">fsharp-ts-mode</a>.</p><h2 id="why-f">Why F#?</h2><p>I’ve written before about my fondness for the ML family of languages, and while OCaml gets most of my attention, last year I developed a soft spot for F#. In some ways I like it even a bit more than OCaml – the tooling is excellent, the .NET ecosystem is massive, and computation expressions are one of the most elegant abstractions I’ve seen in any language. F# manages to feel both practical and beautiful, which is a rare combination.</p><p>The problem is that Emacs has never been particularly popular with F# programmers – or .NET programmers in general. The existing <a href="https://github.com/fsharp/emacs-fsharp-mode">fsharp-mode</a> works, but it’s showing its age: regex-based highlighting, SMIE indentation with quirks, and some legacy code dating back to the caml-mode days. I needed a good F# mode for Emacs, and that’s enough of a reason to build one in my book.</p><h2 id="the-name">The Name</h2><p>I’ll be honest – I spent quite a bit of time trying to come up with a clever name.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> Some candidates that didn’t make the cut:</p><ul><li><strong>fsharpe-mode</strong> (fsharp(evolved/enhanced)-mode)<li><strong>Fa Dièse</strong> (French for F sharp – because after spending time with OCaml you start thinking in French, apparently)<li><strong>fluoride</strong> (a play on <a href="https://ionide.io/">Ionide</a>, the popular F# IDE extension)</ul><p>In the end none of my fun ideas stuck, so I went with the boring-but-obvious <code class="language-plaintext highlighter-rouge">fsharp-ts-mode</code>. Sometimes the straightforward choice is the right one. At least nobody will have trouble finding it.<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p><h2 id="built-on-neocamls-foundation">Built on neocaml’s Foundation</h2><p>I modeled <code class="language-plaintext highlighter-rouge">fsharp-ts-mode</code> directly after <code class="language-plaintext highlighter-rouge">neocaml</code>, and the two packages share a lot of structural similarities – which shouldn’t be surprising given how much OCaml and F# have in common. The same architecture (base mode + language-specific derived modes), the same approach to font-locking (shared + grammar-specific rules), the same REPL integration pattern (<code class="language-plaintext highlighter-rouge">comint</code> with tree-sitter input highlighting), the same build system interaction pattern (minor mode wrapping CLI commands).</p><p>This also meant I could get the basics in place really quickly. Having already solved problems like trailing comment indentation, <code class="language-plaintext highlighter-rouge">forward-sexp</code> hybrid navigation, and <code class="language-plaintext highlighter-rouge">imenu</code> with qualified names in neocaml, porting those solutions to F# was mostly mechanical.</p><h2 id="whats-in-010">What’s in 0.1.0</h2><p>The initial release covers all the essentials:</p><ul><li><strong>Syntax highlighting</strong> via Tree-sitter with 4 customizable levels, supporting <code class="language-plaintext highlighter-rouge">.fs</code>, <code class="language-plaintext highlighter-rouge">.fsx</code>, and <code class="language-plaintext highlighter-rouge">.fsi</code> files<li><strong>Indentation</strong> via Tree-sitter indent rules<li><strong>Imenu</strong> with fully-qualified names (e.g., <code class="language-plaintext highlighter-rouge">MyModule.myFunc</code>)<li><strong>Navigation</strong> – <code class="language-plaintext highlighter-rouge">beginning-of-defun</code>, <code class="language-plaintext highlighter-rouge">end-of-defun</code>, <code class="language-plaintext highlighter-rouge">forward-sexp</code><li><strong>F# Interactive (REPL)</strong> integration with tree-sitter highlighting for input<li><strong>dotnet CLI integration</strong> – build, test, run, clean, format, restore, with watch mode support<li><strong>.NET API documentation lookup</strong> at point (<code class="language-plaintext highlighter-rouge">C-c C-d</code>)<li><strong>Eglot integration</strong> for <a href="https://github.com/fsharp/FsAutoComplete">FsAutoComplete</a><li><strong>Compilation error parsing</strong> for <code class="language-plaintext highlighter-rouge">dotnet build</code> output<li>Shift region left/right, auto-detect indent offset, prettify symbols, outline mode, and more</ul><h3 id="migrating-from-fsharp-mode">Migrating from fsharp-mode</h3><p>If you’re currently using <code class="language-plaintext highlighter-rouge">fsharp-mode</code>, switching is straightforward:</p><div class="language-emacs-lisp highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre><td class="rouge-code"><pre><span class="p">(</span><span class="nb">use-package</span> <span class="nv">fsharp-ts-mode</span>
  <span class="ss">:vc</span> <span class="p">(</span><span class="ss">:url</span> <span class="s">"https://github.com/bbatsov/fsharp-ts-mode"</span> <span class="ss">:rev</span> <span class="ss">:newest</span><span class="p">))</span>
</pre></div></div><p>The main thing <code class="language-plaintext highlighter-rouge">fsharp-ts-mode</code> doesn’t have yet is automatic LSP server installation (the <code class="language-plaintext highlighter-rouge">eglot-fsharp</code> package does this for <code class="language-plaintext highlighter-rouge">fsharp-mode</code>). You’ll need to install FsAutoComplete yourself:</p><pre><code class="language-shellsession">$ dotnet tool install -g fsautocomplete
</code></pre><p>After that, <code class="language-plaintext highlighter-rouge">(add-hook 'fsharp-ts-mode-hook #'eglot-ensure)</code> is all you need.</p><p>See the <a href="https://github.com/bbatsov/fsharp-ts-mode#migrating-from-fsharp-mode">migration guide</a> in the README for a detailed comparison.</p><h2 id="lessons-learned">Lessons Learned</h2><p>Working with the <a href="https://github.com/ionide/tree-sitter-fsharp">ionide/tree-sitter-fsharp</a> grammar surfaced some interesting challenges compared to the OCaml grammar:</p><h3 id="fs-indentation-sensitive-syntax-is-tricky">F#’s indentation-sensitive syntax is tricky</h3><p>Unlike OCaml, where indentation is purely cosmetic, F# uses significant whitespace (the “offside rule”). The tree-sitter grammar needs correct indentation to parse correctly, which creates a chicken-and-egg problem: you need a correct parse tree to indent, but you need correct indentation to parse. For example, if you paste this unindented block:</p><div class="language-fsharp highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre><td class="rouge-code"><pre><span class="k">let</span> <span class="n">f</span> <span class="n">x</span> <span class="p">=</span>
<span class="k">if</span> <span class="n">x</span> <span class="p">&amp;gt;</span> <span class="mi">0</span> <span class="k">then</span>
<span class="n">x</span> <span class="o">+</span> <span class="mi">1</span>
<span class="k">else</span>
<span class="mi">0</span>
</pre></div></div><p>The parser can’t tell that <code class="language-plaintext highlighter-rouge">if</code> is the body of <code class="language-plaintext highlighter-rouge">f</code> or that <code class="language-plaintext highlighter-rouge">x + 1</code> belongs to the <code class="language-plaintext highlighter-rouge">then</code> branch – it produces ERROR nodes everywhere, and <code class="language-plaintext highlighter-rouge">indent-region</code> has nothing useful to work with. But if you’re typing the code line by line, the parser always has enough context from preceding lines to indent the current line correctly. This is a fundamental limitation of any indentation-sensitive grammar.</p><h3 id="the-two-grammars-are-more-different-than-youd-expect">The two grammars are more different than you’d expect</h3><p>OCaml’s tree-sitter-ocaml-interface grammar inherits from the base grammar, so you can share queries freely. F#’s <code class="language-plaintext highlighter-rouge">fsharp</code> and <code class="language-plaintext highlighter-rouge">fsharp_signature</code> grammars are independent with different node types and field names for equivalent concepts. For instance, a <code class="language-plaintext highlighter-rouge">let</code> binding is <code class="language-plaintext highlighter-rouge">function_or_value_defn</code> in the <code class="language-plaintext highlighter-rouge">.fs</code> grammar but <code class="language-plaintext highlighter-rouge">value_definition</code> in the <code class="language-plaintext highlighter-rouge">.fsi</code> grammar. Type names use a <code class="language-plaintext highlighter-rouge">type_name:</code> field in one grammar but not the other. Even some keyword tokens (<code class="language-plaintext highlighter-rouge">of</code>, <code class="language-plaintext highlighter-rouge">open</code>, <code class="language-plaintext highlighter-rouge">type</code>) that work fine as query matches in <code class="language-plaintext highlighter-rouge">fsharp</code> fail at runtime in <code class="language-plaintext highlighter-rouge">fsharp_signature</code>.</p><p>This forced me to split font-lock rules into shared and grammar-specific sets – more code, more testing, more edge cases.</p><h3 id="script-files-are-weird">Script files are weird</h3><p>F# script (<code class="language-plaintext highlighter-rouge">.fsx</code>) files without a <code class="language-plaintext highlighter-rouge">module</code> declaration can mix <code class="language-plaintext highlighter-rouge">let</code> bindings with bare expressions like <code class="language-plaintext highlighter-rouge">printfn</code>. The grammar doesn’t expect a declaration after a bare expression at the top level, so it chains everything into nested <code class="language-plaintext highlighter-rouge">application_expression</code> nodes:</p><div class="language-fsharp highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre><td class="rouge-code"><pre><span class="k">let</span> <span class="n">x</span> <span class="p">=</span> <span class="mi">1</span>
<span class="n">printfn</span> <span class="s2">"%d"</span> <span class="n">x</span>    <span class="c1">// bare expression</span>
<span class="k">let</span> <span class="n">y</span> <span class="p">=</span> <span class="mi">2</span>         <span class="c1">// grammar nests this under the printfn node</span>
</pre></div></div><p>Each subsequent <code class="language-plaintext highlighter-rouge">let</code> ends up one level deeper, causing progressive indentation. I worked around this with a heuristic that detects declarations whose ancestor chain leads back to <code class="language-plaintext highlighter-rouge">file</code> through these misparented nodes and forces them to column 0. Shebangs (<code class="language-plaintext highlighter-rouge">#!/usr/bin/env dotnet fsi</code>) required a different trick – excluding the first line from the parser’s range entirely via <code class="language-plaintext highlighter-rouge">treesit-parser-set-included-ranges</code>.</p><p>I’ve filed issues upstream for the grammar pain points – hopefully they’ll improve over time.</p><h2 id="current-status">Current Status</h2><p>Let me be upfront: this is a 0.1.0 release and it’s probably quite buggy. I’ve tested it against a reasonable set of F# code, but there are certainly indentation edge cases, font-lock gaps, and interactions I haven’t encountered yet. If you try it and something looks wrong, please <a href="https://github.com/bbatsov/fsharp-ts-mode/issues">open an issue</a> – <code class="language-plaintext highlighter-rouge">M-x fsharp-ts-mode-bug-report-info</code> will collect the environment details for you.</p><p>The package can currently be installed only from GitHub (via <code class="language-plaintext highlighter-rouge">package-vc-install</code> or manually). I’ve filed a <a href="https://github.com/melpa/melpa/pull/9917">PR with MELPA</a> and I hope it will get merged soon.</p><h2 id="wrapping-up">Wrapping Up</h2><p>I really need to take a break from building Tree-sitter major modes at this point. Between <code class="language-plaintext highlighter-rouge">clojure-ts-mode</code>, <code class="language-plaintext highlighter-rouge">neocaml</code>, <code class="language-plaintext highlighter-rouge">asciidoc-mode</code>, and now <code class="language-plaintext highlighter-rouge">fsharp-ts-mode</code>, I’ve spent a lot of time staring at tree-sitter node types and indent rules.<sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup> It’s been fun, but I think I’ve earned a vacation from <code class="language-plaintext highlighter-rouge">treesit-font-lock-rules</code>.</p><p>I really wanted to do something nice for the (admittedly small) F#-on-Emacs community, and a modern major mode seemed like the most meaningful contribution I could make. I hope some of you find it useful!</p><p>That’s all from me, folks! Keep hacking!</p><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1"><p>Way more time than I needed to actually implement the mode. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p><li id="fn:2"><p>Many people pointed out they thought <code class="language-plaintext highlighter-rouge">neocaml</code> was some package for neovim. Go figure why! <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p><li id="fn:3"><p>I’ve also been helping a bit with <a href="https://github.com/erlang/emacs-erlang-ts">erlang-ts-mode</a> recently. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p></ol></div>]]></content> <author> <name>bbatsov</name> </author> <summary>I’m pretty much done with the focused development push on neocaml – it’s reached a point where I’m genuinely happy using it daily and the remaining work is mostly incremental polish. So naturally, instead of taking a break I decided it was time to start another project that’s been living in the back of my head for a while: a proper Tree-sitter-based F# mode for Emacs. Meet fsharp-ts-mode. Why...</summary> </entry> <entry><title>Neocaml 0.6: Opam, Dune, and More</title><link href="https://batsov.com/articles/2026/03/25/neocaml-0-6-opam-dune-and-more/" rel="alternate" type="text/html" title="Neocaml 0.6: Opam, Dune, and More" /><published>2026-03-25T10:00:00+02:00</published> <updated>2026-03-25T10:00:00+02:00</updated> <id>https://batsov.com/articles/2026/03/25/neocaml-0-6-opam-dune-and-more/</id> <content type="html"><![CDATA[<p>When I released <a href="/articles/2026/02/14/neocaml-0-1-ready-for-action/">neocaml 0.1</a> last month, I thought I was more or less done with the (main) features for the foreseeable future. The original scope was deliberately small — a couple of Tree-sitter-powered OCaml major modes (for <code class="language-plaintext highlighter-rouge">.ml</code> and <code class="language-plaintext highlighter-rouge">.mli</code>), a REPL integration, and not much else. I was quite happy with how things turned out and figured the next steps would be mostly polish and bug fixes.</p><p>Versions 0.2-0.5 brought polish and bug fixes, but fundamentally the feature set stayed the same. I was even more convinced a grand 1.0 release was just around the corner.</p><p>I was wrong.</p><p>Of course, OCaml files don’t exist in isolation. They live alongside <a href="https://opam.ocaml.org/">Opam</a> files that describe packages and <a href="https://dune.readthedocs.io/">Dune</a> files that configure builds. And as I was poking around the Tree-sitter ecosystem, I discovered that there were already grammars for both <a href="https://github.com/tmcgilchrist/tree-sitter-opam">Opam</a> and <a href="https://github.com/tmcgilchrist/tree-sitter-dune">Dune</a> files. Given how simple both formats are (Opam is mostly key-value pairs, Dune is s-expressions), adding support for them turned out to be fairly straightforward.</p><p>So here we are with neocaml 0.6, which is quite a bit bigger than I expected originally.</p><p><strong>Note:</strong> One thing worth mentioning — all the new modes are completely isolated from the core OCaml modes. They’re separate files with no hard dependency on <code class="language-plaintext highlighter-rouge">neocaml-mode</code>, loaded only when you open the relevant file types. I didn’t want to force them upon anyone — for me it’s convenient to get Opam and Dune support out-of-the-box (given how ubiquitous they are in the OCaml ecosystem), but I totally get it if someone doesn’t care about this.</p><p>Let me walk you through what’s new.</p><h2 id="neocaml-opam-mode">neocaml-opam-mode</h2><p>The new <code class="language-plaintext highlighter-rouge">neocaml-opam-mode</code> activates automatically for <code class="language-plaintext highlighter-rouge">.opam</code> and <code class="language-plaintext highlighter-rouge">opam</code> files. It provides:</p><ul><li>Tree-sitter-based font-lock (field names, strings, operators, version constraints, filter expressions, etc.)<li>Indentation (lists, sections, option braces)<li>Imenu for navigating variables and sections<li>A <strong>flymake backend</strong> that runs <code class="language-plaintext highlighter-rouge">opam lint</code> on the current buffer, giving you inline diagnostics for missing fields, deprecated constructs, and syntax errors</ul><p>The flymake backend registers automatically when <code class="language-plaintext highlighter-rouge">opam</code> is found in your PATH, but you need to enable <code class="language-plaintext highlighter-rouge">flymake-mode</code> yourself:</p><div class="language-emacs-lisp highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre><td class="rouge-code"><pre><span class="p">(</span><span class="nv">add-hook</span> <span class="ss">'neocaml-opam-mode-hook</span> <span class="nf">#'</span><span class="nv">flymake-mode</span><span class="p">)</span>
</pre></div></div><p><a href="https://github.com/flycheck/flycheck">Flycheck</a> users get <code class="language-plaintext highlighter-rouge">opam lint</code> support out of the box via Flycheck’s built-in <code class="language-plaintext highlighter-rouge">opam</code> checker — no extra configuration needed.</p><p>This bridges some of the gap with <a href="https://github.com/ocaml/tuareg">Tuareg</a>, which also bundles an Opam major mode (<code class="language-plaintext highlighter-rouge">tuareg-opam-mode</code>). The Tree-sitter-based approach gives us more accurate highlighting, and the flymake integration is a nice bonus on top.</p><h2 id="neocaml-dune-mode">neocaml-dune-mode</h2><p><code class="language-plaintext highlighter-rouge">neocaml-dune-mode</code> handles <code class="language-plaintext highlighter-rouge">dune</code>, <code class="language-plaintext highlighter-rouge">dune-project</code>, and <code class="language-plaintext highlighter-rouge">dune-workspace</code> files — all three use the same s-expression syntax and share a single Tree-sitter grammar. You get:</p><ul><li>Font-lock for stanza names, field names, action keywords, strings, module names, library names, operators, and brackets<li>Indentation with 1-space offset<li>Imenu for stanza navigation<li>Defun navigation and <code class="language-plaintext highlighter-rouge">which-func</code> support</ul><p>This removes the need to install the separate <a href="https://melpa.org/#/dune">dune</a> package (the standalone <code class="language-plaintext highlighter-rouge">dune-mode</code> maintained by the Dune developers) from MELPA. If you prefer to keep using it, that’s fine too — neocaml’s README has <a href="https://github.com/bbatsov/neocaml#using-the-legacy-dune-mode">instructions</a> for overriding the <code class="language-plaintext highlighter-rouge">auto-mode-alist</code> entries.</p><h2 id="neocaml-dune-interaction-mode">neocaml-dune-interaction-mode</h2><p>Beyond editing Dune files, I wanted a simple way to run Dune commands from any neocaml buffer. <code class="language-plaintext highlighter-rouge">neocaml-dune-interaction-mode</code> is a minor mode that provides keybindings (under <code class="language-plaintext highlighter-rouge">C-c C-d</code>) and a “Dune” menu for common operations:</p><table><thead><tr><th>Keybinding<th>Command<tbody><tr><td><code class="language-plaintext highlighter-rouge">C-c C-d b</code><td>Build<tr><td><code class="language-plaintext highlighter-rouge">C-c C-d t</code><td>Test<tr><td><code class="language-plaintext highlighter-rouge">C-c C-d c</code><td>Clean<tr><td><code class="language-plaintext highlighter-rouge">C-c C-d p</code><td>Promote<tr><td><code class="language-plaintext highlighter-rouge">C-c C-d f</code><td>Format<tr><td><code class="language-plaintext highlighter-rouge">C-c C-d u</code><td>Launch utop with project libraries<tr><td><code class="language-plaintext highlighter-rouge">C-c C-d r</code><td>Run an executable<tr><td><code class="language-plaintext highlighter-rouge">C-c C-d d</code><td>Run any Dune command<tr><td><code class="language-plaintext highlighter-rouge">C-c C-d .</code><td>Find the nearest <code class="language-plaintext highlighter-rouge">dune</code> file</table><p>All commands run via Emacs’s <code class="language-plaintext highlighter-rouge">compile</code>, so you get error navigation, clickable source locations, and the full <code class="language-plaintext highlighter-rouge">compilation-mode</code> interface for free. With a prefix argument (<code class="language-plaintext highlighter-rouge">C-u</code>), build, test, and fmt run in <strong>watch mode</strong> (<code class="language-plaintext highlighter-rouge">--watch</code>), automatically rebuilding when files change.</p><p>The <code class="language-plaintext highlighter-rouge">utop</code> command is special — it launches through <code class="language-plaintext highlighter-rouge">neocaml-repl</code>, so you get the full REPL integration (send region, send definition, etc.) with your project’s libraries preloaded.</p><p>This mode is completely independent from <code class="language-plaintext highlighter-rouge">neocaml-dune-mode</code> — it doesn’t care which major mode you’re using. You can enable it in OCaml buffers like this:</p><div class="language-emacs-lisp highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre><td class="rouge-code"><pre><span class="p">(</span><span class="nv">add-hook</span> <span class="ss">'neocaml-base-mode-hook</span> <span class="nf">#'</span><span class="nv">neocaml-dune-interaction-mode</span><span class="p">)</span>
</pre></div></div><h2 id="rough-edges">Rough Edges</h2><p>Both the Opam and Dune Tree-sitter grammars are relatively young and will need some more work for optimal results. I’ve been filing issues and contributing patches upstream to improve them — for instance, the Dune grammar currently <a href="https://github.com/tmcgilchrist/tree-sitter-dune/issues/9">flattens field-value pairs</a> in a way that makes indentation less precise than it could be, and neither grammar supports <a href="https://github.com/tmcgilchrist/tree-sitter-dune/issues/10">variable interpolation</a> (<code class="language-plaintext highlighter-rouge">%{...}</code>) yet. These are very solvable problems and I expect the grammars to improve over time.</p><h2 id="whats-next">What’s Next?</h2><p>At this point I think I’m (finally!) out of ideas for new functionality. This time I mean it! Neocaml now covers pretty much everything I ever wanted, especially when paired with the awesome <a href="https://github.com/tarides/ocaml-eglot">ocaml-eglot</a>.</p><p>Down the road there might be support for OCamllex (<code class="language-plaintext highlighter-rouge">.mll</code>) or Menhir (<code class="language-plaintext highlighter-rouge">.mly</code>) files, but only if adding them doesn’t bring significant complexity — both are mixed languages with embedded OCaml code, which makes them fundamentally harder to support well than the simple Opam and Dune formats.</p><p>I hope OCaml programmers will find the new functionality useful. If you’re using neocaml, I’d love to hear how it’s working for you — bug reports, feature requests, and general feedback are all welcome on <a href="https://github.com/bbatsov/neocaml">GitHub</a>. You can find the full list of changes in the <a href="https://github.com/bbatsov/neocaml/blob/main/CHANGELOG.md">changelog</a>.</p><p>As usual — update from <a href="https://melpa.org/#/neocaml">MELPA</a>, kick the tires, and let me know what you think.</p><p>That’s all I have for you today! Keep hacking!</p>]]></content> <author> <name>bbatsov</name> </author> <summary>When I released neocaml 0.1 last month, I thought I was more or less done with the (main) features for the foreseeable future. The original scope was deliberately small — a couple of Tree-sitter-powered OCaml major modes (for .ml and .mli), a REPL integration, and not much else. I was quite happy with how things turned out and figured the next steps would be mostly polish and bug fixes. Versio...</summary> </entry> <entry><title>One Year with the HHKB: A Mini Review</title><link href="https://batsov.com/articles/2026/03/18/one-year-with-the-hhkb/" rel="alternate" type="text/html" title="One Year with the HHKB: A Mini Review" /><published>2026-03-18T09:56:00+02:00</published> <updated>2026-03-18T09:56:00+02:00</updated> <id>https://batsov.com/articles/2026/03/18/one-year-with-the-hhkb/</id> <content type="html"><![CDATA[<blockquote><p>The keyboard is the most important tool for a programmer. Choose wisely.</p></blockquote><p>I’m a keyboard nerd. I’ve owned several great keyboards over the years, starting with the legendary <a href="https://deskthority.net/wiki/Das_Keyboard_III">Das Keyboard 3 Ultimate</a> (blank keys and Cherry MX Blue switches – my co-workers <em>loved</em> me), then moving through the <a href="https://www.daskeyboard.com/model-4-ultimate/">Das Keyboard 4</a>, the excellent <a href="https://deskthority.net/wiki/KUL_ES-87">KUL ES-87</a>, and eventually landing on what I considered my dream keyboard: the <a href="https://deskthority.net/wiki/Leopold_FC660C">Leopold FC660C</a>.</p><p><img src="/assets/img/leopold-fc660c.jpg" alt="Leopold FC660C" /> <em>The Leopold FC660C – my daily driver for almost a decade.</em></p><p>The Leopold was a revelation. It’s where I discovered <a href="https://deskthority.net/wiki/Topre">Topre</a> switches – that glorious electrostatic capacitive feel that’s somewhere between membrane and mechanical, yet somehow better than both. After years of clacking away on Cherry MX Blues (much to the dismay of my wife and everyone within a 10-meter radius), the smooth, thocky Topre experience felt like coming home. The compact 65% layout was the cherry on top – small enough to save desk space, but with dedicated arrow keys and a few essential extras. I used the Leopold daily for almost a decade, from 2016 all the way to early 2025. That’s quite a run.</p><h2 id="the-upgrade">The Upgrade</h2><p>So why change something that was working so well? Two words: <strong>wireless Topre</strong>. I wanted to cut the cord, and if you want a wireless keyboard with Topre switches your options are… well, pretty much just the <a href="https://happyhackingkb.com/products/hybrid-types/">HHKB (Happy Hacking Keyboard) Hybrid Type S</a>.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p><p>There was another reason, too. I have two desktop computers – a <a href="/articles/2025/03/14/updating-my-toolbox-ghostty-and-fish/">Mac Mini M4</a> and a <a href="/articles/2022/10/30/why-bother-with-a-custom-desktop-pc-in-2022/">custom-built desktop PC</a> – and I wanted to use one keyboard seamlessly with both of them via Bluetooth. In practice, though, I liked the Mac Mini so much that I haven’t turned on the other desktop a single time since I got it. So I never actually got to test how practical it is to switch the HHKB between multiple computers.</p><p><img src="/assets/img/hhkb-hybrid-type-s.jpg" alt="HHKB Hybrid Type S" /> <em>The HHKB Hybrid Type S – the object of today’s review.</em></p><p>Not to mention I’d been exposed to the HHKB hype for as long as I can remember. The keyboard has an almost cult-like following among programmers, especially in the Unix and Lisp communities. I’m honestly not sure why I went for the Leopold instead of the HHKB back in 2016 – the HHKB was definitely on my radar even then – but in hindsight the Leopold served me incredibly well. When I finally pulled the trigger on the HHKB Hybrid Type S in early 2025, I had sky-high expectations.</p><p>I got it in early 2025, so now I’ve had it for over a year. I deliberately avoided writing about it earlier – I think it’s important to live with a piece of hardware for a good while before passing judgment, especially when there’s an adjustment period involved. So let’s dig in.</p><h2 id="what-i-like">What I Like</h2><p><strong>Looks.</strong> The HHKB is a handsome keyboard. The minimalist design, the clean lines, the elegant keycap legends – it’s a looker. I’d say it edges out the Leopold slightly in the aesthetics department, though the battery housing bump on the back is a bit of an eyesore. A minor quibble, though.</p><p><strong>Weight.</strong> It’s impressively light and portable. Some people complain this makes it feel “cheap” since the body is essentially all plastic, but I appreciate being able to toss it in a bag without thinking twice.<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p><p><strong>Keycaps and switches.</strong> The Topre experience is excellent, as expected. The keycaps are high quality PBT and the switches feel more or less identical to what I had on the Leopold. If you already know you love Topre, you’ll love the HHKB’s typing feel. The keys on the HHKB Type S are a bit quieter and lighter to press than those of the Leopold, but the difference is not big.</p><p><strong>Control key placement.</strong> This is probably the one aspect of the HHKB’s unconventional layout that I actually love. Control sits right where Caps Lock is on a standard keyboard – exactly where it belongs. On every other keyboard I’ve ever owned, the first thing I’d do is remap Caps Lock to Control anyway, so it’s nice to have a keyboard that gets this right out of the box.</p><p><strong>Multi-OS support.</strong> The HHKB natively supports Windows/Linux and macOS via DIP switches, so you can flip between operating systems without any software remapping. A small but welcome touch.</p><p><strong>Wireless.</strong> Being able to pair with multiple devices via Bluetooth and switch between them is genuinely nice. No more cable clutter on the desk. That said, the wireless implementation comes with some significant caveats – more on that below.</p><h2 id="what-i-dont-like">What I Don’t Like</h2><p><strong>The layout.</strong> For a keyboard that markets itself as a “programmer’s keyboard,” some of the layout decisions are baffling. The tilde/backtick key is in a terrible position (top right corner, miles away from where your fingers expect it). For someone who lives in the terminal, that’s a real problem. I remapped it to the Escape key position almost immediately, since I don’t particularly care where Escape lives – I use a dual-mapping on Caps Lock (Control when held, Escape when tapped via <a href="https://karabiner-elements.pqrs.org/">Karabiner Elements</a>).</p><p>The backslash placement is also awkward, and the Alt/Option keys are unnecessarily tiny even though there’s plenty of space to make them bigger. There’s no right Control key despite ample room for one (I compensate with a similar hold/tap mapping on the Return key). And the lack of dedicated arrow keys – while manageable when programming – is genuinely annoying in applications that make heavy use of them (browsers, document editors, Slack, etc.). I’ve mostly gotten used to using Fn+key combos for arrows, but I still miss the Leopold’s dedicated arrow keys on a regular basis.</p><p><strong>The firmware.</strong> For such an expensive and supposedly premium product, the firmware feels primitive. You get basic key remapping and a few DIP switches, but it’s nothing compared to the power and flexibility of <a href="https://qmk.fm/">QMK</a> or <a href="https://www.caniusevia.com/">VIA</a> that you’ll find on many keyboards at half the price. HHKB recently released firmware 2.0 with some interesting updates, but I haven’t had a chance to try it yet. In the meantime, Karabiner Elements does the heavy lifting for me – but I shouldn’t <em>have</em> to rely on third-party software to make a $300+ keyboard work the way I want.</p><p><strong>Battery life.</strong> It’s mediocre at best, and the HHKB uses disposable AA batteries rather than a built-in rechargeable battery. In 2025. For a premium wireless keyboard. I’ll let that sink in.</p><p><strong>The USB-C behavior.</strong> You’d think that plugging in a USB-C cable would automatically switch the keyboard to wired mode. Nope – you have to explicitly select the USB interface with Fn + Control + 0. It’s a minor annoyance, but it feels like an obvious UX miss. Oh, and for the price they’re charging, you’d expect them to include a USB-C cable in the box. They don’t.</p><p><strong>The sleep/wake behavior.</strong> This is my single biggest complaint and the thing that still drives me up the wall a year later. To save battery, the keyboard goes to sleep after 30 minutes of inactivity – that’s perfectly reasonable. What’s <em>not</em> reasonable is that pressing a key doesn’t wake it up. You have to press the power button to bring it back to life. Every. Single. Time. I still don’t understand why it can’t auto-wake like virtually every other wireless keyboard on the market. You come back from a coffee break, start typing, and… nothing. Then you remember, reach for the power button, wait a second for it to reconnect, and <em>then</em> you can start typing. It’s a small thing, but it’s also extremely annoying.</p><p><strong>Bluetooth reliability.</strong> A couple of times the HHKB simply stopped connecting to my computer and I had to re-pair it from scratch. In those moments wireless didn’t feel like a feature at all – you really can’t beat the simplicity and reliability of a wired connection.</p><h2 id="hhkb-vs-leopold-fc660c">HHKB vs. Leopold FC660C</h2><p>Since I spent nearly a decade with the Leopold before switching, a direct comparison seems only fair.</p><p>The <strong>Leopold wins</strong> on layout, sturdiness, and typing feel. The 65% layout with dedicated arrow keys is simply more practical than the HHKB’s 60% layout for everyday use. The build quality feels more solid – you can tell the Leopold is heavier and more rigid. The typing feel is slightly better too – the keys have a bit more weight and a more satisfying thock. The Leopold also has a handy DIP switch that lets you make Escape output tilde/backtick by default (and you can always toggle between the two with Fn). On the HHKB, the secondary function of Escape is the rarely used Power key – a baffling choice.</p><p>The <strong>HHKB wins</strong> on overall aesthetics (nicer legends, cleaner color scheme), connectivity (USB-C and Bluetooth vs. the Leopold’s micro-USB and wired-only), noise level (the Type S is noticeably quieter), and the handy multimedia keys accessible via Fn. The HHKB also has a massive online community, which means plenty of custom keycaps, accessories, and fellow enthusiasts to geek out with.</p><p>I guess there are no wrong choices here – both are excellent Topre keyboards. But if Leopold ever releases an FC660C with USB-C and wireless, I’ll strongly consider getting one again.</p><h2 id="the-verdict">The Verdict</h2><p><img src="/assets/img/das-keyboard-4.jpg" alt="Das Keyboard 4" /> <em>The Das Keyboard 4 – where it all started for me (well, almost). Big, loud, and proud.</em></p><p>So, is the HHKB the real deal, or is it mostly hype?</p><p>After a year of daily use, I’d say it’s… a bit of both. It’s a good keyboard – the typing feel is fantastic (because Topre), it looks great on a desk, and the wireless capability is genuinely useful despite its rough edges. But I wouldn’t say it offers much over other Topre keyboards. The layout quirks, the primitive firmware, the battery situation, and that maddening sleep/wake behavior all hold it back from being the definitive keyboard it’s often made out to be.</p><p>The fundamental problem is that there are so few Topre keyboards on the market that our options are extremely limited. For me it came down to either the HHKB or the <a href="https://www.realforce.co.jp/en/products/R3HH21/">Realforce R3 TKL</a>. The Realforce has a better, more conventional layout for sure, but I didn’t love the aesthetics – it felt too big for what it offered, and visually it didn’t do much for me.<sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p><p>Despite its shortcomings, the HHKB has grown on me. I don’t think my typing experience has actually improved compared to the Leopold, but my desk certainly looks a bit nicer and I always smile when I look down at it. Sometimes that’s enough. I hope this won’t be the last Topre keyboard from HHKB, and that down the road they’ll release a version that addresses some of my frustrations. But I won’t be upset if I end up typing on my current HHKB for a very long time.</p><p>If you have an HHKB or any other Topre keyboard, I’d love to hear about your experience in the comments. What do you love? What drives you crazy? Have you found clever workarounds for the layout quirks? And if you’re still on Cherry MX Blues… well, your co-workers would like a word with you.</p><p>That’s all I have for you today. Keep typing!</p><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1"><p>Realforce does have wireless models, but even their tenkeyless keyboards are much bigger than the Leopold FC660C and the HHKB – I wanted something compact. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p><li id="fn:2"><p>Not that I carry it around much. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p><li id="fn:3"><p>If they tweak it a bit in the future I’ll definitely get one, though. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p></ol></div>]]></content> <author> <name>bbatsov</name> </author> <summary>The keyboard is the most important tool for a programmer. Choose wisely. I’m a keyboard nerd. I’ve owned several great keyboards over the years, starting with the legendary Das Keyboard 3 Ultimate (blank keys and Cherry MX Blue switches – my co-workers loved me), then moving through the Das Keyboard 4, the excellent KUL ES-87, and eventually landing on what I considered my dream keyboard: the...</summary> </entry> <entry><title>Essential Claude Code Skills and Commands</title><link href="https://batsov.com/articles/2026/03/11/essential-claude-code-skills-and-commands/" rel="alternate" type="text/html" title="Essential Claude Code Skills and Commands" /><published>2026-03-11T10:30:00+02:00</published> <updated>2026-03-11T10:30:00+02:00</updated> <id>https://batsov.com/articles/2026/03/11/essential-claude-code-skills-and-commands/</id> <content type="html"><![CDATA[<p>I’ll admit it: when I first started using <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a>, I mostly ignored the built-in skills. Everyone online was saying “go make your own skills,” so that’s what I did. I wrote custom skills for all sorts of things and I got plenty of things done with them.</p><p>It wasn’t until I stumbled into <code class="language-plaintext highlighter-rouge">/review</code> and <code class="language-plaintext highlighter-rouge">/simplify</code> that I realized I’d been overlooking some genuinely useful built-in functionality. And recently, with additions like <code class="language-plaintext highlighter-rouge">/loop</code> and <code class="language-plaintext highlighter-rouge">/batch</code>, the built-in skill set has gotten even more interesting.</p><p>This post covers what’s available out of the box, how to use it effectively, and the difference between skills and slash commands (which confused me initially too).</p><h2 id="skills-vs-slash-commands-whats-the-difference">Skills vs. Slash Commands: What’s the Difference?</h2><p>This distinction tripped me up early on, and I don’t think I’m alone. Here’s the deal:</p><p><strong>Slash commands</strong> are built-in fixed-logic operations. Things like <code class="language-plaintext highlighter-rouge">/clear</code>, <code class="language-plaintext highlighter-rouge">/compact</code>, <code class="language-plaintext highlighter-rouge">/help</code>, <code class="language-plaintext highlighter-rouge">/model</code>, <code class="language-plaintext highlighter-rouge">/cost</code> – these are hardcoded into the Claude Code CLI. They do one specific thing, they don’t involve AI reasoning, and you can’t customize them. Think of them as CLI commands that happen to start with <code class="language-plaintext highlighter-rouge">/</code>.</p><p><strong>Skills</strong> are prompt-based capabilities. When you invoke a skill, it loads a set of instructions (a markdown file) into Claude’s context, and Claude executes them. Skills can spawn subagents, accept arguments, use specific tools, and generally orchestrate complex multi-step workflows. The built-in skills (<code class="language-plaintext highlighter-rouge">/simplify</code>, <code class="language-plaintext highlighter-rouge">/review</code>, <code class="language-plaintext highlighter-rouge">/batch</code>, <code class="language-plaintext highlighter-rouge">/loop</code>, <code class="language-plaintext highlighter-rouge">/debug</code>, <code class="language-plaintext highlighter-rouge">/claude-api</code>) are shipped with Claude Code, but the system is designed for you to create your own as well.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p><p>The confusion comes from the fact that both are invoked with <code class="language-plaintext highlighter-rouge">/</code>, and historically they were separate systems. Claude Code used to have “commands” (<code class="language-plaintext highlighter-rouge">.claude/commands/*.md</code>) and “skills” (<code class="language-plaintext highlighter-rouge">.claude/skills/*/SKILL.md</code>) as distinct concepts. These have since been <strong>merged</strong> – files in either location create the same <code class="language-plaintext highlighter-rouge">/slash-command</code> interface. The skills system is the recommended approach going forward because it supports features that plain commands don’t:</p><ul><li>Supporting files (templates, examples, scripts alongside the skill)<li>Frontmatter control (<code class="language-plaintext highlighter-rouge">disable-model-invocation</code>, <code class="language-plaintext highlighter-rouge">user-invocable</code>, <code class="language-plaintext highlighter-rouge">allowed-tools</code>, <code class="language-plaintext highlighter-rouge">context</code>, <code class="language-plaintext highlighter-rouge">agent</code>)<li>Dynamic context injection via shell command output<li>Subagent execution with <code class="language-plaintext highlighter-rouge">context: fork</code></ul><p>Your existing <code class="language-plaintext highlighter-rouge">.claude/commands/</code> files still work, but new ones should go in <code class="language-plaintext highlighter-rouge">.claude/skills/</code>.</p><blockquote class="prompt-tip"><p>You can list all available skills (both built-in and custom) with <code class="language-plaintext highlighter-rouge">/skills</code>.</p></blockquote><h2 id="the-built-in-skills">The Built-in Skills</h2><h3 id="simplify--code-quality-review">/simplify – Code Quality Review</h3><p>This is the skill I use most often. After making changes to a codebase, <code class="language-plaintext highlighter-rouge">/simplify</code> reviews your recently changed files for code reuse opportunities, quality issues, and efficiency improvements – and then fixes them automatically.</p><p>What makes it effective is that it spawns <strong>three parallel review agents</strong>, each looking at the changes from a different angle. You get a mini code review without leaving your terminal.</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre><td class="rouge-code"><pre>/simplify
</pre></div></div><p>You can optionally pass a focus area:</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre><td class="rouge-code"><pre>/simplify memory efficiency
/simplify error handling
/simplify reduce duplication
</pre></div></div><p>This narrows the review to a specific concern, which is useful when you know where the rough edges are but want AI help with the details.</p><p>I find <code class="language-plaintext highlighter-rouge">/simplify</code> most valuable right after a larger refactoring or after accepting a batch of AI-generated code. It catches things like unused imports, redundant variables, opportunities to extract shared logic, and overly complex conditionals. Think of it as a second pass that keeps your code from accumulating cruft.</p><h3 id="review--code-review">/review – Code Review</h3><p><code class="language-plaintext highlighter-rouge">/review</code> is <code class="language-plaintext highlighter-rouge">/simplify</code>’s sibling, but with a different focus. While <code class="language-plaintext highlighter-rouge">/simplify</code> optimizes code you’ve already written, <code class="language-plaintext highlighter-rouge">/review</code> gives you a proper code review of your changes – the kind of feedback you’d expect from a thorough pull request review.</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre><td class="rouge-code"><pre>/review
/review 123
/review https://github.com/org/repo/pull/123
</pre></div></div><p>Without arguments, it reviews your recent local changes. Pass a PR number or URL and it reviews that pull request instead. It examines the changes for bugs, logic errors, edge cases, style issues, and potential problems that <code class="language-plaintext highlighter-rouge">/simplify</code> wouldn’t catch (because <code class="language-plaintext highlighter-rouge">/simplify</code> is about making code <em>simpler</em>, not about finding <em>bugs</em>).</p><p>My typical workflow is: make changes, run <code class="language-plaintext highlighter-rouge">/review</code> to catch issues, fix anything it flags, then run <code class="language-plaintext highlighter-rouge">/simplify</code> to clean things up. The two skills complement each other nicely – <code class="language-plaintext highlighter-rouge">/review</code> for correctness, <code class="language-plaintext highlighter-rouge">/simplify</code> for cleanliness.</p><h3 id="batch--large-scale-parallel-changes">/batch – Large-Scale Parallel Changes</h3><p><code class="language-plaintext highlighter-rouge">/batch</code> is the heavy hitter. It orchestrates large-scale changes across a codebase by decomposing the work into 5-30 independent units and spawning isolated worktree agents to handle them in parallel.</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre><td class="rouge-code"><pre>/batch migrate all test files from Jest to Vitest
/batch add TypeScript types to all files in src/utils/
/batch update all API endpoints to use the new auth middleware
</pre></div></div><p>Each agent works in its own git worktree, implements its unit of work, runs tests, and can even open a pull request. This means you can kick off a migration and review the results incrementally rather than waiting for one monolithic change.</p><p>The key insight with <code class="language-plaintext highlighter-rouge">/batch</code> is that the instruction needs to describe a <strong>pattern</strong> – something that applies uniformly across multiple files or components. It’s not for “rewrite the authentication system” (that’s a single complex task), it’s for “apply this specific change to every place that matches this pattern.”</p><h3 id="loop--scheduled-recurring-prompts">/loop – Scheduled Recurring Prompts</h3><p><code class="language-plaintext highlighter-rouge">/loop</code> runs a prompt repeatedly on a schedule while your session stays open. It parses a time interval and sets up a recurring task.</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre><td class="rouge-code"><pre>/loop 5m check if the dev server has any new errors in the log
/loop 10m run the test suite and report any failures
/loop 1h check deployment status and summarize metrics
</pre></div></div><p>The interval is optional and defaults to a reasonable period. This is useful for monitoring tasks – watching a long-running build, keeping an eye on a staging deployment, or periodically checking for new issues in a log file.</p><p>I haven’t used <code class="language-plaintext highlighter-rouge">/loop</code> as extensively as the other skills, but I can see it being handy for longer coding sessions where you want background monitoring without switching to another terminal.</p><h3 id="debug--session-troubleshooting">/debug – Session Troubleshooting</h3><p>When something goes wrong in your Claude Code session – a tool call fails silently, Claude seems confused about context, or behavior is just inexplicably odd – <code class="language-plaintext highlighter-rouge">/debug</code> reads the session debug log and helps you figure out what happened.</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre><td class="rouge-code"><pre>/debug
/debug why did the last edit fail
/debug tool calls are timing out
</pre></div></div><p>This is a niche skill, but invaluable when you need it. It’s essentially “Claude, diagnose yourself.”</p><h3 id="claude-api--api-reference-loader">/claude-api – API Reference Loader</h3><p>If you’re building applications with the Claude API or Anthropic SDK, <code class="language-plaintext highlighter-rouge">/claude-api</code> loads the relevant API reference material for your project’s language (Python, TypeScript, Java, Go, Ruby, C#, PHP, or cURL) plus the Agent SDK reference.</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre><td class="rouge-code"><pre>/claude-api
</pre></div></div><p>This skill also activates automatically when Claude detects code that imports <code class="language-plaintext highlighter-rouge">anthropic</code>, <code class="language-plaintext highlighter-rouge">@anthropic-ai/sdk</code>, or <code class="language-plaintext highlighter-rouge">claude_agent_sdk</code>, so you may never need to invoke it manually.</p><h2 id="useful-slash-commands">Useful Slash Commands</h2><p>While not “skills” in the technical sense, several built-in slash commands are worth knowing about:</p><h3 id="compact--context-management">/compact – Context Management</h3><p>When your conversation gets long and Claude starts losing track of earlier context, <code class="language-plaintext highlighter-rouge">/compact</code> compresses the conversation history. You can optionally give it focus instructions:</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre><td class="rouge-code"><pre>/compact focus on the database migration work
</pre></div></div><p>This is essential for long sessions. I use it proactively before starting a new phase of work within the same session.<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p><h3 id="diff--review-changes">/diff – Review Changes</h3><p><code class="language-plaintext highlighter-rouge">/diff</code> opens an interactive diff viewer showing all changes Claude has made. This is much better than mentally tracking what changed across multiple files.</p><p>A few practical tips:</p><ul><li><strong>Use it as a checkpoint.</strong> After Claude makes a series of edits, run <code class="language-plaintext highlighter-rouge">/diff</code> before moving on. It’s your chance to catch mistakes before they compound. Much easier to ask Claude to fix something now than three steps later.<li><strong>Use it before committing.</strong> I always run <code class="language-plaintext highlighter-rouge">/diff</code> right before asking Claude to commit. It’s the equivalent of <code class="language-plaintext highlighter-rouge">git diff --staged</code> but more convenient – you see exactly what Claude changed, not what you manually staged.<li><strong>Combine with /rewind.</strong> If <code class="language-plaintext highlighter-rouge">/diff</code> reveals something you don’t like, you can <code class="language-plaintext highlighter-rouge">/rewind</code> to undo the changes and try a different approach. The two commands pair naturally: review, then decide whether to keep or discard.</ul><h3 id="btw--side-questions">/btw – Side Questions</h3><p><code class="language-plaintext highlighter-rouge">/btw</code> lets you ask a side question without affecting the main conversation context:</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre><td class="rouge-code"><pre>/btw what's the syntax for a Rust match guard again?
</pre></div></div><p>The answer comes back without polluting your working context – handy when you need a quick lookup mid-task.</p><h3 id="copy--copy-response-content">/copy – Copy Response Content</h3><p><code class="language-plaintext highlighter-rouge">/copy</code> copies Claude’s last response to your clipboard. If the response contains multiple code blocks, it opens an interactive picker so you can choose which block to copy. Much faster than manually selecting text in the terminal.</p><h3 id="rewind--undo-changes">/rewind – Undo Changes</h3><p><code class="language-plaintext highlighter-rouge">/rewind</code> is your safety net. It reverts both the conversation and any file changes back to a previous point, effectively letting you say “let’s pretend that never happened.” Claude creates implicit checkpoints as you work, so you can step back through them.</p><p>This is especially useful when Claude goes down the wrong path – maybe it misunderstood your intent, or the approach it chose isn’t working out. Instead of manually reverting files and trying to explain what went wrong, just <code class="language-plaintext highlighter-rouge">/rewind</code> and start that part of the conversation fresh with a clearer prompt.</p><h3 id="usage-cost-and-stats--keeping-track-of-consumption">/usage, /cost, and /stats – Keeping Track of Consumption</h3><p>There are three commands for tracking usage, and which ones you’ll see depends on how you access Claude Code:</p><ul><li><strong><code class="language-plaintext highlighter-rouge">/usage</code></strong> shows your <strong>plan-level</strong> limits and current rate limit status. Use it to check how much of your daily/monthly quota you’ve consumed and whether you’re approaching a rate limit. Available to everyone.<li><strong><code class="language-plaintext highlighter-rouge">/cost</code></strong> shows token usage and estimated dollar cost for the <strong>current session</strong>. This is only relevant (and visible) if you’re using Claude Code via the <strong>API</strong> – subscription users (Pro/Max) won’t see it since tokens are included in the plan.<li><strong><code class="language-plaintext highlighter-rouge">/stats</code></strong> visualizes daily usage patterns, session history, and model preferences. This is the subscription-friendly alternative to <code class="language-plaintext highlighter-rouge">/cost</code> – it shows you <em>how</em> you’re using Claude Code over time rather than what it’s costing per session.</ul><p>In short: <code class="language-plaintext highlighter-rouge">/usage</code> is “how much runway do I have left?”, <code class="language-plaintext highlighter-rouge">/cost</code> is “how much did this session cost in dollars?” (API only), and <code class="language-plaintext highlighter-rouge">/stats</code> is “what do my usage patterns look like?”</p><h3 id="model-and-fast--switching-models">/model and /fast – Switching Models</h3><p><code class="language-plaintext highlighter-rouge">/model</code> lets you switch the AI model mid-session. This is handy when you want to use a cheaper/faster model for routine tasks and switch to a more capable one for complex reasoning:</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre><td class="rouge-code"><pre>/model sonnet
/model opus
</pre></div></div><p><code class="language-plaintext highlighter-rouge">/fast</code> toggles fast mode, which uses the same model but optimizes for faster output. It’s a quick way to speed things up when you don’t need maximum quality:</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre><td class="rouge-code"><pre>/fast on
/fast off
</pre></div></div><h3 id="memory--auto-memory-management">/memory – Auto-Memory Management</h3><p><code class="language-plaintext highlighter-rouge">/memory</code> lets you view and edit Claude’s persistent memory for the current project. Claude automatically saves useful context (project conventions, architectural decisions, your preferences) to a <code class="language-plaintext highlighter-rouge">MEMORY.md</code> file that persists across sessions.</p><p>Use <code class="language-plaintext highlighter-rouge">/memory</code> to review what Claude has remembered, correct anything wrong, or manually add things you want it to always know about your project. You can also explicitly ask Claude to remember something during a conversation, and it’ll write it to the memory file.</p><h3 id="context--context-visualization">/context – Context Visualization</h3><p><code class="language-plaintext highlighter-rouge">/context</code> displays a colored grid showing how your context window is being used. It helps you understand when you’re approaching limits and what’s taking up space.</p><h3 id="plan--plan-mode">/plan – Plan Mode</h3><p><code class="language-plaintext highlighter-rouge">/plan</code> enters plan mode, where Claude designs an implementation strategy without making any changes. This is useful when you want to think through an approach before committing to it. You can also toggle plan mode with <code class="language-plaintext highlighter-rouge">Shift+Tab</code> at any point during your conversation – no need to type the command.</p><p>Here’s an example of how I’d use it:</p><div class="language-plaintext highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre><td class="rouge-code"><pre>/plan refactor the authentication module to support OAuth2 in addition to
the existing API key auth
</pre></div></div><p>Claude will analyze the codebase, identify the affected files, and propose a step-by-step implementation plan – without touching any code. You can discuss the plan, ask questions, suggest alternatives, and iterate until you’re happy. Then exit plan mode (with <code class="language-plaintext highlighter-rouge">Shift+Tab</code> or <code class="language-plaintext highlighter-rouge">/plan</code> again) and tell Claude to execute.</p><p>It’s worth noting that Claude will sometimes enter plan mode on its own when you give it a sufficiently complex task. If it determines that planning is warranted before diving in, it’ll switch to plan mode, present its approach, and wait for your approval. This is generally a good thing – it means Claude is thinking before acting rather than charging ahead with a potentially wrong approach.</p><h2 id="skill-arguments-and-parameters">Skill Arguments and Parameters</h2><p>All skills support arguments via the <code class="language-plaintext highlighter-rouge">$ARGUMENTS</code> placeholder (and indexed variants like <code class="language-plaintext highlighter-rouge">$0</code>, <code class="language-plaintext highlighter-rouge">$1</code> for specific arguments). For the built-in skills:</p><table><thead><tr><th>Skill<th>Arguments<th>Example<tbody><tr><td><code class="language-plaintext highlighter-rouge">/simplify</code><td>Optional focus area<td><code class="language-plaintext highlighter-rouge">/simplify error handling</code><tr><td><code class="language-plaintext highlighter-rouge">/review</code><td>Optional: PR number or URL<td><code class="language-plaintext highlighter-rouge">/review 123</code><tr><td><code class="language-plaintext highlighter-rouge">/batch</code><td>Required: change description<td><code class="language-plaintext highlighter-rouge">/batch add logging to all API handlers</code><tr><td><code class="language-plaintext highlighter-rouge">/loop</code><td>Optional interval + required prompt<td><code class="language-plaintext highlighter-rouge">/loop 5m check build status</code><tr><td><code class="language-plaintext highlighter-rouge">/debug</code><td>Optional issue description<td><code class="language-plaintext highlighter-rouge">/debug why did the edit fail</code><tr><td><code class="language-plaintext highlighter-rouge">/claude-api</code><td>None<td><code class="language-plaintext highlighter-rouge">/claude-api</code></table><p>When creating your own skills, the argument system is quite flexible. In your <code class="language-plaintext highlighter-rouge">SKILL.md</code>:</p><div class="language-markdown highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre><td class="rouge-code"><pre><span class="nn">---</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">fix-issue</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">Fix a GitHub issue</span>
<span class="na">argument-hint</span><span class="pi">:</span> <span class="s">&amp;lt;issue-number&amp;gt;</span>
<span class="nn">---</span>

Fix GitHub issue $ARGUMENTS following our project's standards.
</pre></div></div><p>Invoked as <code class="language-plaintext highlighter-rouge">/fix-issue 123</code>, the <code class="language-plaintext highlighter-rouge">$ARGUMENTS</code> placeholder becomes <code class="language-plaintext highlighter-rouge">123</code>.</p><h2 id="creating-your-own-skills">Creating Your Own Skills</h2><p>The built-in skills are great, but the real power is in creating your own. Skills live in one of three locations:</p><table><thead><tr><th>Scope<th>Path<th>Use case<tbody><tr><td>Personal<td><code class="language-plaintext highlighter-rouge">~/.claude/skills/&amp;lt;name&amp;gt;/SKILL.md</code><td>Your workflows, all projects<tr><td>Project<td><code class="language-plaintext highlighter-rouge">.claude/skills/&amp;lt;name&amp;gt;/SKILL.md</code><td>Team conventions, commit to git<tr><td>Plugin<td><code class="language-plaintext highlighter-rouge">&amp;lt;plugin&amp;gt;/skills/&amp;lt;name&amp;gt;/SKILL.md</code><td>Shared via plugin system</table><p>A skill is just a <code class="language-plaintext highlighter-rouge">SKILL.md</code> file with optional frontmatter:</p><div class="language-markdown highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
13
</pre><td class="rouge-code"><pre><span class="nn">---</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">deploy</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">Deploy the current branch to staging</span>
<span class="na">disable-model-invocation</span><span class="pi">:</span> <span class="kc">true</span>
<span class="na">allowed-tools</span><span class="pi">:</span> <span class="s">Bash</span>
<span class="nn">---</span>

Deploy the current branch to staging:
<span class="p">
1.</span> Run the test suite first: <span class="sb">`npm test`</span>
<span class="p">2.</span> Build the production bundle: <span class="sb">`npm run build`</span>
<span class="p">3.</span> Deploy using: <span class="sb">`./scripts/deploy.sh staging`</span>
<span class="p">4.</span> Verify the deployment by checking the health endpoint
</pre></div></div><p>Key frontmatter options:</p><ul><li><strong><code class="language-plaintext highlighter-rouge">disable-model-invocation: true</code></strong> – Only you can invoke this skill (Claude won’t trigger it automatically). Use this for anything with side effects.<li><strong><code class="language-plaintext highlighter-rouge">user-invocable: false</code></strong> – Only Claude can invoke it (background knowledge). Use this for conventions and guidelines you want Claude to follow automatically.<li><strong><code class="language-plaintext highlighter-rouge">allowed-tools</code></strong> – Restrict which tools Claude can use. Useful for read-only research skills.<li><strong><code class="language-plaintext highlighter-rouge">context: fork</code></strong> – Run in an isolated subagent. Good for research tasks that shouldn’t pollute your main conversation.<li><strong><code class="language-plaintext highlighter-rouge">agent</code></strong> – Which subagent type to use (<code class="language-plaintext highlighter-rouge">Explore</code>, <code class="language-plaintext highlighter-rouge">Plan</code>, <code class="language-plaintext highlighter-rouge">general-purpose</code>).</ul><p>Skills can also include supporting files alongside <code class="language-plaintext highlighter-rouge">SKILL.md</code> – templates, reference docs, example code – and inject dynamic context via shell commands using the <code class="language-plaintext highlighter-rouge">!`command`</code> syntax:</p><div class="language-markdown highlighter-rouge"><div class="highlight">class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre><td class="rouge-code"><pre><span class="nn">---</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">pr-review</span>
<span class="na">description</span><span class="pi">:</span> <span class="s">Review the current PR</span>
<span class="nn">---</span>

Here's the PR diff:
!<span class="sb">`gh pr diff`</span>

Changed files:
!<span class="sb">`gh pr diff --name-only`</span>

Review these changes for correctness, style, and potential issues.
</pre></div></div><h2 id="workflow-tips">Workflow Tips</h2><p><strong>Chain /simplify after AI-generated changes.</strong> Whenever Claude writes a significant chunk of code, run <code class="language-plaintext highlighter-rouge">/simplify</code> as a follow-up. AI-generated code often has subtle redundancies or missed optimization opportunities that a second pass catches.</p><p><strong>Use /batch for migrations, not redesigns.</strong> <code class="language-plaintext highlighter-rouge">/batch</code> shines when the change is repetitive and parallelizable. “Add error handling to all 47 API endpoints” is perfect. “Redesign the API layer” is not – that needs a coherent plan, not parallel execution.</p><p><strong>Use /compact proactively.</strong> Don’t wait until Claude starts losing context. When you finish one logical chunk of work and are about to start another, compact first. Your future self will thank you.</p><p><strong>Start with /plan for complex tasks.</strong> Before diving into implementation, use <code class="language-plaintext highlighter-rouge">/plan</code> to get Claude to think through the approach. You can discuss and refine the plan before a single line of code is written.</p><p><strong>Use /btw liberally.</strong> Side questions are free (context-wise). Don’t pollute your main working context with “wait, how does X work again?” tangents.</p><h2 id="epilogue">Epilogue</h2><p>Claude Code is a fast-moving target these days – new built-in skills and commands get added regularly, and the skill system itself keeps evolving. I’ll make an effort to update this article when something cool catches my eye, so check back from time to time.</p><p>That’s all I have for you today. Keep hacking!</p><div class="footnotes" role="doc-endnotes"><ol><li id="fn:1"><p>You can also install third-party skills via the plugin system. Run <code class="language-plaintext highlighter-rouge">/plugin</code> to manage plugins. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p><li id="fn:2"><p>If you don’t use <code class="language-plaintext highlighter-rouge">/compact</code> yourself eventually Claude will do auto-compaction anyways. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&amp;#8617;&amp;#xfe0e;</a></p></ol></div>]]></content> <author> <name>bbatsov</name> </author> <summary>I’ll admit it: when I first started using Claude Code, I mostly ignored the built-in skills. Everyone online was saying “go make your own skills,” so that’s what I did. I wrote custom skills for all sorts of things and I got plenty of things done with them. It wasn’t until I stumbled into /review and /simplify that I realized I’d been overlooking some genuinely useful built-in functionality. A...</summary> </entry> </feed>
