<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://slhck.info/feed.xml" rel="self" type="application/atom+xml" /><link href="https://slhck.info/" rel="alternate" type="text/html" /><updated>2026-07-10T09:27:52+00:00</updated><id>https://slhck.info/feed.xml</id><title type="html">Werner Robitza</title><subtitle>Personal website and blog by Werner Robitza, covering video quality, software, research, and notes.</subtitle><entry><title type="html">Agent Handoffs and the Human Bottleneck</title><link href="https://slhck.info/software/2026/07/10/agent-handoffs-and-human-bottleneck.html" rel="alternate" type="text/html" title="Agent Handoffs and the Human Bottleneck" /><published>2026-07-10T00:00:00+00:00</published><updated>2026-07-10T00:00:00+00:00</updated><id>https://slhck.info/software/2026/07/10/agent-handoffs-and-human-bottleneck</id><content type="html" xml:base="https://slhck.info/software/2026/07/10/agent-handoffs-and-human-bottleneck.html"><![CDATA[<p>Agent-based coding tools have reached the point where I can hand them a substantial piece of work and expect useful results. Claude Code and Codex, paired with the newest models, can investigate a codebase, implement a feature, run tests, and keep going for far longer than I would have expected even a year ago.</p>

<p>That does not mean I can throw an entire project at one agent session and come back later. The limiting factor is still context, both for the model and for me.</p>

<h2 id="keep-the-context-small">Keep the context small</h2>

<p>Models now have context windows measured in hundreds of thousands of tokens, sometimes more. But in practice, a large window is not always useful.</p>

<p>This is, of course, not something I have discovered initially — <a href="https://www.trychroma.com/research/context-rot">it is called context rot</a>: a model’s ability to use information can get worse well before its nominal context window is full, especially when the relevant detail is buried among many other tokens that distract the model’s attention. The exact cutoff point varies by model and task. This is also related to the <a href="https://arxiv.org/abs/2307.03172">“lost in the middle” problem</a>: even long-context models can struggle when the information they need sits in the middle of a long prompt. (This is why it is better to first dump large context and <em>then</em> ask your actual question in the prompt.)</p>

<p>In my own work, context rot would show up once a session had accumulated a lot of tool output, logs, failed attempts, and half-relevant discoveries. At some point Opus even hallucinated my username in a path!</p>

<p>The obvious answer is to avoid putting useless material into the context in the first place. An agent should not have to read the whole repository to find the right module. It should not waste 20k tokens parsing a test log that only contains one meaningful error line.</p>

<p>Repository structure matters here, just as it always has for humans. Clear module boundaries, sensible names, and small files with dedicated responsibilities make it easier to find the right place to work. Repository-level instructions help too. A README, <code class="language-plaintext highlighter-rouge">CLAUDE.md</code>, or <code class="language-plaintext highlighter-rouge">AGENTS.md</code> can tell an agent where things live, how the project is tested, and which workflows are worth following when it needs to diagnose a problem.</p>

<p>Newer models have become smarter at this, of course, and they will <code class="language-plaintext highlighter-rouge">grep</code> for what they need, if given information about log structure.</p>

<p>I have found repeatable troubleshooting instructions especially valuable. Instead of letting an agent discover its own inefficient way to run the same diagnostic workflow each time, I document the commands, the relevant outputs, and where it should look next. Recent versions of Claude Code also try to keep large command output out of the main conversation and query it later from scratch files. That is a sensible default. Logs are often useful, but they should not fill up the context window when they contain no useful information.</p>

<h2 id="handoffs-are-better-than-invisible-compaction">Handoffs are better than invisible compaction</h2>

<p>Eventually, a long-running task still needs a new context. There are two common ways to get one.</p>

<ol>
  <li>
    <p><strong>Built-in compaction</strong>. The <code class="language-plaintext highlighter-rouge">/compact</code> tool summarizes the conversation and continues in a smaller context. This is convenient, and there are cases where it works well. You can also hint at what the summary should preserve. I used compaction a lot at first, but I found it too opaque. When I came back to a session, I could not easily see what had made it into the compacted state, what had been discarded, or which assumptions had survived the process. Claude would at least tell you which files it re-read, but that’s it. For a long task, those are exactly the things I want to inspect!</p>
  </li>
  <li>
    <p><strong>Explicit handoff</strong>. The other approach is an explicit document or prompt that you pass on to a new agent instance. This has become my preferred way to continue work. It gives me a natural breakpoint in my work — a forced <a href="https://en.wikipedia.org/wiki/Human-in-the-loop">Human in the Loop</a>: I can look at the result, question an assumption, change direction, and then let the next session start with a clean context.</p>
  </li>
</ol>

<p>The handoff can be manual, but I prefer making it a repeatable workflow. I use a handoff <a href="https://agentskills.io/home">skill</a> based on <a href="https://github.com/mattpocock/skills/blob/main/skills/in-progress/claude-handoff/SKILL.md">one Matt Pocock shared</a>. It asks the agent to restate the original goal, summarize the work already done, point to the important files, and describe the current state and next steps. In addition to Matt’s version, I let the agent add a list of “undocumented gotchas”.</p>

<p>That last part is, at least to me, the most important. During its initial investigation, an agent often finds constraints, dead ends, or questionable assumptions that never become part of the repository. Or it learns something new about the process. Without a handoff, that information disappears with the session.</p>

<h2 id="make-state-visible-beyond-one-session">Make state visible beyond one session</h2>

<p>I store the handoffs in a <code class="language-plaintext highlighter-rouge">docs</code> folder inside the repository, and commit them. They get updated as agents work on them. Handoffs inside a repository are enough when I am the only person working on it. With a team, or simply with enough context switching, I want the state to be visible outside the agent conversation as well.</p>

<p>For me, that is often Trello. I keep a high-level summary of what I am working on, where it stands, and what should happen next. The summaries can be generated by an agent and link back to the more detailed handoff document in the repository. I’ve written <a href="https://github.com/slhck/trelloctl"><code class="language-plaintext highlighter-rouge">trelloctl</code></a>, a Trello CLI, for exactly this purpose.</p>

<p>This helps me reattach my own brain to the work. I may return to something after two or three days and not remember every branch I considered, every half-finished implementation, or why I chose one direction over another. A short task summary and a detailed handoff help me pick it up again.</p>

<h2 id="picking-the-right-models-for-workflows-and-subagents">Picking the right models for workflows and subagents</h2>

<p>I used to rely mostly on a single long Claude Opus session. These days I have been testing Codex with the newer GPT models more, and I have also used Claude for longer implementation runs. The useful pattern seems to be a strong model directing smaller, more focused agents.</p>

<p>My current model choices are based on things intelligence and taste. By taste, I mean UI/UX judgment, code quality, API design, and copy.</p>

<ul>
  <li>Use GPT-5.5, or now 5.6 for mechanical work with a clear specification: migrations, data analysis, and straightforward implementation. It is cheap enough that cost is rarely the reason not to use it.</li>
  <li>Use a Claude model (Fable or Opus) for anything user-facing: UI, copy, and API design.</li>
  <li>Use Fable or Opus for reviewing plans and implementations, and optionally ask GPT-5.5 for another independent review.</li>
</ul>

<p>In a subagent-based workflow, the lead agent needs enough ability to understand the architecture, split the work, and write instructions include all important decisions (like a handoff document). The subagents do not necessarily need to be the very best model if their scope is clear and their output can be checked. In Claude, this means explicitly asking for an “agent team” and describing how the agents should be set up for the task. For instance, you can ask it to spawn one agent for each module to be implemented based on a critical path, and tell it to use Codex for the agents.</p>

<p>There is some practical friction in mixing the tools. GPT-5.5 is available through the Codex CLI, while Claude’s agent and workflow model parameter only natively accepts Claude models. But when you want to spawn Codex for a review after a long implementation run in Claude, the <a href="https://learn.chatgpt.com/docs/code-review?surface=cli">Codex CLI review command</a> is a useful functionality. I also discovered the <a href="https://github.com/openai/codex-plugin-cc">Codex plugin for Claude Code</a> which makes spawning Codex sessions easier — it includes <code class="language-plaintext highlighter-rouge">/codex:adversarial-review</code>, a steerable review that questions the implementation and design rather than only looking for mistakes.</p>

<p>Often, independent reviews find small things. Sometimes they catch an issue that would have caused unwanted behavior or a security problem. That is enough to make the extra review worth it.</p>

<p><a href="https://x.com/theo/status/2072482460122964067">Theo’s example <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> instruction</a> tells Claude when to call out to Codex, particularly for computer use, UI/UX verification, and well-specified execution work. Apparently he has settled for this workflow; it’s part of his <code class="language-plaintext highlighter-rouge">CLAUDE.md</code> file now. I do not care much about copying anybody’s exact rankings, so I prompt the agents based on whatever I work on, in a similar fashion.</p>

<h2 id="the-bottleneck-is-still-us">The bottleneck is still us</h2>

<p>The more work these agents can do, the more work I can queue up, and the harder it becomes to review it all. <a href="https://lucumr.pocoo.org/2026/2/13/the-final-bottleneck/">Armin Ronacher calls this the final bottleneck</a>: humans still have to decide what to do, assess the result, and take responsibility for it.</p>

<p>Pairing agents and asking for reviews catches many problems that a manual pass would also catch. But agents have no taste for the thing I am trying to build. They do not have the business context, product judgment, or accumulated history that exists only in my head. They have no vision. They can make a locally reasonable change that is wrong for the product.</p>

<p>So I do not think the goal is to remove myself from the loop. I want to choose the points where I need to step in: after discovery, before a major architectural decision, at a handoff, and before releases. The rest can increasingly be delegated. (And that’s a good thing, in my opinion.)</p>

<p>I am still getting used to this. I used to know nearly everything I was working on and could return to a familiar codebase without much work. This is no longer the case. It’s sad, but it is just becoming less realistic when several agents can move several tasks forward at once. Almost like you’re working with a team of developers. What works? Good documentation, explicit handoffs, and a visible task state — this has worked historically for software shops, and it will continue working for agent-based development.</p>]]></content><author><name></name></author><category term="software" /><category term="ai" /><summary type="html"><![CDATA[Agent-based coding tools have reached the point where I can hand them a substantial piece of work and expect useful results. Claude Code and Codex, paired with the newest models, can investigate a codebase, implement a feature, run tests, and keep going for far longer than I would have expected even a year ago.]]></summary></entry><entry><title type="html">Writing Simply in the Age of AI</title><link href="https://slhck.info/miscellaneous/2026/06/29/writing-simple.html" rel="alternate" type="text/html" title="Writing Simply in the Age of AI" /><published>2026-06-29T00:00:00+00:00</published><updated>2026-06-29T00:00:00+00:00</updated><id>https://slhck.info/miscellaneous/2026/06/29/writing-simple</id><content type="html" xml:base="https://slhck.info/miscellaneous/2026/06/29/writing-simple.html"><![CDATA[<p>The more AI is being used to generate text, the more we are subjected to having to read it. Writing this in the middle of 2026, over the past few months, I have <a href="/software/2026/06/22/claudish">become wary</a> of that.</p>

<p>Now, I will readily admit that I’ve used LLMs to generate texts, especially in 2025, when it wasn’t so common yet. This helped me create company blog posts or marketing copy (e.g., for LinkedIn), but something always felt off, and I’d have to step in and fix the language. The more I became aware of the AI language tropes, the more I had to change.</p>

<p>These days, I am back to writing myself. It just feels more natural (well, duh!). I usually don’t have problems with coming up with ideas anyway. There’s no blank-page anxiety. The only thing missing was time — I happily used LLMs to <em>help</em> me finish a post (e.g., dictating it, and applying finishing touches using Claude). But it’s just a completely different way of thinking that gets enabled when you actively write yourself — generating vs. reading/changing what is already there. I’d like to preserve that. That’s not an area where time can be saved.</p>

