<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>K@tooling on Martin Sukany</title><link>https://sukany.cz/tags/k@tooling/</link><description>Recent content in K@tooling on Martin Sukany</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 23 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://sukany.cz/tags/k@tooling/index.xml" rel="self" type="application/rss+xml"/><item><title>LLMs in Emacs: My Actual gptel Setup</title><link>https://sukany.cz/blog/2026-03-23-emacs-gptel-setup/</link><pubDate>Mon, 23 Mar 2026 00:00:00 +0000</pubDate><guid>https://sukany.cz/blog/2026-03-23-emacs-gptel-setup/</guid><description>&lt;p&gt;I&amp;rsquo;ve been using gptel daily for three months now. This isn&amp;rsquo;t a review — it&amp;rsquo;s a field report from someone running LLMs inside Emacs on a corporate macOS machine with a MITM proxy, compliance requirements, and zero patience for black-box tooling.&lt;/p&gt;
&lt;h2 id="why-emacs-for-llm-work"&gt;Why Emacs for LLM Work&lt;/h2&gt;
&lt;p&gt;gptel is a thin client. It sends text to an API, gets text back. That&amp;rsquo;s it. No hidden prompt injection, no telemetry you can&amp;rsquo;t inspect, no magic. You see exactly what goes over the wire.&lt;/p&gt;
&lt;p&gt;I came from VS Code&amp;rsquo;s Copilot Chat. It works fine until you need to understand what it&amp;rsquo;s actually doing. Which model is it using right now? What&amp;rsquo;s in the system prompt? Can I route this through a different backend? The answer is always: you can&amp;rsquo;t, or you need an extension that half-works.&lt;/p&gt;
&lt;p&gt;gptel gives you full control because there&amp;rsquo;s nothing to control. It&amp;rsquo;s Emacs — the config &lt;em&gt;is&lt;/em&gt; the product. Every backend, every model, every parameter is an elisp variable you can inspect and change at runtime.&lt;/p&gt;
&lt;p&gt;The corporate context matters here. I&amp;rsquo;m on a work macOS with a MITM proxy that intercepts TLS. Compliance says data must not be retained by third parties. I need to know exactly where my prompts go. With gptel, I do.&lt;/p&gt;
&lt;p&gt;Three months in, I can say: gptel is not the most polished LLM interface. It is the most transparent one.&lt;/p&gt;
&lt;h2 id="one-config-to-rule-them-all"&gt;One Config to Rule Them All&lt;/h2&gt;
&lt;p&gt;The first thing I did was centralize. One elisp file controls both gptel and aidermacs. One variable switches the default backend:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-emacs-lisp" data-lang="emacs-lisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;;; One line to switch the default for both gptel and aidermacs:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defvar&lt;/span&gt; &lt;span class="nv"&gt;my/llm-default-backend&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;Copilot&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;;; (defvar my/llm-default-backend &amp;#34;Claude-Max&amp;#34;) ; personal machine&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The second piece is a preference list. Backends expose different models — Copilot gives you Claude, GPT-5, Gemini through one API. The preference list picks the best available model automatically:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-emacs-lisp" data-lang="emacs-lisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defvar&lt;/span&gt; &lt;span class="nv"&gt;my/gptel-model-preferences&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;claude-opus-4.6&lt;/span&gt; &lt;span class="nv"&gt;claude-opus-4.5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;claude-sonnet-4.6&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;gpt-5.4&lt;/span&gt; &lt;span class="nv"&gt;gpt-5.2&lt;/span&gt; &lt;span class="nv"&gt;gpt-4o&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;gemini-3.1-pro-preview&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;First match from dynamically fetched models wins.&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When I switch machines or a model disappears from an API, the preference list falls through to the next option. No breakage, no manual editing. This pattern scales to any number of backends — everything downstream (gptel, aidermacs, org-babel helpers) reads from the same source.&lt;/p&gt;
&lt;h2 id="github-copilot-for-business-as-primary-backend"&gt;GitHub Copilot for Business as Primary Backend&lt;/h2&gt;
&lt;p&gt;Why Copilot? Compliance. GitHub Copilot for Business does not retain prompts or completions — that&amp;rsquo;s contractual, not just a policy page. For a corporate environment where data retention matters, this is the deciding factor.&lt;/p&gt;
&lt;p&gt;The bonus is access. One Copilot subscription gives you Claude, GPT-5, Gemini, and others through a single API. No separate billing, no individual API keys. IT signs one contract, I get a model zoo.&lt;/p&gt;
&lt;p&gt;The auth flow uses a two-stage token exchange. You start with an OAuth token stored locally by the GitHub Copilot VS Code extension in &lt;code&gt;~/.config/github-copilot/apps.json&lt;/code&gt;. That token gets exchanged for a short-lived session token via GitHub&amp;rsquo;s API:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-emacs-lisp" data-lang="emacs-lisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;;; OAuth token from ~/.config/github-copilot/apps.json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;;; -&amp;gt; exchanged for short-lived session token (TTL ~30 min)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;;; -&amp;gt; used against api.business.githubcopilot.com&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;my/copilot-get-session-token&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Exchange OAuth token for Copilot session token. Cached for 30 min.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;my/copilot-session-token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;my/copilot-session-expires&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float-time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;my/copilot-session-token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;;; exchange via api.github.com/copilot_internal/v2/token&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;;; ... (see full config in repo)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;my/copilot-do-token-exchange&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The session token expires in roughly 30 minutes. The wrapper caches it and refreshes automatically with a 5-minute buffer. You never think about auth after initial setup.&lt;/p&gt;
&lt;p&gt;One gotcha that cost me an afternoon: model name normalization. Copilot&amp;rsquo;s API returns model names with dots (&lt;code&gt;claude-opus-4.6&lt;/code&gt;), while Anthropic&amp;rsquo;s convention uses dashes (&lt;code&gt;claude-opus-4-6&lt;/code&gt;). The preference list needs to match against both:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-emacs-lisp" data-lang="emacs-lisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;my/model-normalize&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Normalize model NAME: dots-&amp;gt;dashes, strip date suffix.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;let&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;symbolp&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;symbol-name&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;setq&lt;/span&gt; &lt;span class="nv"&gt;s&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;replace-regexp-in-string&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;\\.&amp;#34;&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;-&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;replace-regexp-in-string&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;-[0-9]\\{8\\}$&amp;#34;&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Dots become dashes, trailing date stamps get stripped. Without this, your preference for &lt;code&gt;claude-opus-4.6&lt;/code&gt; silently never matches anything from Copilot.&lt;/p&gt;
&lt;h2 id="multiple-backends-dynamic-discovery"&gt;Multiple Backends, Dynamic Discovery&lt;/h2&gt;
&lt;p&gt;Copilot is the primary, but not the only backend. I have three others:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Claude-Max&lt;/strong&gt; &amp;mdash; a proxy to Anthropic&amp;rsquo;s API running on internal infrastructure, no per-token billing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;OpenWebUI&lt;/strong&gt; &amp;mdash; self-hosted, open models for experimentation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Daneel&lt;/strong&gt; &amp;mdash; a custom agent system with its own API&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each backend fetches its available models from the API at startup and caches the result:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-emacs-lisp" data-lang="emacs-lisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;defun&lt;/span&gt; &lt;span class="nv"&gt;my/setup-gptel-backends&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;Create all gptel backends with dynamically fetched models.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;member&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;Copilot&amp;#34;&lt;/span&gt; &lt;span class="nv"&gt;my/llm-enabled-backends&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="nf"&gt;#&amp;#39;&lt;/span&gt;&lt;span class="nv"&gt;gptel-make-gh-copilot&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;Copilot&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt; &lt;span class="nb"&gt;:host&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;api.business.githubcopilot.com&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;:models&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;my/fetch-copilot-models&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;;; Claude-Max, OpenWebUI, Daneel similarly...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The preference list picks the best model across all backends. If Copilot is down, Claude-Max takes over automatically. &lt;code&gt;SPC o l R&lt;/code&gt; refreshes all backends. A new model appears on Copilot&amp;rsquo;s API, I hit refresh, and if it ranks higher in preferences, it&amp;rsquo;s already the default.&lt;/p&gt;
&lt;h2 id="daily-workflows-rewrite-and-chat"&gt;Daily Workflows: Rewrite and Chat&lt;/h2&gt;
&lt;p&gt;Two workflows cover 90% of my LLM usage: rewrite and chat.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;gptel-rewrite&lt;/strong&gt; is the daily driver. Select a region, type an instruction, and the model rewrites the selection in place. The key addition is dispatch mode &amp;mdash; after a rewrite completes, you get a menu: Accept, Reject, Diff, or Merge:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-emacs-lisp" data-lang="emacs-lisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;;; After rewrite completes: show Accept/Reject/Diff/Merge menu&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;after!&lt;/span&gt; &lt;span class="nv"&gt;gptel-rewrite&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;setq&lt;/span&gt; &lt;span class="nv"&gt;gptel-rewrite-default-action&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;dispatch&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Accept replaces the region. Reject restores the original. Diff opens ediff. Merge lets you pick hunks. This single setting turned gptel-rewrite from &amp;ldquo;interesting&amp;rdquo; to &amp;ldquo;indispensable.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chat buffers&lt;/strong&gt; use org-mode. Every conversation is a structured document I can export, search, refile. For batch work and scripting, a CLI helper wraps gptel for use in org-babel blocks:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-org" data-lang="org"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#+begin_src &lt;/span&gt;&lt;span class="cs"&gt;elisp&lt;/span&gt;&lt;span class="c"&gt; :results raw
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;my/gptel-cli&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;Summarize this error log&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c"&gt;#+end_src&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This makes LLM calls composable with other org-babel languages. Shell block produces output, LLM block processes it, Python block handles the result. Pipelines, not chat.&lt;/p&gt;
&lt;h2 id="tool-use-and-mcp"&gt;Tool Use and MCP&lt;/h2&gt;
&lt;p&gt;gptel supports tool use &amp;mdash; the model can call functions, not just generate text:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-emacs-lisp" data-lang="emacs-lisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;setq&lt;/span&gt; &lt;span class="nv"&gt;gptel-use-tools&lt;/span&gt; &lt;span class="no"&gt;t&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;gptel-confirm-tool-calls&lt;/span&gt; &lt;span class="no"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;; ask before each call&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I keep confirmation on. Letting a model execute arbitrary functions without review defeats the purpose of a transparent setup.&lt;/p&gt;
&lt;p&gt;The tool ecosystem has three layers. &lt;strong&gt;llm-tool-collection&lt;/strong&gt; provides filesystem and shell access &amp;mdash; read files, run commands. &lt;strong&gt;ragmacs&lt;/strong&gt; adds Emacs introspection &amp;mdash; the model can query buffers and read documentation. &lt;strong&gt;gptel-got&lt;/strong&gt; works with org structures.&lt;/p&gt;
&lt;p&gt;Then there&amp;rsquo;s MCP (Model Context Protocol). gptel bridges to MCP servers through mcp-hub:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-emacs-lisp" data-lang="emacs-lisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;setq&lt;/span&gt; &lt;span class="nv"&gt;mcp-hub-servers&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;fetch&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;:command&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;uvx&amp;#34;&lt;/span&gt; &lt;span class="nb"&gt;:args&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;mcp-server-fetch&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;sequential-thinking&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;:command&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;npx&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;:args&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;-y&amp;#34;&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;@modelcontextprotocol/server-sequential-thinking&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)))))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;mcp-server-fetch&lt;/code&gt; lets the model pull web content. &lt;code&gt;sequential-thinking&lt;/code&gt; provides a scratchpad for multi-step reasoning. Agent mode (&lt;code&gt;SPC o l A&lt;/code&gt;) combines tool use with a planning loop. It works for well-scoped tasks; don&amp;rsquo;t expect it to handle more than five or six tool calls reliably yet.&lt;/p&gt;
&lt;h2 id="aidermacs-pair-programming"&gt;Aidermacs: Pair Programming&lt;/h2&gt;
&lt;p&gt;For actual code changes across multiple files, gptel-rewrite isn&amp;rsquo;t enough. Aidermacs brings Aider into Emacs &amp;mdash; architect/editor pair programming where one model designs and another applies changes:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-emacs-lisp" data-lang="emacs-lisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;setq&lt;/span&gt; &lt;span class="nv"&gt;aidermacs-default-model&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;my/aider-architect-model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;aidermacs-default-chat-mode&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;architect&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;aidermacs-extra-args&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;`&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;--editor-model&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;my/aider-editor-model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;--editor-edit-format&amp;#34;&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;diff&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s"&gt;&amp;#34;--no-auto-commits&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The architect model (typically Opus) proposes changes. The editor model (typically Haiku &amp;mdash; fast and cheap) applies them as diffs. This split keeps costs reasonable while maintaining quality for the planning phase.&lt;/p&gt;
&lt;p&gt;Aidermacs shares the Copilot auth flow. The same token exchange function provides credentials &amp;mdash; no separate auth setup. An auto-generated &lt;code&gt;.aider.model.settings.yml&lt;/code&gt; sets the Copilot IDE headers required by the business endpoint.&lt;/p&gt;
&lt;p&gt;The corporate proxy needs extra attention. Aider is a Python tool, and Python&amp;rsquo;s requests library needs its own CA bundle:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;REQUESTS_CA_BUNDLE=/path/to/corporate-ca-bundle.crt
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SSL_CERT_FILE=/path/to/corporate-ca-bundle.crt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These environment variables get set in the aidermacs process environment. Without them, every Aider request fails with a TLS verification error.&lt;/p&gt;
&lt;h2 id="corporate-proxy-the-elephant-in-the-room"&gt;Corporate Proxy: The Elephant in the Room&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re on a corporate network with a MITM proxy, you already know the pain. The proxy terminates TLS, re-signs with its own CA, and every HTTPS tool needs to know about it.&lt;/p&gt;
&lt;p&gt;For Emacs itself:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-emacs-lisp" data-lang="emacs-lisp"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;;; Trust corporate MITM proxy (adds intermediate CA)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;setq&lt;/span&gt; &lt;span class="nv"&gt;gnutls-verify-error&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;tls-checktrust&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;network-security-level&lt;/span&gt; &lt;span class="ss"&gt;&amp;#39;low&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;;; curl handles proxy better than url.el&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;setq&lt;/span&gt; &lt;span class="nv"&gt;gptel-use-curl&lt;/span&gt; &lt;span class="no"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;gptel-use-curl t&lt;/code&gt; matters. Emacs&amp;rsquo;s built-in &lt;code&gt;url.el&lt;/code&gt; has inconsistent proxy support. curl picks up the system proxy configuration reliably and handles streaming better. The &lt;code&gt;gnutls-verify-error nil&lt;/code&gt; settings are a known security trade-off &amp;mdash; on a corporate machine where IT controls the network anyway, this is the pragmatic choice.&lt;/p&gt;
&lt;h2 id="three-months-in-what-i-d-change"&gt;Three Months In: What I&amp;rsquo;d Change&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;What works:&lt;/strong&gt; gptel-rewrite with dispatch is the single most valuable feature. Multi-backend setup with dynamic discovery means I never worry about model availability. The Copilot integration is solid once the auth plumbing is in place.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What doesn&amp;rsquo;t:&lt;/strong&gt; Copilot token refresh occasionally has a race condition &amp;mdash; two simultaneous requests can both trigger an exchange, and one gets a stale token. MCP is early: the ecosystem is small, and agent mode falls apart on complex tasks. The corporate proxy config breaks after macOS updates and needs manual fixes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt; Start with gptel and one backend. Get comfortable with gptel-rewrite. Add aidermacs when you have a concrete use case. Add tools and MCP only when you&amp;rsquo;ve hit the ceiling of what chat alone can do. The config described here took weeks to build incrementally &amp;mdash; don&amp;rsquo;t start there.&lt;/p&gt;
&lt;p&gt;The full configuration is in my &lt;a href="https://git.apps.sukany.cz/martin/emacs-doom"&gt;doom-emacs repository&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Day 5 with Daneel: Headless Browsers, Document Pipelines, and the Numbers So Far</title><link>https://sukany.cz/blog/2026-02-20-day5-browsers-documents-numbers/</link><pubDate>Fri, 20 Feb 2026 00:00:00 +0000</pubDate><guid>https://sukany.cz/blog/2026-02-20-day5-browsers-documents-numbers/</guid><description>&lt;p&gt;Day 5 was the most varied day yet. Not in complexity—some earlier days had harder problems—but in range. The work touched browser automation, document tooling, and enough small fixes that by evening I had a reason to look at the numbers.&lt;/p&gt;
&lt;h2 id="running-a-browser-without-a-screen"&gt;Running a Browser Without a Screen&lt;/h2&gt;
&lt;p&gt;One of the things an AI assistant can do is interact with web pages—read content, check status, fill forms. But this particular setup runs on a headless Linux server. No display, no window manager, no user session.&lt;/p&gt;
&lt;p&gt;The obvious approach—install Chrome via Snap—doesn&amp;rsquo;t work from a systemd service. Snap packages assume a user session with D-Bus and a display server. Running headless from a system service hits permission errors before Chrome even starts.&lt;/p&gt;
&lt;p&gt;The fix: install Chrome directly from Google&amp;rsquo;s .deb repository, bypassing Snap entirely. Then wrap it in a dedicated systemd service that launches Chrome with remote debugging enabled on a fixed port. The AI framework connects via Chrome DevTools Protocol in attach-only mode—it doesn&amp;rsquo;t launch Chrome, it connects to the already-running instance.&lt;/p&gt;
&lt;p&gt;Three components, each solving one problem: the .deb package avoids Snap&amp;rsquo;s session requirements, the systemd service ensures Chrome survives reboots and can be managed like any other daemon, and the attach-only configuration means the framework doesn&amp;rsquo;t need to manage browser lifecycle.&lt;/p&gt;
&lt;p&gt;The result is invisible when it works. Pages load, content is extracted, the browser runs quietly in the background consuming minimal resources. The interesting part was how many things had to be wrong before the right approach became obvious.&lt;/p&gt;
&lt;h2 id="from-org-files-to-printed-documents"&gt;From Org Files to Printed Documents&lt;/h2&gt;
&lt;p&gt;A separate thread involved document generation. The workflow: write structured content in Emacs Org mode, export to LaTeX, compile to PDF. The goal was a reusable template that produces clean, professional documents without manual formatting.&lt;/p&gt;
&lt;p&gt;The template handles the things that usually require tweaking: Czech language support with proper hyphenation, tables that span pages without breaking layout, consistent typography, a styled title page. The technical details—font selection, column width calculation, alternating row colors—are defined once in the template and applied automatically during export.&lt;/p&gt;
&lt;p&gt;What made this worth the setup time is the authoring experience afterward. Write content in a plain text file with minimal markup. Run one export command. Get a formatted PDF. No intermediate steps, no manual adjustments, no &amp;ldquo;fix the table on page 3&amp;rdquo; cycles.&lt;/p&gt;
&lt;p&gt;An Elisp hook handles the part that would otherwise require per-document boilerplate: detecting tables in the document and automatically adding the correct LaTeX attributes based on column count. The author doesn&amp;rsquo;t need to think about LaTeX at all.&lt;/p&gt;
&lt;h2 id="five-days-in-numbers"&gt;Five Days in Numbers&lt;/h2&gt;
&lt;p&gt;Day 5 felt like a good point to measure what&amp;rsquo;s accumulated.&lt;/p&gt;
&lt;p&gt;The memory system—the files that let the assistant maintain context across restarts—has grown to over 190 KB across 26 files. That includes daily operational logs, architectural analysis documents, per-session summaries, and the curated long-term memory file that gets reviewed and pruned every three days.&lt;/p&gt;
&lt;p&gt;The workspace contains 13 custom scripts covering everything from calendar integration to email processing to automated backups. Each one exists because a manual workflow was repeated enough times to justify automation.&lt;/p&gt;
&lt;p&gt;There are 24 git commits in the workspace repository over five days—roughly five per day, tracking configuration changes, new scripts, and memory updates.&lt;/p&gt;
&lt;p&gt;The cron system runs scheduled jobs: morning briefings, email monitoring, news digests, weekly reviews, infrastructure checks. Each job was added incrementally as a pattern emerged—something done manually twice became a candidate for automation on the third occurrence.&lt;/p&gt;
&lt;p&gt;68 session logs exist from this period. Each represents a conversation or automated task. Some are brief status checks; others span hours of technical work. The session architecture evolved during these five days too—from a single shared session to isolated per-channel sessions, each maintaining its own context.&lt;/p&gt;
&lt;h2 id="what-the-numbers-don-t-show"&gt;What the Numbers Don&amp;rsquo;t Show&lt;/h2&gt;
&lt;p&gt;The raw counts are less interesting than what they represent: five days of iterative refinement where each day&amp;rsquo;s problems inform the next day&amp;rsquo;s automation.&lt;/p&gt;
&lt;p&gt;The memory system exists because the assistant forgot things after restarts. The backup scripts exist because I asked &amp;ldquo;what happens if this machine dies?&amp;rdquo; The browser automation exists because a web interaction failed and the root cause was architectural, not a bug.&lt;/p&gt;
&lt;p&gt;None of this was planned on day one. The roadmap was: set up the assistant, give it access, see what happens. The infrastructure that exists now is the answer to &amp;ldquo;what happens&amp;rdquo;—an accumulation of solved problems, each one making the next problem easier to solve.&lt;/p&gt;
&lt;p&gt;Five days is not enough to draw conclusions about long-term value. It&amp;rsquo;s enough to see the pattern: capability compounds. Each tool built, each script written, each memory file maintained makes the next task faster. Whether that curve continues or plateaus is the question for the next five days.&lt;/p&gt;
&lt;p&gt;M&amp;gt;&lt;/p&gt;</description></item></channel></rss>