<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Mirek Hovorka — Blog (EN)</title>
    <link>https://mirekhovorka.cz/en/blog/</link>
    <atom:link href="https://mirekhovorka.cz/en/blog/feed.xml" rel="self" type="application/rss+xml"/>
    <description>IT architect&#39;s knowledge base — architecture, AI, methodology.</description>
    <language>en-US</language>
    <lastBuildDate>Sun, 12 Jul 2026 16:17:21 GMT</lastBuildDate>
    
    <item>
      <title>One repo, two keyboards: syncing work between Windows and a Mac</title>
      <link>https://mirekhovorka.cz/en/blog/repo-sync-between-machines/</link>
      <guid isPermaLink="true">https://mirekhovorka.cz/en/blog/repo-sync-between-machines/</guid>
      <pubDate>Sun, 12 Jul 2026 00:00:00 GMT</pubDate>
      <description>One private repo as the source of truth for code and Claude Code context alike. An automatic pull at session start, a push reminder at the end — and the traps Windows and macOS set for each other.</description>
      <category>claude-code</category><category>git</category><category>workflow</category>
      <content:encoded><![CDATA[<p>During the day I sit at a Windows desktop, in the evening at a MacBook. Same project, two keyboards. Moving the code between them isn&#39;t the problem — git has done that since day one. The problem is moving the <strong>work in progress in my head</strong>: what was decided yesterday, what the agent I work with remembers, which hooks and scripts a session has available. This article describes the setup I use for that: one private repo as the single source of truth — for code and for context — plus the automation that means I never have to think about syncing at all.</p>
<p>(Day-to-day git work with Claude Code — commits, PRs, rescue situations — is covered by the <a href="/en/blog/git-github-cheatsheet-claude-code/">Git and GitHub cheat sheet for Claude Code</a>. This is the layer above it: multiple machines, one project.)</p>
<h2 id="the-foundation-the-remote-as-the-single-source-of-truth">The foundation: the remote as the single source of truth</h2>
<p>First, what <strong>not</strong> to do: don&#39;t copy folders over USB, and don&#39;t put the repo inside a folder synced by iCloud, Dropbox, or OneDrive. A sync client doesn&#39;t see <code>.git</code> atomically — it transfers files one by one, mid-write, and lock files and half-transferred objects can corrupt a repository in ways that only surface a week later. Git should be the one doing the syncing — a sync client just gets in its way.</p>
<p>The rest is a two-command discipline:</p>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> pull</span><span style="color:#79B8FF"> --ff-only</span><span style="color:#6A737D">   # before I start working</span></span>
<span class="line"><span style="color:#6A737D"># ... work, commits ...</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> push</span><span style="color:#6A737D">             # before I leave the machine</span></span></code></pre><p><code>--ff-only</code> means a pull never silently manufactures a merge commit. If the machines have genuinely diverged — commits on both sides — the command fails and I deal with it consciously, instead of discovering it later in a weird history.</p>
<p>Which yields the one hard rule: <strong>never walk away from a machine with unpushed commits.</strong> A commit can absolutely be unfinished — that&#39;s what branches and <code>wip:</code> prefixes are for. But an unpushed commit is a commit that doesn&#39;t exist on the other machine.</p>
<h2 id="the-second-layer-sync-the-context-not-just-the-code">The second layer: sync the context, not just the code</h2>
<p>If this were only about code, the article would have ended a paragraph ago. But with Claude Code, the repo carries more than code:</p>
<ul>
<li><strong><code>CLAUDE.md</code></strong> — instructions for the agent: conventions, project structure, workflow rules</li>
<li><strong><code>memory/</code></strong> — the agent&#39;s persistent memory, plain markdown files: decisions, section specs, feedback, work in progress</li>
<li><strong><code>.claude/settings.json</code></strong> — shared hooks (more on those in a moment)</li>
<li><strong><code>tools/</code></strong> — the scripts those hooks run</li>
</ul>
<p>When all of this is versioned together with the code, the second machine isn&#39;t a clean install. I sit down at the MacBook and the agent knows what we decided on Wednesday at the Windows desktop — because it reads it from the same files it wrote there.</p>
<p>One condition: a <strong>private repo.</strong> The agent&#39;s memory contains internal project notes, decisions and their reasoning — none of which belongs in a public repository.</p>
<p>And one exception: <strong>paths don&#39;t sync.</strong> <code>C:\Users\...</code> on Windows is not <code>/Users/...</code> on a Mac. Per-machine settings therefore live in a gitignored local config (<code>.claude/settings.local.json</code>), generated by an init script that runs once after cloning:</p>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">./claude-init.sh</span><span style="color:#6A737D">   # writes local paths, installs the per-machine bits</span></span></code></pre><p>The split is then simple: <strong>everything shared between machines is versioned</strong> (instructions, memory, hooks, scripts); <strong>everything per-machine is gitignored</strong> (local paths, builds, <code>node_modules</code>).</p>
<h2 id="automating-the-ritual">Automating the ritual</h2>
<p>The &quot;pull before work, push after&quot; ritual has one weakness: it relies on me. And after a weekend, I won&#39;t remember. Claude Code can run commands on session events — and two of those events map exactly onto the two ends of the ritual:</p>
<pre data-lang="json" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">{</span></span>
<span class="line"><span style="color:#79B8FF">  "hooks"</span><span style="color:#E1E4E8">: {</span></span>
<span class="line"><span style="color:#79B8FF">    "SessionStart"</span><span style="color:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#E1E4E8">      {</span></span>
<span class="line"><span style="color:#79B8FF">        "matcher"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"startup|resume"</span><span style="color:#E1E4E8">,</span></span>
<span class="line"><span style="color:#79B8FF">        "hooks"</span><span style="color:#E1E4E8">: [{ </span><span style="color:#79B8FF">"type"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"command"</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">"command"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"bash tools/sync-pull.sh"</span><span style="color:#E1E4E8"> }]</span></span>
<span class="line"><span style="color:#E1E4E8">      }</span></span>
<span class="line"><span style="color:#E1E4E8">    ],</span></span>
<span class="line"><span style="color:#79B8FF">    "Stop"</span><span style="color:#E1E4E8">: [</span></span>
<span class="line"><span style="color:#E1E4E8">      { </span><span style="color:#79B8FF">"hooks"</span><span style="color:#E1E4E8">: [{ </span><span style="color:#79B8FF">"type"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"command"</span><span style="color:#E1E4E8">, </span><span style="color:#79B8FF">"command"</span><span style="color:#E1E4E8">: </span><span style="color:#9ECBFF">"bash tools/unpushed-check.sh"</span><span style="color:#E1E4E8"> }] }</span></span>
<span class="line"><span style="color:#E1E4E8">    ]</span></span>
<span class="line"><span style="color:#E1E4E8">  }</span></span>
<span class="line"><span style="color:#E1E4E8">}</span></span></code></pre><p><strong>SessionStart</strong> pulls for me. The core of the script:</p>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> fetch</span><span style="color:#79B8FF"> --prune</span><span style="color:#9ECBFF"> origin</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> merge</span><span style="color:#79B8FF"> --ff-only</span><span style="color:#9ECBFF"> '@{u}'</span><span style="color:#6A737D">   # silent when there's nothing to do; shouts only on real divergence</span></span></code></pre><p><strong>Stop</strong> — the end of an agent response — counts unpushed commits and nags:</p>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#E1E4E8">n</span><span style="color:#F97583">=</span><span style="color:#E1E4E8">$(</span><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> log</span><span style="color:#79B8FF"> --oneline</span><span style="color:#9ECBFF"> @{u}..</span><span style="color:#F97583"> 2></span><span style="color:#9ECBFF">/dev/null</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> wc</span><span style="color:#79B8FF"> -l</span><span style="color:#F97583"> |</span><span style="color:#B392F0"> tr</span><span style="color:#79B8FF"> -d</span><span style="color:#9ECBFF"> ' '</span><span style="color:#E1E4E8">)</span></span>
<span class="line"><span style="color:#E1E4E8">[ </span><span style="color:#9ECBFF">"</span><span style="color:#E1E4E8">$n</span><span style="color:#9ECBFF">"</span><span style="color:#F97583"> !=</span><span style="color:#9ECBFF"> "0"</span><span style="color:#E1E4E8"> ] &#x26;&#x26; </span><span style="color:#79B8FF">echo</span><span style="color:#9ECBFF"> "📌 You have </span><span style="color:#E1E4E8">$n</span><span style="color:#9ECBFF"> unpushed commits — git push so the other machine sees them."</span></span></code></pre><p>The effect in practice: I open a session on the MacBook and the repo is current before I&#39;ve typed the first prompt. And since the <code>settings.json</code> with the hooks is itself part of the repo, every machine behaves this way automatically — the automation syncs through the same channel as everything else.</p>
<h2 id="mac-windows-traps">Mac ↔ Windows traps</h2>
<p>The things you don&#39;t learn from documentation — only from being bitten.</p>
<p><strong>Line endings.</strong> Windows git with <code>core.autocrlf=true</code> checks out text files with CRLF — including bash scripts. A trivial script survives that; a script with conditionals and functions dies on an invisible <code>\r</code> at the end of a line. And since a script committed from Windows will also run via a hook on the Mac, it&#39;s a trap with a delayed trigger. The once-and-for-all fix, in the repo:</p>
<pre data-lang="text" tabindex="0"><code># .gitattributes
*.sh text eol=lf</code></pre>
<p><strong>Git identity on a new machine.</strong> My first commit on a fresh MacBook ended with &quot;Author identity unknown&quot;. Nothing dramatic, but it belongs in the new-machine checklist, not in mid-task debugging:</p>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> config</span><span style="color:#79B8FF"> --global</span><span style="color:#9ECBFF"> user.name</span><span style="color:#9ECBFF"> "Firstname Lastname"</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> config</span><span style="color:#79B8FF"> --global</span><span style="color:#9ECBFF"> user.email</span><span style="color:#9ECBFF"> "mail@example.com"</span></span></code></pre><p><strong>A branch deleted on the remote.</strong> A feature branch merged and deleted from the other machine: on the first machine I&#39;m still standing on it, and <code>git pull</code> fails with &quot;no such ref was fetched&quot;. It looks like a conflict, but it&#39;s just a dead reference:</p>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> fetch</span><span style="color:#79B8FF"> --prune</span><span style="color:#6A737D">            # clean up references to deleted remote branches</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> checkout</span><span style="color:#9ECBFF"> main</span><span style="color:#E1E4E8"> &#x26;&#x26; </span><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> pull</span><span style="color:#79B8FF"> --ff-only</span></span></code></pre><p>That&#39;s why the sync script above distinguishes three states: a live upstream (pull it), a branch fully contained in <code>origin/main</code> (say &quot;switch to main&quot;), and a genuine divergence (the only case where it should shout).</p>
<p><strong>Tools missing on the other platform.</strong> <code>lsof</code> is absent from git-bash on Windows; <code>netstat</code> behaves differently on a Mac. Scripts shared across platforms need a guard — <code>command -v lsof &gt;/dev/null || exit 0</code> — so that a missing tool means silent degradation, not a broken hook.</p>
<h2 id="checklist-a-new-machine-in-ten-minutes">Checklist: a new machine in ten minutes</h2>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> repo</span><span style="color:#9ECBFF"> clone</span><span style="color:#F97583"> &#x3C;</span><span style="color:#9ECBFF">use</span><span style="color:#E1E4E8">r</span><span style="color:#F97583">></span><span style="color:#9ECBFF">/</span><span style="color:#F97583">&#x3C;</span><span style="color:#9ECBFF">rep</span><span style="color:#E1E4E8">o</span><span style="color:#F97583">></span><span style="color:#6A737D">          # 1. outside iCloud/OneDrive/Dropbox folders!</span></span>
<span class="line"><span style="color:#79B8FF">cd</span><span style="color:#F97583"> &#x3C;</span><span style="color:#9ECBFF">rep</span><span style="color:#E1E4E8">o</span><span style="color:#F97583">></span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> config</span><span style="color:#79B8FF"> --global</span><span style="color:#9ECBFF"> user.name</span><span style="color:#9ECBFF"> "..."</span><span style="color:#6A737D">  # 2. identity, if the machine is fresh</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> config</span><span style="color:#79B8FF"> --global</span><span style="color:#9ECBFF"> user.email</span><span style="color:#9ECBFF"> "..."</span></span>
<span class="line"><span style="color:#B392F0">./claude-init.sh</span><span style="color:#6A737D">                     # 3. local config, per-machine installs</span></span>
<span class="line"><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> install</span><span style="color:#6A737D">                          # 4. project dependencies</span></span></code></pre><p>And a fifth step worth not skipping: <strong>a verification round-trip.</strong> Commit from the new machine, push, pull on the old one — and the same in reverse. Once both directions pass, hooks included, the machine is a full citizen. Mine caught both the missing identity and the CRLF in scripts.</p>
<h2 id="conclusion">Conclusion</h2>
<p>I&#39;m not syncing files, I&#39;m syncing a <strong>workplace</strong>. The measure of success is simple — I sit down at the other machine, open a session, and the first prompt can be &quot;let&#39;s continue&quot;. No explaining where we left off. Git carries the code, the repo carries the context, and the hooks carry the discipline I could never rely on myself to keep.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Cheat sheet: Git and GitHub with Claude Code</title>
      <link>https://mirekhovorka.cz/en/blog/git-github-cheatsheet-claude-code/</link>
      <guid isPermaLink="true">https://mirekhovorka.cz/en/blog/git-github-cheatsheet-claude-code/</guid>
      <pubDate>Sun, 05 Jul 2026 00:00:00 GMT</pubDate>
      <description>Concrete prompts for git and GitHub with Claude Code — and the commands that actually run under the hood. Commits, PRs, issues, CI, and rescuing a broken repo.</description>
      <category>claude-code</category><category>git</category><category>github</category><category>cheatsheet</category>
      <content:encoded><![CDATA[<p>Claude Code can drive practically your entire git and GitHub workflow — commits, branches, pull requests, reviews, and rescuing a broken repo. This cheat sheet sums up what I hand over without worry after months of daily use, and for every situation it shows both: the prompt I type, and the commands that actually run under the hood. Because delegating doesn&#39;t mean you stop understanding what&#39;s going on.</p>
<h2 id="commits-and-messages">Commits and messages</h2>
<p>The thing I ask for most often. A plain &quot;commit this&quot; is enough, and under the hood this runs:</p>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> status</span><span style="color:#6A737D">                 # what changed, what's untracked</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> diff</span><span style="color:#6A737D">                   # what actually changed</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> add</span><span style="color:#9ECBFF"> build.mjs</span><span style="color:#9ECBFF"> serve.mjs</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> commit</span><span style="color:#79B8FF"> -m</span><span style="color:#9ECBFF"> "fix: dev server did not serve webp from subdirectories"</span></span></code></pre><p>The message comes from what the diff actually says — not from what I think I did. What has worked for me:</p>
<ul>
<li><strong>Conventions belong in CLAUDE.md.</strong> Commit message language, prefixes like <code>feat:</code>/<code>fix:</code>, formatting — write it once into the repository instructions and Claude follows it without being reminded.</li>
<li><strong>Selective commits work reliably.</strong> When I have several unrelated things in progress:</li>
</ul>
<pre data-lang="text" tabindex="0"><code>commit only the changes in build.mjs, leave the rest alone</code></pre>
<p>Under the hood that&#39;s <code>git add build.mjs</code> and a commit — no <code>git add -A</code> that would sweep up the unfinished rest.</p>
<ul>
<li><strong>Let it split unrelated changes.</strong> &quot;Split this into logical commits&quot; — Claude walks the diff, groups files by what belongs together, and stages group by group (falling back to <code>git add -p</code> for individual hunks if needed).</li>
</ul>
<p>What I watch: <strong>push is always an explicit step.</strong> I instruct Claude to never push on its own — a commit is local and reversible, a push is publication.</p>
<h2 id="reverse-gear-amend-restore-stash">Reverse gear: amend, restore, stash</h2>
<p>The small fixes I used to look up the exact command for on Stack Overflow:</p>
<ul>
<li><strong>&quot;Add this to the last commit&quot;:</strong></li>
</ul>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> add</span><span style="color:#9ECBFF"> build.mjs</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> commit</span><span style="color:#79B8FF"> --amend</span><span style="color:#79B8FF"> --no-edit</span></span></code></pre><p>Amend rewrites history — it&#39;s only safe until the commit is pushed. Claude points that out on its own, but it&#39;s worth knowing.</p>
<ul>
<li><strong>&quot;Throw away the changes in serve.mjs&quot;:</strong></li>
</ul>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> restore</span><span style="color:#9ECBFF"> serve.mjs</span><span style="color:#6A737D">            # discard unstaged changes in the file</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> restore</span><span style="color:#79B8FF"> --staged</span><span style="color:#9ECBFF"> serve.mjs</span><span style="color:#6A737D">   # just unstage, keep the changes</span></span></code></pre><ul>
<li><strong>&quot;Stash my work in progress, I need a hotfix&quot;:</strong></li>
</ul>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> stash</span><span style="color:#9ECBFF"> push</span><span style="color:#79B8FF"> -m</span><span style="color:#9ECBFF"> "wip: new footer"</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> stash</span><span style="color:#9ECBFF"> list</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> stash</span><span style="color:#9ECBFF"> pop</span></span></code></pre><p>One caveat: when <code>git stash pop</code> hits a conflict, it applies the changes but keeps the stash — on purpose, so you can&#39;t lose work. Once the conflict is resolved, clean it up yourself: <code>git stash drop</code>.</p>
<p>Since I started using worktrees, I stash less — see the next section.</p>
<h2 id="branches-and-worktrees">Branches and worktrees</h2>
<p>Creating a branch is trivial — &quot;create a branch for the footer rework&quot;:</p>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> switch</span><span style="color:#79B8FF"> -c</span><span style="color:#9ECBFF"> feat/footer</span></span></code></pre><p>The more interesting tool is <strong>worktrees</strong>: a second working copy of the repository on a different branch, in a sibling directory.</p>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> worktree</span><span style="color:#9ECBFF"> add</span><span style="color:#9ECBFF"> ../project-fix</span><span style="color:#9ECBFF"> fix/commission-rounding</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> worktree</span><span style="color:#9ECBFF"> list</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> worktree</span><span style="color:#9ECBFF"> remove</span><span style="color:#9ECBFF"> ../project-fix</span><span style="color:#6A737D">   # once it's done</span></span></code></pre><p>Why this matters with an AI tool: I can run two Claude Code sessions side by side — one building a feature, the other fixing a bug — and they never step on each other&#39;s files, because each works in its own directory. No branch switching, no <code>git stash</code> acrobatics.</p>
<p>Two things worktrees won&#39;t tell a newcomer: the same branch can&#39;t be checked out in two worktrees at once (git refuses — rightly so). And <code>node_modules</code> don&#39;t clone: every copy lives its own life, so <code>git worktree add</code> is followed by another <code>npm install</code>.</p>
<p>(Worktrees solve parallel work on one machine. Keeping one project — Claude Code context included — in sync across two computers is covered by <a href="/en/blog/repo-sync-between-machines/">One repo, two keyboards</a>.)</p>
<h2 id="pull-requests-via-the-gh-cli">Pull requests via the gh CLI</h2>
<p>Claude Code talks to GitHub through the official <code>gh</code> CLI. One-time setup:</p>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">brew</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> gh</span><span style="color:#6A737D">            # macOS</span></span>
<span class="line"><span style="color:#B392F0">winget</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> GitHub.cli</span><span style="color:#6A737D">  # Windows</span></span>
<span class="line"><span style="color:#B392F0">sudo</span><span style="color:#9ECBFF"> apt</span><span style="color:#9ECBFF"> install</span><span style="color:#9ECBFF"> gh</span><span style="color:#6A737D">        # Debian/Ubuntu</span></span>
<span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> auth</span><span style="color:#9ECBFF"> login</span><span style="color:#6A737D">              # one-time sign-in</span></span></code></pre><p>After that:</p>
<pre data-lang="text" tabindex="0"><code>create a PR against main, keep the description short</code></pre>
<p>Under the hood:</p>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> log</span><span style="color:#9ECBFF"> main..HEAD</span><span style="color:#79B8FF"> --oneline</span><span style="color:#6A737D">   # which commits the branch brings</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> diff</span><span style="color:#9ECBFF"> main...HEAD</span><span style="color:#6A737D">           # the branch's full diff against main</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> push</span><span style="color:#79B8FF"> -u</span><span style="color:#9ECBFF"> origin</span><span style="color:#9ECBFF"> feat/footer</span></span>
<span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> pr</span><span style="color:#9ECBFF"> create</span><span style="color:#79B8FF"> --base</span><span style="color:#9ECBFF"> main</span><span style="color:#79B8FF"> --title</span><span style="color:#9ECBFF"> "..."</span><span style="color:#79B8FF"> --body</span><span style="color:#9ECBFF"> "..."</span></span></code></pre><p>The PR description is generated from the <strong>actual diff of the whole branch against main</strong>, not just the last commit — so it stays accurate even after ten fix-up commits. The other direction works too:</p>
<pre data-lang="text" tabindex="0"><code>fetch the review comments from PR #42 and address them</code></pre>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> pr</span><span style="color:#9ECBFF"> view</span><span style="color:#79B8FF"> 42</span><span style="color:#79B8FF"> --comments</span><span style="color:#6A737D">                        # the discussion under the PR</span></span>
<span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> api</span><span style="color:#9ECBFF"> repos/{owner}/{repo}/pulls/42/comments</span><span style="color:#6A737D">   # inline comments in the code</span></span></code></pre><p>Claude pulls the comments, goes through them one by one and tells you what it did with each. For comments it disagrees with, I want it to say so — not to comply blindly.</p>
<p>Reviewing someone else&#39;s PR — &quot;check out PR #42 and walk me through it&quot;:</p>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> pr</span><span style="color:#9ECBFF"> checkout</span><span style="color:#79B8FF"> 42</span><span style="color:#6A737D">   # the PR's branch into your local copy</span></span>
<span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> pr</span><span style="color:#9ECBFF"> diff</span><span style="color:#79B8FF"> 42</span><span style="color:#6A737D">       # a quick look without checking out</span></span></code></pre><p>And the merge, once it&#39;s done — &quot;merge PR #42 with a squash&quot;:</p>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> pr</span><span style="color:#9ECBFF"> merge</span><span style="color:#79B8FF"> 42</span><span style="color:#79B8FF"> --squash</span><span style="color:#79B8FF"> --delete-branch</span></span></code></pre><p>I treat merge the same as push: it&#39;s publication, so I approve it explicitly.</p>
<h2 id="issues-and-ci">Issues and CI</h2>
<p>The section I left out of the first version of this cheat sheet — even though it&#39;s half of what <code>gh</code> does for me daily.</p>
<ul>
<li><strong>&quot;List the open issues labeled bug&quot;:</strong></li>
</ul>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> issue</span><span style="color:#9ECBFF"> list</span><span style="color:#79B8FF"> --label</span><span style="color:#9ECBFF"> bug</span><span style="color:#79B8FF"> --state</span><span style="color:#9ECBFF"> open</span></span>
<span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> issue</span><span style="color:#9ECBFF"> view</span><span style="color:#79B8FF"> 12</span></span></code></pre><ul>
<li><strong>&quot;File an issue for this&quot;</strong> — mid-session, when I hit a bug I don&#39;t want to deal with right now:</li>
</ul>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> issue</span><span style="color:#9ECBFF"> create</span><span style="color:#79B8FF"> --title</span><span style="color:#9ECBFF"> "..."</span><span style="color:#79B8FF"> --body</span><span style="color:#9ECBFF"> "..."</span><span style="color:#79B8FF"> --label</span><span style="color:#9ECBFF"> bug</span></span></code></pre><p>Claude writes the description from the session context while it&#39;s fresh — so it&#39;s concrete: what happened, where, how to reproduce it. An issue filed this way is more useful than the one I&#39;d write from memory three days later.</p>
<ul>
<li><strong>&quot;Why did CI fail on this branch?&quot;</strong> — possibly the biggest time-saver in this whole cheat sheet:</li>
</ul>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> pr</span><span style="color:#9ECBFF"> checks</span><span style="color:#6A737D">                        # check status on the PR</span></span>
<span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> run</span><span style="color:#9ECBFF"> list</span><span style="color:#79B8FF"> --branch</span><span style="color:#9ECBFF"> feat/footer</span><span style="color:#6A737D">    # recent workflow runs</span></span>
<span class="line"><span style="color:#B392F0">gh</span><span style="color:#9ECBFF"> run</span><span style="color:#9ECBFF"> view</span><span style="color:#79B8FF"> 1234567890</span><span style="color:#79B8FF"> --log-failed</span><span style="color:#6A737D"> # the log of only what failed</span></span></code></pre><p>Claude pulls the failure log, finds the cause and proposes a fix right away. No clicking through the web UI, no scrolling a mile-long log.</p>
<h2 id="reviewing-your-own-diff-before-the-pr">Reviewing your own diff before the PR</h2>
<p>Before I open a PR, I have the diff reviewed locally first — it&#39;s cheaper than burning a colleague&#39;s time:</p>
<pre data-lang="text" tabindex="0"><code>review this branch's diff against main — look for bugs and risks, not style</code></pre>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> diff</span><span style="color:#9ECBFF"> main...HEAD</span></span></code></pre><p>The three dots are not a typo: diffing against the common ancestor (merge-base) shows only what my branch introduced — not the changes that landed on main in the meantime.</p>
<p>The more specific the brief, the better it works: &quot;focus on error handling&quot;, &quot;look for race conditions&quot;, &quot;check that no personal data gets logged&quot;. A generic &quot;take a look&quot; returns generic answers.</p>
<h2 id="rescue-situations">Rescue situations</h2>
<p>This is where you find out whether AI helps you or hurts you.</p>
<ul>
<li><strong>Merge/rebase conflicts</strong> — Claude resolves them, but I want to see the reasoning:</li>
</ul>
<pre data-lang="text" tabindex="0"><code>resolve the conflicts; for each file, tell me which side you took and why</code></pre>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> status</span><span style="color:#6A737D">                      # which files are in conflict</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> checkout</span><span style="color:#79B8FF"> --ours</span><span style="color:#9ECBFF"> file.js</span><span style="color:#6A737D">     # take our whole side</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> checkout</span><span style="color:#79B8FF"> --theirs</span><span style="color:#9ECBFF"> file.js</span><span style="color:#6A737D">   # take their whole side</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> rebase</span><span style="color:#79B8FF"> --abort</span><span style="color:#6A737D">              # emergency brake: back to before the rebase</span></span></code></pre><p>Most of the time, though, Claude edits the conflict blocks by hand — more precise than taking whole files from one side. (And mind the classic trap: during a rebase, the meaning of <code>--ours</code>/<code>--theirs</code> flips.)</p>
<ul>
<li><strong>Revert</strong> — a safe operation, it creates a new commit and rewrites nothing:</li>
</ul>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> revert</span><span style="color:#9ECBFF"> abc123</span></span></code></pre><ul>
<li><strong>Bisect</strong> — hunting the commit that broke something is finally painless with AI:</li>
</ul>
<pre data-lang="text" tabindex="0"><code>bisect to find the commit that broke the build; test with node build.mjs</code></pre>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> bisect</span><span style="color:#9ECBFF"> start</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> bisect</span><span style="color:#9ECBFF"> bad</span><span style="color:#6A737D">                  # HEAD is broken</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> bisect</span><span style="color:#9ECBFF"> good</span><span style="color:#9ECBFF"> v1.4</span><span style="color:#6A737D">            # it still worked here</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> bisect</span><span style="color:#9ECBFF"> run</span><span style="color:#9ECBFF"> node</span><span style="color:#9ECBFF"> build.mjs</span><span style="color:#6A737D">   # automated test at every step</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> bisect</span><span style="color:#9ECBFF"> reset</span></span></code></pre><p>With <code>git bisect run</code>, the whole binary search runs without a single manual step — Claude just reads off the result and points at the culprit.</p>
<ul>
<li><strong>Reflog</strong> — the last resort for &quot;there was a commit here yesterday and now it&#39;s gone&quot;:</li>
</ul>
<pre data-lang="bash" class="shiki github-dark" style="background-color:rgba(255,255,255,0.04);color:#e1e4e8" tabindex="0"><code><span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> reflog</span><span style="color:#6A737D">                    # a journal of everywhere HEAD has ever been</span></span>
<span class="line"><span style="color:#B392F0">git</span><span style="color:#9ECBFF"> reset</span><span style="color:#79B8FF"> --hard</span><span style="color:#9ECBFF"> HEAD@{</span><span style="color:#79B8FF">2</span><span style="color:#9ECBFF">}</span><span style="color:#6A737D">     # return to a specific state</span></span></code></pre><p>Reading the reflog is safe. The <code>reset --hard</code> at the end is not — which brings us to the point: <strong><code>git reset --hard</code>, force push, and branch deletion I do by hand</strong>, or at minimum I want Claude to explicitly ask before running them. Claude Code&#39;s permission settings support exactly that: I keep destructive commands on manual approval.</p>
<h2 id="quick-reference">Quick reference</h2>
<div class="table-scroll" tabindex="0"><table>
<thead>
<tr>
<th>Situation</th>
<th>Prompt</th>
<th>Key command</th>
</tr>
</thead>
<tbody><tr>
<td>Commit everything</td>
<td>&quot;commit this&quot;</td>
<td><code>git commit -m</code></td>
</tr>
<tr>
<td>Selective commit</td>
<td>&quot;commit only the changes in X&quot;</td>
<td><code>git add X &amp;&amp; git commit</code></td>
</tr>
<tr>
<td>Fix the last commit</td>
<td>&quot;add this to the last commit&quot;</td>
<td><code>git commit --amend --no-edit</code></td>
</tr>
<tr>
<td>Discard changes</td>
<td>&quot;throw away the changes in X&quot;</td>
<td><code>git restore X</code></td>
</tr>
<tr>
<td>Park work in progress</td>
<td>&quot;stash my work in progress&quot;</td>
<td><code>git stash push -m &quot;...&quot;</code></td>
</tr>
<tr>
<td>New branch</td>
<td>&quot;create a branch for …&quot;</td>
<td><code>git switch -c</code></td>
</tr>
<tr>
<td>Parallel work</td>
<td>two sessions side by side</td>
<td><code>git worktree add</code></td>
</tr>
<tr>
<td>Pull request</td>
<td>&quot;create a PR against main&quot;</td>
<td><code>gh pr create</code></td>
</tr>
<tr>
<td>Addressing review</td>
<td>&quot;fetch comments from PR #N&quot;</td>
<td><code>gh api …/pulls/N/comments</code></td>
</tr>
<tr>
<td>Someone else&#39;s PR</td>
<td>&quot;check out PR #N and walk me through it&quot;</td>
<td><code>gh pr checkout N</code></td>
</tr>
<tr>
<td>Issues</td>
<td>&quot;list the open bugs&quot;</td>
<td><code>gh issue list --label bug</code></td>
</tr>
<tr>
<td>CI failure</td>
<td>&quot;why did CI fail?&quot;</td>
<td><code>gh run view --log-failed</code></td>
</tr>
<tr>
<td>Pre-PR review</td>
<td>&quot;review this branch&#39;s diff against main&quot;</td>
<td><code>git diff main...HEAD</code></td>
</tr>
<tr>
<td>Conflicts</td>
<td>&quot;resolve conflicts and explain each&quot;</td>
<td><code>git checkout --ours/--theirs</code></td>
</tr>
<tr>
<td>Undo a commit</td>
<td>&quot;revert commit X&quot;</td>
<td><code>git revert X</code></td>
</tr>
<tr>
<td>Regression</td>
<td>&quot;bisect to find what broke …&quot;</td>
<td><code>git bisect run</code></td>
</tr>
<tr>
<td>Lost commit</td>
<td>&quot;find the lost commit&quot;</td>
<td><code>git reflog</code></td>
</tr>
</tbody></table></div>
<h2 id="conclusion">Conclusion</h2>
<p>I don&#39;t understand git any less because of Claude — quite the opposite: without that understanding, I wouldn&#39;t catch it when it gets something wrong. I just don&#39;t type the commands I can dictate anymore, and the energy saved goes into checking the results. Git stays the same, it&#39;s just someone else doing the typing.</p>
]]></content:encoded>
    </item>
    <item>
      <title>AI agents in the enterprise: from PoC to production</title>
      <link>https://mirekhovorka.cz/en/blog/ai-agents-in-the-enterprise/</link>
      <guid isPermaLink="true">https://mirekhovorka.cz/en/blog/ai-agents-in-the-enterprise/</guid>
      <pubDate>Sun, 28 Jun 2026 00:00:00 GMT</pubDate>
      <description>You can build an agent demo in a week. What stands between the demo and production in a regulated environment has surprisingly little to do with technology.</description>
      <category>ai-agents</category><category>enterprise</category><category>governance</category>
      <content:encoded><![CDATA[<p>An AI agent that reads documents, calls systems and handles tasks can be put together in a few days. I know because I build them — and I also work as an architect at an insurance company, so I see both sides: how fast an impressive demo comes together, and how much still stands between it and production. And most of it has little to do with technology.</p>
<h2 id="why-a-poc-looks-finished-in-a-week">Why a PoC looks finished in a week</h2>
<p>With a proof of concept, one thing is easy to forget: it performs on scenarios its author picked. Ten showcase cases go smoothly, the audience is impressed, and everyone feels all that&#39;s left is &quot;just deploying it&quot;.</p>
<p>But a PoC usually runs on curated data, with no real permissions, no audit trail, and a human sitting next to it quietly fixing the small slips. You&#39;ll have none of that in production. A PoC answers the question &quot;can this work?&quot; — production asks what happens when it stops working.</p>
<p>Klarna showed what that difference looks like in practice. In February 2024 it <a href="https://www.klarna.com/international/press/klarna-ai-assistant-handles-two-thirds-of-customer-service-chats-in-its-first-month/" class="blog-extlink" target="_blank" rel="noopener noreferrer">announced</a> that its AI assistant had handled two-thirds of customer-support chats in its first month — &quot;the work of 700 people&quot;. A year later the CEO <a href="https://www.customerexperiencedive.com/news/klarna-reinvests-human-talent-customer-service-AI-chatbot/747586/" class="blog-extlink" target="_blank" rel="noopener noreferrer">admitted</a> the AI-driven cuts had gone too far, quality had dropped, and the company was hiring people again. The press release measured how many conversations the agent handled. How well — that only came up a year later.</p>
<h2 id="what-the-enterprise-adds">What the enterprise adds</h2>
<p>In a regulated environment, requirements show up that the demo never had to deal with:</p>
<ul>
<li><strong>Audit trail.</strong> Every decision the agent makes has to be traceable: what it saw, why and how it decided, what it did. &quot;The model judged it that way&quot; won&#39;t satisfy an auditor or a regulator.</li>
<li><strong>Accountability.</strong> When the agent makes a mistake, it has to be clear in advance who&#39;s responsible and how it gets fixed. That&#39;s a process question — and you want the answer before you let the agent into production, not after the first incident. The courts already have an opinion on this: in February 2024 a Canadian tribunal <a href="https://www.canlii.org/en/bc/bccrt/doc/2024/2024bccrt149/2024bccrt149.html" class="blog-extlink" target="_blank" rel="noopener noreferrer">made Air Canada pay</a> for a discount its chatbot had invented. The airline argued the chatbot was &quot;a separate legal entity that is responsible for its own actions&quot; — the tribunal called that a remarkable submission and upheld the claim. It was 812 Canadian dollars, but the case has been cited ever since as a precedent: the company answers for its chatbot&#39;s output.</li>
<li><strong>Data governance.</strong> In a PoC it&#39;s convenient to let the agent see everything. In production, data access has to match purpose — a claims-handling agent has no business reading payroll data, even if it would improve its answers.</li>
<li><strong>Security.</strong> An agent is a new attack surface: inputs it trusts can be forged (prompt injection is just the best-known case), and the more actions it&#39;s allowed to take, the more damage a forged input can do. For a long time this was illustrated mostly by curiosities, like the dealership chatbot <a href="https://incidentdatabase.ai/cite/622/" class="blog-extlink" target="_blank" rel="noopener noreferrer">promising a Chevrolet Tahoe for one dollar</a>. Then in June 2025 came <a href="https://msrc.microsoft.com/update-guide/vulnerability/CVE-2025-32711" class="blog-extlink" target="_blank" rel="noopener noreferrer">EchoLeak</a>: the first zero-click prompt injection against a production enterprise agent (Microsoft 365 Copilot, CVSS 9.3). One ordinary-looking e-mail in the inbox was enough — the agent pulled it into context on its own, and sensitive data leaked without the user clicking anything. Luckily, security researchers found it first.</li>
</ul>
<p>And it&#39;s not just internal caution. Under the <a href="https://artificialintelligenceact.eu/annex/3/" class="blog-extlink" target="_blank" rel="noopener noreferrer">EU AI Act</a>, AI used for risk assessment and pricing in life and health insurance lands straight in the high-risk category, with obligations from August 2026 (Brussels is debating a delay, but the direction stands). So a good part of the bullets above will sooner or later simply be the law.</p>
<h2 id="where-the-agent-may-act-on-its-own">Where the agent may act on its own</h2>
<p>The most important design decision of the whole solution: <strong>which actions the agent may take on its own, and where a human has to be in the loop.</strong> I use a simple grid for this — two axes, reversibility and impact:</p>
<ul>
<li><strong>Reversible + low impact</strong> → the agent acts alone (drafting a reply, categorising, preparing materials).</li>
<li><strong>Reversible + high impact</strong> → the agent acts, humans check samples and metrics.</li>
<li><strong>Irreversible + low impact</strong> → the agent prepares, a human confirms with one click.</li>
<li><strong>Irreversible + high impact</strong> → the agent only recommends; a human decides (paying out a claim, terminating a policy).</li>
</ul>
<p>And that boundary isn&#39;t fixed. It pays off to start conservative and give the agent more authority as the production numbers come in — that&#39;s cheaper than starting bold and locking everything down again after the first incident.</p>
<h2 id="the-agent-as-a-new-participant-in-the-process">The agent as a new participant in the process</h2>
<p>Agents often get treated as just another system to &quot;integrate&quot;. What works better for me is the picture of a new colleague: fast, never bored, but occasionally reading the assignment their own way.</p>
<p>That picture leads to the practical questions you&#39;d ask about any new colleague: who assigns their work, and in what form? Who takes their output? How do they know they&#39;re out of their depth, and who do they tell? And what happens to the process when they just &quot;don&#39;t show up for work&quot;? A process with no answers to these questions isn&#39;t ready for an agent — no matter how good the agent is.</p>
<h2 id="what-to-measure-so-you-can-say-it-works">What to measure so you can say &quot;it works&quot;</h2>
<p>Without metrics, the debate about an agent runs on anecdotes: one person remembers a brilliant answer, another remembers a disaster. This is the minimum set I want from day one:</p>
<ol>
<li><strong>End-to-end success rate</strong> — not &quot;the agent replied&quot; but &quot;the case was handled correctly&quot;, measured by the same yardstick as humans. That&#39;s exactly where the <a href="https://www.cnbc.com/2024/06/17/mcdonalds-to-end-ibm-ai-drive-thru-test.html" class="blog-extlink" target="_blank" rel="noopener noreferrer">McDonald&#39;s drive-thru AI</a> ended: two years of testing across a hundred-plus restaurants, yet order accuracy sat around 80% by franchisees&#39; accounts, against a target of 95. The viral video with 260 nuggets in one order was just the most visible case of it.</li>
<li><strong>Escalation rate</strong> — how often the agent hands work to a human. A low number isn&#39;t automatically good news: an agent that never escalates probably can&#39;t tell when it&#39;s out of its depth.</li>
<li><strong>Cost per handled case</strong> — including the cost of checking and fixing the agent&#39;s work. Leave those out and the number looks better than reality.</li>
<li><strong>Correction trend</strong> — how many of the agent&#39;s outputs people had to redo, and how that develops. This is the number that moves the autonomy boundary from the previous section.</li>
</ol>
<h2 id="conclusion">Conclusion</h2>
<p>Deploying an AI agent in the enterprise mostly means deciding how much trust you&#39;ll give it: what it may do on its own, who answers for it, and how you measure that it works. The technology is more ready than most organisations think. What&#39;s usually missing are the processes, responsibilities and metrics.</p>
]]></content:encoded>
    </item>
    <item>
      <title>When APIs, when events</title>
      <link>https://mirekhovorka.cz/en/blog/when-apis-when-events/</link>
      <guid isPermaLink="true">https://mirekhovorka.cz/en/blog/when-apis-when-events/</guid>
      <pubDate>Sun, 21 Jun 2026 00:00:00 GMT</pubDate>
      <description>Synchronous call or an event? The decision framework I use instead of debates about what&#39;s more modern.</description>
      <category>integration</category><category>api</category><category>event-driven</category>
      <content:encoded><![CDATA[<p>I&#39;ve seen the &quot;APIs versus events&quot; debate in just about every team — and it rarely goes well. People argue from what&#39;s modern, what we&#39;ve already bought, or how it&#39;s always been done. I use a few questions about the process itself instead. They usually settle it on their own; this note sums them up, with examples.</p>
<h2 id="who-needs-the-answer-and-when">Who needs the answer, and when</h2>
<p>The question of which architecture is &quot;better&quot; can&#39;t be settled. It&#39;s more useful to ask who needs the answer, and when: when the caller can&#39;t continue without the result, you need a synchronous call; when the result matters &quot;whenever it&#39;s ready&quot;, or matters to several consumers, you&#39;re in event territory. Most disputes end the moment people stop talking about technology and start talking about the process.</p>
<h2 id="when-a-synchronous-api">When a synchronous API</h2>
<p>A synchronous call is the right choice when:</p>
<ul>
<li><strong>The caller can&#39;t proceed without the answer.</strong> Checking a limit before confirming a deal, computing a price in a quote, verifying permissions. The waiting simply belongs to the operation.</li>
<li><strong>You want errors handled right away, in context.</strong> A synchronous call returns the error to whoever asked, the moment they asked — and they can react right away: fix the input, try again, tell the user.</li>
<li><strong>The flow should be readable.</strong> A chain of synchronous calls reads top to bottom and you see the order of steps at a glance. For processes where order and clarity matter, that&#39;s worth a lot.</li>
</ul>
<p>The price you pay: <strong>coupled availability.</strong> Every link of a synchronous chain inherits everyone else&#39;s outages. Ten services at 99.9% availability in a row come out to 0.999¹⁰ ≈ 99.0% — nine hours of downtime a year just became eighty-seven. And the slowest link sets the pace for everyone.</p>
<p>And then there&#39;s the <strong>retry storm</strong>: a wave of repeated calls blows a small outage up into a big one. That&#39;s exactly what happened in the <a href="https://aws.amazon.com/message/5467D2/" class="blog-extlink" target="_blank" rel="noopener noreferrer">DynamoDB outage of September 2015</a> — the network recovered in moments, but the wave of synchronous retries kept the metadata service overloaded for almost five hours, and the cascade took SQS, CloudWatch and autoscaling with it. So it wasn&#39;t the network that decided how long the outage lasted — it was the architecture around it. And in <a href="https://aws.amazon.com/message/101925/" class="blog-extlink" target="_blank" rel="noopener noreferrer">October 2025</a> the same thing happened again in the same region, this time for fourteen hours.</p>
<h2 id="when-events">When events</h2>
<p>An event is the right choice when:</p>
<ul>
<li><strong>More consumers care about the result.</strong> A closed policy matters to commissions, reporting, documents and marketing alike. Publishing one event beats calling four APIs — and a fifth consumer joins next time without you touching the source.</li>
<li><strong>The producer shouldn&#39;t know its consumers.</strong> An event says &quot;this happened&quot;, not &quot;do this&quot;. The source system doesn&#39;t have to change every time someone new wants the result. That&#39;s the main thing about the event-driven approach: system lifecycles come apart.</li>
<li><strong>Load spikes shouldn&#39;t pass through.</strong> A queue between producer and consumer spreads the surge over time instead of letting it cascade into the systems downstream.</li>
<li><strong>The process tolerates asynchrony.</strong> &quot;Within a few seconds&quot; is fine for most downstream steps — the business just has to know it and sign off on it.</li>
</ul>
<p>The price this time is operational and mental: <strong>events are harder to debug.</strong> You don&#39;t see the process flow in a single trace, consistency is eventual, duplicate deliveries and dead letters (DLQ) show up. Without proper monitoring and idempotent consumers, the &quot;modern architecture&quot; turns into a system nobody understands.</p>
<p>One thing about duplicates that&#39;s better known in advance: <strong>at-least-once delivery is part of the contract.</strong> SQS and Stripe webhooks both say it plainly in their docs — every now and then the same message simply arrives twice. A consumer that answers a second delivery with a second commission payout has a design flaw; duplicates were to be expected. The fix is old and proven: an <a href="https://stripe.com/blog/idempotency" class="blog-extlink" target="_blank" rel="noopener noreferrer">idempotency key</a> — the same request with the same key runs once, no matter how many times it arrives.</p>
<h2 id="the-grey-zone-and-hybrids">The grey zone and hybrids</h2>
<p>The real world rarely fits cleanly into one box:</p>
<ul>
<li><strong>Request-reply over events</strong> — the caller posts a request to a queue and waits for a reply event. Your spikes get spread over time, but you pay with more complex correlation. Makes sense for long-running operations, not as a substitute for an ordinary call.</li>
<li><strong>Outbox pattern</strong> — when you need to write to your own database <em>and</em> reliably publish an event, write both in one transaction (the event into an outbox table) and leave publishing to a separate process. Solves the classic &quot;written but never sent&quot; problem.</li>
<li><strong>Synchronous reads, asynchronous writes</strong> — a common and healthy compromise: you ask for current state via API, changes get broadcast as events.</li>
</ul>
<h2 id="the-price-of-each-choice">The price of each choice</h2>
<p>Neither option is free — they differ only in where and when you pay:</p>
<div class="table-scroll" tabindex="0"><table>
<thead>
<tr>
<th>Aspect</th>
<th>Synchronous API</th>
<th>Events</th>
</tr>
</thead>
<tbody><tr>
<td>Answer for the caller</td>
<td>immediate</td>
<td>none / later</td>
</tr>
<tr>
<td>Coupling between systems</td>
<td>tight (availability, versions)</td>
<td>loose (event contract only)</td>
</tr>
<tr>
<td>Behaviour under load spikes</td>
<td>forwards the load</td>
<td>spreads it via the queue</td>
</tr>
<tr>
<td>Error handling</td>
<td>immediate, in call context</td>
<td>delayed, out of context</td>
</tr>
<tr>
<td>Flow readability</td>
<td>high</td>
<td>needs tooling</td>
</tr>
<tr>
<td>Operational demands</td>
<td>lower</td>
<td>higher (monitoring, DLQ, idempotency)</td>
</tr>
</tbody></table></div>
<h2 id="conclusion">Conclusion</h2>
<p>The whole framework fits into three questions: who needs the answer and when, how many consumers there are, and what should happen during an outage. The answer to &quot;API or event?&quot; usually falls out on its own. And when two parts of a process each ask for something different, that&#39;s fine — they simply need different things.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>