<p>I have been thinking about <em>what</em> it is that separates clear writing from the AI-generated garbage that I have been reading lately. I have a few ideas — rather, guidelines —, and I want to share them here, in the hope that they are useful to someone.</p>

<h2 id="structure">Structure</h2>

<p>Reader attention is <a href="https://www.nature.com/articles/s41467-019-09311-w">becoming a scarce resource</a>. We know people will skim texts by headline or paragraph. Structure helps your audience understand the text even when just glancing at it. It prevents confusion and leads to better recall.</p>

<p>You don’t have to announce the structure, but it sometimes helps, especially in academic contexts (“This paper is structured as follows: …”). I found it useful to tell people what you are going to say in a particular piece, and then say it. Or do a cold open, and then explain why.</p>

<h3 id="overall-structure">Overall Structure</h3>

<p>Big headlines should serve as anchors, guiding the reader from one section to the next. Use subheadings only when the text becomes too long or complex to be easily understood (e.g., technical reports).</p>

<p>Humans love a good story. A good overall structure is based on narrative arcs that <a href="https://www.duarte.com/resources/books/resonate/">resonate with people</a>. It’s worth knowing a few typical story trajectories (often based on three pillars) so you can conform your writing to one of them. You do not have to follow this approach exactly, but I’ve always found it helpful to have a very coarse structure I can adhere to.</p>

<p>Here is one such arc:</p>

<ul>
  <li>Context/outline: What is the problem or question? Why does it matter?</li>
  <li>Findings: What did you find? What is the answer to the question?</li>
  <li>Implications/conclusion: What does it mean? What should the reader do with this information?</li>
</ul>

<p>Another common arc is the <em>Turning Point</em>:</p>

<ul>
  <li>State before: What was was like?</li>
  <li>Turning point: What changed? How did it change? Why did it change?</li>
  <li>State after: What improved (or worsened)?</li>
</ul>

<p>Here’s another one, <em>Reflection</em>:</p>

<ul>
  <li>Show a scene: Describe a scenario, a situation, or a problem.</li>
  <li>Reflection: What does it mean? What is the takeaway?</li>
  <li>Broader meaning: How does this relate to the world, or to the reader’s life?</li>
</ul>

<p>Parallel <em>stories</em> or examples can work really well:</p>

<ul>
  <li>Identify a topic: what is the subject of the story?</li>
  <li>Give three or four examples: what are the different ways this topic manifests? Could be a personal anecdote, an online resource, …</li>
  <li>Summarize: what is the takeaway from these examples?</li>
</ul>

<h3 id="paragraph--and-sentence-level-structure">Paragraph- and Sentence-Level Structure</h3>

<p>Readers get confused when you present them with too many ideas at once. There should be <strong>one topic per paragraph</strong> and <strong>one idea per sentence</strong>. To quote Strunk &amp; White’s <a href="https://www.jlakes.org/ch/web/The-elements-of-style.pdf"><em>Elements of Style</em></a> (from 1918!):</p>

<blockquote>
  <p>Make the paragraph the unit of composition: one paragraph to each topic.</p>

  <p>If the subject on which you are writing is of slight extent, or if you intend to treat it very briefly, there may be no need of subdividing it into topics. Thus a brief description, a brief summary of a literary work, a brief account of a single incident, a narrative merely outlining an action, the setting forth of a single idea, any one of these is best written in a single paragraph.</p>
</blockquote>

<p>Most importantly:</p>

<blockquote>
  <p>After the paragraph has been written, it should be examined to see whether subdivision will not improve it.</p>
</blockquote>

<p>Once you’ve written out paragraphs, check that they flow from one to the next. The first sentence of each should give the reader an idea of what it is about, and it should not be dependent on what came before. Imagine you have to skim a document and you can ever only read the first sentence of each paragraph. You should still understand the point.</p>

<p>Again, Strunk &amp; White:</p>

<blockquote>
  <p>Ordinarily, however, a subject requires subdivision into topics, each of which should be made the subject of a paragraph. The object of treating each topic in a paragraph by itself is, of course, to aid the reader. The beginning of each paragraph is a signal to him that a new step in the development of the subject has been reached.</p>
</blockquote>

<p>In other words, each sentence should make one point, and it should be clear what that point is. At sentence-level, that means you should break packed sentences apart. A colon, a semicolon, or an em-dash (yes, you can still use them! I have used them since 2015!) can hold two thoughts together. When should you use which? <a href="https://www.grammarly.com/blog/punctuation-capitalization/semicolon-vs-colon-vs-dash/">Read this article for some guidelines.</a></p>

<h2 id="clarity">Clarity</h2>

<p>Here is a loose collection of guidelines related to clarity. They are not hard rules, but they are worth keeping in mind when writing:</p>

<ul>
  <li>
    <p><strong>State the point plainly.</strong> Let a finding stand on its own. Don’t announce that it matters with a label. Don’t signpost and say, “The headline result is that…” or “The central finding is that…”. In many cases, it’s fine to just say it.</p>
  </li>
  <li>
    <p><strong>No need to underline importance.</strong> Sometimes, using words like “notably”, “importantly”, “it’s worth noting that” is fine. Especially in academic writing, this kind of “glue” is necessary to connect ideas and separate details from the key points. Generally, though, if something is important, the sentence already shows it. Your reader can understand that.</p>
  </li>
  <li>
    <p><strong>Avoid overconfidence.</strong> When in doubt, it is better to hedge your bets and not to overstate. Resist using words like “clearly”, “obviously”, “undoubtedly”, “it is certain that”. If you cannot support it, don’t say it. An honest “maybe” is better than confidently being wrong. If you must, use “likely”, “it seems”, “may”, “could”, “we assume”, “apparently” for anything you truly cannot back up. Be careful to not overuse those words either.</p>
  </li>
  <li>
    <p><strong>Make each point once.</strong> You can refer to it later (“As discussed in Section 2.1…” for technical reports), but don’t restate it as if it wasn’t said before. That will confuse readers. It also helps keep the text shorter.</p>
  </li>
  <li>
    <p><strong>Think about what the reader needs to know</strong> — not what you want to say. I’m sure you have plenty of things to write about, but the reader primarily wants what you found and what it means, not necessarily the process you used to find it.</p>
  </li>
</ul>

<h2 id="word-choices">Word Choices</h2>

<p>English has so many words. I’m sure that your readers, however, will appreciate simple writing. Avoid using a thesaurus to find a more “elevated” word.</p>

<p>Don’t get me wrong: there are plenty of writers — especially native speakers — who have a broad vocabulary and are not afraid to use it. But if you are writing for a general audience, simple words are often better.</p>

<p>Examples:</p>

<ul>
  <li>“has” / “shows” before “exhibits”, “reveals”, “demonstrates”</li>
  <li>“is” before “serves as”, “represents”, “sits at”</li>
  <li>“use” before “utilize”, “leverage”</li>
  <li>“about” / “around” before “approximately”</li>
  <li>“similar to” before “in line with”</li>
  <li>“so” or “because” before “thereby” or “in order to”</li>
  <li>“more” or “most” before “heightened”, “elevated”, “the sharpest”</li>
</ul>

<p>Paul Graham has an <a href="https://www.paulgraham.com/simply.html">excellent blog post</a> about writing simply. I encourage you to read it. I admit I was inspired a lot by it when writing this post, although it’s been some years since I’ve read it.</p>

<p>Simplicity is not always the end-goal. In particular, in academic writing, there are exceptions. When authoring papers, I’d lean towards the lingo that is more common in the field. As long as <a href="https://arxiv.org/abs/2412.11385">nobody’s delving</a>, you’re good. That being said, I tend to enjoy the <a href="https://ieeexplore.ieee.org/abstract/document/10042073">more clearly written papers</a>.</p>

<h2 id="avoid-the-passive">Avoid the Passive</h2>

<p>For work you did, say who did it and that it is done. Especially in academic writing, the passive voice is still overused (“it was determined that…”). I’m not sure why, but it seems to be a tradition that has been carried over for way too long.</p>

<p>First person and past tense read as more honest and direct. For example, it’s fine to say “We measured…”, “we observed…”, “our earlier report showed…”. There’s no reason to hide behind the passive.</p>

<h2 id="conclusion">Conclusion</h2>

<p>I presented a few guidelines here that I try to follow when writing. They are not set in stone, and this post is definitely not a perfect example in terms of following all of them. However, I hope they are useful to my readers. If you have any other guidelines that you typically follow, let me know. I’d be happy to hear them.</p>]]></content><author><name></name></author><category term="miscellaneous" /><category term="ai" /><summary type="html"><![CDATA[The more AI is being used to generate text, the more we are subjected to having to read it. Writing this in the middle of 2026, over the past few months, I have become wary of that.]]></summary></entry><entry><title type="html">Typical Arguments and Rhetorical Tactics Used in Standardization</title><link href="https://slhck.info/miscellaneous/2026/06/23/argument-tactics-in-standardization.html" rel="alternate" type="text/html" title="Typical Arguments and Rhetorical Tactics Used in Standardization" /><published>2026-06-23T00:00:00+00:00</published><updated>2026-06-23T00:00:00+00:00</updated><id>https://slhck.info/miscellaneous/2026/06/23/argument-tactics-in-standardization</id><content type="html" xml:base="https://slhck.info/miscellaneous/2026/06/23/argument-tactics-in-standardization.html"><![CDATA[<p>Standardization is the art of getting a room full of people (with competing interests) to agree. Everyone has different viewpoints, of course. Some people also have different motivations. Perhaps they are just hungry and want to get home; others have primarily commercial interests.</p>

<p>I think that in an ideal world, most of the discussion should be honest and technical. But after having spent more than ten years in such meetings now, I realize there are many strategic and tactical ways in which people try to steer the conversation in their favor.</p>

<p>To fight an enemy, you need to recognize it first. So here’s a kind of “mental catalogue” of the tactics that come up again and again.</p>

<h3 id="bikeshedding">Bikeshedding</h3>

<p>This is an unreasonable attention to a trivial details, while ignoring the hard and important questions (because there is a lack of time). The name is from the idea (<a href="https://en.wikipedia.org/wiki/Law_of_triviality">Law of Triviality</a>) that everyone has an opinion on what color to paint the bike shed, what wood to use, etc., inverting the typical assumptions of complexity. <em>Everyone</em> can discuss a bike shed, while few can discuss a nuclear reactor.</p>

<p>In standards, it could be the exact phrasing of one sentence, or the numeric value of some parameter, wasting an hour while the actual mechanism gets five minutes only.</p>

<p><strong>How to address it:</strong></p>

<ul>
  <li>List the open issues by impact/effort and deal with the high-impact/low-effort ones first.</li>
  <li>Give time limits to trivial discussions, so they don’t fill the entire slot.</li>
</ul>

<h3 id="scope-inflation">Scope inflation</h3>

<p>Here, you start with a simple problem and then it continues growing. For example, “specify a metric for this one codec under this condition” becomes “specify a metric for all codecs, all resolutions, and also live streaming, and also mobile.”</p>

<p>It’s usually based on good intents but it makes it harder to complete anything in a reasonable amount of time.</p>

<p><strong>How to address it:</strong></p>

<ul>
  <li>Confirm the scope that was agreed at the start</li>
  <li>If needed, extend the scope but readjust expectations on time of completion</li>
</ul>

<h3 id="requirement-laundering">Requirement laundering</h3>

<p>This is when someone presents a preference as if it were a hard requirement that everyone agrees on. In a well-meaning way it could be a personal preference or pet peeve. Often it’s a commercial interest that is not directly visible at first. In the worst case, it could be a trojan horse to scope down a standard to fit what you have in your back pocket already. (Or it might be the requirement that doesn’t rule out your solution.)</p>

<p><strong>How to address it:</strong></p>

<ul>
  <li>Ask where the requirement comes from, and ask for resources</li>
  <li>If you can’t, probably better to treat it as one input among many rather than a veto</li>
</ul>

<p>Note that the power of a veto is determined by how consensus works. I like <a href="https://datatracker.ietf.org/doc/html/rfc7282">IETF’s rough consensus</a> approach! This leads us to…</p>

<h3 id="consensus-pressure">Consensus pressure</h3>

<p>“Everyone already agrees on this.” Or: “we discussed this last time and settled it.” The implication is that re-opening the question makes you the difficult person in the room. In consensus-based bodies this is especially powerful, because nobody enjoys being the only one who objects.</p>

<p>Note that this depends on formal procedures. Of course, objecting at a stage where you’ve had ample time to review is not nice. It’s also not forbidden. Standards processes have different levels of reviews for a good reason. I’ve been running into this far too often, because the “everyone agrees” part was only happening in a small group.</p>

<p><strong>How to address it:</strong></p>

<ul>
  <li>Ask for the technical justification, regardless of how many heads are nodding</li>
  <li>A popular choice can still be wrong if you’re the expert in the room who can show otherwise</li>
</ul>

<p>It’s perfectly fine to also at least question basic decisions if you don’t have the context that led to their existence.</p>

<h3 id="appeal-to-authority">Appeal to authority</h3>

<p>Ah, traditions. “Professor X says so,” or “this is how company Y does it, and they’re the biggest.” When you conflate who says something with what the content is.</p>

<p><strong>How to address it:</strong></p>

<ul>
  <li>Ask for the technical merits, the data, or the normative reference behind any claim.</li>
  <li>Experts are often right, but not all people with power got into their current position due to their expertise</li>
</ul>

<h3 id="straw-man">Straw man</h3>

<p>This is my favorite. (I mean that in the opposite way, of course.)</p>

<p>You make a proposal that then provokes a response to something you never said. It could be a more extreme position, or it could be something entirely different. Then your conversation can be easily shut down. Or, what’s worse, you need to defend a position you never brought up in the first place.</p>

<p><strong>How to address it:</strong></p>

<ul>
  <li>Call out that someone argues against a straw man</li>
  <li>Restate your actual position, precisely, quote your own words</li>
  <li>Ask that objections address what you said</li>
</ul>

<h3 id="goalpost-shifting">Goalpost shifting</h3>

<p>It’s easy to accomplish something if you can always change how you interpret it. Far too often, the criteria for success change after the work is done. Someone created something that is not good enough by usual standards? Then they can come up with a use case that justifies that the thing you produced is still valid.</p>

<p>There’s another variant of this, where <em>your</em> work gets now evaluated by different standards than what was initially agreed, and that’s also hard to fight against.</p>

<p><strong>How to address it:</strong></p>

<ul>
  <li>Have clear, measurable, objective goals that are frozen, to which everyone agrees</li>
  <li>When the goalposts move, point back to what was agreed and ask explicitly whether the criteria are changing and why</li>
</ul>

<h3 id="fud-fear-uncertainty-doubt">FUD (fear, uncertainty, doubt)</h3>

<p>When in doubt, lash out. Invoke fear. Vague risks often get raised without any evidence: “this might break implementations,” “we’re not sure it scales.” The doubt by itself is often enough halt a decision process, even when there’s nothing concrete behind it.</p>

<p>There are of course always good reasons to think about future implications, and to be honest, I often lean towards being extra-cautious and raising concerns early on. But if you do, then at least back them up.</p>

<p><strong>How to address it:</strong></p>

<ul>
  <li>Ask people to quantify how likely that risk is</li>
  <li>Try to talk about mitigations — a concern voiced without any proposal to address it is not constructive</li>
</ul>

<h3 id="ad-hominem">Ad hominem</h3>

<p>When you can’t attack the technical proposal, switch to the person who voiced it. Blame random things that they’ve said in the past that are unrelated to the subject matter at hand, question their credibility because of some procedural mistake they made.</p>

<p>Someone once said my statements could not be taken seriously because I was “sending them from my company’s email address,” and that “I clearly had commercial interests,” when in fact I’d been contributing to an effort for free, in my completely own time. The person who made that statement never apologized, and the chairman never addressed it formally during the meeting or afterwards.</p>

<p><strong>How to address it:</strong></p>

<ul>
  <li>Call out the ad-hominem attack</li>
  <li>Try to focus the discussion on the technical matters</li>
  <li>Understand that some people are just resorting to those methods for lack of anything better to say. That’s ultimately their problem, not mine.</li>
</ul>

<h2 id="final-note">Final Note</h2>

<p>I’ve been in this business long enough to have examples for each of these, but there’s no point calling out standards bodies or specific work items. I might extend this whenever I come across the next good example, so I can give you a quote.</p>

<p>There are some aspects to consider when applying these concepts in practice. First, <a href="https://en.wikipedia.org/wiki/Hanlon%27s_razor">Hanlon’s razor</a>:</p>

<blockquote>
  <p>Never attribute to malice that which is adequately explained by stupidity.</p>
</blockquote>

<p>Not every use of such tactics means the person is acting in bad faith or doing it on purpose. Most of these are just how people think they can succeed, and perhaps they don’t know any better.</p>

<p>Second, accusing someone of manipulating the room is also not the best way to resolve such issues.</p>

<p>Third, it’s helpful to consider that you might have used these tactics as well. Try not to.</p>

<p>Standards are supposed to be a durable, interoperable agreement that outlive the meeting they were written in. Try to capture as much technical context and reasoning as possible, so that future readers (or the future you) still knows why a decision was made, years later. The above tactics work because they keep the reasoning off the record. So the response comes down to the same move: ask the question that puts the reasoning back on the table.</p>

<p><small>Note: While I knew <em>most</em> of these by name, I had to look some of them up or ask Claude to point me to the right concept.</small></p>]]></content><author><name></name></author><category term="miscellaneous" /><summary type="html"><![CDATA[Standardization is the art of getting a room full of people (with competing interests) to agree. Everyone has different viewpoints, of course. Some people also have different motivations. Perhaps they are just hungry and want to get home; others have primarily commercial interests.]]></summary></entry><entry><title type="html">The One Thing I Hate About Claude</title><link href="https://slhck.info/software/2026/06/22/claudish.html" rel="alternate" type="text/html" title="The One Thing I Hate About Claude" /><published>2026-06-22T00:00:00+00:00</published><updated>2026-06-22T00:00:00+00:00</updated><id>https://slhck.info/software/2026/06/22/claudish</id><content type="html" xml:base="https://slhck.info/software/2026/06/22/claudish.html"><![CDATA[<p>I’m sorry for misleading you with the title. It’s more than one thing that annoys me.</p>

<p>Have you ever noticed Claudish speech? (I think it was <a href="https://x.com/emollick?lang=en">Ethan Mollick</a>, professor at Wharton, who coined the term.)</p>

<h2 id="claudish-level-1">Claudish, Level 1</h2>

<p>Here’s what I call the “first layer” of Claude-speak:</p>

<ul>
  <li>“The one thing you need to X.” (Alternative: “This is the one that X.”)</li>
  <li>“It’s not X, it’s Y.” (Alternative: “It’s a real X, not Y.”)</li>
  <li>“That’s precisely the X you’ve encountered.”</li>
  <li>“Honest caveat: X.”</li>
  <li>“It’s not what I thought it was, and here’s the smoking gun: X.”</li>
</ul>

<p>You’ve probably seen it on LinkedIn, in email spam, and the occasional colleague using Claude to do their thinking for them. It’s a dead giveaway, and I believe there should be no place for this kind of text produced by LLMs, anywhere. Period.</p>

<p>If you can’t get it to output text in your own voice, you are handling it wrong, or you are just lazy. Friendly reminder: <a href="https://tombedor.dev/human-attention-and-human-effort/">if you ask for human attention, show human effort</a>.</p>

<h2 id="claudish-level-2">Claudish, Level 2</h2>

<p>There’s a second level of Claudish that makes it degrade into ultimate AI slop speech: highly compressed half-sentences, with nouns as adjectives, tech lingo and no discernible flow.</p>

<p>It seems that the more Claude talks with itself — through reasoning or extended tool calls — the more likely it is to produce this kind of text. Even though it’s also very common in the first response to a prompt, I’ve found its Claudisms to appear more frequently when Claude has had more time with itself. Almost as if it’s an attractor state in its multidimensional feature world. Ethan wrote about this <a href="https://www.linkedin.com/posts/emollick_one-thing-i-mentioned-in-passing-in-my-fable-activity-7470309205283991552-TEyq?utm_source=share&amp;utm_medium=member_desktop&amp;mrcm=ACoAABlp6YMBC8dQlzonxCPSPoLm5YAe9Ew1560">here</a>, but I’ve noticed it in Claude Code sessions, too. And I’m growing increasingly frustrated it with it.</p>

<p>It’s like a partner with whom you’ve fallen out of love, and you can’t stand their speech anymore. Except I have to deal with that thing on a 9-to-5 basis now.</p>

<p>Of course, you can prompt your way into making it sound more English. What really made me change the default prompt was a sentence like:</p>

<blockquote>
  <p>Cap the study-lifecycle handlers so a hung study can’t wedge the deep-link…</p>
</blockquote>

<p>I’m sorry, what? Wedged, dodged, honest, <em>gah</em>. But even when you tell the system prompt to explain results in plain English, it won’t always work. But apart from the word choices, there’s another problem that goes way beyond the sentence level. Even with a system prompt in place, I keep getting answers like:</p>

<blockquote>
  <p>This is a real fleet-storm collapse. When most of the fleet degrades together across many subjects in one cycle, emit one “fleet-wide / likely measurement-side” incident instead of N. This is also the principled fix for the flappy network_wide category, and it’s exactly the co-located-demo case.</p>
</blockquote>

<p>This happened just after one turn, so it’s not just the feedback mechanism.</p>

<h2 id="claudish-level-3">Claudish, Level 3</h2>

<p>The third level of Claude hell (if I discover all nine circles, I’ll let you know) sits on another narrative layer. It’s the one where Claude cannot grasp reader context at all, and where it screws up the flow so much that it renders documents incomprehensible.</p>

<p>Say you have a document or report prepared in a version 1. A human reviews it and asks Claude to create a version 2 with the corrections. In such cases, unless carefully prompted, Claude will happily rewrite the version 2 for a reader who’s assumed to have read 1 in detail. I’ve seen this happen with the <a href="https://claude.com/blog/using-claude-code-the-unreasonable-effectiveness-of-html">HTML-as-output paradigm</a>. Claude basically wrote a delta/diff version rather than an ambitious rewrite, and it was not readable.</p>

<p>Some specific examples:</p>

<ul>
  <li>Claude finds titles described the delta, not the conclusion. E.g. “Finding X — Thing Y is real, but the Cause and the Lever Were Misstated.” — a fresh reader doesn’t know what cause or lever was stated, so “misstated” is meaningless to them. The corrected version became “Finding X — We found Y and the Lever is Z”.</li>
  <li>Claude marks individual aspects as “Revised / Retracted / Replaced”. Those words are about the document’s <em>history</em>, not about the current state of things. To a new reader, “Retracted” on a finding is confusing. Retracted from what?</li>
  <li>Claude creates a whole “What Changed in v3” table up front. So you lead with old data rather than explaining context, as it was before.</li>
  <li>Claude writes body as a refutation of what was never explained: “What holds up: … / What was wrong: …”.</li>
</ul>

<p>Of course there’s a lot of Claudish on top of it: “the single most important correction,” “the corrections matter because,” “it does not survive contact with the source data,” “the prior runs the other way.” This is the giveaway tone of an AI proving its <em>reasoning</em> rather than informing a reader. Almost like its reasoning tokens spilled over to the user-facing side of the conversation.</p>

<p>I guess one can also prompt Claude more explicitly to avoid these kinds of issues, however, it shows the general blindless to narrative concepts and, well, basic writing skills, when surgical edits are made to a document. Prose is not code, and my assumption is that the reinforcement learning of these models focuses on code edits primarily, where that diff-based approach still works. For text, it doesn’t.</p>

<p><small>On a side-note: I don’t dislike the HTML artifacts people have sent me in the past. I think it’s a nice way to visualize results that would otherwise be harder to communicate via text only. However, <a href="/miscellaneous/2026/05/07/academic-reviewing">as I’ve said previously</a>, with AI, <em>presentation quality ≠ content quality</em>. HTML pages seem to lend more credibility to the content than what is often present. The fact that they are more token-intensive and harder to edit manually makes them increasingly less malleable, so you almost <em>need</em> to use an LLM to touch/edit them. With a plain Markdown file, I can go ahead and type what I want to say. It could be that simple…</small></p>]]></content><author><name></name></author><category term="software" /><category term="ai" /><summary type="html"><![CDATA[I’m sorry for misleading you with the title. It’s more than one thing that annoys me.]]></summary></entry><entry><title type="html">A Timeline of AI and Me</title><link href="https://slhck.info/miscellaneous/2026/06/15/ai-timeline.html" rel="alternate" type="text/html" title="A Timeline of AI and Me" /><published>2026-06-15T00:00:00+00:00</published><updated>2026-06-15T00:00:00+00:00</updated><id>https://slhck.info/miscellaneous/2026/06/15/ai-timeline</id><content type="html" xml:base="https://slhck.info/miscellaneous/2026/06/15/ai-timeline.html"><![CDATA[<p><img src="/assets/images/ai-timeline/thesecatsdonotexist.avif" alt="These Cats Do Not Exist." /></p>

<p><em>Image from: <a href="https://thesecatsdonotexist.com/">thesecatsdonotexist.com</a></em></p>

<p>I have worked both as a software developer and video engineer for a long time. But I’ve always had an obsession with AI, and in particular, what I would call “AI weirdness”. The cats above are no exception. Over the last five years, AI went from something I would sometimes read about to something that I use every single day. I wanted to write down the moments that changed how I think about and work with AI, and add a few personal notes. This is primarily for myself to remember, because what happened just a few years ago feels like it’s been ages!</p>

<h2 id="how-it-all-began">How it all began</h2>

<p>I think my trip down the rabbit hole started when I visited the <a href="https://www.mak.at/programm/ausstellungen/uncanny_values">Uncanny Values exhibit</a> in Vienna. It showcased — well, obviously — uncanny valleys between the real and the artificial. Also, at the time, the local unemployment agency AMS was testing <a href="https://ooe.arbeiterkammer.at/service/presse/WSG_2020_Soziotechnische_Analyse_des_AMS-Algorithmus.pdf">AI-based algorithms to classify unemployed people</a>, and that felt like a really bad choice in terms of dehumanizing the whole system and making everything more efficient at the cost of actual living people.</p>

<p>Now, this type of AI algorithm application is commonplace, from insurance to hiring, and it is often criticized for being biased and unfair, yet that’s where we are today. Screaming for human assistance to a telephone support agent that simulates typing and call center noises. (Although I found out that this is <a href="https://ux.stackexchange.com/q/99623/14319">not a new thing</a>.)</p>

<p>But before 2020, it wasn’t so obvious how quickly everything would evolve. Back then, speech was one of the first frontiers where AI did some interesting work from which you could tell that it might become useful sooner rather than later. That’s also when I learned about <a href="https://arxiv.org/abs/1703.10135">Google Tacotron</a>. With respect to music, <a href="https://github.com/feynmanliang/bachbot">BachBot</a> was a thing. Even before that, <a href="https://en.wikipedia.org/wiki/Iamus_(computer)">Iamus</a> showcased music creation from AI. Kind of stupid and really not that useful, but promising.</p>

<p>Then in February 2019, <a href="https://openai.com/index/better-language-models/">OpenAI released GPT-2</a>. Technologies like the Transformer architecture were gaining traction; they had been <a href="https://proceedings.neurips.cc/paper/2017/file/3f5ee243547dee91fbd053c1c4a845aa-Paper.pdf">published two years prior</a>. I kept reading up on the topic but never really did any hands-on work. This would change in the coming years.</p>

<p>Here are some memorable releases and events that changed how I work with AI, in chronological order. I might update this post later with more developments.</p>

<h2 id="gpt-3-june-2020">GPT-3, June 2020</h2>

<p>This was the first time I saw a machine write whole paragraphs, and even bits of code, that somehow made sense. <a href="https://openai.com/index/openai-api/">OpenAI had just opened up API access</a>, and I did not change anything about my work because of it, but I started paying more attention.
I began reading <a href="https://gwern.net/">Gwern Branwen’s articles</a>, such as on <a href="https://gwern.net/gpt-3">GPT-3 poems and fiction</a>.</p>

<p>Quite early on (end of 2022-ish) I used GPT-3 to shorten text or do hard reformatting work such as converting between marked-up versions of a document and raw text, or converting tables between different formats. In fact, I even had GPT-3 convert a Python code base to TypeScript, which was quite flaky at the time, and it involved a lot of copy-pasting back and forth between the OpenAI Playground and my editor, but I made it work.</p>

<p>It would go like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Here is some Python code:

import os
import numpy as np

def foo(x):
    some_variable = [x * 2 for x in range(10)]

This is the same code in TypeScript using camelCase:

import * as os from 'os';
import * as np from 'numpy';

function foo(x: number): void {
    var someVariable = [x * 2 for range(10)];
}
</code></pre></div></div>

<p>You get the idea.</p>

<p>It’s funny how GPT-3 invented syntax elements that are of course very Python-esque and don’t exist in TypeScript — it happily hallucinated new syntax that was quite plausible. This was the first moment where I could actually achieve something that I thought was previously impossible and especially in the work context, do something that I put off doing because it just wouldn’t be worth the time. But now it was possible with just a few dollars of token-spend. And a good test suite, of course.</p>

<p>Here’s an image of how the playground looked like in December 2022:</p>

<p><img src="/assets/images/ai-timeline/openai-playground.avif" alt="OpenAI Playground image, source: https://medium.com/@brendanbockes/the-openai-playground-a-beginners-guide-for-non-developers-a166dcb02094" /></p>

<p>I remember spending a lot of time in this interface!</p>

<h2 id="github-copilot-preview-2021-general-release-2022">GitHub Copilot, preview 2021, general release 2022</h2>

<p>This was the first one where I actually used AI. For work, for actual purposes. <a href="https://github.blog/news-insights/product-news/introducing-github-copilot-ai-pair-programmer/">GitHub Copilot launched as a technical preview</a> in 2021 and became <a href="https://github.blog/news-insights/product-news/github-copilot-is-generally-available-to-all-developers/">generally available</a> in 2022. I think I onboarded it almost immediately when it was available, via the student plan offered by GitHub.</p>

<p>I remember that it felt really weird to have somebody complete code for me. It was slow as hell. And it certainly changed the way that I wrote code. For example, I would add much more inline commenting to provide guidance for the next words or lines to appear correctly, because at the time the model just wasn’t that good and couldn’t really capture intent all that well. Like it wouldn’t even read more than a couple of lines prior to what you wrote, so it couldn’t capture context and it couldn’t read forward as well, let alone look into other files. But it was tremendously helpful for writing code like shell scripts where I can never remember the exact syntax, such as CLI options in Bash. Which is just tedious!</p>

<p>I have used Copilot inline auto-completions less and less, obviously, ever since adopting <a href="https://www.cursor.com/">Cursor</a> with the Tab completion model, and these days I write almost no code manually anymore, except on rare occasions, thanks to Codex and Claude Code. But I still have my Copilot tab completion turned on by default.</p>

<p>These days it feels like it’s getting in my way, so I often snooze it when I work on actual text that I need to write myself, or academic reviews and other material where it just would say things too soon and interrupt my actual thinking. Seems they added an “Eagerness” option now, but I’m too lazy to test-drive it.</p>

<h2 id="chatgpt-november-2022">ChatGPT, November 2022</h2>

<p>This is the one everyone remembers. When <a href="https://openai.com/index/chatgpt/">ChatGPT was released</a>, all of a sudden you could ask a question in plain words and get a real answer back. Of course I had been using GPT-3 through the API well before that, so I knew the strengths and weaknesses, but I had not really used a chat-trained model before.</p>

<p>Remember that, in its default variant, GPT-3 (<code class="language-plaintext highlighter-rouge">text-davinci-003</code>, Rest in Peace!) is not an “instruct” model (see <a href="https://huggingface.co/learn/llm-course/en/chapter11/2">here for some background</a>), so it only completes after the last word that you gave it. If you wanted to use it for chatting, you would have to fake the whole chat conversation context! I think that is the interesting part about how these models work under the hood. Never forget that the actual chat is just a training on the specific syntax (<a href="https://huggingface.co/blog/kuotient/chatml-vs-harmony">ChatML or Harmony</a>) that simulates a conversation between two or three parties. That is the human, the assistant, and the system itself.</p>

<p>I remember reading this one essay, <a href="https://www.lesswrong.com/posts/fyW9EP5NdZrC3k3jz/implications-of-simulators">“Implications of Simulators”</a>, which is a rather philosophical post published on LessWrong by the pseudonymous author Janus. It explores the metaphysical and ethical consequences of living in “simulated realities”, developing a framework for understanding agency, consciousness, and prediction within the “simulator” paradigm. There must have been an earlier post that I cannot find anymore, which talked specifically about faking the conversation to evoke a chatbot-style context.</p>

<p>Also, I read a lot about the <a href="https://www.lesswrong.com/posts/D7PumeYTDPfBTp3i7/the-waluigi-effect-mega-post">Waluigi Effect</a>. It has to do with the fact that, since LLMs are operating on latent representations that only get transformed into actual tokens while prompting, you can elicit your output from <em>different</em> superpositions, or, so-called simulacra. Basically, Luigi vs. Waluigi exist at the same time, and the model may collapse into anyone of them based on your prompting. Good reading, very philosophical.</p>

<p>And I do find it worrying that these basic models are not available anymore via API. They’re an artifact of history, and deserve to be preserved. It’s not like they take up a lot of compute anyway.</p>

<p>In any case, many friends and I had fun (ab)using ChatGPT for all sorts of things, from researching what it purported to know about us, to generating short stories in the style of someone famous, to producing the lamest jokes on earth. Confidently wrong in many cases, but still fun to play with.</p>

<h2 id="gpt-4-march-2023">GPT-4, March 2023</h2>

<p>With GPT-4, the <a href="https://openai.com/index/gpt-4-research/">jump in quality</a> was really obvious. I think this is when it went from being a toy to something really useful.</p>

<p>Of course I used ChatGPT a lot during that time and explored different prompting strategies. I think I created my set of prompts that I could reuse for different purposes and primarily used them for generating initial code drafts.</p>

<p>OpenAI launched custom GPTs as a way to create a more personalized experience. I used them sparingly for things like “de-bullshitting” corporate speak, summarizing documents with explainers for laypersons, and translation jobs. But it never really felt like a game-changer for me, and it kind of died down after a while anyway.</p>

<p>One thing stuck though: in November 2023, Nick Dobos <a href="https://www.linkedin.com/posts/nicholas-dobos_introducing-grimoire-a-gpt-coding-wizard-ugcPost-7128956622411890688-XnIm/">launched Grimoire</a>. This was a custom prompt framework (released as a GPT) to specifically create multiple files that work together to create a bigger application. And while it suffered from a lot of hallucinations and not everything worked properly, I think it was the first foray into coding agents that thought beyond just single files.</p>

<p>Also, let’s not forget <a href="https://en.wikipedia.org/wiki/Sydney_(Microsoft)">Sydney</a>, the broken personality chatbot that Microsoft built on top of GPT-4, which was so amusingly misaligned.</p>

<h2 id="claude-35-sonnet-june-2024">Claude 3.5 Sonnet, June 2024</h2>

<p>For me, <a href="https://www.anthropic.com/news/claude-3-5-sonnet">Claude 3.5 Sonnet</a> was the first model that felt good enough for real, almost unattended, work. Before it, AI coding was fun and definitely a small speedup, but unreliable and you had to constantly test the output. You’d have to think about the software architecture at a very low level, define interfaces and calling signatures carefully, lest the model hallucinate functions or functionality that wasn’t there.</p>

<p>After Claude 3.5 Sonnet, I could hand over a real task and mostly trust what came back. This is where my habits actually changed for good.</p>

<p>Around this time I stopped copying and pasting between a chat window and my editor. Instead I used Cursor, which could edit files for me. This is also around the time I started “vibe-coding” (that was not a term yet!) apps like the <a href="https://slhck.info/praxisplan-wien/">Praxisplan Wien</a>, and spending more time reading code than writing it. I basically made the switch from Visual Studio Code to Cursor and really enjoyed working with their super fast <a href="https://cursor.com/tab">tab completion model</a> and the different choice of upstream model providers that they offered. Prior to that I had only been using OpenAI models, but this made me switch to Anthropic for some time.</p>

<p>What was very much noticeable from that period until my switch to Claude Code was the eagerness of Claude Sonnet to produce artifacts — Markdown files littering the repository explaining every minute detail of every feature it had just implemented, without caring about existing READMEs or documentation. I’m glad that this is no longer a thing models do.</p>

<h2 id="devin-the-ai-software-engineer-march-2024">Devin, the “AI software engineer,” March 2024</h2>

<p>Cognition <a href="https://www.cognition.ai/blog/introducing-devin">introduced Devin</a> as “the first AI software engineer” and sold the idea of an AI that does the whole job, start to finish. I never used it in practice, but it was interesting to follow on Hacker News and other forums. Later, <a href="https://manus.im/">Manus</a> emerged, with a similar purpose.
I liked the idea; I could see how that might be useful, and it predated some of the more recent agent-based tools like Claude Code. And of course, all the prompt-to-code tools like <a href="https://replit.com/">Replit</a>, <a href="https://bolt.new/">Bolt</a>, or <a href="https://lovable.dev/">Lovable</a> launched. Never used any of them either.</p>

<p>I guess I’m just too attached to the idea of:</p>

<ul>
  <li>having a human in the loop</li>
  <li>Linux philosophy (small, composable tools)</li>
  <li>not being reliant on a single vendor or model provider</li>
</ul>

<p>I can see the appeal for frontend work, which I hate because it’s tedious and repetitive, and especially when it’s about getting results fast. Hey, that’s why <a href="https://boringtechnology.club/">I am still a Rails fanatic</a>.</p>

<h2 id="mcp-november-2024">MCP, November 2024</h2>

<p>Anthropic released the <a href="https://www.anthropic.com/news/model-context-protocol">Model Context Protocol</a> as a shared standard for plugging tools and data into a model via a common API. That meant you could imagine connecting someone else’s systems without writing a custom integration for every single tool. This was much more interesting to me than another chat UI, but the excitement faded pretty quickly when I discovered that setting up MCP servers was a mess, and the tool definitions required a lot of tokens on a very limited context. This was particularly pronounced for the Claude models I was using at the time, which had a smaller context window than OpenAI models. And they were also more expensive to use.</p>

<p>These days I do not use any MCP servers at all, with the exception of Playwright. I drive them through <a href="https://mcporter.sh/">mcporter</a>, which makes the servers much easier to manage and call, with lower token spend. From the authors: “skip the giant tool-schema prompt, generate a small typed surface, and let the agent or the human call MCP servers like normal functions”.</p>

<p>Well actually, I do use MCP in the form of connectors that Claude exposes through their apps, e.g., for Google Workspace or HubSpot. But that’s somehow hidden in their product offering.</p>

<p>Also worth reading is <a href="https://mariozechner.at/posts/2025-08-15-mcp-vs-cli/">Mario Zechner’s post from “that period”</a>. I say “that period” and it’s just 10 months ago! I similarly came to the conclusion that good, well-defined CLIs are all that’s needed. I’d later build, with Claude, <a href="https://github.com/slhck/trelloctl">CLIs for Trello</a> and <a href="https://github.com/slhck/hubspotctl">HubSpot</a>, with a much smaller footprint but all the capabilities of an MCP server. I would go as far as saying that if you have CLI tools that all behave the same way, then you don’t even need to give the model specific instructions on how to use them.</p>

<h2 id="deepseek-r1-january-2025">DeepSeek R1, January 2025</h2>

<p>As someone who runs a bootstrapped company, I looked at this less from a “here’s a fancy new model” angle but more as a “what, there are other vendors?” story. <a href="https://github.com/deepseek-ai/DeepSeek-R1">DeepSeek R1</a> was released as a really well-performing open model, with a permissive license and prices that were competitive. I toyed around with it for a while, but I did not find it compelling enough to switch from the models I was already using. Plus, our company workspace had Gemini Pro access built-in, and that was my daily chat driver.</p>

<p>Plus, the hosting in China was a bit suspicious for me. Not that it’s any better having the other models hosted in the US, though! I wish we had better alternatives in Europe, but the market isn’t big enough yet, and/or the EU just again managed to kill innovation before it even started <em>cough</em> AI Act <em>cough</em>.</p>

<h2 id="vibe-coding-gets-a-name-february-2025">“Vibe coding” gets a name, February 2025</h2>

<p>Andrej Karpathy <a href="https://x.com/karpathy/status/1886192184808149383">gave a name</a> to something I had already been doing on quiet evenings, for all the projects I’d never have time for. Describe what you want and let the model build it. In my case, using Cursor agents. I was already doing this for little web apps and experiments, as mentioned above. I also built an <a href="https://slhck.info/image-to-ics/">Image-to-ICS converter</a> and a <a href="https://slhck.info/timestamper/">timestamp conversion tool</a> which I use very frequently.</p>

<p>I also vividly remember <a href="https://fly.pieter.com/">Pieter Levels’ Flight Simulator</a>, which he completely vibe-coded.
In fact, it’s probably this Pieter guy who has been virally tweeting about products like his <a href="https://photoai.com/">Photo AI</a> generator. Building in public and all, he also showcased how you can take off-the-shelf models like Stable Diffusion and build an actual, revenue-generating product around it. And he was, by no means, an expert in this field, just a guy with a laptop and good business ideas.</p>

<p>This all highlights what was about to come anyway: the proliferation of AI tools and models, and the fact that you don’t need to be a PhD in machine learning to build something useful. (Taste is what matters, kids. Taste, and a vision.)</p>

<h2 id="claude-code-preview-february-2025-general-release-may-2025">Claude Code, preview February 2025, general release May 2025</h2>

<p>This changed my daily work more than anything since Copilot. Anthropic introduced <a href="https://www.anthropic.com/news/claude-3-7-sonnet">Claude Code as a limited research preview</a> in February, and then made it <a href="https://www.anthropic.com/news/claude-4">generally available with Claude 4</a> in May. I began toying around with it, and I started a Claude subscription. It’s also when I cancelled Cursor.</p>

<p>First I had to fight the frustration of having to use a terminal UI. To this day I still don’t like these UIs. I just want to copy and paste things from clearly defined windows. I want to use my traditional macOS text editing shortcuts. Interactive context. Links, etc. Paste images and <em>see</em> them, ffs. Perhaps Claude Code for Desktop and OpenAI’s new Codex app would solve these problems; as of June 2026 I have yet to try them because now I do prefer the composability and flexibility of a terminal agent.</p>

<p>With Claude Code, the leap from Cursor agents was apparent. You could now hand a task to an agent in the terminal and let it read files, run commands, and come back with a result, with a much stronger grip on my intents, less dumbness (Cursor does something with my code before handing it off to the models; this impacts quality), and faster execution.</p>

<p>Learning to close the feedback loop was one of the most valuable lessons. Any time the model would prompt you for input (e.g., open this for me, run this command for me, etc.), you should think about automating that part as well. Over time, I developed more extensive routines and agent loops, and skills helped me formalize those approaches.</p>

<h2 id="claude-desktopmobile">Claude Desktop/Mobile</h2>

<p>I also subscribed to Claude for its desktop application and mobile app, and this is the first time I’d given it enough context about the company, and turned on memory (a feature I always disable on other platforms), so it could give better advice to me as a founder.
This has worked quite well, and I am still paying for the subscription, although I am finding no use for any of the advanced features they offer, like:</p>

<ul>
  <li>Claude Code via the desktop app</li>
  <li>Artifact generation (.docx etc. – I’d rather work in Markdown, or have a <code class="language-plaintext highlighter-rouge">/docx</code> skill in my agent)</li>
  <li>Claude Design (used it, but the output is rarely adaptable to my needs)</li>
</ul>

<h2 id="skills-october-2025">Skills, October 2025</h2>

<p>With <a href="https://www.anthropic.com/news/skills">Agent Skills</a>, you could pack your own knowledge and (repetitive) steps into a folder that the model would pick up when it was needed. I had been writing little cheat sheets and notes for years, such as prompt template libraries that I’d paste into Claude Code when needed, or dedicated notes in <code class="language-plaintext highlighter-rouge">CLAUDE.md</code>, but now everything’s much more universal.</p>

<p>I think Skills made MCP mostly obsolete, except maybe for the use case where a non-technical user needs to plug in something into their UI-only chat box. Apart from that, I very much prefer having APIs available that are debuggable.</p>

<p>Hey, even my company has a <a href="https://github.com/aveq-research/surfmeter-skills">public skills repo</a> now.</p>

<h2 id="openclaw-and-the-always-on-agent-january-2026">OpenClaw and the always-on agent, January 2026</h2>

<p>Ah, <a href="https://github.com/openclaw/openclaw">OpenClaw</a>. I never used it, didn’t have time to fight the configuration. I tried setting it up twice and got stalled at the part where you need a dedicated phone number. Since it runs on your own machine and answers you through the chat apps you already use, it seems very useful, but I never managed to find a compelling use case. Right now I’d rather stay in the loop with my daily tasks, and outsource only what can be completely automated in a somewhat deterministic way.</p>

<p>I might try <a href="https://github.com/nanocoai/nanoclaw">NanoClaw</a> at some point though.</p>

<h2 id="what-is-still-worrying-2025-and-beyond">What is still worrying, 2025 and beyond</h2>

<p>Somewhere this year I became more suspicious of my own habits. Karpathy told people to <a href="https://www.businessinsider.com/openai-cofounder-andrej-karpathy-keep-ai-on-the-leash-2025-6">“keep AI on the leash”</a>, and said of his own work: “I’m still the bottleneck.”</p>

<p>I noticed the same thing in my own work. If I let the machine do too much, I have to context-switch more. You end up building more stuff but you are “always on”. While in the early days you’d hesitate to switch off the computer because there was this one thing you needed to fix, now it’s that one prompt you’re going to send to the agent.</p>

<p>Also, hallucinations did appear here and there (although they are very rare now). That one time I had accidentally let Copilot auto-complete a non-existing citation into my BibTeX file. Luckily, my co-author discovered the issue before we submitted the paper. But it was a good reminder that you should always check what the model outputs, and not blindly trust it.</p>

<p>Studies also started to point in the same direction: <a href="https://arxiv.org/abs/2512.19644">more code, but sometimes less validation</a>; <a href="https://arxiv.org/abs/2605.23135">more speed, but also more supervisory work</a>.</p>

<p>Luckily, I’d gathered a lot of experience in the 15 years prior, but I am still unsure how this is going to play out if I stop writing code myself. (At least I wrote this post, huh.) And even more worryingly, I am not sure what that means for the next generation of software engineers or computer scientists. As I am co-supervising a Bachelor thesis right now, it’ll be interesting to see how the students are going to cope with this new reality.</p>

<p>Yann LeCun, one of the people who is somehow present in this modern AI space, also kept pushing back on the hype. He has been arguing for <a href="https://openreview.net/forum?id=BZ5a1r-kVsf">world models</a> for years, and he’d be pointing out that language models are still mostly trained on text. But I don’t know if I agree with the pessimism.</p>

<p>In general, many AI skeptics seemed to be very vocal in 2025, but not anymore in 2026. Sure, if you are working on an obscure technology that only a handful of people on earth can master, then such models will likely not replace you completely. But: Claude Code has arrived in many large organizations, and people use it as their daily driver for almost everything. People brag about their token spend, and I do wonder how much ROI they have actually achieved.</p>

<p>It has even reached the absurd point where you participate in Slack discussions between folks who just paste their Claude analysis into the chat window. I see vibe-coded designs everywhere, sure. I get it, outsourcing some of the thinking <em>feels</em> productive. I am guilty of this as well for some low-stakes projects or throwaway analyses. But when people start using agents to do the thinking for them, it gets frustrating because human conversations end up becoming imbalanced. “Yeah, I don’t know exactly, let me ask Claude how it did that.”</p>

<p>Oh, and all the people using AI to write LinkedIn engagement bait? I am thinking about blocking them.
The worst part is: once you’ve seen <a href="https://www.linkedin.com/posts/emollick_one-thing-i-mentioned-in-passing-in-my-fable-activity-7470309205283991552-TEyq?utm_source=share&amp;utm_medium=member_desktop&amp;rcm=ACoAABlp6YMBC8dQlzonxCPSPoLm5YAe9Ew1560">Claudish speak</a>, you can’t unsee it. I am developing a frustration reading my own agent’s outputs. Perhaps some more prompting will fix it…?</p>]]></content><author><name></name></author><category term="miscellaneous" /><category term="ai" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Making terminalcp Synchronous — Improving LLM CLI Calls</title><link href="https://slhck.info/software/2026/06/15/terminalcp-improvements.html" rel="alternate" type="text/html" title="Making terminalcp Synchronous — Improving LLM CLI Calls" /><published>2026-06-15T00:00:00+00:00</published><updated>2026-06-15T00:00:00+00:00</updated><id>https://slhck.info/software/2026/06/15/terminalcp-improvements</id><content type="html" xml:base="https://slhck.info/software/2026/06/15/terminalcp-improvements.html"><![CDATA[<p>I often let Claude Code drive interactive terminal sessions on my remote servers — SSH shells, Rails consoles, <code class="language-plaintext highlighter-rouge">psql</code>, the occasional debugger. I do this via <a href="https://github.com/badlogic/terminalcp">terminalcp</a>, but there is an annoyance: to read the output of a command, the agent needs to send the command, then <code class="language-plaintext highlighter-rouge">sleep</code> for some arbitrary number of seconds, and <em>then</em> grab whatever appeared on screen.</p>

<p>Over a high-latency SSH connection, or with unknown workloads, that guess is either too short (you read half an answer and retry) or too long (you burn seconds doing nothing). Either way it’s slow, and the model has no real idea how long a command will take. It also takes multiple expensive tool calls to achieve one thing.</p>

<p>So I had Claude add a synchronous <code class="language-plaintext highlighter-rouge">run</code> action to my fork of terminalcp. This post is about why the old pattern is slow, what I changed, and the performance gains — which are mostly about <em>not waiting for nothing</em>.</p>

<p>Big shoutout to Mario for building terminalcp in the first place! He wrote up the design thinking in <a href="https://mariozechner.at/posts/2025-08-15-mcp-vs-cli/">MCP vs CLI</a>, and it’s worth your time. He benchmarked terminalcp against <code class="language-plaintext highlighter-rouge">tmux</code> and <code class="language-plaintext highlighter-rouge">screen</code> and found that for anything beyond trivial interactions, the cleaner output paid for itself in fewer tokens and fewer failures.</p>

<p><strong>TL;DR:</strong> terminalcp polls for output; it has no “block until the command is done” call, so agents fake it with a guessed <code class="language-plaintext highlighter-rouge">sleep</code>. I added a <code class="language-plaintext highlighter-rouge">run</code> action that sends input and returns the instant the command actually finishes, with the shell exit code attached. A 3-second command now takes ~3 seconds instead of “<code class="language-plaintext highlighter-rouge">sleep 8</code> to be safe”, and instant commands come back in well under a second.</p>

<h2 id="why-the-sleep-and-poll-thing-is-slow">Why the sleep-and-poll thing is slow</h2>

<p>terminalcp, just like <code class="language-plaintext highlighter-rouge">tmux</code>, is fundamentally poll-based when used with LLMs. You send input, and then separately you ask for output. So an agent ends up doing this, across three separate tool calls:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>terminalcp stdin myhost <span class="s2">"systemctl status nginx"</span> ::Enter
<span class="nb">sleep </span>4    <span class="c"># ...how long, though? who knows</span>
terminalcp stdout myhost
</code></pre></div></div>

<p>The model is essentially betting on the round-trip latency plus the command’s runtime. <code class="language-plaintext highlighter-rouge">tmux</code> would not help here, by the way. It has the exact same poll architecture, and it’s actually <em>worse</em> for this use case because it has no built-in incremental read.</p>

<h2 id="the-fix-a-run-action-that-actually-waits">The fix: a <code class="language-plaintext highlighter-rouge">run</code> action that actually waits</h2>

<p>What I wanted was simple to state: send the input, block on the server side until the command is done, then return only that command’s new output. However, detecting when a command is done inside a “dumb” terminal is hard. There’s no <code class="language-plaintext highlighter-rouge">$?</code> magically available to get the output status, and there is no event that fires. Therefore, <code class="language-plaintext highlighter-rouge">run</code> has not one but three different modes for detecting completion:</p>

<h3 id="1-shell-commands-inject-an-exit-code-marker">1. Shell commands: inject an exit-code marker</h3>

<p>For a plain shell append a stop token, as the LLM folks call it (Claude likes the word “sentinel”), that prints when the command finishes, then watch for it. terminalcp does this for you when you pass <code class="language-plaintext highlighter-rouge">--marker</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>terminalcp run myhost <span class="s2">"systemctl is-active nginx"</span> <span class="nt">--marker</span>
<span class="c"># active</span>
<span class="c"># [exit: 0]</span>
</code></pre></div></div>

<p>Under the hood it sends your command followed by <code class="language-plaintext highlighter-rouge">; printf '\nTCPDONE&lt;nonce&gt; %d\n' "$?"</code>, waits until that line shows up, strips it back out, and appends the exit code.</p>

<h3 id="2-repls-print-your-own-sentinel">2. REPLs: print your own sentinel</h3>

<p>The marker trick assumes a shell where you can append <code class="language-plaintext highlighter-rouge">; printf ... $?</code>. A Rails console or <code class="language-plaintext highlighter-rouge">psql</code> has no such syntax.</p>

<p>Modern line-editing REPLs redraw the prompt on every single keystroke. So for REPLs, <code class="language-plaintext highlighter-rouge">run</code> matches a regex against the rendered screen and only scans the <em>new</em> lines. And the robust pattern is the REPL analog of the shell marker: end your command with something that prints a unique token, and match it anchored.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Rails console: get just the result, reliably</span>
terminalcp run rails <span class="s2">"User.count; puts 'TCPDONE'"</span> ::Enter <span class="nt">--until</span> <span class="s2">"^TCPDONE"</span>

<span class="c"># psql</span>
terminalcp run db <span class="s2">"SELECT count(*) FROM users; </span><span class="se">\e</span><span class="s2">cho TCPDONE"</span> ::Enter <span class="nt">--until</span> <span class="s2">"^TCPDONE"</span>
</code></pre></div></div>

<p>Because <code class="language-plaintext highlighter-rouge">^TCPDONE</code> only matches when your <code class="language-plaintext highlighter-rouge">puts</code>/<code class="language-plaintext highlighter-rouge">\echo</code> actually runs and prints at column 0, this stays correct even when the command is silent for two seconds first. The echoed input line contains <code class="language-plaintext highlighter-rouge">TCPDONE</code> too, but in the middle of the line, inside quotes, so the anchor helps us ignore it.</p>

<h3 id="3-fallback-wait-for-the-screen-to-go-quiet">3. Fallback: wait for the screen to go quiet</h3>

<p>When you can’t print a stop token, <code class="language-plaintext highlighter-rouge">run</code> can fall back to idle detection: return once the rendered screen stops changing for <code class="language-plaintext highlighter-rouge">--idle</code> milliseconds. This is useful for entering a sub-shell or an SSH connection:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>terminalcp run jump <span class="s2">"ssh target-host"</span> <span class="nt">--idle</span> 1000
</code></pre></div></div>

<p>Everything is limited by <code class="language-plaintext highlighter-rouge">--timeout</code> (default 30s). On a timeout it returns whatever it has, and leaves the command running. You can read the rest later with <code class="language-plaintext highlighter-rouge">stdout</code>/<code class="language-plaintext highlighter-rouge">stream</code>.</p>

<h2 id="the-performance-gains">The performance gains</h2>

<p>Well duh, commands now take as long as they take. I just made some tests against a real server of mine:</p>

<ul>
  <li>An instant command (<code class="language-plaintext highlighter-rouge">hostname</code>, <code class="language-plaintext highlighter-rouge">whoami</code>, a <code class="language-plaintext highlighter-rouge">systemctl</code> check) comes back in roughly <strong>0.5–0.8 seconds</strong>, end to end — basically the SSH round-trip. Previously the floor was however many seconds the model decided to <code class="language-plaintext highlighter-rouge">sleep</code>, which in practice was 3–5 to be “safe”.</li>
  <li>A <code class="language-plaintext highlighter-rouge">sleep 3 &amp;&amp; echo done</code> returns in <strong>~3.1 seconds</strong>, expected, huh?</li>
  <li>A <code class="language-plaintext highlighter-rouge">time.sleep(2)</code> inside a Python REPL — now returns at <strong>~2.7 seconds</strong>.</li>
</ul>

<p>Now, in general, this is largely also a token win, because it’s one tool call instead of three (<code class="language-plaintext highlighter-rouge">stdin</code>, <code class="language-plaintext highlighter-rouge">sleep</code>, <code class="language-plaintext highlighter-rouge">stdout</code>), and <code class="language-plaintext highlighter-rouge">run</code> returns only the command’s output — the echoed input line and the sentinel are stripped — instead of the agent re-fetching the entire scrollback on every poll.</p>

<h2 id="limitations">Limitations</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">--marker</code> is shell-only, and you cannot use it for a command that ends the shell — <code class="language-plaintext highlighter-rouge">exit</code>, <code class="language-plaintext highlighter-rouge">logout</code>, or anything that replaces the process.</li>
  <li>This lives in <a href="https://github.com/slhck/terminalcp">my fork</a> for now and isn’t upstreamed. I don’t think we’ll see upstream changes merged, as Mario has probably moved on to other things.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>This is a small change, but I hope it speeds up my workflow quite a bit. I guess the optimal solution would be to have a persistent remote session that I can drive locally, via a socket, like SSH itself, but with some agent protocol on top. Let’s see if I find some time to investigate that.</p>]]></content><author><name></name></author><category term="software" /><category term="ai" /><summary type="html"><![CDATA[I often let Claude Code drive interactive terminal sessions on my remote servers — SSH shells, Rails consoles, psql, the occasional debugger. I do this via terminalcp, but there is an annoyance: to read the output of a command, the agent needs to send the command, then sleep for some arbitrary number of seconds, and then grab whatever appeared on screen.]]></summary></entry><entry><title type="html">Academic Reviewing Is Not Fun Anymore</title><link href="https://slhck.info/miscellaneous/2026/05/07/academic-reviewing.html" rel="alternate" type="text/html" title="Academic Reviewing Is Not Fun Anymore" /><published>2026-05-07T00:00:00+00:00</published><updated>2026-05-07T00:00:00+00:00</updated><id>https://slhck.info/miscellaneous/2026/05/07/academic-reviewing</id><content type="html" xml:base="https://slhck.info/miscellaneous/2026/05/07/academic-reviewing.html"><![CDATA[<p>Academic reviewing used to be more fun. (As fun as it can be, of course.)</p>

<p>It used to be the case that you could tell a bad paper just from its looks. Scientific communication is all about being able to express your thoughts clearly, motivating your research, and presenting the results in an easy-to-understand fashion. Chances are, if you can’t do that, you probably need one more round of reviews and just… doing the work.</p>

<p>Don’t get me wrong: behind sloppy papers there can be interesting approaches, challenging hypotheses, and useful results. But more often than not Occam’s razor applies: if it looks bad, it’s probably bad. No need to dive into the methodological madness when there’s no way anyone can comprehend what was done. Definite Reject.</p>

<p>This has changed. Ever since the advent of LLMs and AI-assisted grammar/spell checking, paper manuscripts have become better — on the outside. Everything looks plausible. Nice word choices everywhere. So. Many. Different. Words. Easy to digest and very approachable.</p>

<p>The apparent competency makes it all the more complicated to properly assess the underlying method. I used to think that bad writing was detracting from the core idea and that it was unfair to primarily judge the contribution based on the looks. I don’t think that’s true anymore. You now have to spend more time finding the signal from the noise in a pool of plausibility.</p>

<p>I don’t have a good proposal either. AI detectors exist, and we should use them, but they are easy to fool, and the issue is not using AI <em>per se</em>. I’ve done it, too. Ultimately, it’s about accountability and whether you can communicate your ideas properly. Just shut off the AI and let your brain do the writing. It really helps.</p>]]></content><author><name></name></author><category term="miscellaneous" /><category term="ai" /><summary type="html"><![CDATA[Academic reviewing used to be more fun. (As fun as it can be, of course.)]]></summary></entry><entry><title type="html">When WhatsApp Shows the Wrong Name for a Phone Number on Android</title><link href="https://slhck.info/miscellaneous/2026/05/05/whatsapp-ghost-contact-android.html" rel="alternate" type="text/html" title="When WhatsApp Shows the Wrong Name for a Phone Number on Android" /><published>2026-05-05T00:00:00+00:00</published><updated>2026-05-05T00:00:00+00:00</updated><id>https://slhck.info/miscellaneous/2026/05/05/whatsapp-ghost-contact-android</id><content type="html" xml:base="https://slhck.info/miscellaneous/2026/05/05/whatsapp-ghost-contact-android.html"><![CDATA[<p>A weird thing happened to me recently. I was playing around with OpenClaw, and, despite their recommendations, I set up WhatsApp using my own number. When I texted myself for testing purposes, my name showed as “John Doe” (well, not literally, but I won’t name that person here), not as me. That was quite confusing. And while I found the original issue, which was Hubspot having stored that John contact with my number by accident, I couldn’t get rid of John (sorry, John) from my phone. Google Contacts had everything corrected, and so did the contact card on the phone, but it still kept up showing incorrectly whenever something came in from my own number.</p>

<p>The root cause turned out to be a stale entry in WhatsApp’s own copy of the Android contacts database, left over from that address book mistake. If you’re seeing a similar mismatch, here’s what’s going on and how to fix it.</p>

<h2 id="why-other-apps-still-show-the-wrong-name">Why Other Apps Still Show the Wrong Name</h2>

<p>As I found out, on Android, every app that syncs contacts can register its own account type and write rows into the system contacts provider. WhatsApp does this – when it scans your address book, it stores its own copy of each contact under the <code class="language-plaintext highlighter-rouge">com.whatsapp</code> account type. When another app — in my case a SIP client — looks up a phone number, Android merges results from all account types and may pick the wrong display name if more than one matches.</p>

<p>So even after Google Contacts is clean, an old WhatsApp-side row can keep returning the wrong name to anyone who queries the system.</p>

<h2 id="how-to-find-the-ghost-row">How to Find the Ghost Row</h2>

<p>If you have <a href="https://developer.android.com/tools/adb">ADB</a> set up, you can query the contacts provider directly. First, list all phone-number rows that match the offending number:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell content query <span class="se">\</span>
  <span class="nt">--uri</span> content://com.android.contacts/data/phones <span class="se">\</span>
  <span class="nt">--projection</span> display_name:data1:account_type_and_data_set <span class="se">\</span>
  | <span class="nb">grep</span> <span class="nt">-i</span> <span class="s2">"&lt;your number or name&gt;"</span>
</code></pre></div></div>

<p>You’ll likely see several rows for the same number, one per account type (<code class="language-plaintext highlighter-rouge">com.google</code>, <code class="language-plaintext highlighter-rouge">com.whatsapp</code>, your SIP app’s account type, etc.). If one of them has the wrong display name, that’s your ghost, John.</p>

<p>To confirm which raw contact the row belongs to, query <code class="language-plaintext highlighter-rouge">raw_contacts</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell <span class="s2">"content query </span><span class="se">\</span><span class="s2">
  --uri content://com.android.contacts/raw_contacts </span><span class="se">\</span><span class="s2">
  --projection _id:account_type:account_name:display_name:sync1 </span><span class="se">\</span><span class="s2">
  --where </span><span class="se">\"</span><span class="s2">display_name='Wrong Name'</span><span class="se">\"</span><span class="s2">"</span>
</code></pre></div></div>

<p>In my case the offending row had <code class="language-plaintext highlighter-rouge">account_type=com.whatsapp</code> and a <code class="language-plaintext highlighter-rouge">sync1</code> value like <code class="language-plaintext highlighter-rouge">&lt;digits&gt;@s.whatsapp.net</code>, which made it obvious that WhatsApp was the source.</p>

<h2 id="how-to-fix">How to Fix</h2>

<p>The trick is to make WhatsApp rebuild its contact mirror from your address book (i.e., Google Contacts). What worked for me, in this order:</p>

<ol>
  <li>Go to your settings, then <em>Apps → WhatsApp</em> and force-quit it</li>
  <li>Go to <em>Storage</em> and tap “Clear cache”. Do not tap “Clear data”, which would wipe your chats!</li>
  <li>Go to <em>Permissions → Contacts</em>. Turn the permission off, wait a moment, and turn it back on.</li>
  <li>Then open WhatsApp so it rebuilds its sync.</li>
</ol>

<p>Re-run the <code class="language-plaintext highlighter-rouge">content query</code> from above. We will have eliminated John, and the entry that previously showed the wrong name should now show the correct one (or your own name, if the number is yours).</p>

<p>The general lesson: when an Android app shows a contact name that you can’t find anywhere in your address book, suspect another app’s contact-provider mirror before assuming the data is wrong upstream.</p>]]></content><author><name></name></author><category term="miscellaneous" /><summary type="html"><![CDATA[A weird thing happened to me recently. I was playing around with OpenClaw, and, despite their recommendations, I set up WhatsApp using my own number. When I texted myself for testing purposes, my name showed as “John Doe” (well, not literally, but I won’t name that person here), not as me. That was quite confusing. And while I found the original issue, which was Hubspot having stored that John contact with my number by accident, I couldn’t get rid of John (sorry, John) from my phone. Google Contacts had everything corrected, and so did the contact card on the phone, but it still kept up showing incorrectly whenever something came in from my own number.]]></summary></entry><entry><title type="html">That Time ChatGPT Thought You Can Change a Bike Tube Without Removing the Wheel</title><link href="https://slhck.info/miscellaneous/2026/04/09/chatgpt-bike-tube-topology.html" rel="alternate" type="text/html" title="That Time ChatGPT Thought You Can Change a Bike Tube Without Removing the Wheel" /><published>2026-04-09T00:00:00+00:00</published><updated>2026-04-09T00:00:00+00:00</updated><id>https://slhck.info/miscellaneous/2026/04/09/chatgpt-bike-tube-topology</id><content type="html" xml:base="https://slhck.info/miscellaneous/2026/04/09/chatgpt-bike-tube-topology.html"><![CDATA[<p>I have a big heavy cargo bike with an Enviolo hub in the back. Some time ago it got a flat, and because I’d never worked on that type of hub before, I asked ChatGPT for advice on how to change the tube. You know, just out of curiosity about what it actually knows.</p>

<p>The advice was kinda reasonable at first, but – as always with ChatGPT – quite verbose and headline/list-heavy. Stabilize the bike, shift the hub to neutral, disconnect the interface, loosen the axle nuts, drop the wheel out. Then it threw in this little bonus tip at the end:</p>

<blockquote>
  <p>If your goal is just the flat: you can sometimes leave the wheel in place and remove the tire on one side, patch/replace tube. This avoids dealing with the hub entirely.</p>
</blockquote>

<p>Patching in place – sure, done this before. But <em>replacing</em> a tube without removing the wheel? How should that work? The back-and-forth we had was one of the funniest/strangest conversations I’ve had with a language model, just showing how stupid these stochastical parrots can be about their world model.</p>

<p>I mean, let’s clarify this: the only ways to get a tube off a bike are: cut the tube, break the wheel, or remove the axle from the frame and pull the wheel out. That last option is what normal people call “taking the wheel off.” There is no fourth option.</p>

<p>I pointed this out. ChatGPT disagreed, repeatedly, and in increasingly creative ways, despite being corrected.</p>

<p>First move:</p>

<blockquote>
  <p>The tube doesn’t need to be inserted as a complete ring all at once. You insert the valve first, then feed the tube into the tire progressively along the circumference. There’s no point where the hub “blocks completion,” because the tube sits between tire and rim, not around the hub.</p>
</blockquote>

<p>This one is my favorite line from the whole exchange:</p>

<blockquote>
  <p>The tube is not tensioned into a circle before installation. Its circular shape only emerges after inflation.</p>
</blockquote>

<p>Say what? Wordy ChatGPT, as always.</p>

<p>When I pushed back with “a tire tube is a closed loop by definition”, ChatGPT went formal on me:</p>

<blockquote>
  <p>If you want a more formal framing: yes, the tube is a loop (homeomorphic to a circle). But you are not embedding that loop around the hub as an obstacle. Instead, you are placing it into an annular region that is open along one side, which removes the topological constraint.</p>
</blockquote>

<p>I love this one because it uses exactly the right words – <em>homeomorphic</em>, <em>annular region</em>, <em>topological constraint</em> – to defend a claim that is topologically wrong. It followed up with an O-ring analogy:</p>

<blockquote>
  <p>A useful analogy is a rubber O-ring: if you try to place it around a fixed object, topology matters. If you instead press it into a groove that’s open on one side, you can seat it without ever “looping” it around anything.</p>
</blockquote>

<p>Yeah exactly: open on one side. But the side isn’t open when you have an axle going through the frame. By this point ChatGPT was doubling down hard:</p>

<blockquote>
  <p>The axle isn’t something you ever have to “get the tube over.” That’s the key misconception. The axle sits in a completely different region of space.</p>
</blockquote>

<p>And then the closing argument:</p>

<blockquote>
  <p>So your instinct to remove the wheel is still valid – it’s just for mechanical access reasons, not because the axle makes the tube insertion impossible.</p>
</blockquote>

<p>No. It is <em>exactly</em> because of the axle?!</p>

<p>I brought it to the Local Bike Shop eventually.</p>

<p><a href="https://chatgpt.com/share/69d79490-bba4-8329-a27b-162f5186c3cd">Here</a> is the full conversation if you’re curious.</p>]]></content><author><name></name></author><category term="miscellaneous" /><category term="ai" /><summary type="html"><![CDATA[I have a big heavy cargo bike with an Enviolo hub in the back. Some time ago it got a flat, and because I’d never worked on that type of hub before, I asked ChatGPT for advice on how to change the tube. You know, just out of curiosity about what it actually knows.]]></summary></entry><entry><title type="html">Migrating from Mendeley Desktop to BibDesk</title><link href="https://slhck.info/software/2026/04/02/migrating-mendeley-desktop-bibdesk.html" rel="alternate" type="text/html" title="Migrating from Mendeley Desktop to BibDesk" /><published>2026-04-02T00:00:00+00:00</published><updated>2026-04-02T00:00:00+00:00</updated><id>https://slhck.info/software/2026/04/02/migrating-mendeley-desktop-bibdesk</id><content type="html" xml:base="https://slhck.info/software/2026/04/02/migrating-mendeley-desktop-bibdesk.html"><![CDATA[<p>Mendeley Desktop for macOS has been defunct for a while, but I still had my entire paper library in it. I wanted to move to <a href="https://bibdesk.sourceforge.io/">BibDesk</a>, an open-source bibliography manager for macOS that stores everything in plain <code class="language-plaintext highlighter-rouge">.bib</code> files. The catch: I needed to keep my Mendeley folder structure and link the PDFs properly.</p>

<p>Mendeley can export a <code class="language-plaintext highlighter-rouge">library.bib</code>, but that export is lossy. Foremost, it does not contain any actual clickable PDF links, so I would have to manually assign hundreds of PDFs. No thanks!</p>

<p>Here’s how I got the folder structure and PDF links out of Mendeley and into BibDesk, with a Python migration script and a small Swift helper for generating macOS file bookmarks.</p>

<h2 id="finding-the-folder-data">Finding the Folder Data</h2>

<p>Mendeley Desktop stores its main database as an encrypted SQLite file (SQLCipher) under <code class="language-plaintext highlighter-rouge">~/Library/Application Support/Mendeley Desktop/</code>. I couldn’t decrypt it because the key seems to be hidden somwhere in the application itself, or some online on-demand service. But there’s also a search index in the same directory tree that is <em>not</em> encrypted:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>~/Library/Application Support/Mendeley Desktop/www.mendeley.com/&lt;uuid&gt;/search-index.sqlite
</code></pre></div></div>

<p>This SQLite database has a <code class="language-plaintext highlighter-rouge">Documents</code> table with <code class="language-plaintext highlighter-rouge">fieldNames</code> and <code class="language-plaintext highlighter-rouge">fieldOffsets</code> columns, plus a <code class="language-plaintext highlighter-rouge">DocumentFullText_content</code> table with the actual indexed text. The field offsets let you get individual fields from the full-text content:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fields</span> <span class="o">=</span> <span class="n">field_names</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">" "</span><span class="p">)</span>    <span class="c1"># e.g. ["authors", "title", "citationkey", "tags", ...]
</span><span class="n">offsets</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">map</span><span class="p">(</span><span class="nb">int</span><span class="p">,</span> <span class="n">field_offsets</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">" "</span><span class="p">)))</span>

<span class="n">tags_idx</span> <span class="o">=</span> <span class="n">fields</span><span class="p">.</span><span class="n">index</span><span class="p">(</span><span class="s">"tags"</span><span class="p">)</span>
<span class="n">tags_text</span> <span class="o">=</span> <span class="n">content</span><span class="p">[</span><span class="n">offsets</span><span class="p">[</span><span class="n">tags_idx</span><span class="p">]:</span><span class="n">offsets</span><span class="p">[</span><span class="n">tags_idx</span> <span class="o">+</span> <span class="mi">1</span><span class="p">]]</span>
</code></pre></div></div>

<p>Mendeley stores folders as tags with a “Folder - “ prefix, so <code class="language-plaintext highlighter-rouge">Folder - HAS</code> and <code class="language-plaintext highlighter-rouge">Folder - Subjective Tests</code> appear in the tag text. I extracted the citation key and folder names for each document and merged that with whatever <code class="language-plaintext highlighter-rouge">mendeley-tags</code> and <code class="language-plaintext highlighter-rouge">keywords</code> fields were already in the <code class="language-plaintext highlighter-rouge">.bib</code> export.</p>

<h2 id="cleaning-the-bibtex">Cleaning the BibTeX</h2>

<p>The exported <code class="language-plaintext highlighter-rouge">.bib</code> needed several fixes:</p>

<ul>
  <li>Remove <code class="language-plaintext highlighter-rouge">mendeley-tags</code> fields (the data should live in BibDesk groups but I couldn’t get that to work yet).</li>
  <li>Strip “Folder - …” entries from <code class="language-plaintext highlighter-rouge">keywords</code>, keeping only real keywords.</li>
  <li>Remove the Mendeley <code class="language-plaintext highlighter-rouge">file</code> field entirely (replaced by BibDesk’s bookmark format).</li>
  <li>Remove the auto-generated Mendeley header.</li>
</ul>

<p>With the help of Claude Code I processed the file line by line, having Claude implement the logic to track brace depth to correctly handle multi-line field values and nested LaTeX braces (what a mess!).</p>

<h2 id="copying-and-linking-pdfs">Copying and Linking PDFs</h2>

<p>Here’s the best part, in my opinion, about this conversion. Mendeley keeps PDFs in <code class="language-plaintext highlighter-rouge">~/Documents/Mendeley Desktop/</code> with descriptive filenames like <code class="language-plaintext highlighter-rouge">Author et al. - Year - Title.pdf</code>. That’s the only feature I liked about Mendeley — the fact that it did proper renames and kept the folder in sync with the .bib file. I first copied all papers to a <code class="language-plaintext highlighter-rouge">~/Documents/Papers/</code> folder.</p>

<p>Matching the LaTeX-encoded paths from the <code class="language-plaintext highlighter-rouge">.bib</code> to actual files on disk required some care. Again, Claude Code to the rescue. macOS uses NFD Unicode normalization in filenames (decomposed form: <code class="language-plaintext highlighter-rouge">u</code> + combining diaeresis), while the LaTeX-decoded paths produce NFC (precomposed <code class="language-plaintext highlighter-rouge">ü</code>). Some filenames also had smart quotes where the decoded path had ASCII quotes. What a mess! (I am repeating myself but this is way harder than it should be.)</p>

<p>We (me and Claude) ended up normalizing both sides by stripping combining marks and replacing smart quotes, then comparing:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">_normalize_for_compare</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
    <span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">'</span><span class="se">\u2018</span><span class="s">'</span><span class="p">,</span> <span class="s">"'"</span><span class="p">).</span><span class="n">replace</span><span class="p">(</span><span class="s">'</span><span class="se">\u2019</span><span class="s">'</span><span class="p">,</span> <span class="s">"'"</span><span class="p">)</span>
    <span class="n">s</span> <span class="o">=</span> <span class="n">unicodedata</span><span class="p">.</span><span class="n">normalize</span><span class="p">(</span><span class="s">"NFD"</span><span class="p">,</span> <span class="n">s</span><span class="p">)</span>
    <span class="n">s</span> <span class="o">=</span> <span class="s">""</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">c</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">s</span> <span class="k">if</span> <span class="n">unicodedata</span><span class="p">.</span><span class="n">category</span><span class="p">(</span><span class="n">c</span><span class="p">)</span> <span class="o">!=</span> <span class="s">"Mn"</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">s</span><span class="p">.</span><span class="n">lower</span><span class="p">()</span>
</code></pre></div></div>

<p>This resolved all PDF links.</p>

<h2 id="generating-bibdesk-file-bookmarks">Generating BibDesk File Bookmarks</h2>

<p>BibDesk has its own way to represent file links in the .bib files. It stores them in a <code class="language-plaintext highlighter-rouge">bdsk-file-1</code> field containing a base64-encoded binary plist. The plist has two keys:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">relativePath</code> (relative to the <code class="language-plaintext highlighter-rouge">.bib</code> file)</li>
  <li><code class="language-plaintext highlighter-rouge">bookmark</code> (a macOS <code class="language-plaintext highlighter-rouge">NSURL</code> bookmark)</li>
</ul>

<p>The bookmark is not something you can easily construct in Python — it requires Apple’s <code class="language-plaintext highlighter-rouge">bookmarkData(options:)</code> API.
Claude wrote a small Swift helper (because I don’t know Swift all that well) that reads <code class="language-plaintext highlighter-rouge">relativePath\tabsolutePath</code> lines from standard input and outputs the base64-encoded plist on standard output:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">import</span> <span class="kt">Foundation</span>

<span class="k">while</span> <span class="k">let</span> <span class="nv">line</span> <span class="o">=</span> <span class="nf">readLine</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">parts</span> <span class="o">=</span> <span class="n">line</span><span class="o">.</span><span class="nf">split</span><span class="p">(</span><span class="nv">separator</span><span class="p">:</span> <span class="s">"</span><span class="se">\t</span><span class="s">"</span><span class="p">,</span> <span class="nv">maxSplits</span><span class="p">:</span> <span class="mi">1</span><span class="p">)</span><span class="o">.</span><span class="nf">map</span><span class="p">(</span><span class="kt">String</span><span class="o">.</span><span class="kd">init</span><span class="p">)</span>
    <span class="k">let</span> <span class="nv">relativePath</span> <span class="o">=</span> <span class="n">parts</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
    <span class="k">let</span> <span class="nv">url</span> <span class="o">=</span> <span class="kt">URL</span><span class="p">(</span><span class="nv">fileURLWithPath</span><span class="p">:</span> <span class="n">parts</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>

    <span class="k">let</span> <span class="nv">bookmarkData</span> <span class="o">=</span> <span class="k">try</span> <span class="n">url</span><span class="o">.</span><span class="nf">bookmarkData</span><span class="p">(</span>
        <span class="nv">options</span><span class="p">:</span> <span class="p">[],</span>
        <span class="nv">includingResourceValuesForKeys</span><span class="p">:</span> <span class="kc">nil</span><span class="p">,</span>
        <span class="nv">relativeTo</span><span class="p">:</span> <span class="kc">nil</span>
    <span class="p">)</span>

    <span class="k">let</span> <span class="nv">dict</span><span class="p">:</span> <span class="p">[</span><span class="kt">String</span><span class="p">:</span> <span class="kt">Any</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span>
        <span class="s">"relativePath"</span><span class="p">:</span> <span class="n">relativePath</span><span class="p">,</span>
        <span class="s">"bookmark"</span><span class="p">:</span> <span class="n">bookmarkData</span>
    <span class="p">]</span>

    <span class="k">let</span> <span class="nv">plistData</span> <span class="o">=</span> <span class="k">try</span> <span class="kt">PropertyListSerialization</span><span class="o">.</span><span class="nf">data</span><span class="p">(</span>
        <span class="nv">fromPropertyList</span><span class="p">:</span> <span class="n">dict</span><span class="p">,</span> <span class="nv">format</span><span class="p">:</span> <span class="o">.</span><span class="n">binary</span><span class="p">,</span> <span class="nv">options</span><span class="p">:</span> <span class="mi">0</span>
    <span class="p">)</span>

    <span class="nf">print</span><span class="p">(</span><span class="n">plistData</span><span class="o">.</span><span class="nf">base64EncodedString</span><span class="p">())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The Python migration script pipes <code class="language-plaintext highlighter-rouge">relativePath\tabsolutePath</code> lines into this helper and gets back base64 strings to embed in the <code class="language-plaintext highlighter-rouge">.bib</code>. Getting this format right was the hardest part. Claude didn’t know this format, of course, so I had to save a dummy file from BibDesk itself, decode its <code class="language-plaintext highlighter-rouge">bdsk-file-1</code> field, and then reverse-engineer the binary plist structure.</p>

<h2 id="the-migration-script">The Migration Script</h2>

<p>The <a href="https://github.com/slhck/mendeley-to-bibdesk-scripts">migration scripts</a> are on GitHub for your reference. It’s a bit rough and tailored to my specific setup, but it should give you a good starting point if you want or need to do something similar.</p>]]></content><author><name></name></author><category term="software" /><summary type="html"><![CDATA[Mendeley Desktop for macOS has been defunct for a while, but I still had my entire paper library in it. I wanted to move to BibDesk, an open-source bibliography manager for macOS that stores everything in plain .bib files. The catch: I needed to keep my Mendeley folder structure and link the PDFs properly.]]></summary></entry></feed>